Skip to main content

regent_sdk/state/attribute/system/
service.rs

1//! Service management attribute
2//!
3//! This module provides the `ServiceBlockExpectedState` type for managing system services.
4//!
5//! **Compatible OS:**
6//! - Linux (all distributions with systemd) - uses `systemctl`
7//! - Windows (when `windows` feature is enabled) - uses `sc.exe` and `net` commands
8//!
9//! # Examples
10//!
11//! ## Rust API
12//!
13//! ```no_run
14//! use regent_sdk::state::attribute::system::service::{ServiceBlockExpectedState, ServiceExpectedState, ServiceAction};
15//! use regent_sdk::{Attribute, ExpectedState, Privilege};
16//!
17//! // Ensure httpd service is running and enabled
18//! let httpd = ServiceBlockExpectedState::state(
19//!     "httpd",
20//!     ServiceExpectedState::Started,
21//!     true
22//! );
23//!
24//! // Just manage service state (running/stopped)
25//! let nginx = ServiceBlockExpectedState::state("nginx", ServiceExpectedState::Started, false);
26//!
27//! // Just manage whether service is enabled at boot
28//! let mysql = ServiceBlockExpectedState::enabled("mysql", true);
29//!
30//! // Manage unconditional action (restart)
31//! let redis = ServiceBlockExpectedState::restarted("redis");
32//!
33//! let expected_state = ExpectedState::new()
34//!     .with_attribute(Attribute::service(httpd, Privilege::WithSudo, None))
35//!     .build();
36//! ```
37//!
38//! ## YAML API
39//!
40//! ```yaml
41//! Attributes:
42//!   - Name: Httpd must be running and enabled
43//!     Privilege: !WithSudo
44//!     Detail: !Service
45//!       Name: httpd
46//!       State: Started
47//!       Enabled: true
48//! ```
49//!
50//! For state-only configuration:
51//!
52//! ```yaml
53//! Attributes:
54//!   - Name: Nginx must be stopped
55//!     Privilege: !WithSudo
56//!     Detail: !Service
57//!       Name: nginx
58//!       State: Stopped
59//! ```
60//!
61//! For enabled-only configuration:
62//!
63//! ```yaml
64//! Attributes:
65//!   - Name: MySQL must be disabled at boot
66//!     Privilege: !WithSudo
67//!     Detail: !Service
68//!       Name: mysql
69//!       Enabled: false
70//! ```
71
72use crate::error::RegentError;
73use crate::hosts::managed_host::InternalApiCallOutcome;
74use crate::hosts::managed_host::{AssessCompliance, ReachCompliance, Timeout};
75use crate::hosts::properties::{HostProperties, InitSystem, LinuxFlavor, LinuxSpecifics, OsKind};
76use crate::secrets::SecretProvidersPool;
77use crate::state::Check;
78use crate::state::attribute::HostHandler;
79use crate::state::attribute::Privilege;
80use crate::state::attribute::Remediation;
81use crate::state::attribute::RemediationsList;
82use crate::state::compliance::AttributeComplianceAssessment;
83use serde::{Deserialize, Serialize};
84use std::time::Duration;
85
86/// Desired run-state of the service
87///
88/// - `Started`  / `Stopped`  — idempotent: only act if the service is not already in the target state.
89///
90/// # Serialization
91///
92/// This enum is serialized/deserialized in PascalCase:
93/// - `Started` → `"Started"`
94/// - `Stopped` → `"Stopped"`
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96#[serde(rename_all = "PascalCase")]
97pub enum ServiceExpectedState {
98    /// Service should be running
99    Started,
100    /// Service should be stopped
101    Stopped,
102}
103
104/// Desired action to run on the service
105///
106/// - `Restarted`/ `Reloaded` — unconditional: always emit the corresponding systemctl command.
107///
108/// # Serialization
109///
110/// This enum is serialized/deserialized in PascalCase:
111/// - `Restarted` → `"Restarted"`
112/// - `Reloaded` → `"Reloaded"`
113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
114#[serde(rename_all = "PascalCase")]
115pub enum ServiceAction {
116    /// Service should be restarted (unconditional action)
117    Restarted,
118    /// Service should be reloaded (unconditional action)
119    Reloaded,
120}
121
122/// Configuration for a system service
123///
124/// This enum represents the desired state for a system service, supporting configurations:
125/// - `State`: Manage the service's running state (started/stopped) and/or boot enablement
126/// - `Action`: Manage unconditional actions (restart/reload)
127///
128/// # YAML Representation
129///
130/// ## State with enabled:
131/// ```yaml
132/// Name: httpd
133/// State: Started
134/// Enabled: true
135/// ```
136///
137/// ## State only:
138/// ```yaml
139/// Name: nginx
140/// State: Started
141/// ```
142///
143/// ## Enabled only:
144/// ```yaml
145/// Name: mysql
146/// Enabled: true
147/// ```
148///
149/// ## Action only:
150/// ```yaml
151/// Name: nginx
152/// Action: Restarted
153/// ```
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155#[serde(rename_all_fields = "PascalCase")]
156#[serde(untagged)]
157pub enum ServiceBlockExpectedState {
158    /// Manage the service's running state and/or boot enablement
159    ///
160    /// Note: The `enabled` field is only present in this variant to respect the semantics
161    /// that enabled state is managed alongside running state.
162    State {
163        /// Name of the service
164        name: String,
165        /// Desired running state of the service (optional - can be omitted for enabled-only config)
166        #[serde(default)]
167        state: Option<ServiceExpectedState>,
168        /// Whether the service should be enabled at boot
169        enabled: bool,
170    },
171    /// Manage unconditional actions (restart/reload)
172    Action {
173        /// Name of the service
174        name: String,
175        /// Action to perform on the service
176        action: ServiceAction,
177    },
178}
179
180impl Timeout for ServiceBlockExpectedState {
181    fn default_timeout(&self) -> Duration {
182        Duration::from_secs(10)
183    }
184}
185
186impl ServiceBlockExpectedState {
187    /// Create a state configuration with running state and boot enablement
188    pub fn state(
189        name: &str,
190        state: ServiceExpectedState,
191        enabled: bool,
192    ) -> ServiceBlockExpectedState {
193        ServiceBlockExpectedState::State {
194            name: name.to_string(),
195            state: Some(state),
196            enabled,
197        }
198    }
199
200    /// Create an enabled-only configuration (no state management, only enablement)
201    pub fn enabled(name: &str, enabled: bool) -> ServiceBlockExpectedState {
202        ServiceBlockExpectedState::State {
203            name: name.to_string(),
204            state: None,
205            enabled,
206        }
207    }
208
209    /// Create a restarted action configuration
210    pub fn restarted(name: &str) -> ServiceBlockExpectedState {
211        ServiceBlockExpectedState::Action {
212            name: name.to_string(),
213            action: ServiceAction::Restarted,
214        }
215    }
216
217    /// Create a reloaded action configuration
218    pub fn reloaded(name: &str) -> ServiceBlockExpectedState {
219        ServiceBlockExpectedState::Action {
220            name: name.to_string(),
221            action: ServiceAction::Reloaded,
222        }
223    }
224}
225
226impl Check for ServiceBlockExpectedState {
227    fn check(&self) -> Result<(), RegentError> {
228        // if self.state.is_none() && self.enabled.is_none() {
229        //     return Err(RegentError::IncoherentExpectedState(
230        //         "At least one of State or Enabled must be set.".to_string(),
231        //     ));
232        // }
233        Ok(())
234    }
235
236    fn check_host_compatibility(
237        &self,
238        host_properties: &HostProperties,
239    ) -> Result<(), RegentError> {
240        use crate::hosts::properties::InitSystem;
241        match host_properties.os_kind() {
242            OsKind::Linux(linux_specifics) => match linux_specifics.init_system {
243                InitSystem::Systemd => Ok(()),
244                InitSystem::Unknown => Err(RegentError::IncompatibleHost(
245                    "systemctl requires systemd but init system could not be detected".to_string(),
246                )),
247            },
248            #[cfg(feature = "windows")]
249            OsKind::Windows(_) => Ok(()),
250            incompatible_os_kind => Err(RegentError::IncompatibleHost(format!(
251                "Host is {:?} but service management is only supported on Linux with systemd or Windows (with windows feature)",
252                incompatible_os_kind
253            ))),
254        }
255    }
256}
257
258impl<Handler: HostHandler> AssessCompliance<Handler> for ServiceBlockExpectedState {
259    async fn assess_compliance(
260        &self,
261        host_handler: &mut Handler,
262        host_properties: &Option<HostProperties>,
263        privilege: &Privilege,
264        _optional_secret_provider: &Option<SecretProvidersPool>,
265    ) -> Result<AttributeComplianceAssessment, RegentError> {
266        // Early check: verify we're on a compatible host
267        if let Some(props) = host_properties {
268            self.check_host_compatibility(props)?;
269        }
270
271        // Determine the effective OS kind - assume Linux if HostProperties is None
272        let os_kind = host_properties
273            .as_ref()
274            .map(|props| props.os_kind())
275            .unwrap_or(&OsKind::Linux(LinuxSpecifics {
276                linux_flavor: LinuxFlavor::Debian,
277                init_system: InitSystem::Systemd,
278            }));
279
280        // Check OS-dependent prerequisites
281        match os_kind {
282            #[cfg(feature = "windows")]
283            OsKind::Windows(_) => {
284                // Check if sc.exe is available on Windows
285                let command_available = host_handler
286                    .is_this_command_available("sc", privilege)
287                    .await
288                    .unwrap_or(false);
289
290                if !command_available {
291                    return Err(RegentError::FailedDryRunEvaluation(
292                        "Service management commands (sc) are not available on this Windows host"
293                            .to_string(),
294                    ));
295                }
296            }
297            OsKind::Linux(_) => {
298                // Check if systemctl is available on Linux
299                let command_available = host_handler
300                    .is_this_command_available("systemctl", privilege)
301                    .await
302                    .unwrap_or(false);
303
304                if !command_available {
305                    return Err(RegentError::FailedDryRunEvaluation(
306                        "Service management commands (systemctl) are not available on this Linux host".to_string(),
307                    ));
308                }
309            }
310            OsKind::FreeBsd(_) | OsKind::MacOs(_) | OsKind::Unknown => {}
311        }
312
313        // Match on OS kind to determine service checking behavior
314        let mut remediations: Vec<Remediation> = Vec::new();
315
316        match &self {
317            Self::State {
318                name,
319                state,
320                enabled,
321            } => {
322                // Handle state (started/stopped) with optional enabled
323                match os_kind {
324                    #[cfg(feature = "windows")]
325                    OsKind::Windows(_) => {
326                        // Handle state if present
327                        if let Some(state) = state {
328                            match state {
329                                ServiceExpectedState::Started => {
330                                    let active = windows_service_is_active(host_handler, &name)
331                                        .await
332                                        .map_err(|e| RegentError::FailedDryRunEvaluation(e))?;
333                                    if !active {
334                                        remediations.push(Remediation::Service(
335                                            ServiceApiCall::from(
336                                                ServiceModuleInternalApiCall::Start(name.clone()),
337                                                privilege.clone(),
338                                            ),
339                                        ));
340                                    }
341                                }
342                                ServiceExpectedState::Stopped => {
343                                    let active = windows_service_is_active(host_handler, &name)
344                                        .await
345                                        .map_err(|e| RegentError::FailedDryRunEvaluation(e))?;
346                                    if active {
347                                        remediations.push(Remediation::Service(
348                                            ServiceApiCall::from(
349                                                ServiceModuleInternalApiCall::Stop(name.clone()),
350                                                privilege.clone(),
351                                            ),
352                                        ));
353                                    }
354                                }
355                            }
356                        }
357                        // Handle enabled
358                        if *enabled {
359                            let is_enabled = windows_service_is_enabled(host_handler, &name)
360                                .await
361                                .map_err(|e| RegentError::FailedDryRunEvaluation(e))?;
362                            if !is_enabled {
363                                remediations.push(Remediation::Service(ServiceApiCall::from(
364                                    ServiceModuleInternalApiCall::Enable(name.clone()),
365                                    privilege.clone(),
366                                )));
367                            }
368                        } else {
369                            let is_enabled = windows_service_is_enabled(host_handler, &name)
370                                .await
371                                .map_err(|e| RegentError::FailedDryRunEvaluation(e))?;
372                            if is_enabled {
373                                remediations.push(Remediation::Service(ServiceApiCall::from(
374                                    ServiceModuleInternalApiCall::Disable(name.clone()),
375                                    privilege.clone(),
376                                )));
377                            }
378                        }
379                    }
380                    OsKind::Linux(_) => {
381                        // Handle state if present
382                        if let Some(state) = state {
383                            match state {
384                                ServiceExpectedState::Started => {
385                                    let active = service_is_active(host_handler, &name)
386                                        .await
387                                        .map_err(|e| RegentError::FailedDryRunEvaluation(e))?;
388                                    if !active {
389                                        remediations.push(Remediation::Service(
390                                            ServiceApiCall::from(
391                                                ServiceModuleInternalApiCall::Start(name.clone()),
392                                                privilege.clone(),
393                                            ),
394                                        ));
395                                    }
396                                }
397                                ServiceExpectedState::Stopped => {
398                                    let active = service_is_active(host_handler, &name)
399                                        .await
400                                        .map_err(|e| RegentError::FailedDryRunEvaluation(e))?;
401                                    if active {
402                                        remediations.push(Remediation::Service(
403                                            ServiceApiCall::from(
404                                                ServiceModuleInternalApiCall::Stop(name.clone()),
405                                                privilege.clone(),
406                                            ),
407                                        ));
408                                    }
409                                }
410                            }
411                        }
412                        // Handle enabled
413                        if *enabled {
414                            let is_enabled = service_is_enabled(host_handler, &name)
415                                .await
416                                .map_err(|e| RegentError::FailedDryRunEvaluation(e))?;
417                            if !is_enabled {
418                                remediations.push(Remediation::Service(ServiceApiCall::from(
419                                    ServiceModuleInternalApiCall::Enable(name.clone()),
420                                    privilege.clone(),
421                                )));
422                            }
423                        } else {
424                            let is_enabled = service_is_enabled(host_handler, &name)
425                                .await
426                                .map_err(|e| RegentError::FailedDryRunEvaluation(e))?;
427                            if is_enabled {
428                                remediations.push(Remediation::Service(ServiceApiCall::from(
429                                    ServiceModuleInternalApiCall::Disable(name.clone()),
430                                    privilege.clone(),
431                                )));
432                            }
433                        }
434                    }
435                    OsKind::FreeBsd(_) | OsKind::MacOs(_) | OsKind::Unknown => {
436                        return Err(RegentError::FailedDryRunEvaluation(format!(
437                            "Service management is not supported on {:?}",
438                            os_kind
439                        )));
440                    }
441                }
442            }
443            Self::Action { name, action } => {
444                // Handle unconditional actions (restart/reload)
445                match os_kind {
446                    #[cfg(feature = "windows")]
447                    OsKind::Windows(_) => {
448                        match &action {
449                            ServiceAction::Restarted => {
450                                // Unconditional — always restart.
451                                remediations.push(Remediation::Service(ServiceApiCall::from(
452                                    ServiceModuleInternalApiCall::Restart(name.clone()),
453                                    privilege.clone(),
454                                )));
455                            }
456                            ServiceAction::Reloaded => {
457                                // Unconditional — always reload.
458                                remediations.push(Remediation::Service(ServiceApiCall::from(
459                                    ServiceModuleInternalApiCall::Reload(name.clone()),
460                                    privilege.clone(),
461                                )));
462                            }
463                        }
464                    }
465                    OsKind::Linux(_) => {
466                        match &action {
467                            ServiceAction::Restarted => {
468                                // Unconditional — always restart.
469                                remediations.push(Remediation::Service(ServiceApiCall::from(
470                                    ServiceModuleInternalApiCall::Restart(name.clone()),
471                                    privilege.clone(),
472                                )));
473                            }
474                            ServiceAction::Reloaded => {
475                                // Unconditional — always reload.
476                                remediations.push(Remediation::Service(ServiceApiCall::from(
477                                    ServiceModuleInternalApiCall::Reload(name.clone()),
478                                    privilege.clone(),
479                                )));
480                            }
481                        }
482                    }
483                    OsKind::FreeBsd(_) | OsKind::MacOs(_) | OsKind::Unknown => {
484                        return Err(RegentError::FailedDryRunEvaluation(format!(
485                            "Service management is not supported on {:?}",
486                            os_kind
487                        )));
488                    }
489                }
490            }
491        }
492
493        if remediations.is_empty() {
494            Ok(AttributeComplianceAssessment::Compliant)
495        } else {
496            Ok(AttributeComplianceAssessment::NonCompliant(
497                RemediationsList::from(remediations)?,
498            ))
499        }
500    }
501}
502
503#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
504#[serde(rename_all = "PascalCase")]
505pub enum ServiceModuleInternalApiCall {
506    Start(String),
507    Stop(String),
508    Restart(String),
509    Reload(String),
510    Enable(String),
511    Disable(String),
512}
513
514impl std::fmt::Display for ServiceModuleInternalApiCall {
515    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516        match self {
517            ServiceModuleInternalApiCall::Start(s) => write!(f, "start {}", s),
518            ServiceModuleInternalApiCall::Stop(s) => write!(f, "stop {}", s),
519            ServiceModuleInternalApiCall::Restart(s) => write!(f, "restart {}", s),
520            ServiceModuleInternalApiCall::Reload(s) => write!(f, "reload {}", s),
521            ServiceModuleInternalApiCall::Enable(s) => write!(f, "enable {}", s),
522            ServiceModuleInternalApiCall::Disable(s) => write!(f, "disable {}", s),
523        }
524    }
525}
526
527#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
528pub struct ServiceApiCall {
529    pub api_call: ServiceModuleInternalApiCall,
530    privilege: Privilege,
531}
532
533impl ServiceApiCall {
534    pub fn display(&self) -> String {
535        match &self.api_call {
536            ServiceModuleInternalApiCall::Start(s) => format!("Start service {}", s),
537            ServiceModuleInternalApiCall::Stop(s) => format!("Stop service {}", s),
538            ServiceModuleInternalApiCall::Restart(s) => format!("Restart service {}", s),
539            ServiceModuleInternalApiCall::Reload(s) => format!("Reload service {}", s),
540            ServiceModuleInternalApiCall::Enable(s) => format!("Enable service {}", s),
541            ServiceModuleInternalApiCall::Disable(s) => format!("Disable service {}", s),
542        }
543    }
544
545    fn from(api_call: ServiceModuleInternalApiCall, privilege: Privilege) -> ServiceApiCall {
546        ServiceApiCall {
547            api_call,
548            privilege,
549        }
550    }
551}
552
553impl Check for ServiceApiCall {
554    fn check(&self) -> Result<(), RegentError> {
555        Ok(())
556    }
557
558    fn check_host_compatibility(
559        &self,
560        host_properties: &HostProperties,
561    ) -> Result<(), RegentError> {
562        use crate::hosts::properties::InitSystem;
563        match host_properties.os_kind() {
564            OsKind::Linux(linux_specifics) => match linux_specifics.init_system {
565                InitSystem::Systemd => Ok(()),
566                InitSystem::Unknown => Err(RegentError::IncompatibleHost(
567                    "systemctl requires systemd but init system could not be detected".to_string(),
568                )),
569            },
570            #[cfg(feature = "windows")]
571            OsKind::Windows(_) => Ok(()),
572            incompatible_os_kind => Err(RegentError::IncompatibleHost(format!(
573                "Host is {:?} but service management is only supported on Linux with systemd or Windows (with windows feature)",
574                incompatible_os_kind
575            ))),
576        }
577    }
578}
579
580impl<Handler: HostHandler> ReachCompliance<Handler> for ServiceApiCall {
581    async fn call(
582        &self,
583        host_handler: &mut Handler,
584        host_properties: &Option<HostProperties>,
585        _optional_secret_provider: &Option<SecretProvidersPool>,
586    ) -> Result<InternalApiCallOutcome, RegentError> {
587        // Early check: verify we're on a compatible host
588        if let Some(props) = host_properties {
589            self.check_host_compatibility(props)?;
590        }
591
592        // Determine the effective OS kind - assume Linux if HostProperties is None
593        let os_kind = host_properties
594            .as_ref()
595            .map(|props| props.os_kind())
596            .unwrap_or(&OsKind::Linux(LinuxSpecifics {
597                linux_flavor: LinuxFlavor::Debian,
598                init_system: InitSystem::Systemd,
599            }));
600
601        // Match on OS kind to execute the appropriate command
602        match os_kind {
603            #[cfg(feature = "windows")]
604            OsKind::Windows(_) => {
605                // Build Windows command
606                let cmd = match &self.api_call {
607                    ServiceModuleInternalApiCall::Start(s) => format!("net start {}", s),
608                    ServiceModuleInternalApiCall::Stop(s) => format!("net stop {}", s),
609                    ServiceModuleInternalApiCall::Restart(s) => {
610                        // Windows doesn't have a direct restart command, we stop then start
611                        format!("net stop {} && net start {}", s, s)
612                    }
613                    ServiceModuleInternalApiCall::Reload(s) => {
614                        // Windows doesn't have a direct reload command
615                        // This might not be supported for all services
616                        format!("sc control {} 128", s) // Sends a reload parameter, but not all services support this
617                    }
618                    ServiceModuleInternalApiCall::Enable(s) => {
619                        format!("sc config {} start= auto", s)
620                    }
621                    ServiceModuleInternalApiCall::Disable(s) => {
622                        format!("sc config {} start= disabled", s)
623                    }
624                };
625
626                // Execute Windows command
627                let result = host_handler.run_windows_command(&cmd).await;
628
629                match result {
630                    Ok(result) => {
631                        if result.return_code == 0 {
632                            Ok(InternalApiCallOutcome::Success(None))
633                        } else {
634                            Ok(InternalApiCallOutcome::Failure(format!(
635                                "RC: {}, STDOUT: {}, STDERR: {}",
636                                result.return_code, result.stdout, result.stderr
637                            )))
638                        }
639                    }
640                    Err(e) => Ok(InternalApiCallOutcome::Failure(format!(
641                        "Command execution failed: {:?}",
642                        e
643                    ))),
644                }
645            }
646            OsKind::Linux(_) => {
647                // Build Linux command
648                let cmd = match &self.api_call {
649                    ServiceModuleInternalApiCall::Start(s) => format!("systemctl start {}", s),
650                    ServiceModuleInternalApiCall::Stop(s) => format!("systemctl stop {}", s),
651                    ServiceModuleInternalApiCall::Restart(s) => format!("systemctl restart {}", s),
652                    ServiceModuleInternalApiCall::Reload(s) => format!("systemctl reload {}", s),
653                    ServiceModuleInternalApiCall::Enable(s) => format!("systemctl enable {}", s),
654                    ServiceModuleInternalApiCall::Disable(s) => format!("systemctl disable {}", s),
655                };
656
657                // Execute Linux command
658                let result = host_handler.run_command(&cmd, &self.privilege).await;
659
660                match result {
661                    Ok(result) => {
662                        if result.return_code == 0 {
663                            Ok(InternalApiCallOutcome::Success(None))
664                        } else {
665                            Ok(InternalApiCallOutcome::Failure(format!(
666                                "RC: {}, STDOUT: {}, STDERR: {}",
667                                result.return_code, result.stdout, result.stderr
668                            )))
669                        }
670                    }
671                    Err(e) => Ok(InternalApiCallOutcome::Failure(format!(
672                        "Command execution failed: {:?}",
673                        e
674                    ))),
675                }
676            }
677            OsKind::FreeBsd(_) | OsKind::MacOs(_) | OsKind::Unknown => {
678                Err(RegentError::FailedDryRunEvaluation(format!(
679                    "Service management is not supported on {:?}",
680                    os_kind
681                )))
682            }
683        }
684    }
685}
686
687async fn service_is_active<Handler: HostHandler>(
688    host_handler: &mut Handler,
689    name: &str,
690) -> Result<bool, String> {
691    match host_handler
692        .run_command(&format!("systemctl is-active {}", name), &Privilege::None)
693        .await
694    {
695        Ok(r) => match r.return_code {
696            0 => Ok(true),
697            3 => Ok(false),
698            4 => Err(format!("Service not found: {}", name)),
699            _ => Ok(false), // "failed" or other transient states → not active
700        },
701        Err(e) => Err(format!("Unable to check active state of {}: {:?}", name, e)),
702    }
703}
704
705async fn service_is_enabled<Handler: HostHandler>(
706    host_handler: &mut Handler,
707    name: &str,
708) -> Result<bool, String> {
709    match host_handler
710        .run_command(&format!("systemctl is-enabled {}", name), &Privilege::None)
711        .await
712    {
713        Ok(r) => match r.return_code {
714            0 => Ok(true),
715            1 | 3 => Ok(false),
716            4 => Err(format!("Service not found: {}", name)),
717            _ => Ok(false),
718        },
719        Err(e) => Err(format!(
720            "Unable to check enabled state of {}: {:?}",
721            name, e
722        )),
723    }
724}
725
726#[cfg(feature = "windows")]
727async fn windows_service_is_active<Handler: HostHandler>(
728    host_handler: &mut Handler,
729    name: &str,
730) -> Result<bool, String> {
731    match host_handler
732        .run_windows_command(&format!("sc query {}", name))
733        .await
734    {
735        Ok(r) => {
736            // sc query returns 0 for success, but we need to parse the output
737            // The output contains "STATE" line which shows the service state
738            if r.return_code != 0 {
739                // Service might not exist or other error
740                if r.stdout.contains("does not exist") || r.stderr.contains("does not exist") {
741                    return Err(format!("Service not found: {}", name));
742                }
743                return Ok(false);
744            }
745
746            // Parse the output for service state
747            // Looking for lines like: "STATE              : 4  RUNNING"
748            let output = r.stdout.to_lowercase();
749            if output.contains("running") {
750                Ok(true)
751            } else if output.contains("stopped") || output.contains("pending") {
752                Ok(false)
753            } else {
754                // Default to false if we can't determine the state
755                Ok(false)
756            }
757        }
758        Err(e) => Err(format!("Unable to check active state of {}: {:?}", name, e)),
759    }
760}
761
762#[cfg(feature = "windows")]
763async fn windows_service_is_enabled<Handler: HostHandler>(
764    host_handler: &mut Handler,
765    name: &str,
766) -> Result<bool, String> {
767    match host_handler
768        .run_windows_command(&format!("sc qc {}", name))
769        .await
770    {
771        Ok(r) => {
772            // sc qc (query configuration) returns information about the service
773            // We need to look for the START_TYPE line
774            if r.return_code != 0 {
775                if r.stdout.contains("does not exist") || r.stderr.contains("does not exist") {
776                    return Err(format!("Service not found: {}", name));
777                }
778                return Ok(false);
779            }
780
781            // Parse the output for start type
782            // Looking for lines like: "START_TYPE       : 2   AUTO_START"
783            let output = r.stdout.to_lowercase();
784            if output.contains("auto_start") || output.contains("2") {
785                Ok(true)
786            } else if output.contains("disabled") || output.contains("3") || output.contains("4") {
787                Ok(false)
788            } else {
789                // Default to false if we can't determine
790                Ok(false)
791            }
792        }
793        Err(e) => Err(format!(
794            "Unable to check enabled state of {}: {:?}",
795            name, e
796        )),
797    }
798}
799
800#[cfg(test)]
801mod tests {
802    use super::*;
803
804    #[test]
805    fn parsing_service_module_block_from_yaml_str() {
806        let raw = "---
807- Name: nginx
808  State: Started
809  Enabled: true
810
811- Name: nginx
812  State: Stopped
813  Enabled: false
814
815- Name: nginx
816  Action: Restarted
817
818- Name: nginx
819  Action: Reloaded
820
821- Name: nginx
822  Enabled: true
823        ";
824        let _: Vec<ServiceBlockExpectedState> = yaml_serde::from_str(raw).unwrap();
825    }
826}