Skip to content

Commit 47cd10d

Browse files
committed
asyncio: sync with Tulip
Issue #23347: send_signal(), kill() and terminate() methods of BaseSubprocessTransport now check if the transport was closed and if the process exited. Issue #23347: Refactor creation of subprocess transports. Changes on BaseSubprocessTransport: * Add a wait() method to wait until the child process exit * The constructor now accepts an optional waiter parameter. The _post_init() coroutine must not be called explicitly anymore. It makes subprocess transports closer to other transports, and it gives more freedom if we want later to change completly how subprocess transports are created. * close() now kills the process instead of kindly terminate it: the child process may ignore SIGTERM and continue to run. Call explicitly terminate() and wait() if you want to kindly terminate the child process. * close() now logs a warning in debug mode if the process is still running and needs to be killed * _make_subprocess_transport() is now fully asynchronous again: if the creation of the transport failed, wait asynchronously for the process eixt. Before the wait was synchronous. This change requires close() to *kill*, and not terminate, the child process. * Remove the _kill_wait() method, replaced with a more agressive close() method. It fixes _make_subprocess_transport() on error. BaseSubprocessTransport.close() calls the close() method of pipe transports, whereas _kill_wait() closed directly pipes of the subprocess.Popen object without unregistering file descriptors from the selector (which caused severe bugs). These changes simplifies the code of subprocess.py.
1 parent 978a9af commit 47cd10d

File tree

6 files changed

+166
-104
lines changed

6 files changed

+166
-104
lines changed

Lib/asyncio/base_subprocess.py

Lines changed: 65 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import sys
44
import warnings
55

6+
from . import futures
67
from . import protocols
78
from . import transports
89
from .coroutines import coroutine
@@ -13,27 +14,32 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
1314

