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
104 IsRunning:Arc<RwLock<bool>>,
106
107 PlatformInfo:PlatformInfo,
109
110 PidLock:Arc<Mutex<()>>,
112
113 PidChecksum:Arc<Mutex<Option<String>>>,
115
116 ShutdownRequested:Arc<RwLock<bool>>,
118}
119
120#[derive(Debug)]
122pub struct PlatformInfo {
123 pub Platform:Platform,
125
126 pub ServiceName:String,
128
129 pub RunAsUser:Option<String>,
131}
132
133#[derive(Debug, Clone, PartialEq)]
135pub enum Platform {
136 Linux,
137
138 MacOS,
139
140 Windows,
141
142 Unknown,
143}
144
145#[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 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 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 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 fn DetectPlatformInfo() -> PlatformInfo {
218 let platform = Self::DetectPlatform();
219
220 let ServiceName = "Air-daemon".to_string();
221
222 let RunAsUser = std::env::var("USER").ok().or_else(|| std::env::var("USERNAME").ok());
224
225 PlatformInfo { Platform:platform, ServiceName, RunAsUser }
226 }
227
228 pub async fn AcquireLock(&self) -> Result<()> {
236 dev_log!("daemon", "[Daemon] Acquiring daemon lock...");
237
238 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 if *self.ShutdownRequested.read().await {
258 return Err(AirError::ServiceUnavailable(
259 "Shutdown requested, cannot acquire lock".to_string(),
260 ));
261 }
262
263 if self.IsAlreadyRunning().await? {
265 return Err(AirError::ServiceUnavailable("Air daemon is already running".to_string()));
266 }
267
268 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 #[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 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 let mut hasher = Sha256::new();
299
300 hasher.update(PidContent.as_bytes());
301
302 let checksum = hex::encode(hasher.finalize());
307
308 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 #[cfg(unix)]
316 fs::rename(&TempDir, &self.PidFilePath).map_err(|e| {
317 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 *self.PidChecksum.lock().await = Some(checksum);
332
333 *self.IsRunning.write().await = true;
335
336 #[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 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 let PidContent = fs::read_to_string(&self.PidFilePath)
368 .map_err(|e| AirError::FileSystem(format!("Failed to read PID file: {}", e)))?;
369
370 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 if parts.len() >= 3 && parts[1].starts_with("CHECKSUM:") {
389 let StoredChecksum = &parts[1][9..]; 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"); return Ok(true);
398 }
399 }
400 }
401
402 let IsRunning = Self::ValidateProcess(pid);
404
405 if !IsRunning {
406 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 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 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 async fn CleanupStalePidFile(&self) -> Result<()> {
482 if !self.PidFilePath.exists() {
483 return Ok(());
484 }
485
486 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 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 pub async fn ReleaseLock(&self) -> Result<()> {
514 dev_log!("daemon", "[Daemon] Releasing daemon lock...");
515
516 let _lock = self.PidLock.lock().await;
518
519 *self.IsRunning.write().await = false;
521
522 *self.PidChecksum.lock().await = None;
524
525 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); return Err(AirError::FileSystem(format!("Failed to remove PID file: {}", e)));
536 },
537 }
538 }
539
540 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 pub async fn IsRunning(&self) -> bool { *self.IsRunning.read().await }
554
555 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 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 pub async fn IsShutdownRequested(&self) -> bool { *self.ShutdownRequested.read().await }
573
574 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 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 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 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 #[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 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 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 async fn InstallSystemdService(&self) -> Result<()> {
853 let ServiceFileContent = self.GenerateSystemdService()?;
854
855 let ServiceFilePath = format!("/etc/systemd/system/{}.service", self.PlatformInfo.ServiceName);
856
857 let TempPath = format!("{}.tmp", ServiceFilePath);
859
860 if !ServiceFileContent.contains("[Unit]") || !ServiceFileContent.contains("[Service]") {
862 return Err(AirError::Configuration("Generated service file is invalid".to_string()));
863 }
864
865 fs::write(&TempPath, &ServiceFileContent)
867 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary service file: {}", e)))?;
868
869 #[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 #[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 let _ = tokio::process::Command::new("systemctl").args(["daemon-reload"]).output().await;
902
903 Ok(())
904 }
905
906 async fn InstallLaunchdService(&self) -> Result<()> {
908 let ServiceFileContent = self.GenerateLaunchdService()?;
909
910 let ServiceFilePath = format!("/Library/LaunchDaemons/{}.plist", self.PlatformInfo.ServiceName);
911
912 let TempPath = format!("{}.tmp", ServiceFilePath);
914
915 if !ServiceFileContent.contains("<?xml") || !ServiceFileContent.contains("<!DOCTYPE plist") {
917 return Err(AirError::Configuration("Generated plist file is invalid".to_string()));
918 }
919
920 fs::write(&TempPath, &ServiceFileContent)
922 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary plist file: {}", e)))?;
923
924 #[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 #[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 Ok(())
959 }
960
961 #[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 fs::create_dir_all(&ServiceDir)
977 .map_err(|e| AirError::FileSystem(format!("Failed to create service directory: {}", e)))?;
978
979 let TempPath = format!("{}.tmp", ServiceFilePath);
981
982 if !ServiceFileContent.contains("<service>") {
984 return Err(AirError::Configuration("Generated service file is invalid".to_string()));
985 }
986
987 fs::write(&TempPath, &ServiceFileContent)
989 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary service file: {}", e)))?;
990
991 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 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 async fn UninstallSystemdService(&self) -> Result<()> {
1048 let ServiceFilePath = format!("/etc/systemd/system/{}.service", self.PlatformInfo.ServiceName);
1049
1050 let _ = tokio::process::Command::new("systemctl")
1052 .args(["stop", &self.PlatformInfo.ServiceName])
1053 .output()
1054 .await;
1055
1056 let _ = tokio::process::Command::new("systemctl")
1058 .args(["disable", &self.PlatformInfo.ServiceName])
1059 .output()
1060 .await;
1061
1062 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 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 async fn UninstallLaunchdService(&self) -> Result<()> {
1079 let ServiceFilePath = format!("/Library/LaunchDaemons/{}.plist", self.PlatformInfo.ServiceName);
1080
1081 let _ = tokio::process::Command::new("launchctl")
1083 .args(["unload", "-w", &ServiceFilePath])
1084 .output()
1085 .await;
1086
1087 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 #[cfg(target_os = "windows")]
1105 async fn UninstallWindowsService(&self) -> Result<()> {
1106 let ServiceFilePath = format!("C:\\ProgramData\\Air\\{}.xml", self.PlatformInfo.ServiceName);
1107
1108 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#[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 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}