service_manager/
rcd.rs

1use super::{
2    utils, RestartPolicy, ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx,
3    ServiceStopCtx, ServiceUninstallCtx,
4};
5use std::{
6    ffi::{OsStr, OsString},
7    io,
8    path::PathBuf,
9    process::{Command, ExitStatus, Stdio},
10};
11
12static SERVICE: &str = "service";
13
14// NOTE: On FreeBSD, /usr/local/etc/rc.d/{script} has permissions of rwxr-xr-x (755)
15const SCRIPT_FILE_PERMISSIONS: u32 = 0o755;
16
17/// Configuration settings tied to rc.d services
18#[derive(Clone, Debug, Default, PartialEq, Eq)]
19pub struct RcdConfig {}
20
21/// Implementation of [`ServiceManager`] for FreeBSD's [rc.d](https://en.wikipedia.org/wiki/Init#Research_Unix-style/BSD-style)
22#[derive(Clone, Debug, Default, PartialEq, Eq)]
23pub struct RcdServiceManager {
24    /// Configuration settings tied to rc.d services
25    pub config: RcdConfig,
26}
27
28impl RcdServiceManager {
29    /// Creates a new manager instance working with system services
30    pub fn system() -> Self {
31        Self::default()
32    }
33
34    /// Update manager to use the specified config
35    pub fn with_config(self, config: RcdConfig) -> Self {
36        Self { config }
37    }
38}
39
40impl ServiceManager for RcdServiceManager {
41    fn available(&self) -> io::Result<bool> {
42        match std::fs::metadata(service_dir_path()) {
43            Ok(_) => Ok(true),
44            Err(x) if x.kind() == io::ErrorKind::NotFound => Ok(false),
45            Err(x) => Err(x),
46        }
47    }
48
49    fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()> {
50        // rc.d doesn't support restart policies in the basic implementation.
51        // Log a warning if user requested anything other than `Never`.
52        match ctx.restart_policy {
53            RestartPolicy::Never => {
54                // This is fine, rc.d services don't restart by default
55            }
56            RestartPolicy::Always { .. } | RestartPolicy::OnFailure { .. } | RestartPolicy::OnSuccess { .. } => {
57                log::warn!(
58                    "rc.d does not support automatic restart policies; service '{}' will not restart automatically",
59                    ctx.label.to_script_name()
60                );
61            }
62        }
63
64        let service = ctx.label.to_script_name();
65        let script = match ctx.contents {
66            Some(contents) => contents,
67            _ => make_script(&service, &service, ctx.program.as_os_str(), ctx.args),
68        };
69
70        utils::write_file(
71            &rc_d_script_path(&service),
72            script.as_bytes(),
73            SCRIPT_FILE_PERMISSIONS,
74        )?;
75
76        if ctx.autostart {
77            rc_d_script("enable", &service, true)?;
78        }
79
80        Ok(())
81    }
82
83    fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
84        let service = ctx.label.to_script_name();
85
86        // Remove the service from rc.conf
87        rc_d_script("delete", &service, true)?;
88
89        // Delete the actual service file
90        std::fs::remove_file(rc_d_script_path(&service))
91    }
92
93    fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
94        let service = ctx.label.to_script_name();
95        rc_d_script("start", &service, true)?;
96        Ok(())
97    }
98
99    fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
100        let service = ctx.label.to_script_name();
101        rc_d_script("stop", &service, true)?;
102        Ok(())
103    }
104
105    fn level(&self) -> ServiceLevel {
106        ServiceLevel::System
107    }
108
109    fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
110        match level {
111            ServiceLevel::System => Ok(()),
112            ServiceLevel::User => Err(io::Error::new(
113                io::ErrorKind::Unsupported,
114                "rc.d does not support user-level services",
115            )),
116        }
117    }
118
119    fn status(&self, ctx: crate::ServiceStatusCtx) -> io::Result<crate::ServiceStatus> {
120        let service = ctx.label.to_script_name();
121        let status = rc_d_script("status", &service, false)?;
122        match status.code() {
123            Some(0) => Ok(crate::ServiceStatus::Running),
124            Some(3) => Ok(crate::ServiceStatus::Stopped(None)),
125            Some(1) => Ok(crate::ServiceStatus::NotInstalled),
126            _ => {
127                let code = status.code().unwrap_or(-1);
128                let msg = format!("Failed to get status of {service}, exit code: {code}");
129                Err(io::Error::new(io::ErrorKind::Other, msg))
130            }
131        }
132    }
133}
134
135#[inline]
136fn rc_d_script_path(name: &str) -> PathBuf {
137    service_dir_path().join(name)
138}
139
140#[inline]
141fn service_dir_path() -> PathBuf {
142    PathBuf::from("/usr/local/etc/rc.d")
143}
144
145fn rc_d_script(cmd: &str, service: &str, wrap: bool) -> io::Result<ExitStatus> {
146    // NOTE: We MUST mark stdout/stderr as null, otherwise this hangs. Attempting to use output()
147    //       does not work. The alternative is to spawn threads to read the stdout and stderr,
148    //       but that seems overkill for the purpose of displaying an error message.
149    let status = Command::new(SERVICE)
150        .stdin(Stdio::null())
151        .stdout(Stdio::null())
152        .stderr(Stdio::null())
153        .arg(service)
154        .arg(cmd)
155        .status()?;
156    if wrap {
157        if status.success() {
158            Ok(status)
159        } else {
160            let msg = format!("Failed to {cmd} {service}");
161            Err(io::Error::new(io::ErrorKind::Other, msg))
162        }
163    } else {
164        Ok(status)
165    }
166}
167
168fn make_script(description: &str, provide: &str, program: &OsStr, args: Vec<OsString>) -> String {
169    let name = provide.replace('-', "_");
170    let program = program.to_string_lossy();
171    let args = args
172        .into_iter()
173        .map(|a| a.to_string_lossy().to_string())
174        .collect::<Vec<String>>()
175        .join(" ");
176    format!(
177        r#"
178#!/bin/sh
179#
180# PROVIDE: {provide}
181# REQUIRE: LOGIN FILESYSTEMS
182# KEYWORD: shutdown
183
184. /etc/rc.subr
185
186name="{name}"
187desc="{description}"
188rcvar="{name}_enable"
189
190load_rc_config ${{name}}
191
192: ${{{name}_options="{args}"}}
193
194pidfile="/var/run/{name}.pid"
195procname="{program}"
196command="/usr/sbin/daemon"
197command_args="-c -S -T ${{name}} -p ${{pidfile}} ${{procname}} ${{{name}_options}}"
198
199run_rc_command "$1"
200    "#
201    )
202    .trim()
203    .to_string()
204}