Initial Agentsd project commit
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
54
core/agentsd/src/syscall/audio.rs
Normal file
54
core/agentsd/src/syscall/audio.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
/// Audio syscall: playback and recording.
|
||||
/// Actual implementation delegated to system plugins.
|
||||
/// This is the kernel-side stub that dispatches to the active audio driver.
|
||||
pub struct AudioSyscall {
|
||||
available: bool,
|
||||
}
|
||||
|
||||
impl AudioSyscall {
|
||||
pub fn new() -> Self {
|
||||
// Audio availability detected at runtime
|
||||
Self { available: false }
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_available(&self) -> bool {
|
||||
self.available
|
||||
}
|
||||
|
||||
pub fn play(&self, _data: &[u8], _format: &str) -> Result<(), String> {
|
||||
if !self.available {
|
||||
return Err("audio: no driver loaded".to_string());
|
||||
}
|
||||
// Dispatched to system plugin via FFI
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop(&self) -> Result<(), String> {
|
||||
if !self.available {
|
||||
return Err("audio: no driver loaded".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn volume(&self, _level: f32) -> Result<(), String> {
|
||||
if !self.available {
|
||||
return Err("audio: no driver loaded".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn record_start(&self, _format: &str) -> Result<(), String> {
|
||||
if !self.available {
|
||||
return Err("audio: no driver loaded".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn record_stop(&self) -> Result<Vec<u8>, String> {
|
||||
if !self.available {
|
||||
return Err("audio: no driver loaded".to_string());
|
||||
}
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
39
core/agentsd/src/syscall/display.rs
Normal file
39
core/agentsd/src/syscall/display.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use std::io::Write as IoWrite;
|
||||
|
||||
/// Display syscall: output to user.
|
||||
pub struct DisplaySyscall;
|
||||
|
||||
impl DisplaySyscall {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn text(&self, content: &str) -> Result<(), String> {
|
||||
let stdout = std::io::stdout();
|
||||
let mut handle = stdout.lock();
|
||||
handle
|
||||
.write_all(content.as_bytes())
|
||||
.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())
|
||||
}
|
||||
|
||||
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 fn image(&self, _data: &[u8], _format: &str) -> Result<(), String> {
|
||||
self.text("[image]")
|
||||
}
|
||||
|
||||
pub fn clear(&self) -> Result<(), String> {
|
||||
// ANSI clear
|
||||
print!("\x1b[2J\x1b[H");
|
||||
std::io::stdout().flush().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn notify(&self, message: &str) -> Result<(), String> {
|
||||
self.text(&format!("[notify] {}", message))
|
||||
}
|
||||
}
|
||||
114
core/agentsd/src/syscall/fs.rs
Normal file
114
core/agentsd/src/syscall/fs.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use std::path::Path;
|
||||
use tokio::fs as async_fs;
|
||||
|
||||
/// FS syscall: file system operations.
|
||||
pub struct FsSyscall;
|
||||
|
||||
impl FsSyscall {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
Ok(data[start..end].to_vec())
|
||||
}
|
||||
|
||||
pub async fn write(&self, path: &str, data: &[u8], append: bool) -> Result<(), String> {
|
||||
if let Some(parent) = Path::new(path).parent() {
|
||||
async_fs::create_dir_all(parent)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
if append {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let mut file = tokio::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
file.write_all(data).await.map_err(|e| e.to_string())
|
||||
} else {
|
||||
async_fs::write(path, data).await.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(&self, path: &str) -> Result<(), String> {
|
||||
let meta = async_fs::metadata(path).await.map_err(|e| e.to_string())?;
|
||||
if meta.is_dir() {
|
||||
async_fs::remove_dir_all(path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
} else {
|
||||
async_fs::remove_file(path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn rename(&self, from: &str, to: &str) -> Result<(), String> {
|
||||
async_fs::rename(from, to).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn stat(&self, path: &str) -> Result<FsStatInfo, String> {
|
||||
let meta = async_fs::metadata(path).await.map_err(|e| e.to_string())?;
|
||||
let modified = meta
|
||||
.modified()
|
||||
.ok()
|
||||
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||
.map(|d| d.as_millis() as i64)
|
||||
.unwrap_or(0);
|
||||
let created = meta
|
||||
.created()
|
||||
.ok()
|
||||
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||
.map(|d| d.as_millis() as i64)
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(FsStatInfo {
|
||||
size: meta.len() as i64,
|
||||
is_dir: meta.is_dir(),
|
||||
is_file: meta.is_file(),
|
||||
modified,
|
||||
created,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn list(&self, dir: &str) -> Result<Vec<FsEntryInfo>, String> {
|
||||
let mut entries = Vec::new();
|
||||
let mut read_dir = async_fs::read_dir(dir).await.map_err(|e| e.to_string())?;
|
||||
while let Some(entry) = read_dir.next_entry().await.map_err(|e| e.to_string())? {
|
||||
let meta = entry.metadata().await.map_err(|e| e.to_string())?;
|
||||
entries.push(FsEntryInfo {
|
||||
name: entry.file_name().to_string_lossy().to_string(),
|
||||
is_dir: meta.is_dir(),
|
||||
size: meta.len() as i64,
|
||||
});
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FsStatInfo {
|
||||
pub size: i64,
|
||||
pub is_dir: bool,
|
||||
pub is_file: bool,
|
||||
pub modified: i64,
|
||||
pub created: i64,
|
||||
}
|
||||
|
||||
pub struct FsEntryInfo {
|
||||
pub name: String,
|
||||
pub is_dir: bool,
|
||||
pub size: i64,
|
||||
}
|
||||
50
core/agentsd/src/syscall/hid.rs
Normal file
50
core/agentsd/src/syscall/hid.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
/// HID syscall: screen capture and input simulation.
|
||||
/// Capability can be absent (headless environments).
|
||||
pub struct HidSyscall {
|
||||
available: bool,
|
||||
}
|
||||
|
||||
impl HidSyscall {
|
||||
pub fn new() -> Self {
|
||||
// Detect if desktop environment is available
|
||||
let available = std::env::var("DISPLAY").is_ok()
|
||||
|| std::env::var("WAYLAND_DISPLAY").is_ok()
|
||||
|| cfg!(target_os = "windows")
|
||||
|| cfg!(target_os = "macos");
|
||||
Self { available }
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_available(&self) -> bool {
|
||||
self.available
|
||||
}
|
||||
|
||||
pub fn capture(&self, _x: i32, _y: i32, _w: i32, _h: i32) -> Result<Vec<u8>, String> {
|
||||
if !self.available {
|
||||
return Err("hid: no desktop environment".to_string());
|
||||
}
|
||||
// Stub: actual implementation via system plugin
|
||||
Err("hid.capture: not implemented (requires system plugin)".to_string())
|
||||
}
|
||||
|
||||
pub fn mouse(&self, _x: i32, _y: i32, _action: &str, _button: &str) -> Result<(), String> {
|
||||
if !self.available {
|
||||
return Err("hid: no desktop environment".to_string());
|
||||
}
|
||||
Err("hid.mouse: not implemented (requires system plugin)".to_string())
|
||||
}
|
||||
|
||||
pub fn keyboard(&self, _key: &str, _action: &str) -> Result<(), String> {
|
||||
if !self.available {
|
||||
return Err("hid: no desktop environment".to_string());
|
||||
}
|
||||
Err("hid.keyboard: not implemented (requires system plugin)".to_string())
|
||||
}
|
||||
|
||||
pub fn ocr(&self, _x: i32, _y: i32, _w: i32, _h: i32) -> Result<String, String> {
|
||||
if !self.available {
|
||||
return Err("hid: no desktop environment".to_string());
|
||||
}
|
||||
Err("hid.ocr: not implemented (requires system plugin)".to_string())
|
||||
}
|
||||
}
|
||||
21
core/agentsd/src/syscall/input.rs
Normal file
21
core/agentsd/src/syscall/input.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||
|
||||
/// Input syscall: receive user input.
|
||||
pub struct InputSyscall;
|
||||
|
||||
impl InputSyscall {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub async fn text(&self) -> Result<String, String> {
|
||||
let stdin = io::stdin();
|
||||
let mut reader = BufReader::new(stdin);
|
||||
let mut line = String::new();
|
||||
reader
|
||||
.read_line(&mut line)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(line.trim_end().to_string())
|
||||
}
|
||||
}
|
||||
111
core/agentsd/src/syscall/memory.rs
Normal file
111
core/agentsd/src/syscall/memory.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use rusqlite::{params, Connection};
|
||||
use std::sync::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>,
|
||||
}
|
||||
|
||||
impl MemorySyscall {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
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 (
|
||||
key TEXT PRIMARY KEY,
|
||||
value BLOB NOT NULL,
|
||||
text_value TEXT,
|
||||
updated_at INTEGER DEFAULT (strftime('%s','now'))
|
||||
);
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS kv_fts USING fts5(
|
||||
key, text_value, tokenize='trigram'
|
||||
);
|
||||
CREATE TRIGGER IF NOT EXISTS kv_ai AFTER INSERT ON kv BEGIN
|
||||
INSERT INTO kv_fts(key, text_value) VALUES (NEW.key, NEW.text_value);
|
||||
END;
|
||||
CREATE TRIGGER IF NOT EXISTS kv_au AFTER UPDATE ON kv BEGIN
|
||||
DELETE FROM kv_fts WHERE key = OLD.key;
|
||||
INSERT INTO kv_fts(key, text_value) VALUES (NEW.key, NEW.text_value);
|
||||
END;
|
||||
CREATE TRIGGER IF NOT EXISTS kv_ad AFTER DELETE ON kv BEGIN
|
||||
DELETE FROM kv_fts WHERE key = OLD.key;
|
||||
END;",
|
||||
)?;
|
||||
tracing::info!("memory: opened {}", db_path.display());
|
||||
Ok(Self {
|
||||
conn: 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 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",
|
||||
)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
46
core/agentsd/src/syscall/mod.rs
Normal file
46
core/agentsd/src/syscall/mod.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
pub mod display;
|
||||
pub mod audio;
|
||||
pub mod fs;
|
||||
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;
|
||||
|
||||
use crate::callback::CallbackBus;
|
||||
|
||||
/// The kernel holds all syscall implementations.
|
||||
pub struct Kernel {
|
||||
pub display: Arc<display::DisplaySyscall>,
|
||||
pub audio: Arc<audio::AudioSyscall>,
|
||||
pub fs: Arc<fs::FsSyscall>,
|
||||
pub memory: Arc<memory::MemorySyscall>,
|
||||
pub network: Arc<network::NetworkSyscall>,
|
||||
pub process: Arc<process::ProcessSyscall>,
|
||||
pub timer: Arc<timer::TimerSyscall>,
|
||||
pub hid: Arc<hid::HidSyscall>,
|
||||
pub input: Arc<input::InputSyscall>,
|
||||
pub callback_bus: Arc<CallbackBus>,
|
||||
}
|
||||
|
||||
impl Kernel {
|
||||
pub fn new() -> Result<Self> {
|
||||
let callback_bus = Arc::new(CallbackBus::new());
|
||||
Ok(Self {
|
||||
display: Arc::new(display::DisplaySyscall::new()),
|
||||
audio: Arc::new(audio::AudioSyscall::new()),
|
||||
fs: Arc::new(fs::FsSyscall::new()),
|
||||
memory: Arc::new(memory::MemorySyscall::new()?),
|
||||
network: Arc::new(network::NetworkSyscall::new()),
|
||||
process: Arc::new(process::ProcessSyscall::new()),
|
||||
timer: Arc::new(timer::TimerSyscall::new(callback_bus.clone())),
|
||||
hid: Arc::new(hid::HidSyscall::new()),
|
||||
input: Arc::new(input::InputSyscall::new()),
|
||||
callback_bus,
|
||||
})
|
||||
}
|
||||
}
|
||||
60
core/agentsd/src/syscall/network.rs
Normal file
60
core/agentsd/src/syscall/network.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
/// Network syscall: remote communication.
|
||||
pub struct NetworkSyscall;
|
||||
|
||||
impl NetworkSyscall {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
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),
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub async fn request(
|
||||
&self,
|
||||
url: &str,
|
||||
method: &str,
|
||||
headers: &std::collections::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;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
if !body.is_empty() {
|
||||
cmd.arg("-d").arg(String::from_utf8_lossy(body).to_string());
|
||||
}
|
||||
|
||||
cmd.arg(url);
|
||||
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
Ok((status, std::collections::HashMap::new(), response_body))
|
||||
}
|
||||
}
|
||||
124
core/agentsd/src/syscall/process.rs
Normal file
124
core/agentsd/src/syscall/process.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use tokio::process::Command;
|
||||
|
||||
struct ManagedProcess {
|
||||
cmd: String,
|
||||
}
|
||||
|
||||
/// 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>>,
|
||||
}
|
||||
|
||||
impl ProcessSyscall {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
procs: Mutex::new(HashMap::new()),
|
||||
children: tokio::sync::Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn spawn(
|
||||
&self,
|
||||
cmd: &str,
|
||||
args: &[String],
|
||||
env: &HashMap<String, String>,
|
||||
cwd: &str,
|
||||
) -> Result<u64, String> {
|
||||
let mut command = Command::new(cmd);
|
||||
command.args(args);
|
||||
if !cwd.is_empty() {
|
||||
command.current_dir(cwd);
|
||||
}
|
||||
for (k, v) in env {
|
||||
command.env(k, v);
|
||||
}
|
||||
command
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped());
|
||||
|
||||
let 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);
|
||||
|
||||
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))?
|
||||
};
|
||||
|
||||
child.kill().await.map_err(|e| e.to_string())?;
|
||||
self.procs.lock().unwrap().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))
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
} else {
|
||||
Err(format!("pid {} not found", pid))
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
} else {
|
||||
Err(format!("pid {} not found", pid))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn signal(&self, pid: u64, _signal: i32) -> Result<(), String> {
|
||||
self.kill(pid).await
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
80
core/agentsd/src/syscall/timer.rs
Normal file
80
core/agentsd/src/syscall/timer.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::time::{self, Duration};
|
||||
|
||||
use crate::callback::{CallbackBus, TimerEvent};
|
||||
|
||||
struct TimerEntry {
|
||||
handle: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
/// Timer syscall: delayed and periodic execution with callback delivery.
|
||||
pub struct TimerSyscall {
|
||||
timers: Arc<Mutex<HashMap<String, TimerEntry>>>,
|
||||
next_id: Mutex<u64>,
|
||||
bus: Arc<CallbackBus>,
|
||||
}
|
||||
|
||||
impl TimerSyscall {
|
||||
pub fn new(bus: Arc<CallbackBus>) -> Self {
|
||||
Self {
|
||||
timers: Arc::new(Mutex::new(HashMap::new())),
|
||||
next_id: Mutex::new(1),
|
||||
bus,
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedule a one-shot timer. When it fires, a TimerEvent is sent to the plugin.
|
||||
pub fn once(&self, delay_ms: u64, callback_id: String, plugin_id: String) -> String {
|
||||
let timer_id = {
|
||||
let mut next = self.next_id.lock().unwrap();
|
||||
let id = format!("timer_{}", *next);
|
||||
*next += 1;
|
||||
id
|
||||
};
|
||||
|
||||
let tid = timer_id.clone();
|
||||
let cb = callback_id.clone();
|
||||
let pid = plugin_id.clone();
|
||||
let bus = self.bus.clone();
|
||||
let timers = self.timers.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
time::sleep(Duration::from_millis(delay_ms)).await;
|
||||
tracing::info!("timer fired: id={} callback={} plugin={}", tid, cb, pid);
|
||||
bus.fire(
|
||||
&pid,
|
||||
TimerEvent {
|
||||
timer_id: tid.clone(),
|
||||
callback_id: cb,
|
||||
},
|
||||
);
|
||||
timers.lock().unwrap().remove(&tid);
|
||||
});
|
||||
|
||||
self.timers.lock().unwrap().insert(
|
||||
timer_id.clone(),
|
||||
TimerEntry {
|
||||
handle,
|
||||
},
|
||||
);
|
||||
timer_id
|
||||
}
|
||||
|
||||
pub fn cancel(&self, timer_id: &str) -> Result<(), String> {
|
||||
let mut timers = self.timers.lock().unwrap();
|
||||
if let Some(entry) = timers.remove(timer_id) {
|
||||
entry.handle.abort();
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("timer {} not found", timer_id))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn now(&self) -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user