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