use std::io;
use std::path::PathBuf;
#[ derive( Debug ) ]
pub struct ProcessInfo
{
pub pid : u32,
pub cmdline : String,
pub cwd : PathBuf,
pub args : Vec< String >,
}
#[ inline ]
#[ must_use ]
pub fn find_claude_processes() -> Vec< ProcessInfo >
{
let self_pid = std::process::id();
let mut result = vec![];
let proc_root = std::env::var( "CLR_PROC_DIR" )
.unwrap_or_else( |_| "/proc".to_string() );
let Ok( proc_dir ) = std::fs::read_dir( &proc_root ) else { return result; };
for entry in proc_dir
{
let Ok( entry ) = entry else { continue; };
let name = entry.file_name();
let name_str = name.to_string_lossy();
let Ok( pid ) : Result< u32, _ > = name_str.parse() else { continue; };
if pid == self_pid { continue; }
let cmdline_path = format!( "{proc_root}/{pid}/cmdline" );
let Ok( cmdline_raw ) = std::fs::read( &cmdline_path ) else { continue; };
let args : Vec< String > = cmdline_raw
.split( | &b | b == b'\0' )
.filter( | s | !s.is_empty() )
.map( | s | String::from_utf8_lossy( s ).into_owned() )
.collect();
let binary_path = args.first().map_or( "", String::as_str );
let binary_name = std::path::Path::new( binary_path )
.file_name()
.and_then( | s | s.to_str() )
.unwrap_or( "" );
if binary_name != "claude" { continue; }
let cwd_path = format!( "{proc_root}/{pid}/cwd" );
let cwd = std::fs::read_link( &cwd_path ).unwrap_or_default();
let cmdline = args.join( " " );
result.push( ProcessInfo { pid, cmdline, cwd, args } );
}
result
}
#[ cfg( target_os = "linux" ) ]
#[ derive( Debug ) ]
pub struct ProcessMetrics
{
pub state : char,
pub ram_kb : u64,
pub cpu_pct : f32,
pub started_at : u64,
}
#[ cfg( target_os = "linux" ) ]
#[ inline ]
#[ must_use ]
#[ allow( clippy::cast_possible_truncation, clippy::cast_sign_loss ) ]
pub fn read_process_metrics( pid : u32 ) -> Option< ProcessMetrics >
{
let stat = std::fs::read_to_string( format!( "/proc/{pid}/stat" ) ).ok()?;
let after_comm = stat.find( ") " )?;
let rest = stat[ after_comm + 2 .. ].trim_start();
let mut fields = rest.split_whitespace();
let state = fields.next()?.chars().next().unwrap_or( '?' );
for _ in 0..10 { fields.next()?; }
let utime : u64 = fields.next()?.parse().ok()?; let stime : u64 = fields.next()?.parse().ok()?; for _ in 0..6 { fields.next()?; }
let starttime : u64 = fields.next()?.parse().ok()?;
let uptime_raw = std::fs::read_to_string( "/proc/uptime" ).ok()?;
let uptime_secs : f64 = uptime_raw.split_whitespace().next()?.parse().ok()?;
let status = std::fs::read_to_string( format!( "/proc/{pid}/status" ) ).ok()?;
let mut ram_kb = 0_u64;
for line in status.lines()
{
if let Some( rest ) = line.strip_prefix( "VmRSS:" )
{
ram_kb = rest.split_whitespace().next().and_then( | v | v.parse().ok() ).unwrap_or( 0 );
break;
}
}
let hz : f64 = 100.0;
let cpu_total_secs = ( utime + stime ) as f64 / hz;
let cpu_pct = if uptime_secs > 0.0
{
( cpu_total_secs / uptime_secs * 100.0 ) as f32
}
else
{
0.0_f32
};
let current_unix : u64 = std::time::SystemTime::now()
.duration_since( std::time::UNIX_EPOCH )
.map_or( 0, | d | d.as_secs() );
let boot_epoch = current_unix.saturating_sub( uptime_secs as u64 );
let started_at = boot_epoch.saturating_add( starttime / hz as u64 );
Some( ProcessMetrics { state, ram_kb, cpu_pct, started_at } )
}
#[ inline ]
pub fn send_sigterm( pid : u32 ) -> Result< (), io::Error >
{
run_kill( &[ "-TERM", &pid.to_string() ] )
}
#[ inline ]
pub fn send_sigkill( pid : u32 ) -> Result< (), io::Error >
{
run_kill( &[ "-KILL", &pid.to_string() ] )
}
fn run_kill( args : &[ &str ] ) -> Result< (), io::Error >
{
let status = std::process::Command::new( "kill" )
.args( args )
.status()
.map_err( | e | io::Error::other( e.to_string() ) )?;
if status.success()
{
Ok( () )
}
else
{
Err( io::Error::other(
format!( "kill {} exited with: {status}", args.join( " " ) ),
) )
}
}