1use crate::ServiceDetails;
2use anyhow::{bail, Result};
3
4pub const MANAGED_BY_COMMENT: &str = "# Managed by ser";
6
7pub fn generate_timer_file(service: &ServiceDetails) -> Result<String> {
9 let schedule = service
10 .schedule
11 .as_ref()
12 .ok_or_else(|| anyhow::anyhow!("No schedule defined"))?;
13
14 let mut content = String::new();
15 content.push_str(MANAGED_BY_COMMENT);
16 content.push('\n');
17 content.push_str("[Unit]\n");
18 content.push_str(&format!("Description=Timer for {}\n", service.name));
19 content.push_str("\n[Timer]\n");
20 content.push_str(&format!(
21 "OnCalendar={}\n",
22 schedule.to_systemd_oncalendar()
23 ));
24 content.push_str("Persistent=true\n");
25 content.push_str("\n[Install]\n");
26 content.push_str("WantedBy=timers.target\n");
27
28 Ok(content)
29}
30
31pub fn parse_systemd(contents: &str) -> Result<ServiceDetails> {
32 let mut name = None;
34 let mut program = None;
35 let mut arguments = Vec::new();
36 let mut working_directory = None;
37 let mut run_at_load = false;
38 let mut keep_alive = false;
39 let mut env_file = None;
40 let mut env_vars = Vec::new();
41 let mut after = Vec::new();
42
43 for line in contents.lines() {
44 let line = line.trim();
45 if line.starts_with("Description=") {
46 name = line.strip_prefix("Description=").map(|s| s.to_string());
47 }
48 if line.starts_with("ExecStart=") {
49 let exec_start = line.strip_prefix("ExecStart=").unwrap_or("");
50 let mut parts = exec_start.split_whitespace();
51 if let Some(prog) = parts.next() {
52 program = Some(prog.to_string());
53 arguments = parts.map(|s| s.to_string()).collect();
54 } else {
55 bail!("ExecStart line is empty in service file");
56 }
57 } else if line.starts_with("WorkingDirectory=") {
58 working_directory = line
59 .strip_prefix("WorkingDirectory=")
60 .map(|s| s.to_string());
61 } else if line == "WantedBy=multi-user.target" || line == "WantedBy=default.target" {
62 run_at_load = true;
63 } else if line.starts_with("Restart=") {
64 keep_alive = line != "Restart=no";
65 } else if line.starts_with("EnvironmentFile=") {
66 env_file = line.strip_prefix("EnvironmentFile=").map(|s| s.to_string());
67 } else if line.starts_with("Environment=") {
68 let env_line = line.strip_prefix("Environment=").unwrap();
70 let Some((a, b)) = env_line.split_once('=') else {
71 bail!("Environment line is empty in service file");
72 };
73 env_vars.push((a.to_string(), b.to_string()));
74 } else if line.starts_with("After=") {
75 let after_line = line.strip_prefix("After=").unwrap_or("");
76 after = after_line
77 .split_whitespace()
78 .map(|s| s.to_string())
79 .collect();
80 }
81 }
82 Ok(ServiceDetails {
83 name: name.expect("No name for service"),
84 program: program.expect("No program for service"),
85 arguments,
86 working_directory,
87 run_at_load,
88 keep_alive,
89 env_file,
90 env_vars,
91 after,
92 schedule: None, })
94}
95
96pub fn generate_file(service: &ServiceDetails) -> Result<String> {
97 let mut unit_content = String::new();
98 unit_content.push_str(MANAGED_BY_COMMENT);
99 unit_content.push('\n');
100 unit_content.push_str("[Unit]\n");
101 unit_content.push_str(&format!("Description={}\n", service.name));
102 if !service.after.is_empty() {
103 unit_content.push_str("After=");
104 for after in &service.after {
105 unit_content.push_str(after);
106 unit_content.push(' ');
107 }
108 unit_content.pop(); unit_content.push('\n');
110 }
111 unit_content.push_str("\n[Service]\n");
112
113 if service.schedule.is_some() {
115 unit_content.push_str("Type=oneshot\n");
116 }
117
118 unit_content.push_str("ExecStart=");
119 unit_content.push_str(&service.program);
120 for arg in &service.arguments {
121 unit_content.push(' ');
122 unit_content.push_str(arg);
123 }
124 unit_content.push('\n');
125
126 if let Some(ref wd) = service.working_directory {
127 unit_content.push_str(&format!("WorkingDirectory={}\n", wd));
128 }
129
130 if service.schedule.is_none() && service.keep_alive {
132 unit_content.push_str("Restart=always\n");
133 }
134 if let Some(file) = &service.env_file {
135 unit_content.push_str(&format!("EnvironmentFile={}\n", file));
136 }
137 for (key, value) in &service.env_vars {
138 unit_content.push_str(&format!("Environment=\"{}={}\"\n", key, value));
139 }
140
141 if service.schedule.is_none() && service.run_at_load {
143 unit_content.push_str("\n[Install]\n");
144 unit_content.push_str("WantedBy=default.target\n");
145 }
146
147 Ok(unit_content)
148}