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 AirError::FileSystem(format!("Failed to rename PID file: {}", e))
320 })?;
321
322 #[cfg(not(unix))]
323 fs::rename(&TempDir, &self.PidFilePath).map_err(|e| {
324 let _ = fs::remove_file(&TempDir);
325 AirError::FileSystem(format!("Failed to rename PID file: {}", e))
326 })?;
327
328 *self.PidChecksum.lock().await = Some(checksum);
330
331 *self.IsRunning.write().await = true;
333
334 #[cfg(unix)]
336 {
337 use std::os::unix::fs::PermissionsExt;
338
339 let perms = fs::Permissions::from_mode(0o600);
340
341 if let Err(e) = fs::set_permissions(&self.PidFilePath, perms) {
342 dev_log!("daemon", "warn: [Daemon] Failed to set PID file permissions: {}", e);
343 }
344 }
345
346 dev_log!("daemon", "[Daemon] Daemon lock acquired (PID: {})", pid);
347
348 Ok(())
349 }
350
351 pub async fn IsAlreadyRunning(&self) -> Result<bool> {
358 if !self.PidFilePath.exists() {
359 dev_log!("daemon", "[Daemon] PID file does not exist");
360
361 return Ok(false);
362 }
363
364 let PidContent = fs::read_to_string(&self.PidFilePath)
366 .map_err(|e| AirError::FileSystem(format!("Failed to read PID file: {}", e)))?;
367
368 let parts:Vec<&str> = PidContent.split('|').collect();
370
371 if parts.len() < 2 {
372 dev_log!("daemon", "warn: [Daemon] Invalid PID file format, treating as stale");
373
374 self.CleanupStalePidFile().await?;
375
376 return Ok(false);
377 }
378
379 let pid:u32 = parts[0].trim().parse().map_err(|e| {
380 dev_log!("daemon", "warn: [Daemon] Invalid PID in file: {}", e);
381 AirError::FileSystem("Invalid PID file content".to_string())
382 })?;
383
384 if parts.len() >= 3 && parts[1].starts_with("CHECKSUM:") {
386 let StoredChecksum = &parts[1][9..]; let CurrentChecksum = self.PidChecksum.lock().await;
388
389 if let Some(ref cksum) = *CurrentChecksum {
390 if cksum != StoredChecksum {
391 dev_log!("daemon", "warn: [Daemon] PID file checksum mismatch, file may be corrupted"); return Ok(true);
393 }
394 }
395 }
396
397 let IsRunning = Self::ValidateProcess(pid);
399
400 if !IsRunning {
401 dev_log!("daemon", "warn: [Daemon] Detected stale PID file for PID {}", pid);
403
404 self.CleanupStalePidFile().await?;
405 }
406
407 Ok(IsRunning)
408 }
409
410 fn ValidateProcess(pid:u32) -> bool {
413 #[cfg(unix)]
414 {
415 use std::process::Command;
416
417 let output = Command::new("ps").arg("-p").arg(pid.to_string()).output();
418
419 match output {
420 Ok(output) => {
421 if output.status.success() {
422 let stdout = String::from_utf8_lossy(&output.stdout);
423
424 stdout
426 .lines()
427 .skip(1)
428 .any(|line| line.contains("Air") || line.contains("daemon"))
429 } else {
430 false
431 }
432 },
433
434 Err(e) => {
435 dev_log!("daemon", "error: [Daemon] Failed to check process status: {}", e);
436
437 false
438 },
439 }
440 }
441
442 #[cfg(windows)]
443 {
444 use std::process::Command;
445
446 let output = Command::new("tasklist")
447 .arg("/FI")
448 .arg(format!("PID eq {}", pid))
449 .arg("/FO")
450 .arg("CSV")
451 .output();
452
453 match output {
454 Ok(output) => {
455 if output.status.success() {
456 let stdout = String::from_utf8_lossy(&output.stdout);
457
458 stdout.lines().any(|line| {
459 line.contains(&pid.to_string()) && (line.contains("Air") || line.contains("daemon"))
460 })
461 } else {
462 false
463 }
464 },
465
466 Err(e) => {
467 dev_log!("daemon", "error: [Daemon] Failed to check process status: {}", e);
468
469 false
470 },
471 }
472 }
473 }
474
475 async fn CleanupStalePidFile(&self) -> Result<()> {
477 if !self.PidFilePath.exists() {
478 return Ok(());
479 }
480
481 let content = fs::read_to_string(&self.PidFilePath)
483 .map_err(|e| {
484 dev_log!("daemon", "warn: [Daemon] Cannot verify stale PID file: {}", e);
485 return false;
486 })
487 .ok();
488
489 if let Some(content) = content {
490 if content.starts_with(|c:char| c.is_numeric()) {
491 if let Err(e) = fs::remove_file(&self.PidFilePath) {
493 dev_log!("daemon", "warn: [Daemon] Failed to remove stale PID file: {}", e);
494
495 return Err(AirError::FileSystem(format!("Failed to remove stale PID file: {}", e)));
496 }
497
498 dev_log!("daemon", "[Daemon] Cleaned up stale PID file");
499 }
500 }
501
502 Ok(())
503 }
504
505 pub async fn ReleaseLock(&self) -> Result<()> {
508 dev_log!("daemon", "[Daemon] Releasing daemon lock...");
509
510 let _lock = self.PidLock.lock().await;
512
513 *self.IsRunning.write().await = false;
515
516 *self.PidChecksum.lock().await = None;
518
519 if self.PidFilePath.exists() {
521 match fs::remove_file(&self.PidFilePath) {
522 Ok(_) => {
523 dev_log!("daemon", "[Daemon] PID file removed successfully");
524 },
525
526 Err(e) => {
527 dev_log!("daemon", "error: [Daemon] Failed to remove PID file: {}", e); return Err(AirError::FileSystem(format!("Failed to remove PID file: {}", e)));
529 },
530 }
531 }
532
533 let TempDir = PathBuf::from(format!("{}.tmp", self.PidFilePath.display()));
535
536 if TempDir.exists() {
537 let _ = fs::remove_file(&TempDir);
538 }
539
540 dev_log!("daemon", "[Daemon] Daemon lock released");
541
542 Ok(())
543 }
544
545 pub async fn IsRunning(&self) -> bool { *self.IsRunning.read().await }
547
548 pub async fn RequestShutdown(&self) -> Result<()> {
550 dev_log!("daemon", "[Daemon] Requesting graceful shutdown...");
551
552 *self.ShutdownRequested.write().await = true;
553 Ok(())
554 }
555
556 pub async fn ClearShutdownRequest(&self) -> Result<()> {
558 dev_log!("daemon", "[Daemon] Clearing shutdown request");
559
560 *self.ShutdownRequested.write().await = false;
561 Ok(())
562 }
563
564 pub async fn IsShutdownRequested(&self) -> bool { *self.ShutdownRequested.read().await }
566
567 pub async fn GetStatus(&self) -> Result<DaemonStatus> {
569 let IsRunning = self.IsRunning().await;
570
571 let PidFileExists = self.PidFilePath.exists();
572
573 let pid = if PidFileExists {
574 fs::read_to_string(&self.PidFilePath)
575 .ok()
576 .and_then(|content| content.split('|').next().and_then(|s| s.trim().parse().ok()))
577 } else {
578 None
579 };
580
581 Ok(DaemonStatus {
582 IsRunning,
583 PidFileExists,
584 Pid:pid,
585 Platform:self.PlatformInfo.Platform.clone(),
586 ServiceName:self.PlatformInfo.ServiceName.clone(),
587 ShutdownRequested:self.IsShutdownRequested().await,
588 })
589 }
590
591 pub fn GenerateServiceFile(&self) -> Result<String> {
593 match self.PlatformInfo.Platform {
594 Platform::Linux => self.GenerateSystemdService(),
595
596 Platform::MacOS => self.GenerateLaunchdService(),
597
598 #[cfg(target_os = "windows")]
599 Platform::Windows => self.GenerateWindowsService(),
600
601 #[cfg(not(target_os = "windows"))]
602 Platform::Windows => {
603 Err(AirError::ServiceUnavailable(
604 "Windows service generation not available on this platform".to_string(),
605 ))
606 },
607
608 Platform::Unknown => {
609 Err(AirError::ServiceUnavailable(
610 "Unknown platform, cannot generate service file".to_string(),
611 ))
612 },
613 }
614 }
615
616 fn GenerateSystemdService(&self) -> Result<String> {
618 let ExePath = std::env::current_exe()
619 .map_err(|e| AirError::FileSystem(format!("Failed to get executable path: {}", e)))?;
620
621 let user = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
622
623 let group = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
624
625 let ServiceContent = format!(
626 r#"[Unit]
627Description=Air Daemon - Background service for Land code editor
628Documentation=man:Air(1)
629After=network-online.target
630Wants=network-online.target
631StartLimitIntervalSec=0
632
633[Service]
634Type=notify
635NotifyAccess=all
636ExecStart={}
637ExecStop=/bin/kill -s TERM $MAINPID
638Restart=always
639RestartSec=5
640StartLimitBurst=3
641User={}
642Group={}
643Environment=RUST_LOG=info
644Environment=DAEMON_MODE=systemd
645Nice=-5
646LimitNOFILE=65536
647LimitNPROC=4096
648
649# Security hardening
650NoNewPrivileges=true
651PrivateTmp=true
652ProtectSystem=strict
653ProtectHome=true
654ReadWritePaths=/var/log/Air /var/run/Air
655RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
656RestrictRealtime=true
657
658[Install]
659WantedBy=multi-user.target
660"#,
661 ExePath.display(),
662 user,
663 group
664 );
665
666 Ok(ServiceContent)
667 }
668
669 fn GenerateLaunchdService(&self) -> Result<String> {
671 let ExePath = std::env::current_exe()
672 .map(|p| p.display().to_string())
673 .unwrap_or_else(|_| "/usr/local/bin/Air".to_string());
674
675 let ServiceName = &self.PlatformInfo.ServiceName;
676
677 let user = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
678
679 let ServiceContent = format!(
680 r#"<?xml version="1.0" encoding="UTF-8"?>
681<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
682<plist version="1.0">
683<dict>
684 <key>Label</key>
685 <string>{}</string>
686
687 <key>ProgramArguments</key>
688 <array>
689 <string>{}</string>
690 <string>--daemon</string>
691 <string>--mode=launchd</string>
692 </array>
693
694 <key>RunAtLoad</key>
695 <true/>
696
697 <key>KeepAlive</key>
698 <dict>
699 <key>SuccessfulExit</key>
700 <false/>
701 <key>Crashed</key>
702 <true/>
703 </dict>
704
705 <key>ThrottleInterval</key>
706 <integer>5</integer>
707
708 <key>UserName</key>
709 <string>{}</string>
710
711 <key>StandardOutPath</key>
712 <string>/var/log/Air/daemon.log</string>
713
714 <key>StandardErrorPath</key>
715 <string>/var/log/Air/daemon.err</string>
716
717 <key>WorkingDirectory</key>
718 <string>/var/lib/Air</string>
719
720 <key>ProcessType</key>
721 <string>Background</string>
722
723 <key>Nice</key>
724 <integer>-5</integer>
725
726 <key>SoftResourceLimits</key>
727 <dict>
728 <key>NumberOfFiles</key>
729 <integer>65536</integer>
730 </dict>
731
732 <key>HardResourceLimits</key>
733 <dict>
734 <key>NumberOfFiles</key>
735 <integer>65536</integer>
736 </dict>
737
738 <key>EnvironmentVariables</key>
739 <dict>
740 <key>RUST_LOG</key>
741 <string>info</string>
742 <key>DAEMON_MODE</key>
743 <string>launchd</string>
744 </dict>
745</dict>
746</plist>
747"#,
748 ServiceName, ExePath, user
749 );
750
751 Ok(ServiceContent)
752 }
753
754 #[cfg(target_os = "windows")]
760 fn GenerateWindowsService(&self) -> Result<String> {
761 let ExePath = std::env::current_exe()
762 .map(|p| p.display().to_string())
763 .unwrap_or_else(|_| "C:\\Program Files\\Air\\Air.exe".to_string());
764
765 let ServiceName = &self.PlatformInfo.ServiceName;
766
767 let DisplayName = "Air Daemon Service";
768
769 let Description = "Background service for Land code editor";
770
771 let ServiceContent = format!(
773 r#"<service>
774 <id>{}</id>
775 <name>{}</name>
776 <description>{}</description>
777 <executable>{}</executable>
778
779 <arguments>--daemon --mode=windows</arguments>
780
781 <startmode>Automatic</startmode>
782 <delayedAutoStart>true</delayedAutoStart>
783
784 <log mode="roll">
785 <sizeThreshold>10240</sizeThreshold>
786 <keepFiles>8</keepFiles>
787 </log>
788
789 <onfailure action="restart" delay="10 sec"/>
790 <onfailure action="restart" delay="20 sec"/>
791 <onfailure action="restart" delay="60 sec"/>
792
793 <resetfailure>1 hour</resetfailure>
794
795 <depend>EventLog</depend>
796 <depend>TcpIp</depend>
797
798 <serviceaccount>
799 <domain>.</domain>
800 <user>LocalSystem</user>
801 <password></password>
802 <allowservicelogon>true</allowservicelogon>
803 </serviceaccount>
804
805 <workingdirectory>C:\Program Files\Air</workingdirectory>
806
807 <env name="RUST_LOG" value="info"/>
808 <env name="DAEMON_MODE" value="windows"/>
809</service>
810"#,
811 ServiceName, DisplayName, Description, ExePath
812 );
813
814 Ok(ServiceContent)
815 }
816
817 pub async fn InstallService(&self) -> Result<()> {
819 dev_log!("daemon", "[Daemon] Installing system service...");
820
821 match self.PlatformInfo.Platform {
822 Platform::Linux => self.InstallSystemdService().await,
823
824 Platform::MacOS => self.InstallLaunchdService().await,
825
826 #[cfg(target_os = "windows")]
827 Platform::Windows => self.InstallWindowsService().await,
828
829 #[cfg(not(target_os = "windows"))]
830 Platform::Windows => {
831 Err(AirError::ServiceUnavailable(
832 "Windows service installation not available on this platform".to_string(),
833 ))
834 },
835
836 Platform::Unknown => {
837 Err(AirError::ServiceUnavailable(
838 "Unknown platform, cannot install service".to_string(),
839 ))
840 },
841 }
842 }
843
844 async fn InstallSystemdService(&self) -> Result<()> {
846 let ServiceFileContent = self.GenerateSystemdService()?;
847
848 let ServiceFilePath = format!("/etc/systemd/system/{}.service", self.PlatformInfo.ServiceName);
849
850 let TempPath = format!("{}.tmp", ServiceFilePath);
852
853 if !ServiceFileContent.contains("[Unit]") || !ServiceFileContent.contains("[Service]") {
855 return Err(AirError::Configuration("Generated service file is invalid".to_string()));
856 }
857
858 fs::write(&TempPath, &ServiceFileContent)
860 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary service file: {}", e)))?;
861
862 #[cfg(unix)]
864 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
865 let _ = fs::remove_file(&TempPath);
866 AirError::FileSystem(format!("Failed to rename service file: {}", e))
867 })?;
868
869 #[cfg(not(unix))]
870 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
871 let _ = fs::remove_file(&TempPath);
872 AirError::FileSystem(format!("Failed to rename service file: {}", e))
873 })?;
874
875 #[cfg(unix)]
877 {
878 use std::os::unix::fs::PermissionsExt;
879
880 let perms = fs::Permissions::from_mode(0o644);
881
882 fs::set_permissions(&ServiceFilePath, perms)
883 .map_err(|e| {
884 dev_log!("daemon", "error: [Daemon] Failed to set service file permissions: {}", e);
885 })
886 .ok();
887 }
888
889 dev_log!("daemon", "[Daemon] Systemd service installed at {}", ServiceFilePath);
890
891 let _ = tokio::process::Command::new("systemctl").args(["daemon-reload"]).output().await;
893
894 Ok(())
895 }
896
897 async fn InstallLaunchdService(&self) -> Result<()> {
899 let ServiceFileContent = self.GenerateLaunchdService()?;
900
901 let ServiceFilePath = format!("/Library/LaunchDaemons/{}.plist", self.PlatformInfo.ServiceName);
902
903 let TempPath = format!("{}.tmp", ServiceFilePath);
905
906 if !ServiceFileContent.contains("<?xml") || !ServiceFileContent.contains("<!DOCTYPE plist") {
908 return Err(AirError::Configuration("Generated plist file is invalid".to_string()));
909 }
910
911 fs::write(&TempPath, &ServiceFileContent)
913 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary plist file: {}", e)))?;
914
915 #[cfg(unix)]
917 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
918 let _ = fs::remove_file(&TempPath);
919 AirError::FileSystem(format!("Failed to rename plist file: {}", e))
920 })?;
921
922 #[cfg(not(unix))]
923 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
924 let _ = fs::remove_file(&TempPath);
925 AirError::FileSystem(format!("Failed to rename plist file: {}", e))
926 })?;
927
928 #[cfg(unix)]
930 {
931 use std::os::unix::fs::PermissionsExt;
932
933 let perms = fs::Permissions::from_mode(0o644);
934
935 fs::set_permissions(&ServiceFilePath, perms)
936 .map_err(|e| {
937 dev_log!("daemon", "error: [Daemon] Failed to set plist file permissions: {}", e);
938 })
939 .ok();
940 }
941
942 dev_log!("daemon", "[Daemon] Launchd service installed at {}", ServiceFilePath);
943
944 Ok(())
948 }
949
950 #[cfg(target_os = "windows")]
957 async fn InstallWindowsService(&self) -> Result<()> {
958 let ServiceFileContent = self.GenerateWindowsService()?;
959
960 let ServiceDir = "C:\\ProgramData\\Air";
961
962 let ServiceFilePath = format!("{}\\{}.xml", ServiceDir, self.PlatformInfo.ServiceName);
963
964 fs::create_dir_all(&ServiceDir)
966 .map_err(|e| AirError::FileSystem(format!("Failed to create service directory: {}", e)))?;
967
968 let TempPath = format!("{}.tmp", ServiceFilePath);
970
971 if !ServiceFileContent.contains("<service>") {
973 return Err(AirError::Configuration("Generated service file is invalid".to_string()));
974 }
975
976 fs::write(&TempPath, &ServiceFileContent)
978 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary service file: {}", e)))?;
979
980 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
982 let _ = fs::remove_file(&TempPath);
983 AirError::FileSystem(format!("Failed to rename service file: {}", e))
984 })?;
985
986 dev_log!(
987 "daemon",
988 "[Daemon] Windows service configuration written to {}",
989 ServiceFilePath
990 );
991
992 dev_log!("daemon", "[Daemon] To register the service, run:");
993
994 dev_log!(
995 "daemon",
996 "[Daemon] sc create AirDaemon binPath= \"{}\" DisplayName= \"Air Daemon\"",
997 std::env::current_exe().unwrap_or_else(|_| "air.exe".into()).display()
998 );
999
1000 dev_log!("daemon", "[Daemon] sc config AirDaemon start= auto");
1001
1002 dev_log!("daemon", "[Daemon] sc start AirDaemon");
1003
1004 Ok(())
1005 }
1006
1007 pub async fn UninstallService(&self) -> Result<()> {
1009 dev_log!("daemon", "[Daemon] Uninstalling system service...");
1010
1011 match self.PlatformInfo.Platform {
1012 Platform::Linux => self.UninstallSystemdService().await,
1013
1014 Platform::MacOS => self.UninstallLaunchdService().await,
1015
1016 #[cfg(target_os = "windows")]
1017 Platform::Windows => self.UninstallWindowsService().await,
1018
1019 #[cfg(not(target_os = "windows"))]
1020 Platform::Windows => {
1021 Err(AirError::ServiceUnavailable(
1022 "Windows service uninstallation not available on this platform".to_string(),
1023 ))
1024 },
1025
1026 Platform::Unknown => {
1027 Err(AirError::ServiceUnavailable(
1028 "Unknown platform, cannot uninstall service".to_string(),
1029 ))
1030 },
1031 }
1032 }
1033
1034 async fn UninstallSystemdService(&self) -> Result<()> {
1036 let ServiceFilePath = format!("/etc/systemd/system/{}.service", self.PlatformInfo.ServiceName);
1037
1038 let _ = tokio::process::Command::new("systemctl")
1040 .args(["stop", &self.PlatformInfo.ServiceName])
1041 .output()
1042 .await;
1043
1044 let _ = tokio::process::Command::new("systemctl")
1046 .args(["disable", &self.PlatformInfo.ServiceName])
1047 .output()
1048 .await;
1049
1050 if fs::remove_file(&ServiceFilePath).is_ok() {
1052 dev_log!("daemon", "[Daemon] Systemd service file removed");
1053 } else {
1054 dev_log!("daemon", "warn: [Daemon] Service file {} not found", ServiceFilePath);
1055 }
1056
1057 let _ = tokio::process::Command::new("systemctl").args(["daemon-reload"]).output().await;
1059
1060 dev_log!("daemon", "[Daemon] Systemd service uninstalled");
1061
1062 Ok(())
1063 }
1064
1065 async fn UninstallLaunchdService(&self) -> Result<()> {
1067 let ServiceFilePath = format!("/Library/LaunchDaemons/{}.plist", self.PlatformInfo.ServiceName);
1068
1069 let _ = tokio::process::Command::new("launchctl")
1071 .args(["unload", "-w", &ServiceFilePath])
1072 .output()
1073 .await;
1074
1075 if fs::remove_file(&ServiceFilePath).is_ok() {
1077 dev_log!("daemon", "[Daemon] Launchd service file removed");
1078 } else {
1079 dev_log!("daemon", "warn: [Daemon] Service file {} not found", ServiceFilePath);
1080 }
1081
1082 dev_log!("daemon", "[Daemon] Launchd service uninstalled");
1083
1084 Ok(())
1085 }
1086
1087 #[cfg(target_os = "windows")]
1093 async fn UninstallWindowsService(&self) -> Result<()> {
1094 let ServiceFilePath = format!("C:\\ProgramData\\Air\\{}.xml", self.PlatformInfo.ServiceName);
1095
1096 if fs::remove_file(&ServiceFilePath).is_ok() {
1098 dev_log!("daemon", "[Daemon] Windows service configuration removed");
1099 } else {
1100 dev_log!("daemon", "warn: [Daemon] Service file {} not found", ServiceFilePath);
1101 }
1102
1103 dev_log!("daemon", "[Daemon] To unregister the service, run:");
1104
1105 dev_log!("daemon", "[Daemon] sc stop AirDaemon");
1106
1107 dev_log!("daemon", "[Daemon] sc delete AirDaemon");
1108
1109 Ok(())
1110 }
1111}
1112
1113#[derive(Debug, Clone)]
1115pub struct DaemonStatus {
1116 pub IsRunning:bool,
1117
1118 pub PidFileExists:bool,
1119
1120 pub Pid:Option<u32>,
1121
1122 pub Platform:Platform,
1123
1124 pub ServiceName:String,
1125
1126 pub ShutdownRequested:bool,
1127}
1128
1129impl DaemonStatus {
1130 pub fn status_description(&self) -> String {
1132 if self.IsRunning {
1133 format!("Running (PID: {})", self.Pid.unwrap_or(0))
1134 } else if self.PidFileExists {
1135 "Stale PID file exists".to_string()
1136 } else {
1137 "Not running".to_string()
1138 }
1139 }
1140}
1141
1142impl From<ExitCode> for i32 {
1143 fn from(code:ExitCode) -> i32 { code as i32 }
1144}