codex_memory/
manager.rs

1use anyhow::{Context, Result};
2use std::fs;
3use std::io::{BufRead, BufReader};
4use std::path::PathBuf;
5use std::process::{Command, Stdio};
6use tracing::{info, warn};
7
8/// Default paths for manager files
9pub struct ManagerPaths {
10    pub pid_file: PathBuf,
11    pub log_file: PathBuf,
12    pub config_dir: PathBuf,
13}
14
15impl Default for ManagerPaths {
16    fn default() -> Self {
17        let config_dir = dirs::config_dir()
18            .unwrap_or_else(|| PathBuf::from("."))
19            .join("codex-memory");
20        
21        Self {
22            pid_file: config_dir.join("codex-memory.pid"),
23            log_file: config_dir.join("codex-memory.log"),
24            config_dir,
25        }
26    }
27}
28
29/// Server manager for handling daemon processes
30pub struct ServerManager {
31    paths: ManagerPaths,
32}
33
34impl ServerManager {
35    pub fn new() -> Self {
36        let paths = ManagerPaths::default();
37        
38        // Ensure config directory exists
39        if !paths.config_dir.exists() {
40            fs::create_dir_all(&paths.config_dir).ok();
41        }
42        
43        Self { paths }
44    }
45
46    pub fn with_paths(pid_file: Option<String>, log_file: Option<String>) -> Self {
47        let mut paths = ManagerPaths::default();
48        
49        if let Some(pid) = pid_file {
50            paths.pid_file = PathBuf::from(pid);
51        }
52        if let Some(log) = log_file {
53            paths.log_file = PathBuf::from(log);
54        }
55        
56        Self { paths }
57    }
58
59    /// Start the server as a daemon
60    pub async fn start_daemon(&self, daemon: bool) -> Result<()> {
61        // Check if already running
62        if let Some(pid) = self.get_running_pid()? {
63            return Err(anyhow::anyhow!("Server already running with PID: {}", pid));
64        }
65
66        info!("Starting Codex Memory server...");
67
68        if daemon {
69            // Start as daemon using fork or platform-specific method
70            self.start_background_process().await?;
71        } else {
72            // Start in foreground - this would need to be implemented differently
73            // For now, we'll just start in background
74            info!("Starting server in foreground mode...");
75            self.start_background_process().await?;
76        }
77
78        Ok(())
79    }
80
81    /// Start server in background
82    async fn start_background_process(&self) -> Result<()> {
83        let exe = std::env::current_exe()
84            .context("Failed to get current executable path")?;
85
86        let log_file = fs::OpenOptions::new()
87            .create(true)
88            .append(true)
89            .open(&self.paths.log_file)
90            .context("Failed to open log file")?;
91
92        let cmd = Command::new(exe)
93            .arg("start")
94            .arg("--skip-setup")
95            .stdout(Stdio::from(log_file.try_clone()?))
96            .stderr(Stdio::from(log_file))
97            .spawn()
98            .context("Failed to spawn server process")?;
99
100        let pid = cmd.id();
101        
102        // Write PID file
103        fs::write(&self.paths.pid_file, pid.to_string())
104            .context("Failed to write PID file")?;
105
106        info!("Server started with PID: {}", pid);
107        info!("Log file: {}", self.paths.log_file.display());
108
109        Ok(())
110    }
111
112    /// Stop the running server
113    pub async fn stop(&self) -> Result<()> {
114        let pid = self.get_running_pid()?
115            .ok_or_else(|| anyhow::anyhow!("Server is not running"))?;
116
117        info!("Stopping server with PID: {}", pid);
118
119        #[cfg(unix)]
120        {
121            use nix::sys::signal::{self, Signal};
122            use nix::unistd::Pid;
123
124            // Send SIGTERM for graceful shutdown
125            signal::kill(Pid::from_raw(pid as i32), Signal::SIGTERM)
126                .context("Failed to send SIGTERM")?;
127
128            // Wait for process to exit (max 10 seconds)
129            for _ in 0..10 {
130                if !self.is_process_running(pid)? {
131                    break;
132                }
133                tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
134            }
135
136            // Force kill if still running
137            if self.is_process_running(pid)? {
138                warn!("Process didn't stop gracefully, forcing kill");
139                signal::kill(Pid::from_raw(pid as i32), Signal::SIGKILL)
140                    .context("Failed to send SIGKILL")?;
141            }
142        }
143
144        #[cfg(windows)]
145        {
146            // Windows process termination
147            let output = Command::new("taskkill")
148                .args(&["/PID", &pid.to_string(), "/F"])
149                .output()
150                .context("Failed to kill process")?;
151
152            if !output.status.success() {
153                return Err(anyhow::anyhow!("Failed to stop server"));
154            }
155        }
156
157        // Remove PID file
158        fs::remove_file(&self.paths.pid_file).ok();
159        info!("Server stopped");
160
161        Ok(())
162    }
163
164    /// Restart the server
165    pub async fn restart(&self) -> Result<()> {
166        info!("Restarting server...");
167        
168        // Stop if running
169        if self.get_running_pid()?.is_some() {
170            self.stop().await?;
171            tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
172        }
173
174        // Start again
175        self.start_daemon(true).await?;
176        
177        Ok(())
178    }
179
180    /// Get server status
181    pub async fn status(&self, detailed: bool) -> Result<()> {
182        match self.get_running_pid()? {
183            Some(pid) => {
184                println!("● Server is running");
185                println!("  PID: {}", pid);
186                
187                if detailed {
188                    println!("  PID file: {}", self.paths.pid_file.display());
189                    println!("  Log file: {}", self.paths.log_file.display());
190                    
191                    // Show recent logs
192                    if self.paths.log_file.exists() {
193                        println!("\nRecent logs:");
194                        self.show_logs(5, false).await?;
195                    }
196                }
197            }
198            None => {
199                println!("○ Server is not running");
200                
201                if detailed {
202                    println!("  PID file: {}", self.paths.pid_file.display());
203                    println!("  Log file: {}", self.paths.log_file.display());
204                }
205            }
206        }
207        
208        Ok(())
209    }
210
211    /// Show server logs
212    pub async fn show_logs(&self, lines: usize, follow: bool) -> Result<()> {
213        if !self.paths.log_file.exists() {
214            return Err(anyhow::anyhow!("Log file not found"));
215        }
216
217        if follow {
218            // Follow logs (like tail -f)
219            let file = fs::File::open(&self.paths.log_file)?;
220            let reader = BufReader::new(file);
221            
222            println!("Following logs (Ctrl+C to stop)...");
223            for line in reader.lines() {
224                println!("{}", line?);
225            }
226        } else {
227            // Show last N lines
228            let content = fs::read_to_string(&self.paths.log_file)?;
229            let all_lines: Vec<&str> = content.lines().collect();
230            let start = if all_lines.len() > lines { all_lines.len() - lines } else { 0 };
231            
232            for line in &all_lines[start..] {
233                println!("{}", line);
234            }
235        }
236
237        Ok(())
238    }
239
240    /// Install as system service
241    pub async fn install_service(&self, service_type: Option<String>) -> Result<()> {
242        let service_type = service_type.unwrap_or_else(|| {
243            #[cfg(target_os = "linux")]
244            return "systemd".to_string();
245            #[cfg(target_os = "macos")]
246            return "launchd".to_string();
247            #[cfg(target_os = "windows")]
248            return "windows".to_string();
249            #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
250            return "none".to_string();
251        });
252
253        match service_type.as_str() {
254            "systemd" => self.install_systemd_service().await,
255            "launchd" => self.install_launchd_service().await,
256            "windows" => self.install_windows_service().await,
257            _ => Err(anyhow::anyhow!("Unsupported service type: {}", service_type)),
258        }
259    }
260
261    /// Install systemd service (Linux)
262    async fn install_systemd_service(&self) -> Result<()> {
263        #[cfg(target_os = "linux")]
264        {
265            let service_content = format!(
266                r#"[Unit]
267Description=Codex Memory System
268After=network.target postgresql.service
269
270[Service]
271Type=simple
272ExecStart={} start
273ExecStop={} manager stop
274Restart=on-failure
275RestartSec=10
276StandardOutput=append:{}
277StandardError=append:{}
278
279[Install]
280WantedBy=multi-user.target
281"#,
282                std::env::current_exe()?.display(),
283                std::env::current_exe()?.display(),
284                self.paths.log_file.display(),
285                self.paths.log_file.display(),
286            );
287
288            let service_path = PathBuf::from("/etc/systemd/system/codex-memory.service");
289            
290            // Write service file (requires sudo)
291            fs::write(&service_path, service_content)
292                .context("Failed to write service file (need sudo?)")?;
293
294            // Reload systemd
295            Command::new("systemctl")
296                .args(&["daemon-reload"])
297                .status()
298                .context("Failed to reload systemd")?;
299
300            // Enable service
301            Command::new("systemctl")
302                .args(&["enable", "codex-memory.service"])
303                .status()
304                .context("Failed to enable service")?;
305
306            info!("Systemd service installed successfully");
307            info!("Start with: systemctl start codex-memory");
308            Ok(())
309        }
310
311        #[cfg(not(target_os = "linux"))]
312        Err(anyhow::anyhow!("Systemd is only available on Linux"))
313    }
314
315    /// Install launchd service (macOS)
316    async fn install_launchd_service(&self) -> Result<()> {
317        #[cfg(target_os = "macos")]
318        {
319            let plist_content = format!(
320                r#"<?xml version="1.0" encoding="UTF-8"?>
321<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
322<plist version="1.0">
323<dict>
324    <key>Label</key>
325    <string>com.codex.memory</string>
326    <key>ProgramArguments</key>
327    <array>
328        <string>{}</string>
329        <string>start</string>
330    </array>
331    <key>StandardOutPath</key>
332    <string>{}</string>
333    <key>StandardErrorPath</key>
334    <string>{}</string>
335    <key>RunAtLoad</key>
336    <false/>
337    <key>KeepAlive</key>
338    <dict>
339        <key>SuccessfulExit</key>
340        <false/>
341    </dict>
342</dict>
343</plist>
344"#,
345                std::env::current_exe()?.display(),
346                self.paths.log_file.display(),
347                self.paths.log_file.display(),
348            );
349
350            let plist_path = dirs::home_dir()
351                .ok_or_else(|| anyhow::anyhow!("Cannot find home directory"))?
352                .join("Library/LaunchAgents/com.codex.memory.plist");
353
354            // Write plist file
355            fs::write(&plist_path, plist_content)
356                .context("Failed to write plist file")?;
357
358            // Load the service
359            Command::new("launchctl")
360                .args(&["load", plist_path.to_str().unwrap()])
361                .status()
362                .context("Failed to load launchd service")?;
363
364            info!("Launchd service installed successfully");
365            info!("Start with: launchctl start com.codex.memory");
366            Ok(())
367        }
368
369        #[cfg(not(target_os = "macos"))]
370        Err(anyhow::anyhow!("Launchd is only available on macOS"))
371    }
372
373    /// Install Windows service
374    async fn install_windows_service(&self) -> Result<()> {
375        #[cfg(target_os = "windows")]
376        {
377            // Windows service installation using sc.exe
378            let exe_path = std::env::current_exe()?;
379            
380            Command::new("sc")
381                .args(&[
382                    "create",
383                    "CodexMemory",
384                    &format!("binPath= \"{}\" start", exe_path.display()),
385                    "DisplayName= \"Codex Memory System\"",
386                    "start= auto",
387                ])
388                .status()
389                .context("Failed to create Windows service")?;
390
391            info!("Windows service installed successfully");
392            info!("Start with: sc start CodexMemory");
393            Ok(())
394        }
395
396        #[cfg(not(target_os = "windows"))]
397        Err(anyhow::anyhow!("Windows service is only available on Windows"))
398    }
399
400    /// Uninstall system service
401    pub async fn uninstall_service(&self) -> Result<()> {
402        #[cfg(target_os = "linux")]
403        {
404            Command::new("systemctl")
405                .args(&["disable", "codex-memory.service"])
406                .status()?;
407            
408            fs::remove_file("/etc/systemd/system/codex-memory.service")?;
409            
410            Command::new("systemctl")
411                .args(&["daemon-reload"])
412                .status()?;
413                
414            info!("Systemd service uninstalled");
415        }
416
417        #[cfg(target_os = "macos")]
418        {
419            let plist_path = dirs::home_dir()
420                .ok_or_else(|| anyhow::anyhow!("Cannot find home directory"))?
421                .join("Library/LaunchAgents/com.codex.memory.plist");
422
423            Command::new("launchctl")
424                .args(&["unload", plist_path.to_str().unwrap()])
425                .status()?;
426                
427            fs::remove_file(plist_path)?;
428            
429            info!("Launchd service uninstalled");
430        }
431
432        #[cfg(target_os = "windows")]
433        {
434            Command::new("sc")
435                .args(&["delete", "CodexMemory"])
436                .status()?;
437                
438            info!("Windows service uninstalled");
439        }
440
441        Ok(())
442    }
443
444    /// Get PID of running server
445    fn get_running_pid(&self) -> Result<Option<u32>> {
446        if !self.paths.pid_file.exists() {
447            return Ok(None);
448        }
449
450        let pid_str = fs::read_to_string(&self.paths.pid_file)?;
451        let pid: u32 = pid_str.trim().parse()
452            .context("Invalid PID in file")?;
453
454        // Check if process is actually running
455        if self.is_process_running(pid)? {
456            Ok(Some(pid))
457        } else {
458            // Clean up stale PID file
459            fs::remove_file(&self.paths.pid_file).ok();
460            Ok(None)
461        }
462    }
463
464    /// Check if a process is running
465    fn is_process_running(&self, pid: u32) -> Result<bool> {
466        #[cfg(unix)]
467        {
468            use nix::sys::signal::{self, Signal};
469            use nix::unistd::Pid;
470            
471            // Send signal 0 to check if process exists
472            match signal::kill(Pid::from_raw(pid as i32), Signal::SIGCONT) {
473                Ok(_) => Ok(true),
474                Err(nix::errno::Errno::ESRCH) => Ok(false),
475                Err(e) => Err(anyhow::anyhow!("Failed to check process: {}", e)),
476            }
477        }
478
479        #[cfg(windows)]
480        {
481            use std::process::Command;
482            
483            let output = Command::new("tasklist")
484                .args(&["/FI", &format!("PID eq {}", pid)])
485                .output()?;
486                
487            Ok(String::from_utf8_lossy(&output.stdout).contains(&pid.to_string()))
488        }
489    }
490}