Rust · 4502 bytes Raw Blame History
1 //! Shared IPC types for garcard daemon and control clients.
2
3 use serde::{Deserialize, Serialize};
4 use serde_json::Value;
5 use std::ffi::OsString;
6 use std::os::unix::net::UnixListener;
7 use std::path::PathBuf;
8 use std::time::{SystemTime, UNIX_EPOCH};
9
10 /// Protocol version for compatibility checks.
11 pub const PROTOCOL_VERSION: u32 = 1;
12
13 /// Runtime socket filename.
14 pub const SOCKET_BASENAME: &str = "garcard.sock";
15
16 /// Commands accepted by garcard daemon.
17 #[derive(Debug, Clone, Serialize, Deserialize)]
18 #[serde(tag = "command", rename_all = "snake_case")]
19 pub enum Command {
20 Ping,
21 Status,
22 Version,
23 AuthSummary,
24 Quit,
25 }
26
27 /// Standard daemon response envelope.
28 #[derive(Debug, Clone, Serialize, Deserialize)]
29 pub struct Response {
30 pub success: bool,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub data: Option<Value>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub error: Option<String>,
35 }
36
37 impl Response {
38 pub fn ok() -> Self {
39 Self {
40 success: true,
41 data: None,
42 error: None,
43 }
44 }
45
46 pub fn ok_with_data(data: impl Serialize) -> Self {
47 Self {
48 success: true,
49 data: serde_json::to_value(data).ok(),
50 error: None,
51 }
52 }
53
54 pub fn err(message: impl Into<String>) -> Self {
55 Self {
56 success: false,
57 data: None,
58 error: Some(message.into()),
59 }
60 }
61 }
62
63 /// Minimal health and runtime status summary.
64 #[derive(Debug, Clone, Serialize, Deserialize)]
65 pub struct StatusData {
66 pub running: bool,
67 pub pid: u32,
68 pub uptime_secs: u64,
69 pub version: String,
70 pub protocol_version: u32,
71 pub socket_path: String,
72 pub agent_backend: String,
73 }
74
75 /// Version handshake payload.
76 #[derive(Debug, Clone, Serialize, Deserialize)]
77 pub struct VersionData {
78 pub component: String,
79 pub version: String,
80 pub protocol_version: u32,
81 }
82
83 /// Auth workflow summary with no sensitive data.
84 #[derive(Debug, Clone, Serialize, Deserialize)]
85 pub struct AuthSummary {
86 pub state: String,
87 pub active_requests: usize,
88 pub queued_requests: usize,
89 }
90
91 /// Resolve the daemon socket path from `XDG_RUNTIME_DIR` with `/tmp` fallback.
92 pub fn socket_path() -> PathBuf {
93 if let Some(explicit) = std::env::var_os("GARCARD_SOCKET") {
94 return PathBuf::from(explicit);
95 }
96
97 let runtime_dir = std::env::var_os("XDG_RUNTIME_DIR");
98 if runtime_dir.as_ref().is_some_and(runtime_dir_is_usable) {
99 return socket_path_from_runtime_dir(runtime_dir);
100 }
101
102 socket_path_from_runtime_dir(None)
103 }
104
105 /// Build socket path from an explicit runtime dir value.
106 pub fn socket_path_from_runtime_dir(runtime_dir: Option<OsString>) -> PathBuf {
107 match runtime_dir {
108 Some(dir) => PathBuf::from(dir).join(SOCKET_BASENAME),
109 None => PathBuf::from("/tmp").join(SOCKET_BASENAME),
110 }
111 }
112
113 /// Check if we can create a socket in runtime dir.
114 ///
115 /// In sandboxed environments, `XDG_RUNTIME_DIR` can exist but still reject
116 /// socket creation. We probe with a unique temporary socket path so caller
117 /// code can safely fall back to `/tmp`.
118 fn runtime_dir_is_usable(runtime_dir: &OsString) -> bool {
119 let base = PathBuf::from(runtime_dir);
120 let timestamp = SystemTime::now()
121 .duration_since(UNIX_EPOCH)
122 .ok()
123 .map(|d| d.as_nanos())
124 .unwrap_or(0);
125 let probe = base.join(format!(
126 ".garcard-probe-{}-{}.sock",
127 std::process::id(),
128 timestamp
129 ));
130
131 match UnixListener::bind(&probe) {
132 Ok(listener) => {
133 drop(listener);
134 let _ = std::fs::remove_file(&probe);
135 true
136 }
137 Err(_) => false,
138 }
139 }
140
141 #[cfg(test)]
142 mod tests {
143 use super::*;
144
145 #[test]
146 fn command_round_trip() {
147 let cmd = Command::AuthSummary;
148 let encoded = serde_json::to_string(&cmd).expect("encode command");
149 let decoded: Command = serde_json::from_str(&encoded).expect("decode command");
150 assert!(matches!(decoded, Command::AuthSummary));
151 }
152
153 #[test]
154 fn socket_path_falls_back_to_tmp() {
155 let path = socket_path_from_runtime_dir(None);
156 assert_eq!(path, PathBuf::from("/tmp/garcard.sock"));
157 }
158
159 #[test]
160 fn socket_path_uses_runtime_dir() {
161 let path = socket_path_from_runtime_dir(Some(OsString::from("/run/user/1000")));
162 assert_eq!(path, PathBuf::from("/run/user/1000/garcard.sock"));
163 }
164 }
165