use std::io::Read;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::Sender;
use std::thread;
use serde::Serialize;
use serde_json::Value;
use super::paths;
use super::registry::InstalledModule;
use crate::event::AppEvent;
pub const MAX_IN_FLIGHT: usize = 32;
pub const LOG_LIMIT: usize = 200;
pub const OUTPUT_CAP: usize = 64 * 1024;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ModuleStatus {
Running,
Succeeded,
Failed,
}
#[derive(Clone, Serialize)]
pub struct ModuleCommandLog {
pub id: u64,
pub module_id: String,
pub label: String,
pub argv: Vec<String>,
pub status: ModuleStatus,
pub code: Option<i32>,
pub out: String,
pub err: String,
}
pub fn next_log_id() -> u64 {
static NEXT: AtomicU64 = AtomicU64::new(1);
NEXT.fetch_add(1, Ordering::Relaxed)
}
pub fn base_env(module: &InstalledModule, ctx: &Value) -> Vec<(String, String)> {
let config = paths::config_dir(&module.id);
let state = paths::state_dir(&module.id);
let _ = std::fs::create_dir_all(&config);
let _ = std::fs::create_dir_all(&state);
let mut env = vec![
("BOHAY_ENV".to_string(), "1".to_string()),
("BOHAY_MODULE_ID".to_string(), module.id.clone()),
(
"BOHAY_MODULE_ROOT".to_string(),
module.root.display().to_string(),
),
(
"BOHAY_MODULE_CONFIG_DIR".to_string(),
config.display().to_string(),
),
(
"BOHAY_MODULE_STATE_DIR".to_string(),
state.display().to_string(),
),
("BOHAY_MODULE_CONTEXT_JSON".to_string(), ctx.to_string()),
];
if let Some(sock) = crate::ipc::api::socket_path_env() {
env.push(("BOHAY_SOCKET_PATH".to_string(), sock));
}
if let Ok(exe) = std::env::current_exe() {
env.push(("BOHAY_BIN_PATH".to_string(), exe.display().to_string()));
}
env
}
pub fn spawn(
log_id: u64,
root: PathBuf,
argv: Vec<String>,
env: Vec<(String, String)>,
app_tx: Sender<AppEvent>,
) {
thread::spawn(move || {
let (code, out, err) = run(&root, &argv, &env);
let _ = app_tx.send(AppEvent::ModuleCommandFinished {
log_id,
code,
out,
err,
});
});
}
fn run(root: &PathBuf, argv: &[String], env: &[(String, String)]) -> (Option<i32>, String, String) {
let Some((program, args)) = argv.split_first() else {
return (None, String::new(), "empty command".to_string());
};
let mut cmd = Command::new(program);
cmd.args(args)
.current_dir(root)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
for (k, v) in env {
cmd.env(k, v);
}
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
return (
None,
String::new(),
format!("failed to spawn {program}: {e}"),
)
}
};
let mut so = child.stdout.take();
let mut se = child.stderr.take();
let t_out = thread::spawn(move || so.as_mut().map(read_capped).unwrap_or_default());
let t_err = thread::spawn(move || se.as_mut().map(read_capped).unwrap_or_default());
let status = child.wait();
let out = t_out.join().unwrap_or_default();
let err = t_err.join().unwrap_or_default();
match status {
Ok(s) => (s.code(), out, err),
Err(e) => (None, out, format!("{err}\nwait failed: {e}")),
}
}
fn read_capped<R: Read>(r: &mut R) -> String {
let mut kept = Vec::new();
let mut chunk = [0u8; 8192];
loop {
match r.read(&mut chunk) {
Ok(0) => break,
Ok(n) => {
if kept.len() < OUTPUT_CAP {
let take = (OUTPUT_CAP - kept.len()).min(n);
kept.extend_from_slice(&chunk[..take]);
}
}
Err(_) => break,
}
}
String::from_utf8_lossy(&kept).into_owned()
}