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