123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- import collections
- import subprocess
- import warnings
- from . import protocols
- from . import transports
- from .log import logger
- class BaseSubprocessTransport(transports.SubprocessTransport):
- def __init__(self, loop, protocol, args, shell,
- stdin, stdout, stderr, bufsize,
- waiter=None, extra=None, **kwargs):
- super().__init__(extra)
- self._closed = False
- self._protocol = protocol
- self._loop = loop
- self._proc = None
- self._pid = None
- self._returncode = None
- self._exit_waiters = []
- self._pending_calls = collections.deque()
- self._pipes = {}
- self._finished = False
- if stdin == subprocess.PIPE:
- self._pipes[0] = None
- if stdout == subprocess.PIPE:
- self._pipes[1] = None
- if stderr == subprocess.PIPE:
- self._pipes[2] = None
- # Create the child process: set the _proc attribute
- try:
- self._start(args=args, shell=shell, stdin=stdin, stdout=stdout,
- stderr=stderr, bufsize=bufsize, **kwargs)
- except:
- self.close()
- raise
- self._pid = self._proc.pid
- self._extra['subprocess'] = self._proc
- if self._loop.get_debug():
- if isinstance(args, (bytes, str)):
- program = args
- else:
- program = args[0]
- logger.debug('process %r created: pid %s',
- program, self._pid)
- self._loop.create_task(self._connect_pipes(waiter))
- def __repr__(self):
- info = [self.__class__.__name__]
- if self._closed:
- info.append('closed')
- if self._pid is not None:
- info.append(f'pid={self._pid}')
- if self._returncode is not None:
- info.append(f'returncode={self._returncode}')
- elif self._pid is not None:
- info.append('running')
- else:
- info.append('not started')
- stdin = self._pipes.get(0)
- if stdin is not None:
- info.append(f'stdin={stdin.pipe}')
- stdout = self._pipes.get(1)
- stderr = self._pipes.get(2)
- if stdout is not None and stderr is stdout:
- info.append(f'stdout=stderr={stdout.pipe}')
- else:
- if stdout is not None:
- info.append(f'stdout={stdout.pipe}')
- if stderr is not None:
- info.append(f'stderr={stderr.pipe}')
- return '<{}>'.format(' '.join(info))
- def _start(self, args, shell, stdin, stdout, stderr, bufsize, **kwargs):
- raise NotImplementedError
- def set_protocol(self, protocol):
- self._protocol = protocol
- def get_protocol(self):
- return self._protocol
- def is_closing(self):
- return self._closed
- def close(self):
- if self._closed:
- return
- self._closed = True
- for proto in self._pipes.values():
- if proto is None:
- continue
- proto.pipe.close()
- if (self._proc is not None and
- # has the child process finished?
- self._returncode is None and
- # the child process has finished, but the
- # transport hasn't been notified yet?
- self._proc.poll() is None):
- if self._loop.get_debug():
- logger.warning('Close running child process: kill %r', self)
- try:
- self._proc.kill()
- except ProcessLookupError:
- pass
- # Don't clear the _proc reference yet: _post_init() may still run
- def __del__(self, _warn=warnings.warn):
- if not self._closed:
- _warn(f"unclosed transport {self!r}", ResourceWarning, source=self)
- self.close()
- def get_pid(self):
- return self._pid
- def get_returncode(self):
- return self._returncode
- def get_pipe_transport(self, fd):
- if fd in self._pipes:
- return self._pipes[fd].pipe
- else:
- return None
- def _check_proc(self):
- if self._proc is None:
- raise ProcessLookupError()
- def send_signal(self, signal):
- self._check_proc()
- self._proc.send_signal(signal)
- def terminate(self):
- self._check_proc()
- self._proc.terminate()
- def kill(self):
- self._check_proc()
- self._proc.kill()
- async def _connect_pipes(self, waiter):
- try:
- proc = self._proc
- loop = self._loop
- if proc.stdin is not None:
- _, pipe = await loop.connect_write_pipe(
- lambda: WriteSubprocessPipeProto(self, 0),
- proc.stdin)
- self._pipes[0] = pipe
- if proc.stdout is not None:
- _, pipe = await loop.connect_read_pipe(
- lambda: ReadSubprocessPipeProto(self, 1),
- proc.stdout)
- self._pipes[1] = pipe
- if proc.stderr is not None:
- _, pipe = await loop.connect_read_pipe(
- lambda: ReadSubprocessPipeProto(self, 2),
- proc.stderr)
- self._pipes[2] = pipe
- assert self._pending_calls is not None
- loop.call_soon(self._protocol.connection_made, self)
- for callback, data in self._pending_calls:
- loop.call_soon(callback, *data)
- self._pending_calls = None
- except (SystemExit, KeyboardInterrupt):
- raise
- except BaseException as exc:
- if waiter is not None and not waiter.cancelled():
- waiter.set_exception(exc)
- else:
- if waiter is not None and not waiter.cancelled():
- waiter.set_result(None)
- def _call(self, cb, *data):
- if self._pending_calls is not None:
- self._pending_calls.append((cb, data))
- else:
- self._loop.call_soon(cb, *data)
- def _pipe_connection_lost(self, fd, exc):
- self._call(self._protocol.pipe_connection_lost, fd, exc)
- self._try_finish()
- def _pipe_data_received(self, fd, data):
- self._call(self._protocol.pipe_data_received, fd, data)
- def _process_exited(self, returncode):
- assert returncode is not None, returncode
- assert self._returncode is None, self._returncode
- if self._loop.get_debug():
- logger.info('%r exited with return code %r', self, returncode)
- self._returncode = returncode
- if self._proc.returncode is None:
- # asyncio uses a child watcher: copy the status into the Popen
- # object. On Python 3.6, it is required to avoid a ResourceWarning.
- self._proc.returncode = returncode
- self._call(self._protocol.process_exited)
- self._try_finish()
- async def _wait(self):
- """Wait until the process exit and return the process return code.
- This method is a coroutine."""
- if self._returncode is not None:
- return self._returncode
- waiter = self._loop.create_future()
- self._exit_waiters.append(waiter)
- return await waiter
- def _try_finish(self):
- assert not self._finished
- if self._returncode is None:
- return
- if all(p is not None and p.disconnected
- for p in self._pipes.values()):
- self._finished = True
- self._call(self._call_connection_lost, None)
- def _call_connection_lost(self, exc):
- try:
- self._protocol.connection_lost(exc)
- finally:
- # wake up futures waiting for wait()
- for waiter in self._exit_waiters:
- if not waiter.cancelled():
- waiter.set_result(self._returncode)
- self._exit_waiters = None
- self._loop = None
- self._proc = None
- self._protocol = None
- class WriteSubprocessPipeProto(protocols.BaseProtocol):
- def __init__(self, proc, fd):
- self.proc = proc
- self.fd = fd
- self.pipe = None
- self.disconnected = False
- def connection_made(self, transport):
- self.pipe = transport
- def __repr__(self):
- return f'<{self.__class__.__name__} fd={self.fd} pipe={self.pipe!r}>'
- def connection_lost(self, exc):
- self.disconnected = True
- self.proc._pipe_connection_lost(self.fd, exc)
- self.proc = None
- def pause_writing(self):
- self.proc._protocol.pause_writing()
- def resume_writing(self):
- self.proc._protocol.resume_writing()
- class ReadSubprocessPipeProto(WriteSubprocessPipeProto,
- protocols.Protocol):
- def data_received(self, data):
- self.proc._pipe_data_received(self.fd, data)
|