Refactor syscall modules to use async/await patterns
- Updated `DisplaySyscall` to use `tokio::io::AsyncWriteExt` for asynchronous text output. - Refactored `FsSyscall` to read files asynchronously with offset and length handling. - Modified `MemorySyscall` to use `tokio::task::spawn_blocking` for database operations, allowing async access to SQLite. - Enhanced `NetworkSyscall` to utilize `reqwest` for HTTP requests, replacing the previous `curl` command execution. - Improved `ProcessSyscall` to manage subprocesses with async tasks for stdin, stdout, and stderr handling. - Updated `TimerSyscall` to simplify timer management. - Adjusted plugin implementations for better async support and error handling. - Added `tokio-stream` and `tracing-subscriber` dependencies to `Cargo.toml` for enhanced async stream handling and logging.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use std::io::Write as IoWrite;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
/// Display syscall: output to user.
|
||||
pub struct DisplaySyscall;
|
||||
@@ -8,32 +8,34 @@ impl DisplaySyscall {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn text(&self, content: &str) -> Result<(), String> {
|
||||
let stdout = std::io::stdout();
|
||||
let mut handle = stdout.lock();
|
||||
handle
|
||||
pub async fn text(&self, content: &str) -> Result<(), String> {
|
||||
let mut stdout = tokio::io::stdout();
|
||||
stdout
|
||||
.write_all(content.as_bytes())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
handle.write_all(b"\n").map_err(|e| e.to_string())?;
|
||||
handle.flush().map_err(|e| e.to_string())
|
||||
stdout.write_all(b"\n").await.map_err(|e| e.to_string())?;
|
||||
stdout.flush().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn rich(&self, markup: &str, _format: &str) -> Result<(), String> {
|
||||
// For now, render as plain text. Rich rendering is a system plugin concern.
|
||||
self.text(markup)
|
||||
pub async fn rich(&self, markup: &str, _format: &str) -> Result<(), String> {
|
||||
self.text(markup).await
|
||||
}
|
||||
|
||||
pub fn image(&self, _data: &[u8], _format: &str) -> Result<(), String> {
|
||||
self.text("[image]")
|
||||
pub async fn image(&self, _data: &[u8], _format: &str) -> Result<(), String> {
|
||||
self.text("[image]").await
|
||||
}
|
||||
|
||||
pub fn clear(&self) -> Result<(), String> {
|
||||
// ANSI clear
|
||||
print!("\x1b[2J\x1b[H");
|
||||
std::io::stdout().flush().map_err(|e| e.to_string())
|
||||
pub async fn clear(&self) -> Result<(), String> {
|
||||
let mut stdout = tokio::io::stdout();
|
||||
stdout
|
||||
.write_all(b"\x1b[2J\x1b[H")
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
stdout.flush().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn notify(&self, message: &str) -> Result<(), String> {
|
||||
self.text(&format!("[notify] {}", message))
|
||||
pub async fn notify(&self, message: &str) -> Result<(), String> {
|
||||
self.text(&format!("[notify] {}", message)).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use std::path::Path;
|
||||
use tokio::fs as async_fs;
|
||||
use tokio::io::{AsyncReadExt, AsyncSeekExt};
|
||||
|
||||
const MAX_READ_SIZE: usize = 64 * 1024 * 1024; // 64 MB
|
||||
|
||||
/// FS syscall: file system operations.
|
||||
pub struct FsSyscall;
|
||||
@@ -10,17 +13,25 @@ impl FsSyscall {
|
||||
}
|
||||
|
||||
pub async fn read(&self, path: &str, offset: i64, length: i64) -> Result<Vec<u8>, String> {
|
||||
let data = async_fs::read(path).await.map_err(|e| e.to_string())?;
|
||||
let start = if offset > 0 { offset as usize } else { 0 };
|
||||
let end = if length > 0 {
|
||||
std::cmp::min(start + length as usize, data.len())
|
||||
} else {
|
||||
data.len()
|
||||
};
|
||||
if start >= data.len() {
|
||||
return Ok(Vec::new());
|
||||
let mut file = tokio::fs::File::open(path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if offset > 0 {
|
||||
file.seek(std::io::SeekFrom::Start(offset as u64))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(data[start..end].to_vec())
|
||||
let len = if length > 0 {
|
||||
std::cmp::min(length as usize, MAX_READ_SIZE)
|
||||
} else {
|
||||
let meta = file.metadata().await.map_err(|e| e.to_string())?;
|
||||
let remaining = (meta.len() as usize).saturating_sub(offset.max(0) as usize);
|
||||
std::cmp::min(remaining, MAX_READ_SIZE)
|
||||
};
|
||||
let mut buf = vec![0u8; len];
|
||||
let n = file.read(&mut buf).await.map_err(|e| e.to_string())?;
|
||||
buf.truncate(n);
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub async fn write(&self, path: &str, data: &[u8], append: bool) -> Result<(), String> {
|
||||
@@ -50,9 +61,7 @@ impl FsSyscall {
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
} else {
|
||||
async_fs::remove_file(path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
async_fs::remove_file(path).await.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use rusqlite::{params, Connection};
|
||||
use std::sync::Mutex;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Memory syscall: structured KV storage with FTS5 full-text search.
|
||||
/// Data is persisted to `<exe_dir>/data/memory.db`.
|
||||
pub struct MemorySyscall {
|
||||
conn: Mutex<Connection>,
|
||||
conn: Arc<Mutex<Connection>>,
|
||||
}
|
||||
|
||||
impl MemorySyscall {
|
||||
@@ -12,7 +12,9 @@ impl MemorySyscall {
|
||||
let db_path = crate::paths::data_dir().join("memory.db");
|
||||
let conn = Connection::open(&db_path)?;
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS kv (
|
||||
"PRAGMA journal_mode=WAL;
|
||||
PRAGMA busy_timeout=5000;
|
||||
CREATE TABLE IF NOT EXISTS kv (
|
||||
key TEXT PRIMARY KEY,
|
||||
value BLOB NOT NULL,
|
||||
text_value TEXT,
|
||||
@@ -34,78 +36,114 @@ impl MemorySyscall {
|
||||
)?;
|
||||
tracing::info!("memory: opened {}", db_path.display());
|
||||
Ok(Self {
|
||||
conn: Mutex::new(conn),
|
||||
conn: Arc::new(Mutex::new(conn)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read(&self, key: &str) -> Result<Option<Vec<u8>>, String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT value FROM kv WHERE key = ?1")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let result = stmt
|
||||
.query_row(params![key], |row| row.get::<_, Vec<u8>>(0))
|
||||
.ok();
|
||||
Ok(result)
|
||||
pub async fn read(&self, key: &str) -> Result<Option<Vec<u8>>, String> {
|
||||
let conn = self.conn.clone();
|
||||
let key = key.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().unwrap();
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT value FROM kv WHERE key = ?1")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let result = stmt
|
||||
.query_row(params![key], |row| row.get::<_, Vec<u8>>(0))
|
||||
.ok();
|
||||
Ok(result)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
pub fn write(&self, key: &str, value: &[u8]) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let text_value = String::from_utf8(value.to_vec()).ok();
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO kv (key, value, text_value, updated_at) VALUES (?1, ?2, ?3, strftime('%s','now'))",
|
||||
params![key, value, text_value],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete(&self, key: &str) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute("DELETE FROM kv WHERE key = ?1", params![key])
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list(&self, prefix: &str, limit: i32) -> Result<Vec<String>, String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let lim = if limit > 0 { limit } else { 1000 };
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT key FROM kv WHERE key LIKE ?1 ORDER BY key LIMIT ?2")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let pattern = format!("{}%", prefix);
|
||||
let rows = stmt
|
||||
.query_map(params![pattern, lim], |row| row.get::<_, String>(0))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut keys = Vec::new();
|
||||
for row in rows {
|
||||
keys.push(row.map_err(|e| e.to_string())?);
|
||||
}
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
pub fn search(&self, query: &str, limit: i32) -> Result<Vec<(String, String, f32)>, String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let lim = if limit > 0 { limit } else { 20 };
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT key, snippet(kv_fts, 1, '<b>', '</b>', '...', 32), rank
|
||||
FROM kv_fts WHERE kv_fts MATCH ?1 ORDER BY rank LIMIT ?2",
|
||||
pub async fn write(&self, key: &str, value: &[u8]) -> Result<(), String> {
|
||||
let conn = self.conn.clone();
|
||||
let key = key.to_string();
|
||||
let value = value.to_vec();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().unwrap();
|
||||
let text_value: Option<String> =
|
||||
std::str::from_utf8(&value).ok().map(str::to_owned);
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO kv (key, value, text_value, updated_at) VALUES (?1, ?2, ?3, strftime('%s','now'))",
|
||||
params![key, value, text_value],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map(params![query, lim], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, f64>(2)? as f32,
|
||||
))
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut hits = Vec::new();
|
||||
for row in rows {
|
||||
hits.push(row.map_err(|e| e.to_string())?);
|
||||
}
|
||||
Ok(hits)
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
pub async fn delete(&self, key: &str) -> Result<(), String> {
|
||||
let conn = self.conn.clone();
|
||||
let key = key.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().unwrap();
|
||||
conn.execute("DELETE FROM kv WHERE key = ?1", params![key])
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
pub async fn list(&self, prefix: &str, limit: i32) -> Result<Vec<String>, String> {
|
||||
let conn = self.conn.clone();
|
||||
let prefix = prefix.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().unwrap();
|
||||
let lim = if limit > 0 { limit } else { 1000 };
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT key FROM kv WHERE key LIKE ?1 ORDER BY key LIMIT ?2")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let pattern = format!("{}%", prefix);
|
||||
let rows = stmt
|
||||
.query_map(params![pattern, lim], |row| row.get::<_, String>(0))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut keys = Vec::new();
|
||||
for row in rows {
|
||||
keys.push(row.map_err(|e| e.to_string())?);
|
||||
}
|
||||
Ok(keys)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
pub async fn search(
|
||||
&self,
|
||||
query: &str,
|
||||
limit: i32,
|
||||
) -> Result<Vec<(String, String, f32)>, String> {
|
||||
let conn = self.conn.clone();
|
||||
let query = query.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let conn = conn.lock().unwrap();
|
||||
let lim = if limit > 0 { limit } else { 20 };
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT key, snippet(kv_fts, 1, '<b>', '</b>', '...', 32), rank
|
||||
FROM kv_fts WHERE kv_fts MATCH ?1 ORDER BY rank LIMIT ?2",
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map(params![query, lim], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, f64>(2)? as f32,
|
||||
))
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut hits = Vec::new();
|
||||
for row in rows {
|
||||
hits.push(row.map_err(|e| e.to_string())?);
|
||||
}
|
||||
Ok(hits)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
pub mod display;
|
||||
pub mod audio;
|
||||
pub mod display;
|
||||
pub mod fs;
|
||||
pub mod hid;
|
||||
pub mod input;
|
||||
pub mod memory;
|
||||
pub mod network;
|
||||
pub mod process;
|
||||
pub mod timer;
|
||||
pub mod hid;
|
||||
pub mod input;
|
||||
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -1,60 +1,94 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
const MAX_RESPONSE_BODY: u64 = 16 * 1024 * 1024; // 16 MB
|
||||
|
||||
/// Network syscall: remote communication.
|
||||
pub struct NetworkSyscall;
|
||||
pub struct NetworkSyscall {
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl NetworkSyscall {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
let client = reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
.read_timeout(Duration::from_secs(30))
|
||||
.timeout(Duration::from_secs(60))
|
||||
.redirect(reqwest::redirect::Policy::limited(5))
|
||||
.build()
|
||||
.unwrap_or_else(|_| reqwest::Client::new());
|
||||
Self { client }
|
||||
}
|
||||
|
||||
pub fn available(&self) -> bool {
|
||||
// Simple connectivity check: try to resolve a known host
|
||||
std::net::TcpStream::connect_timeout(
|
||||
&std::net::SocketAddr::from(([8, 8, 8, 8], 53)),
|
||||
std::time::Duration::from_secs(2),
|
||||
pub async fn available(&self) -> bool {
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
tokio::net::TcpStream::connect(SocketAddr::from(([8, 8, 8, 8], 53))),
|
||||
)
|
||||
.is_ok()
|
||||
.await
|
||||
.map(|result| result.is_ok())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub async fn request(
|
||||
&self,
|
||||
url: &str,
|
||||
method: &str,
|
||||
headers: &std::collections::HashMap<String, String>,
|
||||
headers: &HashMap<String, String>,
|
||||
body: &[u8],
|
||||
) -> Result<(i32, std::collections::HashMap<String, String>, Vec<u8>), String> {
|
||||
// Minimal HTTP client using tokio TcpStream + manual HTTP.
|
||||
// For production, use hyper or reqwest. For now, shell out to a basic impl.
|
||||
use tokio::process::Command;
|
||||
) -> Result<(i32, HashMap<String, String>, Vec<u8>), String> {
|
||||
let method = method
|
||||
.parse::<reqwest::Method>()
|
||||
.map_err(|e| format!("invalid HTTP method {method:?}: {e}"))?;
|
||||
let mut request = self.client.request(method, url);
|
||||
|
||||
let mut cmd = Command::new("curl");
|
||||
cmd.arg("-s")
|
||||
.arg("-X")
|
||||
.arg(method)
|
||||
.arg("-o")
|
||||
.arg("-")
|
||||
.arg("-w")
|
||||
.arg("\n%{http_code}");
|
||||
|
||||
for (k, v) in headers {
|
||||
cmd.arg("-H").arg(format!("{}: {}", k, v));
|
||||
for (name, value) in headers {
|
||||
let header_name = name
|
||||
.parse::<reqwest::header::HeaderName>()
|
||||
.map_err(|e| format!("invalid HTTP header name {name:?}: {e}"))?;
|
||||
let header_value = value
|
||||
.parse::<reqwest::header::HeaderValue>()
|
||||
.map_err(|e| format!("invalid HTTP header value for {name}: {e}"))?;
|
||||
request = request.header(header_name, header_value);
|
||||
}
|
||||
|
||||
if !body.is_empty() {
|
||||
cmd.arg("-d").arg(String::from_utf8_lossy(body).to_string());
|
||||
request = request.body(body.to_vec());
|
||||
}
|
||||
|
||||
cmd.arg(url);
|
||||
let response = request.send().await.map_err(|e| e.to_string())?;
|
||||
|
||||
let output = cmd.output().await.map_err(|e| e.to_string())?;
|
||||
let raw = String::from_utf8_lossy(&output.stdout);
|
||||
let lines: Vec<&str> = raw.rsplitn(2, '\n').collect();
|
||||
// Check content-length before downloading body
|
||||
if let Some(len) = response.content_length() {
|
||||
if len > MAX_RESPONSE_BODY {
|
||||
return Err(format!(
|
||||
"response body too large: {} bytes (limit {} bytes)",
|
||||
len, MAX_RESPONSE_BODY
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let status = lines
|
||||
.first()
|
||||
.and_then(|s| s.parse::<i32>().ok())
|
||||
.unwrap_or(0);
|
||||
let response_body = lines.get(1).unwrap_or(&"").as_bytes().to_vec();
|
||||
let status = i32::from(response.status().as_u16());
|
||||
let resp_headers = response
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(name, value)| {
|
||||
(
|
||||
name.to_string(),
|
||||
value.to_str().unwrap_or_default().to_string(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let resp_body = response.bytes().await.map_err(|e| e.to_string())?;
|
||||
if resp_body.len() as u64 > MAX_RESPONSE_BODY {
|
||||
return Err(format!(
|
||||
"response body too large: {} bytes (limit {} bytes)",
|
||||
resp_body.len(),
|
||||
MAX_RESPONSE_BODY
|
||||
));
|
||||
}
|
||||
|
||||
Ok((status, std::collections::HashMap::new(), response_body))
|
||||
Ok((status, resp_headers, resp_body.to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::{mpsc, Mutex, RwLock};
|
||||
|
||||
/// Per-process managed state, decoupled from the Child handle.
|
||||
struct ManagedProcess {
|
||||
cmd: String,
|
||||
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>,
|
||||
/// Handle to abort background tasks on kill.
|
||||
_tasks: Vec<tokio::task::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
/// Process syscall: spawn and manage subprocesses.
|
||||
/// Uses the real OS PID as the identifier.
|
||||
pub struct ProcessSyscall {
|
||||
procs: Mutex<HashMap<u64, ManagedProcess>>,
|
||||
children: tokio::sync::Mutex<HashMap<u64, tokio::process::Child>>,
|
||||
procs: RwLock<HashMap<u64, Arc<ManagedProcess>>>,
|
||||
}
|
||||
|
||||
impl ProcessSyscall {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
procs: Mutex::new(HashMap::new()),
|
||||
children: tokio::sync::Mutex::new(HashMap::new()),
|
||||
procs: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,86 +47,244 @@ impl ProcessSyscall {
|
||||
command
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped());
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let child = command.spawn().map_err(|e| e.to_string())?;
|
||||
let mut child = command.spawn().map_err(|e| e.to_string())?;
|
||||
let pid = child
|
||||
.id()
|
||||
.ok_or_else(|| "spawned process has no OS pid".to_string())? as u64;
|
||||
|
||||
self.procs.lock().unwrap().insert(
|
||||
pid,
|
||||
ManagedProcess {
|
||||
cmd: cmd.to_string(),
|
||||
},
|
||||
);
|
||||
self.children.lock().await.insert(pid, child);
|
||||
// Take ownership of stdio handles
|
||||
let child_stdin = child.stdin.take();
|
||||
let child_stdout = child.stdout.take();
|
||||
let child_stderr = child.stderr.take();
|
||||
|
||||
let status = Arc::new(Mutex::new("running".to_string()));
|
||||
let exit_notify = Arc::new(tokio::sync::Notify::new());
|
||||
|
||||
// Stdin writer task
|
||||
let (stdin_tx, mut stdin_rx) = mpsc::channel::<Vec<u8>>(64);
|
||||
let stdin_task = tokio::spawn(async move {
|
||||
if let Some(mut stdin) = child_stdin {
|
||||
while let Some(data) = stdin_rx.recv().await {
|
||||
if stdin.write_all(&data).await.is_err() {
|
||||
break;
|
||||
}
|
||||
if stdin.flush().await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Stdout reader task
|
||||
let (stdout_tx, stdout_rx) = mpsc::channel::<Vec<u8>>(64);
|
||||
let stdout_task = tokio::spawn(async move {
|
||||
if let Some(mut stdout) = child_stdout {
|
||||
loop {
|
||||
let mut buf = vec![0u8; 4096];
|
||||
match stdout.read(&mut buf).await {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
buf.truncate(n);
|
||||
if stdout_tx.send(buf).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Stderr drain task (prevent pipe fill-up)
|
||||
let stderr_task = tokio::spawn(async move {
|
||||
if let Some(mut stderr) = child_stderr {
|
||||
let mut buf = vec![0u8; 4096];
|
||||
loop {
|
||||
match stderr.read(&mut buf).await {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(_) => {} // discard
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Reaper task: waits for exit and updates status
|
||||
let status_clone = status.clone();
|
||||
let exit_notify_clone = exit_notify.clone();
|
||||
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 managed = Arc::new(ManagedProcess {
|
||||
cmd: cmd.to_string(),
|
||||
status,
|
||||
stdin_tx: Some(stdin_tx),
|
||||
stdout_rx: Arc::new(Mutex::new(stdout_rx)),
|
||||
exit_notify,
|
||||
_tasks: vec![stdin_task, stdout_task, stderr_task, reaper_task],
|
||||
});
|
||||
|
||||
self.procs.write().await.insert(pid, managed);
|
||||
tracing::info!("process spawned: pid={} cmd={}", pid, cmd);
|
||||
Ok(pid)
|
||||
}
|
||||
|
||||
pub async fn kill(&self, pid: u64) -> Result<(), String> {
|
||||
let mut child = {
|
||||
let mut children = self.children.lock().await;
|
||||
children.remove(&pid).ok_or_else(|| format!("pid {} not found", pid))?
|
||||
};
|
||||
let procs = self.procs.read().await;
|
||||
let proc = procs
|
||||
.get(&pid)
|
||||
.ok_or_else(|| format!("pid {} not found", pid))?
|
||||
.clone();
|
||||
drop(procs);
|
||||
|
||||
child.kill().await.map_err(|e| e.to_string())?;
|
||||
self.procs.lock().unwrap().remove(&pid);
|
||||
// Abort all background tasks (stdin writer, stdout reader, stderr drain, reaper)
|
||||
for task in proc._tasks.iter() {
|
||||
task.abort();
|
||||
}
|
||||
*proc.status.lock().await = "killed".to_string();
|
||||
proc.exit_notify.notify_waiters();
|
||||
|
||||
// Also send OS kill
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let os_pid = pid as i32;
|
||||
unsafe { libc::kill(os_pid as libc::pid_t, 9) };
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// 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 mut child = {
|
||||
let mut children = self.children.lock().await;
|
||||
children.remove(&pid).ok_or_else(|| format!("pid {} not found", pid))?
|
||||
};
|
||||
self.procs.lock().unwrap().remove(&pid);
|
||||
let status = child.wait().await.map_err(|e| e.to_string())?;
|
||||
Ok(status.code().unwrap_or(-1))
|
||||
let procs = self.procs.read().await;
|
||||
let proc = procs
|
||||
.get(&pid)
|
||||
.ok_or_else(|| format!("pid {} not found", pid))?
|
||||
.clone();
|
||||
drop(procs);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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> {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let mut children = self.children.lock().await;
|
||||
if let Some(child) = children.get_mut(&pid) {
|
||||
if let Some(stdin) = child.stdin.as_mut() {
|
||||
stdin.write_all(data).await.map_err(|e| e.to_string())?;
|
||||
stdin.flush().await.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("stdin not available".to_string())
|
||||
}
|
||||
let procs = self.procs.read().await;
|
||||
let proc = procs
|
||||
.get(&pid)
|
||||
.ok_or_else(|| format!("pid {} not found", pid))?
|
||||
.clone();
|
||||
drop(procs);
|
||||
|
||||
if let Some(ref tx) = proc.stdin_tx {
|
||||
tx.send(data.to_vec())
|
||||
.await
|
||||
.map_err(|_| "stdin channel closed".to_string())
|
||||
} else {
|
||||
Err(format!("pid {} not found", pid))
|
||||
Err("stdin not available".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_stdout(&self, pid: u64, buf: &mut [u8]) -> Result<usize, String> {
|
||||
use tokio::io::AsyncReadExt;
|
||||
let mut children = self.children.lock().await;
|
||||
if let Some(child) = children.get_mut(&pid) {
|
||||
if let Some(stdout) = child.stdout.as_mut() {
|
||||
stdout.read(buf).await.map_err(|e| e.to_string())
|
||||
} else {
|
||||
Err("stdout not available".to_string())
|
||||
let procs = self.procs.read().await;
|
||||
let proc = procs
|
||||
.get(&pid)
|
||||
.ok_or_else(|| format!("pid {} not found", pid))?
|
||||
.clone();
|
||||
drop(procs);
|
||||
|
||||
let mut rx = proc.stdout_rx.lock().await;
|
||||
match rx.recv().await {
|
||||
Some(data) => {
|
||||
let n = std::cmp::min(data.len(), buf.len());
|
||||
buf[..n].copy_from_slice(&data[..n]);
|
||||
Ok(n)
|
||||
}
|
||||
} else {
|
||||
Err(format!("pid {} not found", pid))
|
||||
None => Ok(0), // EOF
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn signal(&self, pid: u64, _signal: i32) -> Result<(), String> {
|
||||
self.kill(pid).await
|
||||
pub async fn signal(&self, pid: u64, signal: i32) -> Result<(), String> {
|
||||
if signal == 9 {
|
||||
return self.kill(pid).await;
|
||||
}
|
||||
|
||||
let procs = self.procs.read().await;
|
||||
if !procs.contains_key(&pid) {
|
||||
return Err(format!("pid {} not found", pid));
|
||||
}
|
||||
drop(procs);
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let os_pid = i32::try_from(pid)
|
||||
.map_err(|_| format!("pid {} out of range for platform signal API", pid))?;
|
||||
let result = unsafe { libc::kill(os_pid as libc::pid_t, signal as libc::c_int) };
|
||||
if result == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"signal {} to pid {} failed: {}",
|
||||
signal,
|
||||
pid,
|
||||
std::io::Error::last_os_error()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
Err(format!(
|
||||
"process.signal only supports signal 9 on this platform, got {}",
|
||||
signal
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list(&self) -> Vec<(u64, String, String)> {
|
||||
let procs = self.procs.lock().unwrap();
|
||||
procs
|
||||
.iter()
|
||||
.map(|(pid, mp)| (*pid, mp.cmd.clone(), "running".to_string()))
|
||||
.collect()
|
||||
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 status = mp.status.lock().await.clone();
|
||||
result.push((*pid, mp.cmd.clone(), status));
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_exit_code(status: &str) -> Result<i32, String> {
|
||||
if let Some(code_str) = status
|
||||
.strip_prefix("exited(")
|
||||
.and_then(|s| s.strip_suffix(')'))
|
||||
{
|
||||
code_str.parse::<i32>().map_err(|e| e.to_string())
|
||||
} else {
|
||||
Ok(-1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,12 +52,10 @@ impl TimerSyscall {
|
||||
timers.lock().unwrap().remove(&tid);
|
||||
});
|
||||
|
||||
self.timers.lock().unwrap().insert(
|
||||
timer_id.clone(),
|
||||
TimerEntry {
|
||||
handle,
|
||||
},
|
||||
);
|
||||
self.timers
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(timer_id.clone(), TimerEntry { handle });
|
||||
timer_id
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user