14
14
from marshal import loads , dumps
15
15
from base64 import b64encode , b64decode
16
16
import functools
17
+ import subprocess
17
18
18
19
from PyInstaller .compat import __wrap_python
19
20
21
+ # WinAPI bindings for Windows-specific codepath
20
22
if os .name == "nt" :
21
- from msvcrt import get_osfhandle , open_osfhandle
22
-
23
- def open (osf_handle , mode ):
24
- # Convert system file handles to file descriptors before opening them.
25
- return os .fdopen (open_osfhandle (osf_handle , 0 ), mode )
26
-
27
- def close (osf_handle ):
28
- # Likewise when closing.
29
- return os .close (open_osfhandle (osf_handle , 0 ))
30
-
31
- else :
32
- close = os .close
23
+ import msvcrt
24
+ import ctypes
25
+ import ctypes .wintypes
26
+
27
+ # CreatePipe
28
+ class SECURITY_ATTRIBUTES (ctypes .Structure ):
29
+ _fields_ = [
30
+ ("nLength" , ctypes .wintypes .DWORD ),
31
+ ("lpSecurityDescriptor" , ctypes .wintypes .LPVOID ),
32
+ ("bInheritHandle" , ctypes .wintypes .BOOL ),
33
+ ]
34
+
35
+ HANDLE_FLAG_INHERIT = 0x0001
36
+
37
+ LPSECURITY_ATTRIBUTES = ctypes .POINTER (SECURITY_ATTRIBUTES )
38
+
39
+ CreatePipe = ctypes .windll .kernel32 .CreatePipe
40
+ CreatePipe .argtypes = [
41
+ ctypes .POINTER (ctypes .wintypes .HANDLE ),
42
+ ctypes .POINTER (ctypes .wintypes .HANDLE ),
43
+ LPSECURITY_ATTRIBUTES ,
44
+ ctypes .wintypes .DWORD ,
45
+ ]
46
+ CreatePipe .restype = ctypes .wintypes .BOOL
47
+
48
+ # CloseHandle
49
+ CloseHandle = ctypes .windll .kernel32 .CloseHandle
50
+ CloseHandle .argtypes = [ctypes .wintypes .HANDLE ]
51
+ CloseHandle .restype = ctypes .wintypes .BOOL
33
52
34
53
CHILD_PY = Path (__file__ ).with_name ("_child.py" )
35
54
36
55
37
- def pipe ( ):
56
+ def create_pipe ( read_handle_inheritable , write_handle_inheritable ):
38
57
"""
39
58
Create a one-way pipe for sending data to child processes.
40
59
60
+ Args:
61
+ read_handle_inheritable:
62
+ A boolean flag indicating whether the handle corresponding to the read end-point of the pipe should be
63
+ marked as inheritable by subprocesses.
64
+ write_handle_inheritable:
65
+ A boolean flag indicating whether the handle corresponding to the write end-point of the pipe should be
66
+ marked as inheritable by subprocesses.
67
+
41
68
Returns:
42
- A read/write pair of file descriptors (which are just integers) on posix or system file handle on Windows.
69
+ A read/write pair of file descriptors (which are just integers) on posix or system file handles on Windows.
43
70
44
71
The pipe may be used either by this process or subprocesses of this process but not globally.
72
+ """
73
+ return _create_pipe_impl (read_handle_inheritable , write_handle_inheritable )
74
+
45
75
76
+ def close_pipe_endpoint (pipe_handle ):
77
+ """
78
+ Close the file descriptor (posix) or handle (Windows) belonging to a pipe.
46
79
"""
47
- read , write = os .pipe ()
48
- # The default behaviour of pipes is that they are process specific. i.e. they can only be used for this process to
49
- # talk to itself. Setting these means that child processes may also use these pipes.
50
- os .set_inheritable (read , True )
51
- os .set_inheritable (write , True )
80
+ return _close_pipe_endpoint_impl (pipe_handle )
52
81
53
- # On Windows, file descriptors are not shareable. They need to be converted to system file handles here then
54
- # converted back with open_osfhandle() by the child.
55
- if os .name == "nt" :
56
- read , write = get_osfhandle (read ), get_osfhandle (write )
57
82
58
- return read , write
83
+ if os .name == "nt" :
84
+
85
+ def _create_pipe_impl (read_handle_inheritable , write_handle_inheritable ):
86
+ # Use WinAPI CreatePipe function to create the pipe. Python's os.pipe() does the same, but wraps the resulting
87
+ # handles into inheritable file descriptors (https://github.com/python/cpython/issues/77046). Instead, we want
88
+ # just handles, and will set the inheritable flag on corresponding handle ourselves.
89
+ read_handle = ctypes .wintypes .HANDLE ()
90
+ write_handle = ctypes .wintypes .HANDLE ()
91
+
92
+ # SECURITY_ATTRIBUTES with inherit handle set to True
93
+ security_attributes = SECURITY_ATTRIBUTES ()
94
+ security_attributes .nLength = ctypes .sizeof (security_attributes )
95
+ security_attributes .bInheritHandle = True
96
+ security_attributes .lpSecurityDescriptor = None
97
+
98
+ # CreatePipe()
99
+ succeeded = CreatePipe (
100
+ ctypes .byref (read_handle ), # hReadPipe
101
+ ctypes .byref (write_handle ), # hWritePipe
102
+ ctypes .byref (security_attributes ), # lpPipeAttributes
103
+ 0 , # nSize
104
+ )
105
+ if not succeeded :
106
+ raise ctypes .WinError ()
107
+
108
+ # Set inheritable flags. Instead of binding and using SetHandleInformation WinAPI function, we can use
109
+ # os.set_handle_inheritable().
110
+ os .set_handle_inheritable (read_handle .value , read_handle_inheritable )
111
+ os .set_handle_inheritable (write_handle .value , write_handle_inheritable )
112
+
113
+ return read_handle .value , write_handle .value
114
+
115
+ def _close_pipe_endpoint_impl (pipe_handle ):
116
+ succeeded = CloseHandle (pipe_handle )
117
+ if not succeeded :
118
+ raise ctypes .WinError ()
119
+ else :
120
+
121
+ def _create_pipe_impl (read_fd_inheritable , write_fd_inheritable ):
122
+ # Create pipe, using os.pipe()
123
+ read_fd , write_fd = os .pipe ()
124
+
125
+ # The default behaviour of pipes is that they are process specific. I.e., they can only be used by this
126
+ # process to talk to itself. Setting inheritable flags means that child processes may also use these pipes.
127
+ os .set_inheritable (read_fd , read_fd_inheritable )
128
+ os .set_inheritable (write_fd , write_fd_inheritable )
129
+
130
+ return read_fd , write_fd
131
+
132
+ def _close_pipe_endpoint_impl (pipe_fd ):
133
+ os .close (pipe_fd )
59
134
60
135
61
136
def child (read_from_parent : int , write_to_parent : int ):
62
137
"""
63
138
Spawn a Python subprocess sending it the two file descriptors it needs to talk back to this parent process.
64
139
"""
65
- from subprocess import Popen
140
+ if os .name != 'nt' :
141
+ # Explicitly disabling close_fds is a requirement for making file descriptors inheritable by child processes.
142
+ extra_kwargs = {
143
+ "env" : _subprocess_env (),
144
+ "close_fds" : False ,
145
+ }
146
+ else :
147
+ # On Windows, we can use subprocess.STARTUPINFO to explicitly pass the list of file handles to be inherited,
148
+ # so we can avoid disabling close_fds
149
+ extra_kwargs = {
150
+ "env" : _subprocess_env (),
151
+ "close_fds" : True ,
152
+ "startupinfo" : subprocess .STARTUPINFO (lpAttributeList = {"handle_list" : [read_from_parent , write_to_parent ]})
153
+ }
66
154
67
155
# Run the _child.py script directly passing it the two file descriptors it needs to talk back to the parent.
68
- cmd , options = __wrap_python (
69
- [str (CHILD_PY ), str (read_from_parent ), str (write_to_parent )],
70
- # Explicitly disabling close_fds is a requirement for making file descriptors inheritable by child processes.
71
- dict (close_fds = False , env = _subprocess_env ()),
72
- )
156
+ cmd , options = __wrap_python ([str (CHILD_PY ), str (read_from_parent ), str (write_to_parent )], extra_kwargs )
157
+
73
158
# I'm intentionally leaving stdout and stderr alone so that print() can still be used for emergency debugging and
74
159
# unhandled errors in the child are still visible.
75
- return Popen (cmd , ** options )
160
+ return subprocess . Popen (cmd , ** options )
76
161
77
162
78
163
def _subprocess_env ():
@@ -110,17 +195,34 @@ def __init__(self):
110
195
self ._child = None
111
196
112
197
def __enter__ (self ):
113
- # We need two pipes. One for the child to send data to the parent.
114
- self ._read_from_child , self ._write_to_parent = pipe ()
115
- # And one for the parent to send data to the child.
116
- self ._read_from_parent , self ._write_to_child = pipe ()
198
+ # We need two pipes. One for the child to send data to the parent. The (write) end-point passed to the
199
+ # child needs to be marked as inheritable.
200
+ read_from_child , write_to_parent = create_pipe (False , True )
201
+ # And one for the parent to send data to the child. The (read) end-point passed to the child needs to be
202
+ # marked as inheritable.
203
+ read_from_parent , write_to_child = create_pipe (True , False )
117
204
118
205
# Spawn a Python subprocess sending it the two file descriptors it needs to talk back to this parent process.
119
- self ._child = child (self ._read_from_parent , self ._write_to_parent )
120
-
121
- # Open file handles to talk to the child.
122
- self ._write_handle = open (self ._write_to_child , "wb" )
123
- self ._read_handle = open (self ._read_from_child , "rb" )
206
+ self ._child = child (read_from_parent , write_to_parent )
207
+
208
+ # Close the end-points that were inherited by the child.
209
+ close_pipe_endpoint (read_from_parent )
210
+ close_pipe_endpoint (write_to_parent )
211
+ del read_from_parent
212
+ del write_to_parent
213
+
214
+ # Open file handles to talk to the child. This should fully transfer ownership of the underlying file
215
+ # descriptor to the opened handle; so when we close the latter, the former should be closed as well.
216
+ if os .name == 'nt' :
217
+ # On Windows, we must first open file descriptor on top of the handle using _open_osfhandle (which
218
+ # python wraps in msvcrt.open_osfhandle). According to MSDN, this transfers the ownership of the
219
+ # underlying file handle to the file descriptors; i.e., they are both closed when the file descriptor
220
+ # is closed).
221
+ self ._write_handle = os .fdopen (msvcrt .open_osfhandle (write_to_child , 0 ), "wb" )
222
+ self ._read_handle = os .fdopen (msvcrt .open_osfhandle (read_from_child , 0 ), "rb" )
223
+ else :
224
+ self ._write_handle = os .fdopen (write_to_child , "wb" )
225
+ self ._read_handle = os .fdopen (read_from_child , "rb" )
124
226
125
227
return self
126
228
@@ -130,14 +232,11 @@ def __exit__(self, exc_type, exc_val, exc_tb):
130
232
self ._write_handle .flush ()
131
233
self ._child .wait ()
132
234
133
- # Then close all the pipes and handles. These steps were determined empirically to appease the corresponding
134
- # test: tests/unit/test_isolation.py::test_pipe_leakage
135
- # I don't really understand why they must be this way.
136
- close (self ._read_from_parent )
137
- close (self ._write_to_parent )
235
+ # Close the handles. This should also close the underlying file descriptors.
138
236
self ._write_handle .close ()
139
237
self ._read_handle .close ()
140
238
del self ._read_handle , self ._write_handle
239
+
141
240
self ._child = None
142
241
143
242
def call (self , function , * args , ** kwargs ):
0 commit comments