1415
def __init__(self, loop, protocol, args, shell,
1516
stdin, stdout, stderr, bufsize,
16-
extra=None, **kwargs):
17+
waiter=None, extra=None, **kwargs):
1718
super().__init__(extra)
1819
self._closed = False
1920
self._protocol = protocol
2021
self._loop = loop
22+
self._proc = None
2123
self._pid = None
22-
24+
self._returncode = None
25+
self._exit_waiters = []
26+
self._pending_calls = collections.deque()
2327
self._pipes = {}
28+
self._finished = False
29+
2430
if stdin == subprocess.PIPE:
2531
self._pipes[0] = None
2632
if stdout == subprocess.PIPE:
2733
self._pipes[1] = None
2834
if stderr == subprocess.PIPE:
2935
self._pipes[2] = None
30-
self._pending_calls = collections.deque()
31-
self._finished = False
32-
self._returncode = None
36+
37+
# Create the child process: set the _proc attribute
3338
self._start(args=args, shell=shell, stdin=stdin, stdout=stdout,
3439
stderr=stderr, bufsize=bufsize, **kwargs)
3540
self._pid = self._proc.pid
3641
self._extra['subprocess'] = self._proc
42+
3743
if self._loop.get_debug():
3844
if isinstance(args, (bytes, str)):
3945
program = args
@@ -42,6 +48,8 @@ def __init__(self, loop, protocol, args, shell,
4248
logger.debug('process %r created: pid %s',
4349
program, self._pid)
4450

51+
self._loop.create_task(self._connect_pipes(waiter))
52+
4553
def __repr__(self):
4654
info = [self.__class__.__name__]
4755
if self._closed:
@@ -77,12 +85,23 @@ def _make_read_subprocess_pipe_proto(self, fd):
7785

7886
def close(self):
7987
self._closed = True
88+
8089
for proto in self._pipes.values():
8190
if proto is None:
8291
continue
8392
proto.pipe.close()
84-
if self._returncode is None:
85-
self.terminate()
93+
94+
if self._proc is not None and self._returncode is None:
95+
if self._loop.get_debug():
96+
logger.warning('Close running child process: kill %r', self)
97+
98+
try:
99+
self._proc.kill()
100+
except ProcessLookupError:
101+
pass
102+
103+
# Don't clear the _proc reference yet because _post_init() may
104+
# still run
86105

87106
# On Python 3.3 and older, objects with a destructor part of a reference
88107
# cycle are never destroyed. It's not more the case on Python 3.4 thanks
@@ -105,59 +124,42 @@ def get_pipe_transport(self, fd):
105124
else:
106125
return None
107126

127+
def _check_proc(self):
128+
if self._closed:
129+
raise ValueError("operation on closed transport")
130+
if self._proc is None:
131+
raise ProcessLookupError()
132+
108133
def send_signal(self, signal):
134+
self._check_proc()
109135
self._proc.send_signal(signal)
110136

111137
def terminate(self):
138+
self._check_proc()
112139
self._proc.terminate()
113140

114141
def kill(self):
142+
self._check_proc()
115143
self._proc.kill()
116144

117-
def _kill_wait(self):
118-
"""Close pipes, kill the subprocess and read its return status.
119-
120-
Function called when an exception is raised during the creation
121-
of a subprocess.
122-
"""
123-
self._closed = True
124-
if self._loop.get_debug():
125-
logger.warning('Exception during subprocess creation, '
126-
'kill the subprocess %r',
127-
self,
128-
exc_info=True)
129-
130-
proc = self._proc
131-
if proc.stdout:
132-
proc.stdout.close()
133-
if proc.stderr:
134-
proc.stderr.close()
135-
if proc.stdin:
136-
proc.stdin.close()
137-
138-
try:
139-
proc.kill()
140-
except ProcessLookupError:
141-
pass
142-
self._returncode = proc.wait()
143-
144-
self.close()
145-
146145
@coroutine
147-
def _post_init(self):
146+
def _connect_pipes(self, waiter):
148147
try:
149148
proc = self._proc
150149
loop = self._loop
150+
151151
if proc.stdin is not None:
152152
_, pipe = yield from loop.connect_write_pipe(
153153
lambda: WriteSubprocessPipeProto(self, 0),
154154
proc.stdin)
155155
self._pipes[0] = pipe
156+
156157
if proc.stdout is not None:
157158
_, pipe = yield from loop.connect_read_pipe(
158159
lambda: ReadSubprocessPipeProto(self, 1),
159160
proc.stdout)
160161
self._pipes[1] = pipe
162+
161163
if proc.stderr is not None:
162164
_, pipe = yield from loop.connect_read_pipe(
163165
lambda: ReadSubprocessPipeProto(self, 2),
@@ -166,13 +168,16 @@ def _post_init(self):
166168

167169
assert self._pending_calls is not None
168170

169-
self._loop.call_soon(self._protocol.connection_made, self)
171+
loop.call_soon(self._protocol.connection_made, self)
170172
for callback, data in self._pending_calls:
171-
self._loop.call_soon(callback, *data)
173+
loop.call_soon(callback, *data)
172174
self._pending_calls = None
173-
except:
174-
self._kill_wait()
175-
raise
175+
except Exception as exc:
176+
if waiter is not None and not waiter.cancelled():
177+
waiter.set_exception(exc)
178+
else:
179+
if waiter is not None and not waiter.cancelled():
180+
waiter.set_result(None)
176181

177182
def _call(self, cb, *data):
178183
if self._pending_calls is not None:
@@ -197,6 +202,23 @@ def _process_exited(self, returncode):
197202
self._call(self._protocol.process_exited)
198203
self._try_finish()
199204

205+
# wake up futures waiting for wait()
206+
for waiter in self._exit_waiters:
207+
if not waiter.cancelled():
208+
waiter.set_result(returncode)
209+
self._exit_waiters = None
210+
211+
def wait(self):
212+
"""Wait until the process exit and return the process return code.
213+
214+
This method is a coroutine."""
215+
if self._returncode is not None:
216+
return self._returncode
217+
218+
waiter = futures.Future(loop=self._loop)
219+
self._exit_waiters.append(waiter)
220+
return (yield from waiter)
221+
200222
def _try_finish(self):
201223
assert not self._finished
202224
if self._returncode is None:
@@ -210,9 +232,9 @@ def _call_connection_lost(self, exc):
210232
try:
211233
self._protocol.connection_lost(exc)
212234
finally:
235+
self._loop = None
213236
self._proc = None
214237
self._protocol = None
215-
self._loop = None
216238

217239

218240
class WriteSubprocessPipeProto(protocols.BaseProtocol):

Lib/asyncio/subprocess.py

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ def __init__(self, limit, loop):
2525
super().__init__(loop=loop)
2626
self._limit = limit
2727
self.stdin = self.stdout = self.stderr = None
28-
self.waiter = futures.Future(loop=loop)
29-
self._waiters = collections.deque()
3028
self._transport = None
3129

3230
def __repr__(self):
@@ -61,9 +59,6 @@ def connection_made(self, transport):
6159
reader=None,
6260
loop=self._loop)
6361

64-
if not self.waiter.cancelled():
65-
self.waiter.set_result(None)
66-
6762
def pipe_data_received(self, fd, data):
6863
if fd == 1:
6964
reader = self.stdout
@@ -94,16 +89,9 @@ def pipe_connection_lost(self, fd, exc):
9489
reader.set_exception(exc)
9590

9691
def process_exited(self):
97-
returncode = self._transport.get_returncode()
9892
self._transport.close()
9993
self._transport = None
10094

101-
# wake up futures waiting for wait()
102-
while self._waiters:
103-
waiter = self._waiters.popleft()
104-
if not waiter.cancelled():
105-
waiter.set_result(returncode)
106-
10795

10896
class Process:
10997
def __init__(self, transport, protocol, loop):
@@ -124,30 +112,18 @@ def returncode(self):
124112

125113
@coroutine
126114
def wait(self):
127-
"""Wait until the process exit and return the process return code."""
128-
returncode = self._transport.get_returncode()
129-
if returncode is not None:
130-
return returncode
131-
132-
waiter = futures.Future(loop=self._loop)
133-
self._protocol._waiters.append(waiter)
134-
yield from waiter
135-
return waiter.result()
115+
"""Wait until the process exit and return the process return code.
136116
137-
def _check_alive(self):
138-
if self._transport.get_returncode() is not None:
139-
raise ProcessLookupError()
117+
This method is a coroutine."""
118+
return (yield from self._transport.wait())
140119

141120
def send_signal(self, signal):
142-
self._check_alive()
143121
self._transport.send_signal(signal)
144122

145123
def terminate(self):
146-
self._check_alive()
147124
self._transport.terminate()
148125

149126
def kill(self):
150-
self._check_alive()
151127
self._transport.kill()
152128

153129
@coroutine
@@ -221,11 +197,6 @@ def create_subprocess_shell(cmd, stdin=None, stdout=None, stderr=None,
221197
protocol_factory,
222198
cmd, stdin=stdin, stdout=stdout,
223199
stderr=stderr, **kwds)
224-
try:
225-
yield from protocol.waiter
226-
except:
227-
transport._kill_wait()
228-
raise
229200
return Process(transport, protocol, loop)
230201

231202
@coroutine
@@ -241,9 +212,4 @@ def create_subprocess_exec(program, *args, stdin=None, stdout=None,
241212
program, *args,
242213
stdin=stdin, stdout=stdout,
243214
stderr=stderr, **kwds)
244-
try:
245-
yield from protocol.waiter
246-
except:
247-
transport._kill_wait()
248-
raise
249215
return Process(transport, protocol, loop)

Lib/asyncio/unix_events.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from . import constants
1717
from . import coroutines
1818
from . import events
19+
from . import futures
1920
from . import selector_events
2021
from . import selectors
2122
from . import transports
@@ -175,16 +176,20 @@ def _make_subprocess_transport(self, protocol, args, shell,
175176
stdin, stdout, stderr, bufsize,
176177
extra=None, **kwargs):
177178
with events.get_child_watcher() as watcher:
179+
waiter = futures.Future(loop=self)
178180
transp = _UnixSubprocessTransport(self, protocol, args, shell,
179181
stdin, stdout, stderr, bufsize,
180-
extra=extra, **kwargs)
182+
waiter=waiter, extra=extra,
183+
**kwargs)
184+
185+
watcher.add_child_handler(transp.get_pid(),
186+
self._child_watcher_callback, transp)
181187
try:
182-
yield from transp._post_init()
188+
yield from waiter
183189
except:
184190
transp.close()
191+
yield from transp.wait()
185192
raise
186-
watcher.add_child_handler(transp.get_pid(),
187-
self._child_watcher_callback, transp)
188193

189194
return transp
190195

@@ -774,7 +779,7 @@ def __exit__(self, a, b, c):
774779
pass
775780

776781
def add_child_handler(self, pid, callback, *args):
777-
self._callbacks[pid] = callback, args
782+
self._callbacks[pid] = (callback, args)
778783

779784
# Prevent a race condition in case the child is already terminated.
780785
self._do_waitpid(pid)

Lib/asyncio/windows_events.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,13 +366,16 @@ def loop_accept_pipe(f=None):
366366
def _make_subprocess_transport(self, protocol, args, shell,
367367
stdin, stdout, stderr, bufsize,
368368
extra=None, **kwargs):
369+
waiter = futures.Future(loop=self)
369370
transp = _WindowsSubprocessTransport(self, protocol, args, shell,
370371
stdin, stdout, stderr, bufsize,
371-
extra=extra, **kwargs)
372+
waiter=waiter, extra=extra,
373+
**kwargs)
372374
try:
373-
yield from transp._post_init()
375+
yield from waiter
374376
except:
375377
transp.close()
378+
yield from transp.wait()
376379
raise
377380

378381
return transp

0 commit comments

Comments
 (0)