service_manager/
rcd.rs

1use super::{
2    utils, ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx, ServiceStopCtx,
3    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        let service = ctx.label.to_script_name();
51        let script = match ctx.contents {
52            Some(contents) => contents,
53            _ => make_script(&service, &service, ctx.program.as_os_str(), ctx.args),
54        };
55
56        utils::write_file(
57            &rc_d_script_path(&service),
58            script.as_bytes(),
59            SCRIPT_FILE_PERMISSIONS,
60        )?;
61
62        if ctx.autostart {
63            rc_d_script("enable", &service, true)?;
64        }
65
66        Ok(())
67    }
68
69    fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
70        let service = ctx.label.to_script_name();
71
72        // Remove the service from rc.conf
73        rc_d_script("delete", &service, true)?;
74
75        // Delete the actual service file
76        std::fs::remove_file(rc_d_script_path(&service))
77    }
78
79    fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
80        let service = ctx.label.to_script_name();
81        rc_d_script("start", &service, true)?;
82        Ok(())
83    }
84
85    fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
86        let service = ctx.label.to_script_name();
87        rc_d_script("stop", &service, true)?;
88        Ok(())
89    }
90
91    fn level(&self) -> ServiceLevel {
92        ServiceLevel::System
93    }
94
95    fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
96        match level {
97            ServiceLevel::System => Ok(()),
98            ServiceLevel::User => Err(io::Error::new(
99                io::ErrorKind::Unsupported,
100                "rc.d does not support user-level services",
101            )),
102        }
103    }
104
105    fn status(&self, ctx: crate::ServiceStatusCtx) -> io::Result<crate::ServiceStatus> {
106        let service = ctx.label.to_script_name();
107        let status = rc_d_script("status", &service, false)?;
108        match status.code() {
109            Some(0) => Ok(crate::ServiceStatus::Running),
110            Some(3) => Ok(crate::ServiceStatus::Stopped(None)),
111            Some(1) => Ok(crate::ServiceStatus::NotInstalled),
112            _ => {
113                let code = status.code().unwrap_or(-1);
114                let msg = format!("Failed to get status of {service}, exit code: {code}");
115                Err(io::Error::new(io::ErrorKind::Other, msg))
116            }
117        }
118    }
119}
120
121#[inline]
122fn rc_d_script_path(name: &str) -> PathBuf {
123    service_dir_path().join(name)
124}
125
126#[inline]
127fn service_dir_path() -> PathBuf {
128    PathBuf::from("/usr/local/etc/rc.d")
129}
130
131fn rc_d_script(cmd: &str, service: &str, wrap: bool) -> io::Result<ExitStatus> {
132    // NOTE: We MUST mark stdout/stderr as null, otherwise this hangs. Attempting to use output()
133    //       does not work. The alternative is to spawn threads to read the stdout and stderr,
134    //       but that seems overkill for the purpose of displaying an error message.
135    let status = Command::new(SERVICE)
136        .stdin(Stdio::null())
137        .stdout(Stdio::null())
138        .stderr(Stdio::null())
139        .arg(service)
140        .arg(cmd)
141        .status()?;
142    if wrap {
143        if status.success() {
144            Ok(status)
145        } else {
146            let msg = format!("Failed to {cmd} {service}");
147            Err(io::Error::new(io::ErrorKind::Other, msg))
148        }
149    } else {
150        Ok(status)
151    }
152}
153
154fn make_script(description: &str, provide: &str, program: &OsStr, args: Vec<OsString>) -> String {
155    let name = provide.replace('-', "_");
156    let program = program.to_string_lossy();
157    let args = args
158        .into_iter()
159        .map(|a| a.to_string_lossy().to_string())
160        .collect::<Vec<String>>()
161        .join(" ");
162    format!(
163        r#"
164#!/bin/sh
165#
166# PROVIDE: {provide}
167# REQUIRE: LOGIN FILESYSTEMS
168# KEYWORD: shutdown
169
170. /etc/rc.subr
171
172name="{name}"
173desc="{description}"
174rcvar="{name}_enable"
175
176load_rc_config ${{name}}
177
178: ${{{name}_options="{args}"}}
179
180pidfile="/var/run/{name}.pid"
181procname="{program}"
182command="/usr/sbin/daemon"
183command_args="-c -S -T ${{name}} -p ${{pidfile}} ${{procname}} ${{{name}_options}}"
184
185run_rc_command "$1"
186    "#
187    )
188    .trim()
189    .to_string()
190}