Merge ~ajorgens/cloud-init:pipe-cat into cloud-init:master

Proposed by Andrew Jorgensen
Status: Work in progress
Proposed branch: ~ajorgens/cloud-init:pipe-cat
Merge into: cloud-init:master
Diff against target: 111 lines (+35/-4)
3 files modified
cloudinit/distros/rhel.py (+1/-1)
cloudinit/util.py (+29/-3)
tests/unittests/test_util.py (+5/-0)
Reviewer Review Type Date Requested Status
Scott Moser Needs Fixing
Server Team CI bot continuous-integration Approve
Review via email: mp+325512@code.launchpad.net

Commit message

util: Disconnect subp from a tty

Some processes are less chatty when they don't find a terminal. Yum is a prime example. Without this it can display progress bars and fill up your console logs with garbage, making debugging harder.

Description of the change

util: Disconnect subp from a tty

Some processes are less chatty when they don't find a terminal. Yum is a prime example. Without this it can display progress bars and fill up your console logs with garbage, making debugging harder.

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

It seems like there is probably a better (or at very least less expensive) way to do this than to incur the cost of a subprocess.

I suspect
a.) change 'yum' to be invoked with '--quiet' or the like
b.) close and re-open stdout or something like that. yum is probably using 'isatty' to change its behavior.

Revision history for this message
Andrew Jorgensen (ajorgens) wrote :

a.) Unfortunately, asking Yum to be quiet means it won't give you any of the debugging information you need when it goes wrong.

b.) Possibly there's a better way to disconnect it from the terminal, but what isatty does is ask the system if stdout is associated with a terminal, so unless you make stdout something other than the one associated with the terminal it will answer that it's a tty. In other words, it has to be piped through something. Piping it through cat seemed the lesser of two evils compared to spinning off a thread (or greenlet or whatever) to consume stdout and spool it to the console.

I'm definitely open to advice on how to do this a better way, if you want to ask around.

Revision history for this message
Andrew Jorgensen (ajorgens) wrote :

Hmm. I think I've found a better way, pending more testing.

~ajorgens/cloud-init:pipe-cat updated
10676d5... by Andrew Jorgensen

read/write stdout instead of piping through cat

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Andrew Jorgensen (ajorgens) wrote :

New revision avoiding spawning cat, but emulating subprocess.communicate() instead.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
~ajorgens/cloud-init:pipe-cat updated
4d09af6... by Andrew Jorgensen

Use a no_tty option instead of always disconnecting from tty

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

Andrew,
thanks. I think the code looks good
But a few things

a.) maybe 'detach_tty' as the name? 'no_tty=False' seems to imply that there would be a tty, when we are not actually creating one.
b.) need some sort of test, at least to push it through here. it might be tricky to do, but i'd like to run a subp 'detach_tty=False' and get some subprocess to believe it is a tty.
   subp(['bash', '-c', '[ "-t" 1 ] && echo attached to tty || echo not attached to tty'])
   the difficulty is that in any c-i or automated environment, we're probably not attached to a tty. but we do need some test of this code path.
c.) i think best to take the detach_tty path only if we *have* a tty.
   if os.isatty(sys.stdout):
      ... go the detach route
   else:
      ... no need to detach

Revision history for this message
Scott Moser (smoser) wrote :

we need *some* test of this though, even if we have to mock os.isatty to get reliable tests.

review: Needs Fixing
Revision history for this message
Andrew Jorgensen (ajorgens) wrote :

Agreed we need a unit test of this code path. I'll see what I can do.

Revision history for this message
Scott Moser (smoser) wrote :

I'll move this to 'work in progress' for now set back to Needs Review when you're ready.
thanks.

Revision history for this message
Andrew Jorgensen (ajorgens) wrote :

So... I spent a lot of time yesterday trying to use pty.openpty() to make a unit test for this, and I give up, at least for now. I'm unsure if the code is bad or if the unit test is impossible.

In Amazon Linux we're using the redirect_output thing to pipe everything through tee anyway, so this is likely a non-issue for us. If I feel ambitious or have a great idea how else to solve this I might do that some day, but for now I'm dropping it.

~ajorgens/cloud-init:pipe-cat updated
7941e58... by Andrew Jorgensen

work-in-progress - broken

Unmerged commits

7941e58... by Andrew Jorgensen

work-in-progress - broken

4d09af6... by Andrew Jorgensen

Use a no_tty option instead of always disconnecting from tty

10676d5... by Andrew Jorgensen

read/write stdout instead of piping through cat

c97434e... by Andrew Jorgensen

rhel: Use the pipe_cat option for Yum commands

5675c65... by Andrew Jorgensen

util: Add an option for subp to pipe the process through cat

