9.1 KiB
Swactor Managed Process Specification
Status: current contract for swactor-process.
swactor-process provides a Swactor actor interface for launching, supervising,
stopping, and observing one operating-system child process per process actor.
The crate owns process lifecycle/control only. Child stdin/stdout/stderr are not managed or observed by this crate.
1. Public API
The crate exports:
ProcessSpec
ProcessLifecycleObservability
ProcessOutputConfig
ProcessCommand
ProcessOutput
ExitStatus
spawn_local_process
send_process_command
Pipeline and YAML exports are separate crate features and are not part of the managed-process protocol described here.
1.1 ProcessSpec
ProcessSpec {
command: String,
args: Vec<String>,
env: HashMap<String, String>,
working_dir: Option<PathBuf>,
label: Option<String>,
}
command is passed directly to std::process::Command::new.
args are passed as direct argv entries. The crate does not split, quote,
unquote, expand, or shell-parse argument strings.
env contains child environment overrides.
working_dir, when present, is passed as the child current working directory.
label, when present, is the lifecycle datastream label source. When label is
absent, the label source is the basename of command.
1.2 ProcessOutputConfig
ProcessOutputConfig::disabled(upstream: ActorAddress) -> ProcessOutputConfig
ProcessOutputConfig::datastream_mirror(
upstream: ActorAddress,
producer: DatastreamProducer,
) -> ProcessOutputConfig
ProcessOutputConfig::upstream(&self) -> ActorAddress
ProcessOutputConfig::observability(&self) -> ProcessLifecycleObservability
upstream is the actor address that receives every public ProcessOutput.
disabled sends only upstream ProcessOutput.
datastream_mirror sends upstream ProcessOutput and also mirrors each
lifecycle/control output to one datastream channel.
1.3 ProcessLifecycleObservability
ProcessLifecycleObservability::Disabled
ProcessLifecycleObservability::DatastreamMirror
This setting controls lifecycle/control mirroring only. It does not enable child stdin/stdout/stderr handling.
1.4 ProcessCommand
ProcessCommand::Stop {
kill_after: Option<Duration>,
}
Stop asks the supervisor to terminate the child process. kill_after, when
present, is the grace duration before kill escalation.
1.5 ProcessOutput
ProcessOutput::Started { pid: u32 }
ProcessOutput::SpawnFailed { error: String }
ProcessOutput::Exited { status: ExitStatus }
ProcessOutput::Error { error: String }
Started means the OS child spawned and pid is the child process id.
SpawnFailed means the process actor was created but the OS child did not spawn.
Exited means the OS child reached a terminal status.
Error means the process supervisor or process actor hit an operational failure
other than OS spawn failure.
1.6 ExitStatus
ExitStatus::Code(i32)
ExitStatus::Signal(i32)
ExitStatus::Unknown
1.7 Spawn and command helpers
spawn_local_process(
ctx: &Ctx,
sender: &ExternalSender,
spec: ProcessSpec,
output: ProcessOutputConfig,
) -> Result<ActorAddress, Error>
spawn_local_process validates lifecycle output configuration, creates one
process actor, wires its private supervisor wake path, and returns the process
actor address.
Success from spawn_local_process means the process actor was created. It does
not mean the OS child spawned successfully. OS spawn success or failure is
reported later as ProcessOutput.
send_process_command(
sender: &ExternalSender,
process: ActorAddress,
command: ProcessCommand,
) -> Result<(), Error>
send_process_command is the public helper for sending process commands. It
wraps the public ProcessCommand in the actor's private mailbox type.
2. Runtime topology
One process actor owns one OS child process lifecycle.
The actor owns:
- lifecycle state;
- the configured upstream output address;
- optional lifecycle datastream mirror state;
- a private supervisor thread handle.
The private supervisor thread owns:
- the child process handle and pid;
- blocking-prone child exit polling;
- terminate/kill signal delivery;
- the stop kill deadline.
The child process owns its own execution.
The supervisor thread is not a public actor and not a public extension point.
3. Child spawn behavior
The supervisor starts the child with:
Command::new(&spec.command)
cmd.args(&spec.args)
cmd.env(key, value) for each spec.env entry
cmd.current_dir(dir) when spec.working_dir is Some(dir)
cmd.stdin(Stdio::null())
cmd.stdout(Stdio::null())
cmd.stderr(Stdio::null())
cmd.spawn()
The crate does not invoke a shell unless the caller explicitly sets command to
a shell executable and supplies shell arguments.
Child stdin/stdout/stderr are connected to null handles. The managed-process protocol does not expose stdin writes, stdout/stderr output events, PTY resize, or arbitrary signal commands.
If cmd.spawn() fails, the actor emits exactly one terminal
ProcessOutput::SpawnFailed { error } and does not emit Started, Exited, or
Error for that spawn failure.
4. Lifecycle output delivery
The actor sends each public ProcessOutput to the configured upstream actor.
When ProcessOutputConfig::datastream_mirror is used, the actor also mirrors
each output to the configured datastream producer. Datastream submit failure is
ignored and does not suppress upstream output or emit ProcessOutput::Error.
Public lifecycle/control output order follows observed lifecycle:
- successful spawn emits
Startedbefore any terminalExited; - spawn failure emits
SpawnFailedwithoutStartedorExited; - supervisor failure emits
Error; - after a terminal output, later public stop attempts emit no additional
ProcessOutput.
5. Lifecycle datastream labels and records
The label source is:
ProcessSpec::label, when present;- otherwise, the final non-empty path segment of
ProcessSpec::command; - otherwise, the full
commandstring.
The label sanitizer:
- trims source whitespace;
- lowercases ASCII alphanumeric characters;
- preserves
_and-; - replaces every other character with
_; - collapses repeated
_; - trims leading and trailing
_; - rejects an empty sanitized result with:
invalid process lifecycle label: empty segment
The lifecycle channel name is:
proc.<label>.lifecycle
When lifecycle mirroring is enabled, the channel is registered as:
ChannelContent::JsonRecord {
schema: Some("swactor_process.lifecycle.v1")
}
Duplicate lifecycle labels on the same datastream stream are rejected before the process actor is spawned with an error containing:
duplicate process lifecycle datastream channel: proc.<label>.lifecycle on stream <stream>
Lifecycle JSON records are:
{"event":"started","pid":123}
{"event":"spawn_failed","error":"..."}
{"event":"exited","status":{"kind":"code","value":0}}
{"event":"exited","status":{"kind":"signal","value":9}}
{"event":"exited","status":{"kind":"unknown"}}
{"event":"error","error":"..."}
The managed-process core registers no proc.<label>.stdout or
proc.<label>.stderr channels.
6. Stop semantics
Use send_process_command with ProcessCommand::Stop { kill_after } to request
shutdown.
If stop is requested before the child reports successful spawn, the actor queues
the stop intent. When the child later starts, the actor still emits Started
first, asks the supervisor to terminate the child, and later emits terminal
Exited unless a supervisor failure occurs.
If stop is requested before an OS spawn failure is reported, the actor still
emits only SpawnFailed for that failed spawn.
If the child is running, stop sends terminate to the child process.
If kill_after is Some(duration), the supervisor sends kill after that
deadline if the child has not exited.
If kill_after is None, the supervisor does not schedule kill escalation.
Duplicate stop while already stopping is a no-op. It does not tighten, extend, or replace the original kill deadline.
Stop after terminal output is a no-op if the actor is still alive. If the actor has already stopped, sending the command may fail at the runtime address layer; that failure does not produce process output.
7. Actor and supervisor cleanup
The actor uses a private mailbox wrapper so supervisor wake messages and public process commands share one actor input type without changing the Swactor runtime.
The actor drains supervisor events when it starts, when it receives a supervisor wake, and before/after applying a public stop command.
The supervisor sends a private ThreadFinished event when its thread reaches the
end of process supervision. The actor stops itself only after a terminal state is
recorded and the supervisor handle has been cleared.
If the actor stops while the supervisor is still present, it sends best-effort shutdown to the supervisor without blocking for a waiting join.
Dropping the supervisor handle sends best-effort shutdown and joins only when the thread is already finished.