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