service_manager/
openrc.rs

1use crate::utils::wrap_output;
2
3use super::{
4    utils, RestartPolicy, ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx,
5    ServiceStopCtx, 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        // OpenRC doesn't support restart policies in the basic implementation.
54        // Log a warning if user requested anything other than `Never`.
55        match ctx.restart_policy {
56            RestartPolicy::Never => {
57                // This is fine, OpenRC services don't restart by default
58            }
59            RestartPolicy::Always { .. } | RestartPolicy::OnFailure { .. } | RestartPolicy::OnSuccess { .. } => {
60                log::warn!(
61                    "OpenRC does not support automatic restart policies; service '{}' will not restart automatically",
62                    ctx.label.to_script_name()
63                );
64            }
65        }
66
67        let dir_path = service_dir_path();
68        std::fs::create_dir_all(&dir_path)?;
69
70        let script_name = ctx.label.to_script_name();
71        let script_path = dir_path.join(&script_name);
72
73        let script = match ctx.contents {
74            Some(contents) => contents,
75            _ => make_script(
76                &script_name,
77                &script_name,
78                ctx.program.as_os_str(),
79                ctx.args,
80            ),
81        };
82
83        utils::write_file(
84            script_path.as_path(),
85            script.as_bytes(),
86            SCRIPT_FILE_PERMISSIONS,
87        )?;
88
89        if ctx.autostart {
90            // Add with default run level explicitly defined to prevent weird systems
91            // like alpine's docker container with openrc from setting a different
92            // run level than default
93            rc_update("add", &script_name, [OsStr::new("default")])?;
94        }
95
96        Ok(())
97    }
98
99    fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
100        // If the script is configured to run at boot, remove it
101        let _ = rc_update("del", &ctx.label.to_script_name(), [OsStr::new("default")]);
102
103        // Uninstall service by removing the script
104        std::fs::remove_file(service_dir_path().join(&ctx.label.to_script_name()))
105    }
106
107    fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
108        wrap_output(rc_service("start", &ctx.label.to_script_name(), [])?)?;
109        Ok(())
110    }
111
112    fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
113        wrap_output(rc_service("stop", &ctx.label.to_script_name(), [])?)?;
114        Ok(())
115    }
116
117    fn level(&self) -> ServiceLevel {
118        ServiceLevel::System
119    }
120
121    fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
122        match level {
123            ServiceLevel::System => Ok(()),
124            ServiceLevel::User => Err(io::Error::new(
125                io::ErrorKind::Unsupported,
126                "OpenRC does not support user-level services",
127            )),
128        }
129    }
130
131    fn status(&self, ctx: crate::ServiceStatusCtx) -> io::Result<crate::ServiceStatus> {
132        let output = rc_service("status", &ctx.label.to_script_name(), [])?;
133        match output.status.code() {
134            Some(1) => {
135                let mut stdio = String::from_utf8_lossy(&output.stderr);
136                if stdio.trim().is_empty() {
137                    stdio = String::from_utf8_lossy(&output.stdout);
138                }
139                if stdio.contains("does not exist") {
140                    Ok(crate::ServiceStatus::NotInstalled)
141                } else {
142                    Err(io::Error::new(
143                        io::ErrorKind::Other,
144                        format!(
145                            "Failed to get status of service {}: {}",
146                            ctx.label.to_script_name(),
147                            stdio
148                        ),
149                    ))
150                }
151            }
152            Some(0) => Ok(crate::ServiceStatus::Running),
153            Some(3) => Ok(crate::ServiceStatus::Stopped(None)),
154            _ => Err(io::Error::new(
155                io::ErrorKind::Other,
156                format!(
157                    "Failed to get status of service {}: {}",
158                    ctx.label.to_script_name(),
159                    String::from_utf8_lossy(&output.stderr)
160                ),
161            )),
162        }
163    }
164}
165
166fn rc_service<'a>(
167    cmd: &str,
168    service: &str,
169    args: impl IntoIterator<Item = &'a OsStr>,
170) -> io::Result<Output> {
171    let mut command = Command::new(RC_SERVICE);
172    command
173        .stdin(Stdio::null())
174        .stdout(Stdio::piped())
175        .stderr(Stdio::piped())
176        .arg(service)
177        .arg(cmd);
178    for arg in args {
179        command.arg(arg);
180    }
181    command.output()
182}
183
184fn rc_update<'a>(
185    cmd: &str,
186    service: &str,
187    args: impl IntoIterator<Item = &'a OsStr>,
188) -> io::Result<()> {
189    let mut command = Command::new(RC_UPDATE);
190    command
191        .stdin(Stdio::null())
192        .stdout(Stdio::piped())
193        .stderr(Stdio::piped())
194        .arg(cmd)
195        .arg(service);
196
197    for arg in args {
198        command.arg(arg);
199    }
200
201    let output = command.output()?;
202
203    if output.status.success() {
204        Ok(())
205    } else {
206        let msg = String::from_utf8(output.stderr)
207            .ok()
208            .filter(|s| !s.trim().is_empty())
209            .or_else(|| {
210                String::from_utf8(output.stdout)
211                    .ok()
212                    .filter(|s| !s.trim().is_empty())
213            })
214            .unwrap_or_else(|| format!("Failed to {cmd} {service}"));
215
216        Err(io::Error::new(io::ErrorKind::Other, msg))
217    }
218}
219
220#[inline]
221fn service_dir_path() -> PathBuf {
222    PathBuf::from("/etc/init.d")
223}
224
225fn make_script(description: &str, provide: &str, program: &OsStr, args: Vec<OsString>) -> String {
226    let program = program.to_string_lossy();
227    let args = args
228        .into_iter()
229        .map(|a| a.to_string_lossy().to_string())
230        .collect::<Vec<String>>()
231        .join(" ");
232    format!(
233        r#"
234#!/sbin/openrc-run
235
236description="{description}"
237command="{program}"
238command_args="{args}"
239pidfile="/run/${{RC_SVCNAME}}.pid"
240command_background=true
241
242depend() {{
243    provide {provide}
244}}
245    "#
246    )
247    .trim()
248    .to_string()
249}