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    /// Invalid request (failed to deserialize)
59    #[serde(skip)]
60    Invalid {
61        error: String,
62    },
63}
64
65#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, strum::Display, strum::EnumIs)]
66pub enum IpcResponse {
67    Ok,
68    /// Successful connect handshake, includes supervisor version for mismatch detection
69    ConnectOk {
70        version: String,
71    },
72    Yes,
73    No,
74    Error(String),
75    Notifications(Vec<(log::LevelFilter, String)>),
76    ActiveDaemons(Vec<Daemon>),
77    DisabledDaemons(Vec<DaemonId>),
78    DaemonAlreadyRunning,
79    DaemonStart {
80        daemon: Daemon,
81    },
82    DaemonFailed {
83        error: String,
84    },
85    /// Port conflict detected with detailed process information
86    PortConflict {
87        port: u16,
88        process: String,
89        pid: u32,
90    },
91    /// No available ports found after exhausting auto-bump attempts
92    NoAvailablePort {
93        start_port: u16,
94        attempts: u32,
95    },
96    DaemonReady {
97        daemon: Daemon,
98    },
99    DaemonFailedWithCode {
100        exit_code: Option<i32>,
101    },
102    /// Process was not running but had a PID record (unexpected exit)
103    DaemonWasNotRunning,
104    /// mDNS sync completed (or was a no-op if LAN mode is disabled)
105    MdnsSynced,
106    /// Failed to kill the process (still running)
107    DaemonStopFailed {
108        error: String,
109    },
110    /// Daemon exists but is not running (no PID)
111    DaemonNotRunning,
112    DaemonNotFound,
113}
114fn fs_name(name: &str) -> Result<Name<'_>> {
115    let path = env::IPC_SOCK_DIR.join(name).with_extension("sock");
116    let fs_name = path.to_fs_name::<GenericFilePath>().into_diagnostic()?;
117    Ok(fs_name)
118}
119
120fn serialize<T: serde::Serialize>(msg: &T) -> Result<Vec<u8>> {
121    if *env::IPC_JSON {
122        serde_json::to_vec(msg)
123            .into_diagnostic()
124            .wrap_err("failed to serialize IPC message as JSON")
125    } else {
126        rmp_serde::to_vec(msg)
127            .into_diagnostic()
128            .wrap_err("failed to serialize IPC message as MessagePack")
129    }
130}
131
132fn deserialize<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> Result<T> {
133    let mut bytes = bytes.to_vec();
134    bytes.pop();
135    let preview = std::str::from_utf8(&bytes).unwrap_or("<binary>");
136    trace!("msg: {preview:?}");
137    if *env::IPC_JSON {
138        serde_json::from_slice(&bytes)
139            .into_diagnostic()
140            .wrap_err("failed to deserialize IPC JSON response")
141    } else {
142        rmp_serde::from_slice(&bytes)
143            .into_diagnostic()
144            .wrap_err("failed to deserialize IPC MessagePack response")
145    }
146}