matrixcode_core/memory/
storage.rs1use anyhow::Result;
4use chrono::Utc;
5use std::fs;
6use std::path::{Path, PathBuf};
7
8use super::config::MemoryConfig;
9use super::types::{AutoMemory, MemoryEntry};
10
11pub struct MemoryFileLock {
17 lock_path: PathBuf,
19 locked: bool,
21}
22
23impl MemoryFileLock {
24 pub fn new(base_dir: &Path) -> Self {
26 Self {
27 lock_path: base_dir.join("memory.lock"),
28 locked: false,
29 }
30 }
31
32 pub fn acquire(&mut self, timeout_ms: u64) -> Result<bool> {
34 if self.locked {
35 return Ok(true);
36 }
37
38 let start = std::time::Instant::now();
39
40 while start.elapsed().as_millis() < timeout_ms as u128 {
41 match fs::File::create_new(&self.lock_path) {
42 Ok(_) => {
43 let lock_info = format!("{}:{}", std::process::id(), Utc::now().to_rfc3339());
44 fs::write(&self.lock_path, lock_info)?;
45 self.locked = true;
46 return Ok(true);
47 }
48 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
49 if self.is_stale_lock()? {
50 self.remove_stale_lock()?;
51 }
52 std::thread::sleep(std::time::Duration::from_millis(50));
53 }
54 Err(e) => {
55 return Err(e.into());
56 }
57 }
58 }
59
60 Ok(false)
61 }
62
63 fn is_stale_lock(&self) -> Result<bool> {
65 if !self.lock_path.exists() {
66 return Ok(false);
67 }
68
69 if let Ok(content) = fs::read_to_string(&self.lock_path)
71 && let Some(pid_str) = content.split(':').next()
72 && let Ok(pid) = pid_str.parse::<u32>()
73 && !self.is_process_running(pid)
74 {
75 return Ok(true);
77 }
78
79 let metadata = fs::metadata(&self.lock_path)?;
81 let modified = metadata.modified()?;
82 let age = std::time::SystemTime::now()
83 .duration_since(modified)
84 .unwrap_or(std::time::Duration::ZERO);
85
86 Ok(age > std::time::Duration::from_secs(60))
87 }
88
89 fn is_process_running(&self, pid: u32) -> bool {
91 #[cfg(unix)]
92 {
93 if std::path::Path::new("/proc").exists() {
95 std::path::Path::new(&format!("/proc/{}", pid)).exists()
96 } else {
97 true
99 }
100 }
101 #[cfg(windows)]
102 {
103 use std::process::Command;
105 let output = Command::new("tasklist")
106 .args(["/FI", &format!("PID eq {}", pid), "/NH"])
107 .output();
108
109 match output {
110 Ok(out) => {
111 let stdout = String::from_utf8_lossy(&out.stdout);
112 stdout.contains(&pid.to_string()) && !stdout.contains("No tasks")
115 }
116 Err(_) => {
117 true
119 }
120 }
121 }
122 #[cfg(not(any(unix, windows)))]
123 {
124 let _ = pid;
125 true
126 }
127 }
128
129 fn remove_stale_lock(&self) -> Result<()> {
131 let temp_path = self.lock_path.with_extension("lock.del");
134 if self.lock_path.exists() {
135 if fs::rename(&self.lock_path, &temp_path).is_ok() {
137 fs::remove_file(&temp_path)?;
138 } else {
139 fs::remove_file(&self.lock_path)?;
141 }
142 }
143 Ok(())
144 }
145
146 pub fn release(&mut self) -> Result<()> {
148 if self.locked {
149 fs::remove_file(&self.lock_path)?;
150 self.locked = false;
151 }
152 Ok(())
153 }
154}
155
156impl Drop for MemoryFileLock {
157 fn drop(&mut self) {
158 let _ = self.release();
159 }
160}
161
162pub struct MemoryStorage {
168 base_dir: PathBuf,
170 project_root: Option<PathBuf>,
172 lock: MemoryFileLock,
174}
175
176impl MemoryStorage {
177 pub fn new(project_root: Option<&Path>) -> Result<Self> {
179 let base_dir = Self::get_base_dir()?;
180 let lock = MemoryFileLock::new(&base_dir);
181 Ok(Self {
182 base_dir,
183 project_root: project_root.map(|p| p.to_path_buf()),
184 lock,
185 })
186 }
187
188 pub fn with_lock_timeout(project_root: Option<&Path>, timeout_ms: u64) -> Result<Self> {
190 let mut storage = Self::new(project_root)?;
191 storage.lock.acquire(timeout_ms)?;
192 Ok(storage)
193 }
194
195 fn get_base_dir() -> Result<PathBuf> {
197 let home = std::env::var_os("HOME")
198 .or_else(|| std::env::var_os("USERPROFILE"))
199 .ok_or_else(|| anyhow::anyhow!("HOME or USERPROFILE not set"))?;
200 let mut p = PathBuf::from(home);
201 p.push(".matrix");
202 Ok(p)
203 }
204
205 pub fn global_memory_path(&self) -> PathBuf {
207 self.base_dir.join("memory.json")
208 }
209
210 pub fn project_memory_path(&self) -> Option<PathBuf> {
212 self.project_root
213 .as_ref()
214 .map(|p| p.join(".matrix/memory.json"))
215 }
216
217 pub fn config_path(&self) -> PathBuf {
219 self.base_dir.join("memory_config.json")
220 }
221
222 fn ensure_dirs(&self) -> Result<()> {
224 fs::create_dir_all(&self.base_dir)?;
225 if let Some(root) = &self.project_root {
226 let memory_dir = root.join(".matrix");
227 fs::create_dir_all(memory_dir)?;
228 }
229 Ok(())
230 }
231
232 fn acquire_lock(&mut self) -> Result<()> {
234 self.lock.acquire(5000)?;
235 Ok(())
236 }
237
238 fn release_lock(&mut self) -> Result<()> {
240 self.lock.release()?;
241 Ok(())
242 }
243
244 pub fn load_global(&self) -> Result<AutoMemory> {
246 let path = self.global_memory_path();
247 if !path.exists() {
248 return Ok(AutoMemory::new());
249 }
250 let data = fs::read_to_string(&path)?;
251 let memory: AutoMemory = serde_json::from_str(&data)?;
252 Ok(memory)
253 }
254
255 pub fn load_project(&self) -> Result<Option<AutoMemory>> {
257 let path = self.project_memory_path();
258 match path {
259 Some(p) if p.exists() => {
260 let data = fs::read_to_string(&p)?;
261 let memory: AutoMemory = serde_json::from_str(&data)?;
262 Ok(Some(memory))
263 }
264 _ => Ok(None),
265 }
266 }
267
268 pub fn load_combined(&self) -> Result<AutoMemory> {
270 let mut combined = self.load_global()?;
271
272 if let Some(project) = self.load_project()? {
273 for entry in project.entries {
274 let mut tagged_entry = entry;
275 if !tagged_entry.tags.contains(&"project".to_string()) {
276 tagged_entry.tags.push("project".to_string());
277 }
278 combined.entries.push(tagged_entry);
279 }
280 combined.prune();
281 }
282
283 Ok(combined)
284 }
285
286 pub fn save_global(&mut self, memory: &AutoMemory) -> Result<()> {
288 self.acquire_lock()?;
289 self.ensure_dirs()?;
290
291 let path = self.global_memory_path();
292 let json = serde_json::to_string_pretty(memory)?;
293
294 let tmp = path.with_extension("json.tmp");
295 fs::write(&tmp, json)?;
296 fs::rename(&tmp, &path)?;
297
298 self.release_lock()?;
299 Ok(())
300 }
301
302 pub fn save_project(&mut self, memory: &AutoMemory) -> Result<()> {
304 self.acquire_lock()?;
305 self.ensure_dirs()?;
306
307 let path = self
308 .project_memory_path()
309 .ok_or_else(|| anyhow::anyhow!("no project root"))?;
310 let json = serde_json::to_string_pretty(memory)?;
311
312 let tmp = path.with_extension("json.tmp");
313 fs::write(&tmp, json)?;
314 fs::rename(&tmp, &path)?;
315
316 self.release_lock()?;
317 Ok(())
318 }
319
320 pub fn save_config(&mut self, config: &MemoryConfig) -> Result<()> {
322 self.ensure_dirs()?;
323 let path = self.config_path();
324 let json = serde_json::to_string_pretty(config)?;
325 fs::write(&path, json)?;
326 Ok(())
327 }
328
329 pub fn load_config(&self) -> Result<MemoryConfig> {
331 let path = self.config_path();
332 if !path.exists() {
333 return Ok(MemoryConfig::default());
334 }
335 let data = fs::read_to_string(&path)?;
336 let config: MemoryConfig = serde_json::from_str(&data)?;
337 Ok(config)
338 }
339
340 pub fn add_entry(&mut self, entry: MemoryEntry, is_project_specific: bool) -> Result<()> {
342 self.acquire_lock()?;
343
344 if is_project_specific {
345 let mut project = self.load_project()?.unwrap_or_else(AutoMemory::new);
346 project.add(entry);
347 self.save_project_locked(&project)?;
348 } else {
349 let mut global = self.load_global()?;
350 global.add(entry);
351 self.save_global_locked(&global)?;
352 }
353
354 self.release_lock()?;
355 Ok(())
356 }
357
358 pub fn remove_entry(&mut self, id: &str, is_project_specific: bool) -> Result<bool> {
360 self.acquire_lock()?;
361
362 let removed = if is_project_specific {
363 if let Some(mut project) = self.load_project()? {
364 let removed = project.remove(id);
365 if removed {
366 self.save_project_locked(&project)?;
367 }
368 removed
369 } else {
370 false
371 }
372 } else {
373 let mut global = self.load_global()?;
374 let removed = global.remove(id);
375 if removed {
376 self.save_global_locked(&global)?;
377 }
378 removed
379 };
380
381 self.release_lock()?;
382 Ok(removed)
383 }
384
385 fn save_global_locked(&self, memory: &AutoMemory) -> Result<()> {
387 let path = self.global_memory_path();
388 let json = serde_json::to_string_pretty(memory)?;
389 let tmp = path.with_extension("json.tmp");
390 fs::write(&tmp, json)?;
391 fs::rename(&tmp, &path)?;
392 Ok(())
393 }
394
395 fn save_project_locked(&self, memory: &AutoMemory) -> Result<()> {
396 let path = self
397 .project_memory_path()
398 .ok_or_else(|| anyhow::anyhow!("no project root"))?;
399 let json = serde_json::to_string_pretty(memory)?;
400 let tmp = path.with_extension("json.tmp");
401 fs::write(&tmp, json)?;
402 fs::rename(&tmp, &path)?;
403 Ok(())
404 }
405}