claude_code_toolkit/utils/
systemd.rs

1use crate::error::*;
2use dirs::home_dir;
3use std::path::PathBuf;
4use std::process::Stdio;
5use tokio::fs;
6use tokio::process::Command;
7use tracing::{ debug, info, warn };
8
9const SERVICE_NAME: &str = "claude-code-sync.service";
10
11pub struct SystemdManager {
12  systemd_dir: PathBuf,
13  service_file: PathBuf,
14}
15
16impl SystemdManager {
17  pub fn new() -> Result<Self> {
18    let systemd_dir = home_dir()
19      .ok_or("Could not determine home directory")?
20      .join(".config")
21      .join("systemd")
22      .join("user");
23
24    let service_file = systemd_dir.join(SERVICE_NAME);
25
26    Ok(Self {
27      systemd_dir,
28      service_file,
29    })
30  }
31
32  pub async fn generate_service_file(&self) -> Result<String> {
33    // Get the path to the current binary
34    let current_exe = std::env
35      ::current_exe()
36      .map_err(|e| {
37        ClaudeCodeError::Systemd(format!("Failed to get current executable: {}", e))
38      })?;
39
40    let service_content = format!(
41      r#"[Unit]
42Description=Claude Code Credential Sync Daemon
43After=network.target
44
45[Service]
46Type=simple
47ExecStart={} daemon
48Restart=on-failure
49RestartSec=10
50StandardOutput=journal
51StandardError=journal
52SyslogIdentifier=claude-code-sync
53
54# Security settings
55PrivateTmp=true
56NoNewPrivileges=true
57ProtectSystem=strict
58ProtectHome=read-only
59ReadWritePaths=%h/.goodiebag/claude-code %h/.claude
60
61[Install]
62WantedBy=default.target"#,
63      current_exe.display()
64    );
65
66    Ok(service_content)
67  }
68
69  pub async fn install(&self) -> Result<()> {
70    // Ensure systemd user directory exists
71    fs::create_dir_all(&self.systemd_dir).await?;
72
73    // Generate and write service file
74    let service_content = self.generate_service_file().await?;
75    fs::write(&self.service_file, service_content).await?;
76
77    // Reload systemd
78    self.run_systemctl(&["daemon-reload"]).await?;
79
80    // Enable and start the service
81    self.run_systemctl(&["enable", SERVICE_NAME]).await?;
82    self.run_systemctl(&["start", SERVICE_NAME]).await?;
83
84    info!("Successfully installed and started systemd service");
85    Ok(())
86  }
87
88  pub async fn uninstall(&self) -> Result<()> {
89    // Stop and disable the service (ignore errors if not running/enabled)
90    let _ = self.run_systemctl(&["stop", SERVICE_NAME]).await;
91    let _ = self.run_systemctl(&["disable", SERVICE_NAME]).await;
92
93    // Remove service file
94    if self.service_file.exists() {
95      fs::remove_file(&self.service_file).await?;
96    }
97
98    // Reload systemd
99    self.run_systemctl(&["daemon-reload"]).await?;
100
101    info!("Successfully uninstalled systemd service");
102    Ok(())
103  }
104
105  pub async fn start(&self) -> Result<()> {
106    self.run_systemctl(&["start", SERVICE_NAME]).await?;
107    info!("Started systemd service");
108    Ok(())
109  }
110
111  pub async fn stop(&self) -> Result<()> {
112    self.run_systemctl(&["stop", SERVICE_NAME]).await?;
113    info!("Stopped systemd service");
114    Ok(())
115  }
116
117  pub async fn restart(&self) -> Result<()> {
118    self.run_systemctl(&["restart", SERVICE_NAME]).await?;
119    info!("Restarted systemd service");
120    Ok(())
121  }
122
123  pub async fn enable(&self) -> Result<()> {
124    self.run_systemctl(&["enable", SERVICE_NAME]).await?;
125    info!("Enabled systemd service for auto-start");
126    Ok(())
127  }
128
129  pub async fn disable(&self) -> Result<()> {
130    self.run_systemctl(&["disable", SERVICE_NAME]).await?;
131    info!("Disabled systemd service auto-start");
132    Ok(())
133  }
134
135  pub async fn is_running(&self) -> Result<bool> {
136    let output = Command::new("systemctl")
137      .args(["--user", "is-active", SERVICE_NAME])
138      .stdout(Stdio::piped())
139      .stderr(Stdio::piped())
140      .output().await?;
141
142    let stdout = String::from_utf8_lossy(&output.stdout);
143    Ok(stdout.trim() == "active")
144  }
145
146  pub async fn status(&self) -> Result<String> {
147    let output = Command::new("systemctl").args(["--user", "status", SERVICE_NAME]).output().await?;
148
149    // systemctl status returns non-zero exit code if service is not running
150    if output.status.success() || !output.stdout.is_empty() {
151      Ok(String::from_utf8_lossy(&output.stdout).to_string())
152    } else {
153      Ok("Service not found".to_string())
154    }
155  }
156
157  pub async fn logs(&self, lines: usize) -> Result<String> {
158    let output = Command::new("journalctl")
159      .args(["--user", "-u", SERVICE_NAME, "-n", &lines.to_string(), "--no-pager"])
160      .output().await?;
161
162    if output.status.success() {
163      Ok(String::from_utf8_lossy(&output.stdout).to_string())
164    } else {
165      let error_msg = String::from_utf8_lossy(&output.stderr);
166      Err(ClaudeCodeError::Systemd(format!("Failed to get logs: {}", error_msg)))
167    }
168  }
169
170  async fn run_systemctl(&self, args: &[&str]) -> Result<()> {
171    let mut cmd = Command::new("systemctl");
172    cmd.arg("--user");
173    cmd.args(args);
174
175    let output = cmd.output().await?;
176
177    if output.status.success() {
178      debug!("systemctl command succeeded: {:?}", args);
179      Ok(())
180    } else {
181      let error_msg = String::from_utf8_lossy(&output.stderr);
182      warn!("systemctl command failed: {:?}, error: {}", args, error_msg);
183      Err(ClaudeCodeError::Systemd(format!("systemctl command failed: {}", error_msg)))
184    }
185  }
186}