service_manager/
openrc.rs1use 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
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 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 rc_update("add", &script_name, [OsStr::new("default")])?;
80 }
81
82 Ok(())
83 }
84
85 fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
86 let _ = rc_update(
88 "del",
89 &ctx.label.to_script_name(),
90 [OsStr::new("default")],
91 );
92
93 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}