use crate::error::{Error, Result};
use service_manager::{
ServiceInstallCtx, ServiceLabel, ServiceLevel, ServiceManager, ServiceStartCtx, ServiceStopCtx,
ServiceUninstallCtx,
};
use std::{
net::{SocketAddr, TcpListener},
path::Path,
};
use sysinfo::System;
pub trait ServiceControl: Sync {
fn create_service_user(&self, username: &str) -> Result<()>;
fn get_available_port(&self) -> Result<u16>;
fn install(&self, install_ctx: ServiceInstallCtx, user_mode: bool) -> Result<()>;
fn get_process_pid(&self, path: &Path) -> Result<u32>;
fn start(&self, service_name: &str, user_mode: bool) -> Result<()>;
fn stop(&self, service_name: &str, user_mode: bool) -> Result<()>;
fn uninstall(&self, service_name: &str, user_mode: bool) -> Result<()>;
fn wait(&self, delay: u64);
}
pub struct ServiceController {}
impl ServiceControl for ServiceController {
#[cfg(target_os = "linux")]
fn create_service_user(&self, username: &str) -> Result<()> {
use std::process::Command;
let output = Command::new("id")
.arg("-u")
.arg(username)
.output()
.inspect_err(|err| error!("Failed to execute id -u: {err:?}"))?;
if output.status.success() {
println!("The {username} user already exists");
return Ok(());
}
let useradd_exists = Command::new("which")
.arg("useradd")
.output()
.inspect_err(|err| error!("Failed to execute which useradd: {err:?}"))?
.status
.success();
let adduser_exists = Command::new("which")
.arg("adduser")
.output()
.inspect_err(|err| error!("Failed to execute which adduser: {err:?}"))?
.status
.success();
let output = if useradd_exists {
Command::new("useradd")
.arg("-m")
.arg("-s")
.arg("/bin/bash")
.arg(username)
.output()
.inspect_err(|err| error!("Failed to execute useradd: {err:?}"))?
} else if adduser_exists {
Command::new("adduser")
.arg("-s")
.arg("/bin/busybox")
.arg("-D")
.arg(username)
.output()
.inspect_err(|err| error!("Failed to execute adduser: {err:?}"))?
} else {
error!("Neither useradd nor adduser is available. ServiceUserAccountCreationFailed");
return Err(Error::ServiceUserAccountCreationFailed);
};
if !output.status.success() {
error!("Failed to create {username} user account: {output:?}");
return Err(Error::ServiceUserAccountCreationFailed);
}
println!("Created {username} user account for running the service");
info!("Created {username} user account for running the service");
Ok(())
}
#[cfg(target_os = "macos")]
fn create_service_user(&self, username: &str) -> Result<()> {
use std::process::Command;
use std::str;
let output = Command::new("dscl")
.arg(".")
.arg("-list")
.arg("/Users")
.output()
.inspect_err(|err| error!("Failed to execute dscl: {err:?}"))?;
let output_str = str::from_utf8(&output.stdout)
.inspect_err(|err| error!("Error while converting output to utf8: {err:?}"))?;
if output_str.lines().any(|line| line == username) {
return Ok(());
}
let output = Command::new("dscl")
.arg(".")
.arg("-list")
.arg("/Users")
.arg("UniqueID")
.output()
.inspect_err(|err| error!("Failed to execute dscl: {err:?}"))?;
let output_str = str::from_utf8(&output.stdout)
.inspect_err(|err| error!("Error while converting output to utf8: {err:?}"))?;
let mut max_id = 0;
for line in output_str.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() == 2 {
if let Ok(id) = parts[1].parse::<u32>() {
if id > max_id {
max_id = id;
}
}
}
}
let new_unique_id = max_id + 1;
let commands = vec![
format!("dscl . -create /Users/{}", username),
format!(
"dscl . -create /Users/{} UserShell /usr/bin/false",
username
),
format!(
"dscl . -create /Users/{} UniqueID {}",
username, new_unique_id
),
format!("dscl . -create /Users/{} PrimaryGroupID 20", username),
];
for cmd in commands {
let status = Command::new("sh")
.arg("-c")
.arg(&cmd)
.status()
.inspect_err(|err| error!("Error while executing dscl command: {err:?}"))?;
if !status.success() {
error!("The command {cmd} failed to execute. ServiceUserAccountCreationFailed");
return Err(Error::ServiceUserAccountCreationFailed);
}
}
Ok(())
}
#[cfg(target_os = "windows")]
fn create_service_user(&self, _username: &str) -> Result<()> {
Ok(())
}
fn get_available_port(&self) -> Result<u16> {
let addr: SocketAddr = "127.0.0.1:0".parse()?;
let socket = TcpListener::bind(addr)?;
let port = socket.local_addr()?.port();
drop(socket);
trace!("Got available port: {port}");
Ok(port)
}
fn get_process_pid(&self, bin_path: &Path) -> Result<u32> {
debug!(
"Searching for process with binary at {}",
bin_path.to_string_lossy()
);
let system = System::new_all();
for (pid, process) in system.processes() {
if let Some(path) = process.exe() {
if bin_path == path {
trace!("Found process {bin_path:?} with PID: {pid}");
return Ok(pid.to_string().parse::<u32>()?);
}
}
}
error!(
"No process was located with a path at {}",
bin_path.to_string_lossy()
);
Err(Error::ServiceProcessNotFound(
bin_path.to_string_lossy().to_string(),
))
}
fn install(&self, install_ctx: ServiceInstallCtx, user_mode: bool) -> Result<()> {
debug!("Installing service: {install_ctx:?}");
let mut manager = <dyn ServiceManager>::native()
.inspect_err(|err| error!("Could not get native ServiceManage: {err:?}"))?;
if user_mode {
manager
.set_level(ServiceLevel::User)
.inspect_err(|err| error!("Could not set service to user mode: {err:?}"))?;
}
manager
.install(install_ctx)
.inspect_err(|err| error!("Error while installing service: {err:?}"))?;
Ok(())
}
fn start(&self, service_name: &str, user_mode: bool) -> Result<()> {
debug!("Starting service: {service_name}");
let label: ServiceLabel = service_name.parse()?;
let mut manager = <dyn ServiceManager>::native()
.inspect_err(|err| error!("Could not get native ServiceManage: {err:?}"))?;
if user_mode {
manager
.set_level(ServiceLevel::User)
.inspect_err(|err| error!("Could not set service to user mode: {err:?}"))?;
}
manager
.start(ServiceStartCtx { label })
.inspect_err(|err| error!("Error while starting service: {err:?}"))?;
Ok(())
}
fn stop(&self, service_name: &str, user_mode: bool) -> Result<()> {
debug!("Stopping service: {service_name}");
let label: ServiceLabel = service_name.parse()?;
let mut manager = <dyn ServiceManager>::native()
.inspect_err(|err| error!("Could not get native ServiceManage: {err:?}"))?;
if user_mode {
manager
.set_level(ServiceLevel::User)
.inspect_err(|err| error!("Could not set service to user mode: {err:?}"))?;
}
manager
.stop(ServiceStopCtx { label })
.inspect_err(|err| error!("Error while stopping service: {err:?}"))?;
Ok(())
}
fn uninstall(&self, service_name: &str, user_mode: bool) -> Result<()> {
debug!("Uninstalling service: {service_name}");
let label: ServiceLabel = service_name.parse()?;
let mut manager = <dyn ServiceManager>::native()
.inspect_err(|err| error!("Could not get native ServiceManage: {err:?}"))?;
if user_mode {
manager
.set_level(ServiceLevel::User)
.inspect_err(|err| error!("Could not set service to user mode: {err:?}"))?;
}
match manager.uninstall(ServiceUninstallCtx { label }) {
Ok(()) => Ok(()),
Err(err) => {
if std::io::ErrorKind::NotFound == err.kind() {
error!("Error while uninstall service, service file might have been removed manually: {service_name}");
Err(Error::ServiceRemovedManually(service_name.to_string()))
} else if err.raw_os_error() == Some(267) {
Err(Error::ServiceDoesNotExists(service_name.to_string()))
} else {
error!("Error while uninstalling service: {err:?}");
Err(err.into())
}
}
}
}
fn wait(&self, delay: u64) {
trace!("Waiting for {delay} milliseconds");
std::thread::sleep(std::time::Duration::from_millis(delay));
}
}