base#

Generic external-app subprocess supervisor (Qt-free, stdlib only).

Launches an arbitrary command, streams its stdout, fires on_ready when a handshake token appears, and on_finished(exit_code) when the process exits. terminate() does a graceful terminate then a kill fallback.

Whole-tree teardown. Stimulus apps spawn their own helper processes (PsychoPy launches a separate iohub process that binds a UDP port; Panda3D may fork workers). Killing only the launched parent orphans those grandchildren, which then linger and hold their sockets – so the next launch fails with “only one usage of each socket address”. To prevent that, the child is launched into a kill-on-close container and the whole tree is reaped:

  • Windows – a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. Every descendant joins the job; TerminateJobObject (or simply closing the last handle, e.g. when mesofield itself exits) kills the entire tree, even after the parent has already died on its own (a crash).

  • POSIX – a new session/process group (start_new_session); the group is signalled with killpg so descendants die with the leader.

The tree is also reaped the instant the parent exits (see _read_stdout()), so a stimulus that crashes on startup cannot leave a hung helper behind.

This is the reusable engine behind mesofield.devices.stimulus_base .SubprocessStimulusDevice – it is deliberately framework-agnostic (no Qt, no mesofield config) so it can babysit any external stimulus app (MousePortal, PsychoPy, …). See mesofield/devices/mouseportal_device.py for a concrete subclass-driven user.

class mesofield.devices.subprocesses.base.SubprocessSupervisor[source]#

Bases: object

Launch and supervise an external subprocess with a stdout handshake.

Parameters#

command:

Full argv to launch (e.g. [python_exe, "-m", "mouseportal", ...]).

ready_token:

Substring printed by the child on stdout once it is ready. When seen, on_ready fires and wait_ready() unblocks.

cwd:

Working directory for the child (so its relative asset paths resolve).

env:

Optional environment mapping; None inherits the parent’s.

on_ready / on_finished:

Callbacks fired (from the reader thread) on the readiness handshake and on process exit, respectively. on_finished receives the exit code.

name:

Short label used in log lines and the reader thread name.

__init__(command, *, ready_token, cwd=None, env=None, on_ready=None, on_finished=None, name='subprocess')[source]#
Parameters:
Return type:

None

start()[source]#

Launch the subprocess (in its own group/job) and stream its stdout.

Return type:

None

wait_ready(timeout=None)[source]#

Block until the readiness handshake fires (or timeout).

Parameters:

timeout (float | None)

Return type:

bool

property pid: int | None#

PID of the launched parent process (None before launch).

property output_tail: str#

The child’s last ~4000 chars of merged stdout/stderr.

terminate(timeout=5.0)[source]#

Stop the subprocess and its whole tree.

Graceful terminate on the parent first, kill as a fallback, then an unconditional tree reap (Job Object / process group) so descendants the app spawned – e.g. PsychoPy’s iohub helper – never linger, even when the parent has already exited on its own.

Parameters:

timeout (float)

Return type:

None