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