1use super::{
2 utils, RestartPolicy, ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx,
3 ServiceStopCtx, 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
14const SCRIPT_FILE_PERMISSIONS: u32 = 0o755;
16
17#[derive(Clone, Debug, Default, PartialEq, Eq)]
19pub struct RcdConfig {}
20
21#[derive(Clone, Debug, Default, PartialEq, Eq)]
23pub struct RcdServiceManager {
24 pub config: RcdConfig,
26}
27
28impl RcdServiceManager {
29 pub fn system() -> Self {
31 Self::default()
32 }
33
34 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 match ctx.restart_policy {
53 RestartPolicy::Never => {
54 }
56 RestartPolicy::Always { .. } | RestartPolicy::OnFailure { .. } | RestartPolicy::OnSuccess { .. } => {
57 log::warn!(
58 "rc.d does not support automatic restart policies; service '{}' will not restart automatically",
59 ctx.label.to_script_name()
60 );
61 }
62 }
63
64 let service = ctx.label.to_script_name();
65 let script = match ctx.contents {
66 Some(contents) => contents,
67 _ => make_script(&service, &service, ctx.program.as_os_str(), ctx.args),
68 };
69
70 utils::write_file(
71 &rc_d_script_path(&service),
72 script.as_bytes(),
73 SCRIPT_FILE_PERMISSIONS,
74 )?;
75
76 if ctx.autostart {
77 rc_d_script("enable", &service, true)?;
78 }
79
80 Ok(())
81 }
82
83 fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
84 let service = ctx.label.to_script_name();
85
86 rc_d_script("delete", &service, true)?;
88
89 std::fs::remove_file(rc_d_script_path(&service))
91 }
92
93 fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
94 let service = ctx.label.to_script_name();
95 rc_d_script("start", &service, true)?;
96 Ok(())
97 }
98
99 fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
100 let service = ctx.label.to_script_name();
101 rc_d_script("stop", &service, true)?;
102 Ok(())
103 }
104
105 fn level(&self) -> ServiceLevel {
106 ServiceLevel::System
107 }
108
109 fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
110 match level {
111 ServiceLevel::System => Ok(()),
112 ServiceLevel::User => Err(io::Error::new(
113 io::ErrorKind::Unsupported,
114 "rc.d does not support user-level services",
115 )),
116 }
117 }
118
119 fn status(&self, ctx: crate::ServiceStatusCtx) -> io::Result<crate::ServiceStatus> {
120 let service = ctx.label.to_script_name();
121 let status = rc_d_script("status", &service, false)?;
122 match status.code() {
123 Some(0) => Ok(crate::ServiceStatus::Running),
124 Some(3) => Ok(crate::ServiceStatus::Stopped(None)),
125 Some(1) => Ok(crate::ServiceStatus::NotInstalled),
126 _ => {
127 let code = status.code().unwrap_or(-1);
128 let msg = format!("Failed to get status of {service}, exit code: {code}");
129 Err(io::Error::new(io::ErrorKind::Other, msg))
130 }
131 }
132 }
133}
134
135#[inline]
136fn rc_d_script_path(name: &str) -> PathBuf {
137 service_dir_path().join(name)
138}
139
140#[inline]
141fn service_dir_path() -> PathBuf {
142 PathBuf::from("/usr/local/etc/rc.d")
143}
144
145fn rc_d_script(cmd: &str, service: &str, wrap: bool) -> io::Result<ExitStatus> {
146 let status = Command::new(SERVICE)
150 .stdin(Stdio::null())
151 .stdout(Stdio::null())
152 .stderr(Stdio::null())
153 .arg(service)
154 .arg(cmd)
155 .status()?;
156 if wrap {
157 if status.success() {
158 Ok(status)
159 } else {
160 let msg = format!("Failed to {cmd} {service}");
161 Err(io::Error::new(io::ErrorKind::Other, msg))
162 }
163 } else {
164 Ok(status)
165 }
166}
167
168fn make_script(description: &str, provide: &str, program: &OsStr, args: Vec<OsString>) -> String {
169 let name = provide.replace('-', "_");
170 let program = program.to_string_lossy();
171 let args = args
172 .into_iter()
173 .map(|a| a.to_string_lossy().to_string())
174 .collect::<Vec<String>>()
175 .join(" ");
176 format!(
177 r#"
178#!/bin/sh
179#
180# PROVIDE: {provide}
181# REQUIRE: LOGIN FILESYSTEMS
182# KEYWORD: shutdown
183
184. /etc/rc.subr
185
186name="{name}"
187desc="{description}"
188rcvar="{name}_enable"
189
190load_rc_config ${{name}}
191
192: ${{{name}_options="{args}"}}
193
194pidfile="/var/run/{name}.pid"
195procname="{program}"
196command="/usr/sbin/daemon"
197command_args="-c -S -T ${{name}} -p ${{pidfile}} ${{procname}} ${{{name}_options}}"
198
199run_rc_command "$1"
200 "#
201 )
202 .trim()
203 .to_string()
204}