Skip to main content

pwr_server/
daemon.rs

1//! Unix daemonization for pwr-server.
2//!
3//! Implements the classic double-fork daemonization pattern:
4//! 1. Fork → parent exits, child continues
5//! 2. setsid() to become session leader
6//! 3. Second fork → child exits, grandchild continues (prevents
7//!    re-acquisition of a controlling terminal)
8//! 4. chdir("/"), umask(0), redirect stdio to /dev/null
9//! 5. Write PID file
10
11use std::fs;
12use std::io;
13use std::os::unix::io::AsRawFd;
14use std::path::Path;
15use std::process;
16use std::thread;
17use std::time::Duration;
18
19/// Default path for the PID file written after successful daemonization
20/// (system-wide install).
21pub const SYSTEM_PID_FILE: &str = "/run/pwr/pwr-server.pid";
22
23/// Return the PID file path for a user-level install.
24pub fn user_pid_file() -> std::path::PathBuf {
25    crate::config::user_runtime_dir().join("pwr-server.pid")
26}
27
28/// Given a config file path, return the appropriate PID file.
29///
30/// - If the config lives under `/etc/pwr/`, use the system PID path.
31/// - Otherwise, use the per-user XDG runtime path.
32pub fn pid_file_for_config(config_path: &Path) -> std::path::PathBuf {
33    let system_base = crate::config::system_config_dir();
34    if config_path.starts_with(&system_base) {
35        std::path::PathBuf::from(SYSTEM_PID_FILE)
36    } else {
37        user_pid_file()
38    }
39}
40
41/// Daemonize the current process.
42///
43/// After this call returns in the grandchild process, the process is:
44/// - Running as a session leader with no controlling terminal
45/// - In the root directory (/)
46/// - With umask 0
47/// - With stdin/stdout/stderr redirected to /dev/null
48/// - With a PID file written to `pid_file`
49///
50/// The parent and intermediate child processes exit with status 0.
51pub fn daemonize(pid_file: &Path) -> Result<(), String> {
52    // Phase 1: first fork — detach from the invoking shell
53    match unsafe { libc::fork() } {
54        -1 => return Err(format!("First fork failed: {}", io::Error::last_os_error())),
55        0 => {} // child continues
56        _ => process::exit(0), // parent exits
57    }
58
59    // Phase 2: become session leader, detach from controlling terminal
60    if unsafe { libc::setsid() } == -1 {
61        return Err(format!("setsid failed: {}", io::Error::last_os_error()));
62    }
63
64    // Phase 3: second fork — relinquish session leadership so the process
65    // can never re-acquire a controlling terminal
66    match unsafe { libc::fork() } {
67        -1 => return Err(format!("Second fork failed: {}", io::Error::last_os_error())),
68        0 => {} // grandchild continues
69        _ => process::exit(0), // first child exits
70    }
71
72    // Phase 4: set up the daemon environment
73    //
74    // Change to root so we don't hold a reference to any directory that
75    // might need to be unmounted.
76    if unsafe { libc::chdir(b"/\0".as_ptr() as *const _) } == -1 {
77        return Err(format!("chdir(/) failed: {}", io::Error::last_os_error()));
78    }
79
80    // Clear the file creation mask so we control permissions explicitly.
81    unsafe {
82        libc::umask(0);
83    }
84
85    // Redirect stdin/stdout/stderr to /dev/null.
86    redirect_stdio_to_devnull()?;
87
88    // Phase 5: write PID file
89    write_pid_file(pid_file)?;
90
91    Ok(())
92}
93
94/// Redirect stdin (0), stdout (1), and stderr (2) to /dev/null.
95fn redirect_stdio_to_devnull() -> Result<(), String> {
96    let devnull = fs::OpenOptions::new()
97        .read(true)
98        .write(true)
99        .open("/dev/null")
100        .map_err(|e| format!("Cannot open /dev/null: {}", e))?;
101
102    let devnull_fd = devnull.as_raw_fd();
103
104    for fd in [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO] {
105        if fd != devnull_fd {
106            if unsafe { libc::dup2(devnull_fd, fd) } == -1 {
107                return Err(format!(
108                    "Cannot redirect fd {} to /dev/null: {}",
109                    fd,
110                    io::Error::last_os_error()
111                ));
112            }
113        }
114    }
115
116    // The devnull File handle is dropped here; the duplicated fds keep
117    // /dev/null open for the process lifetime.
118    Ok(())
119}
120
121/// Atomically write the current PID to `pid_file`.
122///
123/// Creates parent directories as needed.
124fn write_pid_file(path: &Path) -> Result<(), String> {
125    if let Some(parent) = path.parent() {
126        fs::create_dir_all(parent)
127            .map_err(|e| format!("Cannot create PID directory {}: {}", parent.display(), e))?;
128    }
129
130    let pid = unsafe { libc::getpid() };
131    let contents = format!("{}\n", pid);
132
133    fs::write(path, &contents)
134        .map_err(|e| format!("Cannot write PID file {}: {}", path.display(), e))?;
135
136    Ok(())
137}
138
139// ---------------------------------------------------------------------------
140// Daemon stop
141// ---------------------------------------------------------------------------
142
143/// Stop a running pwr-server daemon.
144///
145/// Reads the PID from `pid_file`, sends SIGTERM, waits up to 5 seconds for
146/// graceful shutdown, then escalates to SIGKILL if the process is still alive.
147/// Removes the PID file on successful termination.
148///
149/// Returns a human-readable summary of what happened.
150pub fn stop_daemon(pid_file: &Path) -> Result<String, String> {
151    let pid = read_pid_file(pid_file)?;
152
153    // Check the process exists and is actually a pwr-server
154    if !is_process_running(pid) {
155        // Stale PID file — clean it up
156        remove_pid_file(pid_file)?;
157        return Err(format!(
158            "PID {} is not running (stale PID file removed)",
159            pid
160        ));
161    }
162
163    // Phase 1: graceful shutdown via SIGTERM
164    send_signal(pid, libc::SIGTERM)?;
165
166    // Wait up to 5 seconds for the process to exit
167    let grace = Duration::from_secs(5);
168    let poll = Duration::from_millis(100);
169    let deadline = std::time::Instant::now() + grace;
170
171    let mut clean_exit = false;
172    while std::time::Instant::now() < deadline {
173        if !is_process_running(pid) {
174            clean_exit = true;
175            break;
176        }
177        thread::sleep(poll);
178    }
179
180    if clean_exit {
181        remove_pid_file(pid_file)?;
182        return Ok(format!("pwr-server (PID {}) stopped gracefully", pid));
183    }
184
185    // Phase 2: force kill
186    send_signal(pid, libc::SIGKILL)?;
187    thread::sleep(Duration::from_millis(200));
188
189    if is_process_running(pid) {
190        return Err(format!(
191            "Failed to kill pwr-server (PID {}) — process did not respond to SIGKILL",
192            pid
193        ));
194    }
195
196    remove_pid_file(pid_file)?;
197    Ok(format!(
198        "pwr-server (PID {}) killed (did not respond to SIGTERM within {}s)",
199        pid,
200        grace.as_secs()
201    ))
202}
203
204/// Read a PID from the given file.
205///
206/// Returns an error if the file is missing, unreadable, or contains a
207/// non-numeric value.
208fn read_pid_file(path: &Path) -> Result<libc::pid_t, String> {
209    let contents = fs::read_to_string(path)
210        .map_err(|e| format!("Cannot read PID file {}: {}", path.display(), e))?;
211
212    let pid: libc::pid_t = contents
213        .trim()
214        .parse()
215        .map_err(|e| format!("Invalid PID in {}: {}", path.display(), e))?;
216
217    if pid <= 0 {
218        return Err(format!("Invalid PID ({}) in {}", pid, path.display()));
219    }
220
221    Ok(pid)
222}
223
224/// Check whether a process with the given PID is currently running.
225///
226/// Uses `kill(pid, 0)` which performs error checking without sending a
227/// signal — returns true if the process exists, false otherwise.
228fn is_process_running(pid: libc::pid_t) -> bool {
229    // kill(pid, 0) is the standard POSIX way to check for process existence
230    // without sending an actual signal. Returns 0 if the process exists,
231    // -1 with ESRCH if it doesn't.
232    unsafe { libc::kill(pid, 0) == 0 }
233}
234
235/// Send a signal to a process.
236fn send_signal(pid: libc::pid_t, signal: libc::c_int) -> Result<(), String> {
237    if unsafe { libc::kill(pid, signal) } == -1 {
238        return Err(format!(
239            "Cannot send signal {} to PID {}: {}",
240            signal,
241            pid,
242            io::Error::last_os_error()
243        ));
244    }
245    Ok(())
246}
247
248/// Remove the PID file. Errors if removal fails but not if the file is
249/// already gone (e.g. the daemon cleaned up after itself).
250fn remove_pid_file(path: &Path) -> Result<(), String> {
251    match fs::remove_file(path) {
252        Ok(()) => Ok(()),
253        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
254        Err(e) => Err(format!(
255            "Cannot remove PID file {}: {}",
256            path.display(),
257            e
258        )),
259    }
260}