2026-08-23 21:28:38 +00:00
|
|
|
use std::io::{self, Read};
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
use std::mem;
|
|
|
|
|
use std::os::fd::RawFd;
|
|
|
|
|
use std::process::{Child, Command, Stdio};
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use std::thread::{self, JoinHandle};
|
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
|
|
|
|
|
|
use crossbeam_queue::SegQueue;
|
|
|
|
|
|
|
|
|
|
use crate::types::{ExitStatus, ProcessSpec, Signal};
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub(crate) enum ThreadCommand {
|
|
|
|
|
Stop { kill_after: Option<Duration> },
|
|
|
|
|
ShutdownNow,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub(crate) enum ThreadEvent {
|
|
|
|
|
Started { pid: u32 },
|
|
|
|
|
SpawnFailed { error: String },
|
|
|
|
|
Exited { status: ExitStatus },
|
2026-08-23 21:28:38 +00:00
|
|
|
Output { stderr: bool, bytes: Vec<u8> },
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
Error { error: String },
|
|
|
|
|
ThreadFinished,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub(crate) struct ThreadEventSink {
|
|
|
|
|
queue: Arc<SegQueue<ThreadEvent>>,
|
|
|
|
|
wake: Arc<dyn Fn() + Send + Sync>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) struct ThreadEventReceiver {
|
|
|
|
|
queue: Arc<SegQueue<ThreadEvent>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn thread_event_channel(
|
|
|
|
|
wake: impl Fn() + Send + Sync + 'static,
|
|
|
|
|
) -> (ThreadEventSink, ThreadEventReceiver) {
|
|
|
|
|
let queue = Arc::new(SegQueue::new());
|
|
|
|
|
(
|
|
|
|
|
ThreadEventSink {
|
|
|
|
|
queue: queue.clone(),
|
|
|
|
|
wake: Arc::new(wake),
|
|
|
|
|
},
|
|
|
|
|
ThreadEventReceiver { queue },
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ThreadEventSink {
|
|
|
|
|
pub(crate) fn push(&self, event: ThreadEvent) {
|
|
|
|
|
self.queue.push(event);
|
|
|
|
|
(self.wake)();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ThreadEventReceiver {
|
|
|
|
|
pub(crate) fn drain(&self) -> Vec<ThreadEvent> {
|
|
|
|
|
let mut events = Vec::new();
|
|
|
|
|
while let Some(event) = self.queue.pop() {
|
|
|
|
|
events.push(event);
|
|
|
|
|
}
|
|
|
|
|
events
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
struct WakeFd(Arc<WakeFdInner>);
|
|
|
|
|
|
|
|
|
|
struct WakeFdInner {
|
|
|
|
|
fd: RawFd,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl WakeFd {
|
|
|
|
|
fn new() -> io::Result<Self> {
|
|
|
|
|
let fd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) };
|
|
|
|
|
if fd < 0 {
|
|
|
|
|
return Err(io::Error::last_os_error());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(Self(Arc::new(WakeFdInner { fd })))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn fd(&self) -> RawFd {
|
|
|
|
|
self.0.fd
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn wake(&self) -> io::Result<()> {
|
|
|
|
|
let value: u64 = 1;
|
|
|
|
|
let ptr = (&value as *const u64).cast::<libc::c_void>();
|
|
|
|
|
let len = mem::size_of::<u64>();
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
let written = unsafe { libc::write(self.fd(), ptr, len) };
|
|
|
|
|
if written == len as libc::ssize_t {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
if written < 0 {
|
|
|
|
|
let err = io::Error::last_os_error();
|
|
|
|
|
if err.kind() == io::ErrorKind::Interrupted {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if is_would_block(&err) {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
return Err(err);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Err(io::Error::new(
|
|
|
|
|
io::ErrorKind::WriteZero,
|
|
|
|
|
"short write to process supervisor wake fd",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn drain(&self) -> io::Result<()> {
|
|
|
|
|
let mut value: u64 = 0;
|
|
|
|
|
let ptr = (&mut value as *mut u64).cast::<libc::c_void>();
|
|
|
|
|
let len = mem::size_of::<u64>();
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
let read = unsafe { libc::read(self.fd(), ptr, len) };
|
|
|
|
|
if read == len as libc::ssize_t {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if read < 0 {
|
|
|
|
|
let err = io::Error::last_os_error();
|
|
|
|
|
if err.kind() == io::ErrorKind::Interrupted {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if is_would_block(&err) {
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
return Err(err);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Err(io::Error::new(
|
|
|
|
|
io::ErrorKind::UnexpectedEof,
|
|
|
|
|
"short read from process supervisor wake fd",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Drop for WakeFdInner {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
let _ = unsafe { libc::close(self.fd) };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn poll_command_wake(wake: &WakeFd, timeout: Duration) -> io::Result<bool> {
|
|
|
|
|
let timeout_ms = poll_timeout_ms(timeout);
|
|
|
|
|
let mut pollfd = libc::pollfd {
|
|
|
|
|
fd: wake.fd(),
|
|
|
|
|
events: libc::POLLIN,
|
|
|
|
|
revents: 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
let result = unsafe { libc::poll(&mut pollfd, 1, timeout_ms) };
|
|
|
|
|
if result == 0 {
|
|
|
|
|
return Ok(false);
|
|
|
|
|
}
|
|
|
|
|
if result > 0 {
|
|
|
|
|
let revents = pollfd.revents;
|
|
|
|
|
if revents & (libc::POLLERR | libc::POLLNVAL | libc::POLLHUP) != 0 {
|
|
|
|
|
return Err(io::Error::other(format!(
|
|
|
|
|
"process supervisor wake fd poll failed: revents={revents}"
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
return Ok(revents & libc::POLLIN != 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let err = io::Error::last_os_error();
|
|
|
|
|
if err.kind() == io::ErrorKind::Interrupted {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
return Err(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn poll_timeout_ms(timeout: Duration) -> libc::c_int {
|
|
|
|
|
if timeout.is_zero() {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let millis = timeout.as_millis();
|
|
|
|
|
if millis == 0 {
|
|
|
|
|
1
|
|
|
|
|
} else {
|
|
|
|
|
millis.min(libc::c_int::MAX as u128) as libc::c_int
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_would_block(err: &io::Error) -> bool {
|
|
|
|
|
matches!(
|
|
|
|
|
err.raw_os_error(),
|
|
|
|
|
Some(code) if code == libc::EAGAIN || code == libc::EWOULDBLOCK
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) struct ProcessThreadHandle {
|
|
|
|
|
commands: Arc<SegQueue<ThreadCommand>>,
|
|
|
|
|
wake: WakeFd,
|
|
|
|
|
join: Option<JoinHandle<()>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) struct ProcessSupervisorThread;
|
|
|
|
|
|
|
|
|
|
impl ProcessSupervisorThread {
|
|
|
|
|
pub(crate) fn start(
|
|
|
|
|
spec: ProcessSpec,
|
|
|
|
|
events: ThreadEventSink,
|
|
|
|
|
) -> Result<ProcessThreadHandle, swactor::Error> {
|
|
|
|
|
let commands = Arc::new(SegQueue::new());
|
|
|
|
|
let wake = WakeFd::new().map_err(|err| {
|
|
|
|
|
swactor::Error::from(format!(
|
|
|
|
|
"failed to create process supervisor wake fd: {err}"
|
|
|
|
|
))
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
let thread_commands = commands.clone();
|
|
|
|
|
let thread_wake = wake.clone();
|
|
|
|
|
let join = thread::Builder::new()
|
|
|
|
|
.name("swactor-process-supervisor".to_owned())
|
|
|
|
|
.spawn(move || supervisor_thread_main(spec, events, thread_commands, thread_wake))
|
|
|
|
|
.map_err(|err| {
|
|
|
|
|
swactor::Error::from(format!("failed to start process supervisor thread: {err}"))
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
Ok(ProcessThreadHandle {
|
|
|
|
|
commands,
|
|
|
|
|
wake,
|
|
|
|
|
join: Some(join),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ProcessThreadHandle {
|
|
|
|
|
pub(crate) fn send(&self, command: ThreadCommand) -> Result<(), String> {
|
|
|
|
|
self.commands.push(command);
|
|
|
|
|
self.wake
|
|
|
|
|
.wake()
|
|
|
|
|
.map_err(|err| format!("failed to wake process supervisor: {err}"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn stop(&self, kill_after: Option<Duration>) -> Result<(), String> {
|
|
|
|
|
self.send(ThreadCommand::Stop { kill_after })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn shutdown_now(&self) -> Result<(), String> {
|
|
|
|
|
self.send(ThreadCommand::ShutdownNow)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn is_finished(&self) -> bool {
|
|
|
|
|
match &self.join {
|
|
|
|
|
Some(join) => join.is_finished(),
|
|
|
|
|
None => true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn join_if_finished(&mut self) -> Result<bool, String> {
|
|
|
|
|
if !self.is_finished() {
|
|
|
|
|
return Ok(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(join) = self.join.take() {
|
|
|
|
|
join.join()
|
|
|
|
|
.map_err(|_| "process supervisor thread panicked".to_owned())?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Drop for ProcessThreadHandle {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
let _ = self.shutdown_now();
|
|
|
|
|
let _ = self.join_if_finished();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const WAITPID_POLL_INTERVAL: Duration = Duration::from_millis(10);
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
enum SupervisorState {
|
|
|
|
|
Spawning,
|
|
|
|
|
Running,
|
|
|
|
|
Stopping,
|
|
|
|
|
Done,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum CommandOutcome {
|
|
|
|
|
Continue,
|
|
|
|
|
Exited(ExitStatus),
|
|
|
|
|
Error(String),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn supervisor_thread_main(
|
|
|
|
|
spec: ProcessSpec,
|
|
|
|
|
events: ThreadEventSink,
|
|
|
|
|
commands: Arc<SegQueue<ThreadCommand>>,
|
|
|
|
|
wake: WakeFd,
|
|
|
|
|
) {
|
|
|
|
|
let mut state = SupervisorState::Spawning;
|
|
|
|
|
let mut child: Option<Child>;
|
|
|
|
|
let pid: Option<u32>;
|
|
|
|
|
let mut kill_deadline: Option<Instant> = None;
|
|
|
|
|
let mut kill_sent = false;
|
2026-08-23 21:28:38 +00:00
|
|
|
let mut output_threads = Vec::new();
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
debug_assert!(matches!(state, SupervisorState::Spawning));
|
|
|
|
|
|
|
|
|
|
let mut cmd = Command::new(&spec.command);
|
|
|
|
|
cmd.args(&spec.args);
|
|
|
|
|
for (key, value) in &spec.env {
|
|
|
|
|
cmd.env(key, value);
|
|
|
|
|
}
|
|
|
|
|
if let Some(dir) = &spec.working_dir {
|
|
|
|
|
cmd.current_dir(dir);
|
|
|
|
|
}
|
|
|
|
|
cmd.stdin(Stdio::null())
|
2026-08-23 21:28:38 +00:00
|
|
|
.stdout(Stdio::piped())
|
|
|
|
|
.stderr(Stdio::piped());
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
|
|
|
|
|
match cmd.spawn() {
|
2026-08-23 21:28:38 +00:00
|
|
|
Ok(mut spawned_child) => {
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
let child_pid = spawned_child.id();
|
2026-08-23 21:28:38 +00:00
|
|
|
if let Some(stdout) = spawned_child.stdout.take() {
|
|
|
|
|
output_threads.push(spawn_output_reader(stdout, events.clone(), false));
|
|
|
|
|
}
|
|
|
|
|
if let Some(stderr) = spawned_child.stderr.take() {
|
|
|
|
|
output_threads.push(spawn_output_reader(stderr, events.clone(), true));
|
|
|
|
|
}
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
pid = Some(child_pid);
|
|
|
|
|
child = Some(spawned_child);
|
|
|
|
|
events.push(ThreadEvent::Started { pid: child_pid });
|
|
|
|
|
state = SupervisorState::Running;
|
|
|
|
|
|
|
|
|
|
match drain_queued_commands(
|
|
|
|
|
child_pid,
|
|
|
|
|
&commands,
|
|
|
|
|
&mut state,
|
|
|
|
|
&mut kill_deadline,
|
|
|
|
|
&mut kill_sent,
|
|
|
|
|
) {
|
|
|
|
|
CommandOutcome::Continue => {}
|
|
|
|
|
CommandOutcome::Exited(status) => {
|
2026-08-23 21:28:38 +00:00
|
|
|
finish_with_exit(&events, &mut state, &mut child, &mut output_threads, status);
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
CommandOutcome::Error(error) => {
|
2026-08-23 21:28:38 +00:00
|
|
|
finish_with_error(&events, &mut state, &mut child, &mut output_threads, error);
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
events.push(ThreadEvent::SpawnFailed {
|
|
|
|
|
error: err.to_string(),
|
|
|
|
|
});
|
|
|
|
|
events.push(ThreadEvent::ThreadFinished);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let child_pid = pid.expect("supervisor pid stored after successful spawn");
|
|
|
|
|
loop {
|
|
|
|
|
let timeout = command_poll_timeout(kill_deadline);
|
|
|
|
|
match poll_command_wake(&wake, timeout) {
|
|
|
|
|
Ok(true) => {
|
|
|
|
|
if let Err(err) = wake.drain() {
|
|
|
|
|
finish_with_error(
|
|
|
|
|
&events,
|
|
|
|
|
&mut state,
|
|
|
|
|
&mut child,
|
2026-08-23 21:28:38 +00:00
|
|
|
&mut output_threads,
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
format!("process supervisor command wake failed: {err}"),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match drain_queued_commands(
|
|
|
|
|
child_pid,
|
|
|
|
|
&commands,
|
|
|
|
|
&mut state,
|
|
|
|
|
&mut kill_deadline,
|
|
|
|
|
&mut kill_sent,
|
|
|
|
|
) {
|
|
|
|
|
CommandOutcome::Continue => {}
|
|
|
|
|
CommandOutcome::Exited(status) => {
|
2026-08-23 21:28:38 +00:00
|
|
|
finish_with_exit(
|
|
|
|
|
&events,
|
|
|
|
|
&mut state,
|
|
|
|
|
&mut child,
|
|
|
|
|
&mut output_threads,
|
|
|
|
|
status,
|
|
|
|
|
);
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
CommandOutcome::Error(error) => {
|
2026-08-23 21:28:38 +00:00
|
|
|
finish_with_error(
|
|
|
|
|
&events,
|
|
|
|
|
&mut state,
|
|
|
|
|
&mut child,
|
|
|
|
|
&mut output_threads,
|
|
|
|
|
error,
|
|
|
|
|
);
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(false) => {}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
finish_with_error(
|
|
|
|
|
&events,
|
|
|
|
|
&mut state,
|
|
|
|
|
&mut child,
|
2026-08-23 21:28:38 +00:00
|
|
|
&mut output_threads,
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
format!("process supervisor command wake failed: {err}"),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match try_wait_pid(child_pid) {
|
|
|
|
|
Ok(Some(status)) => {
|
2026-08-23 21:28:38 +00:00
|
|
|
finish_with_exit(&events, &mut state, &mut child, &mut output_threads, status);
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
Ok(None) => {}
|
|
|
|
|
Err(error) => {
|
2026-08-23 21:28:38 +00:00
|
|
|
finish_with_error(&events, &mut state, &mut child, &mut output_threads, error);
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if kill_deadline.is_some_and(|deadline| deadline <= Instant::now()) && !kill_sent {
|
|
|
|
|
match send_kill_or_observe_exit(child_pid) {
|
|
|
|
|
CommandOutcome::Continue => {
|
|
|
|
|
kill_sent = true;
|
|
|
|
|
kill_deadline = None;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
CommandOutcome::Exited(status) => {
|
2026-08-23 21:28:38 +00:00
|
|
|
finish_with_exit(&events, &mut state, &mut child, &mut output_threads, status);
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
CommandOutcome::Error(error) => {
|
2026-08-23 21:28:38 +00:00
|
|
|
finish_with_error(&events, &mut state, &mut child, &mut output_threads, error);
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn command_poll_timeout(kill_deadline: Option<Instant>) -> Duration {
|
|
|
|
|
let Some(deadline) = kill_deadline else {
|
|
|
|
|
return WAITPID_POLL_INTERVAL;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let now = Instant::now();
|
|
|
|
|
if deadline <= now {
|
|
|
|
|
Duration::ZERO
|
|
|
|
|
} else {
|
|
|
|
|
(deadline - now).min(WAITPID_POLL_INTERVAL)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn drain_queued_commands(
|
|
|
|
|
pid: u32,
|
|
|
|
|
commands: &SegQueue<ThreadCommand>,
|
|
|
|
|
state: &mut SupervisorState,
|
|
|
|
|
kill_deadline: &mut Option<Instant>,
|
|
|
|
|
kill_sent: &mut bool,
|
|
|
|
|
) -> CommandOutcome {
|
|
|
|
|
while let Some(command) = commands.pop() {
|
|
|
|
|
match apply_thread_command(pid, command, state, kill_deadline, kill_sent) {
|
|
|
|
|
CommandOutcome::Continue => {}
|
|
|
|
|
outcome => return outcome,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CommandOutcome::Continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn apply_thread_command(
|
|
|
|
|
pid: u32,
|
|
|
|
|
command: ThreadCommand,
|
|
|
|
|
state: &mut SupervisorState,
|
|
|
|
|
kill_deadline: &mut Option<Instant>,
|
|
|
|
|
kill_sent: &mut bool,
|
|
|
|
|
) -> CommandOutcome {
|
|
|
|
|
match command {
|
|
|
|
|
ThreadCommand::Stop { kill_after } => match state {
|
|
|
|
|
SupervisorState::Running => match send_terminate_or_observe_exit(pid) {
|
|
|
|
|
CommandOutcome::Continue => {
|
|
|
|
|
if let Some(duration) = kill_after {
|
|
|
|
|
*kill_deadline = Some(Instant::now() + duration);
|
|
|
|
|
} else {
|
|
|
|
|
*kill_deadline = None;
|
|
|
|
|
}
|
|
|
|
|
*state = SupervisorState::Stopping;
|
|
|
|
|
CommandOutcome::Continue
|
|
|
|
|
}
|
|
|
|
|
outcome => outcome,
|
|
|
|
|
},
|
|
|
|
|
SupervisorState::Spawning | SupervisorState::Stopping | SupervisorState::Done => {
|
|
|
|
|
CommandOutcome::Continue
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
ThreadCommand::ShutdownNow => match state {
|
|
|
|
|
SupervisorState::Done => CommandOutcome::Continue,
|
|
|
|
|
SupervisorState::Spawning | SupervisorState::Running | SupervisorState::Stopping => {
|
|
|
|
|
*state = SupervisorState::Stopping;
|
|
|
|
|
*kill_deadline = None;
|
|
|
|
|
match send_terminate_or_observe_exit(pid) {
|
|
|
|
|
CommandOutcome::Continue => match try_wait_pid(pid) {
|
|
|
|
|
Ok(Some(status)) => CommandOutcome::Exited(status),
|
|
|
|
|
Ok(None) => match send_kill_or_observe_exit(pid) {
|
|
|
|
|
CommandOutcome::Continue => {
|
|
|
|
|
*kill_sent = true;
|
|
|
|
|
CommandOutcome::Continue
|
|
|
|
|
}
|
|
|
|
|
outcome => outcome,
|
|
|
|
|
},
|
|
|
|
|
Err(error) => CommandOutcome::Error(error),
|
|
|
|
|
},
|
|
|
|
|
outcome => outcome,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn send_terminate_or_observe_exit(pid: u32) -> CommandOutcome {
|
|
|
|
|
match send_signal(pid, Signal::Terminate) {
|
|
|
|
|
Ok(()) => CommandOutcome::Continue,
|
|
|
|
|
Err(kill_error) => match try_wait_pid(pid) {
|
|
|
|
|
Ok(Some(status)) => CommandOutcome::Exited(status),
|
|
|
|
|
Ok(None) => {
|
|
|
|
|
CommandOutcome::Error(format!("failed to terminate process {pid}: {kill_error}"))
|
|
|
|
|
}
|
|
|
|
|
Err(error) => CommandOutcome::Error(error),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn send_kill_or_observe_exit(pid: u32) -> CommandOutcome {
|
|
|
|
|
match send_signal(pid, Signal::Kill) {
|
|
|
|
|
Ok(()) => CommandOutcome::Continue,
|
|
|
|
|
Err(kill_error) => match try_wait_pid(pid) {
|
|
|
|
|
Ok(Some(status)) => CommandOutcome::Exited(status),
|
|
|
|
|
Ok(None) => {
|
|
|
|
|
CommandOutcome::Error(format!("failed to kill process {pid}: {kill_error}"))
|
|
|
|
|
}
|
|
|
|
|
Err(error) => CommandOutcome::Error(error),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 21:28:38 +00:00
|
|
|
fn spawn_output_reader(
|
|
|
|
|
mut reader: impl Read + Send + 'static,
|
|
|
|
|
events: ThreadEventSink,
|
|
|
|
|
stderr: bool,
|
|
|
|
|
) -> JoinHandle<()> {
|
|
|
|
|
thread::spawn(move || {
|
|
|
|
|
let mut buffer = vec![0_u8; 4_096];
|
|
|
|
|
loop {
|
|
|
|
|
match reader.read(&mut buffer) {
|
|
|
|
|
Ok(0) => break,
|
|
|
|
|
Ok(read) => events.push(ThreadEvent::Output {
|
|
|
|
|
stderr,
|
|
|
|
|
bytes: buffer[..read].to_vec(),
|
|
|
|
|
}),
|
|
|
|
|
Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
|
|
|
|
|
Err(_) => break,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn join_output_readers(readers: &mut Vec<JoinHandle<()>>) {
|
|
|
|
|
for reader in readers.drain(..) {
|
|
|
|
|
let _ = reader.join();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
fn finish_with_exit(
|
|
|
|
|
events: &ThreadEventSink,
|
|
|
|
|
state: &mut SupervisorState,
|
|
|
|
|
child: &mut Option<Child>,
|
2026-08-23 21:28:38 +00:00
|
|
|
output_threads: &mut Vec<JoinHandle<()>>,
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
status: ExitStatus,
|
|
|
|
|
) {
|
|
|
|
|
*state = SupervisorState::Done;
|
|
|
|
|
debug_assert!(matches!(*state, SupervisorState::Done));
|
|
|
|
|
let _ = child.take();
|
2026-08-23 21:28:38 +00:00
|
|
|
join_output_readers(output_threads);
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
events.push(ThreadEvent::Exited { status });
|
|
|
|
|
events.push(ThreadEvent::ThreadFinished);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn finish_with_error(
|
|
|
|
|
events: &ThreadEventSink,
|
|
|
|
|
state: &mut SupervisorState,
|
|
|
|
|
child: &mut Option<Child>,
|
2026-08-23 21:28:38 +00:00
|
|
|
output_threads: &mut Vec<JoinHandle<()>>,
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
error: String,
|
|
|
|
|
) {
|
|
|
|
|
*state = SupervisorState::Done;
|
|
|
|
|
debug_assert!(matches!(*state, SupervisorState::Done));
|
2026-08-23 21:28:38 +00:00
|
|
|
if let Some(child) = child.as_mut() {
|
|
|
|
|
let _ = child.kill();
|
|
|
|
|
let _ = child.wait();
|
|
|
|
|
}
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
let _ = child.take();
|
2026-08-23 21:28:38 +00:00
|
|
|
join_output_readers(output_threads);
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
events.push(ThreadEvent::Error { error });
|
|
|
|
|
events.push(ThreadEvent::ThreadFinished);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn signal_to_libc(signal: Signal) -> libc::c_int {
|
|
|
|
|
match signal {
|
|
|
|
|
Signal::Terminate => libc::SIGTERM,
|
|
|
|
|
Signal::Kill => libc::SIGKILL,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn send_signal(pid: u32, signal: Signal) -> Result<(), String> {
|
|
|
|
|
let sig = signal_to_libc(signal);
|
|
|
|
|
let ret = unsafe { libc::kill(pid as libc::pid_t, sig) };
|
|
|
|
|
if ret == 0 {
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
Err(format!(
|
|
|
|
|
"kill({}, {}) failed: {}",
|
|
|
|
|
pid,
|
|
|
|
|
sig,
|
|
|
|
|
std::io::Error::last_os_error()
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn decode_wait_status(status: libc::c_int) -> ExitStatus {
|
|
|
|
|
if libc::WIFEXITED(status) {
|
|
|
|
|
ExitStatus::Code(libc::WEXITSTATUS(status))
|
|
|
|
|
} else if libc::WIFSIGNALED(status) {
|
|
|
|
|
ExitStatus::Signal(libc::WTERMSIG(status))
|
|
|
|
|
} else {
|
|
|
|
|
ExitStatus::Unknown
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn try_wait_pid(pid: u32) -> Result<Option<ExitStatus>, String> {
|
|
|
|
|
loop {
|
|
|
|
|
let mut status: libc::c_int = 0;
|
|
|
|
|
let result = unsafe { libc::waitpid(pid as libc::pid_t, &mut status, libc::WNOHANG) };
|
|
|
|
|
|
|
|
|
|
if result == 0 {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
if result == pid as libc::pid_t {
|
|
|
|
|
return Ok(Some(decode_wait_status(status)));
|
|
|
|
|
}
|
|
|
|
|
if result > 0 {
|
|
|
|
|
return Ok(Some(ExitStatus::Unknown));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let err = std::io::Error::last_os_error();
|
|
|
|
|
if err.kind() == io::ErrorKind::Interrupted {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
return Err(format!("waitpid({pid}) failed: {err}"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
|
|
|
|
|
|
|
|
fn spec(command: &str, args: Vec<&str>) -> ProcessSpec {
|
|
|
|
|
ProcessSpec {
|
|
|
|
|
command: command.to_owned(),
|
|
|
|
|
args: args.into_iter().map(str::to_owned).collect(),
|
|
|
|
|
env: HashMap::new(),
|
|
|
|
|
working_dir: None,
|
|
|
|
|
label: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn shell_spec(script: &str) -> ProcessSpec {
|
|
|
|
|
spec("sh", vec!["-c", script])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn collect_until_finished(
|
|
|
|
|
receiver: &ThreadEventReceiver,
|
|
|
|
|
timeout: Duration,
|
|
|
|
|
) -> Vec<ThreadEvent> {
|
|
|
|
|
let deadline = Instant::now() + timeout;
|
|
|
|
|
let mut events = Vec::new();
|
|
|
|
|
|
|
|
|
|
while Instant::now() < deadline {
|
|
|
|
|
events.extend(receiver.drain());
|
|
|
|
|
if matches!(events.last(), Some(ThreadEvent::ThreadFinished)) {
|
|
|
|
|
return events;
|
|
|
|
|
}
|
|
|
|
|
thread::sleep(Duration::from_millis(5));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
events.extend(receiver.drain());
|
|
|
|
|
events
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn join_finished(handle: &mut ProcessThreadHandle) {
|
|
|
|
|
let deadline = Instant::now() + Duration::from_secs(2);
|
|
|
|
|
loop {
|
|
|
|
|
if handle
|
|
|
|
|
.join_if_finished()
|
|
|
|
|
.expect("supervisor thread should join")
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
assert!(
|
|
|
|
|
Instant::now() < deadline,
|
|
|
|
|
"supervisor thread did not finish before join timeout"
|
|
|
|
|
);
|
|
|
|
|
thread::sleep(Duration::from_millis(5));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn thread_command_and_event_shapes_match_target_contract() {
|
|
|
|
|
let stop_with_deadline = ThreadCommand::Stop {
|
|
|
|
|
kill_after: Some(Duration::from_millis(10)),
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
stop_with_deadline,
|
|
|
|
|
ThreadCommand::Stop {
|
|
|
|
|
kill_after: Some(Duration::from_millis(10))
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
match stop_with_deadline {
|
|
|
|
|
ThreadCommand::Stop {
|
|
|
|
|
kill_after: Some(duration),
|
|
|
|
|
} => assert_eq!(duration, Duration::from_millis(10)),
|
|
|
|
|
_ => panic!("expected stop command with deadline"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let stop_without_deadline = ThreadCommand::Stop { kill_after: None };
|
|
|
|
|
assert_eq!(
|
|
|
|
|
stop_without_deadline,
|
|
|
|
|
ThreadCommand::Stop { kill_after: None }
|
|
|
|
|
);
|
|
|
|
|
match stop_without_deadline {
|
|
|
|
|
ThreadCommand::Stop { kill_after: None } => {}
|
|
|
|
|
_ => panic!("expected stop command without deadline"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert_eq!(ThreadCommand::ShutdownNow, ThreadCommand::ShutdownNow);
|
|
|
|
|
match ThreadCommand::ShutdownNow {
|
|
|
|
|
ThreadCommand::ShutdownNow => {}
|
|
|
|
|
_ => panic!("expected shutdown command"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let started = ThreadEvent::Started { pid: 42 };
|
|
|
|
|
assert_eq!(started, ThreadEvent::Started { pid: 42 });
|
|
|
|
|
match started {
|
|
|
|
|
ThreadEvent::Started { pid } => assert_eq!(pid, 42),
|
|
|
|
|
_ => panic!("expected started event"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let spawn_failed = ThreadEvent::SpawnFailed {
|
|
|
|
|
error: "spawn failed".to_owned(),
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
spawn_failed,
|
|
|
|
|
ThreadEvent::SpawnFailed {
|
|
|
|
|
error: "spawn failed".to_owned()
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
match spawn_failed {
|
|
|
|
|
ThreadEvent::SpawnFailed { error } => assert_eq!(error, "spawn failed"),
|
|
|
|
|
_ => panic!("expected spawn failed event"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let exited = ThreadEvent::Exited {
|
|
|
|
|
status: ExitStatus::Code(7),
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
exited,
|
|
|
|
|
ThreadEvent::Exited {
|
|
|
|
|
status: ExitStatus::Code(7)
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
match exited {
|
|
|
|
|
ThreadEvent::Exited { status } => assert_eq!(status, ExitStatus::Code(7)),
|
|
|
|
|
_ => panic!("expected exited event"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let error = ThreadEvent::Error {
|
|
|
|
|
error: "lost".to_owned(),
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
error,
|
|
|
|
|
ThreadEvent::Error {
|
|
|
|
|
error: "lost".to_owned()
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
match error {
|
|
|
|
|
ThreadEvent::Error { error } => assert_eq!(error, "lost"),
|
|
|
|
|
_ => panic!("expected error event"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert_eq!(ThreadEvent::ThreadFinished, ThreadEvent::ThreadFinished);
|
|
|
|
|
match ThreadEvent::ThreadFinished {
|
|
|
|
|
ThreadEvent::ThreadFinished => {}
|
|
|
|
|
_ => panic!("expected thread finished event"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn thread_event_sink_enqueues_then_wakes() {
|
|
|
|
|
let wake_count = Arc::new(AtomicUsize::new(0));
|
|
|
|
|
let wake_count_for_callback = wake_count.clone();
|
|
|
|
|
let (sink, receiver) = thread_event_channel(move || {
|
|
|
|
|
wake_count_for_callback.fetch_add(1, Ordering::SeqCst);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sink.push(ThreadEvent::Started { pid: 1 });
|
|
|
|
|
sink.push(ThreadEvent::Exited {
|
|
|
|
|
status: ExitStatus::Code(0),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert_eq!(wake_count.load(Ordering::SeqCst), 2);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
receiver.drain(),
|
|
|
|
|
vec![
|
|
|
|
|
ThreadEvent::Started { pid: 1 },
|
|
|
|
|
ThreadEvent::Exited {
|
|
|
|
|
status: ExitStatus::Code(0)
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
assert!(receiver.drain().is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn process_thread_handle_queues_commands_and_wakes_supervisor() {
|
|
|
|
|
let commands = Arc::new(SegQueue::new());
|
|
|
|
|
let wake = WakeFd::new().expect("wake fd should be created");
|
|
|
|
|
let handle = ProcessThreadHandle {
|
|
|
|
|
commands: commands.clone(),
|
|
|
|
|
wake: wake.clone(),
|
|
|
|
|
join: None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
handle
|
|
|
|
|
.send(ThreadCommand::Stop {
|
|
|
|
|
kill_after: Some(Duration::from_millis(25)),
|
|
|
|
|
})
|
|
|
|
|
.expect("stop command should queue");
|
|
|
|
|
handle
|
|
|
|
|
.send(ThreadCommand::ShutdownNow)
|
|
|
|
|
.expect("shutdown command should queue");
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
poll_command_wake(&wake, Duration::ZERO).expect("wake poll should succeed"),
|
|
|
|
|
"wake fd should be readable after queued commands"
|
|
|
|
|
);
|
|
|
|
|
wake.drain().expect("wake fd should drain");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
commands.pop(),
|
|
|
|
|
Some(ThreadCommand::Stop {
|
|
|
|
|
kill_after: Some(Duration::from_millis(25))
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(commands.pop(), Some(ThreadCommand::ShutdownNow));
|
|
|
|
|
assert_eq!(commands.pop(), None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn supervisor_reports_started_exited_and_finished() {
|
|
|
|
|
let (sink, receiver) = thread_event_channel(|| {});
|
|
|
|
|
let mut handle = ProcessSupervisorThread::start(shell_spec("exit 7"), sink)
|
|
|
|
|
.expect("supervisor should start");
|
|
|
|
|
|
|
|
|
|
let events = collect_until_finished(&receiver, Duration::from_secs(2));
|
|
|
|
|
join_finished(&mut handle);
|
|
|
|
|
|
|
|
|
|
assert_eq!(events.len(), 3);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
events.first(),
|
|
|
|
|
Some(ThreadEvent::Started { pid }) if *pid > 0
|
|
|
|
|
));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
events.get(1),
|
|
|
|
|
Some(&ThreadEvent::Exited {
|
|
|
|
|
status: ExitStatus::Code(7)
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(events.last(), Some(&ThreadEvent::ThreadFinished));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn supervisor_reports_spawn_failed_and_finished() {
|
|
|
|
|
let (sink, receiver) = thread_event_channel(|| {});
|
|
|
|
|
let mut handle =
|
|
|
|
|
ProcessSupervisorThread::start(spec("/definitely/not/a/real/binary", vec![]), sink)
|
|
|
|
|
.expect("supervisor should start");
|
|
|
|
|
|
|
|
|
|
let events = collect_until_finished(&receiver, Duration::from_secs(2));
|
|
|
|
|
join_finished(&mut handle);
|
|
|
|
|
|
|
|
|
|
assert_eq!(events.len(), 2);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
events.first(),
|
|
|
|
|
Some(ThreadEvent::SpawnFailed { error }) if !error.is_empty()
|
|
|
|
|
));
|
|
|
|
|
assert_eq!(events.last(), Some(&ThreadEvent::ThreadFinished));
|
|
|
|
|
assert!(!events.iter().any(|event| {
|
|
|
|
|
matches!(
|
|
|
|
|
event,
|
|
|
|
|
ThreadEvent::Started { .. }
|
|
|
|
|
| ThreadEvent::Exited { .. }
|
|
|
|
|
| ThreadEvent::Error { .. }
|
|
|
|
|
)
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-08-23 21:28:38 +00:00
|
|
|
fn supervisor_captures_stdout_and_stderr_before_lifecycle_completion() {
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
let (sink, receiver) = thread_event_channel(|| {});
|
|
|
|
|
let mut handle = ProcessSupervisorThread::start(
|
|
|
|
|
shell_spec("echo stdout; echo stderr >&2; exit 0"),
|
|
|
|
|
sink,
|
|
|
|
|
)
|
|
|
|
|
.expect("supervisor should start");
|
|
|
|
|
|
|
|
|
|
let events = collect_until_finished(&receiver, Duration::from_secs(2));
|
|
|
|
|
join_finished(&mut handle);
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
events.first(),
|
|
|
|
|
Some(ThreadEvent::Started { pid }) if *pid > 0
|
|
|
|
|
));
|
2026-08-23 21:28:38 +00:00
|
|
|
let exit = events
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|event| matches!(event, ThreadEvent::Exited { .. }))
|
|
|
|
|
.expect("exited event");
|
|
|
|
|
assert!(events[..exit]
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|event| matches!(event, ThreadEvent::Output { stderr: false, bytes } if bytes == b"stdout\n")));
|
|
|
|
|
assert!(events[..exit]
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|event| matches!(event, ThreadEvent::Output { stderr: true, bytes } if bytes == b"stderr\n")));
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
assert_eq!(
|
2026-08-23 21:28:38 +00:00
|
|
|
events.get(exit),
|
refactor(process): process-manager cleanup
Replace the driver/session/action/event abstraction with a single OS-process supervisor thread and a minimal lifecycle-only public API.
- supervisor: add a dedicated swactor-process-supervisor thread that owns the child, runs it with null stdio, wakes via an eventfd plus poll(2), reaps with waitpid(WNOHANG), and escalates SIGTERM to SIGKILL after a deadline, reporting only lifecycle ThreadEvents over a SegQueue plus wake channel
- actor: collapse ProcessActor<D> into a non-generic state machine (Spawning/Running/Stopping/Done) that owns the supervisor handle, drains events on SupervisorWake, forwards lifecycle as ProcessOutput, and triggers shutdown_now in on_stop
- lifecycle: add ProcessOutputConfig (Disabled/DatastreamMirror) with a JSON proc.<label>.lifecycle mirror (schema swactor_process.lifecycle.v1), command-basename label derivation/sanitization, and an RAII reservation registry preventing duplicate channels
- message/types/spawn/lib: trim the API — ProcessCommand is now only Stop { kill_after }, ProcessOutput covers Started/SpawnFailed/Exited/Error, ProcessSpec keeps command/args/env/working_dir/label; re-export spawn_local_process/send_process_command and drop the custom-driver spawn_process
- removed: delete the action/event/local/mock/session modules and the ProcessDriver/ProcessWaker/EventQueue/PtySize/ProcessMode types plus the old test suite (actor_scenarios, e2e_process, local_driver, proptest_session, session_scenarios); add public_api_stage1/2 tests and the SWACTOR_MANAGED_PROCESS_SPEC.md
- swactor core: demote ProcessOutputObserver to a legacy/custom adapter (no longer auto-attached), remove Runtime::set_process_output_observer and Ctx::process_output_observer, and add the datastream dependency to the process crate for the mirror
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-07-18 11:21:34 +00:00
|
|
|
Some(&ThreadEvent::Exited {
|
|
|
|
|
status: ExitStatus::Code(0)
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(events.last(), Some(&ThreadEvent::ThreadFinished));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn supervisor_stop_escalates_to_kill_after_deadline() {
|
|
|
|
|
let (sink, receiver) = thread_event_channel(|| {});
|
|
|
|
|
let mut handle = ProcessSupervisorThread::start(
|
|
|
|
|
shell_spec("trap '' TERM; while true; do sleep 1; done"),
|
|
|
|
|
sink,
|
|
|
|
|
)
|
|
|
|
|
.expect("supervisor should start");
|
|
|
|
|
let deadline = Instant::now() + Duration::from_secs(3);
|
|
|
|
|
let mut events = Vec::new();
|
|
|
|
|
let mut stop_sent = false;
|
|
|
|
|
|
|
|
|
|
while Instant::now() < deadline {
|
|
|
|
|
events.extend(receiver.drain());
|
|
|
|
|
if !stop_sent
|
|
|
|
|
&& events
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|event| matches!(event, ThreadEvent::Started { pid } if *pid > 0))
|
|
|
|
|
{
|
|
|
|
|
handle
|
|
|
|
|
.stop(Some(Duration::from_millis(20)))
|
|
|
|
|
.expect("stop command should queue");
|
|
|
|
|
stop_sent = true;
|
|
|
|
|
}
|
|
|
|
|
if matches!(events.last(), Some(ThreadEvent::ThreadFinished)) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
thread::sleep(Duration::from_millis(5));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !matches!(events.last(), Some(ThreadEvent::ThreadFinished)) {
|
|
|
|
|
let _ = handle.shutdown_now();
|
|
|
|
|
events.extend(collect_until_finished(&receiver, Duration::from_secs(2)));
|
|
|
|
|
}
|
|
|
|
|
join_finished(&mut handle);
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
stop_sent,
|
|
|
|
|
"supervisor did not report Started before timeout"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(events.last(), Some(&ThreadEvent::ThreadFinished));
|
|
|
|
|
assert!(events.iter().any(|event| {
|
|
|
|
|
matches!(
|
|
|
|
|
event,
|
|
|
|
|
ThreadEvent::Exited {
|
|
|
|
|
status: ExitStatus::Signal(9)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}
|