claude_code_toolkit/utils/
systemd.rs1use 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 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 fs::create_dir_all(&self.systemd_dir).await?;
72
73 let service_content = self.generate_service_file().await?;
75 fs::write(&self.service_file, service_content).await?;
76
77 self.run_systemctl(&["daemon-reload"]).await?;
79
80 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 let _ = self.run_systemctl(&["stop", SERVICE_NAME]).await;
91 let _ = self.run_systemctl(&["disable", SERVICE_NAME]).await;
92
93 if self.service_file.exists() {
95 fs::remove_file(&self.service_file).await?;
96 }
97
98 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 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}