use std::path::Path;
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, Signal, System};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
pub fn running_pids(exe_name: &str) -> Vec<u32> {
let mut sys = System::new();
sys.refresh_processes_specifics(ProcessesToUpdate::All, true, ProcessRefreshKind::nothing());
sys.processes()
.values()
.filter(|p| matches_exe(p.name().to_string_lossy().as_ref(), exe_name))
.map(|p| p.pid().as_u32())
.collect()
}
pub fn ensure_not_running(exe_name: &str) -> Result<(), crate::error::Error> {
let pids = running_pids(exe_name);
if pids.is_empty() {
Ok(())
} else {
Err(crate::error::Error::AlreadyRunning {
name: exe_name.to_string(),
pids,
})
}
}
pub fn kill_by_name(exe_name: &str) -> usize {
let mut sys = System::new();
sys.refresh_processes_specifics(ProcessesToUpdate::All, true, ProcessRefreshKind::nothing());
let mut count = 0usize;
for process in sys.processes().values() {
if matches_exe(process.name().to_string_lossy().as_ref(), exe_name) {
let _ = process
.kill_with(Signal::Term)
.or_else(|| Some(process.kill()));
count += 1;
}
}
count
}
pub async fn spawn_and_wait_for_listening(
binary_path: &Path,
address: &str,
port: u16,
) -> Result<String, crate::error::Error> {
let name = binary_path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| binary_path.display().to_string());
let mut cmd = Command::new(binary_path);
cmd.env("ADDRESS", address)
.env("PORT", port.to_string())
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0800_0008);
}
let mut child = cmd
.spawn()
.map_err(|e| crate::error::Error::Spawn(name.clone(), e))?;
let stderr = child
.stderr
.take()
.ok_or_else(|| crate::error::Error::SpawnNoListeningLine { name: name.clone() })?;
let mut reader = BufReader::new(stderr).lines();
let mut listening_line: Option<String> = None;
loop {
tokio::select! {
line = reader.next_line() => {
match line {
Ok(Some(line)) => {
if line.to_ascii_lowercase().contains("listening") {
listening_line = Some(line);
break;
}
}
Ok(None) => break, Err(_) => break,
}
}
status = child.wait() => {
let _ = status;
break;
}
}
}
let Some(line) = listening_line else {
return Err(crate::error::Error::SpawnNoListeningLine { name });
};
tokio::spawn(async move { while let Ok(Some(_)) = reader.next_line().await {} });
drop(child);
Ok(line)
}
pub fn apply_config_env(cmd: &mut Command, cfg: &crate::Config) {
if cfg.config_set_forbidden {
cmd.env("CONFIG_SET_FORBIDDEN", "true");
}
if let Some(v) = cfg.config_base_dir.as_deref() {
cmd.env("CONFIG_BASE_DIR", v);
}
if let Some(v) = cfg.commit_author_name.as_deref() {
cmd.env("COMMIT_AUTHOR_NAME", v);
}
if let Some(v) = cfg.commit_author_email.as_deref() {
cmd.env("COMMIT_AUTHOR_EMAIL", v);
}
if let Some(v) = cfg.github_authorization.as_deref() {
cmd.env("GITHUB_AUTHORIZATION", v);
}
cmd.env("OBJECTIVEAI_AGENT_INSTANCE_HIERARCHY", &cfg.agent_instance_hierarchy);
match cfg.agent_id.as_deref() {
Some(v) => {
cmd.env("OBJECTIVEAI_AGENT_ID", v);
}
None => {
cmd.env_remove("OBJECTIVEAI_AGENT_ID");
}
}
match cfg.agent_full_id.as_deref() {
Some(v) => {
cmd.env("OBJECTIVEAI_AGENT_FULL_ID", v);
}
None => {
cmd.env_remove("OBJECTIVEAI_AGENT_FULL_ID");
}
}
match cfg.agent_remote.as_deref() {
Some(v) => {
cmd.env("OBJECTIVEAI_AGENT_REMOTE", v);
}
None => {
cmd.env_remove("OBJECTIVEAI_AGENT_REMOTE");
}
}
match cfg.response_id.as_deref() {
Some(v) => {
cmd.env("OBJECTIVEAI_RESPONSE_ID", v);
}
None => {
cmd.env_remove("OBJECTIVEAI_RESPONSE_ID");
}
}
match cfg.response_ids.as_deref() {
Some(v) => {
cmd.env("OBJECTIVEAI_RESPONSE_IDS", v);
}
None => {
cmd.env_remove("OBJECTIVEAI_RESPONSE_IDS");
}
}
match cfg.mcp_session_id.as_deref() {
Some(v) => {
cmd.env(objectiveai_sdk::mcp::MCP_SESSION_ID_ENV, v);
}
None => {
cmd.env_remove(objectiveai_sdk::mcp::MCP_SESSION_ID_ENV);
}
}
match cfg.plugin_owner.as_deref() {
Some(v) => {
cmd.env("OBJECTIVEAI_PLUGIN_OWNER", v);
}
None => {
cmd.env_remove("OBJECTIVEAI_PLUGIN_OWNER");
}
}
match cfg.plugin_repository.as_deref() {
Some(v) => {
cmd.env("OBJECTIVEAI_PLUGIN_REPOSITORY", v);
}
None => {
cmd.env_remove("OBJECTIVEAI_PLUGIN_REPOSITORY");
}
}
match cfg.plugin_version.as_deref() {
Some(v) => {
cmd.env("OBJECTIVEAI_PLUGIN_VERSION", v);
}
None => {
cmd.env_remove("OBJECTIVEAI_PLUGIN_VERSION");
}
}
}
fn matches_exe(observed: &str, target: &str) -> bool {
let trim = |s: &str| {
s.strip_suffix(".exe")
.or_else(|| s.strip_suffix(".EXE"))
.unwrap_or(s)
.to_ascii_lowercase()
};
trim(observed) == trim(target)
}