@@ -1202,6 +1202,79 @@ def _do_waitpid_all(self):
1202
1202
callback (pid , returncode , * args )
1203
1203
1204
1204
1205
+ # Internal helper for MultiLoopChildWatcher.
1206
+ # Manage the child handlers for a single event loop.
1207
+ # So all accesses to this are from the same thread.
1208
+ class _LoopChildWatcher :
1209
+ def __init__ (self , loop ):
1210
+ self ._loop = loop
1211
+ self ._callbacks = {}
1212
+
1213
+ def add_child_handler (self , pid , callback , * args ):
1214
+ self ._callbacks [pid ] = (callback , args )
1215
+
1216
+ # Prevent a race condition in case the child is already terminated.
1217
+ self ._do_waitpid (pid )
1218
+
1219
+ def remove_child_handler (self , pid ):
1220
+ try :
1221
+ del self ._callbacks [pid ]
1222
+ return True
1223
+ except KeyError :
1224
+ return False
1225
+
1226
+ def empty (self ):
1227
+ return not self ._callbacks
1228
+
1229
+ def _do_waitpid (self , expected_pid ):
1230
+ assert expected_pid > 0
1231
+
1232
+ try :
1233
+ pid , status = os .waitpid (expected_pid , os .WNOHANG )
1234
+ except ChildProcessError :
1235
+ # The child process is already reaped
1236
+ # (may happen if waitpid() is called elsewhere).
1237
+ pid = expected_pid
1238
+ returncode = 255
1239
+ logger .warning (
1240
+ "Unknown child process pid %d, will report returncode 255" ,
1241
+ pid )
1242
+ debug_log = False
1243
+ else :
1244
+ if pid == 0 :
1245
+ # The child process is still alive.
1246
+ return
1247
+
1248
+ returncode = _compute_returncode (status )
1249
+ debug_log = True
1250
+ try :
1251
+ callback , args = self ._callbacks .pop (pid )
1252
+ except KeyError : # pragma: no cover
1253
+ # May happen if .remove_child_handler() is called
1254
+ # after os.waitpid() returns.
1255
+ logger .warning ("Child watcher got an unexpected pid: %r" ,
1256
+ pid , exc_info = True )
1257
+ else :
1258
+ if debug_log and self ._loop .get_debug ():
1259
+ logger .debug ('process %s exited with returncode %s' ,
1260
+ expected_pid , returncode )
1261
+ self ._loop .call_soon (callback , pid , returncode , * args )
1262
+
1263
+ def do_waitpid_all (self ):
1264
+ try :
1265
+ for pid in list (self ._callbacks ):
1266
+ self ._do_waitpid (pid )
1267
+ except (SystemExit , KeyboardInterrupt ):
1268
+ raise
1269
+ except BaseException :
1270
+ # self._loop should always be available here
1271
+ # as we are only called via loop.call_soon_threadsafe()
1272
+ self ._loop .call_exception_handler ({
1273
+ 'message' : 'Unknown exception in SIGCHLD handler' ,
1274
+ 'exception' : exc ,
1275
+ })
1276
+
1277
+
1205
1278
class MultiLoopChildWatcher (AbstractChildWatcher ):
1206
1279
"""A watcher that doesn't require running loop in the main thread.
1207
1280
@@ -1222,14 +1295,14 @@ class MultiLoopChildWatcher(AbstractChildWatcher):
1222
1295
# but retrieves the current loop by get_running_loop()
1223
1296
1224
1297
def __init__ (self ):
1225
- self ._callbacks = {}
1298
+ self ._loops = {} # event loop -> _LoopChildWatcher
1226
1299
self ._saved_sighandler = None
1227
1300
1228
1301
def is_active (self ):
1229
1302
return self ._saved_sighandler is not None
1230
1303
1231
1304
def close (self ):
1232
- self ._callbacks .clear ()
1305
+ self ._loops .clear ()
1233
1306
if self ._saved_sighandler is not None :
1234
1307
handler = signal .getsignal (signal .SIGCHLD )
1235
1308
if handler != self ._sig_chld :
@@ -1246,17 +1319,18 @@ def __exit__(self, exc_type, exc_val, exc_tb):
1246
1319
1247
1320
def add_child_handler (self , pid , callback , * args ):
1248
1321
loop = events .get_running_loop ()
1249
- self . _callbacks [ pid ] = ( loop , callback , args )
1250
-
1251
- # Prevent a race condition in case the child is already terminated.
1252
- self . _do_waitpid (pid )
1322
+ if not loop in self . _loops :
1323
+ self . _loops [ loop ] = _LoopChildWatcher ( loop )
1324
+ watcher = self . _loops [ loop ]
1325
+ watcher . add_child_handler (pid , callback , * args )
1253
1326
1254
1327
def remove_child_handler (self , pid ):
1255
- try :
1256
- del self ._callbacks [pid ]
1257
- return True
1258
- except KeyError :
1328
+ if not loop in self ._loops :
1259
1329
return False
1330
+ watcher = self ._loops [loop ]
1331
+ ret = watcher .remove_child_handler (pid )
1332
+ if watcher .empty ():
1333
+ del self ._loops [loop ]
1260
1334
1261
1335
def attach_loop (self , loop ):
1262
1336
# Don't save the loop but initialize itself if called first time
@@ -1273,54 +1347,13 @@ def attach_loop(self, loop):
1273
1347
# Set SA_RESTART to limit EINTR occurrences.
1274
1348
signal .siginterrupt (signal .SIGCHLD , False )
1275
1349
1276
- def _do_waitpid_all (self ):
1277
- for pid in list (self ._callbacks ):
1278
- self ._do_waitpid (pid )
1279
-
1280
- def _do_waitpid (self , expected_pid ):
1281
- assert expected_pid > 0
1282
-
1283
- try :
1284
- pid , status = os .waitpid (expected_pid , os .WNOHANG )
1285
- except ChildProcessError :
1286
- # The child process is already reaped
1287
- # (may happen if waitpid() is called elsewhere).
1288
- pid = expected_pid
1289
- returncode = 255
1290
- logger .warning (
1291
- "Unknown child process pid %d, will report returncode 255" ,
1292
- pid )
1293
- debug_log = False
1294
- else :
1295
- if pid == 0 :
1296
- # The child process is still alive.
1297
- return
1298
-
1299
- returncode = _compute_returncode (status )
1300
- debug_log = True
1301
- try :
1302
- loop , callback , args = self ._callbacks .pop (pid )
1303
- except KeyError : # pragma: no cover
1304
- # May happen if .remove_child_handler() is called
1305
- # after os.waitpid() returns.
1306
- logger .warning ("Child watcher got an unexpected pid: %r" ,
1307
- pid , exc_info = True )
1308
- else :
1350
+ def _sig_chld (self , signum , frame ):
1351
+ for loop , watcher in self ._loops .items ():
1352
+ # TODO - is this good enough? can we do better?
1309
1353
if loop .is_closed ():
1310
- logger .warning ("Loop %r that handles pid %r is closed " , loop , pid )
1354
+ logger .warning ("Loop %r is closed, but it still had running subprocesses " , loop )
1311
1355
else :
1312
- if debug_log and loop .get_debug ():
1313
- logger .debug ('process %s exited with returncode %s' ,
1314
- expected_pid , returncode )
1315
- loop .call_soon_threadsafe (callback , pid , returncode , * args )
1316
-
1317
- def _sig_chld (self , signum , frame ):
1318
- try :
1319
- self ._do_waitpid_all ()
1320
- except (SystemExit , KeyboardInterrupt ):
1321
- raise
1322
- except BaseException :
1323
- logger .warning ('Unknown exception in SIGCHLD handler' , exc_info = True )
1356
+ loop .call_soon_threadsafe (watcher .do_waitpid_all )
1324
1357
1325
1358
1326
1359
class ThreadedChildWatcher (AbstractChildWatcher ):
0 commit comments