mod error;
mod utils;
pub use error::Error;
pub use utils::is_root;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone)]
pub struct ServiceConfig {
pub name: String,
pub description: String,
pub exec_start: String,
pub working_directory: Option<String>,
pub user: Option<String>,
pub group: Option<String>,
pub restart: Option<String>,
pub restart_sec: Option<u32>,
pub wanted_by: Option<String>,
pub environment: Option<Vec<(String, String)>>,
pub after: Option<Vec<String>>,
pub log_file: Option<String>,
}
impl Default for ServiceConfig {
fn default() -> Self {
Self {
name: String::new(),
description: String::new(),
exec_start: String::new(),
working_directory: None,
user: None,
group: None,
restart: Some("always".to_string()),
restart_sec: Some(5),
wanted_by: Some("multi-user.target".to_string()),
environment: None,
after: None,
log_file: None,
}
}
}
impl ServiceConfig {
pub fn new(name: &str, exec_start: &str, description: &str) -> Self {
Self {
name: name.to_string(),
description: description.to_string(),
exec_start: exec_start.to_string(),
..Default::default()
}
}
pub fn working_directory(mut self, dir: &str) -> Self {
self.working_directory = Some(dir.to_string());
self
}
pub fn user(mut self, user: &str) -> Self {
self.user = Some(user.to_string());
self
}
pub fn group(mut self, group: &str) -> Self {
self.group = Some(group.to_string());
self
}
pub fn restart(mut self, restart: &str) -> Self {
self.restart = Some(restart.to_string());
self
}
pub fn restart_sec(mut self, sec: u32) -> Self {
self.restart_sec = Some(sec);
self
}
pub fn wanted_by(mut self, target: &str) -> Self {
self.wanted_by = Some(target.to_string());
self
}
pub fn environment(mut self, env: Vec<(String, String)>) -> Self {
self.environment = Some(env);
self
}
pub fn after(mut self, after: Vec<String>) -> Self {
self.after = Some(after);
self
}
pub fn log_file(mut self, file_path: &str) -> Self {
self.log_file = Some(file_path.to_string());
self
}
}
pub struct SystemdService {
config: ServiceConfig,
}
impl SystemdService {
pub fn new(config: ServiceConfig) -> Self {
SystemdService { config }
}
pub fn generate(&self) -> String {
let mut content = String::new();
content.push_str("[Unit]\n");
content.push_str(&format!("Description={}\n", self.config.description));
if let Some(after) = &self.config.after
&& !after.is_empty()
{
content.push_str(&format!("After={}\n", after.join(" ")));
}
content.push('\n');
content.push_str("[Service]\n");
if let Some(working_directory) = &self.config.working_directory {
content.push_str(&format!("WorkingDirectory={}\n", working_directory));
}
if let Some(user) = &self.config.user {
content.push_str(&format!("User={}\n", user));
}
if let Some(group) = &self.config.group {
content.push_str(&format!("Group={}\n", group));
}
if let Some(restart) = &self.config.restart {
content.push_str(&format!("Restart={}\n", restart));
}
if let Some(restart_sec) = self.config.restart_sec {
content.push_str(&format!("RestartSec={}\n", restart_sec));
}
content.push_str(&format!("ExecStart={}\n", self.config.exec_start));
if let Some(log_file) = &self.config.log_file {
content.push_str(&format!("StandardOutput=append:{}\n", log_file));
content.push_str("StandardError=inherit\n");
}
if let Some(environment) = &self.config.environment
&& !environment.is_empty()
{
for (key, value) in environment {
content.push_str(&format!("Environment=\"{}={}\"\n", key, value));
}
}
content.push('\n');
content.push_str("[Install]\n");
if let Some(wanted_by) = &self.config.wanted_by {
content.push_str(&format!("WantedBy={}\n", wanted_by));
}
content
}
pub fn write(&self, path: &Path) -> Result<(), Error> {
validate_root_privileges()?;
let content = self.generate();
write_service_file(&content, path)
}
pub fn install_and_enable(&self) -> Result<(), Error> {
let path = self.get_service_file_path()?;
let service_path = Path::new(&path);
self.write(service_path)?;
Self::reload_systemd()?;
self.enable()?;
println!("Service '{}' installed and enabled", self.config.name); Ok(())
}
pub fn enable(&self) -> Result<(), Error> {
validate_root_privileges()?;
let status = Command::new("systemctl")
.arg("enable")
.arg(&self.config.name)
.status()?;
if !status.success() {
return Err(Error::Command(format!(
"enable '{}' failed",
self.config.name
)));
}
println!("Service '{}' enabled", self.config.name);
Ok(())
}
pub fn start(&self) -> Result<(), Error> {
validate_root_privileges()?;
let status = Command::new("systemctl")
.arg("start")
.arg(&self.config.name)
.status()?;
if !status.success() {
return Err(Error::Command(format!(
"start '{}' failed",
self.config.name
)));
}
println!("Service '{}' start", self.config.name);
Ok(())
}
pub fn get_service_file_path(&self) -> Result<String, Error> {
let path = format!("/etc/systemd/system/{}.service", self.config.name);
if Path::new(&path).exists() {
return Err(Error::Io("Service file exists".to_string()));
}
Ok(path)
}
pub fn reload_systemd() -> Result<(), Error> {
validate_root_privileges()?;
let status = Command::new("systemctl").arg("daemon-reload").status()?;
if !status.success() {
return Err(Error::Command("systemctl daemon-reload failed".to_string()));
}
println!("systemd has been reloaded"); Ok(())
}
pub fn stop(&self) -> Result<(), Error> {
validate_root_privileges()?;
let status = Command::new("systemctl")
.arg("stop")
.arg(&self.config.name)
.status()?;
if !status.success() {
return Err(Error::Command(format!(
"stop '{}' failed",
self.config.name
)));
}
println!("Service '{}' stoped", self.config.name);
Ok(())
}
pub fn restart(&self) -> Result<(), Error> {
validate_root_privileges()?;
let status = Command::new("systemctl")
.arg("restart")
.arg(&self.config.name)
.status()?;
if !status.success() {
return Err(Error::Command(format!(
"restart '{}' failed",
self.config.name
)));
}
println!("Service '{}' restart", self.config.name);
Ok(())
}
}
pub fn validate_root_privileges() -> Result<(), Error> {
if !is_root() {
return Err(Error::Permission("need root privileges".to_string()));
}
Ok(())
}
fn write_service_file(content: &str, path: &Path) -> Result<(), Error> {
File::create(path)?.write_all(content.as_bytes())?;
println!("Service file created: {}", path.display());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_service_file() {
let config = ServiceConfig::new(
"myapp",
"/usr/local/bin/myapp --daemon",
"My Application Service",
)
.working_directory("/var/lib/myapp")
.user("myapp")
.group("myapp")
.after(vec![
"network.target".to_string(),
"postgresql.service".to_string(),
])
.environment(vec![
("RUST_LOG".to_string(), "info".to_string()),
(
"DATABASE_URL".to_string(),
"postgresql://localhost/myapp".to_string(),
),
]);
let systemd = SystemdService::new(config);
let service_content = systemd.generate();
println!("{}", service_content);
assert!(service_content.contains("Description=My Application Service"));
assert!(service_content.contains("ExecStart=/usr/local/bin/myapp --daemon"));
assert!(service_content.contains("User=myapp"));
assert!(service_content.contains("After=network.target postgresql.service"));
assert!(service_content.contains("Environment=\"RUST_LOG=info\""));
}
#[test]
fn test_minimal_service() {
let config = ServiceConfig::new("minimal", "/usr/bin/sleep infinity", "Minimal Service");
let systemd = SystemdService::new(config);
let service_content = systemd.generate();
println!("{}", service_content);
assert!(service_content.contains("Description=Minimal Service"));
assert!(service_content.contains("ExecStart=/usr/bin/sleep infinity"));
assert!(service_content.contains("Restart=always")); assert!(service_content.contains("WantedBy=multi-user.target")); }
#[test]
fn test_root_check() {
let result = is_root();
eprintln!("is root:{}", result);
}
}