lattice_ai/mcp/
lockfile.rs1use std::fs;
14use std::path::{Path, PathBuf};
15
16use serde::{Deserialize, Serialize};
17
18use crate::mcp::error::Result;
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23#[serde(rename_all = "camelCase")]
24pub struct LockfileContents {
25 pub pid: u32,
27 pub workspace_folders: Vec<String>,
29 pub ide_name: String,
31 pub transport: String,
33 pub auth_token: String,
36 #[serde(default)]
39 pub running_in_windows: bool,
40}
41
42pub fn default_lock_dir() -> Option<PathBuf> {
45 dirs::home_dir().map(|h| h.join(".claude").join("ide"))
46}
47
48#[derive(Debug)]
51pub struct Lockfile {
52 path: PathBuf,
53}
54
55impl Lockfile {
56 pub fn write(dir: &Path, port: u16, contents: &LockfileContents) -> Result<Self> {
59 fs::create_dir_all(dir)?;
60 let path = dir.join(format!("{port}.lock"));
61 let json = serde_json::to_vec_pretty(contents)?;
62 fs::write(&path, json)?;
63 Ok(Self { path })
64 }
65
66 pub fn path(&self) -> &Path {
68 &self.path
69 }
70}
71
72impl Drop for Lockfile {
73 fn drop(&mut self) {
74 if let Err(e) = fs::remove_file(&self.path) {
77 tracing::debug!(path = %self.path.display(), error = %e, "lockfile unlink failed");
78 }
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 #![allow(clippy::unwrap_used, clippy::panic)]
85 use super::*;
86 use std::sync::atomic::{AtomicU32, Ordering};
87
88 fn unique_dir(tag: &str) -> PathBuf {
89 static COUNTER: AtomicU32 = AtomicU32::new(0);
90 let n = COUNTER.fetch_add(1, Ordering::SeqCst);
91 std::env::temp_dir().join(format!("lattice-cc-{}-{}-{}", tag, std::process::id(), n))
92 }
93
94 fn sample(port_token: &str) -> LockfileContents {
95 LockfileContents {
96 pid: 4242,
97 workspace_folders: vec!["/work/project".to_string()],
98 ide_name: "Lattice".to_string(),
99 transport: "ws".to_string(),
100 auth_token: port_token.to_string(),
101 running_in_windows: false,
102 }
103 }
104
105 #[test]
106 fn round_trips_through_disk() {
107 let dir = unique_dir("roundtrip");
108 let contents = sample("deadbeefcafef00ddeadbeefcafef00d");
109 let lock = Lockfile::write(&dir, 12345, &contents).expect("write");
110 assert!(lock.path().exists());
111 assert_eq!(lock.path().file_name().unwrap(), "12345.lock");
112
113 let raw = std::fs::read(lock.path()).expect("read");
114 let parsed: LockfileContents = serde_json::from_slice(&raw).expect("parse");
115 assert_eq!(parsed, contents);
116 let text = String::from_utf8(raw).unwrap();
118 assert!(text.contains("\"authToken\""));
119 assert!(text.contains("\"workspaceFolders\""));
120
121 drop(lock);
122 let _ = std::fs::remove_dir_all(&dir);
123 }
124
125 #[test]
126 fn unlinks_on_drop() {
127 let dir = unique_dir("unlink");
128 let path = {
129 let lock = Lockfile::write(&dir, 23456, &sample("token")).expect("write");
130 let p = lock.path().to_path_buf();
131 assert!(p.exists());
132 p
133 };
135 assert!(!path.exists(), "lockfile should be removed on Drop");
136 let _ = std::fs::remove_dir_all(&dir);
137 }
138}