use std::collections::HashMap;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
use super::graph::{ServiceGraph, ServiceId};
#[derive(Debug, Clone)]
pub struct ChildProcess {
pub pid: u32,
pub ppid: u32,
pub comm: String,
pub state: char,
}
pub fn get_child_processes(parent_pid: u32) -> Vec<ChildProcess> {
let mut children = Vec::new();
let proc_dir = match fs::read_dir("/proc") {
Ok(dir) => dir,
Err(_) => return children,
};
for entry in proc_dir.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
let pid: u32 = match name_str.parse() {
Ok(p) => p,
Err(_) => continue,
};
let stat_path = format!("/proc/{}/stat", pid);
let stat_content = match fs::read_to_string(&stat_path) {
Ok(c) => c,
Err(_) => continue,
};
let comm_start = match stat_content.find('(') {
Some(i) => i + 1,
None => continue,
};
let comm_end = match stat_content.rfind(')') {
Some(i) => i,
None => continue,
};
let comm = stat_content[comm_start..comm_end].to_string();
let after_comm = &stat_content[comm_end + 2..];
let parts: Vec<&str> = after_comm.split_whitespace().collect();
if parts.len() < 2 {
continue;
}
let state = parts[0].chars().next().unwrap_or('?');
let ppid: u32 = match parts[1].parse() {
Ok(p) => p,
Err(_) => continue,
};
if ppid == parent_pid {
children.push(ChildProcess {
pid,
ppid,
comm,
state,
});
}
}
children
}
pub fn get_process_tree(pid: u32) -> Vec<ChildProcess> {
let mut all_descendants = Vec::new();
let mut to_visit = vec![pid];
while let Some(current_pid) = to_visit.pop() {
let children = get_child_processes(current_pid);
for child in &children {
to_visit.push(child.pid);
}
all_descendants.extend(children);
}
all_descendants
}
pub fn format_process_tree(pid: u32, service_name: &str) -> String {
let mut lines = Vec::new();
lines.push(format!("{} (pid={})", service_name, pid));
let children = get_process_tree(pid);
for (i, child) in children.iter().enumerate() {
let prefix = if i == children.len() - 1 {
"└─"
} else {
"├─"
};
let state_desc = match child.state {
'R' => "running",
'S' => "sleeping",
'D' => "disk sleep",
'Z' => "zombie",
'T' => "stopped",
'X' => "dead",
_ => "unknown",
};
lines.push(format!(
" {} {} (pid={}, ppid={}, {})",
prefix, child.comm, child.pid, child.ppid, state_desc
));
}
if children.is_empty() {
lines.push(" └─ (no child processes)".to_string());
}
lines.join("\n")
}
pub fn format_graph_state(
graph: &ServiceGraph,
pending_timers: &HashMap<ServiceId, Vec<String>>,
) -> String {
let mut lines = Vec::new();
lines.push("=== Service Graph State ===".to_string());
lines.push(String::new());
let order = graph.start_order();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
for id in &order {
let service = match graph.get(*id) {
Some(s) => s,
None => continue,
};
let state_info = match &service.state {
crate::sdk::ServiceState::Running { pid } => {
let uptime = service
.started_at
.map(|started| {
let secs = (now.saturating_sub(started)) / 1000;
format_duration(secs)
})
.unwrap_or_else(|| "?".to_string());
format!("Running (pid={}, uptime={})", pid, uptime)
}
crate::sdk::ServiceState::Starting { pid } => {
format!("Starting (pid={})", pid)
}
crate::sdk::ServiceState::Stopping { pid } => {
format!("Stopping (pid={})", pid)
}
crate::sdk::ServiceState::Blocked { waiting_on } => {
format!("Blocked (waiting on: {})", waiting_on.join(", "))
}
crate::sdk::ServiceState::Exited { exit_code } => {
format!("Exited (code={:?})", exit_code)
}
crate::sdk::ServiceState::Failed { reason } => {
format!("Failed ({})", reason)
}
crate::sdk::ServiceState::Inactive => "Inactive".to_string(),
};
lines.push(format!(
"{} {} - {}",
service.state.symbol(),
service.name,
state_info
));
if service.restart_count > 0 {
lines.push(format!(
" restart_count: {}, next_delay: {}ms",
service.restart_count, service.current_restart_delay_ms
));
}
let deps = graph.dependencies(*id);
if !deps.is_empty() {
lines.push(" ← depends on:".to_string());
for (dep_id, dep_type) in &deps {
let dep = graph.get(*dep_id);
if let Some(dep) = dep {
let satisfied = dep.state.is_satisfied();
let marker = if satisfied { "✓" } else { "✗" };
lines.push(format!(
" {} {} {} ({})",
marker,
dep_type,
dep.name,
dep.state.name()
));
}
}
}
let dependents = graph.dependents(*id);
if !dependents.is_empty() {
let names: Vec<String> = dependents
.iter()
.filter_map(|did| graph.get(*did))
.map(|s| s.name.clone())
.collect();
lines.push(format!(" → required by: {}", names.join(", ")));
}
if let Some(timers) = pending_timers.get(id)
&& !timers.is_empty()
{
lines.push(format!(" ⏱ pending: {}", timers.join(", ")));
}
if let Some(pid) = service.state.pid()
&& pid > 0
{
let children = get_child_processes(pid);
if !children.is_empty() {
lines.push(" └─ child processes:".to_string());
for child in &children {
lines.push(format!(
" └─ {} (pid={}, state={})",
child.comm, child.pid, child.state
));
}
}
}
lines.push(String::new());
}
lines.join("\n")
}
fn format_duration(secs: u64) -> String {
if secs < 60 {
format!("{}s", secs)
} else if secs < 3600 {
format!("{}m{}s", secs / 60, secs % 60)
} else {
format!("{}h{}m", secs / 3600, (secs % 3600) / 60)
}
}
#[derive(Debug, Clone)]
pub struct StateTransition {
pub timestamp_ms: u64,
pub service: String,
pub from_state: String,
pub to_state: String,
pub trigger: String,
}
impl StateTransition {
pub fn new(service: &str, from: &str, to: &str, trigger: &str) -> Self {
let timestamp_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
Self {
timestamp_ms,
service: service.to_string(),
from_state: from.to_string(),
to_state: to.to_string(),
trigger: trigger.to_string(),
}
}
}
pub fn format_state_transition(service: &str, from: &str, to: &str, trigger: &str) -> String {
format!("state: {} {} → {} ({})", service, from, to, trigger)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_duration() {
assert_eq!(format_duration(0), "0s");
assert_eq!(format_duration(45), "45s");
assert_eq!(format_duration(60), "1m0s");
assert_eq!(format_duration(90), "1m30s");
assert_eq!(format_duration(3600), "1h0m");
assert_eq!(format_duration(3661), "1h1m");
}
#[test]
fn test_get_child_processes_self() {
let children = get_child_processes(std::process::id());
let _ = children;
}
}