Merge ~ltrager/maas:emulate_terminal into maas:master

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: ff037e0fef34d76471fbc471b1d44e95c7d39b6f
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ltrager/maas:emulate_terminal
Merge into: maas:master
Diff against target: 96 lines (+59/-4)
2 files modified
src/provisioningserver/refresh/maas_api_helper.py (+41/-4)
src/provisioningserver/refresh/tests/test_maas_api_helper.py (+18/-0)
Reviewer Review Type Date Requested Status
Newell Jensen (community) Approve
Review via email: mp+327927@code.launchpad.net

Commit message

Enumate a terminal when logging script output

Some applications don't properly detect that they are not being run in a
terminal and refresh output for progress bars, counters, and spinners. These
characters quickly add up making the log difficult to read. When writing output
from an application emulate a terminal so readable data is captured.

Fixes: LP: #1705792

To post a comment you must log in.
Revision history for this message
Newell Jensen (newell-jensen) wrote :

How does this fix progress bars or spinners? Some progress bars are dots with a percentage as show in the bug, but some might use # signs etc. Additionally, spinners, when spinning will show hyphens '-' while the spinner is spinning between backslash-hyphen-forwardslash and so forth. Is this branch to fix all those possibilities or are we just doing it for the carriage return and backslash like you have in your code?

review: Needs Information
Revision history for this message
Lee Trager (ltrager) wrote :

The carriage return and back slash are how you change the output on the screen. [1] is a spinner. It writes the character, sleeps, then moves the position back one space and writes over it. \b would be used to update a counter or any other character as well. MAAS now looks for the backspace and carriage return characters and seeks back as a terminal does. So previously if you ran [1] as a test script in MAAS you would get a file containing 'Processing... ' and '|/-\\' 10 times repeated, now you just get 'Processing... '

http://paste.ubuntu.com/25143239/

Revision history for this message
Lee Trager (ltrager) :
Revision history for this message
Newell Jensen (newell-jensen) wrote :

Thanks for the clarification!

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Revision history for this message
MAAS Lander (maas-lander) wrote :
Revision history for this message
Mike Pontillo (mpontillo) wrote :

This is interesting.

I should hope that MAAS (and related utilities that generate logging in MAAS, such as curtin, or any test scripts) would run most commands with proper arguments for non-interactive usage, so that we can avoid this issue.

But this seems like a nice workaround for cases where that isn't true, and seems to be simple enough that it won't break our usual non-interactive logs.

One nit: I might have liked to see more comprehensive testing of edge cases, such as seeking to the beginning of a line in a case where you aren't seeking to the start of the file.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/src/provisioningserver/refresh/maas_api_helper.py b/src/provisioningserver/refresh/maas_api_helper.py
index ca2271a..d4dd138 100644
--- a/src/provisioningserver/refresh/maas_api_helper.py
+++ b/src/provisioningserver/refresh/maas_api_helper.py
@@ -17,6 +17,7 @@ from datetime import (
17from email.utils import parsedate17from email.utils import parsedate
18import json18import json
19import mimetypes19import mimetypes
20import os
20import random21import random
21import selectors22import selectors
22import socket23import socket
@@ -296,8 +297,12 @@ def capture_script_output(
296 # Pad the timeout by 60 seconds to allow for cleanup.297 # Pad the timeout by 60 seconds to allow for cleanup.
297 timeout = datetime.now() + timedelta(seconds=(timeout_seconds + 60))298 timeout = datetime.now() + timedelta(seconds=(timeout_seconds + 60))
298299
299 with open(stdout_path, 'wb') as out, open(stderr_path, 'wb') as err:300 # Create the file and then open it in read write mode for terminal
300 with open(combined_path, 'wb') as combined:301 # emulation.
302 for path in (stdout_path, stderr_path, combined_path):
303 open(path, 'w').close()
304 with open(stdout_path, 'r+b') as out, open(stderr_path, 'r+b') as err:
305 with open(combined_path, 'r+b') as combined:
301 with selectors.DefaultSelector() as selector:306 with selectors.DefaultSelector() as selector:
302 selector.register(proc.stdout, selectors.EVENT_READ, out)307 selector.register(proc.stdout, selectors.EVENT_READ, out)
303 selector.register(proc.stderr, selectors.EVENT_READ, err)308 selector.register(proc.stderr, selectors.EVENT_READ, err)
@@ -342,5 +347,37 @@ def _select_script_output(selector, combined, timeout):
342 if len(chunk) == 0: # EOF347 if len(chunk) == 0: # EOF
343 selector.unregister(key.fileobj)348 selector.unregister(key.fileobj)
344 else:349 else:
345 key.data.write(chunk)350 # The list comprehension is needed to get byte objects instead
346 combined.write(chunk)351 # of their numeric value.
352 for i in [chunk[i:i + 1] for i in range(len(chunk))]:
353 for f in [key.data, combined]:
354 # Some applications don't properly detect that they are
355 # not being run in a terminal and refresh output for
356 # progress bars, counters, and spinners. These
357 # characters quickly add up making the log difficult to
358 # read. When writing output from an application emulate
359 # a terminal so readable data is captured.
360 if i == b'\b':
361 # Backspace - Go back one space, if we can.
362 if f.tell() != 0:
363 f.seek(-1, os.SEEK_CUR)
364 elif i == b'\r':
365 # Carriage return - Seek to the beginning of the
366 # line, as indicated by a line feed, or file.
367 while f.tell() != 0:
368 f.seek(-1, os.SEEK_CUR)
369 if f.read(1) == b'\n':
370 # Check if line feed was found.
371 break
372 else:
373 # The read advances the file position by
374 # one so seek back again.
375 f.seek(-1, os.SEEK_CUR)
376 elif i == b'\n':
377 # Line feed - Some applications do a carriage
378 # return and then a line feed. The data on the line
379 # should be saved, not overwritten.
380 f.seek(0, os.SEEK_END)
381 f.write(i)
382 else:
383 f.write(i)
diff --git a/src/provisioningserver/refresh/tests/test_maas_api_helper.py b/src/provisioningserver/refresh/tests/test_maas_api_helper.py
index be50262..3bf9b83 100644
--- a/src/provisioningserver/refresh/tests/test_maas_api_helper.py
+++ b/src/provisioningserver/refresh/tests/test_maas_api_helper.py
@@ -596,6 +596,24 @@ class TestCaptureScriptOutput(MAASTestCase):
596 # This is a complete XML document; we've captured all output.596 # This is a complete XML document; we've captured all output.
597 self.assertThat(etree.fromstring(stdout).tag, Equals("list"))597 self.assertThat(etree.fromstring(stdout).tag, Equals("list"))
598598
599 def test__interprets_backslash(self):
600 proc = Popen(
601 'bash -c "echo -en \bmas\bas"',
602 stdout=PIPE, stderr=PIPE, shell=True)
603 self.assertThat(
604 self.capture(proc), MatchesListwise((
605 Equals(0), Equals("maas"), Equals(""), Equals("maas")
606 )))
607
608 def test__interprets_carriage_return(self):
609 proc = Popen(
610 'bash -c "echo -en foo\rmaas"',
611 stdout=PIPE, stderr=PIPE, shell=True)
612 self.assertThat(
613 self.capture(proc), MatchesListwise((
614 Equals(0), Equals("maas"), Equals(""), Equals("maas")
615 )))
616
599 def test__timeout(self):617 def test__timeout(self):
600 # The timeout is padded by 60 seconds, override it so the test can618 # The timeout is padded by 60 seconds, override it so the test can
601 # run quickly.619 # run quickly.

Subscribers

People subscribed via source and target branches