Skip to content

Commit b5b3b12

Browse files
committed
subprocess: Support 'preexec_fn' and 'restore_signals' arguments
1 parent a11c5b8 commit b5b3b12

File tree

9 files changed

+248
-21
lines changed

9 files changed

+248
-21
lines changed

tests/test_process.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import asyncio
22
import contextlib
3+
import os
34
import signal
45
import subprocess
56
import sys
67
import tempfile
8+
import time
79

810
from asyncio import test_utils
911
from uvloop import _testbase as tb
@@ -46,6 +48,59 @@ async def test():
4648

4749
self.loop.run_until_complete(test())
4850

51+
def test_process_preexec_fn_1(self):
52+
# Copied from CPython/test_suprocess.py
53+
54+
# DISCLAIMER: Setting environment variables is *not* a good use
55+
# of a preexec_fn. This is merely a test.
56+
57+
async def test():
58+
cmd = sys.executable
59+
proc = await asyncio.create_subprocess_exec(
60+
cmd, '-c',
61+
'import os,sys;sys.stdout.write(os.getenv("FRUIT"))',
62+
stdout=subprocess.PIPE,
63+
preexec_fn=lambda: os.putenv("FRUIT", "apple"),
64+
loop=self.loop)
65+
66+
out, _ = await proc.communicate()
67+
self.assertEqual(out, b'apple')
68+
self.assertEqual(proc.returncode, 0)
69+
70+
self.loop.run_until_complete(test())
71+
72+
def test_process_preexec_fn_2(self):
73+
# Copied from CPython/test_suprocess.py
74+
75+
def raise_it():
76+
raise ValueError("spam")
77+
78+
async def test():
79+
cmd = sys.executable
80+
proc = await asyncio.create_subprocess_exec(
81+
cmd, '-c', 'import time; time.sleep(10)',
82+
preexec_fn=raise_it,
83+
loop=self.loop)
84+
85+
await proc.communicate()
86+
87+
started = time.time()
88+
try:
89+
self.loop.run_until_complete(test())
90+
except subprocess.SubprocessError as ex:
91+
self.assertIn('preexec_fn', ex.args[0])
92+
if ex.__cause__ is not None:
93+
# uvloop will set __cause__
94+
self.assertIs(type(ex.__cause__), ValueError)
95+
self.assertEqual(ex.__cause__.args[0], 'spam')
96+
else:
97+
self.fail(
98+
'exception in preexec_fn did not propagate to the parent')
99+
100+
if time.time() - started > 5:
101+
self.fail(
102+
'exception in preexec_fn did not kill the child process')
103+
49104
def test_process_executable_1(self):
50105
async def test():
51106
proc = await asyncio.create_subprocess_exec(

uvloop/handles/process.pxd

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ cdef class UVProcess(UVHandle):
33
object _returncode
44
object _pid
55

6+
object _errpipe_read
7+
object _errpipe_write
8+
object _preexec_fn
9+
bint _restore_signals
10+
611
set _fds_to_close
712

813
# Attributes used to compose uv_process_options_t:
@@ -18,7 +23,9 @@ cdef class UVProcess(UVHandle):
1823
cdef _init(self, Loop loop, list args, dict env, cwd,
1924
start_new_session,
2025
_stdin, _stdout, _stderr, pass_fds,
21-
debug_flags)
26+
debug_flags, preexec_fn, restore_signals)
27+
28+
cdef _after_fork(self)
2229

2330
cdef char** __to_cstring_array(self, list arr)
2431
cdef _init_args(self, list args)
@@ -67,4 +74,5 @@ cdef class UVProcessTransport(UVProcess):
6774
start_new_session,
6875
_stdin, _stdout, _stderr, pass_fds,
6976
waiter,
70-
debug_flags)
77+
debug_flags,
78+
preexec_fn, restore_signals)

uvloop/handles/process.pyx

Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ cdef class UVProcess(UVHandle):
88
self._returncode = None
99
self._pid = None
1010
self._fds_to_close = set()
11+
self._preexec_fn = None
12+
self._restore_signals = True
1113

