##// END OF EJS Templates
Backport PR #12360: use $SHELL in system_piped
Matthias Bussonnier -
Show More
@@ -1,224 +1,225 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-2011 The IPython Development Team
7 # Copyright (C) 2010-2011 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
16
17 # Stdlib
17 # Stdlib
18 import errno
18 import errno
19 import os
19 import os
20 import subprocess as sp
20 import subprocess as sp
21 import sys
21 import sys
22
22
23 import pexpect
23 import pexpect
24
24
25 # Our own
25 # Our own
26 from ._process_common import getoutput, arg_split
26 from ._process_common import getoutput, arg_split
27 from IPython.utils import py3compat
27 from IPython.utils import py3compat
28 from IPython.utils.encoding import DEFAULT_ENCODING
28 from IPython.utils.encoding import DEFAULT_ENCODING
29
29
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31 # Function definitions
31 # Function definitions
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33
33
34 def _find_cmd(cmd):
34 def _find_cmd(cmd):
35 """Find the full path to a command using which."""
35 """Find the full path to a command using which."""
36
36
37 path = sp.Popen(['/usr/bin/env', 'which', cmd],
37 path = sp.Popen(['/usr/bin/env', 'which', cmd],
38 stdout=sp.PIPE, stderr=sp.PIPE).communicate()[0]
38 stdout=sp.PIPE, stderr=sp.PIPE).communicate()[0]
39 return py3compat.decode(path)
39 return py3compat.decode(path)
40
40
41
41
42 class ProcessHandler(object):
42 class ProcessHandler(object):
43 """Execute subprocesses under the control of pexpect.
43 """Execute subprocesses under the control of pexpect.
44 """
44 """
45 # Timeout in seconds to wait on each reading of the subprocess' output.
45 # Timeout in seconds to wait on each reading of the subprocess' output.
46 # This should not be set too low to avoid cpu overusage from our side,
46 # This should not be set too low to avoid cpu overusage from our side,
47 # since we read in a loop whose period is controlled by this timeout.
47 # since we read in a loop whose period is controlled by this timeout.
48 read_timeout = 0.05
48 read_timeout = 0.05
49
49
50 # Timeout to give a process if we receive SIGINT, between sending the
50 # Timeout to give a process if we receive SIGINT, between sending the
51 # SIGINT to the process and forcefully terminating it.
51 # SIGINT to the process and forcefully terminating it.
52 terminate_timeout = 0.2
52 terminate_timeout = 0.2
53
53
54 # File object where stdout and stderr of the subprocess will be written
54 # File object where stdout and stderr of the subprocess will be written
55 logfile = None
55 logfile = None
56
56
57 # Shell to call for subprocesses to execute
57 # Shell to call for subprocesses to execute
58 _sh = None
58 _sh = None
59
59
60 @property
60 @property
61 def sh(self):
61 def sh(self):
62 if self._sh is None:
62 if self._sh is None:
63 self._sh = pexpect.which('sh')
63 shell_name = os.environ.get("SHELL", "sh")
64 self._sh = pexpect.which(shell_name)
64 if self._sh is None:
65 if self._sh is None:
65 raise OSError('"sh" shell not found')
66 raise OSError('"{}" shell not found'.format(shell_name))
66
67
67 return self._sh
68 return self._sh
68
69
69 def __init__(self, logfile=None, read_timeout=None, terminate_timeout=None):
70 def __init__(self, logfile=None, read_timeout=None, terminate_timeout=None):
70 """Arguments are used for pexpect calls."""
71 """Arguments are used for pexpect calls."""
71 self.read_timeout = (ProcessHandler.read_timeout if read_timeout is
72 self.read_timeout = (ProcessHandler.read_timeout if read_timeout is
72 None else read_timeout)
73 None else read_timeout)
73 self.terminate_timeout = (ProcessHandler.terminate_timeout if
74 self.terminate_timeout = (ProcessHandler.terminate_timeout if
74 terminate_timeout is None else
75 terminate_timeout is None else
75 terminate_timeout)
76 terminate_timeout)
76 self.logfile = sys.stdout if logfile is None else logfile
77 self.logfile = sys.stdout if logfile is None else logfile
77
78
78 def getoutput(self, cmd):
79 def getoutput(self, cmd):
79 """Run a command and return its stdout/stderr as a string.
80 """Run a command and return its stdout/stderr as a string.
80
81
81 Parameters
82 Parameters
82 ----------
83 ----------
83 cmd : str
84 cmd : str
84 A command to be executed in the system shell.
85 A command to be executed in the system shell.
85
86
86 Returns
87 Returns
87 -------
88 -------
88 output : str
89 output : str
89 A string containing the combination of stdout and stderr from the
90 A string containing the combination of stdout and stderr from the
90 subprocess, in whatever order the subprocess originally wrote to its
91 subprocess, in whatever order the subprocess originally wrote to its
91 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
92 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).
93 """
94 """
94 try:
95 try:
95 return pexpect.run(self.sh, args=['-c', cmd]).replace('\r\n', '\n')
96 return pexpect.run(self.sh, args=['-c', cmd]).replace('\r\n', '\n')
96 except KeyboardInterrupt:
97 except KeyboardInterrupt:
97 print('^C', file=sys.stderr, end='')
98 print('^C', file=sys.stderr, end='')
98
99
99 def getoutput_pexpect(self, cmd):
100 def getoutput_pexpect(self, cmd):
100 """Run a command and return its stdout/stderr as a string.
101 """Run a command and return its stdout/stderr as a string.
101
102
102 Parameters
103 Parameters
103 ----------
104 ----------
104 cmd : str
105 cmd : str
105 A command to be executed in the system shell.
106 A command to be executed in the system shell.
106
107
107 Returns
108 Returns
108 -------
109 -------
109 output : str
110 output : str
110 A string containing the combination of stdout and stderr from the
111 A string containing the combination of stdout and stderr from the
111 subprocess, in whatever order the subprocess originally wrote to its
112 subprocess, in whatever order the subprocess originally wrote to its
112 file descriptors (so the order of the information in this string is the
113 file descriptors (so the order of the information in this string is the
113 correct order as would be seen if running the command in a terminal).
114 correct order as would be seen if running the command in a terminal).
114 """
115 """
115 try:
116 try:
116 return pexpect.run(self.sh, args=['-c', cmd]).replace('\r\n', '\n')
117 return pexpect.run(self.sh, args=['-c', cmd]).replace('\r\n', '\n')
117 except KeyboardInterrupt:
118 except KeyboardInterrupt:
118 print('^C', file=sys.stderr, end='')
119 print('^C', file=sys.stderr, end='')
119
120
120 def system(self, cmd):
121 def system(self, cmd):
121 """Execute a command in a subshell.
122 """Execute a command in a subshell.
122
123
123 Parameters
124 Parameters
124 ----------
125 ----------
125 cmd : str
126 cmd : str
126 A command to be executed in the system shell.
127 A command to be executed in the system shell.
127
128
128 Returns
129 Returns
129 -------
130 -------
130 int : child's exitstatus
131 int : child's exitstatus
131 """
132 """
132 # Get likely encoding for the output.
133 # Get likely encoding for the output.
133 enc = DEFAULT_ENCODING
134 enc = DEFAULT_ENCODING
134
135
135 # Patterns to match on the output, for pexpect. We read input and
136 # Patterns to match on the output, for pexpect. We read input and
136 # allow either a short timeout or EOF
137 # allow either a short timeout or EOF
137 patterns = [pexpect.TIMEOUT, pexpect.EOF]
138 patterns = [pexpect.TIMEOUT, pexpect.EOF]
138 # the index of the EOF pattern in the list.
139 # the index of the EOF pattern in the list.
139 # even though we know it's 1, this call means we don't have to worry if
140 # even though we know it's 1, this call means we don't have to worry if
140 # we change the above list, and forget to change this value:
141 # we change the above list, and forget to change this value:
141 EOF_index = patterns.index(pexpect.EOF)
142 EOF_index = patterns.index(pexpect.EOF)
142 # 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.
143 # 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
144 # 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*
145 # content from the buffer.
146 # content from the buffer.
146 out_size = 0
147 out_size = 0
147 try:
148 try:
148 # 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
149 # 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.
150 # 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
151 # the text itself.
152 # the text itself.
152 #child = pexpect.spawn(pcmd, searchwindowsize=1)
153 #child = pexpect.spawn(pcmd, searchwindowsize=1)
153 if hasattr(pexpect, 'spawnb'):
154 if hasattr(pexpect, 'spawnb'):
154 child = pexpect.spawnb(self.sh, args=['-c', cmd]) # Pexpect-U
155 child = pexpect.spawnb(self.sh, args=['-c', cmd]) # Pexpect-U
155 else:
156 else:
156 child = pexpect.spawn(self.sh, args=['-c', cmd]) # Vanilla Pexpect
157 child = pexpect.spawn(self.sh, args=['-c', cmd]) # Vanilla Pexpect
157 flush = sys.stdout.flush
158 flush = sys.stdout.flush
158 while True:
159 while True:
159 # res is the index of the pattern that caused the match, so we
160 # res is the index of the pattern that caused the match, so we
160 # know whether we've finished (if we matched EOF) or not
161 # know whether we've finished (if we matched EOF) or not
161 res_idx = child.expect_list(patterns, self.read_timeout)
162 res_idx = child.expect_list(patterns, self.read_timeout)
162 print(child.before[out_size:].decode(enc, 'replace'), end='')
163 print(child.before[out_size:].decode(enc, 'replace'), end='')
163 flush()
164 flush()
164 if res_idx==EOF_index:
165 if res_idx==EOF_index:
165 break
166 break
166 # Update the pointer to what we've already printed
167 # Update the pointer to what we've already printed
167 out_size = len(child.before)
168 out_size = len(child.before)
168 except KeyboardInterrupt:
169 except KeyboardInterrupt:
169 # We need to send ^C to the process. The ascii code for '^C' is 3
170 # We need to send ^C to the process. The ascii code for '^C' is 3
170 # (the character is known as ETX for 'End of Text', see
171 # (the character is known as ETX for 'End of Text', see
171 # curses.ascii.ETX).
172 # curses.ascii.ETX).
172 child.sendline(chr(3))
173 child.sendline(chr(3))
173 # Read and print any more output the program might produce on its
174 # Read and print any more output the program might produce on its
174 # way out.
175 # way out.
175 try:
176 try:
176 out_size = len(child.before)
177 out_size = len(child.before)
177 child.expect_list(patterns, self.terminate_timeout)
178 child.expect_list(patterns, self.terminate_timeout)
178 print(child.before[out_size:].decode(enc, 'replace'), end='')
179 print(child.before[out_size:].decode(enc, 'replace'), end='')
179 sys.stdout.flush()
180 sys.stdout.flush()
180 except KeyboardInterrupt:
181 except KeyboardInterrupt:
181 # Impatient users tend to type it multiple times
182 # Impatient users tend to type it multiple times
182 pass
183 pass
183 finally:
184 finally:
184 # Ensure the subprocess really is terminated
185 # Ensure the subprocess really is terminated
185 child.terminate(force=True)
186 child.terminate(force=True)
186 # add isalive check, to ensure exitstatus is set:
187 # add isalive check, to ensure exitstatus is set:
187 child.isalive()
188 child.isalive()
188
189
189 # We follow the subprocess pattern, returning either the exit status
190 # We follow the subprocess pattern, returning either the exit status
190 # as a positive number, or the terminating signal as a negative
191 # as a positive number, or the terminating signal as a negative
191 # number.
192 # number.
192 # on Linux, sh returns 128+n for signals terminating child processes on Linux
193 # on Linux, sh returns 128+n for signals terminating child processes on Linux
193 # on BSD (OS X), the signal code is set instead
194 # on BSD (OS X), the signal code is set instead
194 if child.exitstatus is None:
195 if child.exitstatus is None:
195 # on WIFSIGNALED, pexpect sets signalstatus, leaving exitstatus=None
196 # on WIFSIGNALED, pexpect sets signalstatus, leaving exitstatus=None
196 if child.signalstatus is None:
197 if child.signalstatus is None:
197 # this condition may never occur,
198 # this condition may never occur,
198 # but let's be certain we always return an integer.
199 # but let's be certain we always return an integer.
199 return 0
200 return 0
200 return -child.signalstatus
201 return -child.signalstatus
201 if child.exitstatus > 128:
202 if child.exitstatus > 128:
202 return -(child.exitstatus - 128)
203 return -(child.exitstatus - 128)
203 return child.exitstatus
204 return child.exitstatus
204
205
205
206
206 # Make system() with a functional interface for outside use. Note that we use
207 # Make system() with a functional interface for outside use. Note that we use
207 # getoutput() from the _common utils, which is built on top of popen(). Using
208 # getoutput() from the _common utils, which is built on top of popen(). Using
208 # pexpect to get subprocess output produces difficult to parse output, since
209 # pexpect to get subprocess output produces difficult to parse output, since
209 # programs think they are talking to a tty and produce highly formatted output
210 # programs think they are talking to a tty and produce highly formatted output
210 # (ls is a good example) that makes them hard.
211 # (ls is a good example) that makes them hard.
211 system = ProcessHandler().system
212 system = ProcessHandler().system
212
213
213 def check_pid(pid):
214 def check_pid(pid):
214 try:
215 try:
215 os.kill(pid, 0)
216 os.kill(pid, 0)
216 except OSError as err:
217 except OSError as err:
217 if err.errno == errno.ESRCH:
218 if err.errno == errno.ESRCH:
218 return False
219 return False
219 elif err.errno == errno.EPERM:
220 elif err.errno == errno.EPERM:
220 # Don't have permission to signal the process - probably means it exists
221 # Don't have permission to signal the process - probably means it exists
221 return True
222 return True
222 raise
223 raise
223 else:
224 else:
224 return True
225 return True
General Comments 0
You need to be logged in to leave comments. Login now