Migrate test plugins to Rust and async-ify syscall modules
- Replace Python test plugins (echo, ai_test, claudecode) with Rust implementations driven by the plugin SDK - Convert fs/network/process/timer syscall modules to async/await - Change Network.OpenConn to return stream ConnEvent (conn_id handed to client in first "open" frame for Send/Close routing) - Bind timer callbacks to plugin identity; stream_call subscriptions are identity-checked - Update docs and regenerate Python SDK protobuf stubs Verified: cargo build/test/clippy clean; echo + ai_test full syscall suites pass; hermes & claudecode register and heartbeat; cross-plugin bridge.call routing and auth denial verified end-to-end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,8 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::{mpsc, Mutex, RwLock};
|
||||
use tokio::sync::{mpsc, watch, Mutex, RwLock};
|
||||
use tokio::time::Duration;
|
||||
|
||||
/// Per-process managed state, decoupled from the Child handle.
|
||||
struct ManagedProcess {
|
||||
@@ -10,8 +11,8 @@ struct ManagedProcess {
|
||||
status: Arc<Mutex<String>>,
|
||||
stdin_tx: Option<mpsc::Sender<Vec<u8>>>,
|
||||
stdout_rx: Arc<Mutex<mpsc::Receiver<Vec<u8>>>>,
|
||||
/// Notified when the process exits. Clone the receiver to wait.
|
||||
exit_notify: Arc<tokio::sync::Notify>,
|
||||
/// Watch receiver: becomes `true` when process exits. Clone to wait.
|
||||
exit_rx: watch::Receiver<bool>,
|
||||
/// Handle to abort background tasks on kill.
|
||||
_tasks: Vec<tokio::task::JoinHandle<()>>,
|
||||
}
|
||||
@@ -19,13 +20,13 @@ struct ManagedProcess {
|
||||
/// Process syscall: spawn and manage subprocesses.
|
||||
/// Uses the real OS PID as the identifier.
|
||||
pub struct ProcessSyscall {
|
||||
procs: RwLock<HashMap<u64, Arc<ManagedProcess>>>,
|
||||
procs: Arc<RwLock<HashMap<u64, Arc<ManagedProcess>>>>,
|
||||
}
|
||||
|
||||
impl ProcessSyscall {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
procs: RwLock::new(HashMap::new()),
|
||||
procs: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +62,7 @@ impl ProcessSyscall {
|
||||
let child_stderr = child.stderr.take();
|
||||
|
||||
let status = Arc::new(Mutex::new("running".to_string()));
|
||||
let exit_notify = Arc::new(tokio::sync::Notify::new());
|
||||
let (exit_tx, exit_rx) = watch::channel(false);
|
||||
|
||||
// Stdin writer task
|
||||
let (stdin_tx, mut stdin_rx) = mpsc::channel::<Vec<u8>>(64);
|
||||
@@ -111,14 +112,17 @@ impl ProcessSyscall {
|
||||
}
|
||||
});
|
||||
|
||||
// Reaper task: waits for exit and updates status
|
||||
// Reaper task: waits for exit, updates status, signals watchers, delayed cleanup
|
||||
let status_clone = status.clone();
|
||||
let exit_notify_clone = exit_notify.clone();
|
||||
let procs_clone = Arc::clone(&self.procs);
|
||||
let reaper_task = tokio::spawn(async move {
|
||||
let exit_status = child.wait().await;
|
||||
let code = exit_status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1);
|
||||
*status_clone.lock().await = format!("exited({})", code);
|
||||
exit_notify_clone.notify_waiters();
|
||||
let _ = exit_tx.send(true);
|
||||
// Delayed cleanup: give stdout readers time to drain buffered data
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
procs_clone.write().await.remove(&pid);
|
||||
});
|
||||
|
||||
let managed = Arc::new(ManagedProcess {
|
||||
@@ -126,7 +130,7 @@ impl ProcessSyscall {
|
||||
status,
|
||||
stdin_tx: Some(stdin_tx),
|
||||
stdout_rx: Arc::new(Mutex::new(stdout_rx)),
|
||||
exit_notify,
|
||||
exit_rx,
|
||||
_tasks: vec![stdin_task, stdout_task, stderr_task, reaper_task],
|
||||
});
|
||||
|
||||
@@ -136,19 +140,25 @@ impl ProcessSyscall {
|
||||
}
|
||||
|
||||
pub async fn kill(&self, pid: u64) -> Result<(), String> {
|
||||
let procs = self.procs.read().await;
|
||||
let proc = procs
|
||||
.get(&pid)
|
||||
.ok_or_else(|| format!("pid {} not found", pid))?
|
||||
.clone();
|
||||
drop(procs);
|
||||
let proc = {
|
||||
let procs = self.procs.read().await;
|
||||
procs
|
||||
.get(&pid)
|
||||
.ok_or_else(|| format!("pid {} not found", pid))?
|
||||
.clone()
|
||||
};
|
||||
|
||||
// Set status to killed first
|
||||
*proc.status.lock().await = "killed".to_string();
|
||||
|
||||
// Abort all background tasks (stdin writer, stdout reader, stderr drain, reaper)
|
||||
// Aborting reaper drops exit_tx, so any wait() blocked on changed() will wake with Err.
|
||||
for task in proc._tasks.iter() {
|
||||
task.abort();
|
||||
}
|
||||
*proc.status.lock().await = "killed".to_string();
|
||||
proc.exit_notify.notify_waiters();
|
||||
|
||||
// Remove from map
|
||||
self.procs.write().await.remove(&pid);
|
||||
|
||||
// Also send OS kill
|
||||
#[cfg(unix)]
|
||||
@@ -161,46 +171,41 @@ impl ProcessSyscall {
|
||||
// On Windows, aborting the reaper task triggers kill_on_drop on the Child
|
||||
}
|
||||
|
||||
self.procs.write().await.remove(&pid);
|
||||
tracing::info!("process killed: pid={}", pid);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn wait(&self, pid: u64) -> Result<i32, String> {
|
||||
let procs = self.procs.read().await;
|
||||
let proc = procs
|
||||
.get(&pid)
|
||||
.ok_or_else(|| format!("pid {} not found", pid))?
|
||||
.clone();
|
||||
drop(procs);
|
||||
let proc = {
|
||||
let procs = self.procs.read().await;
|
||||
procs
|
||||
.get(&pid)
|
||||
.ok_or_else(|| format!("pid {} not found", pid))?
|
||||
.clone()
|
||||
};
|
||||
|
||||
// Check if already exited before waiting
|
||||
{
|
||||
let status = proc.status.lock().await;
|
||||
if status.starts_with("exited") || *status == "killed" {
|
||||
let s = status.clone();
|
||||
drop(status);
|
||||
self.procs.write().await.remove(&pid);
|
||||
return parse_exit_code(&s);
|
||||
// Use watch channel: no race between checking status and waiting.
|
||||
// watch retains the latest value, so even if send(true) happened before
|
||||
// we subscribe, borrow_and_update() will see `true` immediately.
|
||||
let mut rx = proc.exit_rx.clone();
|
||||
while !*rx.borrow_and_update() {
|
||||
if rx.changed().await.is_err() {
|
||||
break; // sender dropped (e.g. kill aborted reaper)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for exit notification
|
||||
proc.exit_notify.notified().await;
|
||||
let status = proc.status.lock().await.clone();
|
||||
|
||||
// Clean up from map
|
||||
self.procs.write().await.remove(&pid);
|
||||
parse_exit_code(&status)
|
||||
}
|
||||
|
||||
pub async fn write_stdin(&self, pid: u64, data: &[u8]) -> Result<(), String> {
|
||||
let procs = self.procs.read().await;
|
||||
let proc = procs
|
||||
.get(&pid)
|
||||
.ok_or_else(|| format!("pid {} not found", pid))?
|
||||
.clone();
|
||||
drop(procs);
|
||||
let proc = {
|
||||
let procs = self.procs.read().await;
|
||||
procs
|
||||
.get(&pid)
|
||||
.ok_or_else(|| format!("pid {} not found", pid))?
|
||||
.clone()
|
||||
};
|
||||
|
||||
if let Some(ref tx) = proc.stdin_tx {
|
||||
tx.send(data.to_vec())
|
||||
@@ -212,12 +217,13 @@ impl ProcessSyscall {
|
||||
}
|
||||
|
||||
pub async fn read_stdout(&self, pid: u64, buf: &mut [u8]) -> Result<usize, String> {
|
||||
let procs = self.procs.read().await;
|
||||
let proc = procs
|
||||
.get(&pid)
|
||||
.ok_or_else(|| format!("pid {} not found", pid))?
|
||||
.clone();
|
||||
drop(procs);
|
||||
let proc = {
|
||||
let procs = self.procs.read().await;
|
||||
procs
|
||||
.get(&pid)
|
||||
.ok_or_else(|| format!("pid {} not found", pid))?
|
||||
.clone()
|
||||
};
|
||||
|
||||
let mut rx = proc.stdout_rx.lock().await;
|
||||
match rx.recv().await {
|
||||
@@ -268,9 +274,13 @@ impl ProcessSyscall {
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> Vec<(u64, String, String)> {
|
||||
let procs = self.procs.read().await;
|
||||
let mut result = Vec::with_capacity(procs.len());
|
||||
for (pid, mp) in procs.iter() {
|
||||
let snapshot: Vec<(u64, Arc<ManagedProcess>)> = {
|
||||
let procs = self.procs.read().await;
|
||||
procs.iter().map(|(pid, mp)| (*pid, mp.clone())).collect()
|
||||
};
|
||||
// procs read lock is dropped here
|
||||
let mut result = Vec::with_capacity(snapshot.len());
|
||||
for (pid, mp) in &snapshot {
|
||||
let status = mp.status.lock().await.clone();
|
||||
result.push((*pid, mp.cmd.clone(), status));
|
||||
}
|
||||
@@ -288,3 +298,4 @@ fn parse_exit_code(status: &str) -> Result<i32, String> {
|
||||
Ok(-1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user