Some processes are less chatty when piped through cat, because they won't
detect a terminal (yum being a prime example).

Reviewed-by: Matt Nierzwicki <email address hidden>
Reviewed-by: Ethan Faust <email address hidden>
[<email address hidden>: rebase onto 0.7.9]

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py
2index 1fecb61..03f02de 100644
3--- a/cloudinit/distros/rhel.py
4+++ b/cloudinit/distros/rhel.py
5@@ -212,7 +212,7 @@ class Distro(distros.Distro):
6 cmd.extend(pkglist)
7
8 # Allow the output of this to flow outwards (ie not be captured)
9- util.subp(cmd, capture=False)
10+ util.subp(cmd, capture=False, detach_tty=True)
11
12 def update_package_sources(self):
13 self._runner.run("update-sources", self.package_command,
14diff --git a/cloudinit/util.py b/cloudinit/util.py
15index c93b6d7..9f5715d 100644
16--- a/cloudinit/util.py
17+++ b/cloudinit/util.py
18@@ -1,10 +1,12 @@
19 # Copyright (C) 2012 Canonical Ltd.
20 # Copyright (C) 2012, 2013 Hewlett-Packard Development Company, L.P.
21 # Copyright (C) 2012 Yahoo! Inc.
22+# Copyright (C) 2017 Amazon.com, Inc. or its affiliates.
23 #
24 # Author: Scott Moser <scott.moser@canonical.com>
25 # Author: Juerg Haefliger <juerg.haefliger@hp.com>
26 # Author: Joshua Harlow <harlowja@yahoo-inc.com>
27+# Author: Andrew Jorgensen <ajorgens@amazon.com>
28 #
29 # This file is part of cloud-init. See LICENSE file for license information.
30
31@@ -1771,13 +1773,18 @@ def delete_dir_contents(dirname):
32
33
34 def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,
35- logstring=False, decode="replace", target=None, update_env=None):
36+ logstring=False, decode="replace", target=None, update_env=None,
37+ detach_tty=False):
38
39 # not supported in cloud-init (yet), for now kept in the call signature
40 # to ease maintaining code shared between cloud-init and curtin
41 if target is not None:
42 raise ValueError("target arg not supported by cloud-init")
43
44+ if detach_tty and not sys.stdout.isatty():
45+ LOG.debug("No TTY detected, ignoring detach_tty")
46+ detach_tty = False
47+
48 if rcs is None:
49 rcs = [0]
50
51@@ -1803,8 +1810,9 @@ def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,
52 stdin = None
53 stdout = None
54 stderr = None
55- if capture:
56+ if detach_tty or capture:
57 stdout = subprocess.PIPE
58+ if capture:
59 stderr = subprocess.PIPE
60 if data is None:
61 # using devnull assures any reads get null, rather
62@@ -1819,7 +1827,25 @@ def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,
63 sp = subprocess.Popen(args, stdout=stdout,
64 stderr=stderr, stdin=stdin,
65 env=env, shell=shell)
66- (out, err) = sp.communicate(data)
67+
68+ # Some processes are less chatty when disconnected from a tty.
69+ # (capture=True already doesn't have a tty)
70+ if detach_tty and not capture:
71+ (out, err) = (None, None)
72+ if data is not None:
73+ try:
74+ sp.stdin.write(data)
75+ # Borrowed from cpython 2.7 subprocess.py
76+ except IOError as e:
77+ if e.errno != errno.EPIPE and e.errno != errno.EINVAL:
78+ raise
79+ sp.stdin.close()
80+ while sp.poll() is None:
81+ sys.stdout.write(sp.stdout.readline())
82+ sys.stdout.flush()
83+ sp.stdout.close()
84+ else:
85+ (out, err) = sp.communicate(data)
86
87 # Just ensure blank instead of none.
88 if not out and capture:
89diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
90index a73fd26..37354b4 100644
91--- a/tests/unittests/test_util.py
92+++ b/tests/unittests/test_util.py
93@@ -4,6 +4,7 @@ from __future__ import print_function
94
95 import logging
96 import os
97+import pty
98 import shutil
99 import stat
100 import tempfile
101@@ -627,6 +628,10 @@ class TestSubp(helpers.TestCase):
102 self.assertEqual(
103 ['FOO=BAR', 'HOME=/myhome', 'K1=V1', 'K2=V2'], out.splitlines())
104
105+ def test_subp_detach_tty():
106+ with pty.openpty() as (master, slave):
107+ pass
108+
109 def test_returns_none_if_no_capture(self):
110 (out, err) = util.subp(self.stdin2out, data=b'', capture=False)
111 self.assertIsNone(err)

Subscribers

People subscribed via source and target branches