Skip to main content

AirLibrary/Daemon/
mod.rs

1//! # Daemon Lifecycle Management
2//!
3//! This module provides comprehensive daemon lifecycle management for the Air
4//! daemon service, responsible for managing background processes in the Land
5//! code editor ecosystem.
6//!
7//! ## Architecture Overview
8//!
9//! The daemon follows VSCode's daemon architecture pattern:
10//! - Reference: VSCode service management
11//!   (Dependency/Microsoft/Editor/src/vs/base/node/processexitorutility)
12//! - Singleton enforcement through PID file locking
13//! - Platform-native service integration (systemd, launchd, Windows Service)
14//! - Graceful shutdown coordination with Mountain (main editor process)
15//! - Resource cleanup and state persistence across restarts
16//!
17//! ## Core Responsibilities
18//!
19//! 1. **Process Management**
20//!    - PID file creation, validation, and cleanup
21//!    - Checksum-based PID file integrity verification
22//!    - Process existence validation and stale detection
23//!    - Race condition protection for lock acquisition
24//!    - Timeout handling for all async operations
25//!
26//! 2. **Service Installation**
27//!    - systemd service generation and installation (Linux)
28//!    - launchd plist generation and installation (macOS)
29//!    - Windows Service registration (Windows using winsvc)
30//!    - Service validation and health checks
31//!    - Post-installation verification
32//!
33//! 3. **Lifecycle Coordination**
34//!    - Lock acquisition with atomic operations
35//!    - Graceful shutdown signals
36//!    - Resource cleanup on errors
37//!    - State persistence and recovery
38//!
39//! 4. **Platform Integration**
40//!    - Linux: systemd socket activation support
41//!    - macOS: launchd session management
42//!    - Windows: Windows Service API integration
43//!    - Cross-platform log rotation
44//!
45//! ## FUTURE Enhancements
46//!
47//! - [ ] Implement Windows winsvc integration for actual service registration
48//! - [ ] Add systemd socket activation support
49//! - [ ] Implement daemon auto-update notifications
50//! - [ ] Add crash recovery and state restoration
51//! - [ ] Implement daemon health monitoring with metrics
52//! - [ ] Add log rotation for daemon logs
53//! - [ ] Implement daemon upgrade path (in-place hot reload)
54//! - [ ] Add daemon configuration reloading without restart
55//! - [ ] Implement grace period for Mountain shutdown coordination
56//! - [ ] Add daemon sandbox support for security isolation
57//! ## Platform-Specific Considerations
58//!
59//! ### Linux (systemd)
60//! - PID file location: `/var/run/Air.pid`
61//! - Service file: `/etc/systemd/system/Air-daemon.service`
62//! - Requires root privileges for installation
63//! - Supports socket activation and notify-ready
64//!
65//! ### macOS (launchd)
66//! - PID file location: `/tmp/Air.pid`
67//! - Service file: `/Library/LaunchDaemons/Air-daemon.plist`
68//! - Requires root privileges for system daemon
69//! - Supports launchctl unload/start/stop commands
70//!
71//! ### Windows
72//! - PID file location: `C:\ProgramData\Air\Air.pid`
73//! - Service registration via SCManager API
74//! - Requires Administrator privileges
75//! - Uses winsvc crate or similar for service management
76//!
77//! ## Security Considerations
78//!
79//! - PID file protected with checksum to prevent tampering
80//! - Directory creation with secure permissions (0700)
81//! - SUID/SGID not used for security
82//! - User-level isolation for multi-user systems
83//!
84//! ## Error Handling
85//!
86//! All operations return `Result<T>` with comprehensive error types:
87//! - `ServiceUnavailable`: Daemon already running or unavailable
88//! - `FileSystem`: PID file or directory operations failed
89//! - `PermissionDenied`: Insufficient privileges for service operations
90
91use std::{fs, path::PathBuf, sync::Arc, time::Duration};
92
93use tokio::sync::{Mutex, RwLock};
94use sha2::{Digest, Sha256};
95
96use crate::{AirError, Result, dev_log};
97
98/// Daemon lifecycle manager
99#[derive(Debug)]
100pub struct DaemonManager {
101	/// PID file path
102	PidFilePath:PathBuf,
103	/// Whether daemon is running
104	IsRunning:Arc<RwLock<bool>>,
105	/// Platform-specific daemon info
106	PlatformInfo:PlatformInfo,
107	/// Lock for atomic PID file operations (prevents race conditions)
108	PidLock:Arc<Mutex<()>>,
109	/// Checksum for PID file integrity verification
110	PidChecksum:Arc<Mutex<Option<String>>>,
111	/// Graceful shutdown flag
112	ShutdownRequested:Arc<RwLock<bool>>,
113}
114
115/// Platform-specific daemon information
116#[derive(Debug)]
117pub struct PlatformInfo {
118	/// Platform type
119	pub Platform:Platform,
120	/// Service name for system integration
121	pub ServiceName:String,
122	/// User under which daemon runs
123	pub RunAsUser:Option<String>,
124}
125
126/// Platform enum
127#[derive(Debug, Clone, PartialEq)]
128pub enum Platform {
129	Linux,
130	MacOS,
131	Windows,
132	Unknown,
133}
134
135/// Exit codes for daemon operations
136#[derive(Debug, Clone)]
137pub enum ExitCode {
138	Success = 0,
139	ConfigurationError = 1,
140	AlreadyRunning = 2,
141	PermissionDenied = 3,
142	ServiceError = 4,
143	ResourceError = 5,
144	NetworkError = 6,
145	AuthenticationError = 7,
146	FileSystemError = 8,
147	InternalError = 9,
148	UnknownError = 10,
149}
150
151impl DaemonManager {
152	/// Create a new DaemonManager instance
153	pub fn New(PidFilePath:Option<PathBuf>) -> Result<Self> {
154		let PidFilePath = PidFilePath.unwrap_or_else(|| Self::DefaultPidFilePath());
155		let PlatformInfo = Self::DetectPlatformInfo();
156
157		Ok(Self {
158			PidFilePath,
159			IsRunning:Arc::new(RwLock::new(false)),
160			PlatformInfo,
161			PidLock:Arc::new(Mutex::new(())),
162			PidChecksum:Arc::new(Mutex::new(None)),
163			ShutdownRequested:Arc::new(RwLock::new(false)),
164		})
165	}
166
167	/// Get default PID file path based on platform
168	fn DefaultPidFilePath() -> PathBuf {
169		let platform = Self::DetectPlatform();
170		match platform {
171			Platform::Linux => PathBuf::from("/var/run/Air.pid"),
172			Platform::MacOS => PathBuf::from("/tmp/Air.pid"),
173			Platform::Windows => PathBuf::from("C:\\ProgramData\\Air\\Air.pid"),
174			Platform::Unknown => PathBuf::from("./Air.pid"),
175		}
176	}
177
178	/// Detect current platform
179	fn DetectPlatform() -> Platform {
180		if cfg!(target_os = "linux") {
181			Platform::Linux
182		} else if cfg!(target_os = "macos") {
183			Platform::MacOS
184		} else if cfg!(target_os = "windows") {
185			Platform::Windows
186		} else {
187			Platform::Unknown
188		}
189	}
190
191	/// Detect platform-specific information
192	fn DetectPlatformInfo() -> PlatformInfo {
193		let platform = Self::DetectPlatform();
194		let ServiceName = "Air-daemon".to_string();
195
196		// Get current user
197		let RunAsUser = std::env::var("USER").ok().or_else(|| std::env::var("USERNAME").ok());
198
199		PlatformInfo { Platform:platform, ServiceName, RunAsUser }
200	}
201
202	/// Acquire daemon lock to ensure single instance
203	/// This method provides comprehensive defensive coding with:
204	/// - Race condition protection through mutex locking
205	/// - PID file checksum verification
206	/// - Process validation checks
207	/// - Atomic operations with rollback on failure
208	/// - Timeout handling
209	pub async fn AcquireLock(&self) -> Result<()> {
210		dev_log!("daemon", "[Daemon] Acquiring daemon lock...");
211		// Acquire lock to prevent race conditions
212		tokio::select! {
213			_ = tokio::time::timeout(Duration::from_secs(30), self.PidLock.lock()) => {
214				let _lock_guard = self.PidLock.lock().await;
215			},
216			_ = tokio::time::sleep(Duration::from_secs(30)) => {
217				return Err(AirError::Internal(
218					"Timeout acquiring PID lock".to_string()
219				));
220			}
221		}
222
223		let _lock = self.PidLock.lock().await;
224
225		// Check if shutdown has been requested
226		if *self.ShutdownRequested.read().await {
227			return Err(AirError::ServiceUnavailable(
228				"Shutdown requested, cannot acquire lock".to_string(),
229			));
230		}
231
232		// Check if PID file exists and process is running with validation
233		if self.IsAlreadyRunning().await? {
234			return Err(AirError::ServiceUnavailable("Air daemon is already running".to_string()));
235		}
236
237		// Create PID directory with secure permissions if it doesn't exist
238		let TempDir = PathBuf::from(format!("{}.tmp", self.PidFilePath.display()));
239		if let Some(parent) = self.PidFilePath.parent() {
240			fs::create_dir_all(parent)
241				.map_err(|e| AirError::FileSystem(format!("Failed to create PID directory: {}", e)))?;
242
243			// Set secure permissions on directory (user only)
244			#[cfg(unix)]
245			{
246				use std::os::unix::fs::PermissionsExt;
247				let perms = fs::Permissions::from_mode(0o700);
248				fs::set_permissions(parent, perms)
249					.map_err(|e| AirError::FileSystem(format!("Failed to set directory permissions: {}", e)))?;
250			}
251		}
252
253		// Generate PID content with checksum for validation
254		let pid = std::process::id();
255		let timestamp = std::time::SystemTime::now()
256			.duration_since(std::time::UNIX_EPOCH)
257			.unwrap()
258			.as_secs();
259		let PidContent = format!("{}|{}", pid, timestamp);
260
261		// Calculate checksum for integrity verification
262		let mut hasher = Sha256::new();
263		hasher.update(PidContent.as_bytes());
264		let checksum = format!("{:x}", hasher.finalize());
265
266		// Write to temporary file first (atomic operation)
267		let TempFileContent = format!("{}|CHECKSUM:{}", PidContent, checksum);
268		fs::write(&TempDir, &TempFileContent)
269			.map_err(|e| AirError::FileSystem(format!("Failed to write temporary PID file: {}", e)))?;
270
271		// Atomic rename to avoid partial writes
272		#[cfg(unix)]
273		fs::rename(&TempDir, &self.PidFilePath).map_err(|e| {
274			// Rollback: clean up temp file on failure
275			let _ = fs::remove_file(&TempDir);
276			AirError::FileSystem(format!("Failed to rename PID file: {}", e))
277		})?;
278
279		#[cfg(not(unix))]
280		fs::rename(&TempDir, &self.PidFilePath).map_err(|e| {
281			let _ = fs::remove_file(&TempDir);
282			AirError::FileSystem(format!("Failed to rename PID file: {}", e))
283		})?;
284
285		// Store checksum for later validation
286		*self.PidChecksum.lock().await = Some(checksum);
287
288		// Set running state
289		*self.IsRunning.write().await = true;
290
291		// Set secure permissions on PID file
292		#[cfg(unix)]
293		{
294			use std::os::unix::fs::PermissionsExt;
295			let perms = fs::Permissions::from_mode(0o600);
296			if let Err(e) = fs::set_permissions(&self.PidFilePath, perms) {
297				dev_log!("daemon", "warn: [Daemon] Failed to set PID file permissions: {}", e);
298			}
299		}
300
301		dev_log!("daemon", "[Daemon] Daemon lock acquired (PID: {})", pid);
302		Ok(())
303	}
304
305	/// Check if daemon is already running
306	/// Performs comprehensive validation including:
307	/// - PID file existence check
308	/// - Checksum verification
309	/// - Process existence validation
310	/// - Stale PID file cleanup
311	pub async fn IsAlreadyRunning(&self) -> Result<bool> {
312		if !self.PidFilePath.exists() {
313			dev_log!("daemon", "[Daemon] PID file does not exist");
314			return Ok(false);
315		}
316
317		// Read PID from file
318		let PidContent = fs::read_to_string(&self.PidFilePath)
319			.map_err(|e| AirError::FileSystem(format!("Failed to read PID file: {}", e)))?;
320
321		// Parse PID content with checksum
322		let parts:Vec<&str> = PidContent.split('|').collect();
323		if parts.len() < 2 {
324			dev_log!("daemon", "warn: [Daemon] Invalid PID file format, treating as stale");
325			self.CleanupStalePidFile().await?;
326			return Ok(false);
327		}
328
329		let pid:u32 = parts[0].trim().parse().map_err(|e| {
330			dev_log!("daemon", "warn: [Daemon] Invalid PID in file: {}", e);
331			AirError::FileSystem("Invalid PID file content".to_string())
332		})?;
333
334		// Verify checksum if present
335		if parts.len() >= 3 && parts[1].starts_with("CHECKSUM:") {
336			let StoredChecksum = &parts[1][9..]; // Remove "CHECKSUM:" prefix
337			let CurrentChecksum = self.PidChecksum.lock().await;
338
339			if let Some(ref cksum) = *CurrentChecksum {
340				if cksum != StoredChecksum {
341					dev_log!("daemon", "warn: [Daemon] PID file checksum mismatch, file may be corrupted"); // Don't automatically delete - could be a different daemon instance
342					return Ok(true);
343				}
344			}
345		}
346
347		// Check if process exists with validation
348		let IsRunning = Self::ValidateProcess(pid);
349
350		if !IsRunning {
351			// Clean up stale PID file with validation
352			dev_log!("daemon", "warn: [Daemon] Detected stale PID file for PID {}", pid);
353			self.CleanupStalePidFile().await?;
354		}
355
356		Ok(IsRunning)
357	}
358
359	/// Validate that a process with the given PID is running
360	/// Performs thorough process validation and existence checks
361	fn ValidateProcess(pid:u32) -> bool {
362		#[cfg(unix)]
363		{
364			use std::process::Command;
365			let output = Command::new("ps").arg("-p").arg(pid.to_string()).output();
366
367			match output {
368				Ok(output) => {
369					if output.status.success() {
370						let stdout = String::from_utf8_lossy(&output.stdout);
371						// Validate it's actually an Air daemon process
372						stdout
373							.lines()
374							.skip(1)
375							.any(|line| line.contains("Air") || line.contains("daemon"))
376					} else {
377						false
378					}
379				},
380				Err(e) => {
381					dev_log!("daemon", "error: [Daemon] Failed to check process status: {}", e);
382					false
383				},
384			}
385		}
386
387		#[cfg(windows)]
388		{
389			use std::process::Command;
390			let output = Command::new("tasklist")
391				.arg("/FI")
392				.arg(format!("PID eq {}", pid))
393				.arg("/FO")
394				.arg("CSV")
395				.output();
396
397			match output {
398				Ok(output) => {
399					if output.status.success() {
400						let stdout = String::from_utf8_lossy(&output.stdout);
401						stdout.lines().any(|line| {
402							line.contains(&pid.to_string()) && (line.contains("Air") || line.contains("daemon"))
403						})
404					} else {
405						false
406					}
407				},
408				Err(e) => {
409					dev_log!("daemon", "error: [Daemon] Failed to check process status: {}", e);
410					false
411				},
412			}
413		}
414	}
415
416	/// Cleanup stale PID file with validation and error handling
417	async fn CleanupStalePidFile(&self) -> Result<()> {
418		if !self.PidFilePath.exists() {
419			return Ok(());
420		}
421
422		// Verify the file is actually stale before deleting
423		let content = fs::read_to_string(&self.PidFilePath)
424			.map_err(|e| {
425				dev_log!("daemon", "warn: [Daemon] Cannot verify stale PID file: {}", e);
426				return false;
427			})
428			.ok();
429
430		if let Some(content) = content {
431			if content.starts_with(|c:char| c.is_numeric()) {
432				// Clean up the stale PID file
433				if let Err(e) = fs::remove_file(&self.PidFilePath) {
434					dev_log!("daemon", "warn: [Daemon] Failed to remove stale PID file: {}", e);
435					return Err(AirError::FileSystem(format!("Failed to remove stale PID file: {}", e)));
436				}
437				dev_log!("daemon", "[Daemon] Cleaned up stale PID file");
438			}
439		}
440
441		Ok(())
442	}
443
444	/// Release daemon lock with proper cleanup and rollback
445	/// Ensures all resources are properly cleaned up even on failure
446	pub async fn ReleaseLock(&self) -> Result<()> {
447		dev_log!("daemon", "[Daemon] Releasing daemon lock...");
448		// Acquire lock for atomic cleanup
449		let _lock = self.PidLock.lock().await;
450
451		// Set running state before cleanup
452		*self.IsRunning.write().await = false;
453
454		// Clear checksum
455		*self.PidChecksum.lock().await = None;
456
457		// Remove PID file with validation
458		if self.PidFilePath.exists() {
459			match fs::remove_file(&self.PidFilePath) {
460				Ok(_) => {
461					dev_log!("daemon", "[Daemon] PID file removed successfully");
462				},
463				Err(e) => {
464					dev_log!("daemon", "error: [Daemon] Failed to remove PID file: {}", e); // Don't fail entire operation if PID file cleanup fails
465					return Err(AirError::FileSystem(format!("Failed to remove PID file: {}", e)));
466				},
467			}
468		}
469
470		// Try to clean up any temporary files
471		let TempDir = PathBuf::from(format!("{}.tmp", self.PidFilePath.display()));
472		if TempDir.exists() {
473			let _ = fs::remove_file(&TempDir);
474		}
475
476		dev_log!("daemon", "[Daemon] Daemon lock released");
477		Ok(())
478	}
479
480	/// Check if daemon is running
481	pub async fn IsRunning(&self) -> bool { *self.IsRunning.read().await }
482
483	/// Request graceful shutdown
484	pub async fn RequestShutdown(&self) -> Result<()> {
485		dev_log!("daemon", "[Daemon] Requesting graceful shutdown...");
486		*self.ShutdownRequested.write().await = true;
487		Ok(())
488	}
489
490	/// Clear shutdown request (for restart scenarios)
491	pub async fn ClearShutdownRequest(&self) -> Result<()> {
492		dev_log!("daemon", "[Daemon] Clearing shutdown request");
493		*self.ShutdownRequested.write().await = false;
494		Ok(())
495	}
496
497	/// Check if shutdown has been requested
498	pub async fn IsShutdownRequested(&self) -> bool { *self.ShutdownRequested.read().await }
499
500	/// Get daemon status with comprehensive health information
501	pub async fn GetStatus(&self) -> Result<DaemonStatus> {
502		let IsRunning = self.IsRunning().await;
503		let PidFileExists = self.PidFilePath.exists();
504
505		let pid = if PidFileExists {
506			fs::read_to_string(&self.PidFilePath)
507				.ok()
508				.and_then(|content| content.split('|').next().and_then(|s| s.trim().parse().ok()))
509		} else {
510			None
511		};
512
513		Ok(DaemonStatus {
514			IsRunning,
515			PidFileExists,
516			Pid:pid,
517			Platform:self.PlatformInfo.Platform.clone(),
518			ServiceName:self.PlatformInfo.ServiceName.clone(),
519			ShutdownRequested:self.IsShutdownRequested().await,
520		})
521	}
522
523	/// Generate system service file for installation
524	pub fn GenerateServiceFile(&self) -> Result<String> {
525		match self.PlatformInfo.Platform {
526			Platform::Linux => self.GenerateSystemdService(),
527			Platform::MacOS => self.GenerateLaunchdService(),
528			#[cfg(target_os = "windows")]
529			Platform::Windows => self.GenerateWindowsService(),
530			#[cfg(not(target_os = "windows"))]
531			Platform::Windows => {
532				Err(AirError::ServiceUnavailable(
533					"Windows service generation not available on this platform".to_string(),
534				))
535			},
536			Platform::Unknown => {
537				Err(AirError::ServiceUnavailable(
538					"Unknown platform, cannot generate service file".to_string(),
539				))
540			},
541		}
542	}
543
544	/// Generate systemd service file with comprehensive configuration
545	fn GenerateSystemdService(&self) -> Result<String> {
546		let ExePath = std::env::current_exe()
547			.map_err(|e| AirError::FileSystem(format!("Failed to get executable path: {}", e)))?;
548
549		let user = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
550		let group = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
551
552		let ServiceContent = format!(
553			r#"[Unit]
554Description=Air Daemon - Background service for Land code editor
555Documentation=man:Air(1)
556After=network-online.target
557Wants=network-online.target
558StartLimitIntervalSec=0
559
560[Service]
561Type=notify
562NotifyAccess=all
563ExecStart={}
564ExecStop=/bin/kill -s TERM $MAINPID
565Restart=always
566RestartSec=5
567StartLimitBurst=3
568User={}
569Group={}
570Environment=RUST_LOG=info
571Environment=DAEMON_MODE=systemd
572Nice=-5
573LimitNOFILE=65536
574LimitNPROC=4096
575
576# Security hardening
577NoNewPrivileges=true
578PrivateTmp=true
579ProtectSystem=strict
580ProtectHome=true
581ReadWritePaths=/var/log/Air /var/run/Air
582RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
583RestrictRealtime=true
584
585[Install]
586WantedBy=multi-user.target
587"#,
588			ExePath.display(),
589			user,
590			group
591		);
592
593		Ok(ServiceContent)
594	}
595
596	/// Generate launchd service file with comprehensive configuration
597	fn GenerateLaunchdService(&self) -> Result<String> {
598		let ExePath = std::env::current_exe()
599			.map(|p| p.display().to_string())
600			.unwrap_or_else(|_| "/usr/local/bin/Air".to_string());
601
602		let ServiceName = &self.PlatformInfo.ServiceName;
603		let user = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
604
605		let ServiceContent = format!(
606			r#"<?xml version="1.0" encoding="UTF-8"?>
607<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
608<plist version="1.0">
609<dict>
610    <key>Label</key>
611    <string>{}</string>
612    
613    <key>ProgramArguments</key>
614    <array>
615        <string>{}</string>
616        <string>--daemon</string>
617        <string>--mode=launchd</string>
618    </array>
619    
620    <key>RunAtLoad</key>
621    <true/>
622    
623    <key>KeepAlive</key>
624    <dict>
625        <key>SuccessfulExit</key>
626        <false/>
627        <key>Crashed</key>
628        <true/>
629    </dict>
630    
631    <key>ThrottleInterval</key>
632    <integer>5</integer>
633    
634    <key>UserName</key>
635    <string>{}</string>
636    
637    <key>StandardOutPath</key>
638    <string>/var/log/Air/daemon.log</string>
639    
640    <key>StandardErrorPath</key>
641    <string>/var/log/Air/daemon.err</string>
642    
643    <key>WorkingDirectory</key>
644    <string>/var/lib/Air</string>
645    
646    <key>ProcessType</key>
647    <string>Background</string>
648    
649    <key>Nice</key>
650    <integer>-5</integer>
651    
652    <key>SoftResourceLimits</key>
653    <dict>
654        <key>NumberOfFiles</key>
655        <integer>65536</integer>
656    </dict>
657    
658    <key>HardResourceLimits</key>
659    <dict>
660        <key>NumberOfFiles</key>
661        <integer>65536</integer>
662    </dict>
663    
664    <key>EnvironmentVariables</key>
665    <dict>
666        <key>RUST_LOG</key>
667        <string>info</string>
668        <key>DAEMON_MODE</key>
669        <string>launchd</string>
670    </dict>
671</dict>
672</plist>
673"#,
674			ServiceName, ExePath, user
675		);
676
677		Ok(ServiceContent)
678	}
679
680	/// Generate Windows service configuration file
681	///
682	/// Note: For production use with actual Windows service registration,
683	/// integrate with the winsvc crate or windows-rs API.
684	/// This method generates a configuration file compatible with winsvc.
685	#[cfg(target_os = "windows")]
686	fn GenerateWindowsService(&self) -> Result<String> {
687		let ExePath = std::env::current_exe()
688			.map(|p| p.display().to_string())
689			.unwrap_or_else(|_| "C:\\Program Files\\Air\\Air.exe".to_string());
690
691		let ServiceName = &self.PlatformInfo.ServiceName;
692		let DisplayName = "Air Daemon Service";
693		let Description = "Background service for Land code editor";
694
695		// Generate winsvc-compatible XML configuration
696		let ServiceContent = format!(
697			r#"<service>
698		  <id>{}</id>
699		  <name>{}</name>
700		  <description>{}</description>
701		  <executable>{}</executable>
702    
703    <arguments>--daemon --mode=windows</arguments>
704    
705    <startmode>Automatic</startmode>
706    <delayedAutoStart>true</delayedAutoStart>
707    
708    <log mode="roll">
709        <sizeThreshold>10240</sizeThreshold>
710        <keepFiles>8</keepFiles>
711    </log>
712    
713    <onfailure action="restart" delay="10 sec"/>
714    <onfailure action="restart" delay="20 sec"/>
715    <onfailure action="restart" delay="60 sec"/>
716    
717    <resetfailure>1 hour</resetfailure>
718    
719    <depend>EventLog</depend>
720    <depend>TcpIp</depend>
721    
722    <serviceaccount>
723        <domain>.</domain>
724        <user>LocalSystem</user>
725        <password></password>
726        <allowservicelogon>true</allowservicelogon>
727    </serviceaccount>
728    
729    <workingdirectory>C:\Program Files\Air</workingdirectory>
730    
731    <env name="RUST_LOG" value="info"/>
732    <env name="DAEMON_MODE" value="windows"/>
733</service>
734"#,
735			ServiceName, DisplayName, Description, ExePath
736		);
737
738		Ok(ServiceContent)
739	}
740
741	/// Install daemon as system service with validation
742	pub async fn InstallService(&self) -> Result<()> {
743		dev_log!("daemon", "[Daemon] Installing system service...");
744		match self.PlatformInfo.Platform {
745			Platform::Linux => self.InstallSystemdService().await,
746			Platform::MacOS => self.InstallLaunchdService().await,
747			#[cfg(target_os = "windows")]
748			Platform::Windows => self.InstallWindowsService().await,
749			#[cfg(not(target_os = "windows"))]
750			Platform::Windows => {
751				Err(AirError::ServiceUnavailable(
752					"Windows service installation not available on this platform".to_string(),
753				))
754			},
755			Platform::Unknown => {
756				Err(AirError::ServiceUnavailable(
757					"Unknown platform, cannot install service".to_string(),
758				))
759			},
760		}
761	}
762
763	/// Install systemd service with validation
764	async fn InstallSystemdService(&self) -> Result<()> {
765		let ServiceFileContent = self.GenerateSystemdService()?;
766		let ServiceFilePath = format!("/etc/systemd/system/{}.service", self.PlatformInfo.ServiceName);
767
768		// Create temporary file for atomic write
769		let TempPath = format!("{}.tmp", ServiceFilePath);
770
771		// Validate service content
772		if !ServiceFileContent.contains("[Unit]") || !ServiceFileContent.contains("[Service]") {
773			return Err(AirError::Configuration("Generated service file is invalid".to_string()));
774		}
775
776		// Write to temporary file first
777		fs::write(&TempPath, &ServiceFileContent)
778			.map_err(|e| AirError::FileSystem(format!("Failed to write temporary service file: {}", e)))?;
779
780		// Atomic rename
781		#[cfg(unix)]
782		fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
783			let _ = fs::remove_file(&TempPath);
784			AirError::FileSystem(format!("Failed to rename service file: {}", e))
785		})?;
786
787		#[cfg(not(unix))]
788		fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
789			let _ = fs::remove_file(&TempPath);
790			AirError::FileSystem(format!("Failed to rename service file: {}", e))
791		})?;
792
793		// Set proper permissions
794		#[cfg(unix)]
795		{
796			use std::os::unix::fs::PermissionsExt;
797			let perms = fs::Permissions::from_mode(0o644);
798			fs::set_permissions(&ServiceFilePath, perms)
799				.map_err(|e| {
800					dev_log!("daemon", "error: [Daemon] Failed to set service file permissions: {}", e);
801				})
802				.ok();
803		}
804
805		dev_log!("daemon", "[Daemon] Systemd service installed at {}", ServiceFilePath);
806		// Run daemon-reload to notify systemd
807		let _ = tokio::process::Command::new("systemctl").args(["daemon-reload"]).output().await;
808
809		Ok(())
810	}
811
812	/// Install launchd service with validation
813	async fn InstallLaunchdService(&self) -> Result<()> {
814		let ServiceFileContent = self.GenerateLaunchdService()?;
815		let ServiceFilePath = format!("/Library/LaunchDaemons/{}.plist", self.PlatformInfo.ServiceName);
816
817		// Create temporary file for atomic write
818		let TempPath = format!("{}.tmp", ServiceFilePath);
819
820		// Validate plist content
821		if !ServiceFileContent.contains("<?xml") || !ServiceFileContent.contains("<!DOCTYPE plist") {
822			return Err(AirError::Configuration("Generated plist file is invalid".to_string()));
823		}
824
825		// Write to temporary file first
826		fs::write(&TempPath, &ServiceFileContent)
827			.map_err(|e| AirError::FileSystem(format!("Failed to write temporary plist file: {}", e)))?;
828
829		// Atomic rename
830		#[cfg(unix)]
831		fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
832			let _ = fs::remove_file(&TempPath);
833			AirError::FileSystem(format!("Failed to rename plist file: {}", e))
834		})?;
835
836		#[cfg(not(unix))]
837		fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
838			let _ = fs::remove_file(&TempPath);
839			AirError::FileSystem(format!("Failed to rename plist file: {}", e))
840		})?;
841
842		// Set proper permissions
843		#[cfg(unix)]
844		{
845			use std::os::unix::fs::PermissionsExt;
846			let perms = fs::Permissions::from_mode(0o644);
847			fs::set_permissions(&ServiceFilePath, perms)
848				.map_err(|e| {
849					dev_log!("daemon", "error: [Daemon] Failed to set plist file permissions: {}", e);
850				})
851				.ok();
852		}
853
854		dev_log!("daemon", "[Daemon] Launchd service installed at {}", ServiceFilePath);
855		// No need to load immediately - launchd will pick it up automatically
856		// User can run: sudo launchctl load -w /Library/LaunchDaemons/Air-daemon.plist
857
858		Ok(())
859	}
860
861	/// Install Windows service
862	///
863	/// Note: For production use, integrate with the winsvc crate or windows-rs
864	/// API to perform actual Windows service registration via the Service
865	/// Control Manager (SCM). This method writes a configuration file that can
866	/// be used with winsvc.
867	#[cfg(target_os = "windows")]
868	async fn InstallWindowsService(&self) -> Result<()> {
869		let ServiceFileContent = self.GenerateWindowsService()?;
870		let ServiceDir = "C:\\ProgramData\\Air";
871		let ServiceFilePath = format!("{}\\{}.xml", ServiceDir, self.PlatformInfo.ServiceName);
872
873		// Create directory if it doesn't exist
874		fs::create_dir_all(&ServiceDir)
875			.map_err(|e| AirError::FileSystem(format!("Failed to create service directory: {}", e)))?;
876
877		// Create temporary file for atomic write
878		let TempPath = format!("{}.tmp", ServiceFilePath);
879
880		// Validate service content
881		if !ServiceFileContent.contains("<service>") {
882			return Err(AirError::Configuration("Generated service file is invalid".to_string()));
883		}
884
885		// Write to temporary file first
886		fs::write(&TempPath, &ServiceFileContent)
887			.map_err(|e| AirError::FileSystem(format!("Failed to write temporary service file: {}", e)))?;
888
889		// Atomic rename
890		fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
891			let _ = fs::remove_file(&TempPath);
892			AirError::FileSystem(format!("Failed to rename service file: {}", e))
893		})?;
894
895		dev_log!(
896			"daemon",
897			"[Daemon] Windows service configuration written to {}",
898			ServiceFilePath
899		);
900		dev_log!("daemon", "[Daemon] To register the service, run:");
901		dev_log!(
902			"daemon",
903			"[Daemon]   sc create AirDaemon binPath= \"{}\" DisplayName= \"Air Daemon\"",
904			std::env::current_exe().unwrap_or_else(|_| "air.exe".into()).display()
905		);
906		dev_log!("daemon", "[Daemon]   sc config AirDaemon start= auto");
907		dev_log!("daemon", "[Daemon]   sc start AirDaemon");
908		Ok(())
909	}
910
911	/// Uninstall system service with proper coordination
912	pub async fn UninstallService(&self) -> Result<()> {
913		dev_log!("daemon", "[Daemon] Uninstalling system service...");
914		match self.PlatformInfo.Platform {
915			Platform::Linux => self.UninstallSystemdService().await,
916			Platform::MacOS => self.UninstallLaunchdService().await,
917			#[cfg(target_os = "windows")]
918			Platform::Windows => self.UninstallWindowsService().await,
919			#[cfg(not(target_os = "windows"))]
920			Platform::Windows => {
921				Err(AirError::ServiceUnavailable(
922					"Windows service uninstallation not available on this platform".to_string(),
923				))
924			},
925			Platform::Unknown => {
926				Err(AirError::ServiceUnavailable(
927					"Unknown platform, cannot uninstall service".to_string(),
928				))
929			},
930		}
931	}
932
933	/// Uninstall systemd service with proper coordination
934	async fn UninstallSystemdService(&self) -> Result<()> {
935		let ServiceFilePath = format!("/etc/systemd/system/{}.service", self.PlatformInfo.ServiceName);
936
937		// Stop service first if running
938		let _ = tokio::process::Command::new("systemctl")
939			.args(["stop", &self.PlatformInfo.ServiceName])
940			.output()
941			.await;
942
943		// Disable service
944		let _ = tokio::process::Command::new("systemctl")
945			.args(["disable", &self.PlatformInfo.ServiceName])
946			.output()
947			.await;
948
949		// Remove service file
950		if fs::remove_file(&ServiceFilePath).is_ok() {
951			dev_log!("daemon", "[Daemon] Systemd service file removed");
952		} else {
953			dev_log!("daemon", "warn: [Daemon] Service file {} not found", ServiceFilePath);
954		}
955
956		// Reload systemd
957		let _ = tokio::process::Command::new("systemctl").args(["daemon-reload"]).output().await;
958
959		dev_log!("daemon", "[Daemon] Systemd service uninstalled");
960		Ok(())
961	}
962
963	/// Uninstall launchd service with proper coordination
964	async fn UninstallLaunchdService(&self) -> Result<()> {
965		let ServiceFilePath = format!("/Library/LaunchDaemons/{}.plist", self.PlatformInfo.ServiceName);
966
967		// Unload service first
968		let _ = tokio::process::Command::new("launchctl")
969			.args(["unload", "-w", &ServiceFilePath])
970			.output()
971			.await;
972
973		// Remove service file
974		if fs::remove_file(&ServiceFilePath).is_ok() {
975			dev_log!("daemon", "[Daemon] Launchd service file removed");
976		} else {
977			dev_log!("daemon", "warn: [Daemon] Service file {} not found", ServiceFilePath);
978		}
979
980		dev_log!("daemon", "[Daemon] Launchd service uninstalled");
981		Ok(())
982	}
983
984	/// Uninstall Windows service
985	///
986	/// Note: For production use, integrate with the winsvc crate or windows-rs
987	/// API to properly stop and remove the Windows service via the Service
988	/// Control Manager (SCM).
989	#[cfg(target_os = "windows")]
990	async fn UninstallWindowsService(&self) -> Result<()> {
991		let ServiceFilePath = format!("C:\\ProgramData\\Air\\{}.xml", self.PlatformInfo.ServiceName);
992
993		// Remove the configuration file
994		if fs::remove_file(&ServiceFilePath).is_ok() {
995			dev_log!("daemon", "[Daemon] Windows service configuration removed");
996		} else {
997			dev_log!("daemon", "warn: [Daemon] Service file {} not found", ServiceFilePath);
998		}
999
1000		dev_log!("daemon", "[Daemon] To unregister the service, run:");
1001		dev_log!("daemon", "[Daemon]   sc stop AirDaemon");
1002		dev_log!("daemon", "[Daemon]   sc delete AirDaemon");
1003		Ok(())
1004	}
1005}
1006
1007/// Daemon status information
1008#[derive(Debug, Clone)]
1009pub struct DaemonStatus {
1010	pub IsRunning:bool,
1011	pub PidFileExists:bool,
1012	pub Pid:Option<u32>,
1013	pub Platform:Platform,
1014	pub ServiceName:String,
1015	pub ShutdownRequested:bool,
1016}
1017
1018impl DaemonStatus {
1019	/// Get human-readable status description
1020	pub fn status_description(&self) -> String {
1021		if self.IsRunning {
1022			format!("Running (PID: {})", self.Pid.unwrap_or(0))
1023		} else if self.PidFileExists {
1024			"Stale PID file exists".to_string()
1025		} else {
1026			"Not running".to_string()
1027		}
1028	}
1029}
1030
1031impl From<ExitCode> for i32 {
1032	fn from(code:ExitCode) -> i32 { code as i32 }
1033}