Most of the current answers to this question suggest spinning up one thread per process just to wait for that callback. That strikes me as needlessly wasteful: A single thread should suffice for all callbacks from all processes created this way.
Another answer suggests using signals, but that creates a race condition where the signal handler might get called again before the previous call finished. On Linux, signalfd(2)
could help with that but it's not supported by Python (although it's easy enough to add via ctypes
).
The alternative used by asyncio
in Python is to use signal.set_wakeup_fd
. However, there is another solution based on the fact that the OS will close all open fds on process exit:
import os
import select
import subprocess
import threading
import weakref
def _close_and_join(fd, thread):
os.close(fd)
thread.join()
def _run_poll_callbacks(quitfd, poll, callbacks):
poll.register(quitfd, select.POLLHUP)
while True:
for fd, event in poll.poll(1000.0):
poll.unregister(fd)
if fd == quitfd:
return
callback = callbacks.pop(fd)
if callback is not None:
callback()
class PollProcs:
def __init__(self):
self.poll = select.poll()
self.callbacks = {}
self.closed = False
r, w = os.pipe()
self.thread = threading.Thread(
target=_run_poll_callbacks, args=(r, self.poll, self.callbacks)
)
self.thread.start()
self.finalizer = weakref.finalize(self, _close_and_join, w, self.thread)
def run(self, cmd, callback=None):
if self.closed:
return
r, w = os.pipe()
self.callbacks[r] = callback
self.poll.register(r, select.POLLHUP)
popen = subprocess.Popen(cmd, pass_fds=(w,))
os.close(w)
print("running", " ".join(cmd), "as", popen.pid)
return popen
def main():
procs = PollProcs()
for i in range(3, 0, -1):
procs.run(["sleep", str(i)], callback=lambda i=i: print(f"sleep {i} done?"))
import time
print("Waiting...")
time.sleep(3)
if __name__ == "__main__":
main()
If supporting MacOS isn't a requirement select.epoll
is likely a better choice as it allows updating ongoing polling.