Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/cli/supervisor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ impl Supervisor {
///
/// This is a low-level helper — callers are responsible for user-facing messages.
pub async fn kill_or_stop(existing_pid: u32, force: bool) -> Result<KillOrStopOutcome> {
PROCS.refresh_pids(&[existing_pid]);
if PROCS.is_running(existing_pid) {
if force {
debug!("killing pid {existing_pid}");
Expand Down
1 change: 1 addition & 0 deletions src/cli/supervisor/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ impl Start {
let pid = existing_pid.expect("Killed implies a pid exists");
// Wait briefly for the old process to fully exit
for _ in 0..20 {
PROCS.refresh_pids(&[pid]);
if !PROCS.is_running(pid) {
break;
}
Expand Down
2 changes: 1 addition & 1 deletion src/cli/wait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ impl Wait {

let mut interval = time::interval(time::Duration::from_millis(100));
loop {
PROCS.refresh_pids(&[pid]);
if !PROCS.is_running(pid) {
break;
}
interval.tick().await;
PROCS.refresh_processes();
}

Ok(())
Expand Down
28 changes: 24 additions & 4 deletions src/procs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,9 @@ impl Default for Procs {

impl Procs {
pub fn new() -> Self {
let procs = Self {
Self {
system: Mutex::new(sysinfo::System::new()),
};
procs.refresh_processes();
procs
}
}

fn lock_system(&self) -> std::sync::MutexGuard<'_, sysinfo::System> {
Expand Down Expand Up @@ -69,6 +67,7 @@ impl Procs {
children
}

/// Async wrapper for `kill_process_group`. See its docs for details and precondition.
pub async fn kill_process_group_async(
&self,
pid: u32,
Expand All @@ -90,6 +89,11 @@ impl Procs {
/// so this atomically signals all descendant processes.
///
/// Returns `Err` if the signal could not be sent (e.g. permission denied).
///
/// **Precondition:** the caller must have called `refresh_pids` or `refresh_processes`
/// for `pid` before invoking this method. Without a prior refresh, the internal
/// process map is empty and this method will incorrectly conclude the process is
/// already dead (`Ok(false)`) without sending any signal.
#[cfg(unix)]
fn kill_process_group(
&self,
Expand Down Expand Up @@ -176,6 +180,7 @@ impl Procs {
self.kill(pid, 0, None)
}

/// Async wrapper for `kill`. See its docs for details and precondition.
pub async fn kill_async(
&self,
pid: u32,
Expand All @@ -196,6 +201,11 @@ impl Procs {
///
/// Returns `Err` if the signal could not be sent (e.g. permission denied
/// when targeting a process owned by another user/root).
///
/// **Precondition:** the caller must have called `refresh_pids` or `refresh_processes`
/// for `pid` before invoking this method. Without a prior refresh, the internal
/// process map is empty and this method will incorrectly conclude the process is
/// already dead (`Ok(false)`) without sending any signal.
fn kill(
&self,
pid: u32,
Expand Down Expand Up @@ -335,6 +345,16 @@ impl Procs {
.refresh_processes(ProcessesToUpdate::Some(&sysinfo_pids), true);
}

/// Collect daemon PIDs from a StateFile and refresh only those.
/// Avoids the cost of refreshing all system processes when we only
/// need stats for managed daemons.
pub(crate) fn refresh_daemon_pids(&self, state: &crate::state_file::StateFile) {
let pids: Vec<u32> = state.daemons.values().filter_map(|d| d.pid).collect();
if !pids.is_empty() {
self.refresh_pids(&pids);
}
}

/// Get aggregated stats for multiple process groups in a single pass.
///
/// Builds the parent→children map once (O(N)) and then BFS-es from each
Expand Down
3 changes: 2 additions & 1 deletion src/supervisor/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ impl Supervisor {
}
};
info!("started daemon {id} with pid {pid}");
PROCS.refresh_pids(&[pid]);
let daemon = self
.upsert_daemon(
UpsertDaemonOpts::builder(id.clone())
Expand Down Expand Up @@ -1107,7 +1108,7 @@ impl Supervisor {
trace!("daemon to stop: {daemon}");
if let Some(pid) = daemon.pid {
trace!("killing pid: {pid}");
PROCS.refresh_processes();
PROCS.refresh_pids(&[pid]);
if PROCS.is_running(pid) {
// First set status to Stopping (preserve PID for monitoring task)
self.upsert_daemon(
Expand Down
8 changes: 6 additions & 2 deletions src/supervisor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,11 @@ pub fn start_if_not_running() -> Result<()> {
let sf = StateFile::get();
if let Some(d) = sf.daemons.get(&DaemonId::pitchfork())
&& let Some(pid) = d.pid
&& PROCS.is_running(pid)
{
return Ok(());
PROCS.refresh_pids(&[pid]);
if PROCS.is_running(pid) {
return Ok(());
}
}
start_in_background()
}
Expand Down Expand Up @@ -166,6 +168,8 @@ impl Supervisor {
fix_state_dir_permissions();

let pid = std::process::id();
// Ensure PROCS has data for the supervisor PID before upsert_daemon reads title()
PROCS.refresh_pids(&[pid]);
// Determine container mode: CLI flag takes priority, then settings
let container_mode = container || settings().supervisor.container;
if container_mode {
Expand Down
5 changes: 4 additions & 1 deletion src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1150,7 +1150,10 @@ impl App {
}

fn refresh_process_stats(&mut self) {
PROCS.refresh_processes();
let pids: Vec<u32> = self.daemons.iter().filter_map(|d| d.pid).collect();
if !pids.is_empty() {
PROCS.refresh_pids(&pids);
}
self.process_stats.clear();
for daemon in &self.daemons {
if let Some(pid) = daemon.pid
Expand Down
28 changes: 19 additions & 9 deletions src/web/routes/daemons.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use axum::{
use serde::Deserialize;

use crate::daemon::is_valid_daemon_id;
use crate::daemon_list::get_all_daemons_direct;
use crate::daemon_list::{DaemonListEntry, get_all_daemons_direct};
use crate::env;
use crate::ipc::batch::{StartOptions, build_run_options};
use crate::pitchfork_toml::PitchforkToml;
Expand All @@ -17,6 +17,14 @@ use crate::web::helpers::{
css_safe_id, daemon_row, format_daemon_id_html, html_escape, url_encode,
};

/// Refresh process info for accurate CPU/memory stats (only managed daemons).
fn refresh_daemon_list_pids(entries: &[DaemonListEntry]) {
let pids: Vec<u32> = entries.iter().filter_map(|e| e.daemon.pid).collect();
if !pids.is_empty() {
PROCS.refresh_pids(&pids);
}
}

/// Get daemon command from the stored cmd field
fn get_daemon_command(daemon: &crate::daemon::Daemon) -> String {
daemon
Expand Down Expand Up @@ -89,12 +97,12 @@ pub async fn list() -> Html<String> {

async fn list_content() -> String {
let bp = bp();
// Refresh process info for accurate CPU/memory stats
PROCS.refresh_processes();

let entries = get_all_daemons_direct(&SUPERVISOR)
.await
.unwrap_or_default();

refresh_daemon_list_pids(&entries);
let mut rows = String::new();

for entry in entries {
Expand Down Expand Up @@ -158,12 +166,12 @@ async fn list_content() -> String {

pub async fn list_partial() -> Html<String> {
let bp = bp();
// Refresh process info for accurate CPU/memory stats
PROCS.refresh_processes();

let entries = get_all_daemons_direct(&SUPERVISOR)
.await
.unwrap_or_default();

refresh_daemon_list_pids(&entries);
let mut rows = String::new();

for entry in entries {
Expand Down Expand Up @@ -218,13 +226,15 @@ pub async fn show(Path(id): Path<String>) -> Html<String> {
}
};

// Refresh process info for accurate stats
PROCS.refresh_processes();
// Refresh process info for accurate stats (only this daemon's PID)
let state = StateFile::read(&*env::PITCHFORK_STATE_FILE)
.unwrap_or_else(|_| StateFile::new(env::PITCHFORK_STATE_FILE.clone()));
if let Some(pid) = state.daemons.get(&daemon_id).and_then(|d| d.pid) {
PROCS.refresh_pids(&[pid]);
}

let safe_id = html_escape(&id);
let display_html = format_daemon_id_html(&daemon_id);
let state = StateFile::read(&*env::PITCHFORK_STATE_FILE)
.unwrap_or_else(|_| StateFile::new(env::PITCHFORK_STATE_FILE.clone()));
let pt = match PitchforkToml::all_merged() {
Ok(pt) => pt,
Err(e) => {
Expand Down
6 changes: 3 additions & 3 deletions src/web/routes/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ pub async fn stats_partial() -> Html<String> {
pub async fn index() -> Html<String> {
let bp = bp();

// Refresh process info for accurate CPU/memory stats
PROCS.refresh_processes();

let state = StateFile::read(&*env::PITCHFORK_STATE_FILE)
.unwrap_or_else(|_| StateFile::new(env::PITCHFORK_STATE_FILE.clone()));

// Refresh process info for accurate CPU/memory stats (only managed daemons)
PROCS.refresh_daemon_pids(&state);
let pt = match PitchforkToml::all_merged() {
Ok(pt) => pt,
Err(e) => {
Expand Down
Loading