1use std::path::PathBuf;
2
3use astrid_core::session_token::SessionToken;
4use tokio::net::UnixListener;
5use tracing::warn;
6
7#[must_use]
9pub(crate) fn kernel_socket_path() -> PathBuf {
10 use astrid_core::dirs::AstridHome;
11 match AstridHome::resolve() {
12 Ok(home) => home.socket_path(),
13 Err(e) => {
14 warn!(error = %e, "Failed to resolve ASTRID_HOME; falling back to /tmp/.astrid/run/system.sock");
15 PathBuf::from("/tmp/.astrid/run/system.sock")
16 },
17 }
18}
19
20#[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "openbsd"))]
23const MAX_SOCKET_PATH_LEN: usize = 104;
24#[cfg(not(any(target_os = "macos", target_os = "freebsd", target_os = "openbsd")))]
25const MAX_SOCKET_PATH_LEN: usize = 108;
26
27pub(crate) fn bind_session_socket() -> Result<UnixListener, std::io::Error> {
35 let path = kernel_socket_path();
36
37 prepare_socket_path(&path)?;
38
39 remove_readiness_file();
42
43 if let Some(parent) = path.parent() {
44 std::fs::create_dir_all(parent).map_err(|e| {
45 std::io::Error::other(format!(
46 "Failed to create socket parent directory {}: {e}",
47 parent.display()
48 ))
49 })?;
50
51 #[cfg(unix)]
56 {
57 use std::os::unix::fs::PermissionsExt;
58 std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))?;
59 }
60 }
61
62 UnixListener::bind(&path)
63}
64
65pub(crate) fn generate_session_token() -> Result<(SessionToken, PathBuf), std::io::Error> {
81 use astrid_core::dirs::AstridHome;
82
83 let token = SessionToken::generate();
84
85 let home = AstridHome::resolve().map_err(|e| {
86 std::io::Error::other(format!(
87 "Cannot generate session token: failed to resolve ASTRID_HOME: {e}"
88 ))
89 })?;
90
91 let path = home.token_path();
92 token.write_to_file(&path)?;
93 Ok((token, path))
94}
95
96fn prepare_socket_path(path: &std::path::Path) -> Result<(), std::io::Error> {
102 let path_len = path.as_os_str().as_encoded_bytes().len();
103 if path_len >= MAX_SOCKET_PATH_LEN {
104 return Err(std::io::Error::other(format!(
105 "Socket path is {path_len} bytes, exceeding the platform limit of {MAX_SOCKET_PATH_LEN} bytes: {}",
106 path.display()
107 )));
108 }
109
110 if path.is_symlink() {
111 warn!(path = %path.display(), "Removing unexpected symlink at socket path");
112 std::fs::remove_file(path).map_err(|e| {
113 std::io::Error::other(format!(
114 "Failed to remove symlink at socket path {}: {e}",
115 path.display()
116 ))
117 })?;
118 } else if path.exists() {
119 match std::os::unix::net::UnixStream::connect(path) {
120 Ok(_stream) => {
121 return Err(std::io::Error::other(format!(
122 "Another kernel instance is already running on this socket: {}",
123 path.display()
124 )));
125 },
126 Err(e) if e.kind() == std::io::ErrorKind::ConnectionRefused => {
127 std::fs::remove_file(path).map_err(|e| {
129 std::io::Error::other(format!(
130 "Failed to remove stale socket {}: {e}",
131 path.display()
132 ))
133 })?;
134 },
135 Err(e) => {
136 return Err(std::io::Error::other(format!(
139 "Failed to probe existing socket {}: {e}",
140 path.display()
141 )));
142 },
143 }
144 }
145
146 Ok(())
147}
148
149#[must_use]
155pub fn readiness_path() -> PathBuf {
156 use astrid_core::dirs::AstridHome;
157 match AstridHome::resolve() {
158 Ok(home) => home.ready_path(),
159 Err(e) => {
160 warn!(
161 error = %e,
162 "Failed to resolve ASTRID_HOME; falling back to /tmp/.astrid/run/system.ready"
163 );
164 PathBuf::from("/tmp/.astrid/run/system.ready")
165 },
166 }
167}
168
169pub fn write_readiness_file() -> Result<(), std::io::Error> {
182 use std::fs::OpenOptions;
183
184 let path = readiness_path();
185
186 if let Some(parent) = path.parent() {
189 std::fs::create_dir_all(parent)?;
190 }
191
192 let mut opts = OpenOptions::new();
196 opts.write(true).create(true).truncate(true);
197
198 #[cfg(unix)]
199 {
200 use std::os::unix::fs::OpenOptionsExt;
201 opts.mode(0o600);
202 }
203
204 opts.open(&path)?;
205 Ok(())
206}
207
208pub fn remove_readiness_file() {
214 let _ = std::fs::remove_file(readiness_path());
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn path_too_long_is_rejected() {
223 let long_name = "a".repeat(MAX_SOCKET_PATH_LEN + 10);
225 let path = PathBuf::from(format!("/tmp/{long_name}.sock"));
226 let err = prepare_socket_path(&path).unwrap_err();
227 assert!(
228 err.to_string().contains("exceeding the platform limit"),
229 "unexpected error: {err}"
230 );
231 }
232
233 #[test]
234 fn stale_socket_is_removed() {
235 let dir = tempfile::tempdir().unwrap();
238 let sock = dir.path().join("test.sock");
239
240 let _listener = std::os::unix::net::UnixListener::bind(&sock).unwrap();
242 drop(_listener);
243
244 assert!(sock.exists(), "socket file should exist after bind");
245 prepare_socket_path(&sock).unwrap();
246 assert!(!sock.exists(), "stale socket should have been removed");
247 }
248
249 #[test]
250 fn live_socket_is_rejected() {
251 let dir = tempfile::tempdir().unwrap();
252 let sock = dir.path().join("test.sock");
253
254 let _listener = std::os::unix::net::UnixListener::bind(&sock).unwrap();
256
257 let err = prepare_socket_path(&sock).unwrap_err();
258 assert!(
259 err.to_string().contains("already running"),
260 "unexpected error: {err}"
261 );
262 }
263
264 #[test]
265 fn symlink_is_removed() {
266 let dir = tempfile::tempdir().unwrap();
267 let target = dir.path().join("target");
268 std::fs::write(&target, "not a socket").unwrap();
269
270 let sock = dir.path().join("test.sock");
271 std::os::unix::fs::symlink(&target, &sock).unwrap();
272 assert!(sock.is_symlink());
273
274 prepare_socket_path(&sock).unwrap();
275 assert!(!sock.exists(), "symlink should have been removed");
276 assert!(target.exists(), "target should be untouched");
277 }
278
279 #[test]
280 fn nonexistent_path_succeeds() {
281 let dir = tempfile::tempdir().unwrap();
282 let sock = dir.path().join("does_not_exist.sock");
283 prepare_socket_path(&sock).unwrap();
284 }
285}