Skip to content

Commit a78e4d2

Browse files
authored
Fix subprocess.close() to let its processes die gracefully (#151)
* Fix subprocess.close() to let its processes die gracefully Fixes #128.
1 parent 0e5db37 commit a78e4d2

File tree

4 files changed

+67
-3
lines changed

4 files changed

+67
-3
lines changed

tests/test_process.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import contextlib
3+
import gc
34
import os
45
import pathlib
56
import signal
@@ -574,12 +575,41 @@ def cancel_make_transport():
574575
except asyncio.CancelledError:
575576
pass
576577

578+
yield from asyncio.sleep(0.3, loop=self.loop)
579+
gc.collect()
580+
577581
# ignore the log:
578582
# "Exception during subprocess creation, kill the subprocess"
579583
with tb.disable_logger():
580584
self.loop.run_until_complete(cancel_make_transport())
581585
tb.run_briefly(self.loop)
582586

587+
def test_close_gets_process_closed(self):
588+
589+
loop = self.loop
590+
591+
class Protocol(asyncio.SubprocessProtocol):
592+
593+
def __init__(self):
594+
self.closed = loop.create_future()
595+
596+
def connection_lost(self, exc):
597+
self.closed.set_result(1)
598+
599+
@asyncio.coroutine
600+
def test_subprocess():
601+
transport, protocol = yield from loop.subprocess_exec(
602+
Protocol, *self.PROGRAM_BLOCKED)
603+
pid = transport.get_pid()
604+
transport.close()
605+
self.assertIsNone(transport.get_returncode())
606+
yield from protocol.closed
607+
self.assertIsNotNone(transport.get_returncode())
608+
with self.assertRaises(ProcessLookupError):
609+
os.kill(pid, 0)
610+
611+
loop.run_until_complete(test_subprocess())
612+
583613

584614
class Test_UV_Process(_TestProcess, tb.UVTestCase):
585615

uvloop/handles/handle.pyx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ cdef class UVHandle:
5555
'{} is open in __dealloc__ with loop set to NULL'
5656
.format(self.__class__.__name__))
5757

58-
if self._closed == 1:
58+
if self._closed:
5959
# So _handle is not NULL and self._closed == 1?
6060
raise RuntimeError(
6161
'{}.__dealloc__: _handle is NULL, _closed == 1'.format(

uvloop/handles/process.pxd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ cdef class UVProcess(UVHandle):
2020
char *uv_opt_file
2121
bytes __cwd
2222

23+
bint _kill_requested
24+
2325
cdef _init(self, Loop loop, list args, dict env, cwd,
2426
start_new_session,
2527
_stdin, _stdout, _stderr, pass_fds,

uvloop/handles/process.pyx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ cdef class UVProcess(UVHandle):
1010
self._fds_to_close = set()
1111
self._preexec_fn = None
1212
self._restore_signals = True
13+
self._kill_requested = False
1314

1415
cdef _init(self, Loop loop, list args, dict env,
1516
cwd, start_new_session,
@@ -182,7 +183,7 @@ cdef class UVProcess(UVHandle):
182183
'UVProcess._close_after_spawn called after uv_spawn')
183184
self._fds_to_close.add(fd)
184185

185-
def __dealloc__(self):
186+
cdef _dealloc_impl(self):
186187
if self.uv_opt_env is not NULL:
187188
PyMem_RawFree(self.uv_opt_env)
188189
self.uv_opt_env = NULL
@@ -191,6 +192,8 @@ cdef class UVProcess(UVHandle):
191192
PyMem_RawFree(self.uv_opt_args)
192193
self.uv_opt_args = NULL
193194

195+
UVHandle._dealloc_impl(self)
196+
194197
cdef char** __to_cstring_array(self, list arr):
195198
cdef:
196199
int i
@@ -303,6 +306,8 @@ cdef class UVProcess(UVHandle):
303306
cdef _kill(self, int signum):
304307
cdef int err
305308
self._ensure_alive()
309+
if signum in {uv.SIGKILL, uv.SIGTERM}:
310+
self._kill_requested = True
306311
err = uv.uv_process_kill(<uv.uv_process_t*>self._handle, signum)
307312
if err < 0:
308313
raise convert_error(err)
@@ -532,6 +537,11 @@ cdef class UVProcessTransport(UVProcess):
532537
else:
533538
self._pending_calls.append((_CALL_CONNECTION_LOST, None, None))
534539

540+
cdef _warn_unclosed(self):
541+
if self._kill_requested:
542+
return
543+
super()._warn_unclosed()
544+
535545
def __stdio_inited(self, waiter, stdio_fut):
536546
exc = stdio_fut.exception()
537547
if exc is not None:
@@ -546,6 +556,21 @@ cdef class UVProcessTransport(UVProcess):
546556
<method1_t>self._call_connection_made,
547557
self, waiter))
548558

559+
cdef _dealloc_impl(self):
560+
cdef int fix_needed
561+
562+
if UVLOOP_DEBUG:
563+
# Check when __dealloc__ will simply call uv.uv_close()
564+
# directly, thus *skipping* incrementing the debug counter;
565+
# we need to fix that.
566+
fix_needed = not self._closed and self._inited
567+
568+
UVProcess._dealloc_impl(self)
569+
570+
if UVLOOP_DEBUG and fix_needed and self._kill_requested:
571+
self._loop._debug_handles_closed.update([
572+
self.__class__.__name__])
573+
549574
@staticmethod
550575
cdef UVProcessTransport new(Loop loop, protocol, args, env,
551576
cwd, start_new_session,
@@ -628,7 +653,14 @@ cdef class UVProcessTransport(UVProcess):
628653
if self._stderr is not None:
629654
self._stderr.close()
630655

631-
self._close()
656+
if self._returncode is not None:
657+
# The process is dead, just close the UV handle.
658+
#
659+
# (If "self._returncode is None", the process should have been
660+
# killed already and we're just waiting for a SIGCHLD; after
661+
# which the transport will be GC'ed and the uvhandle will be
662+
# closed in UVHandle.__dealloc__.)
663+
self._close()
632664

633665
def get_extra_info(self, name, default=None):
634666
return default

0 commit comments

Comments
 (0)