Skip to main content

pitchfork_cli/
daemon_list.rs

1use crate::Result;
2use crate::daemon::Daemon;
3use crate::daemon_id::DaemonId;
4use crate::daemon_status::DaemonStatus;
5use crate::ipc::client::IpcClient;
6use crate::pitchfork_toml::PitchforkToml;
7use crate::settings::settings;
8use std::collections::HashSet;
9
10/// Represents a daemon entry that can be either tracked (from state file) or available (from config only)
11#[derive(Debug, Clone)]
12pub struct DaemonListEntry {
13    pub id: DaemonId,
14    pub daemon: Daemon,
15    pub is_disabled: bool,
16    pub is_available: bool, // true if daemon is only in config, not in state
17}
18
19/// Get a unified list of all daemons from IPC client and config
20///
21/// This function merges daemons from the state file (including failed daemons) with daemons
22/// defined in config files. Daemons that are only in config (not in state file) are marked
23/// as "available".
24///
25/// This logic is shared across:
26/// - `pitchfork list` command
27/// - TUI daemon list
28///
29/// # Arguments
30/// * `client` - IPC client to communicate with supervisor (used only for disabled list)
31///
32/// # Returns
33/// A vector of daemon entries with their current status
34pub async fn get_all_daemons(client: &IpcClient) -> Result<Vec<DaemonListEntry>> {
35    let config = PitchforkToml::all_merged()?;
36
37    // Read state file to get all daemons (including failed ones)
38    let state_file = crate::state_file::StateFile::read(&*crate::env::PITCHFORK_STATE_FILE)?;
39    let state_daemons: Vec<Daemon> = state_file.daemons.values().cloned().collect();
40
41    let disabled_daemons = client.get_disabled_daemons().await?;
42    let disabled_set: HashSet<DaemonId> = disabled_daemons.into_iter().collect();
43
44    build_daemon_list(state_daemons, disabled_set, config)
45}
46
47/// Get a unified list of all daemons from supervisor directly (for Web UI)
48///
49/// This function is used by the Web UI which runs inside the supervisor process
50/// and can access the supervisor directly without IPC.
51///
52/// # Arguments
53/// * `supervisor` - Reference to the supervisor instance
54///
55/// # Returns
56/// A vector of daemon entries with their current status
57pub async fn get_all_daemons_direct(
58    supervisor: &crate::supervisor::Supervisor,
59) -> Result<Vec<DaemonListEntry>> {
60    let config = PitchforkToml::all_merged()?;
61
62    // Read all daemons from state file (including failed/stopped ones)
63    // Note: Don't use supervisor.active_daemons() as it only returns daemons with PIDs
64    let state_file = supervisor.state_file.lock().await;
65    let state_daemons: Vec<Daemon> = state_file.daemons.values().cloned().collect();
66    let disabled_set: HashSet<DaemonId> = state_file.disabled.clone().into_iter().collect();
67    drop(state_file); // Release lock early
68
69    build_daemon_list(state_daemons, disabled_set, config)
70}
71
72/// Internal helper to build the daemon list from state daemons and config
73fn build_daemon_list(
74    state_daemons: Vec<Daemon>,
75    disabled_set: HashSet<DaemonId>,
76    config: PitchforkToml,
77) -> Result<Vec<DaemonListEntry>> {
78    let mut entries = Vec::new();
79    let mut seen_ids = HashSet::new();
80
81    // Skip the supervisor itself
82    let pitchfork_id = DaemonId::pitchfork();
83
84    // First, add all daemons from state file
85    for daemon in state_daemons {
86        if daemon.id == pitchfork_id {
87            continue; // Skip supervisor itself
88        }
89
90        // proxy and mise are stored as Option<bool> in the Daemon struct.
91        // None means "inherit from global settings", which is resolved at display/routing time.
92        // No override needed here — daemon_list consumers call .unwrap_or(settings()...) themselves.
93
94        seen_ids.insert(daemon.id.clone());
95        entries.push(DaemonListEntry {
96            id: daemon.id.clone(),
97            is_disabled: disabled_set.contains(&daemon.id),
98            is_available: false,
99            daemon,
100        });
101    }
102
103    // Then, add daemons from config that aren't in state file (available daemons)
104    for (daemon_id, daemon_config) in &config.daemons {
105        if *daemon_id == pitchfork_id || seen_ids.contains(daemon_id) {
106            continue;
107        }
108
109        // Create a placeholder daemon for config-only entries
110        let placeholder = Daemon {
111            id: daemon_id.clone(),
112            status: DaemonStatus::Stopped,
113            port_bump_attempts: settings().default_port_bump_attempts(),
114            depends: vec![],
115            env: None,
116            watch: vec![],
117            watch_base_dir: None,
118            mise: daemon_config.mise,
119            active_port: None,
120            slug: None,
121            proxy: None,
122            memory_limit: daemon_config.memory_limit,
123            cpu_limit: daemon_config.cpu_limit,
124            ..Daemon::default()
125        };
126
127        entries.push(DaemonListEntry {
128            id: daemon_id.clone(),
129            daemon: placeholder,
130            is_disabled: disabled_set.contains(daemon_id),
131            is_available: true,
132        });
133    }
134
135    Ok(entries)
136}