use mlua::Lua;
use std::time::{Duration, Instant};
#[cfg(target_os = "linux")]
fn list_processes() -> Result<Vec<(i64, String, Option<String>)>, String> {
let entries = std::fs::read_dir("/proc")
.map_err(|e| format!("process.list: failed to read /proc: {e}"))?;
let mut result = Vec::new();
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(_) => continue,
};
let name = entry.file_name();
let name_str = name.to_string_lossy();
let pid: i64 = match name_str.parse() {
Ok(p) => p,
Err(_) => continue,
};
let comm_path = entry.path().join("comm");
let comm = std::fs::read_to_string(&comm_path)
.unwrap_or_default()
.trim()
.to_string();
if comm.is_empty() {
continue;
}
let cmdline_path = entry.path().join("cmdline");
let cmdline = std::fs::read_to_string(&cmdline_path)
.ok()
.map(|s| s.replace('\0', " ").trim().to_string())
.filter(|s| !s.is_empty());
result.push((pid, comm, cmdline));
}
Ok(result)
}
#[cfg(not(target_os = "linux"))]
fn list_processes() -> Result<Vec<(i64, String, Option<String>)>, String> {
let output = std::process::Command::new("ps")
.args(["-eo", "pid,comm"])
.output()
.map_err(|e| format!("process.list: failed to run ps: {e}"))?;
if !output.status.success() {
return Err(format!(
"process.list: ps failed: {}",
String::from_utf8_lossy(&output.stderr)
));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut result = Vec::new();
for line in stdout.lines().skip(1) {
let line = line.trim();
if line.is_empty() {
continue;
}
let mut parts = line.splitn(2, char::is_whitespace);
let pid: i64 = match parts.next().and_then(|s| s.trim().parse().ok()) {
Some(p) => p,
None => continue,
};
let comm = match parts.next() {
Some(s) => s.trim().to_string(),
None => continue,
};
if comm.is_empty() {
continue;
}
let name = comm.rsplit('/').next().unwrap_or(&comm).to_string();
result.push((pid, name, Some(comm)));
}
Ok(result)
}
fn is_process_running(name: &str) -> bool {
match list_processes() {
Ok(procs) => procs.iter().any(|(_, comm, _)| comm == name),
Err(_) => false,
}
}
pub fn register_process(lua: &Lua) -> mlua::Result<()> {
let process_table = lua.create_table()?;
let list_fn = lua.create_function(|lua, ()| {
let proc_list = list_processes().map_err(mlua::Error::runtime)?;
let procs = lua.create_table()?;
for (i, (pid, name, cmdline)) in proc_list.into_iter().enumerate() {
let info = lua.create_table()?;
info.set("pid", pid)?;
info.set("name", name)?;
if let Some(cmd) = cmdline {
info.set("cmdline", cmd)?;
}
procs.set(i + 1, info)?;
}
Ok(procs)
})?;
process_table.set("list", list_fn)?;
let is_running_fn = lua.create_function(|_, name: String| Ok(is_process_running(&name)))?;
process_table.set("is_running", is_running_fn)?;
let kill_fn = lua.create_function(|_, args: mlua::MultiValue| {
let mut args_iter = args.into_iter();
let pid: i32 = args_iter
.next()
.ok_or_else(|| mlua::Error::runtime("process.kill: pid required"))
.and_then(|v| match v {
mlua::Value::Integer(n) => Ok(n as i32),
mlua::Value::Number(n) => Ok(n as i32),
_ => Err(mlua::Error::runtime("process.kill: pid must be a number")),
})?;
if pid <= 0 {
return Err(mlua::Error::runtime(format!(
"process.kill: pid must be > 0, got {pid}"
)));
}
let signal: i32 = args_iter
.next()
.map(|v| match v {
mlua::Value::Integer(n) => Ok(n as i32),
mlua::Value::Number(n) => Ok(n as i32),
mlua::Value::Nil => Ok(15), _ => Err(mlua::Error::runtime(
"process.kill: signal must be a number",
)),
})
.unwrap_or(Ok(15))?;
if signal < 0 {
return Err(mlua::Error::runtime(format!(
"process.kill: signal must be >= 0, got {signal}"
)));
}
let result = unsafe { libc::kill(pid, signal) };
if result == 0 {
Ok(true)
} else {
let err = std::io::Error::last_os_error();
Err(mlua::Error::runtime(format!(
"process.kill: failed to send signal {signal} to pid {pid}: {err}"
)))
}
})?;
process_table.set("kill", kill_fn)?;
let spawn_fn = lua.create_function(|lua, opts: mlua::Table| {
let cmd: String = opts
.get("cmd")
.map_err(|_| mlua::Error::runtime("process.spawn: opts.cmd (string) is required"))?;
let args: Option<Vec<String>> = opts.get::<Option<mlua::Table>>("args")?.map(|t| {
t.sequence_values::<String>()
.filter_map(|v| v.ok())
.collect()
});
let cwd: Option<String> = opts.get("cwd")?;
let env_pairs: Option<Vec<(String, String)>> =
opts.get::<Option<mlua::Table>>("env")?.map(|t| {
let mut pairs = Vec::new();
for entry in t.pairs::<String, String>().flatten() {
pairs.push(entry);
}
pairs
});
let stdout_path: Option<String> = opts.get("stdout")?;
let stderr_path: Option<String> = opts.get("stderr")?;
let mut command = std::process::Command::new(&cmd);
if let Some(a) = args {
command.args(a);
}
if let Some(ref d) = cwd {
command.current_dir(d);
}
if let Some(ref vars) = env_pairs {
for (k, v) in vars {
command.env(k, v);
}
}
command.stdin(std::process::Stdio::null());
if let Some(ref path) = stdout_path {
let file = std::fs::File::create(path).map_err(|e| {
mlua::Error::runtime(format!(
"process.spawn: failed to open stdout file '{path}': {e}"
))
})?;
command.stdout(std::process::Stdio::from(file));
}
if let Some(ref path) = stderr_path {
let file = std::fs::File::create(path).map_err(|e| {
mlua::Error::runtime(format!(
"process.spawn: failed to open stderr file '{path}': {e}"
))
})?;
command.stderr(std::process::Stdio::from(file));
}
let child = command.spawn().map_err(|e| {
mlua::Error::runtime(format!("process.spawn: failed to launch '{cmd}': {e}"))
})?;
let pid = child.id() as i32;
std::mem::forget(child);
let result = lua.create_table()?;
result.set("pid", pid)?;
Ok(result)
})?;
process_table.set("spawn", spawn_fn)?;
let wait_fn = lua.create_function(|lua, args: mlua::MultiValue| {
let mut iter = args.into_iter();
let pid_v = iter
.next()
.ok_or_else(|| mlua::Error::runtime("process.wait: pid required"))?;
let pid: i32 = match pid_v {
mlua::Value::Integer(n) => n as i32,
mlua::Value::Number(n) => n as i32,
_ => return Err(mlua::Error::runtime("process.wait: pid must be a number")),
};
if pid <= 0 {
return Err(mlua::Error::runtime("process.wait: pid must be > 0"));
}
let mut timeout_secs: Option<f64> = None;
if let Some(mlua::Value::Table(opts)) = iter.next() {
timeout_secs = opts.get::<Option<f64>>("timeout")?;
if let Some(t) = timeout_secs
&& (!t.is_finite() || t < 0.0)
{
return Err(mlua::Error::runtime(
"process.wait: timeout must be a non-negative finite number",
));
}
}
let deadline = timeout_secs.map(|s| Instant::now() + Duration::from_secs_f64(s));
let mut raw_status: i32 = 0;
loop {
let flags = if deadline.is_some() { libc::WNOHANG } else { 0 };
let ret = unsafe { libc::waitpid(pid, &mut raw_status, flags) };
if ret == pid {
break;
} else if ret == 0 && deadline.is_some() {
if Instant::now() >= deadline.unwrap() {
let result = lua.create_table()?;
result.set("status", -1)?;
result.set("exited", false)?;
result.set("signaled", false)?;
result.set("timed_out", true)?;
return Ok(result);
}
std::thread::sleep(Duration::from_millis(50));
continue;
} else if ret < 0 {
let err = std::io::Error::last_os_error();
return Err(mlua::Error::runtime(format!(
"process.wait: waitpid({pid}) failed: {err}"
)));
} else {
return Err(mlua::Error::runtime(format!(
"process.wait: waitpid({pid}) returned unexpected pid {ret}"
)));
}
}
let exited = libc::WIFEXITED(raw_status);
let signaled = libc::WIFSIGNALED(raw_status);
let status = if exited {
libc::WEXITSTATUS(raw_status)
} else if signaled {
128 + libc::WTERMSIG(raw_status)
} else {
-1
};
let result = lua.create_table()?;
result.set("status", status)?;
result.set("exited", exited)?;
result.set("signaled", signaled)?;
result.set("timed_out", false)?;
Ok(result)
})?;
process_table.set("wait", wait_fn)?;
lua.globals().set("process", process_table)?;
lua.load(
r#"
-- process.wait_idle(names, timeout, interval)
-- Wait until none of the named processes are running.
-- names: string or table of strings
-- timeout: max seconds to wait (default 30)
-- interval: poll interval in seconds (default 1)
-- Returns true if all idle, false if timed out.
function process.wait_idle(names, timeout, interval)
timeout = timeout or 30
interval = interval or 1
if type(names) == "string" then names = {names} end
local deadline = time() + timeout
while time() < deadline do
local any_running = false
for _, name in ipairs(names) do
if process.is_running(name) then
any_running = true
break
end
end
if not any_running then return true end
sleep(interval)
end
return false
end
"#,
)
.exec()?;
Ok(())
}