7
7
from queue import Queue
8
8
import http
9
9
import json
10
+ import multiprocessing .connection
10
11
import os
11
12
import shutil
12
13
import socket
@@ -48,26 +49,33 @@ def on_stderr_message(self, message: str) -> None:
48
49
49
50
class AbstractProcessor (Generic [T ]):
50
51
51
- def write_data (self , writer : IO [bytes ], data : T ) -> None :
52
+ def write_data (self , writer : IO [bytes ], data : T , is_node_ipc = False ) -> None :
52
53
raise NotImplementedError ()
53
54
54
- def read_data (self , reader : IO [bytes ]) -> Optional [T ]:
55
+ def read_data (self , reader : IO [bytes ], is_node_ipc = False ) -> Optional [T ]:
55
56
raise NotImplementedError ()
56
57
57
58
58
59
class JsonRpcProcessor (AbstractProcessor [Dict [str , Any ]]):
59
60
60
- def write_data (self , writer : IO [bytes ], data : Dict [str , Any ]) -> None :
61
+ def write_data (self , writer : IO [bytes ], data : Dict [str , Any ], is_node_ipc = False ) -> None :
61
62
body = self ._encode (data )
62
- writer .writelines (("Content-Length: {}\r \n \r \n " .format (len (body )).encode ('ascii' ), body ))
63
+ if not is_node_ipc :
64
+ writer .writelines (("Content-Length: {}\r \n \r \n " .format (len (body )).encode ('ascii' ), body ))
65
+ else :
66
+ writer .write (body + b"\n " )
67
+
68
+ def read_data (self , reader : IO [bytes ], is_node_ipc = False ) -> Optional [Dict [str , Any ]]:
69
+ if not is_node_ipc :
70
+ headers = http .client .parse_headers (reader ) # type: ignore
71
+ try :
72
+ body = reader .read (int (headers .get ("Content-Length" )))
73
+ except TypeError :
74
+ # Expected error on process stopping. Stop the read loop.
75
+ raise StopLoopError ()
76
+ else :
77
+ body = reader .readline ()
63
78
64
- def read_data (self , reader : IO [bytes ]) -> Optional [Dict [str , Any ]]:
65
- headers = http .client .parse_headers (reader ) # type: ignore
66
- try :
67
- body = reader .read (int (headers .get ("Content-Length" )))
68
- except TypeError :
69
- # Expected error on process stopping. Stop the read loop.
70
- raise StopLoopError ()
71
79
try :
72
80
return self ._decode (body )
73
81
except Exception as ex :
@@ -79,7 +87,6 @@ def _encode(data: Dict[str, Any]) -> bytes:
79
87
return json .dumps (
80
88
data ,
81
89
ensure_ascii = False ,
82
- sort_keys = False ,
83
90
check_circular = False ,
84
91
separators = (',' , ':' )
85
92
).encode ('utf-8' )
@@ -93,7 +100,7 @@ class ProcessTransport(Transport[T]):
93
100
94
101
def __init__ (self , name : str , process : subprocess .Popen , socket : Optional [socket .socket ], reader : IO [bytes ],
95
102
writer : IO [bytes ], stderr : Optional [IO [bytes ]], processor : AbstractProcessor [T ],
96
- callback_object : TransportCallbacks [T ]) -> None :
103
+ callback_object : TransportCallbacks [T ], is_node_ipc : bool ) -> None :
97
104
self ._closed = False
98
105
self ._process = process
99
106
self ._socket = socket
@@ -105,6 +112,7 @@ def __init__(self, name: str, process: subprocess.Popen, socket: Optional[socket
105
112
self ._writer_thread = threading .Thread (target = self ._write_loop , name = '{}-writer' .format (name ))
106
113
self ._stderr_thread = threading .Thread (target = self ._stderr_loop , name = '{}-stderr' .format (name ))
107
114
self ._callback_object = weakref .ref (callback_object )
115
+ self ._is_node_ipc = is_node_ipc
108
116
self ._send_queue = Queue (0 ) # type: Queue[Union[T, None]]
109
117
self ._reader_thread .start ()
110
118
self ._writer_thread .start ()
@@ -137,7 +145,7 @@ def __del__(self) -> None:
137
145
def _read_loop (self ) -> None :
138
146
try :
139
147
while self ._reader :
140
- payload = self ._processor .read_data (self ._reader )
148
+ payload = self ._processor .read_data (self ._reader , self . _is_node_ipc )
141
149
if payload is None :
142
150
continue
143
151
@@ -190,8 +198,9 @@ def _write_loop(self) -> None:
190
198
d = self ._send_queue .get ()
191
199
if d is None :
192
200
break
193
- self ._processor .write_data (self ._writer , d )
194
- self ._writer .flush ()
201
+ self ._processor .write_data (self ._writer , d , self ._is_node_ipc )
202
+ if not self ._is_node_ipc :
203
+ self ._writer .flush ()
195
204
except (BrokenPipeError , AttributeError ):
196
205
pass
197
206
except Exception as ex :
@@ -223,24 +232,58 @@ def _stderr_loop(self) -> None:
223
232
json_rpc_processor = JsonRpcProcessor ()
224
233
225
234
235
+ class NodeIpcIO ():
236
+ _buf = bytearray ()
237
+ _lines = 0
238
+
239
+ def __init__ (self , conn : multiprocessing .connection ._ConnectionBase ):
240
+ self ._conn = conn
241
+
242
+ # https://github.com/python/cpython/blob/330f1d58282517bdf1f19577ab9317fa9810bf95/Lib/multiprocessing/connection.py#L378-L392
243
+ def readline (self ):
244
+ while self ._lines == 0 :
245
+ chunk = self ._conn ._read (self ._conn .fileno (), 65536 ) # type: bytes
246
+ self ._buf += chunk
247
+ self ._lines += chunk .count (b'\n ' )
248
+
249
+ self ._lines -= 1
250
+ foo , _ , self ._buf = self ._buf .partition (b'\n ' )
251
+ print ('READLINE: ' + str (foo ))
252
+ return foo
253
+
254
+ # https://github.com/python/cpython/blob/330f1d58282517bdf1f19577ab9317fa9810bf95/Lib/multiprocessing/connection.py#L369-L376
255
+ def write (self , data : bytes ):
256
+ while len (data ):
257
+ n = self ._conn ._write (self ._conn .fileno (), data )
258
+ data = data [n :]
259
+
260
+
226
261
def create_transport (config : TransportConfig , cwd : Optional [str ],
227
262
callback_object : TransportCallbacks ) -> Transport [Dict [str , Any ]]:
263
+ stderr = subprocess .PIPE
264
+ pass_fds = ()
228
265
if config .tcp_port is not None :
229
266
assert config .tcp_port is not None
230
267
if config .tcp_port < 0 :
231
268
stdout = subprocess .PIPE
232
269
else :
233
270
stdout = subprocess .DEVNULL
234
271
stdin = subprocess .DEVNULL
235
- else :
272
+ elif not config . node_ipc :
236
273
stdout = subprocess .PIPE
237
274
stdin = subprocess .PIPE
275
+ else :
276
+ stdout = subprocess .PIPE
277
+ stdin = subprocess .DEVNULL
278
+ stderr = subprocess .STDOUT
279
+ pass_fds = (config .node_ipc .child_conn .fileno (),)
280
+
238
281
startupinfo = _fixup_startup_args (config .command )
239
282
sock = None # type: Optional[socket.socket]
240
283
process = None # type: Optional[subprocess.Popen]
241
284
242
285
def start_subprocess () -> subprocess .Popen :
243
- return _start_subprocess (config .command , stdin , stdout , subprocess . PIPE , startupinfo , config .env , cwd )
286
+ return _start_subprocess (config .command , stdin , stdout , stderr , startupinfo , config .env , cwd , pass_fds )
244
287
245
288
if config .listener_socket :
246
289
assert isinstance (config .tcp_port , int ) and config .tcp_port > 0
@@ -258,13 +301,16 @@ def start_subprocess() -> subprocess.Popen:
258
301
raise RuntimeError ("Failed to connect on port {}" .format (config .tcp_port ))
259
302
reader = sock .makefile ('rwb' ) # type: ignore
260
303
writer = reader
261
- else :
304
+ elif not config . node_ipc :
262
305
reader = process .stdout # type: ignore
263
306
writer = process .stdin # type: ignore
307
+ else :
308
+ reader = writer = NodeIpcIO (config .node_ipc .parent_conn )
264
309
if not reader or not writer :
265
310
raise RuntimeError ('Failed initializing transport: reader: {}, writer: {}' .format (reader , writer ))
266
- return ProcessTransport (config .name , process , sock , reader , writer , process .stderr , json_rpc_processor ,
267
- callback_object )
311
+ stderr_reader = process .stdout if config .node_ipc else process .stderr
312
+ return ProcessTransport (config .name , process , sock , reader , writer , stderr_reader , json_rpc_processor ,
313
+ callback_object , bool (config .node_ipc ))
268
314
269
315
270
316
_subprocesses = weakref .WeakSet () # type: weakref.WeakSet[subprocess.Popen]
@@ -312,7 +358,8 @@ def _start_subprocess(
312
358
stderr : int ,
313
359
startupinfo : Any ,
314
360
env : Dict [str , str ],
315
- cwd : Optional [str ]
361
+ cwd : Optional [str ],
362
+ pass_fds : Union [Tuple [()], Tuple [int ]]
316
363
) -> subprocess .Popen :
317
364
debug ("starting {} in {}" .format (args , cwd if cwd else os .getcwd ()))
318
365
process = subprocess .Popen (
@@ -322,7 +369,8 @@ def _start_subprocess(
322
369
stderr = stderr ,
323
370
startupinfo = startupinfo ,
324
371
env = env ,
325
- cwd = cwd )
372
+ cwd = cwd ,
373
+ pass_fds = pass_fds )
326
374
_subprocesses .add (process )
327
375
return process
328
376
0 commit comments