Skip to main content

pitchfork_cli/ipc/
mod.rs

1use crate::Result;
2use crate::daemon::{Daemon, RunOptions};
3use crate::daemon_id::DaemonId;
4use crate::env;
5use interprocess::local_socket::{GenericFilePath, Name, ToFsName};
6use miette::{Context, IntoDiagnostic};
7use std::path::PathBuf;
8
9pub(crate) mod batch;
10pub(crate) mod client;
11pub(crate) mod server;
12
13// #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, strum::Display, strum::EnumIs)]
14// pub enum IpcMessage {
15//     Connect(String),
16//     ConnectOK,
17//     Run(String, Vec<String>),
18//     Stop(String),
19//     DaemonAlreadyRunning(String),
20//     DaemonAlreadyStopped(String),
21//     DaemonStart(Daemon),
22//     DaemonStop { name: String },
23//     DaemonFailed { name: String, error: String },
24//     Response(String),
25// }
26
27#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, strum::Display, strum::EnumIs)]
28#[allow(clippy::large_enum_variant)]
29pub enum IpcRequest {
30    Connect,
31    /// Versioned connect handshake (v2): client sends its version so the supervisor can
32    /// detect mismatches. Kept as a separate variant so the wire format of `Connect`
33    /// (unit variant) stays unchanged for backward compatibility with older supervisors.
34    ConnectV2 {
35        version: String,
36    },
37    Clean,
38    Stop {
39        id: DaemonId,
40    },
41    GetActiveDaemons,
42    GetDisabledDaemons,
43    Run(RunOptions),
44    Enable {
45        id: DaemonId,
46    },
47    Disable {
48        id: DaemonId,
49    },
50    UpdateShellDir {
51        shell_pid: u32,
52        dir: PathBuf,
53    },
54    GetNotifications,
55    /// Notify the supervisor that the slug registry has changed (e.g. `proxy add/remove`).
56    /// The supervisor should re-read slugs and update mDNS records accordingly.
57    SyncMdns,
58    /// Notify the supervisor that settings have changed.
59    /// The supervisor should reload settings from config files.
60    ReloadConfig,
61    /// Invalid request (failed to deserialize)
62    #[serde(skip)]
63    Invalid {
64        error: String,
65    },
66}
67
68#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, strum::Display, strum::EnumIs)]
69pub enum IpcResponse {
70    Ok,
71    /// Successful connect handshake, includes supervisor version for mismatch detection
72    ConnectOk {
73        version: String,
74    },
75    Yes,
76    No,
77    Error(String),
78    Notifications(Vec<(log::LevelFilter, String)>),
79    ActiveDaemons(Vec<Daemon>),
80    DisabledDaemons(Vec<DaemonId>),
81    DaemonAlreadyRunning,
82    DaemonStart {
83        daemon: Daemon,
84    },
85    DaemonFailed {
86        error: String,
87    },
88    /// Port conflict detected with detailed process information
89    PortConflict {
90        port: u16,
91        process: String,
92        pid: u32,
93    },
94    /// No available ports found after exhausting auto-bump attempts
95    NoAvailablePort {
96        start_port: u16,
97        attempts: u32,
98    },
99    DaemonReady {
100        daemon: Daemon,
101    },
102    DaemonFailedWithCode {
103        exit_code: Option<i32>,
104    },
105    /// Process was not running but had a PID record (unexpected exit)
106    DaemonWasNotRunning,
107    /// mDNS sync completed (or was a no-op if LAN mode is disabled)
108    MdnsSynced,
109    /// Settings reloaded from config files
110    ConfigReloaded,
111    /// Failed to kill the process (still running)
112    DaemonStopFailed {
113        error: String,
114    },
115    /// Daemon exists but is not running (no PID)
116    DaemonNotRunning,
117    DaemonNotFound,
118}
119fn fs_name(name: &str) -> Result<Name<'_>> {
120    let path = env::IPC_SOCK_DIR.join(name).with_extension("sock");
121    let fs_name = path.to_fs_name::<GenericFilePath>().into_diagnostic()?;
122    Ok(fs_name)
123}
124
125fn serialize<T: serde::Serialize>(msg: &T) -> Result<Vec<u8>> {
126    if *env::IPC_JSON {
127        serde_json::to_vec(msg)
128            .into_diagnostic()
129            .wrap_err("failed to serialize IPC message as JSON")
130    } else {
131        rmp_serde::to_vec(msg)
132            .into_diagnostic()
133            .wrap_err("failed to serialize IPC message as MessagePack")
134    }
135}
136
137fn deserialize<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> Result<T> {
138    let mut bytes = bytes.to_vec();
139    bytes.pop();
140    let preview = std::str::from_utf8(&bytes).unwrap_or("<binary>");
141    trace!("msg: {preview:?}");
142    if *env::IPC_JSON {
143        serde_json::from_slice(&bytes)
144            .into_diagnostic()
145            .wrap_err("failed to deserialize IPC JSON response")
146    } else {
147        rmp_serde::from_slice(&bytes)
148            .into_diagnostic()
149            .wrap_err("failed to deserialize IPC MessagePack response")
150    }
151}