1214
cdef _init(self, Loop loop, list args, dict env,
1315
cwd, start_new_session,
1416
_stdin, _stdout, _stderr, # std* can be defined as macros in C
15-
pass_fds, debug_flags):
17+
pass_fds, debug_flags, preexec_fn, restore_signals):
18+
19+
global __forking
20+
global __forking_loop
1621

1722
cdef int err
1823

@@ -43,14 +48,62 @@ cdef class UVProcess(UVHandle):
4348
self._abort_init()
4449
raise
4550

46-
err = uv.uv_spawn(loop.uvloop,
47-
<uv.uv_process_t*>self._handle,
48-
&self.options)
49-
if err < 0:
51+
if __forking or loop.active_process_handler is not None:
52+
# Our pthread_atfork handlers won't work correctly when
53+
# another loop is forking in another thread (even though
54+
# GIL should help us to avoid that.)
5055
self._abort_init()
51-
raise convert_error(err)
56+
raise RuntimeError(
57+
'Racing with another loop to spawn a process.')
58+
59+
self._errpipe_read, self._errpipe_write = os_pipe()
60+
try:
61+
os_set_inheritable(self._errpipe_write, True)
62+
63+
self._preexec_fn = preexec_fn
64+
self._restore_signals = restore_signals
65+
66+
loop.active_process_handler = self
67+
__forking = 1
68+
__forking_loop = loop
69+
70+
_PyImport_AcquireLock()
71+
72+
err = uv.uv_spawn(loop.uvloop,
73+
<uv.uv_process_t*>self._handle,
74+
&self.options)
5275

53-
self._finish_init()
76+
__forking = 0
77+
__forking_loop = None
78+
loop.active_process_handler = None
79+
80+
if _PyImport_ReleaseLock() < 0:
81+
# See CPython/posixmodule.c for details
82+
self._abort_init()
83+
raise RuntimeError('not holding the import lock')
84+
85+
if err < 0:
86+
self._abort_init()
87+
raise convert_error(err)
88+
89+
self._finish_init()
90+
91+
os_close(self._errpipe_write)
92+
93+
errpipe_data = bytearray()
94+
while True:
95+
part = os_read(self._errpipe_read, 50000)
96+
errpipe_data += part
97+
if not part or len(errpipe_data) > 50000:
98+
break
99+
100+
finally:
101+
os_close(self._errpipe_read)
102+
try:
103+
os_close(self._errpipe_write)
104+
except OSError:
105+
# Might be already closed
106+
pass
54107

55108
# asyncio caches the PID in BaseSubprocessTransport,
56109
# so that the transport knows what the PID was even
@@ -68,6 +121,52 @@ cdef class UVProcess(UVHandle):
68121
if debug_flags & __PROCESS_DEBUG_SLEEP_AFTER_FORK:
69122
time_sleep(1)
70123

124+
if errpipe_data:
125+
# preexec_fn has raised an exception. The child
126+
# process must be dead now.
127+
try:
128+
exc_name, exc_msg = errpipe_data.split(b':', 1)
129+
exc_name = exc_name.decode()
130+
exc_msg = exc_msg.decode()
131+
except:
132+
self._close()
133+
raise subprocess_SubprocessError(
134+
'Bad exception data from child: {!r}'.format(
135+
errpipe_data))
136+
exc_cls = getattr(__builtins__, exc_name,
137+
subprocess_SubprocessError)
138+
139+
exc = subprocess_SubprocessError(
140+
'Exception occurred in preexec_fn.')
141+
exc.__cause__ = exc_cls(exc_msg)
142+
self._close()
143+
raise exc
144+
145+
cdef _after_fork(self):
146+
# See CPython/_posixsubprocess.c for details
147+
148+
if self._restore_signals:
149+
_Py_RestoreSignals()
150+
151+
if self._preexec_fn is not None:
152+
PyOS_AfterFork()
153+
154+
try:
155+
gc_disable()
156+
self._preexec_fn()
157+
except BaseException as ex:
158+
try:
159+
with open(self._errpipe_write, 'wb') as f:
160+
f.write(str(ex.__class__.__name__).encode())
161+
f.write(b':')
162+
f.write(str(ex.args[0]).encode())
163+
finally:
164+
system._exit(255)
165+
else:
166+
os_close(self._errpipe_write)
167+
else:
168+
os_close(self._errpipe_write)
169+
71170
cdef _close_after_spawn(self, int fd):
72171
if self._fds_to_close is None:
73172
raise RuntimeError(
@@ -438,7 +537,9 @@ cdef class UVProcessTransport(UVProcess):
438537
cwd, start_new_session,
439538
_stdin, _stdout, _stderr, pass_fds,
440539
waiter,
441-
debug_flags):
540+
debug_flags,
541+
preexec_fn,
542+
restore_signals):
442543

