_process_posix.py
190 lines
| 7.4 KiB
| text/x-python
|
PythonLexer
Fernando Perez
|
r2908 | """Posix-specific implementation of process utilities. | ||
This file is only meant to be imported by process.py, not by end-users. | ||||
""" | ||||
#----------------------------------------------------------------------------- | ||||
Matthias BUSSONNIER
|
r5390 | # Copyright (C) 2010-2011 The IPython Development Team | ||
Fernando Perez
|
r2908 | # | ||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
#----------------------------------------------------------------------------- | ||||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
from __future__ import print_function | ||||
# Stdlib | ||||
import subprocess as sp | ||||
import sys | ||||
Thomas Kluyver
|
r4836 | from IPython.external import pexpect | ||
Fernando Perez
|
r2908 | |||
# Our own | ||||
from .autoattr import auto_attr | ||||
Fernando Perez
|
r3002 | from ._process_common import getoutput | ||
Thomas Kluyver
|
r4836 | from IPython.utils import text | ||
Thomas Kluyver
|
r4896 | from IPython.utils import py3compat | ||
Fernando Perez
|
r2908 | |||
#----------------------------------------------------------------------------- | ||||
# Function definitions | ||||
#----------------------------------------------------------------------------- | ||||
def _find_cmd(cmd): | ||||
"""Find the full path to a command using which.""" | ||||
Thomas Kluyver
|
r4896 | path = sp.Popen(['/usr/bin/env', 'which', cmd], | ||
Fernando Perez
|
r2908 | stdout=sp.PIPE).communicate()[0] | ||
Thomas Kluyver
|
r4896 | return py3compat.bytes_to_str(path) | ||
Fernando Perez
|
r2908 | |||
class ProcessHandler(object): | ||||
"""Execute subprocesses under the control of pexpect. | ||||
""" | ||||
# Timeout in seconds to wait on each reading of the subprocess' output. | ||||
# This should not be set too low to avoid cpu overusage from our side, | ||||
# since we read in a loop whose period is controlled by this timeout. | ||||
read_timeout = 0.05 | ||||
# Timeout to give a process if we receive SIGINT, between sending the | ||||
# SIGINT to the process and forcefully terminating it. | ||||
terminate_timeout = 0.2 | ||||
# File object where stdout and stderr of the subprocess will be written | ||||
logfile = None | ||||
# Shell to call for subprocesses to execute | ||||
sh = None | ||||
@auto_attr | ||||
def sh(self): | ||||
sh = pexpect.which('sh') | ||||
if sh is None: | ||||
raise OSError('"sh" shell not found') | ||||
return sh | ||||
def __init__(self, logfile=None, read_timeout=None, terminate_timeout=None): | ||||
"""Arguments are used for pexpect calls.""" | ||||
self.read_timeout = (ProcessHandler.read_timeout if read_timeout is | ||||
None else read_timeout) | ||||
self.terminate_timeout = (ProcessHandler.terminate_timeout if | ||||
terminate_timeout is None else | ||||
terminate_timeout) | ||||
self.logfile = sys.stdout if logfile is None else logfile | ||||
def getoutput(self, cmd): | ||||
"""Run a command and return its stdout/stderr as a string. | ||||
Parameters | ||||
---------- | ||||
cmd : str | ||||
A command to be executed in the system shell. | ||||
Returns | ||||
------- | ||||
output : str | ||||
A string containing the combination of stdout and stderr from the | ||||
subprocess, in whatever order the subprocess originally wrote to its | ||||
file descriptors (so the order of the information in this string is the | ||||
correct order as would be seen if running the command in a terminal). | ||||
""" | ||||
try: | ||||
MinRK
|
r5296 | return pexpect.run(self.sh, args=['-c', cmd]).replace('\r\n', '\n') | ||
Fernando Perez
|
r2908 | except KeyboardInterrupt: | ||
print('^C', file=sys.stderr, end='') | ||||
Fernando Perez
|
r3002 | def getoutput_pexpect(self, cmd): | ||
"""Run a command and return its stdout/stderr as a string. | ||||
Parameters | ||||
---------- | ||||
cmd : str | ||||
A command to be executed in the system shell. | ||||
Returns | ||||
------- | ||||
output : str | ||||
A string containing the combination of stdout and stderr from the | ||||
subprocess, in whatever order the subprocess originally wrote to its | ||||
file descriptors (so the order of the information in this string is the | ||||
correct order as would be seen if running the command in a terminal). | ||||
""" | ||||
try: | ||||
MinRK
|
r5296 | return pexpect.run(self.sh, args=['-c', cmd]).replace('\r\n', '\n') | ||
Fernando Perez
|
r3002 | except KeyboardInterrupt: | ||
print('^C', file=sys.stderr, end='') | ||||
Fernando Perez
|
r2908 | def system(self, cmd): | ||
"""Execute a command in a subshell. | ||||
Parameters | ||||
---------- | ||||
cmd : str | ||||
A command to be executed in the system shell. | ||||
Returns | ||||
------- | ||||
MinRK
|
r3910 | int : child's exitstatus | ||
Fernando Perez
|
r2908 | """ | ||
Thomas Kluyver
|
r4836 | # Get likely encoding for the output. | ||
enc = text.getdefaultencoding() | ||||
Fernando Perez
|
r2908 | # Patterns to match on the output, for pexpect. We read input and | ||
# allow either a short timeout or EOF | ||||
patterns = [pexpect.TIMEOUT, pexpect.EOF] | ||||
# the index of the EOF pattern in the list. | ||||
MinRK
|
r5296 | # even though we know it's 1, this call means we don't have to worry if | ||
# we change the above list, and forget to change this value: | ||||
EOF_index = patterns.index(pexpect.EOF) | ||||
Fernando Perez
|
r2908 | # The size of the output stored so far in the process output buffer. | ||
# Since pexpect only appends to this buffer, each time we print we | ||||
# record how far we've printed, so that next time we only print *new* | ||||
# content from the buffer. | ||||
out_size = 0 | ||||
try: | ||||
# Since we're not really searching the buffer for text patterns, we | ||||
# can set pexpect's search window to be tiny and it won't matter. | ||||
# We only search for the 'patterns' timeout or EOF, which aren't in | ||||
# the text itself. | ||||
MinRK
|
r5296 | child = pexpect.spawn(self.sh, args=['-c', cmd]) | ||
Fernando Perez
|
r2908 | flush = sys.stdout.flush | ||
while True: | ||||
# res is the index of the pattern that caused the match, so we | ||||
# know whether we've finished (if we matched EOF) or not | ||||
res_idx = child.expect_list(patterns, self.read_timeout) | ||||
Thomas Kluyver
|
r4836 | print(child.before[out_size:].decode(enc, 'replace'), end='') | ||
Fernando Perez
|
r2908 | flush() | ||
if res_idx==EOF_index: | ||||
break | ||||
Fernando Perez
|
r3075 | # Update the pointer to what we've already printed | ||
out_size = len(child.before) | ||||
Fernando Perez
|
r2908 | except KeyboardInterrupt: | ||
# We need to send ^C to the process. The ascii code for '^C' is 3 | ||||
# (the character is known as ETX for 'End of Text', see | ||||
# curses.ascii.ETX). | ||||
child.sendline(chr(3)) | ||||
# Read and print any more output the program might produce on its | ||||
# way out. | ||||
try: | ||||
out_size = len(child.before) | ||||
child.expect_list(patterns, self.terminate_timeout) | ||||
Thomas Kluyver
|
r4836 | print(child.before[out_size:].decode(enc, 'replace'), end='') | ||
Fernando Perez
|
r3075 | sys.stdout.flush() | ||
Fernando Perez
|
r2908 | except KeyboardInterrupt: | ||
# Impatient users tend to type it multiple times | ||||
pass | ||||
finally: | ||||
# Ensure the subprocess really is terminated | ||||
child.terminate(force=True) | ||||
MinRK
|
r5296 | # add isalive check, to ensure exitstatus is set: | ||
child.isalive() | ||||
MinRK
|
r3910 | return child.exitstatus | ||
Fernando Perez
|
r2908 | |||
Fernando Perez
|
r3002 | # Make system() with a functional interface for outside use. Note that we use | ||
# getoutput() from the _common utils, which is built on top of popen(). Using | ||||
# pexpect to get subprocess output produces difficult to parse output, since | ||||
# programs think they are talking to a tty and produce highly formatted output | ||||
# (ls is a good example) that makes them hard. | ||||
system = ProcessHandler().system | ||||