1mod error;
56mod utils;
57
58pub use error::Error;
59pub use utils::is_root;
60
61use std::fs::File;
62use std::io::Write;
63use std::path::Path;
64use std::process::Command;
65
66#[derive(Debug, Clone)]
71pub struct ServiceConfig {
72 pub name: String,
74 pub description: String,
76 pub exec_start: String,
78 pub working_directory: Option<String>,
80 pub user: Option<String>,
82 pub group: Option<String>,
84 pub restart: Option<String>,
86 pub restart_sec: Option<u32>,
88 pub wanted_by: Option<String>,
90 pub environment: Option<Vec<(String, String)>>,
92 pub after: Option<Vec<String>>,
94 pub log_file: Option<String>,
96}
97
98impl Default for ServiceConfig {
104 fn default() -> Self {
105 Self {
106 name: String::new(),
107 description: String::new(),
108 exec_start: String::new(),
109 working_directory: None,
110 user: None,
111 group: None,
112 restart: Some("always".to_string()),
113 restart_sec: Some(5),
114 wanted_by: Some("multi-user.target".to_string()),
115 environment: None,
116 after: None,
117 log_file: None,
118 }
119 }
120}
121
122impl ServiceConfig {
123 pub fn new(name: &str, exec_start: &str, description: &str) -> Self {
127 Self {
128 name: name.to_string(),
129 description: description.to_string(),
130 exec_start: exec_start.to_string(),
131 ..Default::default()
132 }
133 }
134
135 pub fn working_directory(mut self, dir: &str) -> Self {
137 self.working_directory = Some(dir.to_string());
138 self
139 }
140
141 pub fn user(mut self, user: &str) -> Self {
143 self.user = Some(user.to_string());
144 self
145 }
146
147 pub fn group(mut self, group: &str) -> Self {
149 self.group = Some(group.to_string());
150 self
151 }
152
153 pub fn restart(mut self, restart: &str) -> Self {
155 self.restart = Some(restart.to_string());
156 self
157 }
158
159 pub fn restart_sec(mut self, sec: u32) -> Self {
161 self.restart_sec = Some(sec);
162 self
163 }
164
165 pub fn wanted_by(mut self, target: &str) -> Self {
167 self.wanted_by = Some(target.to_string());
168 self
169 }
170
171 pub fn environment(mut self, env: Vec<(String, String)>) -> Self {
173 self.environment = Some(env);
174 self
175 }
176
177 pub fn after(mut self, after: Vec<String>) -> Self {
179 self.after = Some(after);
180 self
181 }
182
183 pub fn log_file(mut self, file_path: &str) -> Self {
185 self.log_file = Some(file_path.to_string());
186 self
187 }
188}
189
190pub struct SystemdService {
195 config: ServiceConfig,
196}
197
198impl SystemdService {
199 pub fn new(config: ServiceConfig) -> Self {
201 SystemdService { config }
202 }
203
204 pub fn generate(&self) -> String {
206 let mut content = String::new();
207
208 content.push_str("[Unit]\n");
210 content.push_str(&format!("Description={}\n", self.config.description));
211
212 if let Some(after) = &self.config.after
213 && !after.is_empty()
214 {
215 content.push_str(&format!("After={}\n", after.join(" ")));
216 }
217
218 content.push('\n');
219
220 content.push_str("[Service]\n");
222
223 if let Some(working_directory) = &self.config.working_directory {
224 content.push_str(&format!("WorkingDirectory={}\n", working_directory));
225 }
226
227 if let Some(user) = &self.config.user {
228 content.push_str(&format!("User={}\n", user));
229 }
230
231 if let Some(group) = &self.config.group {
232 content.push_str(&format!("Group={}\n", group));
233 }
234
235 if let Some(restart) = &self.config.restart {
236 content.push_str(&format!("Restart={}\n", restart));
237 }
238
239 if let Some(restart_sec) = self.config.restart_sec {
240 content.push_str(&format!("RestartSec={}\n", restart_sec));
241 }
242
243 content.push_str(&format!("ExecStart={}\n", self.config.exec_start));
244
245 if let Some(log_file) = &self.config.log_file {
246 content.push_str(&format!("StandardOutput=append:{}\n", log_file));
247 content.push_str("StandardError=inherit\n");
248 }
249
250 if let Some(environment) = &self.config.environment
251 && !environment.is_empty()
252 {
253 for (key, value) in environment {
254 content.push_str(&format!("Environment=\"{}={}\"\n", key, value));
255 }
256 }
257 content.push('\n');
258
259 content.push_str("[Install]\n");
261 if let Some(wanted_by) = &self.config.wanted_by {
262 content.push_str(&format!("WantedBy={}\n", wanted_by));
263 }
264
265 content
266 }
267
268 pub fn write(&self, path: &Path) -> Result<(), Error> {
274 validate_root_privileges()?;
275 let content = self.generate();
276 write_service_file(&content, path)
277 }
278
279 pub fn install_and_enable(&self) -> Result<(), Error> {
291 let path = self.get_service_file_path()?;
292 let service_path = Path::new(&path);
293
294 self.write(service_path)?;
296
297 Self::reload_systemd()?;
299
300 self.enable()?;
302
303 println!("Service '{}' installed and enabled", self.config.name); Ok(())
305 }
306
307 pub fn enable(&self) -> Result<(), Error> {
312 validate_root_privileges()?;
313 let status = Command::new("systemctl")
314 .arg("enable")
315 .arg(&self.config.name)
316 .status()?;
317
318 if !status.success() {
319 return Err(Error::Command(format!(
320 "enable '{}' failed",
321 self.config.name
322 )));
323 }
324
325 println!("Service '{}' enabled", self.config.name);
326 Ok(())
327 }
328
329 pub fn start(&self) -> Result<(), Error> {
335 validate_root_privileges()?;
336 let status = Command::new("systemctl")
337 .arg("start")
338 .arg(&self.config.name)
339 .status()?;
340
341 if !status.success() {
342 return Err(Error::Command(format!(
343 "start '{}' failed",
344 self.config.name
345 )));
346 }
347
348 println!("Service '{}' start", self.config.name);
349 Ok(())
350 }
351
352 pub fn get_service_file_path(&self) -> Result<String, Error> {
357 let path = format!("/etc/systemd/system/{}.service", self.config.name);
358 if Path::new(&path).exists() {
359 return Err(Error::Io("Service file exists".to_string()));
361 }
362 Ok(path)
363 }
364
365 pub fn reload_systemd() -> Result<(), Error> {
369 validate_root_privileges()?;
370 let status = Command::new("systemctl").arg("daemon-reload").status()?;
371
372 if !status.success() {
373 return Err(Error::Command("systemctl daemon-reload failed".to_string()));
374 }
375
376 println!("systemd has been reloaded"); Ok(())
378 }
379
380 pub fn stop(&self) -> Result<(), Error> {
386 validate_root_privileges()?;
387 let status = Command::new("systemctl")
388 .arg("stop")
389 .arg(&self.config.name)
390 .status()?;
391
392 if !status.success() {
393 return Err(Error::Command(format!(
394 "stop '{}' failed",
395 self.config.name
396 )));
397 }
398
399 println!("Service '{}' stoped", self.config.name);
400 Ok(())
401 }
402
403 pub fn restart(&self) -> Result<(), Error> {
409 validate_root_privileges()?;
410 let status = Command::new("systemctl")
411 .arg("restart")
412 .arg(&self.config.name)
413 .status()?;
414
415 if !status.success() {
416 return Err(Error::Command(format!(
417 "restart '{}' failed",
418 self.config.name
419 )));
420 }
421
422 println!("Service '{}' restart", self.config.name);
423 Ok(())
424 }
425}
426
427pub fn validate_root_privileges() -> Result<(), Error> {
432 if !is_root() {
433 return Err(Error::Permission("need root privileges".to_string()));
434 }
435 Ok(())
436}
437
438fn write_service_file(content: &str, path: &Path) -> Result<(), Error> {
443 File::create(path)?.write_all(content.as_bytes())?;
444
445 println!("Service file created: {}", path.display());
446 Ok(())
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452
453 #[test]
454 fn test_generate_service_file() {
455 let config = ServiceConfig::new(
456 "myapp",
457 "/usr/local/bin/myapp --daemon",
458 "My Application Service",
459 )
460 .working_directory("/var/lib/myapp")
461 .user("myapp")
462 .group("myapp")
463 .after(vec![
464 "network.target".to_string(),
465 "postgresql.service".to_string(),
466 ])
467 .environment(vec![
468 ("RUST_LOG".to_string(), "info".to_string()),
469 (
470 "DATABASE_URL".to_string(),
471 "postgresql://localhost/myapp".to_string(),
472 ),
473 ]);
474 let systemd = SystemdService::new(config);
475 let service_content = systemd.generate();
476 println!("{}", service_content);
477
478 assert!(service_content.contains("Description=My Application Service"));
479 assert!(service_content.contains("ExecStart=/usr/local/bin/myapp --daemon"));
480 assert!(service_content.contains("User=myapp"));
481 assert!(service_content.contains("After=network.target postgresql.service"));
482 assert!(service_content.contains("Environment=\"RUST_LOG=info\""));
483 }
484
485 #[test]
486 fn test_minimal_service() {
487 let config = ServiceConfig::new("minimal", "/usr/bin/sleep infinity", "Minimal Service");
488
489 let systemd = SystemdService::new(config);
490 let service_content = systemd.generate();
491 println!("{}", service_content);
492
493 assert!(service_content.contains("Description=Minimal Service"));
494 assert!(service_content.contains("ExecStart=/usr/bin/sleep infinity"));
495 assert!(service_content.contains("Restart=always")); assert!(service_content.contains("WantedBy=multi-user.target")); }
498
499 #[test]
500 fn test_root_check() {
501 let result = is_root();
502 eprintln!("is root:{}", result);
503 }
504}