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