Skip to main content

matrixcode_core/memory/
storage.rs

1//! Memory storage with file locking.
2
3use anyhow::Result;
4use chrono::Utc;
5use std::fs;
6use std::path::{Path, PathBuf};
7
8use super::config::MemoryConfig;
9use super::types::{AutoMemory, MemoryEntry};
10
11// ============================================================================
12// File Lock
13// ============================================================================
14
15/// File lock for preventing concurrent access to memory storage.
16pub struct MemoryFileLock {
17    /// Path to the lock file.
18    lock_path: PathBuf,
19    /// Whether we currently hold the lock.
20    locked: bool,
21}
22
23impl MemoryFileLock {
24    /// Create a new file lock for the given directory.
25    pub fn new(base_dir: &Path) -> Self {
26        Self {
27            lock_path: base_dir.join("memory.lock"),
28            locked: false,
29        }
30    }
31
32    /// Acquire the lock (blocking with timeout).
33    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    /// Check if the existing lock is stale (either old or process is dead).
64    fn is_stale_lock(&self) -> Result<bool> {
65        if !self.lock_path.exists() {
66            return Ok(false);
67        }
68
69        // First check if the lock owner process is still running
70        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            // Process is dead, lock is stale
76            return Ok(true);
77        }
78
79        // Then check lock age as fallback
80        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    /// Check if a process with the given PID is still running.
90    fn is_process_running(&self, pid: u32) -> bool {
91        #[cfg(unix)]
92        {
93            // On Unix, check if process exists by checking /proc
94            if std::path::Path::new("/proc").exists() {
95                std::path::Path::new(&format!("/proc/{}", pid)).exists()
96            } else {
97                // Fallback: assume process is running if we can't check
98                true
99            }
100        }
101        #[cfg(windows)]
102        {
103            // On Windows, use tasklist command to check if process exists
104            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                    // tasklist returns "INFO: No tasks are running..." if process not found
113                    // or returns a line with the PID if running
114                    stdout.contains(&pid.to_string()) && !stdout.contains("No tasks")
115                }
116                Err(_) => {
117                    // If tasklist fails, assume process might be running (safer)
118                    true
119                }
120            }
121        }
122        #[cfg(not(any(unix, windows)))]
123        {
124            let _ = pid;
125            true
126        }
127    }
128
129    /// Remove stale lock file with atomic retry.
130    fn remove_stale_lock(&self) -> Result<()> {
131        // Use atomic rename to avoid race condition
132        // Create a temp deletion marker and rename it over the lock
133        let temp_path = self.lock_path.with_extension("lock.del");
134        if self.lock_path.exists() {
135            // Try atomic rename first
136            if fs::rename(&self.lock_path, &temp_path).is_ok() {
137                fs::remove_file(&temp_path)?;
138            } else {
139                // Fallback to direct removal if rename fails
140                fs::remove_file(&self.lock_path)?;
141            }
142        }
143        Ok(())
144    }
145
146    /// Release the lock.
147    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
162// ============================================================================
163// Memory Storage
164// ============================================================================
165
166/// Storage for memory files (global and project-level) with file locking.
167pub struct MemoryStorage {
168    /// Base directory for global memory (~/.matrix).
169    base_dir: PathBuf,
170    /// Project root directory (optional).
171    project_root: Option<PathBuf>,
172    /// File lock for preventing concurrent writes.
173    lock: MemoryFileLock,
174}
175
176impl MemoryStorage {
177    /// Create a new memory storage.
178    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    /// Create a new storage with explicit lock timeout.
189    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    /// Get the base directory for memory storage.
196    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    /// Path to global memory file.
206    pub fn global_memory_path(&self) -> PathBuf {
207        self.base_dir.join("memory.json")
208    }
209
210    /// Path to project memory file.
211    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    /// Path to config file.
218    pub fn config_path(&self) -> PathBuf {
219        self.base_dir.join("memory_config.json")
220    }
221
222    /// Ensure directories exist.
223    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    /// Acquire lock before write operations.
233    fn acquire_lock(&mut self) -> Result<()> {
234        self.lock.acquire(5000)?;
235        Ok(())
236    }
237
238    /// Release lock after write operations.
239    fn release_lock(&mut self) -> Result<()> {
240        self.lock.release()?;
241        Ok(())
242    }
243
244    /// Load global memory.
245    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    /// Load project memory.
256    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    /// Load combined memory (global + project).
269    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    /// Save global memory (with file lock).
287    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    /// Save project memory (with file lock).
303    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    /// Save config to separate file.
321    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    /// Load config from file.
330    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    /// Add entry to appropriate storage.
341    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    /// Remove entry from storage by ID.
359    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    /// Internal save methods (assumed already locked).
386    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}