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