mirror of
https://github.com/python/cpython.git
synced 2024-11-21 21:09:37 +01:00
bd473aa598
asyncio earlier relied on subprocess module to send signals to the process, this has some drawbacks one being that subprocess module unnecessarily calls waitpid on child processes and hence it races with asyncio implementation which internally uses child watchers. To mitigate this, now asyncio sends signals directly to the process without going through the subprocess on non windows systems. On Windows it fallbacks to subprocess module handling but on windows there are no child watchers so this issue doesn't exists altogether.
304 lines
9.2 KiB
Python
304 lines
9.2 KiB
Python
import collections
|
|
import subprocess
|
|
import warnings
|
|
import os
|
|
import signal
|
|
import sys
|
|
|
|
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, PermissionError):
|
|
# the process may have already exited or may be running setuid
|
|
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()
|
|
|
|
if sys.platform == 'win32':
|
|
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()
|
|
else:
|
|
def send_signal(self, signal):
|
|
self._check_proc()
|
|
try:
|
|
os.kill(self._proc.pid, signal)
|
|
except ProcessLookupError:
|
|
pass
|
|
|
|
def terminate(self):
|
|
self.send_signal(signal.SIGTERM)
|
|
|
|
def kill(self):
|
|
self.send_signal(signal.SIGKILL)
|
|
|
|
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)
|