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