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