Skip to content

Commit

Permalink
feat: support status fn for StatusManager (#27)
Browse files Browse the repository at this point in the history
* feat: support status command for winsw & sc
* feat: support status command for systemctl
* feat: support status command for launchd
* feat: support status command for openrc
* feat: support status command for rcd
* feat: add encoding util for different code page in windows
  • Loading branch information
greenhat616 authored Sep 17, 2024
1 parent d4e3336 commit 13dae5e
Show file tree
Hide file tree
Showing 12 changed files with 478 additions and 136 deletions.
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ repository = "https://github.com/chipsenkbeil/service-manager-rs"
readme = "README.md"
license = "MIT OR Apache-2.0"

[features]
default = ["encoding"]
encoding = [
"dep:encoding_rs",
"dep:encoding-utils",
] # probe OsStr encoding while parsing

[workspace]
members = ["system-tests"]

Expand All @@ -22,6 +29,8 @@ plist = "1.1"
serde = { version = "1", features = ["derive"], optional = true }
which = "4.0"
xml-rs = "0.8.19"
encoding_rs = { version = "0.8", optional = true }
encoding-utils = { version ="0.1", optional = true }

[dev-dependencies]
assert_fs = "1.0.13"
Expand Down
104 changes: 78 additions & 26 deletions src/launchd.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
use crate::utils::wrap_output;

use super::{
utils, ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx, ServiceStopCtx,
ServiceUninstallCtx,
};
use plist::{Dictionary, Value};
use std::{
borrow::Cow,
ffi::OsStr,
io,
path::PathBuf,
process::{Command, Stdio},
process::{Command, Output, Stdio},
};

static LAUNCHCTL: &str = "launchctl";
Expand Down Expand Up @@ -83,8 +86,8 @@ impl LaunchdServiceManager {
} else {
global_daemon_dir_path()
};
let plist_path = dir_path.join(format!("{}.plist", qualified_name));
plist_path

dir_path.join(format!("{}.plist", qualified_name))
}
}

Expand Down Expand Up @@ -128,7 +131,7 @@ impl ServiceManager for LaunchdServiceManager {
)?;

if ctx.autostart {
launchctl("load", plist_path.to_string_lossy().as_ref())?;
wrap_output(launchctl("load", plist_path.to_string_lossy().as_ref())?)?;
}

Ok(())
Expand All @@ -137,18 +140,20 @@ impl ServiceManager for LaunchdServiceManager {
fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
let plist_path = self.get_plist_path(ctx.label.to_qualified_name());

launchctl("unload", plist_path.to_string_lossy().as_ref())?;
wrap_output(launchctl("unload", plist_path.to_string_lossy().as_ref())?)?;
std::fs::remove_file(plist_path)
}

fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
let plist_path = self.get_plist_path(ctx.label.to_qualified_name());
launchctl("load", plist_path.to_string_lossy().as_ref())
wrap_output(launchctl("load", plist_path.to_string_lossy().as_ref())?)?;
Ok(())
}

fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
let plist_path = self.get_plist_path(ctx.label.to_qualified_name());
launchctl("unload", plist_path.to_string_lossy().as_ref())
wrap_output(launchctl("unload", plist_path.to_string_lossy().as_ref())?)?;
Ok(())
}

fn level(&self) -> ServiceLevel {
Expand All @@ -167,32 +172,79 @@ impl ServiceManager for LaunchdServiceManager {

Ok(())
}

fn status(&self, ctx: crate::ServiceStatusCtx) -> io::Result<crate::ServiceStatus> {
let mut service_name = ctx.label.to_qualified_name();
// Due to we could not get the status of a service via a service label, so we have to run this command twice
// in first time, if there is a service exists, the output will advice us a full service label with a prefix.
// Or it will return nothing, it means the service is not installed(not exists).
let mut out: Cow<str> = Cow::Borrowed("");
for i in 0..2 {
let output = launchctl("print", &service_name)?;
if !output.status.success() {
if output.status.code() == Some(64) {
// 64 is the exit code for a service not found
out = Cow::Owned(String::from_utf8_lossy(&output.stderr).to_string());
if out.trim().is_empty() {
out = Cow::Owned(String::from_utf8_lossy(&output.stdout).to_string());
}
if i == 0 {
let label = out.lines().find(|line| line.contains(&service_name));
match label {
Some(label) => {
service_name = label.trim().to_string();
continue;
}
None => return Ok(crate::ServiceStatus::NotInstalled),
}
} else {
// We have access to the full service label, so it impossible to get the failed status, or it must be input error.
return Err(io::Error::new(
io::ErrorKind::Other,
format!(
"Command failed with exit code {}: {}",
output.status.code().unwrap_or(-1),
out
),
));
}
} else {
return Err(io::Error::new(
io::ErrorKind::Other,
format!(
"Command failed with exit code {}: {}",
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stderr)
),
));
}
}
out = Cow::Owned(String::from_utf8_lossy(&output.stdout).to_string());
}
let lines = out
.lines()
.map(|s| s.trim())
.filter(|s| s.contains("state"))
.collect::<Vec<&str>>();
if lines
.into_iter()
.any(|s| !s.contains("not running") && s.contains("running"))
{
Ok(crate::ServiceStatus::Running)
} else {
Ok(crate::ServiceStatus::Stopped(None))
}
}
}

