##// END OF EJS Templates
Minor robustness/speed improvements to process handling
Fernando Perez -
Show More
@@ -1,192 +1,194 b''
1 1 """Posix-specific implementation of process utilities.
2 2
3 3 This file is only meant to be imported by process.py, not by end-users.
4 4 """
5 5
6 6 #-----------------------------------------------------------------------------
7 7 # Copyright (C) 2010 The IPython Development Team
8 8 #
9 9 # Distributed under the terms of the BSD License. The full license is in
10 10 # the file COPYING, distributed as part of this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16 from __future__ import print_function
17 17
18 18 # Stdlib
19 19 import subprocess as sp
20 20 import sys
21 21
22 22 # Third-party
23 23 # We ship our own copy of pexpect (it's a single file) to minimize dependencies
24 24 # for users, but it's only used if we don't find the system copy.
25 25 try:
26 26 import pexpect
27 27 except ImportError:
28 28 from IPython.external import pexpect
29 29
30 30 # Our own
31 31 from .autoattr import auto_attr
32 32 from ._process_common import getoutput
33 33
34 34 #-----------------------------------------------------------------------------
35 35 # Function definitions
36 36 #-----------------------------------------------------------------------------
37 37
38 38 def _find_cmd(cmd):
39 39 """Find the full path to a command using which."""
40 40
41 41 return sp.Popen(['/usr/bin/env', 'which', cmd],
42 42 stdout=sp.PIPE).communicate()[0]
43 43
44 44
45 45 class ProcessHandler(object):
46 46 """Execute subprocesses under the control of pexpect.
47 47 """
48 48 # Timeout in seconds to wait on each reading of the subprocess' output.
49 49 # This should not be set too low to avoid cpu overusage from our side,
50 50 # since we read in a loop whose period is controlled by this timeout.
51 51 read_timeout = 0.05
52 52
53 53 # Timeout to give a process if we receive SIGINT, between sending the
54 54 # SIGINT to the process and forcefully terminating it.
55 55 terminate_timeout = 0.2
56 56
57 57 # File object where stdout and stderr of the subprocess will be written
58 58 logfile = None
59 59
60 60 # Shell to call for subprocesses to execute
61 61 sh = None
62 62
63 63 @auto_attr
64 64 def sh(self):
65 65 sh = pexpect.which('sh')
66 66 if sh is None:
67 67 raise OSError('"sh" shell not found')
68 68 return sh
69 69
70 70 def __init__(self, logfile=None, read_timeout=None, terminate_timeout=None):
71 71 """Arguments are used for pexpect calls."""
72 72 self.read_timeout = (ProcessHandler.read_timeout if read_timeout is
73 73 None else read_timeout)
74 74 self.terminate_timeout = (ProcessHandler.terminate_timeout if
75 75 terminate_timeout is None else
76 76 terminate_timeout)
77 77 self.logfile = sys.stdout if logfile is None else logfile
78 78
79 79 def getoutput(self, cmd):
80 80 """Run a command and return its stdout/stderr as a string.
81 81
82 82 Parameters
83 83 ----------
84 84 cmd : str
85 85 A command to be executed in the system shell.
86 86
87 87 Returns
88 88 -------
89 89 output : str
90 90 A string containing the combination of stdout and stderr from the
91 91 subprocess, in whatever order the subprocess originally wrote to its
92 92 file descriptors (so the order of the information in this string is the
93 93 correct order as would be seen if running the command in a terminal).
94 94 """
95 95 pcmd = self._make_cmd(cmd)
96 96 try:
97 97 return pexpect.run(pcmd).replace('\r\n', '\n')
98 98 except KeyboardInterrupt:
99 99 print('^C', file=sys.stderr, end='')
100 100
101 101 def getoutput_pexpect(self, cmd):
102 102 """Run a command and return its stdout/stderr as a string.
103 103
104 104 Parameters
105 105 ----------
106 106 cmd : str
107 107 A command to be executed in the system shell.
108 108
109 109 Returns
110 110 -------
111 111 output : str
112 112 A string containing the combination of stdout and stderr from the
113 113 subprocess, in whatever order the subprocess originally wrote to its
114 114 file descriptors (so the order of the information in this string is the
115 115 correct order as would be seen if running the command in a terminal).
116 116 """
117 117 pcmd = self._make_cmd(cmd)
118 118 try:
119 119 return pexpect.run(pcmd).replace('\r\n', '\n')
120 120 except KeyboardInterrupt:
121 121 print('^C', file=sys.stderr, end='')
122 122
123 123 def system(self, cmd):
124 124 """Execute a command in a subshell.
125 125
126 126 Parameters
127 127 ----------
128 128 cmd : str
129 129 A command to be executed in the system shell.
130 130
131 131 Returns
132 132 -------
133 133 None : we explicitly do NOT return the subprocess status code, as this
134 134 utility is meant to be used extensively in IPython, where any return
135 135 value would trigger :func:`sys.displayhook` calls.
136 136 """
137 137 pcmd = self._make_cmd(cmd)
138 138 # Patterns to match on the output, for pexpect. We read input and
139 139 # allow either a short timeout or EOF
140 140 patterns = [pexpect.TIMEOUT, pexpect.EOF]
141 141 # the index of the EOF pattern in the list.
142 142 EOF_index = 1 # Fix this index if you change the list!!
143 143 # The size of the output stored so far in the process output buffer.
144 144 # Since pexpect only appends to this buffer, each time we print we
145 145 # record how far we've printed, so that next time we only print *new*
146 146 # content from the buffer.
147 147 out_size = 0
148 148 try:
149 149 # Since we're not really searching the buffer for text patterns, we
150 150 # can set pexpect's search window to be tiny and it won't matter.
151 151 # We only search for the 'patterns' timeout or EOF, which aren't in
152 152 # the text itself.
153 child = pexpect.spawn(pcmd, searchwindowsize=1)
153 #child = pexpect.spawn(pcmd, searchwindowsize=1)
154 child = pexpect.spawn(pcmd)
154 155 flush = sys.stdout.flush
155 156 while True:
156 157 # res is the index of the pattern that caused the match, so we
157 158 # know whether we've finished (if we matched EOF) or not
158 159 res_idx = child.expect_list(patterns, self.read_timeout)
159 160 print(child.before[out_size:], end='')
160 161 flush()
161 # Update the pointer to what we've already printed
162 out_size = len(child.before)
163 162 if res_idx==EOF_index:
164 163 break
164 # Update the pointer to what we've already printed
165 out_size = len(child.before)
165 166 except KeyboardInterrupt:
166 167 # We need to send ^C to the process. The ascii code for '^C' is 3
167 168 # (the character is known as ETX for 'End of Text', see
168 169 # curses.ascii.ETX).
169 170 child.sendline(chr(3))
170 171 # Read and print any more output the program might produce on its
171 172 # way out.
172 173 try:
173 174 out_size = len(child.before)
174 175 child.expect_list(patterns, self.terminate_timeout)
175 176 print(child.before[out_size:], end='')
177 sys.stdout.flush()
176 178 except KeyboardInterrupt:
177 179 # Impatient users tend to type it multiple times
178 180 pass
179 181 finally:
180 182 # Ensure the subprocess really is terminated
181 183 child.terminate(force=True)
182 184
183 185 def _make_cmd(self, cmd):
184 186 return '%s -c "%s"' % (self.sh, cmd)
185 187
186 188
187 189 # Make system() with a functional interface for outside use. Note that we use
188 190 # getoutput() from the _common utils, which is built on top of popen(). Using
189 191 # pexpect to get subprocess output produces difficult to parse output, since
190 192 # programs think they are talking to a tty and produce highly formatted output
191 193 # (ls is a good example) that makes them hard.
192 194 system = ProcessHandler().system
General Comments 0
You need to be logged in to leave comments. Login now