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 = hex::encode(hasher.finalize());
269
270 let TempFileContent = format!("{}|CHECKSUM:{}", PidContent, checksum);
272 fs::write(&TempDir, &TempFileContent)
273 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary PID file: {}", e)))?;
274
275 #[cfg(unix)]
277 fs::rename(&TempDir, &self.PidFilePath).map_err(|e| {
278 let _ = fs::remove_file(&TempDir);
280 AirError::FileSystem(format!("Failed to rename PID file: {}", e))
281 })?;
282
283 #[cfg(not(unix))]
284 fs::rename(&TempDir, &self.PidFilePath).map_err(|e| {
285 let _ = fs::remove_file(&TempDir);
286 AirError::FileSystem(format!("Failed to rename PID file: {}", e))
287 })?;
288
289 *self.PidChecksum.lock().await = Some(checksum);
291
292 *self.IsRunning.write().await = true;
294
295 #[cfg(unix)]
297 {
298 use std::os::unix::fs::PermissionsExt;
299 let perms = fs::Permissions::from_mode(0o600);
300 if let Err(e) = fs::set_permissions(&self.PidFilePath, perms) {
301 dev_log!("daemon", "warn: [Daemon] Failed to set PID file permissions: {}", e);
302 }
303 }
304
305 dev_log!("daemon", "[Daemon] Daemon lock acquired (PID: {})", pid);
306 Ok(())
307 }
308
309 pub async fn IsAlreadyRunning(&self) -> Result<bool> {
316 if !self.PidFilePath.exists() {
317 dev_log!("daemon", "[Daemon] PID file does not exist");
318 return Ok(false);
319 }
320
321 let PidContent = fs::read_to_string(&self.PidFilePath)
323 .map_err(|e| AirError::FileSystem(format!("Failed to read PID file: {}", e)))?;
324
325 let parts:Vec<&str> = PidContent.split('|').collect();
327 if parts.len() < 2 {
328 dev_log!("daemon", "warn: [Daemon] Invalid PID file format, treating as stale");
329 self.CleanupStalePidFile().await?;
330 return Ok(false);
331 }
332
333 let pid:u32 = parts[0].trim().parse().map_err(|e| {
334 dev_log!("daemon", "warn: [Daemon] Invalid PID in file: {}", e);
335 AirError::FileSystem("Invalid PID file content".to_string())
336 })?;
337
338 if parts.len() >= 3 && parts[1].starts_with("CHECKSUM:") {
340 let StoredChecksum = &parts[1][9..]; let CurrentChecksum = self.PidChecksum.lock().await;
342
343 if let Some(ref cksum) = *CurrentChecksum {
344 if cksum != StoredChecksum {
345 dev_log!("daemon", "warn: [Daemon] PID file checksum mismatch, file may be corrupted"); return Ok(true);
347 }
348 }
349 }
350
351 let IsRunning = Self::ValidateProcess(pid);
353
354 if !IsRunning {
355 dev_log!("daemon", "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 dev_log!("daemon", "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 dev_log!("daemon", "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 dev_log!("daemon", "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 dev_log!("daemon", "warn: [Daemon] Failed to remove stale PID file: {}", e);
439 return Err(AirError::FileSystem(format!("Failed to remove stale PID file: {}", e)));
440 }
441 dev_log!("daemon", "[Daemon] Cleaned up stale PID file");
442 }
443 }
444
445 Ok(())
446 }
447
448 pub async fn ReleaseLock(&self) -> Result<()> {
451 dev_log!("daemon", "[Daemon] Releasing daemon lock...");
452 let _lock = self.PidLock.lock().await;
454
455 *self.IsRunning.write().await = false;
457
458 *self.PidChecksum.lock().await = None;
460
461 if self.PidFilePath.exists() {
463 match fs::remove_file(&self.PidFilePath) {
464 Ok(_) => {
465 dev_log!("daemon", "[Daemon] PID file removed successfully");
466 },
467 Err(e) => {
468 dev_log!("daemon", "error: [Daemon] Failed to remove PID file: {}", e); return Err(AirError::FileSystem(format!("Failed to remove PID file: {}", e)));
470 },
471 }
472 }
473
474 let TempDir = PathBuf::from(format!("{}.tmp", self.PidFilePath.display()));
476 if TempDir.exists() {
477 let _ = fs::remove_file(&TempDir);
478 }
479
480 dev_log!("daemon", "[Daemon] Daemon lock released");
481 Ok(())
482 }
483
484 pub async fn IsRunning(&self) -> bool { *self.IsRunning.read().await }
486
487 pub async fn RequestShutdown(&self) -> Result<()> {
489 dev_log!("daemon", "[Daemon] Requesting graceful shutdown...");
490 *self.ShutdownRequested.write().await = true;
491 Ok(())
492 }
493
494 pub async fn ClearShutdownRequest(&self) -> Result<()> {
496 dev_log!("daemon", "[Daemon] Clearing shutdown request");
497 *self.ShutdownRequested.write().await = false;
498 Ok(())
499 }
500
501 pub async fn IsShutdownRequested(&self) -> bool { *self.ShutdownRequested.read().await }
503
504 pub async fn GetStatus(&self) -> Result<DaemonStatus> {
506 let IsRunning = self.IsRunning().await;
507 let PidFileExists = self.PidFilePath.exists();
508
509 let pid = if PidFileExists {
510 fs::read_to_string(&self.PidFilePath)
511 .ok()
512 .and_then(|content| content.split('|').next().and_then(|s| s.trim().parse().ok()))
513 } else {
514 None
515 };
516
517 Ok(DaemonStatus {
518 IsRunning,
519 PidFileExists,
520 Pid:pid,
521 Platform:self.PlatformInfo.Platform.clone(),
522 ServiceName:self.PlatformInfo.ServiceName.clone(),
523 ShutdownRequested:self.IsShutdownRequested().await,
524 })
525 }
526
527 pub fn GenerateServiceFile(&self) -> Result<String> {
529 match self.PlatformInfo.Platform {
530 Platform::Linux => self.GenerateSystemdService(),
531 Platform::MacOS => self.GenerateLaunchdService(),
532 #[cfg(target_os = "windows")]
533 Platform::Windows => self.GenerateWindowsService(),
534 #[cfg(not(target_os = "windows"))]
535 Platform::Windows => {
536 Err(AirError::ServiceUnavailable(
537 "Windows service generation not available on this platform".to_string(),
538 ))
539 },
540 Platform::Unknown => {
541 Err(AirError::ServiceUnavailable(
542 "Unknown platform, cannot generate service file".to_string(),
543 ))
544 },
545 }
546 }
547
548 fn GenerateSystemdService(&self) -> Result<String> {
550 let ExePath = std::env::current_exe()
551 .map_err(|e| AirError::FileSystem(format!("Failed to get executable path: {}", e)))?;
552
553 let user = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
554 let group = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
555
556 let ServiceContent = format!(
557 r#"[Unit]
558Description=Air Daemon - Background service for Land code editor
559Documentation=man:Air(1)
560After=network-online.target
561Wants=network-online.target
562StartLimitIntervalSec=0
563
564[Service]
565Type=notify
566NotifyAccess=all
567ExecStart={}
568ExecStop=/bin/kill -s TERM $MAINPID
569Restart=always
570RestartSec=5
571StartLimitBurst=3
572User={}
573Group={}
574Environment=RUST_LOG=info
575Environment=DAEMON_MODE=systemd
576Nice=-5
577LimitNOFILE=65536
578LimitNPROC=4096
579
580# Security hardening
581NoNewPrivileges=true
582PrivateTmp=true
583ProtectSystem=strict
584ProtectHome=true
585ReadWritePaths=/var/log/Air /var/run/Air
586RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
587RestrictRealtime=true
588
589[Install]
590WantedBy=multi-user.target
591"#,
592 ExePath.display(),
593 user,
594 group
595 );
596
597 Ok(ServiceContent)
598 }
599
600 fn GenerateLaunchdService(&self) -> Result<String> {
602 let ExePath = std::env::current_exe()
603 .map(|p| p.display().to_string())
604 .unwrap_or_else(|_| "/usr/local/bin/Air".to_string());
605
606 let ServiceName = &self.PlatformInfo.ServiceName;
607 let user = self.PlatformInfo.RunAsUser.as_deref().unwrap_or("root");
608
609 let ServiceContent = format!(
610 r#"<?xml version="1.0" encoding="UTF-8"?>
611<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
612<plist version="1.0">
613<dict>
614 <key>Label</key>
615 <string>{}</string>
616
617 <key>ProgramArguments</key>
618 <array>
619 <string>{}</string>
620 <string>--daemon</string>
621 <string>--mode=launchd</string>
622 </array>
623
624 <key>RunAtLoad</key>
625 <true/>
626
627 <key>KeepAlive</key>
628 <dict>
629 <key>SuccessfulExit</key>
630 <false/>
631 <key>Crashed</key>
632 <true/>
633 </dict>
634
635 <key>ThrottleInterval</key>
636 <integer>5</integer>
637
638 <key>UserName</key>
639 <string>{}</string>
640
641 <key>StandardOutPath</key>
642 <string>/var/log/Air/daemon.log</string>
643
644 <key>StandardErrorPath</key>
645 <string>/var/log/Air/daemon.err</string>
646
647 <key>WorkingDirectory</key>
648 <string>/var/lib/Air</string>
649
650 <key>ProcessType</key>
651 <string>Background</string>
652
653 <key>Nice</key>
654 <integer>-5</integer>
655
656 <key>SoftResourceLimits</key>
657 <dict>
658 <key>NumberOfFiles</key>
659 <integer>65536</integer>
660 </dict>
661
662 <key>HardResourceLimits</key>
663 <dict>
664 <key>NumberOfFiles</key>
665 <integer>65536</integer>
666 </dict>
667
668 <key>EnvironmentVariables</key>
669 <dict>
670 <key>RUST_LOG</key>
671 <string>info</string>
672 <key>DAEMON_MODE</key>
673 <string>launchd</string>
674 </dict>
675</dict>
676</plist>
677"#,
678 ServiceName, ExePath, user
679 );
680
681 Ok(ServiceContent)
682 }
683
684 #[cfg(target_os = "windows")]
690 fn GenerateWindowsService(&self) -> Result<String> {
691 let ExePath = std::env::current_exe()
692 .map(|p| p.display().to_string())
693 .unwrap_or_else(|_| "C:\\Program Files\\Air\\Air.exe".to_string());
694
695 let ServiceName = &self.PlatformInfo.ServiceName;
696 let DisplayName = "Air Daemon Service";
697 let Description = "Background service for Land code editor";
698
699 let ServiceContent = format!(
701 r#"<service>
702 <id>{}</id>
703 <name>{}</name>
704 <description>{}</description>
705 <executable>{}</executable>
706
707 <arguments>--daemon --mode=windows</arguments>
708
709 <startmode>Automatic</startmode>
710 <delayedAutoStart>true</delayedAutoStart>
711
712 <log mode="roll">
713 <sizeThreshold>10240</sizeThreshold>
714 <keepFiles>8</keepFiles>
715 </log>
716
717 <onfailure action="restart" delay="10 sec"/>
718 <onfailure action="restart" delay="20 sec"/>
719 <onfailure action="restart" delay="60 sec"/>
720
721 <resetfailure>1 hour</resetfailure>
722
723 <depend>EventLog</depend>
724 <depend>TcpIp</depend>
725
726 <serviceaccount>
727 <domain>.</domain>
728 <user>LocalSystem</user>
729 <password></password>
730 <allowservicelogon>true</allowservicelogon>
731 </serviceaccount>
732
733 <workingdirectory>C:\Program Files\Air</workingdirectory>
734
735 <env name="RUST_LOG" value="info"/>
736 <env name="DAEMON_MODE" value="windows"/>
737</service>
738"#,
739 ServiceName, DisplayName, Description, ExePath
740 );
741
742 Ok(ServiceContent)
743 }
744
745 pub async fn InstallService(&self) -> Result<()> {
747 dev_log!("daemon", "[Daemon] Installing system service...");
748 match self.PlatformInfo.Platform {
749 Platform::Linux => self.InstallSystemdService().await,
750 Platform::MacOS => self.InstallLaunchdService().await,
751 #[cfg(target_os = "windows")]
752 Platform::Windows => self.InstallWindowsService().await,
753 #[cfg(not(target_os = "windows"))]
754 Platform::Windows => {
755 Err(AirError::ServiceUnavailable(
756 "Windows service installation not available on this platform".to_string(),
757 ))
758 },
759 Platform::Unknown => {
760 Err(AirError::ServiceUnavailable(
761 "Unknown platform, cannot install service".to_string(),
762 ))
763 },
764 }
765 }
766
767 async fn InstallSystemdService(&self) -> Result<()> {
769 let ServiceFileContent = self.GenerateSystemdService()?;
770 let ServiceFilePath = format!("/etc/systemd/system/{}.service", self.PlatformInfo.ServiceName);
771
772 let TempPath = format!("{}.tmp", ServiceFilePath);
774
775 if !ServiceFileContent.contains("[Unit]") || !ServiceFileContent.contains("[Service]") {
777 return Err(AirError::Configuration("Generated service file is invalid".to_string()));
778 }
779
780 fs::write(&TempPath, &ServiceFileContent)
782 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary service file: {}", e)))?;
783
784 #[cfg(unix)]
786 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
787 let _ = fs::remove_file(&TempPath);
788 AirError::FileSystem(format!("Failed to rename service file: {}", e))
789 })?;
790
791 #[cfg(not(unix))]
792 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
793 let _ = fs::remove_file(&TempPath);
794 AirError::FileSystem(format!("Failed to rename service file: {}", e))
795 })?;
796
797 #[cfg(unix)]
799 {
800 use std::os::unix::fs::PermissionsExt;
801 let perms = fs::Permissions::from_mode(0o644);
802 fs::set_permissions(&ServiceFilePath, perms)
803 .map_err(|e| {
804 dev_log!("daemon", "error: [Daemon] Failed to set service file permissions: {}", e);
805 })
806 .ok();
807 }
808
809 dev_log!("daemon", "[Daemon] Systemd service installed at {}", ServiceFilePath);
810 let _ = tokio::process::Command::new("systemctl").args(["daemon-reload"]).output().await;
812
813 Ok(())
814 }
815
816 async fn InstallLaunchdService(&self) -> Result<()> {
818 let ServiceFileContent = self.GenerateLaunchdService()?;
819 let ServiceFilePath = format!("/Library/LaunchDaemons/{}.plist", self.PlatformInfo.ServiceName);
820
821 let TempPath = format!("{}.tmp", ServiceFilePath);
823
824 if !ServiceFileContent.contains("<?xml") || !ServiceFileContent.contains("<!DOCTYPE plist") {
826 return Err(AirError::Configuration("Generated plist file is invalid".to_string()));
827 }
828
829 fs::write(&TempPath, &ServiceFileContent)
831 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary plist file: {}", e)))?;
832
833 #[cfg(unix)]
835 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
836 let _ = fs::remove_file(&TempPath);
837 AirError::FileSystem(format!("Failed to rename plist file: {}", e))
838 })?;
839
840 #[cfg(not(unix))]
841 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
842 let _ = fs::remove_file(&TempPath);
843 AirError::FileSystem(format!("Failed to rename plist file: {}", e))
844 })?;
845
846 #[cfg(unix)]
848 {
849 use std::os::unix::fs::PermissionsExt;
850 let perms = fs::Permissions::from_mode(0o644);
851 fs::set_permissions(&ServiceFilePath, perms)
852 .map_err(|e| {
853 dev_log!("daemon", "error: [Daemon] Failed to set plist file permissions: {}", e);
854 })
855 .ok();
856 }
857
858 dev_log!("daemon", "[Daemon] Launchd service installed at {}", ServiceFilePath);
859 Ok(())
863 }
864
865 #[cfg(target_os = "windows")]
872 async fn InstallWindowsService(&self) -> Result<()> {
873 let ServiceFileContent = self.GenerateWindowsService()?;
874 let ServiceDir = "C:\\ProgramData\\Air";
875 let ServiceFilePath = format!("{}\\{}.xml", ServiceDir, self.PlatformInfo.ServiceName);
876
877 fs::create_dir_all(&ServiceDir)
879 .map_err(|e| AirError::FileSystem(format!("Failed to create service directory: {}", e)))?;
880
881 let TempPath = format!("{}.tmp", ServiceFilePath);
883
884 if !ServiceFileContent.contains("<service>") {
886 return Err(AirError::Configuration("Generated service file is invalid".to_string()));
887 }
888
889 fs::write(&TempPath, &ServiceFileContent)
891 .map_err(|e| AirError::FileSystem(format!("Failed to write temporary service file: {}", e)))?;
892
893 fs::rename(&TempPath, &ServiceFilePath).map_err(|e| {
895 let _ = fs::remove_file(&TempPath);
896 AirError::FileSystem(format!("Failed to rename service file: {}", e))
897 })?;
898
899 dev_log!(
900 "daemon",
901 "[Daemon] Windows service configuration written to {}",
902 ServiceFilePath
903 );
904 dev_log!("daemon", "[Daemon] To register the service, run:");
905 dev_log!(
906 "daemon",
907 "[Daemon] sc create AirDaemon binPath= \"{}\" DisplayName= \"Air Daemon\"",
908 std::env::current_exe().unwrap_or_else(|_| "air.exe".into()).display()
909 );
910 dev_log!("daemon", "[Daemon] sc config AirDaemon start= auto");
911 dev_log!("daemon", "[Daemon] sc start AirDaemon");
912 Ok(())
913 }
914
915 pub async fn UninstallService(&self) -> Result<()> {
917 dev_log!("daemon", "[Daemon] Uninstalling system service...");
918 match self.PlatformInfo.Platform {
919 Platform::Linux => self.UninstallSystemdService().await,
920 Platform::MacOS => self.UninstallLaunchdService().await,
921 #[cfg(target_os = "windows")]
922 Platform::Windows => self.UninstallWindowsService().await,
923 #[cfg(not(target_os = "windows"))]
924 Platform::Windows => {
925 Err(AirError::ServiceUnavailable(
926 "Windows service uninstallation not available on this platform".to_string(),
927 ))
928 },
929 Platform::Unknown => {
930 Err(AirError::ServiceUnavailable(
931 "Unknown platform, cannot uninstall service".to_string(),
932 ))
933 },
934 }
935 }
936
937 async fn UninstallSystemdService(&self) -> Result<()> {
939 let ServiceFilePath = format!("/etc/systemd/system/{}.service", self.PlatformInfo.ServiceName);
940
941 let _ = tokio::process::Command::new("systemctl")
943 .args(["stop", &self.PlatformInfo.ServiceName])
944 .output()
945 .await;
946
947 let _ = tokio::process::Command::new("systemctl")
949 .args(["disable", &self.PlatformInfo.ServiceName])
950 .output()
951 .await;
952
953 if fs::remove_file(&ServiceFilePath).is_ok() {
955 dev_log!("daemon", "[Daemon] Systemd service file removed");
956 } else {
957 dev_log!("daemon", "warn: [Daemon] Service file {} not found", ServiceFilePath);
958 }
959
960 let _ = tokio::process::Command::new("systemctl").args(["daemon-reload"]).output().await;
962
963 dev_log!("daemon", "[Daemon] Systemd service uninstalled");
964 Ok(())
965 }
966
967 async fn UninstallLaunchdService(&self) -> Result<()> {
969 let ServiceFilePath = format!("/Library/LaunchDaemons/{}.plist", self.PlatformInfo.ServiceName);
970
971 let _ = tokio::process::Command::new("launchctl")
973 .args(["unload", "-w", &ServiceFilePath])
974 .output()
975 .await;
976
977 if fs::remove_file(&ServiceFilePath).is_ok() {
979 dev_log!("daemon", "[Daemon] Launchd service file removed");
980 } else {
981 dev_log!("daemon", "warn: [Daemon] Service file {} not found", ServiceFilePath);
982 }
983
984 dev_log!("daemon", "[Daemon] Launchd service uninstalled");
985 Ok(())
986 }
987
988 #[cfg(target_os = "windows")]
994 async fn UninstallWindowsService(&self) -> Result<()> {
995 let ServiceFilePath = format!("C:\\ProgramData\\Air\\{}.xml", self.PlatformInfo.ServiceName);
996
997 if fs::remove_file(&ServiceFilePath).is_ok() {
999 dev_log!("daemon", "[Daemon] Windows service configuration removed");
1000 } else {
1001 dev_log!("daemon", "warn: [Daemon] Service file {} not found", ServiceFilePath);
1002 }
1003
1004 dev_log!("daemon", "[Daemon] To unregister the service, run:");
1005 dev_log!("daemon", "[Daemon] sc stop AirDaemon");
1006 dev_log!("daemon", "[Daemon] sc delete AirDaemon");
1007 Ok(())
1008 }
1009}
1010
1011#[derive(Debug, Clone)]
1013pub struct DaemonStatus {
1014 pub IsRunning:bool,
1015 pub PidFileExists:bool,
1016 pub Pid:Option<u32>,
1017 pub Platform:Platform,
1018 pub ServiceName:String,
1019 pub ShutdownRequested:bool,
1020}
1021
1022impl DaemonStatus {
1023 pub fn status_description(&self) -> String {
1025 if self.IsRunning {
1026 format!("Running (PID: {})", self.Pid.unwrap_or(0))
1027 } else if self.PidFileExists {
1028 "Stale PID file exists".to_string()
1029 } else {
1030 "Not running".to_string()
1031 }
1032 }
1033}
1034
1035impl From<ExitCode> for i32 {
1036 fn from(code:ExitCode) -> i32 { code as i32 }
1037}