Skip to main content

kasl/libs/
daemon.rs

1//! Background process management for `kasl watch`: spawn detached,
2//! track by PID file, stop, and the daemon's own signal-handled entry
3//! point.
4//!
5//! ```rust,no_run
6//! # async fn f() -> anyhow::Result<()> {
7//! use kasl::libs::daemon;
8//!
9//! daemon::spawn()?;                           // Start background monitoring
10//! daemon::stop()?;                            // Stop background monitoring
11//! daemon::run_with_signal_handling().await?;  // Run with signal handling
12//! # Ok(())
13//! # }
14//! ```
15
16use crate::libs::config::Config;
17use crate::libs::data_storage::DataStorage;
18use crate::libs::messages::Message;
19use crate::libs::monitor::Monitor;
20use crate::{msg_bail_anyhow, msg_error, msg_error_anyhow, msg_info, msg_warning};
21use anyhow::Result;
22use std::time::Duration;
23use tracing::{debug, info, instrument, warn};
24
25/// PID file in the app data directory; written on spawn, removed on
26/// shutdown, and the single source of "is a daemon running".
27const PID_FILE: &str = "kasl-watch.pid";
28
29/// The daemon entry point: runs the monitor and the Jira inbox poller,
30/// shutting both down on SIGTERM/SIGINT (Ctrl+C on Windows) and removing
31/// the PID file on the way out.
32#[instrument]
33pub async fn run_with_signal_handling() -> Result<()> {
34    info!("Starting daemon with signal handling");
35
36    // Set up a channel to handle shutdown signals
37    // This allows coordinated shutdown between signal handlers and the monitor
38    let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
39
40    // Spawn the signal handler in a separate task
41    // This ensures signal handling doesn't block the main monitoring loop
42    #[cfg(unix)]
43    {
44        tokio::spawn(async move {
45            use tokio::signal::unix::{SignalKind, signal};
46
47            // Set up handlers for standard Unix termination signals
48            let mut sigterm = signal(SignalKind::terminate()).unwrap_or_else(|_| panic!("{}", Message::FailedToCreateSigtermHandler));
49            let mut sigint = signal(SignalKind::interrupt()).unwrap_or_else(|_| panic!("{}", Message::FailedToCreateSigintHandler));
50
51            // Wait for any termination signal
52            tokio::select! {
53                _ = sigterm.recv() => {
54                    msg_info!(Message::WatcherReceivedSigterm);
55                }
56                _ = sigint.recv() => {
57                    msg_info!(Message::WatcherReceivedSigint);
58                }
59            }
60
61            // Signal the main loop to shut down gracefully
62            let _ = shutdown_tx.send(());
63        });
64    }
65
66    #[cfg(windows)]
67    {
68        tokio::spawn(async move {
69            // Handle Windows console events
70            match tokio::signal::ctrl_c().await {
71                Ok(()) => {
72                    msg_info!(Message::WatcherReceivedCtrlC);
73                }
74                Err(e) => {
75                    msg_error!(Message::WatcherCtrlCListenFailed(e.to_string()));
76                }
77            }
78
79            // Signal the main loop to shut down gracefully
80            let _ = shutdown_tx.send(());
81        });
82    }
83
84    #[cfg(not(any(unix, windows)))]
85    {
86        // For other platforms, just run without signal handling
87        // This ensures the application still works on unsupported platforms
88        msg_warning!(Message::WatcherSignalHandlingNotSupported);
89    }
90
91    // Run the monitor in a separate task
92    // This allows concurrent execution with signal handling
93    let monitor_handle = tokio::spawn(async move {
94        match run_monitor().await {
95            Ok(()) => Ok(()),
96            Err(e) => Err(Message::MonitorError(e.to_string())),
97        }
98    });
99
100    // Poll Jira inbox in a sibling task (independent cadence from activity monitor)
101    let inbox_handle = tokio::spawn(async move {
102        crate::libs::jira_inbox::run_poller().await;
103    });
104
105    // Wait for either the monitor to finish or a shutdown signal
106    // This provides coordinated shutdown between different components
107    tokio::select! {
108        result = monitor_handle => {
109            // Monitor task completed (either successfully or with error)
110            inbox_handle.abort();
111            match result {
112                Ok(Ok(())) => msg_info!(Message::MonitorExitedNormally),
113                Ok(Err(e)) => msg_error!(Message::MonitorError(e.to_string())),
114                Err(e) => msg_error!(Message::MonitorTaskPanicked(e.to_string())),
115            }
116        }
117        _ = shutdown_rx => {
118            // Received shutdown signal
119            inbox_handle.abort();
120            msg_info!(Message::MonitorShuttingDown);
121            // The monitor will be dropped when this function exits
122        }
123    }
124
125    // Clean up PID file on exit
126    // This ensures the PID file doesn't become stale
127    let pid_path = DataStorage::new().get_path(PID_FILE)?;
128    if pid_path.exists() {
129        let _ = std::fs::remove_file(&pid_path);
130    }
131
132    Ok(())
133}
134
135/// Loads config (defaults for missing sections) and runs the monitor loop.
136async fn run_monitor() -> Result<()> {
137    let config = Config::read()?;
138    let monitor_config = config.monitor.unwrap_or_default();
139
140    let mut monitor = Monitor::new(monitor_config)?;
141    monitor.run().await
142}
143
144/// Re-launches the current executable detached (`--daemon-run`), first
145/// stopping any daemon the PID file points at, and records the new PID.
146/// Detachment is `setsid` on Unix, `CREATE_NO_WINDOW` on Windows; a
147/// failed stop of the old daemon is a warning, not a blocker.
148///
149/// ```rust,no_run
150/// # fn main() -> anyhow::Result<()> {
151/// use kasl::libs::daemon;
152///
153/// // Start background monitoring
154/// daemon::spawn()?;
155/// println!("Background monitoring started");
156/// # Ok(())
157/// # }
158/// ```
159#[instrument]
160pub fn spawn() -> Result<()> {
161    debug!("Attempting to spawn daemon process");
162    let pid_path = DataStorage::new().get_path(PID_FILE)?;
163
164    // Check if a daemon is already running and stop it
165    // This ensures only one daemon instance is active at a time
166    if pid_path.exists()
167        && let Ok(pid_str) = std::fs::read_to_string(&pid_path)
168    {
169        msg_info!(Message::WatcherStoppingExisting(pid_str.trim().to_string()));
170
171        // Try to stop the existing daemon
172        if let Err(e) = stop_internal() {
173            msg_warning!(Message::WatcherFailedToStopExisting(e.to_string()));
174            // Remove the PID file anyway in case the process is already dead
175            let _ = std::fs::remove_file(&pid_path);
176        }
177
178        // Give the old process time to clean up
179        std::thread::sleep(Duration::from_millis(1000));
180    }
181
182    // Get the current executable path for spawning
183    let current_exe = std::env::current_exe().unwrap_or_else(|_| panic!("{}", Message::FailedToGetCurrentExecutable.to_string()));
184
185    #[cfg(unix)]
186    {
187        use std::os::unix::process::CommandExt;
188
189        // Spawn daemon process with session detachment
190        let mut command = std::process::Command::new(current_exe);
191        command.arg("--daemon-run");
192        // SAFETY: setsid is async-signal-safe and touches no shared state,
193        // which is all pre_exec requires between fork and exec.
194        unsafe {
195            command.pre_exec(|| {
196                // Detach from the current session to become a daemon
197                // This ensures the process continues running after parent exits
198                nix::unistd::setsid()?;
199                Ok(())
200            });
201        }
202        let child = command.spawn()?;
203
204        let pid = child.id();
205        std::fs::write(pid_path, pid.to_string())?;
206        msg_info!(Message::WatcherStarted(pid));
207    }
208
209    #[cfg(windows)]
210    {
211        use std::os::windows::process::CommandExt;
212
213        // Windows-specific flags for background process creation
214        const CREATE_NO_WINDOW: u32 = 0x08000000;
215
216        // Spawn daemon process without console window
217        let child = std::process::Command::new(current_exe)
218            .arg("--daemon-run")
219            .creation_flags(CREATE_NO_WINDOW)
220            .spawn()?;
221
222        let pid = child.id();
223        std::fs::write(pid_path, pid.to_string())?;
224        msg_info!(Message::WatcherStarted(pid));
225    }
226
227    #[cfg(not(any(unix, windows)))]
228    {
229        // Platform not supported for daemon mode
230        msg_bail_anyhow!(Message::DaemonModeNotSupported);
231    }
232
233    Ok(())
234}
235
236/// True when the PID file exists, parses, and names a live process.
237pub fn is_running() -> bool {
238    let pid_path = match DataStorage::new().get_path(PID_FILE) {
239        Ok(path) => path,
240        Err(_) => return false,
241    };
242
243    // Check if PID file exists
244    if !pid_path.exists() {
245        return false;
246    }
247
248    // Read and parse the PID from the file
249    let pid_str = match std::fs::read_to_string(&pid_path) {
250        Ok(content) => content,
251        Err(_) => return false,
252    };
253
254    let pid: u32 = match pid_str.trim().parse() {
255        Ok(pid) => pid,
256        Err(_) => return false,
257    };
258
259    // Check if process is actually running
260    is_process_running(pid)
261}
262
263/// Platform-specific "does this PID exist" probe.
264fn is_process_running(pid: u32) -> bool {
265    #[cfg(windows)]
266    {
267        use winapi::um::errhandlingapi::GetLastError;
268        use winapi::um::handleapi::CloseHandle;
269        use winapi::um::processthreadsapi::OpenProcess;
270        use winapi::um::winnt::PROCESS_QUERY_INFORMATION;
271
272        unsafe {
273            let handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
274            if handle.is_null() {
275                let error = GetLastError();
276                // ERROR_INVALID_PARAMETER (87) means process doesn't exist
277                return error != 87;
278            }
279            CloseHandle(handle);
280            true
281        }
282    }
283
284    #[cfg(unix)]
285    {
286        use std::process::Command;
287
288        // Use ps command to check if process exists
289        match Command::new("ps").arg("-p").arg(pid.to_string()).output() {
290            Ok(output) => output.status.success(),
291            Err(_) => false,
292        }
293    }
294
295    #[cfg(not(any(unix, windows)))]
296    {
297        // For unsupported platforms, assume not running
298        false
299    }
300}
301
302/// Stops the daemon; "already stopped" counts as success, so cleanup
303/// scripts can call it unconditionally.
304///
305/// ```rust,no_run
306/// # fn main() -> anyhow::Result<()> {
307/// use kasl::libs::daemon;
308///
309/// daemon::stop()?;
310/// println!("Monitoring stopped");
311/// # Ok(())
312/// # }
313/// ```
314pub fn stop() -> Result<()> {
315    match stop_internal() {
316        Ok(()) => Ok(()),
317        Err(e) => {
318            // If the daemon wasn't running, that's okay
319            // This provides a better user experience than reporting errors
320            if e.to_string().contains("not found") || e.to_string().contains("not running") {
321                msg_info!(Message::WatcherNotRunning);
322                Ok(())
323            } else {
324                Err(e)
325            }
326        }
327    }
328}
329
330/// Termination with precise errors, shared by [`stop`] and [`spawn`];
331/// the PID file is removed even when the process is already gone.
332fn stop_internal() -> Result<()> {
333    let pid_path = DataStorage::new().get_path(PID_FILE)?;
334
335    // The daemon removes its own PID file on shutdown, so every file
336    // operation below can race with a dying daemon: a file that has
337    // disappeared at any step means the watcher is already stopped.
338    let pid_str = match std::fs::read_to_string(&pid_path) {
339        Ok(content) => content,
340        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
341            msg_bail_anyhow!(Message::WatcherNotRunningPidNotFound);
342        }
343        Err(e) => return Err(e.into()),
344    };
345    let pid: u32 = pid_str.trim().parse().map_err(|_| msg_error_anyhow!(Message::InvalidPidFileContent))?;
346
347    // Attempt to terminate the process
348    let killed = kill_process(pid)?;
349
350    // Clean up the PID file regardless of whether the process was found
351    // This prevents stale PID files from interfering with future operations
352    if let Err(e) = std::fs::remove_file(&pid_path)
353        && e.kind() != std::io::ErrorKind::NotFound
354    {
355        return Err(e.into());
356    }
357
358    if killed {
359        msg_info!(Message::WatcherStopped(pid));
360    } else {
361        // The process was already gone; removing the stale PID file is all
362        // that stopping requires, so this is a success, not an error.
363        msg_info!(Message::WatcherNotRunning);
364    }
365    Ok(())
366}
367
368/// Terminates the process via `TerminateProcess` - Windows has no
369/// SIGTERM equivalent, so forceful is the reliable option. Returns
370/// `Ok(false)` when the process does not exist.
371#[cfg(windows)]
372fn kill_process(pid: u32) -> Result<bool> {
373    use winapi::um::errhandlingapi::GetLastError;
374    use winapi::um::handleapi::CloseHandle;
375    use winapi::um::processthreadsapi::{OpenProcess, TerminateProcess};
376    use winapi::um::winnt::PROCESS_TERMINATE;
377
378    unsafe {
379        // Open a handle to the target process with termination rights
380        let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
381        if handle.is_null() {
382            let error = GetLastError();
383            if error == 87 {
384                // ERROR_INVALID_PARAMETER - process doesn't exist
385                return Ok(false);
386            }
387            msg_bail_anyhow!(Message::FailedToOpenProcess(error));
388        }
389
390        // Attempt to terminate the process
391        let result = TerminateProcess(handle, 0);
392
393        // Always close the handle to prevent resource leaks
394        CloseHandle(handle);
395
396        if result == 0 {
397            // Termination failed - get error details
398            let error = GetLastError();
399            msg_bail_anyhow!(Message::FailedToTerminateProcess(error));
400        } else {
401            // Give the process time to actually terminate
402            std::thread::sleep(Duration::from_millis(100));
403            Ok(true)
404        }
405    }
406}
407
408/// SIGTERM first, up to a second of grace, then SIGKILL; uses `ps` and
409/// `kill` rather than raw syscalls. Returns `Ok(false)` when the process
410/// does not exist.
411#[cfg(unix)]
412fn kill_process(pid: u32) -> Result<bool> {
413    use std::process::Command;
414
415    // Check if process exists using ps
416    let output = Command::new("ps").arg("-p").arg(pid.to_string()).output()?;
417
418    if !output.status.success() {
419        // Process doesn't exist
420        return Ok(false);
421    }
422
423    // Send SIGTERM for graceful shutdown
424    Command::new("kill").arg("-TERM").arg(pid.to_string()).output()?;
425
426    // Give the process time to terminate gracefully
427    for _ in 0..10 {
428        std::thread::sleep(Duration::from_millis(100));
429
430        // Check if process still exists
431        let check = Command::new("ps").arg("-p").arg(pid.to_string()).output()?;
432
433        if !check.status.success() {
434            // Process terminated gracefully
435            return Ok(true);
436        }
437    }
438
439    // Process didn't terminate gracefully, force kill
440    Command::new("kill").arg("-9").arg(pid.to_string()).output()?;
441
442    // Give a brief moment for forced termination
443    std::thread::sleep(Duration::from_millis(100));
444    Ok(true)
445}
446
447#[cfg(not(any(unix, windows)))]
448fn kill_process(_pid: u32) -> Result<bool> {
449    msg_bail_anyhow!(Message::ProcessTerminationNotSupported);
450}