daemon_forge 0.1.0

A robust, cross-platform library for daemonizing Rust applications.
Documentation

Crates.io Documentation License

DaemonForge

DaemonForge is a cross-platform library for creating system daemons (background services) in Rust. It abstracts away the low-level complexities of operating system process management, providing a safe, idiomatic, and ergonomic builder API.

This crate is suitable for learning and experimentation, but not recommended for serious or production projects.

Key Features

  • True Cross-Platform Support:
    • Unix/Linux (Hybrid Mode):
      • Systemd Native: Automatically detects if running under Systemd (NOTIFY_SOCKET). If detected, it stays in the foreground and sends READY=1 notifications via sd-notify.
      • Legacy Daemonization: If run manually, it falls back to the canonical double-fork routine (setsid, umask, standard IO redirection) to run in the background.
    • Windows: Uses native "Detached Processes" and manages creation flags for true background execution without a console window (NOT a Windows Service).
  • Locking Mechanism:
    • Automatically prevents multiple instances of the same service from running simultaneously.
    • Utilizes flock (Unix) and Global Named Mutexes (Windows) for reliable exclusion.
  • Security First:
    • Secure environment variable clearing.
    • Support for privilege dropping (User/Group switching) and chroot jail on Unix systems.
  • Other features:
    • Panic Capture: Redirects stdout/stderr to log files, ensuring that panics and crashes are recorded instead of being lost.

Usage Examples

Linux/Unix Example (Systemd & Manual Compatible)

The library now uses an "Inversion of Control" pattern. You pass your main loop closure to privileged_action.

Note: For proper Systemd support, your loop must handle termination signals (like SIGTERM) to exit cleanly.

use daemon_forge::ForgeDaemon;
use std::fs::OpenOptions;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;
use signal_hook::consts::signal::*;
use signal_hook::flag;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let pwd = std::env::current_dir().unwrap();
    let log_path = pwd.join("service.log");
    let err_path = pwd.join("service.err");
    let pid_path = pwd.join("service.pid");

    // Open logs in append mode
    let stdout_file = OpenOptions::new().create(true).append(true).open(&log_path)?;
    let stderr_file = OpenOptions::new().create(true).append(true).open(&err_path)?;

    let daemon = ForgeDaemon::new()
        .name("my_service")
        .pid_file(&pid_path)
        .working_directory(&pwd)
        .stdout(stdout_file)
        .stderr(stderr_file)
        .privileged_action(move || {
            // --- MAIN DAEMON LOGIC ---
            
            // 1. Setup Signal Handling
            let term = Arc::new(AtomicBool::new(false));
            flag::register(SIGTERM, Arc::clone(&term))?; // Systemd Stop
            flag::register(SIGINT, Arc::clone(&term))?;  // Manual Ctrl+C

            println!("Service started. PID: {}", std::process::id());

            // 2. Main Loop
            while !term.load(Ordering::Relaxed) {
                // Your logic here...
                println!("Working...");
                
                // Reactive sleep (check for stop signal frequently)
                for _ in 0..10 {
                    if term.load(Ordering::Relaxed) { break; }
                    thread::sleep(Duration::from_millis(100));
                }
            }

            println!("Shutdown signal received.");
            Ok(())
        });

    // Automatically decides between Systemd (foreground) or Fork (background)
    daemon_forge::unix::start(daemon)?;

    Ok(())
}

Windows Example

On Windows, it is highly recommended to set a .name() for your daemon. This creates a global mutex to ensure uniqueness.

use daemon_forge::ForgeDaemon;
use std::fs::OpenOptions;
use std::env;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let pwd = env::current_dir().unwrap();
    let log_path = pwd.join("log.log");
    let err_path = pwd.join("error.err");
    let pid_path = pwd.join("pid.pid");

    // (Optional) We open them in append mode so we dont erase the history
    let stdout_file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(&log_path)
        .expect("Couldn't open stdout");

    let stderr_file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(&err_path)
        .expect("Couldn't open stderr");

    let daemon = ForgeDaemon::new()
        .name("MyUniqueService") // Creates "Global\DaemonForge_MyUniqueService" Mutex
        .pid_file(&pid_path)
        .working_directory(&pwd)
        .stdout(stdout_file)
        .stderr(stderr_file)
        .privileged_action(|| {
            // Your Windows Service logic here
            println!("I am running in the background on Windows!");
            loop {
                std::thread::sleep(std::time::Duration::from_secs(5));
            }
            Ok(())
        });

    // Launches the detached process
    daemon.start();

    println!("Launcher finished. Service is running in background.");
    Ok(())
}

Systemd Integration (Linux)

To use the Systemd features, create a service file at /etc/systemd/system/my_service.service.

Crucial Settings:

  1. Type=notify: Tells Systemd to wait for the READY=1 signal sent by DaemonForge.
  2. ExecStart: Must be the absolute path to your binary.
[Unit]
Description=My Rust Daemon

[Service]
# Important: This allows DaemonForge to handshake with Systemd
Type=notify

# Replace with your actual path
ExecStart=/path/to/your/release/binary

# Logs will appear in `journalctl -u my_service`
StandardOutput=journal
StandardError=journal

Restart=on-failure

[Install]
WantedBy=multi-user.target

Advanced Configuration

You can execute a privileged_action before the process fully daemonizes. This is useful for tasks that require higher permissions (like binding to port 80) or for checking environment prerequisites.

# use daemon_forge::ForgeDaemon;
ForgeDaemon::new()
    .clear_env(true) // Clears inherited environment variables
    .env("API_KEY", "secret_value") // Sets explicit variables
    .privileged_action(|| {
        println!("Initializing resources...");
        // If this returns Err, the daemon will abort startup.
        Ok("Initialization Done")
    });