cc_switch/daemon/
commands.rs1use crate::config::ConfigStorage;
2use anyhow::Result;
3
4#[cfg(unix)]
5use crate::daemon::lifecycle::LifecycleConfig;
6#[cfg(unix)]
7use crate::daemon::pidfile::{Pidfile, process_alive, process_name};
8#[cfg(unix)]
9use crate::daemon::state::DaemonState;
10#[cfg(unix)]
11use anyhow::Context;
12
13pub enum DaemonAction {
14 Start {
15 foreground: bool,
16 log_level: Option<String>,
17 verbose: u8,
18 },
19 Stop,
20 Status {
21 json: bool,
22 },
23 Restart {
24 foreground: bool,
25 log_level: Option<String>,
26 verbose: u8,
27 },
28}
29
30pub fn handle_daemon_command(action: DaemonAction, storage: &ConfigStorage) -> Result<()> {
31 #[cfg(not(unix))]
32 {
33 let _ = (action, storage);
34 anyhow::bail!("cc daemon is Unix-only in v1 — run `ccs-proxy serve` directly");
35 }
36
37 #[cfg(unix)]
38 match action {
39 DaemonAction::Start {
40 foreground,
41 log_level,
42 verbose,
43 } => handle_start(foreground, log_level, verbose, storage),
44 DaemonAction::Stop => handle_stop(),
45 DaemonAction::Status { json } => handle_status(json, storage),
46 DaemonAction::Restart {
47 foreground,
48 log_level,
49 verbose,
50 } => {
51 let _ = handle_stop();
52 handle_start(foreground, log_level, verbose, storage)
53 }
54 }
55}
56
57#[cfg(unix)]
58fn handle_start(
59 foreground: bool,
60 log_level: Option<String>,
61 verbose: u8,
62 storage: &ConfigStorage,
63) -> Result<()> {
64 let cfg = LifecycleConfig::from_storage(storage, foreground)?;
65
66 let pidfile = Pidfile::new(cfg.pidfile_path.clone());
68 if let Some(pid) = pidfile.read()? {
69 if process_alive(pid)? {
70 let is_ours = match process_name(pid) {
72 Some(name) => name.contains("cc-switch") || name.contains("cc_switch"),
73 None => false,
76 };
77 if is_ours {
78 anyhow::bail!(
79 "daemon already running (PID {pid}). Use `cc-switch daemon stop` first."
80 );
81 }
82 eprintln!(
84 "warning: pidfile references PID {pid} which is alive but not cc-switch — removing stale pidfile"
85 );
86 pidfile.release()?;
87 } else {
88 eprintln!("warning: stale pidfile for dead PID {pid} — removing");
89 pidfile.release()?;
90 }
91 }
92
93 if !foreground {
94 let home = dirs::home_dir().context("could not find home directory")?;
95 let log_path = home.join(".cc-switch").join("daemon.log");
96 let is_daemon = crate::daemon::fork::double_fork_into_background(&log_path)?;
97 if !is_daemon {
98 wait_and_print_status(&cfg.state_path);
100 std::process::exit(0);
101 }
102 }
103
104 crate::daemon::lifecycle::run_daemon_blocking(cfg, log_level, verbose)
105}
106
107#[cfg(unix)]
108fn wait_and_print_status(state_path: &std::path::Path) {
109 for _ in 0..30 {
110 std::thread::sleep(std::time::Duration::from_millis(100));
111 if let Ok(contents) = std::fs::read_to_string(state_path)
112 && let Ok(state) = serde_json::from_str::<DaemonState>(&contents)
113 {
114 eprintln!("daemon started (PID {})", state.pid);
115 if let Some(port) = state.agg_port {
116 eprintln!("aggregate dashboard: http://localhost:{port}");
117 }
118 for proxy in &state.proxies {
119 eprintln!(" proxy :{} → {}", proxy.proxy_port, proxy.upstream);
120 }
121 return;
122 }
123 }
124 eprintln!("daemon starting in background (state file not yet available)");
125}
126
127#[cfg(unix)]
128fn handle_stop() -> Result<()> {
129 let home = dirs::home_dir().context("could not find home directory")?;
130 let pidfile_path = home.join(".cc-switch").join("daemon.pid");
131 let pidfile = Pidfile::new(pidfile_path);
132
133 let pid = match pidfile.read()? {
134 Some(pid) => pid,
135 None => {
136 eprintln!("daemon not running (no pidfile)");
137 return Ok(());
138 }
139 };
140
141 if !process_alive(pid)? {
142 eprintln!("daemon not running (stale pidfile for PID {pid}) — cleaning up");
143 pidfile.release()?;
144 return Ok(());
145 }
146
147 let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGTERM) };
149 if ret != 0 {
150 let err = std::io::Error::last_os_error();
151 if err.raw_os_error() == Some(libc::ESRCH) {
152 eprintln!("daemon not running (PID {pid} gone) — cleaning up");
153 pidfile.release()?;
154 return Ok(());
155 }
156 return Err(err).with_context(|| format!("failed to send SIGTERM to PID {pid}"));
157 }
158
159 for _ in 0..50 {
161 std::thread::sleep(std::time::Duration::from_millis(100));
162 if !process_alive(pid)? {
163 eprintln!("daemon stopped (PID {pid})");
164 return Ok(());
165 }
166 }
167
168 eprintln!("warning: daemon PID {pid} did not exit after 5s — sending SIGKILL");
170 unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL) };
171 std::thread::sleep(std::time::Duration::from_millis(200));
172 pidfile.release()?;
173 eprintln!("daemon killed");
174 Ok(())
175}
176
177#[cfg(unix)]
178fn handle_status(json: bool, storage: &ConfigStorage) -> Result<()> {
179 let home = dirs::home_dir().context("could not find home directory")?;
180 let cc_switch_dir = home.join(".cc-switch");
181 let pidfile_path = cc_switch_dir.join("daemon.pid");
182 let state_path = cc_switch_dir.join("daemon-state.json");
183
184 let pidfile = Pidfile::new(pidfile_path);
185 let pid = match pidfile.read()? {
186 Some(pid) => pid,
187 None => {
188 if json {
189 println!("{{\"status\":\"stopped\"}}");
190 } else {
191 println!("ccs-daemon: STOPPED (no pidfile)");
192 }
193 return Ok(());
194 }
195 };
196
197 if !process_alive(pid)? {
198 if json {
199 println!("{{\"status\":\"stopped\",\"stale_pid\":{pid}}}");
200 } else {
201 println!("ccs-daemon: STOPPED (stale pidfile, PID {pid} is dead)");
202 }
203 return Ok(());
204 }
205
206 let state = match DaemonState::load(&state_path)? {
207 Some(s) => s,
208 None => {
209 if json {
210 println!("{{\"status\":\"running\",\"pid\":{pid},\"proxies\":[]}}");
211 } else {
212 println!("ccs-daemon: RUNNING (pid {pid}) — no state file");
213 }
214 return Ok(());
215 }
216 };
217
218 let aliases_by_upstream = crate::daemon::status::build_aliases_by_upstream(storage);
219 let statuses = crate::daemon::status::collect_status(&state);
220
221 if json {
222 let output = crate::daemon::status::format_status_json(&state, &statuses);
223 println!("{}", serde_json::to_string_pretty(&output)?);
224 } else {
225 let text =
226 crate::daemon::status::format_status_text(&state, &statuses, &aliases_by_upstream);
227 print!("{text}");
228 crate::daemon::print_version_mismatch_warning();
229 }
230
231 Ok(())
232}