g3 console initial cut + error doesnt kill auto

This commit is contained in:
Dhanji Prasanna
2025-11-04 11:39:26 +11:00
parent 6913c5f72e
commit aaf918828f
53 changed files with 6796 additions and 23 deletions

View File

@@ -0,0 +1,270 @@
use anyhow::{anyhow, Context, Result};
use std::process::{Command, Stdio};
use std::os::unix::process::CommandExt;
use std::collections::HashMap;
use std::sync::Mutex;
use std::path::PathBuf;
use sysinfo::{Pid, Signal, System, Process};
use tracing::{debug, error, info};
use crate::models::LaunchParams;
pub struct ProcessController {
system: System,
launch_params: Mutex<HashMap<u32, LaunchParams>>,
}
impl ProcessController {
pub fn new() -> Self {
Self {
system: System::new_all(),
launch_params: Mutex::new(HashMap::new()),
}
}
pub fn kill_process(&mut self, pid: u32) -> Result<()> {
let sysinfo_pid = Pid::from_u32(pid);
self.system.refresh_processes();
if let Some(process) = self.system.process(sysinfo_pid) {
info!("Killing process {} ({})", pid, process.name());
// Try SIGTERM first
if process.kill_with(Signal::Term).is_some() {
debug!("Sent SIGTERM to process {}", pid);
// Wait a bit and check if it's still running
std::thread::sleep(std::time::Duration::from_secs(2));
self.system.refresh_processes();
if self.system.process(sysinfo_pid).is_some() {
// Still running, send SIGKILL
if let Some(proc) = self.system.process(sysinfo_pid) {
proc.kill_with(Signal::Kill);
debug!("Sent SIGKILL to process {}", pid);
}
}
Ok(())
} else {
Err(anyhow!("Failed to send signal to process {}", pid))
}
} else {
Err(anyhow!("Process {} not found", pid))
}
}
#[cfg(unix)]
pub fn launch_g3(
&mut self,
workspace: &str,
provider: &str,
model: &str,
prompt: &str,
autonomous: bool,
g3_binary_path: Option<&str>,
) -> Result<u32> {
let binary = g3_binary_path.unwrap_or("g3");
let mut cmd = Command::new(binary);
cmd.arg("--workspace")
.arg(workspace)
.arg("--provider")
.arg(provider)
.arg("--model")
.arg(model);
if autonomous {
cmd.arg("--autonomous");
}
cmd.arg(prompt);
// Run in background with proper detachment
cmd.stdout(Stdio::null())
.stderr(Stdio::null())
.stdin(Stdio::null());
// Double-fork technique to prevent zombie processes:
// 1. Fork once to create intermediate process
// 2. Intermediate process forks again and exits immediately
// 3. Grandchild is adopted by init (PID 1) which will reap it
unsafe {
cmd.pre_exec(|| {
// Fork again inside the child
match libc::fork() {
-1 => return Err(std::io::Error::last_os_error()),
0 => {
// Grandchild: create new session and continue
libc::setsid();
// Continue execution (this becomes the actual g3 process)
}
_ => {
// Child: exit immediately so parent can reap it
libc::_exit(0);
}
}
Ok(())
});
}
info!("Launching g3: {:?}", cmd);
// Spawn and wait for the intermediate process to exit
let mut child = cmd.spawn().context("Failed to spawn g3 process")?;
let intermediate_pid = child.id();
// Wait for intermediate process (it will exit immediately after forking)
child.wait().context("Failed to wait for intermediate process")?;
// The actual g3 process is now running as orphan
// We need to scan for it by matching workspace and recent start time
info!("Scanning for newly launched g3 process in workspace: {}", workspace);
// Wait a moment for the process to fully start
std::thread::sleep(std::time::Duration::from_millis(500));
// Refresh and scan for the process
self.system.refresh_processes();
let workspace_path = PathBuf::from(workspace);
let mut found_pid = None;
for (pid, process) in self.system.processes() {
let cmd = process.cmd();
let cmd_str = cmd.join(" ");
// Check if this is a g3 process
let is_g3 = process.name().contains("g3") || cmd_str.contains("g3");
if !is_g3 {
continue;
}
// Check if it has our workspace
let has_workspace = cmd.iter().any(|arg| {
if let Ok(path) = PathBuf::from(arg).canonicalize() {
if let Ok(ws) = workspace_path.canonicalize() {
return path == ws;
}
}
false
});
if has_workspace {
// Check if it's recent (started within last 5 seconds)
let now = std::time::SystemTime::now();
let start_time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(process.start_time());
if let Ok(duration) = now.duration_since(start_time) {
if duration.as_secs() < 5 {
found_pid = Some(pid.as_u32());
break;
}
}
}
}
let pid = found_pid.unwrap_or(intermediate_pid);
info!("Launched g3 process with PID {}", pid);
// Store launch params for restart
let params = LaunchParams {
workspace: workspace.into(),
provider: provider.to_string(),
model: model.to_string(),
prompt: prompt.to_string(),
autonomous,
g3_binary_path: g3_binary_path.map(|s| s.to_string()),
};
if let Ok(mut map) = self.launch_params.lock() {
map.insert(pid, params);
}
Ok(pid)
}
pub fn get_launch_params(&mut self, pid: u32) -> Option<LaunchParams> {
// First check if we have stored params (for console-launched instances)
if let Ok(map) = self.launch_params.lock() {
if let Some(params) = map.get(&pid) {
return Some(params.clone());
}
}
// If not found, try to parse from process command line (for detected instances)
self.system.refresh_processes();
let sysinfo_pid = Pid::from_u32(pid);
if let Some(process) = self.system.process(sysinfo_pid) {
let cmd = process.cmd();
return self.parse_launch_params_from_cmd(cmd);
}
None
}
fn parse_launch_params_from_cmd(&self, cmd: &[String]) -> Option<LaunchParams> {
let mut workspace = None;
let mut provider = None;
let mut model = None;
let mut prompt = None;
let mut autonomous = false;
let mut g3_binary_path = None;
let mut i = 0;
while i < cmd.len() {
match cmd[i].as_str() {
"--workspace" | "-w" if i + 1 < cmd.len() => {
workspace = Some(PathBuf::from(&cmd[i + 1]));
i += 2;
}
"--provider" if i + 1 < cmd.len() => {
provider = Some(cmd[i + 1].clone());
i += 2;
}
"--model" if i + 1 < cmd.len() => {
model = Some(cmd[i + 1].clone());
i += 2;
}
"--autonomous" => {
autonomous = true;
i += 1;
}
_ => {
// Last non-flag argument is likely the prompt
if !cmd[i].starts_with('-') && i == cmd.len() - 1 {
prompt = Some(cmd[i].clone());
}
i += 1;
}
}
}
// Try to determine binary path from cmd[0]
if !cmd.is_empty() {
let first = &cmd[0];
if first.contains("g3") && !first.contains("cargo") {
g3_binary_path = Some(first.clone());
}
}
// Only return params if we have the minimum required fields
if let (Some(ws), Some(prov), Some(mdl), Some(prmt)) = (workspace, provider, model, prompt) {
Some(LaunchParams {
workspace: ws,
provider: prov,
model: mdl,
prompt: prmt,
autonomous,
g3_binary_path,
})
} else {
None
}
}
}
impl Default for ProcessController {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,172 @@
use crate::models::{ExecutionMethod, Instance, InstanceStatus, InstanceType};
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use std::path::PathBuf;
use std::process::Command;
use sysinfo::{System, Process, Pid};
use tracing::{debug, warn};
pub struct ProcessDetector {
system: System,
}
impl ProcessDetector {
pub fn new() -> Self {
Self {
system: System::new_all(),
}
}
pub fn detect_instances(&mut self) -> Result<Vec<Instance>> {
self.system.refresh_processes();
let mut instances = Vec::new();
// Find all g3 processes
for (pid, process) in self.system.processes() {
let cmd = process.cmd();
if cmd.is_empty() {
continue;
}
// Check if this is a g3 process (binary or cargo run)
if let Some(instance) = self.parse_g3_process(*pid, process, cmd) {
instances.push(instance);
}
}
debug!("Detected {} g3 instances", instances.len());
Ok(instances)
}
fn parse_g3_process(
&self,
pid: Pid,
process: &Process,
cmd: &[String],
) -> Option<Instance> {
let cmd_str = cmd.join(" ");
// Check if this is a g3 binary
let is_g3_binary = cmd.get(0).map(|s| s.ends_with("g3")).unwrap_or(false);
// Check if this is cargo run with g3
let is_cargo_run = cmd.get(0).map(|s| s.contains("cargo")).unwrap_or(false)
&& cmd.iter().any(|s| s == "run" || s.contains("g3"));
if !is_g3_binary && !is_cargo_run {
return None;
}
// Extract workspace directory
let workspace = self.extract_workspace(pid, process, cmd)?;
// Determine execution method
let execution_method = if is_cargo_run {
ExecutionMethod::CargoRun
} else {
ExecutionMethod::Binary
};
// Determine instance type (ensemble if --autonomous flag present)
let instance_type = if cmd.iter().any(|s| s == "--autonomous") {
InstanceType::Ensemble
} else {
InstanceType::Single
};
// Extract provider and model
let provider = self.extract_flag_value(cmd, "--provider");
let model = self.extract_flag_value(cmd, "--model");
// Get start time
let start_time = DateTime::from_timestamp(process.start_time() as i64, 0)
.unwrap_or_else(Utc::now);
// Generate instance ID from PID and start time
let id = format!("{}_{}", pid, start_time.timestamp());
Some(Instance {
id,
pid: pid.as_u32(),
workspace,
start_time,
status: InstanceStatus::Running,
instance_type,
provider,
model,
execution_method,
command_line: cmd_str,
launch_params: None, // Not available for detected processes
})
}
fn extract_workspace(&self, pid: Pid, process: &Process, cmd: &[String]) -> Option<PathBuf> {
// Look for --workspace flag
for i in 0..cmd.len() {
if cmd[i] == "--workspace" && i + 1 < cmd.len() {
return Some(PathBuf::from(&cmd[i + 1]));
}
if cmd[i] == "-w" && i + 1 < cmd.len() {
return Some(PathBuf::from(&cmd[i + 1]));
}
}
// Fallback: Try to get the working directory of the process
#[cfg(target_os = "linux")]
{
// On Linux, read /proc/<pid>/cwd symlink
let cwd_path = format!("/proc/{}/cwd", pid.as_u32());
if let Ok(cwd) = std::fs::read_link(&cwd_path) {
debug!("Found workspace via /proc for PID {}: {:?}", pid, cwd);
return Some(cwd);
}
}
#[cfg(target_os = "macos")]
{
// On macOS, use lsof to get the current working directory
if let Ok(output) = std::process::Command::new("lsof")
.args(["-p", &pid.as_u32().to_string(), "-a", "-d", "cwd", "-Fn"])
.output()
{
if let Ok(stdout) = String::from_utf8(output.stdout) {
if let Some(line) = stdout.lines().find(|l| l.starts_with('n')) {
let cwd = PathBuf::from(&line[1..]);
debug!("Found workspace via lsof for PID {}: {:?}", pid, cwd);
return Some(cwd);
}
}
}
}
// Final fallback: use current directory of console
warn!("Could not determine workspace for PID {}, using current directory", pid);
std::env::current_dir().ok()
}
fn extract_flag_value(&self, cmd: &[String], flag: &str) -> Option<String> {
for i in 0..cmd.len() {
if cmd[i] == flag && i + 1 < cmd.len() {
return Some(cmd[i + 1].clone());
}
}
None
}
pub fn get_process_status(&mut self, pid: u32) -> Option<InstanceStatus> {
self.system.refresh_processes();
let sysinfo_pid = Pid::from_u32(pid);
if self.system.process(sysinfo_pid).is_some() {
Some(InstanceStatus::Running)
} else {
Some(InstanceStatus::Terminated)
}
}
}
impl Default for ProcessDetector {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,5 @@
pub mod detector;
pub mod controller;
pub use detector::*;
pub use controller::*;