Skip to main content

running_process/broker/backend_lifecycle/
identity.rs

1//! Normalized daemon identity carried by `BackendHandle`.
2//!
3//! `DaemonProcess` is the typed form of `CacheManifest.current_daemon`. It is
4//! deliberately more specific than the generated protobuf message: paths are
5//! `PathBuf`s, executable hashes are fixed 32-byte arrays, and the IPC endpoint
6//! is required. That keeps malformed manifests out of the `BackendHandle` probe
7//! path.
8
9use std::convert::TryFrom;
10use std::fs;
11use std::io;
12use std::path::{Path, PathBuf};
13use std::time::{SystemTime, UNIX_EPOCH};
14
15use serde::{Deserialize, Deserializer, Serialize, Serializer};
16use sha2::{Digest, Sha256};
17
18use crate::broker::host_identity;
19use crate::broker::protocol::{self, CacheManifest, Endpoint};
20
21/// A backend daemon identity with fixed-width fields suitable for verification.
22///
23/// This mirrors `CacheManifest.current_daemon`, but normalizes protobuf strings
24/// and byte vectors into path and digest types that are harder to misuse.
25///
26/// Persist this value only after the daemon has selected its final IPC endpoint
27/// and executable. Later consumers can pass the same identity to
28/// [`crate::broker::backend_handle::BackendHandle::probe`] or store it as
29/// `CacheManifest.current_daemon`.
30///
31/// ```no_run
32/// use running_process::broker::backend_handle::DaemonProcess;
33/// use running_process::broker::protocol::{CacheManifest, Endpoint};
34///
35/// # fn example(mut manifest: CacheManifest)
36/// #     -> Result<CacheManifest, running_process::broker::backend_lifecycle::identity::IdentityError>
37/// # {
38/// let endpoint = Endpoint {
39///     namespace_id: "host-namespace".to_owned(),
40///     path: "running-process-backend.sock".to_owned(),
41/// };
42/// let daemon = DaemonProcess::current_process(endpoint, Some(600))?;
43///
44/// manifest.current_daemon = Some(daemon.to_proto());
45/// # Ok(manifest)
46/// # }
47/// ```
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct DaemonProcess {
50    /// Operating-system process ID.
51    pub pid: u32,
52    /// Executable path recorded when the daemon identity was written.
53    pub exe_path: PathBuf,
54    /// SHA-256 of the daemon executable.
55    pub exe_sha256: [u8; 32],
56    /// Host boot ID observed when the daemon started.
57    pub boot_id: String,
58    /// IPC endpoint used to connect to the daemon.
59    pub ipc_endpoint: Endpoint,
60    /// Daemon start timestamp in Unix milliseconds.
61    pub started_at_unix_ms: u64,
62    /// Optional idle timeout advertised by the daemon.
63    pub idle_timeout_secs: Option<u32>,
64}
65
66impl DaemonProcess {
67    /// Build a daemon identity for the current process.
68    ///
69    /// This is primarily useful for tests and direct-daemon consumers that have
70    /// just spawned a backend and need to persist a manifest entry.
71    ///
72    /// The executable digest is taken from `std::env::current_exe()` at the time
73    /// this method runs. If a daemon relocates or replaces its executable after
74    /// startup, record the final identity after relocation instead.
75    pub fn current_process(
76        ipc_endpoint: Endpoint,
77        idle_timeout_secs: Option<u32>,
78    ) -> Result<Self, IdentityError> {
79        let exe_path = std::env::current_exe().map_err(IdentityError::CurrentExe)?;
80        let exe_sha256 = sha256_file(&exe_path)?;
81        Ok(Self {
82            pid: std::process::id(),
83            exe_path,
84            exe_sha256,
85            boot_id: host_identity::current().boot_id,
86            ipc_endpoint,
87            started_at_unix_ms: unix_now_ms(),
88            idle_timeout_secs,
89        })
90    }
91
92    /// Convert this identity into the protobuf form stored in `CacheManifest`.
93    ///
94    /// The conversion preserves the fixed-width SHA-256 value as bytes for the
95    /// wire schema.
96    pub fn to_proto(&self) -> protocol::DaemonProcess {
97        protocol::DaemonProcess {
98            pid: self.pid,
99            exe_path: self.exe_path.to_string_lossy().into_owned(),
100            exe_sha256: self.exe_sha256.to_vec(),
101            ipc_endpoint: Some(self.ipc_endpoint.clone()),
102            started_at_unix_ms: self.started_at_unix_ms,
103            boot_id: self.boot_id.clone(),
104            idle_timeout_secs: self.idle_timeout_secs,
105        }
106    }
107
108    /// Read and normalize `CacheManifest.current_daemon`.
109    ///
110    /// Returns `Ok(None)` when the manifest has no daemon entry. Malformed
111    /// entries, such as a missing endpoint or non-32-byte executable digest,
112    /// return an [`IdentityError`].
113    pub fn from_manifest_current_daemon(
114        manifest: &CacheManifest,
115    ) -> Result<Option<Self>, IdentityError> {
116        manifest
117            .current_daemon
118            .clone()
119            .map(Self::try_from)
120            .transpose()
121    }
122}
123
124impl TryFrom<protocol::DaemonProcess> for DaemonProcess {
125    type Error = IdentityError;
126
127    fn try_from(value: protocol::DaemonProcess) -> Result<Self, Self::Error> {
128        let ipc_endpoint = value.ipc_endpoint.ok_or(IdentityError::MissingEndpoint)?;
129        let exe_sha256 =
130            vec_to_sha256(value.exe_sha256).map_err(IdentityError::InvalidSha256Length)?;
131        Ok(Self {
132            pid: value.pid,
133            exe_path: PathBuf::from(value.exe_path),
134            exe_sha256,
135            boot_id: value.boot_id,
136            ipc_endpoint,
137            started_at_unix_ms: value.started_at_unix_ms,
138            idle_timeout_secs: value.idle_timeout_secs,
139        })
140    }
141}
142
143impl From<&DaemonProcess> for protocol::DaemonProcess {
144    fn from(value: &DaemonProcess) -> Self {
145        value.to_proto()
146    }
147}
148
149impl Serialize for DaemonProcess {
150    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
151    where
152        S: Serializer,
153    {
154        DaemonProcessSerde::from(self).serialize(serializer)
155    }
156}
157
158impl<'de> Deserialize<'de> for DaemonProcess {
159    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
160    where
161        D: Deserializer<'de>,
162    {
163        let value = DaemonProcessSerde::deserialize(deserializer)?;
164        Ok(Self {
165            pid: value.pid,
166            exe_path: value.exe_path,
167            exe_sha256: value.exe_sha256,
168            boot_id: value.boot_id,
169            ipc_endpoint: value.ipc_endpoint.into(),
170            started_at_unix_ms: value.started_at_unix_ms,
171            idle_timeout_secs: value.idle_timeout_secs,
172        })
173    }
174}
175
176/// Errors returned while normalizing daemon identity.
177#[derive(Debug, thiserror::Error)]
178pub enum IdentityError {
179    /// The protobuf daemon identity did not include an IPC endpoint.
180    #[error("daemon process is missing ipc_endpoint")]
181    MissingEndpoint,
182    /// The protobuf daemon identity had an executable digest with the wrong size.
183    #[error("daemon process exe_sha256 must be 32 bytes, got {0}")]
184    InvalidSha256Length(usize),
185    /// The current executable path could not be read.
186    #[error("failed to resolve current executable: {0}")]
187    CurrentExe(io::Error),
188    /// A filesystem operation failed while hashing the executable.
189    #[error("failed to hash executable: {0}")]
190    Io(#[from] io::Error),
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
194struct DaemonProcessSerde {
195    pid: u32,
196    exe_path: PathBuf,
197    exe_sha256: [u8; 32],
198    boot_id: String,
199    ipc_endpoint: EndpointSerde,
200    started_at_unix_ms: u64,
201    idle_timeout_secs: Option<u32>,
202}
203
204impl From<&DaemonProcess> for DaemonProcessSerde {
205    fn from(value: &DaemonProcess) -> Self {
206        Self {
207            pid: value.pid,
208            exe_path: value.exe_path.clone(),
209            exe_sha256: value.exe_sha256,
210            boot_id: value.boot_id.clone(),
211            ipc_endpoint: EndpointSerde::from(&value.ipc_endpoint),
212            started_at_unix_ms: value.started_at_unix_ms,
213            idle_timeout_secs: value.idle_timeout_secs,
214        }
215    }
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
219struct EndpointSerde {
220    namespace_id: String,
221    path: String,
222}
223
224impl From<&Endpoint> for EndpointSerde {
225    fn from(value: &Endpoint) -> Self {
226        Self {
227            namespace_id: value.namespace_id.clone(),
228            path: value.path.clone(),
229        }
230    }
231}
232
233impl From<EndpointSerde> for Endpoint {
234    fn from(value: EndpointSerde) -> Self {
235        Endpoint {
236            namespace_id: value.namespace_id,
237            path: value.path,
238        }
239    }
240}
241
242pub(crate) fn sha256_file(path: &Path) -> Result<[u8; 32], io::Error> {
243    let bytes = fs::read(path)?;
244    let digest = Sha256::digest(&bytes);
245    let mut out = [0_u8; 32];
246    out.copy_from_slice(&digest);
247    Ok(out)
248}
249
250fn vec_to_sha256(bytes: Vec<u8>) -> Result<[u8; 32], usize> {
251    let len = bytes.len();
252    let Ok(out) = bytes.try_into() else {
253        return Err(len);
254    };
255    Ok(out)
256}
257
258fn unix_now_ms() -> u64 {
259    SystemTime::now()
260        .duration_since(UNIX_EPOCH)
261        .map(|duration| duration.as_millis() as u64)
262        .unwrap_or(0)
263}