ant_service_management/
control.rs1use crate::error::{Error, Result};
10use service_manager::{
11 ServiceInstallCtx, ServiceLabel, ServiceLevel, ServiceManager, ServiceStartCtx, ServiceStopCtx,
12 ServiceUninstallCtx,
13};
14use std::{
15 net::{SocketAddr, TcpListener},
16 path::Path,
17};
18use sysinfo::{Pid, ProcessRefreshKind, System, UpdateKind};
19
20fn normalize_path_for_comparison(path: &str) -> String {
29 path.trim_end_matches(" (deleted)")
30 .trim_end_matches(".bak")
31 .trim_end_matches(".old")
32 .to_string()
33}
34
35fn parse_version_from_output(output: &str) -> Option<String> {
42 let first_line = output.lines().next()?;
43
44 if first_line.contains("Nightly Release") {
46 return first_line
47 .split("Nightly Release")
48 .nth(1)?
49 .split_whitespace()
50 .next()
51 .map(String::from);
52 }
53
54 if let Some(v_pos) = first_line.find('v') {
56 let version = first_line[v_pos + 1..]
57 .split_whitespace()
58 .next()?
59 .to_string();
60
61 if version.contains('.') && version.chars().any(|c| c.is_numeric()) {
63 return Some(version);
64 }
65 }
66
67 None
68}
69
70pub trait ServiceControl: Sync {
78 fn create_service_user(&self, username: &str) -> Result<()>;
79 fn get_available_port(&self) -> Result<u16>;
80 fn install(&self, install_ctx: ServiceInstallCtx, user_mode: bool) -> Result<()>;
81 fn get_process_pid(&self, path: &Path) -> Result<u32>;
82 fn get_process_version(&self, pid: u32) -> Result<Option<String>>;
91 fn start(&self, service_name: &str, user_mode: bool) -> Result<()>;
92 fn stop(&self, service_name: &str, user_mode: bool) -> Result<()>;
93 fn uninstall(&self, service_name: &str, user_mode: bool) -> Result<()>;
94 fn verify_process_by_pid(&self, pid: u32, expected_name: &str) -> Result<bool>;
95 fn wait(&self, delay: u64);
96}
97
98pub struct ServiceController {}
99
100impl ServiceControl for ServiceController {
101 #[cfg(target_os = "linux")]
102 fn create_service_user(&self, username: &str) -> Result<()> {
103 use std::process::Command;
104
105 let output = Command::new("id")
106 .arg("-u")
107 .arg(username)
108 .output()
109 .inspect_err(|err| error!("Failed to execute id -u: {err:?}"))?;
110 if output.status.success() {
111 println!("The {username} user already exists");
112 return Ok(());
113 }
114
115 let useradd_exists = Command::new("which")
116 .arg("useradd")
117 .output()
118 .inspect_err(|err| error!("Failed to execute which useradd: {err:?}"))?
119 .status
120 .success();
121 let adduser_exists = Command::new("which")
122 .arg("adduser")
123 .output()
124 .inspect_err(|err| error!("Failed to execute which adduser: {err:?}"))?
125 .status
126 .success();
127
128 let output = if useradd_exists {
129 Command::new("useradd")
130 .arg("-m")
131 .arg("-s")
132 .arg("/bin/bash")
133 .arg(username)
134 .output()
135 .inspect_err(|err| error!("Failed to execute useradd: {err:?}"))?
136 } else if adduser_exists {
137 Command::new("adduser")
138 .arg("-s")
139 .arg("/bin/busybox")
140 .arg("-D")
141 .arg(username)
142 .output()
143 .inspect_err(|err| error!("Failed to execute adduser: {err:?}"))?
144 } else {
145 error!("Neither useradd nor adduser is available. ServiceUserAccountCreationFailed");
146 return Err(Error::ServiceUserAccountCreationFailed);
147 };
148
149 if !output.status.success() {
150 error!("Failed to create {username} user account: {output:?}");
151 return Err(Error::ServiceUserAccountCreationFailed);
152 }
153 println!("Created {username} user account for running the service");
154 info!("Created {username} user account for running the service");
155 Ok(())
156 }
157
158 #[cfg(target_os = "macos")]
159 fn create_service_user(&self, username: &str) -> Result<()> {
160 use std::process::Command;
161 use std::str;
162
163 let output = Command::new("dscl")
164 .arg(".")
165 .arg("-list")
166 .arg("/Users")
167 .output()
168 .inspect_err(|err| error!("Failed to execute dscl: {err:?}"))?;
169 let output_str = str::from_utf8(&output.stdout)
170 .inspect_err(|err| error!("Error while converting output to utf8: {err:?}"))?;
171 if output_str.lines().any(|line| line == username) {
172 return Ok(());
173 }
174
175 let output = Command::new("dscl")
176 .arg(".")
177 .arg("-list")
178 .arg("/Users")
179 .arg("UniqueID")
180 .output()
181 .inspect_err(|err| error!("Failed to execute dscl: {err:?}"))?;
182 let output_str = str::from_utf8(&output.stdout)
183 .inspect_err(|err| error!("Error while converting output to utf8: {err:?}"))?;
184 let mut max_id = 0;
185
186 for line in output_str.lines() {
187 let parts: Vec<&str> = line.split_whitespace().collect();
188 if let Ok(id) = parts[1].parse::<u32>()
189 && id > max_id
190 && parts.len() == 2
191 {
192 max_id = id;
193 }
194 }
195
196 let new_unique_id = max_id + 1;
197
198 let commands = vec![
199 format!("dscl . -create /Users/{}", username),
200 format!(
201 "dscl . -create /Users/{} UserShell /usr/bin/false",
202 username
203 ),
204 format!(
205 "dscl . -create /Users/{} UniqueID {}",
206 username, new_unique_id
207 ),
208 format!("dscl . -create /Users/{} PrimaryGroupID 20", username),
209 ];
210 for cmd in commands {
211 let status = Command::new("sh")
212 .arg("-c")
213 .arg(&cmd)
214 .status()
215 .inspect_err(|err| error!("Error while executing dscl command: {err:?}"))?;
216 if !status.success() {
217 error!("The command {cmd} failed to execute. ServiceUserAccountCreationFailed");
218 return Err(Error::ServiceUserAccountCreationFailed);
219 }
220 }
221 Ok(())
222 }
223
224 #[cfg(target_os = "windows")]
225 fn create_service_user(&self, _username: &str) -> Result<()> {
226 Ok(())
227 }
228
229 fn get_available_port(&self) -> Result<u16> {
230 let addr: SocketAddr = "127.0.0.1:0".parse()?;
231
232 let socket = TcpListener::bind(addr)?;
233 let port = socket.local_addr()?.port();
234 drop(socket);
235 trace!("Got available port: {port}");
236
237 Ok(port)
238 }
239
240 fn get_process_pid(&self, bin_path: &Path) -> Result<u32> {
241 debug!(
242 "Searching for process with binary at {}",
243 bin_path.to_string_lossy()
244 );
245 let system = System::new_all();
246 let bin_path_str = bin_path.to_string_lossy();
247 let normalized_bin_path = normalize_path_for_comparison(&bin_path_str);
248
249 for (pid, process) in system.processes() {
250 if let Some(path) = process.exe() {
251 if bin_path == path {
253 trace!("Found process {bin_path:?} with PID: {pid} (exact match)");
254 return Ok(pid.to_string().parse::<u32>()?);
255 }
256
257 let path_str = path.to_string_lossy();
259 let normalized_path = normalize_path_for_comparison(&path_str);
260
261 if normalized_path == normalized_bin_path {
262 debug!(
263 "Found process with modified path: '{}' matches expected '{}' (normalized)",
264 path_str, bin_path_str
265 );
266 trace!("Found process {bin_path:?} with PID: {pid}");
267 return Ok(pid.to_string().parse::<u32>()?);
268 }
269 }
270 }
271 error!(
272 "No process was located with a path at {}",
273 bin_path.to_string_lossy()
274 );
275 Err(Error::ServiceProcessNotFound(
276 bin_path.to_string_lossy().to_string(),
277 ))
278 }
279
280 fn verify_process_by_pid(&self, pid: u32, expected_name: &str) -> Result<bool> {
281 debug!("Verifying process with PID {pid}, expected name: {expected_name}");
282
283 let mut system = System::new();
284 let pid = Pid::from_u32(pid);
285
286 if !system.refresh_process_specifics(
287 pid,
288 ProcessRefreshKind::new().with_exe(UpdateKind::OnlyIfNotSet),
289 ) {
290 debug!("Process with PID {pid} does not exist");
291 return Ok(false);
292 }
293
294 if let Some(process) = system.process(pid)
295 && let Some(exe_path) = process.exe()
296 {
297 let process_name = exe_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
298
299 let normalized_name = normalize_path_for_comparison(process_name);
301 let is_match =
302 normalized_name == expected_name || normalized_name.starts_with(expected_name);
303
304 debug!(
305 "Process {pid} name: '{}' (normalized: '{}'), expected: '{}', match: {}",
306 process_name, normalized_name, expected_name, is_match
307 );
308 return Ok(is_match);
309 }
310
311 Ok(false)
312 }
313
314 fn get_process_version(&self, pid: u32) -> Result<Option<String>> {
315 use std::io::Read;
316 use std::process::{Command, Stdio};
317
318 debug!("Extracting version from process with PID {pid}");
319
320 let system = System::new_all();
321 if let Some(process) = system.process(Pid::from_u32(pid))
322 && let Some(exe_path) = process.exe()
323 {
324 debug!("Found exe path for PID {pid}: {}", exe_path.display());
325
326 match Command::new(exe_path)
327 .arg("--version")
328 .stdout(Stdio::piped())
329 .stderr(Stdio::null())
330 .spawn()
331 {
332 Ok(mut child) => {
333 let mut output = String::new();
334 if let Some(ref mut stdout) = child.stdout {
335 let _ = stdout.read_to_string(&mut output);
336 }
337
338 let timeout = std::time::Duration::from_secs(2);
340 let start = std::time::Instant::now();
341 loop {
342 match child.try_wait() {
343 Ok(Some(status)) if status.success() => {
344 if let Some(version) = parse_version_from_output(&output) {
345 debug!(
346 "Successfully extracted version '{version}' from PID {pid}"
347 );
348 return Ok(Some(version));
349 }
350 break;
351 }
352 Ok(Some(_)) => break, Ok(None) => {
354 if start.elapsed() > timeout {
355 debug!("Version extraction timed out for PID {pid}");
356 let _ = child.kill();
357 break;
358 }
359 std::thread::sleep(std::time::Duration::from_millis(50));
360 }
361 Err(e) => {
362 debug!("Error waiting for version command: {e:?}");
363 break;
364 }
365 }
366 }
367 }
368 Err(e) => {
369 debug!("Failed to spawn --version command: {e:?}");
370 }
371 }
372 }
373
374 debug!("Could not extract version from process {pid}");
375 Ok(None)
376 }
377
378 fn install(&self, install_ctx: ServiceInstallCtx, user_mode: bool) -> Result<()> {
379 debug!("Installing service: {install_ctx:?}");
380 let mut manager = <dyn ServiceManager>::native()
381 .inspect_err(|err| error!("Could not get native ServiceManage: {err:?}"))?;
382 if user_mode {
383 manager
384 .set_level(ServiceLevel::User)
385 .inspect_err(|err| error!("Could not set service to user mode: {err:?}"))?;
386 }
387 manager
388 .install(install_ctx)
389 .inspect_err(|err| error!("Error while installing service: {err:?}"))?;
390 Ok(())
391 }
392
393 fn start(&self, service_name: &str, user_mode: bool) -> Result<()> {
394 debug!("Starting service: {service_name}");
395 let label: ServiceLabel = service_name.parse()?;
396 let mut manager = <dyn ServiceManager>::native()
397 .inspect_err(|err| error!("Could not get native ServiceManage: {err:?}"))?;
398 if user_mode {
399 manager
400 .set_level(ServiceLevel::User)
401 .inspect_err(|err| error!("Could not set service to user mode: {err:?}"))?;
402 }
403 manager
404 .start(ServiceStartCtx { label })
405 .inspect_err(|err| error!("Error while starting service: {err:?}"))?;
406 Ok(())
407 }
408
409 fn stop(&self, service_name: &str, user_mode: bool) -> Result<()> {
410 debug!("Stopping service: {service_name}");
411 let label: ServiceLabel = service_name.parse()?;
412 let mut manager = <dyn ServiceManager>::native()
413 .inspect_err(|err| error!("Could not get native ServiceManage: {err:?}"))?;
414 if user_mode {
415 manager
416 .set_level(ServiceLevel::User)
417 .inspect_err(|err| error!("Could not set service to user mode: {err:?}"))?;
418 }
419 manager
420 .stop(ServiceStopCtx { label })
421 .inspect_err(|err| error!("Error while stopping service: {err:?}"))?;
422
423 Ok(())
424 }
425
426 fn uninstall(&self, service_name: &str, user_mode: bool) -> Result<()> {
427 debug!("Uninstalling service: {service_name}");
428 let label: ServiceLabel = service_name.parse()?;
429 let mut manager = <dyn ServiceManager>::native()
430 .inspect_err(|err| error!("Could not get native ServiceManage: {err:?}"))?;
431
432 if user_mode {
433 manager
434 .set_level(ServiceLevel::User)
435 .inspect_err(|err| error!("Could not set service to user mode: {err:?}"))?;
436 }
437 match manager.uninstall(ServiceUninstallCtx { label }) {
438 Ok(()) => Ok(()),
439 Err(err) => {
440 if std::io::ErrorKind::NotFound == err.kind() {
441 error!(
442 "Error while uninstall service, service file might have been removed manually: {service_name}"
443 );
444 Err(Error::ServiceRemovedManually(service_name.to_string()))
448 } else if err.raw_os_error() == Some(267) {
449 Err(Error::ServiceDoesNotExists(service_name.to_string()))
456 } else {
457 error!("Error while uninstalling service: {err:?}");
458 Err(err.into())
459 }
460 }
461 }
462 }
463
464 fn wait(&self, delay: u64) {
468 trace!("Waiting for {delay} milliseconds");
469 std::thread::sleep(std::time::Duration::from_millis(delay));
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn test_parse_version_standard() {
479 let output = "Autonomi Node v0.4.9\nNetwork version: ant/1.0/1\nPackage version: 2025.11.2.1\nGit info: feat-restart_on_success / 892e58a79 / 2025-12-03";
480 assert_eq!(parse_version_from_output(output), Some("0.4.9".to_string()));
481 }
482
483 #[test]
484 fn test_parse_version_prerelease() {
485 let output = "Autonomi Node v0.4.10-rc.1\nNetwork version: ant/1.0/1";
486 assert_eq!(
487 parse_version_from_output(output),
488 Some("0.4.10-rc.1".to_string())
489 );
490 }
491
492 #[test]
493 fn test_parse_version_nightly() {
494 let output = "Autonomi Node -- Nightly Release 2024.12.03\nNetwork version: ant/1.0/1";
495 assert_eq!(
496 parse_version_from_output(output),
497 Some("2024.12.03".to_string())
498 );
499 }
500
501 #[test]
502 fn test_parse_version_invalid() {
503 assert_eq!(parse_version_from_output("Invalid output"), None);
504 }
505
506 #[test]
507 fn test_parse_version_empty() {
508 assert_eq!(parse_version_from_output(""), None);
509 }
510
511 #[test]
512 fn test_parse_version_no_version_prefix() {
513 let output = "Autonomi Node 0.4.9";
514 assert_eq!(parse_version_from_output(output), None);
515 }
516}