proc_daemon/
daemon.rs

1//! Core daemon implementation with builder pattern.
2//!
3//! This module provides the main `Daemon` struct and `DaemonBuilder` for creating
4//! high-performance, resilient daemon services. The builder pattern allows for
5//! flexible configuration while maintaining zero-copy performance characteristics.
6
7use std::future::Future;
8use std::sync::Arc;
9use std::time::Instant;
10use tracing::{debug, error, info, instrument, warn};
11
12use crate::config::Config;
13use crate::error::{Error, Result};
14use crate::shutdown::{ShutdownCoordinator, ShutdownReason};
15use crate::signal::{SignalConfig, SignalHandler};
16use crate::subsystem::{Subsystem, SubsystemId, SubsystemManager};
17
18#[cfg(feature = "config-watch")]
19use arc_swap::ArcSwap;
20#[cfg(feature = "config-watch")]
21use notify::RecommendedWatcher;
22
23/// Type alias for subsystem registration function
24type SubsystemRegistrationFn = Box<dyn FnOnce(&SubsystemManager) -> SubsystemId + Send + 'static>;
25
26/// Main daemon instance that coordinates all subsystems and handles lifecycle.
27pub struct Daemon {
28    /// Configuration
29    config: Arc<Config>,
30    /// Live-updating configuration snapshot (when config-watch is enabled)
31    #[cfg(feature = "config-watch")]
32    config_shared: Arc<ArcSwap<Config>>,
33    /// Shutdown coordination
34    shutdown_coordinator: ShutdownCoordinator,
35    /// Subsystem management
36    subsystem_manager: SubsystemManager,
37    /// Signal handling
38    signal_handler: Option<Arc<SignalHandler>>,
39    /// Keep the config watcher alive (when enabled)
40    #[cfg(feature = "config-watch")]
41    _config_watcher: Option<RecommendedWatcher>,
42    /// Start time
43    started_at: Option<Instant>,
44}
45
46impl Daemon {
47    /// Create a new daemon builder with the provided configuration.
48    #[must_use]
49    pub fn builder(config: Config) -> DaemonBuilder {
50        DaemonBuilder::new(config)
51    }
52
53    /// Create a new daemon with default configuration.
54    ///
55    /// # Errors
56    ///
57    /// Will return an error if the default configuration is invalid.
58    pub fn with_defaults() -> Result<DaemonBuilder> {
59        let config = Config::new()?;
60        Ok(Self::builder(config))
61    }
62
63    /// Run the daemon until shutdown is requested.
64    /// This is the main entry point that starts all subsystems and waits for shutdown.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if logging initialization fails, configuration validation fails,
69    /// subsystem startup fails, or if there is an error during the shutdown sequence.
70    #[instrument(skip(self), fields(daemon_name = %self.config.name))]
71    pub async fn run(mut self) -> Result<()> {
72        info!(daemon_name = %self.config.name, "Starting daemon");
73        self.started_at = Some(Instant::now());
74
75        // Initialize logging
76        self.init_logging()?;
77
78        // Validate configuration
79        self.config.validate()?;
80
81        // Apply optional scheduler hints (no-op placeholders for future tuning)
82        #[cfg(feature = "scheduler-hints")]
83        {
84            crate::scheduler::apply_process_hints(&self.config);
85            crate::scheduler::apply_runtime_hints();
86        }
87
88        // Start all subsystems
89        if let Err(e) = self.subsystem_manager.start_all().await {
90            error!(error = %e, "Failed to start all subsystems");
91            return Err(e);
92        }
93
94        // Start signal handling in the background
95        // Only spawn when a supported async runtime is enabled.
96        #[cfg(any(feature = "tokio", feature = "async-std"))]
97        let signal_task = self.signal_handler.as_ref().map(|signal_handler| {
98            let handler = Arc::clone(signal_handler);
99            Self::spawn_signal_handler(handler)
100        });
101
102        #[cfg(not(any(feature = "tokio", feature = "async-std")))]
103        let _signal_task: Option<()> = None;
104
105        // Wait for shutdown to be initiated
106        info!("Daemon started successfully, waiting for shutdown signal");
107
108        // Main daemon loop - wait for shutdown
109        loop {
110            if self.shutdown_coordinator.is_shutdown() {
111                break;
112            }
113
114            // Check subsystem health periodically
115            if self.config.monitoring.health_checks {
116                let health_results = self.subsystem_manager.run_health_checks();
117                let unhealthy: Vec<_> = health_results
118                    .iter()
119                    .filter(|(_, _, healthy)| !healthy)
120                    .map(|(id, name, _)| (id, name))
121                    .collect();
122
123                if !unhealthy.is_empty() {
124                    warn!("Unhealthy subsystems detected: {:?}", unhealthy);
125                    // Could implement auto-restart logic here
126                }
127            }
128
129            // Sleep for a short interval
130            #[cfg(feature = "tokio")]
131            tokio::time::sleep(self.config.health_check_interval()).await;
132
133            #[cfg(all(feature = "async-std", not(feature = "tokio")))]
134            async_std::task::sleep(self.config.health_check_interval()).await;
135        }
136
137        // Graceful shutdown sequence
138        info!("Shutdown initiated, beginning graceful shutdown");
139
140        // Stop signal handling
141        if let Some(signal_handler) = &self.signal_handler {
142            signal_handler.stop();
143        }
144
145        // Wait for signal handler task to complete
146        #[cfg(any(feature = "tokio", feature = "async-std"))]
147        if let Some(task) = signal_task {
148            #[cfg(feature = "tokio")]
149            {
150                if let Err(e) = task.await {
151                    warn!(error = %e, "Signal handler task failed");
152                }
153            }
154
155            #[cfg(all(feature = "async-std", not(feature = "tokio")))]
156            {
157                if let Err(e) = task.await {
158                    warn!(error = %e, "Signal handler task failed");
159                }
160            }
161        }
162
163        // Stop all subsystems
164        if let Err(e) = self.subsystem_manager.stop_all().await {
165            error!(error = %e, "Failed to stop all subsystems gracefully");
166        }
167
168        // Wait for graceful shutdown with timeout
169        if let Err(e) = self.shutdown_coordinator.wait_for_shutdown().await {
170            warn!(error = %e, "Graceful shutdown timeout exceeded");
171
172            // Wait for force shutdown timeout
173            if let Err(e) = self.shutdown_coordinator.wait_for_force_shutdown().await {
174                error!(error = %e, "Force shutdown timeout exceeded, exiting immediately");
175            }
176        }
177
178        let elapsed = self.started_at.map(|t| t.elapsed());
179        info!(uptime = ?elapsed, "Daemon shutdown complete");
180
181        Ok(())
182    }
183
184    /// Initialize the logging system based on configuration.
185    fn init_logging(&self) -> Result<()> {
186        use tracing_subscriber::fmt::format::FmtSpan;
187        use tracing_subscriber::{EnvFilter, FmtSubscriber};
188
189        let level: tracing::Level = self.config.logging.level.into();
190        let filter = EnvFilter::from_default_env().add_directive(level.into());
191
192        // Configure output format
193        if self.config.is_json_logging() {
194            #[cfg(feature = "json-logs")]
195            {
196                let base_subscriber = FmtSubscriber::builder()
197                    .with_env_filter(filter)
198                    .with_span_events(FmtSpan::CLOSE)
199                    .with_target(true)
200                    .with_thread_ids(true)
201                    .with_thread_names(true);
202
203                let json_subscriber = base_subscriber
204                    .json()
205                    .flatten_event(true)
206                    .with_current_span(false);
207
208                tracing::subscriber::set_global_default(json_subscriber.finish()).map_err(|e| {
209                    Error::config(format!("Failed to initialize JSON logging: {e}"))
210                })?;
211
212                return Ok(()); // Return early as logging is initialized
213            }
214
215            #[cfg(not(feature = "json-logs"))]
216            {
217                return Err(Error::config(
218                    "JSON logging requested but feature not enabled",
219                ));
220            }
221        }
222
223        // Regular non-JSON logging
224        let base_subscriber = FmtSubscriber::builder()
225            .with_env_filter(filter)
226            .with_span_events(FmtSpan::CLOSE)
227            .with_target(true)
228            .with_thread_ids(true)
229            .with_thread_names(true);
230
231        let regular_subscriber = base_subscriber
232            .with_ansi(self.config.is_colored_logging())
233            .compact();
234
235        tracing::subscriber::set_global_default(regular_subscriber.finish())
236            .map_err(|e| Error::config(format!("Failed to initialize logging: {e}")))?;
237
238        debug!(
239            "Logging initialized with level: {:?}",
240            self.config.logging.level
241        );
242        Ok(())
243    }
244
245    /// Spawn the signal handler task.
246    #[cfg(feature = "tokio")]
247    fn spawn_signal_handler(handler: Arc<SignalHandler>) -> tokio::task::JoinHandle<Result<()>> {
248        tokio::spawn(async move { handler.handle_signals().await })
249    }
250
251    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
252    fn spawn_signal_handler(
253        handler: Arc<SignalHandler>,
254    ) -> async_std::task::JoinHandle<Result<()>> {
255        async_std::task::spawn(async move { handler.handle_signals().await })
256    }
257
258    /// Get daemon statistics.
259    pub fn get_stats(&self) -> DaemonStats {
260        let subsystem_stats = self.subsystem_manager.get_stats();
261        let shutdown_stats = self.shutdown_coordinator.get_stats();
262        let total_restarts = subsystem_stats.total_restarts;
263
264        DaemonStats {
265            name: self.config.name.clone(),
266            uptime: self.started_at.map(|t| t.elapsed()),
267            is_shutdown: shutdown_stats.is_shutdown,
268            shutdown_reason: shutdown_stats.reason,
269            subsystem_stats,
270            total_restarts,
271        }
272    }
273
274    /// Request graceful shutdown programmatically.
275    pub fn shutdown(&self) -> bool {
276        self.shutdown_coordinator
277            .initiate_shutdown(ShutdownReason::Requested)
278    }
279
280    /// Check if the daemon is running.
281    pub fn is_running(&self) -> bool {
282        !self.shutdown_coordinator.is_shutdown()
283    }
284
285    /// Get the daemon configuration.
286    pub fn config(&self) -> &Config {
287        &self.config
288    }
289
290    /// Get a snapshot of the current configuration. When the `config-watch` feature
291    /// is enabled and hot-reload is active, this reflects the most recent loaded
292    /// configuration; otherwise it returns the initial configuration.
293    #[cfg(feature = "config-watch")]
294    pub fn config_snapshot(&self) -> Arc<Config> {
295        self.config_shared.load_full()
296    }
297}
298
299impl Clone for Daemon {
300    fn clone(&self) -> Self {
301        Self {
302            config: Arc::clone(&self.config),
303            #[cfg(feature = "config-watch")]
304            config_shared: Arc::clone(&self.config_shared),
305            shutdown_coordinator: self.shutdown_coordinator.clone(),
306            subsystem_manager: self.subsystem_manager.clone(),
307            signal_handler: self.signal_handler.clone(),
308            #[cfg(feature = "config-watch")]
309            _config_watcher: None,
310            started_at: self.started_at,
311        }
312    }
313}
314
315/// Statistics about the daemon's current state.
316#[derive(Debug, Clone)]
317pub struct DaemonStats {
318    /// Daemon name
319    pub name: String,
320    /// Time since daemon started
321    pub uptime: Option<std::time::Duration>,
322    /// Whether shutdown has been initiated
323    pub is_shutdown: bool,
324    /// Reason for shutdown (if any)
325    pub shutdown_reason: Option<crate::shutdown::ShutdownReason>,
326    /// Subsystem statistics
327    pub subsystem_stats: crate::subsystem::SubsystemStats,
328    /// Total number of subsystem restarts
329    pub total_restarts: u64,
330}
331
332/// Builder for creating daemon instances with fluent API.
333pub struct DaemonBuilder {
334    config: Config,
335    // Pre-allocate the vector with a reasonable capacity
336    subsystems: Vec<SubsystemRegistrationFn>,
337    signal_config: Option<SignalConfig>,
338    enable_signals: bool,
339}
340
341impl DaemonBuilder {
342    /// Create a new daemon builder with the provided configuration.
343    #[must_use]
344    pub fn new(config: Config) -> Self {
345        Self {
346            config,
347            // Pre-allocate the subsystems vector with a reasonable capacity
348            subsystems: Vec::with_capacity(16),
349            signal_config: None,
350            enable_signals: true,
351        }
352    }
353
354    /// Configure signal handling.
355    #[must_use]
356    pub fn with_signal_config(mut self, config: SignalConfig) -> Self {
357        self.signal_config = Some(config);
358        self
359    }
360
361    /// Disable signal handling.
362    #[must_use]
363    pub const fn without_signals(mut self) -> Self {
364        self.enable_signals = false;
365        self
366    }
367
368    /// Enable only specific signals.
369    #[must_use]
370    pub fn with_signals(mut self, sigterm: bool, sigint: bool) -> Self {
371        let mut config = SignalConfig::new();
372        if !sigterm {
373            config = config.without_sigterm();
374        }
375        if !sigint {
376            config = config.without_sigint();
377        }
378        self.signal_config = Some(config);
379        self
380    }
381
382    /// Add a task that will be run as part of the daemon.
383    ///
384    /// A task is a function that will be executed repeatedly until shutdown is requested.
385    ///
386    /// # Arguments
387    ///
388    /// * `name` - Name of the task for identification
389    /// * `task_fn` - Function that implements the task logic
390    ///
391    /// # Returns
392    ///
393    /// Updated builder instance
394    #[must_use]
395    pub fn with_task<F, Fut>(mut self, name: &str, task_fn: F) -> Self
396    where
397        F: Fn(crate::shutdown::ShutdownHandle) -> Fut + Send + Sync + 'static,
398        Fut: Future<Output = Result<()>> + Send + 'static,
399    {
400        // Clone the name to avoid lifetime issues
401        let name = name.to_string();
402        let subsystem_fn = Box::new(move |subsystem_manager: &SubsystemManager| {
403            subsystem_manager.register_fn(&name, task_fn)
404        });
405
406        self.subsystems.push(subsystem_fn);
407        self
408    }
409
410    /// Add a subsystem that will be managed by the daemon.
411    ///
412    /// A subsystem is a component that implements the `Subsystem` trait.
413    ///
414    /// # Arguments
415    ///
416    /// * `subsystem` - The subsystem to add
417    ///
418    /// # Returns
419    ///
420    /// Updated builder instance
421    #[must_use]
422    pub fn with_subsystem<S>(mut self, subsystem: S) -> Self
423    where
424        S: Subsystem + Send + Sync + 'static,
425    {
426        let subsystem_fn = Box::new(move |subsystem_manager: &SubsystemManager| {
427            subsystem_manager.register(subsystem)
428        });
429
430        self.subsystems.push(subsystem_fn);
431        self
432    }
433
434    /// Add a subsystem using a registration function.
435    ///
436    /// This is a lower-level method that gives direct access to the `SubsystemManager`
437    /// for registration. It's useful when you need more control over the registration process.
438    ///
439    /// # Arguments
440    ///
441    /// * `name` - Name for identification in logs
442    /// * `register_fn` - Function that handles the subsystem registration
443    ///
444    /// # Returns
445    ///
446    /// Updated builder instance
447    #[must_use]
448    pub fn with_subsystem_fn<F>(mut self, name: &str, register_fn: F) -> Self
449    where
450        F: FnOnce(&SubsystemManager) -> SubsystemId + Send + 'static,
451    {
452        debug!("Adding subsystem registration function for {}", name);
453        self.subsystems.push(Box::new(register_fn));
454        self
455    }
456
457    /// Build the daemon instance.
458    /// Builds a daemon from the configured builder
459    ///
460    /// # Errors
461    ///
462    /// Returns an error if the daemon configuration is invalid or if required components cannot be initialized
463    pub fn build(self) -> Result<Daemon> {
464        // Validate configuration
465        self.config.validate()?;
466
467        // Create shutdown coordinator
468        let shutdown_coordinator =
469            ShutdownCoordinator::new(self.config.shutdown.force, self.config.shutdown.kill);
470
471        // Create subsystem manager
472        let subsystem_manager = SubsystemManager::new(shutdown_coordinator.clone());
473
474        // Register all subsystems
475        for subsystem_fn in self.subsystems {
476            let id = subsystem_fn(&subsystem_manager);
477            debug!(subsystem_id = id, "Registered subsystem");
478        }
479
480        // Create signal handler if enabled
481        let signal_handler = if self.enable_signals {
482            Some(Arc::new(SignalHandler::new(shutdown_coordinator.clone())))
483        } else {
484            None
485        };
486
487        // Prepare configuration arcs
488        let config_arc = Arc::new(self.config);
489
490        #[cfg(feature = "config-watch")]
491        let config_shared: Arc<ArcSwap<Config>> = Arc::new(ArcSwap::from(config_arc.clone()));
492
493        // Optionally start config watcher when hot_reload is enabled
494        #[cfg(feature = "config-watch")]
495        let mut config_watcher: Option<RecommendedWatcher> = None;
496
497        #[cfg(feature = "config-watch")]
498        {
499            if config_arc.hot_reload {
500                let swap = Arc::clone(&config_shared);
501                match Config::watch_file(crate::DEFAULT_CONFIG_FILE, move |res| match res {
502                    Ok(new_cfg) => {
503                        swap.store(Arc::new(new_cfg));
504                        info!(
505                            "Configuration hot-reloaded from {}",
506                            crate::DEFAULT_CONFIG_FILE
507                        );
508                    }
509                    Err(e) => {
510                        warn!(error = %e, "Configuration reload failed");
511                    }
512                }) {
513                    Ok(w) => {
514                        config_watcher = Some(w);
515                        info!("Config watcher started for {}", crate::DEFAULT_CONFIG_FILE);
516                    }
517                    Err(e) => {
518                        warn!(error = %e, "Failed to start config watcher; continuing without hot-reload");
519                    }
520                }
521            }
522        }
523
524        Ok(Daemon {
525            config: config_arc,
526            #[cfg(feature = "config-watch")]
527            config_shared,
528            shutdown_coordinator,
529            subsystem_manager,
530            signal_handler,
531            #[cfg(feature = "config-watch")]
532            _config_watcher: config_watcher,
533            started_at: None,
534        })
535    }
536
537    /// Build and run the daemon in one step.
538    /// Runs the daemon until completion or error
539    ///
540    /// # Errors
541    ///
542    /// Returns an error if the daemon encounters an unrecoverable error during execution
543    pub async fn run(self) -> Result<()> {
544        let daemon = self.build()?;
545        daemon.run().await
546    }
547}
548
549/// Convenience macro for creating subsystems from closures.
550#[macro_export]
551macro_rules! subsystem {
552    ($name:expr, $closure:expr) => {
553        Box::new(move |shutdown: $crate::shutdown::ShutdownHandle| {
554            Box::pin($closure(shutdown)) as Pin<Box<dyn Future<Output = $crate::Result<()>> + Send>>
555        })
556    };
557}
558
559/// Convenience macro for creating simple task-based subsystems.
560#[macro_export]
561macro_rules! task {
562    ($name:expr, $body:expr) => {
563        |shutdown: $crate::shutdown::ShutdownHandle| async move {
564            #[cfg(feature = "tokio")]
565            let mut shutdown = shutdown;
566            loop {
567                #[cfg(feature = "tokio")]
568                {
569                    tokio::select! {
570                        _ = shutdown.cancelled() => {
571                            tracing::info!("Task '{}' shutting down", $name);
572                            break;
573                        }
574                        _ = async { $body } => {}
575                    }
576                }
577
578                #[cfg(all(feature = "async-std", not(feature = "tokio")))]
579                {
580                    if shutdown.is_shutdown() {
581                        tracing::info!("Task '{}' shutting down", $name);
582                        break;
583                    }
584                    // Execute the body directly without awaiting
585                    $body;
586                    // Add a small delay to prevent tight loop
587                    async_std::task::sleep(std::time::Duration::from_millis(10)).await;
588                }
589            }
590            Ok(())
591        }
592    };
593}
594
595#[cfg(test)]
596mod tests {
597    use super::*;
598    use std::pin::Pin;
599    use std::time::Duration;
600
601    async fn test_subsystem(shutdown: crate::shutdown::ShutdownHandle) -> Result<()> {
602        #[cfg(feature = "tokio")]
603        let mut shutdown = shutdown;
604        loop {
605            #[cfg(feature = "tokio")]
606            {
607                tokio::select! {
608                    () = shutdown.cancelled() => break,
609                    () = tokio::time::sleep(Duration::from_millis(10)) => {}
610                }
611            }
612
613            #[cfg(all(feature = "async-std", not(feature = "tokio")))]
614            {
615                if shutdown.is_shutdown() {
616                    break;
617                }
618                async_std::task::sleep(Duration::from_millis(10)).await;
619            }
620        }
621        Ok(())
622    }
623
624    #[cfg(feature = "tokio")]
625    #[cfg_attr(miri, ignore)]
626    #[tokio::test]
627    async fn test_daemon_builder() {
628        // Add a test timeout to prevent freezing
629        let test_result = tokio::time::timeout(Duration::from_secs(5), async {
630            let config = Config::new().unwrap();
631            let daemon = Daemon::builder(config)
632                .with_subsystem_fn("test", |subsystem_manager| {
633                    subsystem_manager.register_fn("test_subsystem", test_subsystem)
634                })
635                .build()
636                .unwrap();
637
638            assert!(daemon.is_running());
639            assert_eq!(daemon.config().name, "proc-daemon");
640
641            // Ensure proper cleanup
642            daemon.shutdown();
643        })
644        .await;
645
646        assert!(test_result.is_ok(), "Test timed out after 5 seconds");
647    }
648
649    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
650    #[async_std::test]
651    async fn test_daemon_builder() {
652        // Add a test timeout to prevent freezing
653        let test_result = async_std::future::timeout(Duration::from_secs(5), async {
654            let config = Config::new().unwrap();
655            let daemon = Daemon::builder(config)
656                .with_subsystem_fn("test", |subsystem_manager| {
657                    subsystem_manager.register_fn("test_subsystem", test_subsystem)
658                })
659                .build()
660                .unwrap();
661
662            assert!(daemon.is_running());
663            assert_eq!(daemon.config().name, "proc-daemon");
664
665            // Ensure proper cleanup
666            daemon.shutdown();
667        })
668        .await;
669
670        assert!(test_result.is_ok(), "Test timed out after 5 seconds");
671    }
672
673    #[cfg(feature = "tokio")]
674    #[cfg_attr(miri, ignore)]
675    #[tokio::test]
676    async fn test_daemon_with_defaults() {
677        // Add a test timeout to prevent freezing
678        let test_result = tokio::time::timeout(Duration::from_secs(5), async {
679            let builder = Daemon::with_defaults().unwrap();
680            let daemon = builder
681                .with_task("simple_task", |_shutdown| async {
682                    tokio::time::sleep(Duration::from_millis(10)).await;
683                    Ok(())
684                })
685                .build()
686                .unwrap();
687
688            assert!(daemon.is_running());
689
690            // Ensure proper cleanup
691            daemon.shutdown();
692        })
693        .await;
694
695        assert!(test_result.is_ok(), "Test timed out after 5 seconds");
696    }
697
698    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
699    #[async_std::test]
700    async fn test_daemon_with_defaults() {
701        // Add a test timeout to prevent freezing
702        let test_result = async_std::future::timeout(Duration::from_secs(5), async {
703            let builder = Daemon::with_defaults().unwrap();
704            let daemon = builder
705                .with_task("simple_task", |_shutdown| async {
706                    async_std::task::sleep(Duration::from_millis(10)).await;
707                    Ok(())
708                })
709                .build()
710                .unwrap();
711
712            assert!(daemon.is_running());
713
714            // Ensure proper cleanup
715            daemon.shutdown();
716        })
717        .await;
718
719        assert!(test_result.is_ok(), "Test timed out after 5 seconds");
720    }
721
722    #[cfg(feature = "tokio")]
723    #[cfg_attr(miri, ignore)]
724    #[tokio::test]
725    async fn test_daemon_shutdown() {
726        // Add a test timeout to prevent freezing
727        let test_result = tokio::time::timeout(Duration::from_secs(5), async {
728            let config = Config::builder()
729                .name("test-daemon")
730                .shutdown_timeout(Duration::from_millis(100))
731                .unwrap()
732                .build()
733                .unwrap();
734
735            let daemon = Daemon::builder(config)
736                .with_subsystem_fn("test", |subsystem_manager| {
737                    subsystem_manager.register_fn("test_subsystem", test_subsystem)
738                })
739                .without_signals()
740                .build()
741                .unwrap();
742
743            // Request shutdown
744            daemon.shutdown();
745            assert!(!daemon.is_running());
746        })
747        .await;
748
749        assert!(test_result.is_ok(), "Test timed out after 5 seconds");
750    }
751
752    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
753    #[async_std::test]
754    async fn test_daemon_shutdown() {
755        // Add a test timeout to prevent freezing
756        let test_result = async_std::future::timeout(Duration::from_secs(5), async {
757            let config = Config::builder()
758                .name("test-daemon")
759                .shutdown_timeout(Duration::from_millis(100))
760                .unwrap()
761                .build()
762                .unwrap();
763
764            let daemon = Daemon::builder(config)
765                .with_subsystem_fn("test", |subsystem_manager| {
766                    subsystem_manager.register_fn("test_subsystem", test_subsystem)
767                })
768                .without_signals()
769                .build()
770                .unwrap();
771
772            // Request shutdown
773            daemon.shutdown();
774            assert!(!daemon.is_running());
775        })
776        .await;
777
778        assert!(test_result.is_ok(), "Test timed out after 5 seconds");
779    }
780
781    #[test]
782    fn test_daemon_stats() {
783        let config = Config::new().unwrap();
784        let daemon = Daemon::builder(config).build().unwrap();
785
786        let stats = daemon.get_stats();
787        assert_eq!(stats.name, "proc-daemon");
788        assert!(stats.uptime.is_none()); // Not started yet
789        assert!(!stats.is_shutdown);
790    }
791
792    struct TestSubsystemStruct {
793        name: String,
794    }
795
796    impl TestSubsystemStruct {
797        fn new(name: &str) -> Self {
798            Self {
799                name: name.to_string(),
800            }
801        }
802    }
803
804    impl Subsystem for TestSubsystemStruct {
805        fn run(
806            &self,
807            shutdown: crate::shutdown::ShutdownHandle,
808        ) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> {
809            Box::pin(async move {
810                #[cfg(feature = "tokio")]
811                let mut shutdown = shutdown;
812                loop {
813                    #[cfg(feature = "tokio")]
814                    {
815                        tokio::select! {
816                            () = shutdown.cancelled() => break,
817                            () = tokio::time::sleep(Duration::from_millis(10)) => {}
818                        }
819                    }
820
821                    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
822                    {
823                        if shutdown.is_shutdown() {
824                            break;
825                        }
826                        async_std::task::sleep(Duration::from_millis(10)).await;
827                    }
828                }
829                Ok(())
830            })
831        }
832
833        fn name(&self) -> &str {
834            &self.name
835        }
836    }
837
838    #[cfg(feature = "tokio")]
839    #[cfg_attr(miri, ignore)]
840    #[tokio::test]
841    async fn test_daemon_with_struct_subsystem() {
842        // Add a test timeout to prevent freezing
843        let test_result = tokio::time::timeout(Duration::from_secs(5), async {
844            let config = Config::new().unwrap();
845            let subsystem = TestSubsystemStruct::new("struct_test");
846
847            let daemon = Daemon::builder(config)
848                .with_subsystem(subsystem)
849                .without_signals()
850                .build()
851                .unwrap();
852
853            let stats = daemon.get_stats();
854            assert_eq!(stats.subsystem_stats.total_subsystems, 1);
855
856            // Ensure proper cleanup
857            daemon.shutdown();
858        })
859        .await;
860
861        assert!(test_result.is_ok(), "Test timed out after 5 seconds");
862    }
863
864    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
865    #[async_std::test]
866    async fn test_daemon_with_struct_subsystem() {
867        // Add a test timeout to prevent freezing
868        let test_result = async_std::future::timeout(Duration::from_secs(5), async {
869            let config = Config::new().unwrap();
870            let subsystem = TestSubsystemStruct::new("struct_test");
871
872            let daemon = Daemon::builder(config)
873                .with_subsystem(subsystem)
874                .without_signals()
875                .build()
876                .unwrap();
877
878            let stats = daemon.get_stats();
879            assert_eq!(stats.subsystem_stats.total_subsystems, 1);
880
881            // Ensure proper cleanup
882            daemon.shutdown();
883        })
884        .await;
885
886        assert!(test_result.is_ok(), "Test timed out after 5 seconds");
887    }
888
889    #[cfg(feature = "tokio")]
890    #[cfg_attr(miri, ignore)]
891    #[tokio::test]
892    async fn test_daemon_signal_configuration() {
893        // Add a test timeout to prevent freezing
894        let test_result = tokio::time::timeout(Duration::from_secs(5), async {
895            let config = Config::new().unwrap();
896            let signal_config = SignalConfig::new().with_sighup().without_sigint();
897
898            let daemon = Daemon::builder(config)
899                .with_signal_config(signal_config)
900                .build()
901                .unwrap();
902
903            assert!(daemon.signal_handler.is_some());
904
905            // Ensure proper cleanup
906            daemon.shutdown();
907        })
908        .await;
909
910        assert!(test_result.is_ok(), "Test timed out after 5 seconds");
911    }
912
913    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
914    #[async_std::test]
915    async fn test_daemon_signal_configuration() {
916        // Add a test timeout to prevent freezing
917        let test_result = async_std::future::timeout(Duration::from_secs(5), async {
918            let config = Config::new().unwrap();
919            let signal_config = SignalConfig::new().with_sighup().without_sigint();
920
921            let daemon = Daemon::builder(config)
922                .with_signal_config(signal_config)
923                .build()
924                .unwrap();
925
926            assert!(daemon.signal_handler.is_some());
927
928            // Ensure proper cleanup
929            daemon.shutdown();
930        })
931        .await;
932
933        assert!(test_result.is_ok(), "Test timed out after 5 seconds");
934    }
935
936    #[cfg(feature = "tokio")]
937    #[cfg_attr(miri, ignore)]
938    #[tokio::test]
939    async fn test_macro_usage() {
940        // Add a test timeout to prevent freezing
941        let test_result = tokio::time::timeout(Duration::from_secs(5), async {
942            let config = Config::new().unwrap();
943
944            let daemon = Daemon::builder(config)
945                .with_task(
946                    "macro_test",
947                    task!("macro_test", {
948                        tokio::time::sleep(Duration::from_millis(1)).await;
949                    }),
950                )
951                .without_signals()
952                .build()
953                .unwrap();
954
955            let stats = daemon.get_stats();
956            assert_eq!(stats.subsystem_stats.total_subsystems, 1);
957
958            // Ensure proper cleanup
959            daemon.shutdown();
960        })
961        .await;
962
963        assert!(test_result.is_ok(), "Test timed out after 5 seconds");
964    }
965
966    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
967    #[async_std::test]
968    async fn test_macro_usage() {
969        // Add a test timeout to prevent freezing
970        let test_result = async_std::future::timeout(Duration::from_secs(5), async {
971            let config = Config::new().unwrap();
972
973            let daemon = Daemon::builder(config)
974                .with_task(
975                    "macro_test",
976                    task!("macro_test", {
977                        async_std::task::sleep(Duration::from_millis(1)).await;
978                    }),
979                )
980                .without_signals()
981                .build()
982                .unwrap();
983
984            let stats = daemon.get_stats();
985            assert_eq!(stats.subsystem_stats.total_subsystems, 1);
986
987            // Ensure proper cleanup
988            daemon.shutdown();
989        })
990        .await;
991
992        assert!(test_result.is_ok(), "Test timed out after 5 seconds");
993    }
994}