async def test_Process_object_wait_for_pid(): async def return7(): return 7 with trio.fail_after(2): async with open_in_process(return7) as proc: await proc.wait_pid() assert isinstance(proc.pid, int)
async def test_Process_object_wait_for_returncode(): async def system_exit_123(): raise SystemExit(123) with trio.fail_after(2): async with open_in_process(system_exit_123) as proc: await proc.wait_returncode() assert proc.returncode == 123
async def test_Process_object_wait_for_error(): async def raise_error(): raise ValueError("child-error") with trio.fail_after(2): async with open_in_process(raise_error) as proc: await proc.wait_error() assert isinstance(proc.error, ValueError)
async def test_Process_object_wait_for_result_when_error(): async def raise_error(): raise ValueError("child-error") with trio.fail_after(2): async with open_in_process(raise_error) as proc: with pytest.raises(ValueError, match="child-error"): await proc.wait_result_or_raise()
async def test_Process_object_wait_for_return_value(): async def return7(): return 7 with trio.fail_after(2): async with open_in_process(return7) as proc: await proc.wait_return_value() assert proc.return_value == 7
async def test_open_proc_invalid_function_call(): async def takes_no_args(): pass with trio.fail_after(2): async with open_in_process(takes_no_args, 1, 2, 3) as proc: pass assert proc.returncode == 1 assert isinstance(proc.error, TypeError)
async def test_Process_object_wait_for_result_when_return_value(): async def return7(): return 7 with trio.fail_after(2): async with open_in_process(return7) as proc: result = await proc.wait_result_or_raise() assert result == 7 assert proc.error is None
async def test_timeout_waiting_for_pid(monkeypatch): async def wait_pid(self): await trio.sleep(constants.STARTUP_TIMEOUT_SECONDS + 0.1) monkeypatch.setattr(Process, "wait_pid", wait_pid) monkeypatch.setattr(constants, "STARTUP_TIMEOUT_SECONDS", 1) with pytest.raises(trio.TooSlowError): async with open_in_process(trio.sleep_forever): pass
async def test_open_in_proc_termination_while_running(): async def do_sleep_forever(): import trio await trio.sleep_forever() with trio.fail_after(2): async with open_in_process(do_sleep_forever) as proc: proc.terminate() assert proc.returncode == 15
async def test_open_proc_unpickleable_params(touch_path): async def takes_open_file(f): pass with trio.fail_after(2): with pytest.raises(pickle.PickleError): with open(touch_path, "w") as touch_file: async with open_in_process(takes_open_file, touch_file): # this code block shouldn't get executed assert False
async def test_open_proc_interrupt_while_running(): async def monitor_for_interrupt(): import trio await trio.sleep_forever() with trio.fail_after(2): async with open_in_process(monitor_for_interrupt) as proc: proc.send_signal(signal.SIGINT) assert proc.returncode == 2
async def test_Process_object_state_api(): async def return7(): return 7 with trio.fail_after(2): async with open_in_process(return7) as proc: assert proc.state.is_on_or_after(State.STARTED) await proc.wait_for_state(State.FINISHED) assert proc.state is State.FINISHED assert proc.return_value == 7
async def test_open_proc_outer_KeyboardInterrupt(): async def sleep_forever(): import trio await trio.sleep_forever() with trio.fail_after(2): with pytest.raises(KeyboardInterrupt): async with open_in_process(sleep_forever) as proc: raise KeyboardInterrupt assert proc.returncode == 2
async def test_timeout_waiting_for_executing_state(monkeypatch): async def wait_for_state(self, state): if state is State.EXECUTING: await trio.sleep(constants.STARTUP_TIMEOUT_SECONDS + 0.1) monkeypatch.setattr(Process, "wait_for_state", wait_for_state) monkeypatch.setattr(constants, "STARTUP_TIMEOUT_SECONDS", 1) with pytest.raises(trio.TooSlowError): async with open_in_process(trio.sleep_forever): pass
async def test_open_in_proc_kill_while_running(): async def do_sleep_forever(): import trio await trio.sleep_forever() with trio.fail_after(2): async with open_in_process(do_sleep_forever) as proc: proc.kill() assert proc.returncode == -9 assert isinstance(proc.error, ProcessKilled)
async def test_unpickleable_exc(): sleep = trio.sleep # Custom exception classes requiring multiple arguments cannot be pickled: # https://bugs.python.org/issue32696 class CustomException(BaseException): def __init__(self, msg, arg2): super().__init__(msg) self.arg2 = arg2 async def raise_(): await sleep(0.01) raise CustomException("msg", "arg2") with trio.fail_after(2): with pytest.raises(InvalidDataFromChild): async with open_in_process(raise_) as proc: await proc.wait_result_or_raise()
async def test_proc_ignores_KeyboardInterrupt(monkeypatch): # If we get a KeyboardInterrupt and the child process does not terminate after being sent a # SIGINT, we send a SIGKILL to avoid open_in_process() from hanging indefinitely. async def sleep_forever(): import trio while True: try: await trio.lowlevel.checkpoint() except KeyboardInterrupt: pass monkeypatch.setattr(constants, "SIGINT_TIMEOUT_SECONDS", 0.2) with trio.fail_after(constants.SIGINT_TIMEOUT_SECONDS + 1): with pytest.raises(KeyboardInterrupt): async with open_in_process(sleep_forever) as proc: raise KeyboardInterrupt assert proc.returncode == -9 assert isinstance(proc.error, ProcessKilled)
async def test_open_proc_interrupt_while_running(): with trio.fail_after(2): async with open_in_process(trio.sleep_forever) as proc: proc.send_signal(signal.SIGINT) assert proc.returncode == 2
async def new_proc( name: str, actor_nursery: 'ActorNursery', # type: ignore subactor: Actor, errors: Dict[Tuple[str, str], Exception], # passed through to actor main bind_addr: Tuple[str, int], parent_addr: Tuple[str, int], use_trio_run_in_process: bool = False, task_status: TaskStatus[Portal] = trio.TASK_STATUS_IGNORED) -> None: """Create a new ``multiprocessing.Process`` using the spawn method as configured using ``try_set_start_method()``. """ cancel_scope = None # mark the new actor with the global spawn method subactor._spawn_method = _spawn_method async with trio.open_nursery() as nursery: if use_trio_run_in_process or _spawn_method == 'trio_run_in_process': # trio_run_in_process async with trio_run_in_process.open_in_process( subactor._trip_main, bind_addr, parent_addr, ) as proc: log.info(f"Started {proc}") # wait for actor to spawn and connect back to us # channel should have handshake completed by the # local actor by the time we get a ref to it event, chan = await actor_nursery._actor.wait_for_peer( subactor.uid) portal = Portal(chan) actor_nursery._children[subactor.uid] = (subactor, proc, portal) task_status.started(portal) # wait for ActorNursery.wait() to be called await actor_nursery._join_procs.wait() if portal in actor_nursery._cancel_after_result_on_exit: cancel_scope = await nursery.start(cancel_on_completion, portal, subactor, errors) # TRIP blocks here until process is complete else: # `multiprocessing` assert _ctx start_method = _ctx.get_start_method() if start_method == 'forkserver': # XXX do our hackery on the stdlib to avoid multiple # forkservers (one at each subproc layer). fs = forkserver._forkserver curr_actor = current_actor() if is_main_process() and not curr_actor._forkserver_info: # if we're the "main" process start the forkserver # only once and pass its ipc info to downstream # children # forkserver.set_forkserver_preload(rpc_module_paths) forkserver.ensure_running() fs_info = ( fs._forkserver_address, fs._forkserver_alive_fd, getattr(fs, '_forkserver_pid', None), getattr(resource_tracker._resource_tracker, '_pid', None), resource_tracker._resource_tracker._fd, ) else: assert curr_actor._forkserver_info fs_info = ( fs._forkserver_address, fs._forkserver_alive_fd, fs._forkserver_pid, resource_tracker._resource_tracker._pid, resource_tracker._resource_tracker._fd, ) = curr_actor._forkserver_info else: fs_info = (None, None, None, None, None) proc = _ctx.Process( # type: ignore target=subactor._mp_main, args=(bind_addr, fs_info, start_method, parent_addr), # daemon=True, name=name, ) # `multiprocessing` only (since no async interface): # register the process before start in case we get a cancel # request before the actor has fully spawned - then we can wait # for it to fully come up before sending a cancel request actor_nursery._children[subactor.uid] = (subactor, proc, None) proc.start() if not proc.is_alive(): raise ActorFailure("Couldn't start sub-actor?") log.info(f"Started {proc}") # wait for actor to spawn and connect back to us # channel should have handshake completed by the # local actor by the time we get a ref to it event, chan = await actor_nursery._actor.wait_for_peer(subactor.uid ) portal = Portal(chan) actor_nursery._children[subactor.uid] = (subactor, proc, portal) # unblock parent task task_status.started(portal) # wait for ``ActorNursery`` block to signal that # subprocesses can be waited upon. # This is required to ensure synchronization # with user code that may want to manually await results # from nursery spawned sub-actors. We don't want the # containing nurseries here to collect results or error # while user code is still doing it's thing. Only after the # nursery block closes do we allow subactor results to be # awaited and reported upwards to the supervisor. await actor_nursery._join_procs.wait() if portal in actor_nursery._cancel_after_result_on_exit: cancel_scope = await nursery.start(cancel_on_completion, portal, subactor, errors) # TODO: timeout block here? if proc.is_alive(): await proc_waiter(proc) proc.join() log.debug(f"Joined {proc}") # pop child entry to indicate we are no longer managing this subactor subactor, proc, portal = actor_nursery._children.pop(subactor.uid) # cancel result waiter that may have been spawned in # tandem if not done already if cancel_scope: log.warning( f"Cancelling existing result waiter task for {subactor.uid}") cancel_scope.cancel()