service_manager/
openrc.rs

1use crate::utils::wrap_output;
2
3use super::{
4    utils, ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx, ServiceStopCtx,
5    ServiceUninstallCtx,
6};
7use std::{
8    ffi::{OsStr, OsString},
9    io,
10    path::PathBuf,
11    process::{Command, Output, Stdio},
12};
13
14static RC_SERVICE: &str = "rc-service";
15static RC_UPDATE: &str = "rc-update";
16
17// NOTE: On Alpine Linux, /etc/init.d/{script} has permissions of rwxr-xr-x (755)
18const SCRIPT_FILE_PERMISSIONS: u32 = 0o755;
19
20/// Configuration settings tied to OpenRC services
21#[derive(Clone, Debug, Default, PartialEq, Eq)]
22pub struct OpenRcConfig {}
23
24/// Implementation of [`ServiceManager`] for Linux's [OpenRC](https://en.wikipedia.org/wiki/OpenRC)
25#[derive(Clone, Debug, Default, PartialEq, Eq)]
26pub struct OpenRcServiceManager {
27    /// Configuration settings tied to OpenRC services
28    pub config: OpenRcConfig,
29}
30
31impl OpenRcServiceManager {
32    /// Creates a new manager instance working with system services
33    pub fn system() -> Self {
34        Self::default()
35    }
36
37    /// Update manager to use the specified config
38    pub fn with_config(self, config: OpenRcConfig) -> Self {
39        Self { config }
40    }
41}
42
43impl ServiceManager for OpenRcServiceManager {
44    fn available(&self) -> io::Result<bool> {
45        match which::which(RC_SERVICE) {
46            Ok(_) => Ok(true),
47            Err(which::Error::CannotFindBinaryPath) => Ok(false),
48            Err(x) => Err(io::Error::new(io::ErrorKind::Other, x)),
49        }
50    }
51
52    fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()> {
53        let dir_path = service_dir_path();
54        std::fs::create_dir_all(&dir_path)?;
55
56        let script_name = ctx.label.to_script_name();
57        let script_path = dir_path.join(&script_name);
58
59        let script = match ctx.contents {
60            Some(contents) => contents,
61            _ => make_script(
62                &script_name,
63                &script_name,
64                ctx.program.as_os_str(),
65                ctx.args,
66            ),
67        };
68
69        utils::write_file(
70            script_path.as_path(),
71            script.as_bytes(),
72            SCRIPT_FILE_PERMISSIONS,
73        )?;
74
75        if ctx.autostart {
76            // Add with default run level explicitly defined to prevent weird systems
77            // like alpine's docker container with openrc from setting a different
78            // run level than default
79            rc_update("add", &script_name, [OsStr::new("default")])?;
80        }
81
82        Ok(())
83    }
84
85    fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
86        // If the script is configured to run at boot, remove it
87        let _ = rc_update(
88            "del",
89            &ctx.label.to_script_name(),
90            [OsStr::new("default")],
91        );
92
93        // Uninstall service by removing the script
94        std::fs::remove_file(service_dir_path().join(&ctx.label.to_script_name()))
95    }
96
97    fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
98        wrap_output(rc_service("start", &ctx.label.to_script_name(), [])?)?;
99        Ok(())
100    }
101
102    fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
103        wrap_output(rc_service("stop", &ctx.label.to_script_name(), [])?)?;
104        Ok(())
105    }
106
107    fn level(&self) -> ServiceLevel {
108        ServiceLevel::System
109    }
110
111    fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
112        match level {
113            ServiceLevel::System => Ok(()),
114            ServiceLevel::User => Err(io::Error::new(
115                io::ErrorKind::Unsupported,
116                "OpenRC does not support user-level services",
117            )),
118        }
119    }
120
121    fn status(&self, ctx: crate::ServiceStatusCtx) -> io::Result<crate::ServiceStatus> {
122        let output = rc_service("status", &ctx.label.to_script_name(), [])?;
123        match output.status.code() {
124            Some(1) => {
125                let mut stdio = String::from_utf8_lossy(&output.stderr);
126                if stdio.trim().is_empty() {
127                    stdio = String::from_utf8_lossy(&output.stdout);
128                }
129                if stdio.contains("does not exist") {
130                    Ok(crate::ServiceStatus::NotInstalled)
131                } else {
132                    Err(io::Error::new(
133                        io::ErrorKind::Other,
134                        format!(
135                            "Failed to get status of service {}: {}",
136                            ctx.label.to_script_name(),
137                            stdio
138                        ),
139                    ))
140                }
141            }
142            Some(0) => Ok(crate::ServiceStatus::Running),
143            Some(3) => Ok(crate::ServiceStatus::Stopped(None)),
144            _ => Err(io::Error::new(
145                io::ErrorKind::Other,
146                format!(
147                    "Failed to get status of service {}: {}",
148                    ctx.label.to_script_name(),
149                    String::from_utf8_lossy(&output.stderr)
150                ),
151            )),
152        }
153    }
154}
155
156fn rc_service<'a>(
157    cmd: &str,
158    service: &str,
159    args: impl IntoIterator<Item = &'a OsStr>,
160) -> io::Result<Output> {
161    let mut command = Command::new(RC_SERVICE);
162    command
163        .stdin(Stdio::null())
164        .stdout(Stdio::piped())
165        .stderr(Stdio::piped())
166        .arg(service)
167        .arg(cmd);
168    for arg in args {
169        command.arg(arg);
170    }
171    command.output()
172}
173
174fn rc_update<'a>(
175    cmd: &str,
176    service: &str,
177    args: impl IntoIterator<Item = &'a OsStr>,
178) -> io::Result<()> {
179    let mut command = Command::new(RC_UPDATE);
180    command
181        .stdin(Stdio::null())
182        .stdout(Stdio::piped())
183        .stderr(Stdio::piped())
184        .arg(cmd)
185        .arg(service);
186
187    for arg in args {
188        command.arg(arg);
189    }
190
191    let output = command.output()?;
192
193    if output.status.success() {
194        Ok(())
195    } else {
196        let msg = String::from_utf8(output.stderr)
197            .ok()
198            .filter(|s| !s.trim().is_empty())
199            .or_else(|| {
200                String::from_utf8(output.stdout)
201                    .ok()
202                    .filter(|s| !s.trim().is_empty())
203            })
204            .unwrap_or_else(|| format!("Failed to {cmd} {service}"));
205
206        Err(io::Error::new(io::ErrorKind::Other, msg))
207    }
208}
209
210#[inline]
211fn service_dir_path() -> PathBuf {
212    PathBuf::from("/etc/init.d")
213}
214
215fn make_script(description: &str, provide: &str, program: &OsStr, args: Vec<OsString>) -> String {
216    let program = program.to_string_lossy();
217    let args = args
218        .into_iter()
219        .map(|a| a.to_string_lossy().to_string())
220        .collect::<Vec<String>>()
221        .join(" ");
222    format!(
223        r#"
224#!/sbin/openrc-run
225
226description="{description}"
227command="{program}"
228command_args="{args}"
229pidfile="/run/${{RC_SVCNAME}}.pid"
230command_background=true
231
232depend() {{
233    provide {provide}
234}}
235    "#
236    )
237    .trim()
238    .to_string()
239}