Merge ~ltrager/maas:emulate_terminal_2.2 into maas:2.2

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: 028c4fadf293029093ef6fe8f6608e6e3486785a
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ltrager/maas:emulate_terminal_2.2
Merge into: maas:2.2
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
Lee Trager (community) Approve
Review via email: mp+328141@code.launchpad.net

Commit message

Emulate 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
Backport: abc094c196c4f7c7d96bb336f0f33c894eeff0bb

To post a comment you must log in.
Revision history for this message
Lee Trager (ltrager) wrote :
review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

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