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 crate::daemon::fork::double_fork_into_background(&log_path)?;
97 }
98
99 crate::daemon::lifecycle::run_daemon_blocking(cfg, log_level, verbose)
100}
101
102#[cfg(unix)]
103fn handle_stop() -> Result<()> {
104 let home = dirs::home_dir().context("could not find home directory")?;
105 let pidfile_path = home.join(".cc-switch").join("daemon.pid");
106 let pidfile = Pidfile::new(pidfile_path);
107
108 let pid = match pidfile.read()? {
109 Some(pid) => pid,
110 None => {
111 eprintln!("daemon not running (no pidfile)");
112 return Ok(());
113 }
114 };
115
116 if !process_alive(pid)? {
117 eprintln!("daemon not running (stale pidfile for PID {pid}) — cleaning up");
118 pidfile.release()?;
119 return Ok(());
120 }
121
122 let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGTERM) };
124 if ret != 0 {
125 let err = std::io::Error::last_os_error();
126 if err.raw_os_error() == Some(libc::ESRCH) {
127 eprintln!("daemon not running (PID {pid} gone) — cleaning up");
128 pidfile.release()?;
129 return Ok(());
130 }
131 return Err(err).with_context(|| format!("failed to send SIGTERM to PID {pid}"));
132 }
133
134 for _ in 0..50 {
136 std::thread::sleep(std::time::Duration::from_millis(100));
137 if !process_alive(pid)? {
138 eprintln!("daemon stopped (PID {pid})");
139 return Ok(());
140 }
141 }
142
143 eprintln!("warning: daemon PID {pid} did not exit after 5s — sending SIGKILL");
145 unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL) };
146 std::thread::sleep(std::time::Duration::from_millis(200));
147 pidfile.release()?;
148 eprintln!("daemon killed");
149 Ok(())
150}
151
152#[cfg(unix)]
153fn handle_status(json: bool, storage: &ConfigStorage) -> Result<()> {
154 let home = dirs::home_dir().context("could not find home directory")?;
155 let cc_switch_dir = home.join(".cc-switch");
156 let pidfile_path = cc_switch_dir.join("daemon.pid");
157 let state_path = cc_switch_dir.join("daemon-state.json");
158
159 let pidfile = Pidfile::new(pidfile_path);
160 let pid = match pidfile.read()? {
161 Some(pid) => pid,
162 None => {
163 if json {
164 println!("{{\"status\":\"stopped\"}}");
165 } else {
166 println!("ccs-daemon: STOPPED (no pidfile)");
167 }
168 return Ok(());
169 }
170 };
171
172 if !process_alive(pid)? {
173 if json {
174 println!("{{\"status\":\"stopped\",\"stale_pid\":{pid}}}");
175 } else {
176 println!("ccs-daemon: STOPPED (stale pidfile, PID {pid} is dead)");
177 }
178 return Ok(());
179 }
180
181 let state = match DaemonState::load(&state_path)? {
182 Some(s) => s,
183 None => {
184 if json {
185 println!("{{\"status\":\"running\",\"pid\":{pid},\"proxies\":[]}}");
186 } else {
187 println!("ccs-daemon: RUNNING (pid {pid}) — no state file");
188 }
189 return Ok(());
190 }
191 };
192
193 let aliases_by_upstream = crate::daemon::status::build_aliases_by_upstream(storage);
194 let statuses = crate::daemon::status::collect_status(&state);
195
196 if json {
197 let output = crate::daemon::status::format_status_json(&state, &statuses);
198 println!("{}", serde_json::to_string_pretty(&output)?);
199 } else {
200 let text =
201 crate::daemon::status::format_status_text(&state, &statuses, &aliases_by_upstream);
202 print!("{text}");
203 }
204
205 Ok(())
206}