summaryrefslogtreecommitdiff
path: root/src/daemon.rs
blob: 8bf8adb73e8c6de455e4690fcd95145d26c95b4a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
use log::{debug, error, info};
use nix::sys::signal::kill;
use nix::sys::signal::Signal;
use nix::unistd::Pid;
use std::ffi::OsStr;
use std::fs::read;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::thread::sleep;
use std::time::{Duration, Instant};
use tempfile::NamedTempFile;

/// Possible errors from starting and stopping daemons.
#[derive(Debug, thiserror::Error)]
pub enum DaemonError {
    /// The daemon took too long to start. The timeout can be
    /// configured with [DaemonManager::timeout].
    #[error("daemon took longer than {0} ms to start: {1}\n{2}")]
    Timeout(u128, String, String),

    /// Something went wrong, when handling temporary files.
    #[error(transparent)]
    TempFile(#[from] std::io::Error),

    /// Something went wrong read standard output of daemon.
    #[error("failed to read daemon stdout: {0}")]
    Stdout(std::io::Error),

    /// Something went wrong read error output of daemon.
    #[error("failed to read daemon stderr: {0}")]
    Stderr(std::io::Error),

    /// Failed to kill a daemon.
    #[error("failed to kill process {0}: {1}")]
    Kill(i32, nix::Error),
}

/// Manage daemons.
///
/// A daemon is a process running in the background, doing useful
/// things. For Obnam benchmarks, it's the Obnam server, but this is a
/// generic manager. This version requires the `daemonize` helper
/// program to be available on $PATH.
pub struct DaemonManager {
    timeout: Duration,
}

impl Default for DaemonManager {
    fn default() -> Self {
        Self {
            timeout: Duration::from_millis(1000),
        }
    }
}

impl DaemonManager {
    /// Create a new manager instance, with default settings.
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the timeout for waiting on a daemon to start, in
    /// milliseconds.
    pub fn timeout(mut self, millis: u64) -> Self {
        self.timeout = Duration::from_millis(millis);
        self
    }

    /// Start a daemon.
    ///
    /// The daemon is considered started if its process id (PID) is
    /// known. The daemon may take longer to actually be in a useful
    /// state, and it may fail after the PID is known, for example if
    /// it reads a configuration file and that has errors. This
    /// function won't wait for that to happen: it only cares about
    /// the PID.
    pub fn start(
        &self,
        argv: &[&OsStr],
        stdout: &Path,
        stderr: &Path,
    ) -> Result<Daemon, DaemonError> {
        info!("start daemon: {:?}", argv);
        let pid = NamedTempFile::new()?;
        let output = Command::new("daemonize")
            .args(&[
                "-c",
                "/",
                "-e",
                &stderr.display().to_string(),
                "-o",
                &stdout.display().to_string(),
                "-p",
                &pid.path().display().to_string(),
            ])
            .args(argv)
            .output()
            .unwrap();
        if output.status.code() != Some(0) {
            eprintln!("{}", String::from_utf8_lossy(&output.stdout));
            eprintln!("{}", String::from_utf8_lossy(&output.stderr));
            std::process::exit(1);
        }

        debug!("waiting for daemon to write PID file");
        let time = Instant::now();
        while time.elapsed() < self.timeout {
            // Do we have the pid file?
            if let Ok(pid) = std::fs::read(pid.path()) {
                // Parse it as a string. We don't mind if it's not purely UTF8.
                let pid = String::from_utf8_lossy(&pid).into_owned();
                // Strip newline, if any.
                if let Some(pid) = pid.strip_suffix('\n') {
                    // Parse as an integer, if possible.
                    if let Ok(pid) = pid.parse() {
                        // We have a pid, stop waiting.
                        info!("got pid for daemon: pid");
                        return Ok(Daemon::new(pid, stdout, stderr));
                    }
                }
                sleep_briefly();
            } else {
                sleep_briefly();
            }
        }

        error!(
            "no PID file within {} ms, giving up",
            self.timeout.as_millis()
        );
        let mut cmd = String::new();
        for arg in argv {
            if !cmd.is_empty() {
                cmd.push(' ');
            }
            cmd.push_str(
                &String::from_utf8_lossy(arg.as_bytes())
                    .to_owned()
                    .to_string(),
            );
        }
        let err = read(&stderr).map_err(DaemonError::Stderr)?;
        let err = String::from_utf8_lossy(&err).into_owned();
        Err(DaemonError::Timeout(self.timeout.as_millis(), cmd, err))
    }
}

/// A running daemon.
///
/// The daemon process is killed, when the `Daemon` struct is dropped.
#[derive(Debug)]
pub struct Daemon {
    pid: Option<i32>,
    stdout: PathBuf,
    stderr: PathBuf,
}

impl Daemon {
    fn new(pid: i32, stdout: &Path, stderr: &Path) -> Self {
        info!("started daemon with PID {}", pid);
        Self {
            pid: Some(pid),
            stdout: stdout.to_path_buf(),
            stderr: stderr.to_path_buf(),
        }
    }

    /// Explicitly stop a daemon.
    ///
    /// Calling this function is only useful if you want to handle
    /// errors. It can only be called once.
    pub fn stop(&mut self) -> Result<(), DaemonError> {
        if let Some(raw_pid) = self.pid.take() {
            info!("stopping daemon with PID {}", raw_pid);
            let pid = Pid::from_raw(raw_pid);
            kill(pid, Some(Signal::SIGKILL)).map_err(|e| DaemonError::Kill(raw_pid, e))?;
        }
        Ok(())
    }

    /// Return what the daemon has written to its stderr so far.
    pub fn stdout(&self) -> Result<Vec<u8>, DaemonError> {
        std::fs::read(&self.stdout).map_err(DaemonError::Stdout)
    }

    /// Return what the daemon has written to its stderr so far.
    pub fn stderr(&self) -> Result<Vec<u8>, DaemonError> {
        std::fs::read(&self.stderr).map_err(DaemonError::Stderr)
    }
}

impl Drop for Daemon {
    fn drop(&mut self) {
        if self.stop().is_err() {
            // Do nothing.
        }
    }
}

fn sleep_briefly() {
    sleep(Duration::from_millis(100));
}