systemd_service/
lib.rs

1//! A library for Generate, installing, and managing
2//! systemd services on Linux for Rust binary.
3//!
4//! This crate provides a fluent builder (`ServiceConfig`) to define a systemd
5//! service unit and a `SystemdService` struct to handle the generation,
6//! installation, and management (start, enable) of that service.
7//!
8//! ## ⚠️ Important: Requires Root Privileges
9//!
10//! Most operations in this crate (writing to `/etc/systemd/system`,
11//! running `systemctl` commands) **require root privileges** to execute.
12//! The methods will return an [`Error::Permission`] if run by a non-root user.
13//!
14//! ## Example Usage
15//!
16//! ```no_run
17//!
18//! use systemd_service::{ServiceConfig, SystemdService, Error};
19//!
20//! fn setup_my_service() -> Result<(), Error> {
21//!     // 1. Define the service configuration using the builder
22//!     let config = ServiceConfig::new(
23//!         "myapp",
24//!         "/usr/local/bin/myapp --run",
25//!         "My Application Service",
26//!     )
27//!     .user("myapp-user")
28//!     .group("myapp-group")
29//!     .working_directory("/var/lib/myapp")
30//!     .after(vec!["network.target".to_string()])
31//!     .environment(vec![
32//!         ("RUST_LOG".to_string(), "info".to_string()),
33//!         ("PORT".to_string(), "8080".to_string()),
34//!     ])
35//!     .restart("on-failure")
36//!     .restart_sec(10);
37//!
38//!     // 2. Create the service manager
39//!     let service = SystemdService::new(config);
40//!
41//!     // 3. Install, enable, and reload systemd
42//!     // This requires root privileges!
43//!     service.install_and_enable()?;
44//!
45//!     // 4. Start the service
46//!     // This also requires root privileges!
47//!     service.start()?;
48//!
49//!     println!("Service 'myapp' installed and started successfully.");
50//!     Ok(())
51//! }
52//! ```
53//!
54
55mod 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/// Represents the configuration for a systemd service.
67///
68/// Use [`ServiceConfig::new`] to create a basic configuration
69/// and chain builder methods to set optional parameters.
70#[derive(Debug, Clone)]
71pub struct ServiceConfig {
72    /// The name of the service (e.g., "myapp"). This is used for the .service filename.
73    pub name: String,
74    /// A brief description of the service (e.g., "My Application Service").
75    pub description: String,
76    /// The command to execute to start the service (e.g., "/usr/local/bin/myapp --daemon").
77    pub exec_start: String,
78    /// The working directory for the service process.
79    pub working_directory: Option<String>,
80    /// The user to run the service as.
81    pub user: Option<String>,
82    /// The group to run the service as.
83    pub group: Option<String>,
84    /// Restart policy (e.g., "no", "on-success", "on-failure", "always").
85    pub restart: Option<String>,
86    /// Delay (in seconds) before restarting the service.
87    pub restart_sec: Option<u32>,
88    /// The target to install this service under (usually "multi-user.target").
89    pub wanted_by: Option<String>,
90    /// Environment variables to set for the service (e.g., `vec![("RUST_LOG".to_string(), "info".to_string())]`).
91    pub environment: Option<Vec<(String, String)>>,
92    /// Services that must be started before this one (e.g., `vec!["network.target".to_string()]`).
93    pub after: Option<Vec<String>>,
94    /// File path to redirect `StandardOutput` to. `StandardError` is set to inherit.
95    pub log_file: Option<String>,
96}
97
98/// Provides default values for `ServiceConfig`.
99///
100/// - `restart`: "always"
101/// - `restart_sec`: 5
102/// - `wanted_by`: "multi-user.target"
103impl 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    /// Creates a new `ServiceConfig` with the essential fields.
124    ///
125    /// All other fields are set to their default values.
126    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    /// Sets the working directory for the service (builder method).
136    pub fn working_directory(mut self, dir: &str) -> Self {
137        self.working_directory = Some(dir.to_string());
138        self
139    }
140
141    /// Sets the user for the service (builder method).
142    pub fn user(mut self, user: &str) -> Self {
143        self.user = Some(user.to_string());
144        self
145    }
146
147    /// Sets the group for the service (builder method).
148    pub fn group(mut self, group: &str) -> Self {
149        self.group = Some(group.to_string());
150        self
151    }
152
153    /// Sets the restart policy for the service (builder method).
154    pub fn restart(mut self, restart: &str) -> Self {
155        self.restart = Some(restart.to_string());
156        self
157    }
158
159    /// Sets the restart delay (in seconds) for the service (builder method).
160    pub fn restart_sec(mut self, sec: u32) -> Self {
161        self.restart_sec = Some(sec);
162        self
163    }
164
165    /// Sets the `WantedBy` target for the service (builder method).
166    pub fn wanted_by(mut self, target: &str) -> Self {
167        self.wanted_by = Some(target.to_string());
168        self
169    }
170
171    /// Sets environment variables for the service (builder method).
172    pub fn environment(mut self, env: Vec<(String, String)>) -> Self {
173        self.environment = Some(env);
174        self
175    }
176
177    /// Sets service dependencies (`After=`) (builder method).
178    pub fn after(mut self, after: Vec<String>) -> Self {
179        self.after = Some(after);
180        self
181    }
182
183    /// Sets the log file path for `StandardOutput` (builder method).
184    pub fn log_file(mut self, file_path: &str) -> Self {
185        self.log_file = Some(file_path.to_string());
186        self
187    }
188}
189
190/// Manages a systemd service based on a [`ServiceConfig`].
191///
192/// This struct provides methods to generate the service file content,
193/// write it to disk, and interact with `systemctl` to manage the service.
194pub struct SystemdService {
195    config: ServiceConfig,
196}
197
198impl SystemdService {
199    /// Creates a new `SystemdService` from a given configuration.
200    pub fn new(config: ServiceConfig) -> Self {
201        SystemdService { config }
202    }
203
204    /// Generates the content of the .service unit file as a string.
205    pub fn generate(&self) -> String {
206        let mut content = String::new();
207
208        // [Unit] section
209        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        // [Service] section
221        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        // [Install] section
260        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    /// Writes the generated service file content to the specified path.
269    ///
270    /// # Errors
271    /// - [`Error::Permission`] if not run with root privileges.
272    /// - [`Error::Io`] on file creation or write failures.
273    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    /// Installs, enables, and reloads the systemd daemon.
280    ///
281    /// This is the primary method for setting up a new service. It performs:
282    /// 1. Writes the service file to `/etc/systemd/system/`.
283    /// 2. Reloads the systemd daemon (`systemctl daemon-reload`).
284    /// 3. Enables the service (`systemctl enable`).
285    ///
286    /// # Errors
287    /// - [`Error::Permission`] if not run with root privileges.
288    /// - [`Error::Io`] if the service file already exists or on write failures.
289    /// - [`Error::Command`] if `systemctl` commands fail.
290    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        // 1. Write the file
295        self.write(service_path)?;
296
297        // 2. Reload systemd
298        Self::reload_systemd()?;
299
300        // 3. Enable the service
301        self.enable()?;
302
303        println!("Service '{}' installed and enabled", self.config.name); // "Service '...' installed and enabled"
304        Ok(())
305    }
306
307    /// Enables the service using `systemctl enable`.
308    ///
309    /// Assumes the service file already exists and systemd has been reloaded.
310    /// Requires root privileges.
311    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    /// Starts the service using `systemctl start`.
330    ///
331    /// # Errors
332    /// - [`Error::Permission`] if not run with root privileges.
333    /// - [`Error::Command`] if the `systemctl start` command fails.
334    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    /// Gets the conventional path for the service file (e.g., `/etc/systemd/system/myapp.service`).
353    ///
354    /// # Errors
355    /// - [`Error::Io`] if the service file already exists at this path.
356    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            // Fails if the file already exists to avoid overwriting.
360            return Err(Error::Io("Service file exists".to_string()));
361        }
362        Ok(path)
363    }
364
365    /// Reloads the systemd daemon (`systemctl daemon-reload`).
366    ///
367    /// Requires root privileges.
368    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"); // "systemd has been reloaded"
377        Ok(())
378    }
379
380    /// Stop the service using `systemctl start`.
381    ///
382    /// # Errors
383    /// - [`Error::Permission`] if not run with root privileges.
384    /// - [`Error::Command`] if the `systemctl stop` command fails.
385    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    /// Restarts the service using `systemctl restart`.
404    ///
405    /// # Errors
406    /// - [`Error::Permission`] if not run with root privileges.
407    /// - [`Error::Command`] if the `systemctl restart` command fails.
408    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
427/// Checks for root privileges and returns an error if not root.
428///
429/// # Errors
430/// - [`Error::Permission`] if the check fails (i.e., not root).
431pub 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
438/// Helper function to write the service file content to the specified path.
439///
440/// This function does *not* check for root privileges; it assumes
441/// the caller (e.g., `SystemdService::write`) has already done so.
442fn 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")); // default value
496        assert!(service_content.contains("WantedBy=multi-user.target")); // default value
497    }
498
499    #[test]
500    fn test_root_check() {
501        let result = is_root();
502        eprintln!("is root:{}", result);
503    }
504}