1use 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#[derive(Debug)]
100pub struct DaemonManager {
101 PidFilePath:PathBuf,
103 IsRunning:Arc<RwLock<bool>>,
105 PlatformInfo:PlatformInfo,
107 PidLock:Arc<Mutex<()>>,
109 PidChecksum:Arc<Mutex<Option<String>>>,
111 ShutdownRequested:Arc<RwLock<bool>>,
113}
114
115#[derive(Debug)]
117pub struct PlatformInfo {
118 pub Platform:Platform,
120 pub ServiceName:String,
122 pub RunAsUser:Option<String>,
124}
125
126#[derive(Debug, Clone, PartialEq)]
128pub enum Platform {
129 Linux,
130 MacOS,
131 Windows,
132 Unknown,
133}
134
135#[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 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 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 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 fn DetectPlatformInfo() -> PlatformInfo {
193 let platform = Self::DetectPlatform();
194 let ServiceName = "Air-daemon".to_string();
195
196 let RunAsUser = std::env::var("USER").ok().or_else(|| std::env::var("USERNAME").ok());
198
199 PlatformInfo { Platform:platform, ServiceName, RunAsUser }
200 }
201
202 pub async fn AcquireLock(&self) -> Result<()> {
210 dev_log!("daemon", "[Daemon] Acquiring daemon lock...");
211 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 if *self.ShutdownRequested.read().await {
227 return Err(AirError::ServiceUnavailable(
228 "Shutdown requested, cannot acquire lock".to_string(),
229 ));
230 }
231
232 if self.IsAlreadyRunning().await? {
234 return Err(AirError::ServiceUnavailable("Air daemon is already running".to_string()));
235 }
236
237 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 #[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 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 let mut hasher = Sha256::new();
263 hasher.update(PidContent.as_bytes());
264 let checksum = format!("{:x}", hasher.finalize());
265
266 let TempFileContent = format!("{}|CHECKSUM:{}", PidContent, checksum);
268 fs::write(&TempDir, &TempFileContent)
269 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary PID file: {}", e)))?;
270
271 #[cfg(unix)]
273 fs::rename(&TempDir, &self.PidFilePath).map_err(|e| {
274 let _ = fs::remove_file(&TempDir);
276 AirError::FileSystem(format!("Failed to rename PID file: {}", e))
277 })?;
278
279 #[cfg(not(unix))]
280 fs::rename(&TempDir, &self.PidFilePath).map_err(|e| {
281 let _ = fs::remove_file(&TempDir);
282 AirError::FileSystem(format!("Failed to rename PID file: {}", e))
283 })?;
284
285 *self.PidChecksum.lock().await = Some(checksum);
287
288 *self.IsRunning.write().await = true;
290
291 #[cfg(unix)]
293 {
294 use std::os::unix::fs::PermissionsExt;
295 let perms = fs::Permissions::from_mode(0o600);
296 if let Err(e) = fs::set_permissions(&self.PidFilePath, perms) {
297 dev_log!("daemon", "warn: [Daemon] Failed to set PID file permissions: {}", e);
298 }
299 }
300
301 dev_log!("daemon", "[Daemon] Daemon lock acquired (PID: {})", pid);
302 Ok(())
303 }
304
305 pub async fn IsAlreadyRunning(&self) -> Result<bool> {
312 if !self.PidFilePath.exists() {
313 dev_log!("daemon", "[Daemon] PID file does not exist");
314 return Ok(false);
315 }
316
317 let PidContent = fs::read_to_string(&self.PidFilePath)
319 .map_err(|e| AirError::FileSystem(format!("Failed to read PID file: {}", e)))?;
320
321 let parts:Vec<&str> = PidContent.split('|').collect();
323 if parts.len() < 2 {
324 dev_log!("daemon", "warn: [Daemon] Invalid PID file format, treating as stale");
325 self.CleanupStalePidFile().await?;
326 return Ok(false);
327 }
328
329 let pid:u32 = parts[0].trim().parse().map_err(|e| {
330 dev_log!("daemon", "warn: [Daemon] Invalid PID in file: {}", e);
331 AirError::FileSystem("Invalid PID file content".to_string())
332 })?;
333
334 if parts.len() >= 3 && parts[1].starts_with("CHECKSUM:") {
336 let StoredChecksum = &parts[1][9..]; let CurrentChecksum = self.PidChecksum.lock().await;
338
339 if let Some(ref cksum) = *CurrentChecksum {
340 if cksum != StoredChecksum {
341 dev_log!("daemon", "warn: [Daemon] PID file checksum mismatch, file may be corrupted"); return Ok(true);
343 }
344 }
345 }
346
347 let IsRunning = Self::ValidateProcess(pid);
349
350 if !IsRunning {
351 dev_log!("daemon", "warn: [Daemon] Detected stale PID file for PID {}", pid);
353 self.CleanupStalePidFile().await?;
354 }
355
356 Ok(IsRunning)
357 }
358
359 fn ValidateProcess(pid:u32) -> bool {
362 #[cfg(unix)]
363 {
364 use std::process::Command;
365 let output = Command::new("ps").arg("-p").arg(pid.to_string()).output();
366
367 match output {
368 Ok(output) => {
369 if output.status.success() {
370 let stdout = String::from_utf8_lossy(&output.stdout);
371 stdout
373 .lines()
374 .skip(1)
375 .any(|line| line.contains("Air") || line.contains("daemon"))
376 } else {
377 false
378 }
379 },
380 Err(e) => {
381 dev_log!("daemon", "error: [Daemon] Failed to check process status: {}", e);
382 false
383 },
384 }
385 }
386
387 #[cfg(windows)]
388 {
389 use std::process::Command;
390 let output = Command::new("tasklist")
391 .arg("/FI")
392 .arg(format!("PID eq {}", pid))
393 .arg("/FO")
394 .arg("CSV")
395 .output();
396
397 match output {
398 Ok(output) => {
399 if output.status.success() {
400 let stdout = String::from_utf8_lossy(&output.stdout);
401 stdout.lines().any(|line| {
402 line.contains(&pid.to_string()) && (line.contains("Air") || line.contains("daemon"))
403 })
404 } else {
405 false
406 }
407 },
408 Err(e) => {
409 dev_log!("daemon", "error: [Daemon] Failed to check process status: {}", e);
410 false
411 },
412 }
413 }
414 }
415
416 async fn CleanupStalePidFile(&self) -> Result<()> {
418 if !self.PidFilePath.exists() {
419 return Ok(());
420 }
421
422 let content = fs::read_to_string(&self.PidFilePath)
424 .map_err(|e| {
425 dev_log!("daemon", "warn: [Daemon] Cannot verify stale PID file: {}", e);
426 return false;
427 })
428 .ok();
429
430 if let Some(content) = content {
431 if content.starts_with(|c:char| c.is_numeric()) {
432 if let Err(e) = fs::remove_file(&self.PidFilePath) {
434 dev_log!("daemon", "warn: [Daemon] Failed to remove stale PID file: {}", e);
435 return Err(AirError::FileSystem(format!("Failed to remove stale PID file: {}", e)));
436 }
437 dev_log!("daemon", "[Daemon] Cleaned up stale PID file");
438 }
439 }
440
441 Ok(())
442 }
443
444 pub async fn ReleaseLock(&self) -> Result<()> {
447 dev_log!("daemon", "[Daemon] Releasing daemon lock...");
448 let _lock = self.PidLock.lock().await;
450
451 *self.IsRunning.write().await = false;
453
454 *self.PidChecksum.lock().await = None;
456
457 if self.PidFilePath.exists() {
459 match fs::remove_file(&self.PidFilePath) {
460 Ok(_) => {
461 dev_log!("daemon", "[Daemon] PID file removed successfully");
462 },
463 Err(e) => {
464 dev_log!("daemon", "error: [Daemon] Failed to remove PID file: {}", e); return Err(AirError::FileSystem(format!("Failed to remove PID file: {}", e)));
466 },
467 }
468 }
469
470 let TempDir = PathBuf::from(format!("{}.tmp", self.PidFilePath.display()));
472 if TempDir.exists() {
473 let _ = fs::remove_file(&TempDir);
474 }
475
476 dev_log!("daemon", "[Daemon] Daemon lock released");
477 Ok(())
478 }
479
480 pub async fn IsRunning(&self) -> bool { *self.IsRunning.read().await }
482
483 pub async fn RequestShutdown(&self) -> Result<()> {
485 dev_log!("daemon", "[Daemon] Requesting graceful shutdown...");
486 *self.ShutdownRequested.write().await = true;
487 Ok(())
488 }
489
490 pub async fn ClearShutdownRequest(&self) -> Result<()> {
492 dev_log!("daemon", "[Daemon] Clearing shutdown request");
493 *self.ShutdownRequested.write().await = false;
494 Ok(())
495 }
496
497 pub async fn IsShutdownRequested(&self) -> bool { *self.ShutdownRequested.read().await }
499
500 pub async fn GetStatus(&self) -> Result<DaemonStatus> {
502 let IsRunning = self.IsRunning().await;
503 let PidFileExists = self.PidFilePath.exists();
504
505 let pid = if PidFileExists {
506 fs::read_to_string(&self.PidFilePath)
507 .ok()
508 .and_then(|content| content.split('|').next().and_then(|s| s.trim().parse().ok()))
509 } else {
510 None
511 };
512
513 Ok(DaemonStatus {
514 IsRunning,
515 PidFileExists,
516 Pid:pid,
517 Platform:self.PlatformInfo.Platform.clone(),
518 ServiceName:self.PlatformInfo.ServiceName.clone(),
519 ShutdownRequested:self.IsShutdownRequested().await,
520 })
521 }
522
523 pub fn GenerateServiceFile(&self) -> Result<String> {
525 match self.PlatformInfo.Platform {
526 Platform::Linux => self.GenerateSystemdService(),
527 Platform::MacOS => self.GenerateLaunchdService(),
528 #[cfg(target_os = "windows")]
529 Platform::Windows => self.GenerateWindowsService(),
530 #[cfg(not(target_os = "windows"))]
531 Platform::Windows => {
532 Err(AirError::ServiceUnavailable(
533 "Windows service generation not available on this platform".to_string(),
534 ))
535 },
536 Platform::Unknown => {
537 Err(AirError::ServiceUnavailable(
538 "Unknown platform, cannot generate service file".to_string(),
539 ))
540 },
541 }
542 }
543
544 fn GenerateSystemdService(&self) -> Result<String> {
546 let ExePath = std::env::current_exe()
547 .map_err(|e| AirError::FileSystem(format!("Failed to get executable path: {}", e)))?;
548
549 let user = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
550 let group = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
551
552 let ServiceContent = format!(
553 r#"[Unit]
554Description=Air Daemon - Background service for Land code editor
555Documentation=man:Air(1)
556After=network-online.target
557Wants=network-online.target
558StartLimitIntervalSec=0
559
560[Service]
561Type=notify
562NotifyAccess=all
563ExecStart={}
564ExecStop=/bin/kill -s TERM $MAINPID
565Restart=always
566RestartSec=5
567StartLimitBurst=3
568User={}
569Group={}
570Environment=RUST_LOG=info
571Environment=DAEMON_MODE=systemd
572Nice=-5
573LimitNOFILE=65536
574LimitNPROC=4096
575
576# Security hardening
577NoNewPrivileges=true
578PrivateTmp=true
579ProtectSystem=strict
580ProtectHome=true
581ReadWritePaths=/var/log/Air /var/run/Air
582RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
583RestrictRealtime=true
584
585[Install]
586WantedBy=multi-user.target
587"#,
588 ExePath.display(),
589 user,
590 group
591 );
592
593 Ok(ServiceContent)
594 }
595
596 fn GenerateLaunchdService(&self) -> Result<String> {
598 let ExePath = std::env::current_exe()
599 .map(|p| p.display().to_string())
600 .unwrap_or_else(|_| "/usr/local/bin/Air".to_string());
601
602 let ServiceName = &self.PlatformInfo.ServiceName;
603 let user = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
604
605 let ServiceContent = format!(
606 r#"<?xml version="1.0" encoding="UTF-8"?>
607<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
608<plist version="1.0">
609<dict>
610 <key>Label</key>
611 <string>{}</string>
612
613 <key>ProgramArguments</key>
614 <array>
615 <string>{}</string>
616 <string>--daemon</string>
617 <string>--mode=launchd</string>
618 </array>
619
620 <key>RunAtLoad</key>
621 <true/>
622
623 <key>KeepAlive</key>
624 <dict>
625 <key>SuccessfulExit</key>
626 <false/>
627 <key>Crashed</key>
628 <true/>
629 </dict>
630
631 <key>ThrottleInterval</key>
632 <integer>5</integer>
633
634 <key>UserName</key>
635 <string>{}</string>
636
637 <key>StandardOutPath</key>
638 <string>/var/log/Air/daemon.log</string>
639
640 <key>StandardErrorPath</key>
641 <string>/var/log/Air/daemon.err</string>
642
643 <key>WorkingDirectory</key>
644 <string>/var/lib/Air</string>
645
646 <key>ProcessType</key>
647 <string>Background</string>
648
649 <key>Nice</key>
650 <integer>-5</integer>
651
652 <key>SoftResourceLimits</key>
653 <dict>
654 <key>NumberOfFiles</key>
655 <integer>65536</integer>
656 </dict>
657
658 <key>HardResourceLimits</key>
659 <dict>
660 <key>NumberOfFiles</key>
661 <integer>65536</integer>
662 </dict>
663
664 <key>EnvironmentVariables</key>
665 <dict>
666 <key>RUST_LOG</key>
667 <string>info</string>
668 <key>DAEMON_MODE</key>
669 <string>launchd</string>
670 </dict>
671</dict>
672</plist>
673"#,
674 ServiceName, ExePath, user
675 );
676
677 Ok(ServiceContent)
678 }
679
680 #[cfg(target_os = "windows")]
686 fn GenerateWindowsService(&self) -> Result<String> {
687 let ExePath = std::env::current_exe()
688 .map(|p| p.display().to_string())
689 .unwrap_or_else(|_| "C:\\Program Files\\Air\\Air.exe".to_string());
690
691 let ServiceName = &self.PlatformInfo.ServiceName;
692 let DisplayName = "Air Daemon Service";
693 let Description = "Background service for Land code editor";
694
695 let ServiceContent = format!(
697 r#"<service>
698 <id>{}</id>
699 <name>{}</name>
700 <description>{}</description>
701 <executable>{}</executable>
702
703 <arguments>--daemon --mode=windows</arguments>
704
705 <startmode>Automatic</startmode>
706 <delayedAutoStart>true</delayedAutoStart>
707
708 <log mode="roll">
709 <sizeThreshold>10240</sizeThreshold>
710 <keepFiles>8</keepFiles>
711 </log>
712
713 <onfailure action="restart" delay="10 sec"/>
714 <onfailure action="restart" delay="20 sec"/>
715 <onfailure action="restart" delay="60 sec"/>
716
717 <resetfailure>1 hour</resetfailure>
718
719 <depend>EventLog</depend>
720 <depend>TcpIp</depend>
721
722 <serviceaccount>
723 <domain>.</domain>
724 <user>LocalSystem</user>
725 <password></password>
726 <allowservicelogon>true</allowservicelogon>
727 </serviceaccount>
728
729 <workingdirectory>C:\Program Files\Air</workingdirectory>
730
731 <env name="RUST_LOG" value="info"/>
732 <env name="DAEMON_MODE" value="windows"/>
733</service>
734"#,
735 ServiceName, DisplayName, Description, ExePath
736 );
737
738 Ok(ServiceContent)
739 }
740
741 pub async fn InstallService(&self) -> Result<()> {
743 dev_log!("daemon", "[Daemon] Installing system service...");
744 match self.PlatformInfo.Platform {
745 Platform::Linux => self.InstallSystemdService().await,
746 Platform::MacOS => self.InstallLaunchdService().await,
747 #[cfg(target_os = "windows")]
748 Platform::Windows => self.InstallWindowsService().await,
749 #[cfg(not(target_os = "windows"))]
750 Platform::Windows => {
751 Err(AirError::ServiceUnavailable(
752 "Windows service installation not available on this platform".to_string(),
753 ))
754 },
755 Platform::Unknown => {
756 Err(AirError::ServiceUnavailable(
757 "Unknown platform, cannot install service".to_string(),
758 ))
759 },
760 }
761 }
762
763 async fn InstallSystemdService(&self) -> Result<()> {
765 let ServiceFileContent = self.GenerateSystemdService()?;
766 let ServiceFilePath = format!("/etc/systemd/system/{}.service", self.PlatformInfo.ServiceName);
767
768 let TempPath = format!("{}.tmp", ServiceFilePath);
770
771 if !ServiceFileContent.contains("[Unit]") || !ServiceFileContent.contains("[Service]") {
773 return Err(AirError::Configuration("Generated service file is invalid".to_string()));
774 }
775
776 fs::write(&TempPath, &ServiceFileContent)
778 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary service file: {}", e)))?;
779
780 #[cfg(unix)]
782 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
783 let _ = fs::remove_file(&TempPath);
784 AirError::FileSystem(format!("Failed to rename service file: {}", e))
785 })?;
786
787 #[cfg(not(unix))]
788 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
789 let _ = fs::remove_file(&TempPath);
790 AirError::FileSystem(format!("Failed to rename service file: {}", e))
791 })?;
792
793 #[cfg(unix)]
795 {
796 use std::os::unix::fs::PermissionsExt;
797 let perms = fs::Permissions::from_mode(0o644);
798 fs::set_permissions(&ServiceFilePath, perms)
799 .map_err(|e| {
800 dev_log!("daemon", "error: [Daemon] Failed to set service file permissions: {}", e);
801 })
802 .ok();
803 }
804
805 dev_log!("daemon", "[Daemon] Systemd service installed at {}", ServiceFilePath);
806 let _ = tokio::process::Command::new("systemctl").args(["daemon-reload"]).output().await;
808
809 Ok(())
810 }
811
812 async fn InstallLaunchdService(&self) -> Result<()> {
814 let ServiceFileContent = self.GenerateLaunchdService()?;
815 let ServiceFilePath = format!("/Library/LaunchDaemons/{}.plist", self.PlatformInfo.ServiceName);
816
817 let TempPath = format!("{}.tmp", ServiceFilePath);
819
820 if !ServiceFileContent.contains("<?xml") || !ServiceFileContent.contains("<!DOCTYPE plist") {
822 return Err(AirError::Configuration("Generated plist file is invalid".to_string()));
823 }
824
825 fs::write(&TempPath, &ServiceFileContent)
827 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary plist file: {}", e)))?;
828
829 #[cfg(unix)]
831 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
832 let _ = fs::remove_file(&TempPath);
833 AirError::FileSystem(format!("Failed to rename plist file: {}", e))
834 })?;
835
836 #[cfg(not(unix))]
837 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
838 let _ = fs::remove_file(&TempPath);
839 AirError::FileSystem(format!("Failed to rename plist file: {}", e))
840 })?;
841
842 #[cfg(unix)]
844 {
845 use std::os::unix::fs::PermissionsExt;
846 let perms = fs::Permissions::from_mode(0o644);
847 fs::set_permissions(&ServiceFilePath, perms)
848 .map_err(|e| {
849 dev_log!("daemon", "error: [Daemon] Failed to set plist file permissions: {}", e);
850 })
851 .ok();
852 }
853
854 dev_log!("daemon", "[Daemon] Launchd service installed at {}", ServiceFilePath);
855 Ok(())
859 }
860
861 #[cfg(target_os = "windows")]
868 async fn InstallWindowsService(&self) -> Result<()> {
869 let ServiceFileContent = self.GenerateWindowsService()?;
870 let ServiceDir = "C:\\ProgramData\\Air";
871 let ServiceFilePath = format!("{}\\{}.xml", ServiceDir, self.PlatformInfo.ServiceName);
872
873 fs::create_dir_all(&ServiceDir)
875 .map_err(|e| AirError::FileSystem(format!("Failed to create service directory: {}", e)))?;
876
877 let TempPath = format!("{}.tmp", ServiceFilePath);
879
880 if !ServiceFileContent.contains("<service>") {
882 return Err(AirError::Configuration("Generated service file is invalid".to_string()));
883 }
884
885 fs::write(&TempPath, &ServiceFileContent)
887 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary service file: {}", e)))?;
888
889 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
891 let _ = fs::remove_file(&TempPath);
892 AirError::FileSystem(format!("Failed to rename service file: {}", e))
893 })?;
894
895 dev_log!(
896 "daemon",
897 "[Daemon] Windows service configuration written to {}",
898 ServiceFilePath
899 );
900 dev_log!("daemon", "[Daemon] To register the service, run:");
901 dev_log!(
902 "daemon",
903 "[Daemon] sc create AirDaemon binPath= \"{}\" DisplayName= \"Air Daemon\"",
904 std::env::current_exe().unwrap_or_else(|_| "air.exe".into()).display()
905 );
906 dev_log!("daemon", "[Daemon] sc config AirDaemon start= auto");
907 dev_log!("daemon", "[Daemon] sc start AirDaemon");
908 Ok(())
909 }
910
911 pub async fn UninstallService(&self) -> Result<()> {
913 dev_log!("daemon", "[Daemon] Uninstalling system service...");
914 match self.PlatformInfo.Platform {
915 Platform::Linux => self.UninstallSystemdService().await,
916 Platform::MacOS => self.UninstallLaunchdService().await,
917 #[cfg(target_os = "windows")]
918 Platform::Windows => self.UninstallWindowsService().await,
919 #[cfg(not(target_os = "windows"))]
920 Platform::Windows => {
921 Err(AirError::ServiceUnavailable(
922 "Windows service uninstallation not available on this platform".to_string(),
923 ))
924 },
925 Platform::Unknown => {
926 Err(AirError::ServiceUnavailable(
927 "Unknown platform, cannot uninstall service".to_string(),
928 ))
929 },
930 }
931 }
932
933 async fn UninstallSystemdService(&self) -> Result<()> {
935 let ServiceFilePath = format!("/etc/systemd/system/{}.service", self.PlatformInfo.ServiceName);
936
937 let _ = tokio::process::Command::new("systemctl")
939 .args(["stop", &self.PlatformInfo.ServiceName])
940 .output()
941 .await;
942
943 let _ = tokio::process::Command::new("systemctl")
945 .args(["disable", &self.PlatformInfo.ServiceName])
946 .output()
947 .await;
948
949 if fs::remove_file(&ServiceFilePath).is_ok() {
951 dev_log!("daemon", "[Daemon] Systemd service file removed");
952 } else {
953 dev_log!("daemon", "warn: [Daemon] Service file {} not found", ServiceFilePath);
954 }
955
956 let _ = tokio::process::Command::new("systemctl").args(["daemon-reload"]).output().await;
958
959 dev_log!("daemon", "[Daemon] Systemd service uninstalled");
960 Ok(())
961 }
962
963 async fn UninstallLaunchdService(&self) -> Result<()> {
965 let ServiceFilePath = format!("/Library/LaunchDaemons/{}.plist", self.PlatformInfo.ServiceName);
966
967 let _ = tokio::process::Command::new("launchctl")
969 .args(["unload", "-w", &ServiceFilePath])
970 .output()
971 .await;
972
973 if fs::remove_file(&ServiceFilePath).is_ok() {
975 dev_log!("daemon", "[Daemon] Launchd service file removed");
976 } else {
977 dev_log!("daemon", "warn: [Daemon] Service file {} not found", ServiceFilePath);
978 }
979
980 dev_log!("daemon", "[Daemon] Launchd service uninstalled");
981 Ok(())
982 }
983
984 #[cfg(target_os = "windows")]
990 async fn UninstallWindowsService(&self) -> Result<()> {
991 let ServiceFilePath = format!("C:\\ProgramData\\Air\\{}.xml", self.PlatformInfo.ServiceName);
992
993 if fs::remove_file(&ServiceFilePath).is_ok() {
995 dev_log!("daemon", "[Daemon] Windows service configuration removed");
996 } else {
997 dev_log!("daemon", "warn: [Daemon] Service file {} not found", ServiceFilePath);
998 }
999
1000 dev_log!("daemon", "[Daemon] To unregister the service, run:");
1001 dev_log!("daemon", "[Daemon] sc stop AirDaemon");
1002 dev_log!("daemon", "[Daemon] sc delete AirDaemon");
1003 Ok(())
1004 }
1005}
1006
1007#[derive(Debug, Clone)]
1009pub struct DaemonStatus {
1010 pub IsRunning:bool,
1011 pub PidFileExists:bool,
1012 pub Pid:Option<u32>,
1013 pub Platform:Platform,
1014 pub ServiceName:String,
1015 pub ShutdownRequested:bool,
1016}
1017
1018impl DaemonStatus {
1019 pub fn status_description(&self) -> String {
1021 if self.IsRunning {
1022 format!("Running (PID: {})", self.Pid.unwrap_or(0))
1023 } else if self.PidFileExists {
1024 "Stale PID file exists".to_string()
1025 } else {
1026 "Not running".to_string()
1027 }
1028 }
1029}
1030
1031impl From<ExitCode> for i32 {
1032 fn from(code:ExitCode) -> i32 { code as i32 }
1033}