2026-02-23 04:44:46 +00:00
|
|
|
use std::sync::{Arc, OnceLock};
|
|
|
|
|
|
|
|
|
|
use swactor::actor::{ActorAddress, ActorInterface, Ctx};
|
2026-06-06 17:53:25 +00:00
|
|
|
use swactor::process_observer::ProcessOutputObserver;
|
2026-02-23 04:44:46 +00:00
|
|
|
|
2026-06-06 17:53:25 +00:00
|
|
|
use crate::action::{OutputStream, ProcessAction};
|
2026-02-23 04:44:46 +00:00
|
|
|
use crate::event::ProcessEvent;
|
|
|
|
|
use crate::message::{ProcessCommand, ProcessNotification};
|
|
|
|
|
use crate::session::ProcessSession;
|
refactor: remove dead code, consolidate files (#52)
Remove the unused TCP transport and dead CLI/simulation scaffolding and collapse scattered single-purpose modules into consolidated files across the dashboard, process, and simulation crates.
- crates/transport: drop the TCP transport (src/tcp.rs and its tcp feature), leaving only ed25519 identity and encoding utilities behind the iroh transport
- crates/dashboard: collapse actor_detail_html/actors_html/dashboard_html/topology_html into a single html.rs, drop command/parse.rs, and inline the trace types into lib.rs
- crates/process: fold driver, pipeline_types, queue, subscriber, waker, and local pipes/signal/wait into local/mod.rs, pipeline.rs, and a unified types.rs, consolidating the public re-exports
- crates/simulation: remove the ci subdirectory (local_sim, sim) and dead node/config/trace modules, and flatten the distribution subdirectory into top-level files
- crates/datastore: remove the unused store_cli, cli, and in-memory storage, and deduplicate crypto.rs across datastore and distribution (about 190 lines of shared code removed)
- crates/distribution: drop dead codec code and trim the messages module
Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-02-25 14:33:04 +00:00
|
|
|
use crate::types::{ProcessDriver, ProcessWaker};
|
2026-02-23 04:44:46 +00:00
|
|
|
|
|
|
|
|
/// Actor wrapper around a `ProcessSession` and its driver.
|
|
|
|
|
///
|
|
|
|
|
/// Generic over `D: ProcessDriver` so that tests can use `MockDriver` or
|
|
|
|
|
/// `TestDriver` while production uses `LocalDriver`.
|
|
|
|
|
pub struct ProcessActor<D: ProcessDriver> {
|
|
|
|
|
session: ProcessSession,
|
|
|
|
|
driver: D,
|
|
|
|
|
self_addr: Option<ActorAddress>,
|
|
|
|
|
/// Actions from `ProcessSession::new()`, executed in `on_start`.
|
|
|
|
|
deferred_actions: Option<Vec<ProcessAction>>,
|
|
|
|
|
/// Shared slot for the waker — filled after the actor address is known.
|
|
|
|
|
pub waker_slot: Arc<OnceLock<ProcessWaker>>,
|
2026-06-06 17:53:25 +00:00
|
|
|
/// Per-node observer that taps this process's output (command-basename
|
|
|
|
|
/// `label`) onto the node's telemetry stream. `None` when the runtime has
|
|
|
|
|
/// no observer installed.
|
|
|
|
|
output_observer: Option<Arc<dyn ProcessOutputObserver>>,
|
|
|
|
|
/// Command basename used to label this process's output to the observer.
|
|
|
|
|
label: String,
|
2026-02-23 04:44:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<D: ProcessDriver> ProcessActor<D> {
|
|
|
|
|
pub fn new(
|
|
|
|
|
session: ProcessSession,
|
|
|
|
|
driver: D,
|
|
|
|
|
initial_actions: Vec<ProcessAction>,
|
|
|
|
|
waker_slot: Arc<OnceLock<ProcessWaker>>,
|
2026-06-06 17:53:25 +00:00
|
|
|
output_observer: Option<Arc<dyn ProcessOutputObserver>>,
|
|
|
|
|
label: String,
|
2026-02-23 04:44:46 +00:00
|
|
|
) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
session,
|
|
|
|
|
driver,
|
|
|
|
|
self_addr: None,
|
|
|
|
|
deferred_actions: Some(initial_actions),
|
|
|
|
|
waker_slot,
|
2026-06-06 17:53:25 +00:00
|
|
|
output_observer,
|
|
|
|
|
label,
|
2026-02-23 04:44:46 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Drain events from the driver, apply each to the session, and dispatch
|
|
|
|
|
/// all resulting actions.
|
|
|
|
|
fn drain_and_dispatch(&mut self, ctx: &Ctx) {
|
|
|
|
|
let events = self.driver.poll();
|
|
|
|
|
for event in events {
|
|
|
|
|
let actions = self.session.apply(event);
|
|
|
|
|
self.dispatch_actions(ctx, actions);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Execute actions produced by the session state machine.
|
|
|
|
|
fn dispatch_actions(&mut self, ctx: &Ctx, actions: Vec<ProcessAction>) {
|
|
|
|
|
let self_addr = self.self_addr.expect("self_addr not set");
|
|
|
|
|
for action in actions {
|
|
|
|
|
match action {
|
|
|
|
|
// Driver commands — forward to the driver
|
|
|
|
|
ProcessAction::SpawnProcess { .. }
|
|
|
|
|
| ProcessAction::WriteStdin { .. }
|
|
|
|
|
| ProcessAction::SendSignal { .. }
|
|
|
|
|
| ProcessAction::ResizePty { .. }
|
|
|
|
|
| ProcessAction::CloseStdin
|
|
|
|
|
| ProcessAction::ScheduleKillTimeout { .. } => {
|
|
|
|
|
self.driver.execute(action);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Subscriber notifications — send to each subscriber
|
|
|
|
|
ProcessAction::NotifyStarted { subscribers } => {
|
2026-05-25 18:19:06 +00:00
|
|
|
let notif = ProcessNotification::Started {
|
|
|
|
|
process: self_addr,
|
|
|
|
|
pid: self.driver.pid(),
|
|
|
|
|
};
|
2026-02-23 04:44:46 +00:00
|
|
|
for sub in subscribers {
|
|
|
|
|
let _ = ctx.send(sub, notif.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ProcessAction::NotifyOutput {
|
|
|
|
|
subscribers,
|
|
|
|
|
data,
|
|
|
|
|
stream,
|
|
|
|
|
} => {
|
2026-06-06 17:53:25 +00:00
|
|
|
// Tap the node's telemetry observer before the data is moved
|
|
|
|
|
// into the subscriber notification. Per-node, auto-attached.
|
|
|
|
|
if let Some(obs) = &self.output_observer {
|
|
|
|
|
obs.on_output(&self.label, matches!(stream, OutputStream::Stderr), &data);
|
|
|
|
|
}
|
2026-02-23 04:44:46 +00:00
|
|
|
let notif = ProcessNotification::Output {
|
|
|
|
|
process: self_addr,
|
|
|
|
|
data,
|
|
|
|
|
stream,
|
|
|
|
|
};
|
|
|
|
|
for sub in subscribers {
|
|
|
|
|
let _ = ctx.send(sub, notif.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ProcessAction::NotifyExited {
|
|
|
|
|
subscribers,
|
|
|
|
|
status,
|
|
|
|
|
} => {
|
|
|
|
|
let notif = ProcessNotification::Exited {
|
|
|
|
|
process: self_addr,
|
|
|
|
|
status,
|
|
|
|
|
};
|
|
|
|
|
for sub in subscribers {
|
|
|
|
|
let _ = ctx.send(sub, notif.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-23 15:42:28 +00:00
|
|
|
ProcessAction::NotifyError { subscribers, error } => {
|
2026-02-23 04:44:46 +00:00
|
|
|
let notif = ProcessNotification::Error {
|
|
|
|
|
process: self_addr,
|
|
|
|
|
error,
|
|
|
|
|
};
|
|
|
|
|
for sub in subscribers {
|
|
|
|
|
let _ = ctx.send(sub, notif.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Lifecycle
|
|
|
|
|
ProcessAction::SelfTerminate => {
|
|
|
|
|
ctx.stop_self();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Map a `ProcessCommand` to the corresponding `ProcessEvent`.
|
|
|
|
|
fn command_to_event(cmd: ProcessCommand) -> Option<ProcessEvent> {
|
|
|
|
|
match cmd {
|
|
|
|
|
ProcessCommand::WriteStdin { data } => Some(ProcessEvent::WriteStdin { data }),
|
|
|
|
|
ProcessCommand::SendSignal { signal } => Some(ProcessEvent::SendSignal { signal }),
|
|
|
|
|
ProcessCommand::ResizePty { size } => Some(ProcessEvent::ResizePty { size }),
|
|
|
|
|
ProcessCommand::CloseStdin => Some(ProcessEvent::CloseStdin),
|
|
|
|
|
ProcessCommand::Close => Some(ProcessEvent::CloseRequested),
|
|
|
|
|
ProcessCommand::Subscribe { address } => Some(ProcessEvent::Subscribe { address }),
|
2026-06-23 15:42:28 +00:00
|
|
|
ProcessCommand::Unsubscribe { address } => Some(ProcessEvent::Unsubscribe { address }),
|
2026-02-23 04:44:46 +00:00
|
|
|
ProcessCommand::PollTick => None, // handled by drain
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<D: ProcessDriver + 'static> ActorInterface for ProcessActor<D> {
|
|
|
|
|
type Incoming = ProcessCommand;
|
|
|
|
|
type Response = ();
|
|
|
|
|
|
|
|
|
|
fn on_start(&mut self, ctx: &Ctx) {
|
|
|
|
|
self.self_addr = Some(ctx.self_addr());
|
|
|
|
|
if let Some(actions) = self.deferred_actions.take() {
|
|
|
|
|
self.dispatch_actions(ctx, actions);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn handle(&mut self, ctx: &Ctx, msg: ProcessCommand) {
|
|
|
|
|
// Process the incoming command first — this ensures Subscribe
|
|
|
|
|
// registers before drain dispatches notifications, and keeps
|
|
|
|
|
// user commands (Close, WriteStdin) responsive.
|
|
|
|
|
if let Some(event) = Self::command_to_event(msg) {
|
|
|
|
|
let actions = self.session.apply(event);
|
|
|
|
|
self.dispatch_actions(ctx, actions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Then drain pending I/O events from background threads.
|
|
|
|
|
self.drain_and_dispatch(ctx);
|
|
|
|
|
}
|
|
|
|
|
}
|