service_manager/
openrc.rs1use 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
17const SCRIPT_FILE_PERMISSIONS: u32 = 0o755;
19
20#[derive(Clone, Debug, Default, PartialEq, Eq)]
22pub struct OpenRcConfig {}
23
24#[derive(Clone, Debug, Default, PartialEq, Eq)]
26pub struct OpenRcServiceManager {
27 pub config: OpenRcConfig,
29}
30
31impl OpenRcServiceManager {
32 pub fn system() -> Self {
34 Self::default()
35 }
36
37 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 match ctx.restart_policy {
56 RestartPolicy::Never => {
57 }
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 rc_update("add", &script_name, [OsStr::new("default")])?;
94 }
95
96 Ok(())
97 }
98
99 fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
100 let _ = rc_update("del", &ctx.label.to_script_name(), [OsStr::new("default")]);
102
103 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}