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