fn launchctl(cmd: &str, label: &str) -> io::Result<()> {
let output = Command::new(LAUNCHCTL)
fn launchctl(cmd: &str, label: &str) -> io::Result<Output> {
Command::new(LAUNCHCTL)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.arg(cmd)
.arg(label)
.output()?;

if output.status.success() {
Ok(())
} else {
let msg = String::from_utf8(output.stderr)
.ok()
.filter(|s| !s.trim().is_empty())
.or_else(|| {
String::from_utf8(output.stdout)
.ok()
.filter(|s| !s.trim().is_empty())
})
.unwrap_or_else(|| format!("Failed to {cmd} for {label}"));

Err(io::Error::new(io::ErrorKind::Other, msg))
}
.output()
}

#[inline]
Expand Down
20 changes: 20 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ pub trait ServiceManager {

/// Sets the target level for the manager
fn set_level(&mut self, level: ServiceLevel) -> io::Result<()>;

/// Return the service status info
fn status(&self, ctx: ServiceStatusCtx) -> io::Result<ServiceStatus>;
}

impl dyn ServiceManager {
Expand Down Expand Up @@ -108,6 +111,14 @@ pub enum ServiceLevel {
User,
}

/// Represents the status of a service
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum ServiceStatus {
NotInstalled,
Running,
Stopped(Option<String>), // Provide a reason if possible
}

/// Label describing the service (e.g. `org.example.my_application`
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ServiceLabel {
Expand Down Expand Up @@ -274,6 +285,15 @@ pub struct ServiceStopCtx {
pub label: ServiceLabel,
}

/// Context provided to the status function of [`ServiceManager`]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServiceStatusCtx {
/// Label associated with the service
///
/// E.g. `rocks.distant.manager`
pub label: ServiceLabel,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
69 changes: 52 additions & 17 deletions src/openrc.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::utils::wrap_output;

use super::{
utils, ServiceInstallCtx, ServiceLevel, ServiceManager, ServiceStartCtx, ServiceStopCtx,
ServiceUninstallCtx,
Expand All @@ -6,7 +8,7 @@ use std::{
ffi::{OsStr, OsString},
io,
path::PathBuf,
process::{Command, Stdio},
process::{Command, Output, Stdio},
};

static RC_SERVICE: &str = "rc-service";
Expand Down Expand Up @@ -89,11 +91,13 @@ impl ServiceManager for OpenRcServiceManager {
}

fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
rc_service("start", &ctx.label.to_script_name())
wrap_output(rc_service("start", &ctx.label.to_script_name(), [])?)?;
Ok(())
}

fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
rc_service("stop", &ctx.label.to_script_name())
wrap_output(rc_service("stop", &ctx.label.to_script_name(), [])?)?;
Ok(())
}

fn level(&self) -> ServiceLevel {
Expand All @@ -109,27 +113,58 @@ impl ServiceManager for OpenRcServiceManager {
)),
}
}

fn status(&self, ctx: crate::ServiceStatusCtx) -> io::Result<crate::ServiceStatus> {
let output = rc_service("status", &ctx.label.to_script_name(), [])?;
match output.status.code() {
Some(1) => {
let mut stdio = String::from_utf8_lossy(&output.stderr);
if stdio.trim().is_empty() {
stdio = String::from_utf8_lossy(&output.stdout);
}
if stdio.contains("does not exist") {
Ok(crate::ServiceStatus::NotInstalled)
} else {
Err(io::Error::new(
io::ErrorKind::Other,
format!(
"Failed to get status of service {}: {}",
ctx.label.to_script_name(),
stdio
),
))
}
}
Some(0) => Ok(crate::ServiceStatus::Running),
Some(3) => Ok(crate::ServiceStatus::Stopped(None)),
_ => Err(io::Error::new(
io::ErrorKind::Other,
format!(
"Failed to get status of service {}: {}",
ctx.label.to_script_name(),
String::from_utf8_lossy(&output.stderr)
),
)),
}
}
}

fn rc_service(cmd: &str, service: &str) -> io::Result<()> {
let output = Command::new(RC_SERVICE)
fn rc_service<'a>(
cmd: &str,
service: &str,
args: impl IntoIterator<Item = &'a OsStr>,
) -> io::Result<Output> {
let mut command = Command::new(RC_SERVICE);
command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.arg(service)
.arg(cmd)
.output()?;

if output.status.success() {
Ok(())
} else {
let msg = String::from_utf8(output.stderr)
.ok()
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| format!("Failed to {cmd} {service}"));

Err(io::Error::new(io::ErrorKind::Other, msg))
.arg(cmd);
for arg in args {
command.arg(arg);
}
command.output()
}

fn rc_update<'a>(
Expand Down
Loading

0 comments on commit 13dae5e

Please sign in to comment.