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		// sha2 0.11: `Digest::finalize()` output dropped its `LowerHex` impl
265		// (moved onto `hybrid_array::Array`). `hex::encode` produces the same
266		// lowercase-hex string as the former `format!("{:x}", …)` and keeps
267		// the PID-file checksum payload byte-identical.
268		let checksum = hex::encode(hasher.finalize());
269
270		// Write to temporary file first (atomic operation)
271		let TempFileContent = format!("{}|CHECKSUM:{}", PidContent, checksum);
272		fs::write(&TempDir, &TempFileContent)
273			.map_err(|e| AirError::FileSystem(format!("Failed to write temporary PID file: {}", e)))?;
274
275		// Atomic rename to avoid partial writes
276		#[cfg(unix)]
277		fs::rename(&TempDir, &self.PidFilePath).map_err(|e| {
278			// Rollback: clean up temp file on failure
279			let _ = fs::remove_file(&TempDir);
280			AirError::FileSystem(format!("Failed to rename PID file: {}", e))
281		})?;
282
283		#[cfg(not(unix))]
284		fs::rename(&TempDir, &self.PidFilePath).map_err(|e| {
285			let _ = fs::remove_file(&TempDir);
286			AirError::FileSystem(format!("Failed to rename PID file: {}", e))
287		})?;
288
289		// Store checksum for later validation
290		*self.PidChecksum.lock().await = Some(checksum);
291
292		// Set running state
293		*self.IsRunning.write().await = true;
294
295		// Set secure permissions on PID file
296		#[cfg(unix)]
297		{
298			use std::os::unix::fs::PermissionsExt;
299			let perms = fs::Permissions::from_mode(0o600);
300			if let Err(e) = fs::set_permissions(&self.PidFilePath, perms) {
301				dev_log!("daemon", "warn: [Daemon] Failed to set PID file permissions: {}", e);
302			}
303		}
304
305		dev_log!("daemon", "[Daemon] Daemon lock acquired (PID: {})", pid);
306		Ok(())
307	}
308
309	/// Check if daemon is already running
310	/// Performs comprehensive validation including:
311	/// - PID file existence check
312	/// - Checksum verification
313	/// - Process existence validation
314	/// - Stale PID file cleanup
315	pub async fn IsAlreadyRunning(&self) -> Result<bool> {
316		if !self.PidFilePath.exists() {
317			dev_log!("daemon", "[Daemon] PID file does not exist");
318			return Ok(false);
319		}
320
321		// Read PID from file
322		let PidContent = fs::read_to_string(&self.PidFilePath)
323			.map_err(|e| AirError::FileSystem(format!("Failed to read PID file: {}", e)))?;
324
325		// Parse PID content with checksum
326		let parts:Vec<&str> = PidContent.split('|').collect();
327		if parts.len() < 2 {
328			dev_log!("daemon", "warn: [Daemon] Invalid PID file format, treating as stale");
329			self.CleanupStalePidFile().await?;
330			return Ok(false);
331		}
332
333		let pid:u32 = parts[0].trim().parse().map_err(|e| {
334			dev_log!("daemon", "warn: [Daemon] Invalid PID in file: {}", e);
335			AirError::FileSystem("Invalid PID file content".to_string())
336		})?;
337
338		// Verify checksum if present
339		if parts.len() >= 3 && parts[1].starts_with("CHECKSUM:") {
340			let StoredChecksum = &parts[1][9..]; // Remove "CHECKSUM:" prefix
341			let CurrentChecksum = self.PidChecksum.lock().await;
342
343			if let Some(ref cksum) = *CurrentChecksum {
344				if cksum != StoredChecksum {
345					dev_log!("daemon", "warn: [Daemon] PID file checksum mismatch, file may be corrupted"); // 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			dev_log!("daemon", "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					dev_log!("daemon", "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					dev_log!("daemon", "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				dev_log!("daemon", "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					dev_log!("daemon", "warn: [Daemon] Failed to remove stale PID file: {}", e);
439					return Err(AirError::FileSystem(format!("Failed to remove stale PID file: {}", e)));
440				}
441				dev_log!("daemon", "[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		dev_log!("daemon", "[Daemon] Releasing daemon lock...");
452		// Acquire lock for atomic cleanup
453		let _lock = self.PidLock.lock().await;
454
455		// Set running state before cleanup
456		*self.IsRunning.write().await = false;
457
458		// Clear checksum
459		*self.PidChecksum.lock().await = None;
460
461		// Remove PID file with validation
462		if self.PidFilePath.exists() {
463			match fs::remove_file(&self.PidFilePath) {
464				Ok(_) => {
465					dev_log!("daemon", "[Daemon] PID file removed successfully");
466				},
467				Err(e) => {
468					dev_log!("daemon", "error: [Daemon] Failed to remove PID file: {}", e); // Don't fail entire operation if PID file cleanup fails
469					return Err(AirError::FileSystem(format!("Failed to remove PID file: {}", e)));
470				},
471			}
472		}
473
474		// Try to clean up any temporary files
475		let TempDir = PathBuf::from(format!("{}.tmp", self.PidFilePath.display()));
476		if TempDir.exists() {
477			let _ = fs::remove_file(&TempDir);
478		}
479
480		dev_log!("daemon", "[Daemon] Daemon lock released");
481		Ok(())
482	}
483
484	/// Check if daemon is running
485	pub async fn IsRunning(&self) -> bool { *self.IsRunning.read().await }
486
487	/// Request graceful shutdown
488	pub async fn RequestShutdown(&self) -> Result<()> {
489		dev_log!("daemon", "[Daemon] Requesting graceful shutdown...");
490		*self.ShutdownRequested.write().await = true;
491		Ok(())
492	}
493
494	/// Clear shutdown request (for restart scenarios)
495	pub async fn ClearShutdownRequest(&self) -> Result<()> {
496		dev_log!("daemon", "[Daemon] Clearing shutdown request");
497		*self.ShutdownRequested.write().await = false;
498		Ok(())
499	}
500
501	/// Check if shutdown has been requested
502	pub async fn IsShutdownRequested(&self) -> bool { *self.ShutdownRequested.read().await }
503
504	/// Get daemon status with comprehensive health information
505	pub async fn GetStatus(&self) -> Result<DaemonStatus> {
506		let IsRunning = self.IsRunning().await;
507		let PidFileExists = self.PidFilePath.exists();
508
509		let pid = if PidFileExists {
510			fs::read_to_string(&self.PidFilePath)
511				.ok()
512				.and_then(|content| content.split('|').next().and_then(|s| s.trim().parse().ok()))
513		} else {
514			None
515		};
516
517		Ok(DaemonStatus {
518			IsRunning,
519			PidFileExists,
520			Pid:pid,
521			Platform:self.PlatformInfo.Platform.clone(),
522			ServiceName:self.PlatformInfo.ServiceName.clone(),
523			ShutdownRequested:self.IsShutdownRequested().await,
524		})
525	}
526
527	/// Generate system service file for installation
528	pub fn GenerateServiceFile(&self) -> Result<String> {
529		match self.PlatformInfo.Platform {
530			Platform::Linux => self.GenerateSystemdService(),
531			Platform::MacOS => self.GenerateLaunchdService(),
532			#[cfg(target_os = "windows")]
533			Platform::Windows => self.GenerateWindowsService(),
534			#[cfg(not(target_os = "windows"))]
535			Platform::Windows => {
536				Err(AirError::ServiceUnavailable(
537					"Windows service generation not available on this platform".to_string(),
538				))
539			},
540			Platform::Unknown => {
541				Err(AirError::ServiceUnavailable(
542					"Unknown platform, cannot generate service file".to_string(),
543				))
544			},
545		}
546	}
547
548	/// Generate systemd service file with comprehensive configuration
549	fn GenerateSystemdService(&self) -> Result<String> {
550		let ExePath = std::env::current_exe()
551			.map_err(|e| AirError::FileSystem(format!("Failed to get executable path: {}", e)))?;
552
553		let user = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
554		let group = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
555
556		let ServiceContent = format!(
557			r#"[Unit]
558Description=Air Daemon - Background service for Land code editor
559Documentation=man:Air(1)
560After=network-online.target
561Wants=network-online.target
562StartLimitIntervalSec=0
563
564[Service]
565Type=notify
566NotifyAccess=all
567ExecStart={}
568ExecStop=/bin/kill -s TERM $MAINPID
569Restart=always
570RestartSec=5
571StartLimitBurst=3
572User={}
573Group={}
574Environment=RUST_LOG=info
575Environment=DAEMON_MODE=systemd
576Nice=-5
577LimitNOFILE=65536
578LimitNPROC=4096
579
580# Security hardening
581NoNewPrivileges=true
582PrivateTmp=true
583ProtectSystem=strict
584ProtectHome=true
585ReadWritePaths=/var/log/Air /var/run/Air
586RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
587RestrictRealtime=true
588
589[Install]
590WantedBy=multi-user.target
591"#,
592			ExePath.display(),
593			user,
594			group
595		);
596
597		Ok(ServiceContent)
598	}
599
600	/// Generate launchd service file with comprehensive configuration
601	fn GenerateLaunchdService(&self) -> Result<String> {
602		let ExePath = std::env::current_exe()
603			.map(|p| p.display().to_string())
604			.unwrap_or_else(|_| "/usr/local/bin/Air".to_string());
605
606		let ServiceName = &self.PlatformInfo.ServiceName;
607		let user = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
608
609		let ServiceContent = format!(
610			r#"<?xml version="1.0" encoding="UTF-8"?>
611<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
612<plist version="1.0">
613<dict>
614    <key>Label</key>
615    <string>{}</string>
616    
617    <key>ProgramArguments</key>
618    <array>
619        <string>{}</string>
620        <string>--daemon</string>
621        <string>--mode=launchd</string>
622    </array>
623    
624    <key>RunAtLoad</key>
625    <true/>
626    
627    <key>KeepAlive</key>
628    <dict>
629        <key>SuccessfulExit</key>
630        <false/>
631        <key>Crashed</key>
632        <true/>
633    </dict>
634    
635    <key>ThrottleInterval</key>
636    <integer>5</integer>
637    
638    <key>UserName</key>
639    <string>{}</string>
640    
641    <key>StandardOutPath</key>
642    <string>/var/log/Air/daemon.log</string>
643    
644    <key>StandardErrorPath</key>
645    <string>/var/log/Air/daemon.err</string>
646    
647    <key>WorkingDirectory</key>
648    <string>/var/lib/Air</string>
649    
650    <key>ProcessType</key>
651    <string>Background</string>
652    
653    <key>Nice</key>
654    <integer>-5</integer>
655    
656    <key>SoftResourceLimits</key>
657    <dict>
658        <key>NumberOfFiles</key>
659        <integer>65536</integer>
660    </dict>
661    
662    <key>HardResourceLimits</key>
663    <dict>
664        <key>NumberOfFiles</key>
665        <integer>65536</integer>
666    </dict>
667    
668    <key>EnvironmentVariables</key>
669    <dict>
670        <key>RUST_LOG</key>
671        <string>info</string>
672        <key>DAEMON_MODE</key>
673        <string>launchd</string>
674    </dict>
675</dict>
676</plist>
677"#,
678			ServiceName, ExePath, user
679		);
680
681		Ok(ServiceContent)
682	}
683
684	/// Generate Windows service configuration file
685	///
686	/// Note: For production use with actual Windows service registration,
687	/// integrate with the winsvc crate or windows-rs API.
688	/// This method generates a configuration file compatible with winsvc.
689	#[cfg(target_os = "windows")]
690	fn GenerateWindowsService(&self) -> Result<String> {
691		let ExePath = std::env::current_exe()
692			.map(|p| p.display().to_string())
693			.unwrap_or_else(|_| "C:\\Program Files\\Air\\Air.exe".to_string());
694
695		let ServiceName = &self.PlatformInfo.ServiceName;
696		let DisplayName = "Air Daemon Service";
697		let Description = "Background service for Land code editor";
698
699		// Generate winsvc-compatible XML configuration
700		let ServiceContent = format!(
701			r#"<service>
702		  <id>{}</id>
703		  <name>{}</name>
704		  <description>{}</description>
705		  <executable>{}</executable>
706    
707    <arguments>--daemon --mode=windows</arguments>
708    
709    <startmode>Automatic</startmode>
710    <delayedAutoStart>true</delayedAutoStart>
711    
712    <log mode="roll">
713        <sizeThreshold>10240</sizeThreshold>
714        <keepFiles>8</keepFiles>
715    </log>
716    
717    <onfailure action="restart" delay="10 sec"/>
718    <onfailure action="restart" delay="20 sec"/>
719    <onfailure action="restart" delay="60 sec"/>
720    
721    <resetfailure>1 hour</resetfailure>
722    
723    <depend>EventLog</depend>
724    <depend>TcpIp</depend>
725    
726    <serviceaccount>
727        <domain>.</domain>
728        <user>LocalSystem</user>
729        <password></password>
730        <allowservicelogon>true</allowservicelogon>
731    </serviceaccount>
732    
733    <workingdirectory>C:\Program Files\Air</workingdirectory>
734    
735    <env name="RUST_LOG" value="info"/>
736    <env name="DAEMON_MODE" value="windows"/>
737</service>
738"#,
739			ServiceName, DisplayName, Description, ExePath
740		);
741
742		Ok(ServiceContent)
743	}
744
745	/// Install daemon as system service with validation
746	pub async fn InstallService(&self) -> Result<()> {
747		dev_log!("daemon", "[Daemon] Installing system service...");
748		match self.PlatformInfo.Platform {
749			Platform::Linux => self.InstallSystemdService().await,
750			Platform::MacOS => self.InstallLaunchdService().await,
751			#[cfg(target_os = "windows")]
752			Platform::Windows => self.InstallWindowsService().await,
753			#[cfg(not(target_os = "windows"))]
754			Platform::Windows => {
755				Err(AirError::ServiceUnavailable(
756					"Windows service installation not available on this platform".to_string(),
757				))
758			},
759			Platform::Unknown => {
760				Err(AirError::ServiceUnavailable(
761					"Unknown platform, cannot install service".to_string(),
762				))
763			},
764		}
765	}
766
767	/// Install systemd service with validation
768	async fn InstallSystemdService(&self) -> Result<()> {
769		let ServiceFileContent = self.GenerateSystemdService()?;
770		let ServiceFilePath = format!("/etc/systemd/system/{}.service", self.PlatformInfo.ServiceName);
771
772		// Create temporary file for atomic write
773		let TempPath = format!("{}.tmp", ServiceFilePath);
774
775		// Validate service content
776		if !ServiceFileContent.contains("[Unit]") || !ServiceFileContent.contains("[Service]") {
777			return Err(AirError::Configuration("Generated service file is invalid".to_string()));
778		}
779
780		// Write to temporary file first
781		fs::write(&TempPath, &ServiceFileContent)
782			.map_err(|e| AirError::FileSystem(format!("Failed to write temporary service file: {}", e)))?;
783
784		// Atomic rename
785		#[cfg(unix)]
786		fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
787			let _ = fs::remove_file(&TempPath);
788			AirError::FileSystem(format!("Failed to rename service file: {}", e))
789		})?;
790
791		#[cfg(not(unix))]
792		fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
793			let _ = fs::remove_file(&TempPath);
794			AirError::FileSystem(format!("Failed to rename service file: {}", e))
795		})?;
796
797		// Set proper permissions
798		#[cfg(unix)]
799		{
800			use std::os::unix::fs::PermissionsExt;
801			let perms = fs::Permissions::from_mode(0o644);
802			fs::set_permissions(&ServiceFilePath, perms)
803				.map_err(|e| {
804					dev_log!("daemon", "error: [Daemon] Failed to set service file permissions: {}", e);
805				})
806				.ok();
807		}
808
809		dev_log!("daemon", "[Daemon] Systemd service installed at {}", ServiceFilePath);
810		// Run daemon-reload to notify systemd
811		let _ = tokio::process::Command::new("systemctl").args(["daemon-reload"]).output().await;
812
813		Ok(())
814	}
815
816	/// Install launchd service with validation
817	async fn InstallLaunchdService(&self) -> Result<()> {
818		let ServiceFileContent = self.GenerateLaunchdService()?;
819		let ServiceFilePath = format!("/Library/LaunchDaemons/{}.plist", self.PlatformInfo.ServiceName);
820
821		// Create temporary file for atomic write
822		let TempPath = format!("{}.tmp", ServiceFilePath);
823
824		// Validate plist content
825		if !ServiceFileContent.contains("<?xml") || !ServiceFileContent.contains("<!DOCTYPE plist") {
826			return Err(AirError::Configuration("Generated plist file is invalid".to_string()));
827		}
828
829		// Write to temporary file first
830		fs::write(&TempPath, &ServiceFileContent)
831			.map_err(|e| AirError::FileSystem(format!("Failed to write temporary plist file: {}", e)))?;
832
833		// Atomic rename
834		#[cfg(unix)]
835		fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
836			let _ = fs::remove_file(&TempPath);
837			AirError::FileSystem(format!("Failed to rename plist file: {}", e))
838		})?;
839
840		#[cfg(not(unix))]
841		fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
842			let _ = fs::remove_file(&TempPath);
843			AirError::FileSystem(format!("Failed to rename plist file: {}", e))
844		})?;
845
846		// Set proper permissions
847		#[cfg(unix)]
848		{
849			use std::os::unix::fs::PermissionsExt;
850			let perms = fs::Permissions::from_mode(0o644);
851			fs::set_permissions(&ServiceFilePath, perms)
852				.map_err(|e| {
853					dev_log!("daemon", "error: [Daemon] Failed to set plist file permissions: {}", e);
854				})
855				.ok();
856		}
857
858		dev_log!("daemon", "[Daemon] Launchd service installed at {}", ServiceFilePath);
859		// No need to load immediately - launchd will pick it up automatically
860		// User can run: sudo launchctl load -w /Library/LaunchDaemons/Air-daemon.plist
861
862		Ok(())
863	}
864
865	/// Install Windows service
866	///
867	/// Note: For production use, integrate with the winsvc crate or windows-rs
868	/// API to perform actual Windows service registration via the Service
869	/// Control Manager (SCM). This method writes a configuration file that can
870	/// be used with winsvc.
871	#[cfg(target_os = "windows")]
872	async fn InstallWindowsService(&self) -> Result<()> {
873		let ServiceFileContent = self.GenerateWindowsService()?;
874		let ServiceDir = "C:\\ProgramData\\Air";
875		let ServiceFilePath = format!("{}\\{}.xml", ServiceDir, self.PlatformInfo.ServiceName);
876
877		// Create directory if it doesn't exist
878		fs::create_dir_all(&ServiceDir)
879			.map_err(|e| AirError::FileSystem(format!("Failed to create service directory: {}", e)))?;
880
881		// Create temporary file for atomic write
882		let TempPath = format!("{}.tmp", ServiceFilePath);
883
884		// Validate service content
885		if !ServiceFileContent.contains("<service>") {
886			return Err(AirError::Configuration("Generated service file is invalid".to_string()));
887		}
888
889		// Write to temporary file first
890		fs::write(&TempPath, &ServiceFileContent)
891			.map_err(|e| AirError::FileSystem(format!("Failed to write temporary service file: {}", e)))?;
892
893		// Atomic rename
894		fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
895			let _ = fs::remove_file(&TempPath);
896			AirError::FileSystem(format!("Failed to rename service file: {}", e))
897		})?;
898
899		dev_log!(
900			"daemon",
901			"[Daemon] Windows service configuration written to {}",
902			ServiceFilePath
903		);
904		dev_log!("daemon", "[Daemon] To register the service, run:");
905		dev_log!(
906			"daemon",
907			"[Daemon]   sc create AirDaemon binPath= \"{}\" DisplayName= \"Air Daemon\"",
908			std::env::current_exe().unwrap_or_else(|_| "air.exe".into()).display()
909		);
910		dev_log!("daemon", "[Daemon]   sc config AirDaemon start= auto");
911		dev_log!("daemon", "[Daemon]   sc start AirDaemon");
912		Ok(())
913	}
914
915	/// Uninstall system service with proper coordination
916	pub async fn UninstallService(&self) -> Result<()> {
917		dev_log!("daemon", "[Daemon] Uninstalling system service...");
918		match self.PlatformInfo.Platform {
919			Platform::Linux => self.UninstallSystemdService().await,
920			Platform::MacOS => self.UninstallLaunchdService().await,
921			#[cfg(target_os = "windows")]
922			Platform::Windows => self.UninstallWindowsService().await,
923			#[cfg(not(target_os = "windows"))]
924			Platform::Windows => {
925				Err(AirError::ServiceUnavailable(
926					"Windows service uninstallation not available on this platform".to_string(),
927				))
928			},
929			Platform::Unknown => {
930				Err(AirError::ServiceUnavailable(
931					"Unknown platform, cannot uninstall service".to_string(),
932				))
933			},
934		}
935	}
936
937	/// Uninstall systemd service with proper coordination
938	async fn UninstallSystemdService(&self) -> Result<()> {
939		let ServiceFilePath = format!("/etc/systemd/system/{}.service", self.PlatformInfo.ServiceName);
940
941		// Stop service first if running
942		let _ = tokio::process::Command::new("systemctl")
943			.args(["stop", &self.PlatformInfo.ServiceName])
944			.output()
945			.await;
946
947		// Disable service
948		let _ = tokio::process::Command::new("systemctl")
949			.args(["disable", &self.PlatformInfo.ServiceName])
950			.output()
951			.await;
952
953		// Remove service file
954		if fs::remove_file(&ServiceFilePath).is_ok() {
955			dev_log!("daemon", "[Daemon] Systemd service file removed");
956		} else {
957			dev_log!("daemon", "warn: [Daemon] Service file {} not found", ServiceFilePath);
958		}
959
960		// Reload systemd
961		let _ = tokio::process::Command::new("systemctl").args(["daemon-reload"]).output().await;
962
963		dev_log!("daemon", "[Daemon] Systemd service uninstalled");
964		Ok(())
965	}
966
967	/// Uninstall launchd service with proper coordination
968	async fn UninstallLaunchdService(&self) -> Result<()> {
969		let ServiceFilePath = format!("/Library/LaunchDaemons/{}.plist", self.PlatformInfo.ServiceName);
970
971		// Unload service first
972		let _ = tokio::process::Command::new("launchctl")
973			.args(["unload", "-w", &ServiceFilePath])
974			.output()
975			.await;
976
977		// Remove service file
978		if fs::remove_file(&ServiceFilePath).is_ok() {
979			dev_log!("daemon", "[Daemon] Launchd service file removed");
980		} else {
981			dev_log!("daemon", "warn: [Daemon] Service file {} not found", ServiceFilePath);
982		}
983
984		dev_log!("daemon", "[Daemon] Launchd service uninstalled");
985		Ok(())
986	}
987
988	/// Uninstall Windows service
989	///
990	/// Note: For production use, integrate with the winsvc crate or windows-rs
991	/// API to properly stop and remove the Windows service via the Service
992	/// Control Manager (SCM).
993	#[cfg(target_os = "windows")]
994	async fn UninstallWindowsService(&self) -> Result<()> {
995		let ServiceFilePath = format!("C:\\ProgramData\\Air\\{}.xml", self.PlatformInfo.ServiceName);
996
997		// Remove the configuration file
998		if fs::remove_file(&ServiceFilePath).is_ok() {
999			dev_log!("daemon", "[Daemon] Windows service configuration removed");
1000		} else {
1001			dev_log!("daemon", "warn: [Daemon] Service file {} not found", ServiceFilePath);
1002		}
1003
1004		dev_log!("daemon", "[Daemon] To unregister the service, run:");
1005		dev_log!("daemon", "[Daemon]   sc stop AirDaemon");
1006		dev_log!("daemon", "[Daemon]   sc delete AirDaemon");
1007		Ok(())
1008	}
1009}
1010
1011/// Daemon status information
1012#[derive(Debug, Clone)]
1013pub struct DaemonStatus {
1014	pub IsRunning:bool,
1015	pub PidFileExists:bool,
1016	pub Pid:Option<u32>,
1017	pub Platform:Platform,
1018	pub ServiceName:String,
1019	pub ShutdownRequested:bool,
1020}
1021
1022impl DaemonStatus {
1023	/// Get human-readable status description
1024	pub fn status_description(&self) -> String {
1025		if self.IsRunning {
1026			format!("Running (PID: {})", self.Pid.unwrap_or(0))
1027		} else if self.PidFileExists {
1028			"Stale PID file exists".to_string()
1029		} else {
1030			"Not running".to_string()
1031		}
1032	}
1033}
1034
1035impl From<ExitCode> for i32 {
1036	fn from(code:ExitCode) -> i32 { code as i32 }
1037}