443544
cdef UVProcessTransport handle
444545
handle = UVProcessTransport.__new__(UVProcessTransport)
@@ -448,7 +549,9 @@ cdef class UVProcessTransport(UVProcess):
448549
__process_convert_fileno(_stdout),
449550
__process_convert_fileno(_stderr),
450551
pass_fds,
451-
debug_flags)
552+
debug_flags,
553+
preexec_fn,
554+
restore_signals)
452555

453556
if handle._init_futs:
454557
handle._stdio_ready = 0

uvloop/includes/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import collections
44
import concurrent.futures
55
import functools
6+
import gc
67
import inspect
78
import itertools
89
import os

uvloop/includes/python.pxd

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@ cdef extern from "Python.h":
66

77
object PyUnicode_EncodeFSDefault(object)
88
void PyErr_SetInterrupt() nogil
9+
10+
void PyOS_AfterFork()
11+
void _PyImport_AcquireLock()
12+
int _PyImport_ReleaseLock()
13+
void _Py_RestoreSignals()

uvloop/includes/stdlib.pxi

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import asyncio, asyncio.log, asyncio.base_events, \
44
import collections
55
import concurrent.futures
66
import functools
7+
import gc
78
import inspect
89
import itertools
910
import os
@@ -44,6 +45,8 @@ cdef cc_Future = concurrent.futures.Future
4445

4546
cdef ft_partial = functools.partial
4647

48+
cdef gc_disable = gc.disable
49+
4750
cdef iter_chain = itertools.chain
4851
cdef inspect_isgenerator = inspect.isgenerator
4952

@@ -88,6 +91,8 @@ cdef os_close = os.close
8891
cdef os_open = os.open
8992
cdef os_devnull = os.devnull
9093
cdef os_O_RDWR = os.O_RDWR
94+
cdef os_pipe = os.pipe
95+
cdef os_read = os.read
9196

9297
cdef sys_ignore_environment = sys.flags.ignore_environment
9398
cdef sys_exc_info = sys.exc_info
@@ -102,6 +107,7 @@ cdef long MAIN_THREAD_ID = <long>threading.main_thread().ident
102107
cdef int subprocess_PIPE = subprocess.PIPE
103108
cdef int subprocess_STDOUT = subprocess.STDOUT
104109
cdef int subprocess_DEVNULL = subprocess.DEVNULL
110+
cdef subprocess_SubprocessError = subprocess.SubprocessError
105111

106112
cdef int signal_NSIG = std_signal.NSIG
107113

uvloop/includes/system.pxd

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ cdef extern from "sys/socket.h" nogil:
4848
cdef extern from "unistd.h" nogil:
4949

5050
ssize_t write(int fd, const void *buf, size_t count)
51+
void _exit(int status)
52+
53+
54+
cdef extern from "pthread.h" nogil:
55+
56+
int pthread_atfork(
57+
void (*prepare)() nogil,
58+
void (*parent)() nogil,
59+
void (*child)() nogil)
5160

5261

5362
cdef extern from "includes/compat.h" nogil:

uvloop/loop.pxd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ cdef class Loop:
6363
dict _signal_handlers
6464
bint _custom_sigint
6565

66+
UVProcess active_process_handler
67+
6668
UVAsync handler_async
6769
UVIdle handler_idle
6870
UVCheck handler_check__exec_writes

0 commit comments

Comments
 (0)