1use std::{fs, path::PathBuf, sync::Arc, time::Duration};
93
94use log::{debug, error, info, warn};
95use tokio::sync::{Mutex, RwLock};
96use sha2::{Digest, Sha256};
97
98use crate::{AirError, Result};
99
100#[derive(Debug)]
102pub struct DaemonManager {
103 PidFilePath:PathBuf,
105 IsRunning:Arc<RwLock<bool>>,
107 PlatformInfo:PlatformInfo,
109 PidLock:Arc<Mutex<()>>,
111 PidChecksum:Arc<Mutex<Option<String>>>,
113 ShutdownRequested:Arc<RwLock<bool>>,
115}
116
117#[derive(Debug)]
119pub struct PlatformInfo {
120 pub Platform:Platform,
122 pub ServiceName:String,
124 pub RunAsUser:Option<String>,
126}
127
128#[derive(Debug, Clone, PartialEq)]
130pub enum Platform {
131 Linux,
132 MacOS,
133 Windows,
134 Unknown,
135}
136
137#[derive(Debug, Clone)]
139pub enum ExitCode {
140 Success = 0,
141 ConfigurationError = 1,
142 AlreadyRunning = 2,
143 PermissionDenied = 3,
144 ServiceError = 4,
145 ResourceError = 5,
146 NetworkError = 6,
147 AuthenticationError = 7,
148 FileSystemError = 8,
149 InternalError = 9,
150 UnknownError = 10,
151}
152
153impl DaemonManager {
154 pub fn New(PidFilePath:Option<PathBuf>) -> Result<Self> {
156 let PidFilePath = PidFilePath.unwrap_or_else(|| Self::DefaultPidFilePath());
157 let PlatformInfo = Self::DetectPlatformInfo();
158
159 Ok(Self {
160 PidFilePath,
161 IsRunning:Arc::new(RwLock::new(false)),
162 PlatformInfo,
163 PidLock:Arc::new(Mutex::new(())),
164 PidChecksum:Arc::new(Mutex::new(None)),
165 ShutdownRequested:Arc::new(RwLock::new(false)),
166 })
167 }
168
169 fn DefaultPidFilePath() -> PathBuf {
171 let platform = Self::DetectPlatform();
172 match platform {
173 Platform::Linux => PathBuf::from("/var/run/Air.pid"),
174 Platform::MacOS => PathBuf::from("/tmp/Air.pid"),
175 Platform::Windows => PathBuf::from("C:\\ProgramData\\Air\\Air.pid"),
176 Platform::Unknown => PathBuf::from("./Air.pid"),
177 }
178 }
179
180 fn DetectPlatform() -> Platform {
182 if cfg!(target_os = "linux") {
183 Platform::Linux
184 } else if cfg!(target_os = "macos") {
185 Platform::MacOS
186 } else if cfg!(target_os = "windows") {
187 Platform::Windows
188 } else {
189 Platform::Unknown
190 }
191 }
192
193 fn DetectPlatformInfo() -> PlatformInfo {
195 let platform = Self::DetectPlatform();
196 let ServiceName = "Air-daemon".to_string();
197
198 let RunAsUser = std::env::var("USER").ok().or_else(|| std::env::var("USERNAME").ok());
200
201 PlatformInfo { Platform:platform, ServiceName, RunAsUser }
202 }
203
204 pub async fn AcquireLock(&self) -> Result<()> {
212 info!("[Daemon] Acquiring daemon lock...");
213
214 tokio::select! {
216 _ = tokio::time::timeout(Duration::from_secs(30), self.PidLock.lock()) => {
217 let _lock_guard = self.PidLock.lock().await;
218 },
219 _ = tokio::time::sleep(Duration::from_secs(30)) => {
220 return Err(AirError::Internal(
221 "Timeout acquiring PID lock".to_string()
222 ));
223 }
224 }
225
226 let _lock = self.PidLock.lock().await;
227
228 if *self.ShutdownRequested.read().await {
230 return Err(AirError::ServiceUnavailable(
231 "Shutdown requested, cannot acquire lock".to_string(),
232 ));
233 }
234
235 if self.IsAlreadyRunning().await? {
237 return Err(AirError::ServiceUnavailable("Air daemon is already running".to_string()));
238 }
239
240 let TempDir = PathBuf::from(format!("{}.tmp", self.PidFilePath.display()));
242 if let Some(parent) = self.PidFilePath.parent() {
243 fs::create_dir_all(parent)
244 .map_err(|e| AirError::FileSystem(format!("Failed to create PID directory: {}", e)))?;
245
246 #[cfg(unix)]
248 {
249 use std::os::unix::fs::PermissionsExt;
250 let perms = fs::Permissions::from_mode(0o700);
251 fs::set_permissions(parent, perms)
252 .map_err(|e| AirError::FileSystem(format!("Failed to set directory permissions: {}", e)))?;
253 }
254 }
255
256 let pid = std::process::id();
258 let timestamp = std::time::SystemTime::now()
259 .duration_since(std::time::UNIX_EPOCH)
260 .unwrap()
261 .as_secs();
262 let PidContent = format!("{}|{}", pid, timestamp);
263
264 let mut hasher = Sha256::new();
266 hasher.update(PidContent.as_bytes());
267 let checksum = format!("{:x}", hasher.finalize());
268
269 let TempFileContent = format!("{}|CHECKSUM:{}", PidContent, checksum);
271 fs::write(&TempDir, &TempFileContent)
272 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary PID file: {}", e)))?;
273
274 #[cfg(unix)]
276 fs::rename(&TempDir, &self.PidFilePath).map_err(|e| {
277 let _ = fs::remove_file(&TempDir);
279 AirError::FileSystem(format!("Failed to rename PID file: {}", e))
280 })?;
281
282 #[cfg(not(unix))]
283 fs::rename(&TempDir, &self.PidFilePath).map_err(|e| {
284 let _ = fs::remove_file(&TempDir);
285 AirError::FileSystem(format!("Failed to rename PID file: {}", e))
286 })?;
287
288 *self.PidChecksum.lock().await = Some(checksum);
290
291 *self.IsRunning.write().await = true;
293
294 #[cfg(unix)]
296 {
297 use std::os::unix::fs::PermissionsExt;
298 let perms = fs::Permissions::from_mode(0o600);
299 if let Err(e) = fs::set_permissions(&self.PidFilePath, perms) {
300 warn!("[Daemon] Failed to set PID file permissions: {}", e);
301 }
302 }
303
304 info!("[Daemon] Daemon lock acquired (PID: {})", pid);
305 Ok(())
306 }
307
308 pub async fn IsAlreadyRunning(&self) -> Result<bool> {
315 if !self.PidFilePath.exists() {
316 debug!("[Daemon] PID file does not exist");
317 return Ok(false);
318 }
319
320 let PidContent = fs::read_to_string(&self.PidFilePath)
322 .map_err(|e| AirError::FileSystem(format!("Failed to read PID file: {}", e)))?;
323
324 let parts:Vec<&str> = PidContent.split('|').collect();
326 if parts.len() < 2 {
327 warn!("[Daemon] Invalid PID file format, treating as stale");
328 self.CleanupStalePidFile().await?;
329 return Ok(false);
330 }
331
332 let pid:u32 = parts[0].trim().parse().map_err(|e| {
333 warn!("[Daemon] Invalid PID in file: {}", e);
334 AirError::FileSystem("Invalid PID file content".to_string())
335 })?;
336
337 if parts.len() >= 3 && parts[1].starts_with("CHECKSUM:") {
339 let StoredChecksum = &parts[1][9..]; let CurrentChecksum = self.PidChecksum.lock().await;
341
342 if let Some(ref cksum) = *CurrentChecksum {
343 if cksum != StoredChecksum {
344 warn!("[Daemon] PID file checksum mismatch, file may be corrupted");
345 return Ok(true);
347 }
348 }
349 }
350
351 let IsRunning = Self::ValidateProcess(pid);
353
354 if !IsRunning {
355 warn!("[Daemon] Detected stale PID file for PID {}", pid);
357 self.CleanupStalePidFile().await?;
358 }
359
360 Ok(IsRunning)
361 }
362
363 fn ValidateProcess(pid:u32) -> bool {
366 #[cfg(unix)]
367 {
368 use std::process::Command;
369 let output = Command::new("ps").arg("-p").arg(pid.to_string()).output();
370
371 match output {
372 Ok(output) => {
373 if output.status.success() {
374 let stdout = String::from_utf8_lossy(&output.stdout);
375 stdout
377 .lines()
378 .skip(1)
379 .any(|line| line.contains("Air") || line.contains("daemon"))
380 } else {
381 false
382 }
383 },
384 Err(e) => {
385 error!("[Daemon] Failed to check process status: {}", e);
386 false
387 },
388 }
389 }
390
391 #[cfg(windows)]
392 {
393 use std::process::Command;
394 let output = Command::new("tasklist")
395 .arg("/FI")
396 .arg(format!("PID eq {}", pid))
397 .arg("/FO")
398 .arg("CSV")
399 .output();
400
401 match output {
402 Ok(output) => {
403 if output.status.success() {
404 let stdout = String::from_utf8_lossy(&output.stdout);
405 stdout.lines().any(|line| {
406 line.contains(&pid.to_string()) && (line.contains("Air") || line.contains("daemon"))
407 })
408 } else {
409 false
410 }
411 },
412 Err(e) => {
413 error!("[Daemon] Failed to check process status: {}", e);
414 false
415 },
416 }
417 }
418 }
419
420 async fn CleanupStalePidFile(&self) -> Result<()> {
422 if !self.PidFilePath.exists() {
423 return Ok(());
424 }
425
426 let content = fs::read_to_string(&self.PidFilePath)
428 .map_err(|e| {
429 warn!("[Daemon] Cannot verify stale PID file: {}", e);
430 return false;
431 })
432 .ok();
433
434 if let Some(content) = content {
435 if content.starts_with(|c:char| c.is_numeric()) {
436 if let Err(e) = fs::remove_file(&self.PidFilePath) {
438 warn!("[Daemon] Failed to remove stale PID file: {}", e);
439 return Err(AirError::FileSystem(format!("Failed to remove stale PID file: {}", e)));
440 }
441 info!("[Daemon] Cleaned up stale PID file");
442 }
443 }
444
445 Ok(())
446 }
447
448 pub async fn ReleaseLock(&self) -> Result<()> {
451 info!("[Daemon] Releasing daemon lock...");
452
453 let _lock = self.PidLock.lock().await;
455
456 *self.IsRunning.write().await = false;
458
459 *self.PidChecksum.lock().await = None;
461
462 if self.PidFilePath.exists() {
464 match fs::remove_file(&self.PidFilePath) {
465 Ok(_) => {
466 debug!("[Daemon] PID file removed successfully");
467 },
468 Err(e) => {
469 error!("[Daemon] Failed to remove PID file: {}", e);
470 return Err(AirError::FileSystem(format!("Failed to remove PID file: {}", e)));
472 },
473 }
474 }
475
476 let TempDir = PathBuf::from(format!("{}.tmp", self.PidFilePath.display()));
478 if TempDir.exists() {
479 let _ = fs::remove_file(&TempDir);
480 }
481
482 info!("[Daemon] Daemon lock released");
483 Ok(())
484 }
485
486 pub async fn IsRunning(&self) -> bool { *self.IsRunning.read().await }
488
489 pub async fn RequestShutdown(&self) -> Result<()> {
491 info!("[Daemon] Requesting graceful shutdown...");
492 *self.ShutdownRequested.write().await = true;
493 Ok(())
494 }
495
496 pub async fn ClearShutdownRequest(&self) -> Result<()> {
498 info!("[Daemon] Clearing shutdown request");
499 *self.ShutdownRequested.write().await = false;
500 Ok(())
501 }
502
503 pub async fn IsShutdownRequested(&self) -> bool { *self.ShutdownRequested.read().await }
505
506 pub async fn GetStatus(&self) -> Result<DaemonStatus> {
508 let IsRunning = self.IsRunning().await;
509 let PidFileExists = self.PidFilePath.exists();
510
511 let pid = if PidFileExists {
512 fs::read_to_string(&self.PidFilePath)
513 .ok()
514 .and_then(|content| content.split('|').next().and_then(|s| s.trim().parse().ok()))
515 } else {
516 None
517 };
518
519 Ok(DaemonStatus {
520 IsRunning,
521 PidFileExists,
522 Pid:pid,
523 Platform:self.PlatformInfo.Platform.clone(),
524 ServiceName:self.PlatformInfo.ServiceName.clone(),
525 ShutdownRequested:self.IsShutdownRequested().await,
526 })
527 }
528
529 pub fn GenerateServiceFile(&self) -> Result<String> {
531 match self.PlatformInfo.Platform {
532 Platform::Linux => self.GenerateSystemdService(),
533 Platform::MacOS => self.GenerateLaunchdService(),
534 Platform::Windows => self.GenerateWindowsService(),
535 Platform::Unknown => {
536 Err(AirError::ServiceUnavailable(
537 "Unknown platform, cannot generate service file".to_string(),
538 ))
539 },
540 }
541 }
542
543 fn GenerateSystemdService(&self) -> Result<String> {
545 let ExePath = std::env::current_exe()
546 .map_err(|e| AirError::FileSystem(format!("Failed to get executable path: {}", e)))?;
547
548 let user = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
549 let group = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
550
551 let ServiceContent = format!(
552 r#"[Unit]
553Description=Air Daemon - Background service for Land code editor
554Documentation=man:Air(1)
555After=network-online.target
556Wants=network-online.target
557StartLimitIntervalSec=0
558
559[Service]
560Type=notify
561NotifyAccess=all
562ExecStart={}
563ExecStop=/bin/kill -s TERM $MAINPID
564Restart=always
565RestartSec=5
566StartLimitBurst=3
567User={}
568Group={}
569Environment=RUST_LOG=info
570Environment=DAEMON_MODE=systemd
571Nice=-5
572LimitNOFILE=65536
573LimitNPROC=4096
574
575# Security hardening
576NoNewPrivileges=true
577PrivateTmp=true
578ProtectSystem=strict
579ProtectHome=true
580ReadWritePaths=/var/log/Air /var/run/Air
581RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
582RestrictRealtime=true
583
584[Install]
585WantedBy=multi-user.target
586"#,
587 ExePath.display(),
588 user,
589 group
590 );
591
592 Ok(ServiceContent)
593 }
594
595 fn GenerateLaunchdService(&self) -> Result<String> {
597 let ExePath = std::env::current_exe()
598 .map(|p| p.display().to_string())
599 .unwrap_or_else(|_| "/usr/local/bin/Air".to_string());
600
601 let ServiceName = &self.PlatformInfo.ServiceName;
602 let user = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
603
604 let ServiceContent = format!(
605 r#"<?xml version="1.0" encoding="UTF-8"?>
606<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
607<plist version="1.0">
608<dict>
609 <key>Label</key>
610 <string>{}</string>
611
612 <key>ProgramArguments</key>
613 <array>
614 <string>{}</string>
615 <string>--daemon</string>
616 <string>--mode=launchd</string>
617 </array>
618
619 <key>RunAtLoad</key>
620 <true/>
621
622 <key>KeepAlive</key>
623 <dict>
624 <key>SuccessfulExit</key>
625 <false/>
626 <key>Crashed</key>
627 <true/>
628 </dict>
629
630 <key>ThrottleInterval</key>
631 <integer>5</integer>
632
633 <key>UserName</key>
634 <string>{}</string>
635
636 <key>StandardOutPath</key>
637 <string>/var/log/Air/daemon.log</string>
638
639 <key>StandardErrorPath</key>
640 <string>/var/log/Air/daemon.err</string>
641
642 <key>WorkingDirectory</key>
643 <string>/var/lib/Air</string>
644
645 <key>ProcessType</key>
646 <string>Background</string>
647
648 <key>Nice</key>
649 <integer>-5</integer>
650
651 <key>SoftResourceLimits</key>
652 <dict>
653 <key>NumberOfFiles</key>
654 <integer>65536</integer>
655 </dict>
656
657 <key>HardResourceLimits</key>
658 <dict>
659 <key>NumberOfFiles</key>
660 <integer>65536</integer>
661 </dict>
662
663 <key>EnvironmentVariables</key>
664 <dict>
665 <key>RUST_LOG</key>
666 <string>info</string>
667 <key>DAEMON_MODE</key>
668 <string>launchd</string>
669 </dict>
670</dict>
671</plist>
672"#,
673 ServiceName, ExePath, user
674 );
675
676 Ok(ServiceContent)
677 }
678
679 fn GenerateWindowsService(&self) -> Result<String> {
683 let ExePath = std::env::current_exe()
684 .map(|p| p.display().to_string())
685 .unwrap_or_else(|_| "C:\\Program Files\\Air\\Air.exe".to_string());
686
687 let ServiceName = &self.PlatformInfo.ServiceName;
688 let DisplayName = "Air Daemon Service";
689 let Description = "Background service for Land code editor";
690
691 let ServiceContent = format!(
693 r#"<service>
694 <id>{}</id>
695 <name>{}</name>
696 <description>{}</description>
697 <executable>{}</executable>
698
699 <arguments>--daemon --mode=windows</arguments>
700
701 <startmode>Automatic</startmode>
702 <delayedAutoStart>true</delayedAutoStart>
703
704 <log mode="roll">
705 <sizeThreshold>10240</sizeThreshold>
706 <keepFiles>8</keepFiles>
707 </log>
708
709 <onfailure action="restart" delay="10 sec"/>
710 <onfailure action="restart" delay="20 sec"/>
711 <onfailure action="restart" delay="60 sec"/>
712
713 <resetfailure>1 hour</resetfailure>
714
715 <depend>EventLog</depend>
716 <depend>TcpIp</depend>
717
718 <serviceaccount>
719 <domain>.</domain>
720 <user>LocalSystem</user>
721 <password></password>
722 <allowservicelogon>true</allowservicelogon>
723 </serviceaccount>
724
725 <workingdirectory>C:\Program Files\Air</workingdirectory>
726
727 <env name="RUST_LOG" value="info"/>
728 <env name="DAEMON_MODE" value="windows"/>
729</service>
730"#,
731 ServiceName, DisplayName, Description, ExePath
732 );
733
734 Ok(ServiceContent)
735 }
736
737 pub async fn InstallService(&self) -> Result<()> {
739 info!("[Daemon] Installing system service...");
740
741 match self.PlatformInfo.Platform {
742 Platform::Linux => self.InstallSystemdService().await,
743 Platform::MacOS => self.InstallLaunchdService().await,
744 Platform::Windows => self.InstallWindowsService().await,
745 Platform::Unknown => {
746 Err(AirError::ServiceUnavailable(
747 "Unknown platform, cannot install service".to_string(),
748 ))
749 },
750 }
751 }
752
753 async fn InstallSystemdService(&self) -> Result<()> {
755 let ServiceFileContent = self.GenerateSystemdService()?;
756 let ServiceFilePath = format!("/etc/systemd/system/{}.service", self.PlatformInfo.ServiceName);
757
758 let TempPath = format!("{}.tmp", ServiceFilePath);
760
761 if !ServiceFileContent.contains("[Unit]") || !ServiceFileContent.contains("[Service]") {
763 return Err(AirError::Configuration("Generated service file is invalid".to_string()));
764 }
765
766 fs::write(&TempPath, &ServiceFileContent)
768 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary service file: {}", e)))?;
769
770 #[cfg(unix)]
772 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
773 let _ = fs::remove_file(&TempPath);
774 AirError::FileSystem(format!("Failed to rename service file: {}", e))
775 })?;
776
777 #[cfg(not(unix))]
778 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
779 let _ = fs::remove_file(&TempPath);
780 AirError::FileSystem(format!("Failed to rename service file: {}", e))
781 })?;
782
783 #[cfg(unix)]
785 {
786 use std::os::unix::fs::PermissionsExt;
787 let perms = fs::Permissions::from_mode(0o644);
788 fs::set_permissions(&ServiceFilePath, perms)
789 .map_err(|e| {
790 error!("[Daemon] Failed to set service file permissions: {}", e);
791 })
792 .ok();
793 }
794
795 info!("[Daemon] Systemd service installed at {}", ServiceFilePath);
796
797 let _ = tokio::process::Command::new("systemctl").args(["daemon-reload"]).output().await;
799
800 Ok(())
801 }
802
803 async fn InstallLaunchdService(&self) -> Result<()> {
805 let ServiceFileContent = self.GenerateLaunchdService()?;
806 let ServiceFilePath = format!("/Library/LaunchDaemons/{}.plist", self.PlatformInfo.ServiceName);
807
808 let TempPath = format!("{}.tmp", ServiceFilePath);
810
811 if !ServiceFileContent.contains("<?xml") || !ServiceFileContent.contains("<!DOCTYPE plist") {
813 return Err(AirError::Configuration("Generated plist file is invalid".to_string()));
814 }
815
816 fs::write(&TempPath, &ServiceFileContent)
818 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary plist file: {}", e)))?;
819
820 #[cfg(unix)]
822 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
823 let _ = fs::remove_file(&TempPath);
824 AirError::FileSystem(format!("Failed to rename plist file: {}", e))
825 })?;
826
827 #[cfg(not(unix))]
828 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
829 let _ = fs::remove_file(&TempPath);
830 AirError::FileSystem(format!("Failed to rename plist file: {}", e))
831 })?;
832
833 #[cfg(unix)]
835 {
836 use std::os::unix::fs::PermissionsExt;
837 let perms = fs::Permissions::from_mode(0o644);
838 fs::set_permissions(&ServiceFilePath, perms)
839 .map_err(|e| {
840 error!("[Daemon] Failed to set plist file permissions: {}", e);
841 })
842 .ok();
843 }
844
845 info!("[Daemon] Launchd service installed at {}", ServiceFilePath);
846
847 Ok(())
851 }
852
853 async fn InstallWindowsService(&self) -> Result<()> {
856 let ServiceFileContent = self.GenerateWindowsService()?;
857 let ServiceDir = "C:\\ProgramData\\Air";
858 let ServiceFilePath = format!("{}\\{}.xml", ServiceDir, self.PlatformInfo.ServiceName);
859
860 fs::create_dir_all(&ServiceDir)
862 .map_err(|e| AirError::FileSystem(format!("Failed to create service directory: {}", e)))?;
863
864 let TempPath = format!("{}.tmp", ServiceFilePath);
866
867 if !ServiceFileContent.contains("<service>") {
869 return Err(AirError::Configuration("Generated service file is invalid".to_string()));
870 }
871
872 fs::write(&TempPath, &ServiceFileContent)
874 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary service file: {}", e)))?;
875
876 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
878 let _ = fs::remove_file(&TempPath);
879 AirError::FileSystem(format!("Failed to rename service file: {}", e))
880 })?;
881
882 info!("[Daemon] Windows service configuration written to {}", ServiceFilePath);
883 warn!("[Daemon] Windows service installation requires additional integration with winsvc crate");
884 warn!("[Daemon] Manual installation may be required: Use SC.EXE or winsvc to register service");
885
886 Ok(())
887 }
888
889 pub async fn UninstallService(&self) -> Result<()> {
891 info!("[Daemon] Uninstalling system service...");
892
893 match self.PlatformInfo.Platform {
894 Platform::Linux => self.UninstallSystemdService().await,
895 Platform::MacOS => self.UninstallLaunchdService().await,
896 Platform::Windows => self.UninstallWindowsService().await,
897 Platform::Unknown => {
898 Err(AirError::ServiceUnavailable(
899 "Unknown platform, cannot uninstall service".to_string(),
900 ))
901 },
902 }
903 }
904
905 async fn UninstallSystemdService(&self) -> Result<()> {
907 let ServiceFilePath = format!("/etc/systemd/system/{}.service", self.PlatformInfo.ServiceName);
908
909 let _ = tokio::process::Command::new("systemctl")
911 .args(["stop", &self.PlatformInfo.ServiceName])
912 .output()
913 .await;
914
915 let _ = tokio::process::Command::new("systemctl")
917 .args(["disable", &self.PlatformInfo.ServiceName])
918 .output()
919 .await;
920
921 if fs::remove_file(&ServiceFilePath).is_ok() {
923 info!("[Daemon] Systemd service file removed");
924 } else {
925 warn!("[Daemon] Service file {} not found", ServiceFilePath);
926 }
927
928 let _ = tokio::process::Command::new("systemctl").args(["daemon-reload"]).output().await;
930
931 info!("[Daemon] Systemd service uninstalled");
932 Ok(())
933 }
934
935 async fn UninstallLaunchdService(&self) -> Result<()> {
937 let ServiceFilePath = format!("/Library/LaunchDaemons/{}.plist", self.PlatformInfo.ServiceName);
938
939 let _ = tokio::process::Command::new("launchctl")
941 .args(["unload", "-w", &ServiceFilePath])
942 .output()
943 .await;
944
945 if fs::remove_file(&ServiceFilePath).is_ok() {
947 info!("[Daemon] Launchd service file removed");
948 } else {
949 warn!("[Daemon] Service file {} not found", ServiceFilePath);
950 }
951
952 info!("[Daemon] Launchd service uninstalled");
953 Ok(())
954 }
955
956 async fn UninstallWindowsService(&self) -> Result<()> {
958 let ServiceFilePath = format!("C:\\ProgramData\\Air\\{}.xml", self.PlatformInfo.ServiceName);
959
960 if fs::remove_file(&ServiceFilePath).is_ok() {
964 info!("[Daemon] Windows service configuration removed");
965 } else {
966 warn!("[Daemon] Service file {} not found", ServiceFilePath);
967 }
968
969 warn!("[Daemon] Manual Windows service removal may be required: Use SC.EXE or winsvc");
970
971 Ok(())
972 }
973}
974
975#[derive(Debug, Clone)]
977pub struct DaemonStatus {
978 pub IsRunning:bool,
979 pub PidFileExists:bool,
980 pub Pid:Option<u32>,
981 pub Platform:Platform,
982 pub ServiceName:String,
983 pub ShutdownRequested:bool,
984}
985
986impl DaemonStatus {
987 pub fn status_description(&self) -> String {
989 if self.IsRunning {
990 format!("Running (PID: {})", self.Pid.unwrap_or(0))
991 } else if self.PidFileExists {
992 "Stale PID file exists".to_string()
993 } else {
994 "Not running".to_string()
995 }
996 }
997}
998
999impl From<ExitCode> for i32 {
1000 fn from(code:ExitCode) -> i32 { code as i32 }
1001}