ant_service_management/
control.rs

1// Copyright (C) 2024 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9use 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
20/// Normalizes a path by stripping common upgrade-related suffixes.
21///
22/// During in-place binary upgrades on Unix systems, the running process may show patterns like:
23/// - " (deleted)": appears when the binary has been replaced while still running
24/// - ".bak", ".old": common backup file suffixes
25///
26/// This function strips these suffixes to allow matching between the expected path and the actual
27/// running process path.
28fn 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
35/// Parse version string from `antnode --version` output.
36///
37/// Expected format: "Autonomi Node v0.4.9" or "Autonomi Node v0.4.10-rc.1"
38/// Nightly format: "Autonomi Node -- Nightly Release 2024.12.03"
39///
40/// Returns `None` if the version cannot be parsed.
41fn parse_version_from_output(output: &str) -> Option<String> {
42    let first_line = output.lines().next()?;
43
44    // Check for nightly format first
45    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    // Standard version format: find 'v' and extract after it
55    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        // Validate it looks like a version (has dots and numbers)
62        if version.contains('.') && version.chars().any(|c| c.is_numeric()) {
63            return Some(version);
64        }
65    }
66
67    None
68}
69
70/// A thin wrapper around the `service_manager::ServiceManager`, which makes our own testing
71/// easier.
72///
73/// We can make an assumption that this external component works correctly, so our own tests only
74/// need assert that the service manager is used. Testing code that used the real service manager
75/// would result in real services on the machines we are testing on; that can leave a bit of a mess
76/// to clean up, especially if the tests fail.
77pub 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    /// Get the version of a running process by its PID.
83    ///
84    /// Attempts to extract version by executing `--version` on the process's binary.
85    /// This works even when the binary has been replaced on disk during upgrades,
86    /// by using cross-platform process information from sysinfo.
87    ///
88    /// Returns `None` if version cannot be determined (not an error condition).
89    /// Errors only on system-level failures.
90    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                // Direct match (existing logic)
252                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                // Enhanced matching: handle upgrade-related path patterns
258                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            // Handle " (deleted)" suffix that appears during binary replacement
300            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                    // Wait with timeout (2 seconds)
339                    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, // Non-zero exit
353                            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                    // In this case the user has removed the service definition file manually,
445                    // which the service manager crate treats as an error. We can propagate the
446                    // it to the caller and they can decide how to handle it.
447                    Err(Error::ServiceRemovedManually(service_name.to_string()))
448                } else if err.raw_os_error() == Some(267) {
449                    // This requires the unstable io_error_more feature, use raw code for now
450                    // else if err.kind() == std::io::ErrorKind::NotADirectory {}
451
452                    // This happens on windows when the service has been already cleared, but was not updated
453                    // in the registry. Happens when the Service application (in windows) is open while calling
454                    // 'remove' or 'reset'.
455                    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    /// Provide a delay for the service to start or stop.
465    ///
466    /// This is wrapped mainly just for unit testing.
467    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}