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(
42 home: &astrid_core::dirs::AstridHome,
43) -> Result<(UnixListener, std::fs::File), std::io::Error> {
44 let path = home.socket_path();
45
46 if let Some(parent) = path.parent() {
51 std::fs::create_dir_all(parent).map_err(|e| {
52 std::io::Error::other(format!(
53 "Failed to create socket parent directory {}: {e}",
54 parent.display()
55 ))
56 })?;
57
58 #[cfg(unix)]
59 {
60 use std::os::unix::fs::PermissionsExt;
61 std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))?;
62 }
63 }
64
65 let lock = acquire_singleton_lock(&path.with_file_name("system.lock"))?;
73
74 prepare_socket_path(&path)?;
75
76 remove_readiness_file();
79
80 let listener = UnixListener::bind(&path)?;
81 Ok((listener, lock))
82}
83
84fn acquire_singleton_lock(lock_path: &std::path::Path) -> Result<std::fs::File, std::io::Error> {
90 use std::fs::OpenOptions;
91
92 let mut opts = OpenOptions::new();
93 opts.read(true).write(true).create(true);
94 #[cfg(unix)]
95 {
96 use std::os::unix::fs::OpenOptionsExt;
97 opts.mode(0o600);
98 }
99 let file = opts.open(lock_path).map_err(|e| {
100 std::io::Error::other(format!(
101 "Failed to open singleton lockfile {}: {e}",
102 lock_path.display()
103 ))
104 })?;
105
106 file.try_lock().map_err(|e| match e {
107 std::fs::TryLockError::WouldBlock => std::io::Error::other(format!(
108 "Another kernel instance is already running (singleton lock held): {}",
109 lock_path.display()
110 )),
111 std::fs::TryLockError::Error(err) => std::io::Error::other(format!(
112 "Failed to acquire singleton lock {}: {err}",
113 lock_path.display()
114 )),
115 })?;
116
117 Ok(file)
118}
119
120pub(crate) fn generate_session_token() -> Result<(SessionToken, PathBuf), std::io::Error> {
136 use astrid_core::dirs::AstridHome;
137
138 let token = SessionToken::generate();
139
140 let home = AstridHome::resolve().map_err(|e| {
141 std::io::Error::other(format!(
142 "Cannot generate session token: failed to resolve ASTRID_HOME: {e}"
143 ))
144 })?;
145
146 let path = home.token_path();
147 token.write_to_file(&path)?;
148 Ok((token, path))
149}
150
151fn prepare_socket_path(path: &std::path::Path) -> Result<(), std::io::Error> {
157 let path_len = path.as_os_str().as_encoded_bytes().len();
158 if path_len >= MAX_SOCKET_PATH_LEN {
159 return Err(std::io::Error::other(format!(
160 "Socket path is {path_len} bytes, exceeding the platform limit of {MAX_SOCKET_PATH_LEN} bytes: {}",
161 path.display()
162 )));
163 }
164
165 if path.is_symlink() {
166 warn!(path = %path.display(), "Removing unexpected symlink at socket path");
167 std::fs::remove_file(path).map_err(|e| {
168 std::io::Error::other(format!(
169 "Failed to remove symlink at socket path {}: {e}",
170 path.display()
171 ))
172 })?;
173 } else if path.exists() {
174 match std::os::unix::net::UnixStream::connect(path) {
175 Ok(_stream) => {
176 return Err(std::io::Error::other(format!(
177 "Another kernel instance is already running on this socket: {}",
178 path.display()
179 )));
180 },
181 Err(e) if e.kind() == std::io::ErrorKind::ConnectionRefused => {
182 std::fs::remove_file(path).map_err(|e| {
184 std::io::Error::other(format!(
185 "Failed to remove stale socket {}: {e}",
186 path.display()
187 ))
188 })?;
189 },
190 Err(e) => {
191 return Err(std::io::Error::other(format!(
194 "Failed to probe existing socket {}: {e}",
195 path.display()
196 )));
197 },
198 }
199 }
200
201 Ok(())
202}
203
204#[must_use]
210pub fn readiness_path() -> PathBuf {
211 use astrid_core::dirs::AstridHome;
212 match AstridHome::resolve() {
213 Ok(home) => home.ready_path(),
214 Err(e) => {
215 warn!(
216 error = %e,
217 "Failed to resolve ASTRID_HOME; falling back to /tmp/.astrid/run/system.ready"
218 );
219 PathBuf::from("/tmp/.astrid/run/system.ready")
220 },
221 }
222}
223
224pub fn write_readiness_file() -> Result<(), std::io::Error> {
237 use std::fs::OpenOptions;
238
239 let path = readiness_path();
240
241 if let Some(parent) = path.parent() {
244 std::fs::create_dir_all(parent)?;
245 }
246
247 let mut opts = OpenOptions::new();
251 opts.write(true).create(true).truncate(true);
252
253 #[cfg(unix)]
254 {
255 use std::os::unix::fs::OpenOptionsExt;
256 opts.mode(0o600);
257 }
258
259 opts.open(&path)?;
260 Ok(())
261}
262
263pub fn remove_readiness_file() {
269 let _ = std::fs::remove_file(readiness_path());
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn path_too_long_is_rejected() {
278 let long_name = "a".repeat(MAX_SOCKET_PATH_LEN + 10);
280 let path = PathBuf::from(format!("/tmp/{long_name}.sock"));
281 let err = prepare_socket_path(&path).unwrap_err();
282 assert!(
283 err.to_string().contains("exceeding the platform limit"),
284 "unexpected error: {err}"
285 );
286 }
287
288 #[test]
289 fn stale_socket_is_removed() {
290 let dir = tempfile::tempdir().unwrap();
293 let sock = dir.path().join("test.sock");
294
295 let _listener = std::os::unix::net::UnixListener::bind(&sock).unwrap();
297 drop(_listener);
298
299 assert!(sock.exists(), "socket file should exist after bind");
300 prepare_socket_path(&sock).unwrap();
301 assert!(!sock.exists(), "stale socket should have been removed");
302 }
303
304 #[test]
305 fn live_socket_is_rejected() {
306 let dir = tempfile::tempdir().unwrap();
307 let sock = dir.path().join("test.sock");
308
309 let _listener = std::os::unix::net::UnixListener::bind(&sock).unwrap();
311
312 let err = prepare_socket_path(&sock).unwrap_err();
313 assert!(
314 err.to_string().contains("already running"),
315 "unexpected error: {err}"
316 );
317 }
318
319 #[test]
320 fn symlink_is_removed() {
321 let dir = tempfile::tempdir().unwrap();
322 let target = dir.path().join("target");
323 std::fs::write(&target, "not a socket").unwrap();
324
325 let sock = dir.path().join("test.sock");
326 std::os::unix::fs::symlink(&target, &sock).unwrap();
327 assert!(sock.is_symlink());
328
329 prepare_socket_path(&sock).unwrap();
330 assert!(!sock.exists(), "symlink should have been removed");
331 assert!(target.exists(), "target should be untouched");
332 }
333
334 #[test]
335 fn nonexistent_path_succeeds() {
336 let dir = tempfile::tempdir().unwrap();
337 let sock = dir.path().join("does_not_exist.sock");
338 prepare_socket_path(&sock).unwrap();
339 }
340
341 #[test]
342 fn singleton_lock_is_exclusive() {
343 let dir = tempfile::tempdir().unwrap();
344 let lock = dir.path().join("system.lock");
345
346 let _first = acquire_singleton_lock(&lock).expect("first acquisition succeeds");
348
349 let err = acquire_singleton_lock(&lock).unwrap_err();
352 assert!(
353 err.to_string().contains("already running"),
354 "unexpected error: {err}"
355 );
356 }
357
358 #[test]
359 fn singleton_lock_is_released_on_drop() {
360 let dir = tempfile::tempdir().unwrap();
361 let lock = dir.path().join("system.lock");
362
363 {
365 let _first = acquire_singleton_lock(&lock).expect("first acquisition succeeds");
366 }
367
368 let _second =
370 acquire_singleton_lock(&lock).expect("lock should be re-acquirable after release");
371 }
372}