diff --git a/src/system_dns.rs b/src/system_dns.rs index 564dca4..da04cc7 100644 --- a/src/system_dns.rs +++ b/src/system_dns.rs @@ -186,8 +186,9 @@ pub fn uninstall_system_dns() -> Result<(), String> { #[cfg(target_os = "macos")] fn numa_data_dir() -> std::path::PathBuf { let home = std::env::var("HOME") + .or_else(|_| std::env::var("SUDO_USER").map(|u| format!("/Users/{}", u))) .map(std::path::PathBuf::from) - .unwrap_or_else(|_| std::path::PathBuf::from("/tmp")); + .unwrap_or_else(|_| std::path::PathBuf::from("/var/root")); home.join(".numa") } @@ -407,15 +408,23 @@ fn install_service_macos() -> Result<(), String> { #[cfg(target_os = "macos")] fn uninstall_service_macos() -> Result<(), String> { + // Remove plist first so service won't restart on boot even if unload fails + if let Err(e) = std::fs::remove_file(PLIST_DEST) { + if e.kind() != std::io::ErrorKind::NotFound { + return Err(format!("failed to remove {}: {}", PLIST_DEST, e)); + } + } + // Unload the service - let _ = std::process::Command::new("launchctl") + let status = std::process::Command::new("launchctl") .args(["unload", "-w", PLIST_DEST]) .status(); - - // Remove plist - if std::path::Path::new(PLIST_DEST).exists() { - std::fs::remove_file(PLIST_DEST) - .map_err(|e| format!("failed to remove {}: {}", PLIST_DEST, e))?; + if let Ok(s) = status { + if !s.success() { + eprintln!( + " warning: launchctl unload returned non-zero (service may still be running)" + ); + } } eprintln!(" Service uninstalled. Numa will no longer auto-start.\n"); @@ -449,24 +458,63 @@ fn backup_path_linux() -> std::path::PathBuf { let home = std::env::var("HOME") .map(std::path::PathBuf::from) .unwrap_or_else(|_| std::path::PathBuf::from("/root")); - let dir = home.join(".numa"); - let _ = std::fs::create_dir_all(&dir); - dir.join("original-resolv.conf") + home.join(".numa").join("original-resolv.conf") +} + +#[cfg(target_os = "linux")] +fn is_systemd_resolved_active() -> bool { + std::process::Command::new("systemctl") + .args(["is-active", "--quiet", "systemd-resolved"]) + .status() + .map(|s| s.success()) + .unwrap_or(false) } #[cfg(target_os = "linux")] fn install_linux() -> Result<(), String> { - let backup = backup_path_linux(); - let resolv = std::path::Path::new("/etc/resolv.conf"); + // Detect systemd-resolved — direct resolv.conf manipulation won't persist + if is_systemd_resolved_active() { + let resolved_dir = std::path::Path::new("/etc/systemd/resolved.conf.d"); + std::fs::create_dir_all(resolved_dir) + .map_err(|e| format!("failed to create {}: {}", resolved_dir.display(), e))?; - // Save current resolv.conf - if resolv.exists() { - std::fs::copy(resolv, &backup) - .map_err(|e| format!("failed to backup /etc/resolv.conf: {}", e))?; - eprintln!(" Saved /etc/resolv.conf to {}", backup.display()); + let drop_in = resolved_dir.join("numa.conf"); + std::fs::write(&drop_in, "[Resolve]\nDNS=127.0.0.1\nDomains=~.\n") + .map_err(|e| format!("failed to write {}: {}", drop_in.display(), e))?; + + let _ = run_systemctl(&["restart", "systemd-resolved"]); + eprintln!(" systemd-resolved detected."); + eprintln!(" Installed drop-in: {}", drop_in.display()); + eprintln!(" Run 'sudo numa uninstall' to remove.\n"); + return Ok(()); + } + + // Fallback: direct resolv.conf manipulation + let resolv = std::path::Path::new("/etc/resolv.conf"); + let backup = backup_path_linux(); + + // Ensure backup directory exists + if let Some(parent) = backup.parent() { + std::fs::create_dir_all(parent) + .map_err(|e| format!("failed to create {}: {}", parent.display(), e))?; + } + + // Back up current resolv.conf (ignore NotFound) + match std::fs::copy(resolv, &backup) { + Ok(_) => eprintln!(" Saved /etc/resolv.conf to {}", backup.display()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => return Err(format!("failed to backup /etc/resolv.conf: {}", e)), + } + + if resolv + .symlink_metadata() + .map(|m| m.file_type().is_symlink()) + .unwrap_or(false) + { + eprintln!(" warning: /etc/resolv.conf is a symlink — changes may not persist."); + eprintln!(" Consider using systemd-resolved or NetworkManager instead.\n"); } - // Write new resolv.conf pointing to Numa let content = "# Generated by Numa — run 'sudo numa uninstall' to restore\nnameserver 127.0.0.1\n"; std::fs::write(resolv, content) @@ -479,26 +527,45 @@ fn install_linux() -> Result<(), String> { #[cfg(target_os = "linux")] fn uninstall_linux() -> Result<(), String> { + // Check for systemd-resolved drop-in first + let drop_in = std::path::Path::new("/etc/systemd/resolved.conf.d/numa.conf"); + if drop_in.exists() { + std::fs::remove_file(drop_in) + .map_err(|e| format!("failed to remove {}: {}", drop_in.display(), e))?; + let _ = run_systemctl(&["restart", "systemd-resolved"]); + eprintln!(" Removed systemd-resolved drop-in. DNS restored.\n"); + return Ok(()); + } + + // Fallback: restore resolv.conf from backup let backup = backup_path_linux(); let resolv = std::path::Path::new("/etc/resolv.conf"); - if backup.exists() { - std::fs::copy(&backup, resolv) - .map_err(|e| format!("failed to restore /etc/resolv.conf: {}", e))?; - std::fs::remove_file(&backup).ok(); - eprintln!(" Restored /etc/resolv.conf from backup. Backup removed.\n"); - } else { - eprintln!(" No backup found at {}.", backup.display()); - eprintln!(" Manually edit /etc/resolv.conf to restore your DNS.\n"); + match std::fs::copy(&backup, resolv) { + Ok(_) => { + std::fs::remove_file(&backup).ok(); + eprintln!(" Restored /etc/resolv.conf from backup. Backup removed.\n"); + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + eprintln!(" No backup found at {}.", backup.display()); + eprintln!(" Manually edit /etc/resolv.conf to restore your DNS.\n"); + } + Err(e) => return Err(format!("failed to restore /etc/resolv.conf: {}", e)), + } + Ok(()) +} + +#[cfg(target_os = "linux")] +fn ensure_binary_installed() -> Result<(), String> { + if !std::path::Path::new("/usr/local/bin/numa").exists() { + return Err("numa binary not found at /usr/local/bin/numa. Run: sudo cp target/release/numa /usr/local/bin/numa".to_string()); } Ok(()) } #[cfg(target_os = "linux")] fn install_service_linux() -> Result<(), String> { - if !std::path::Path::new("/usr/local/bin/numa").exists() { - return Err("numa binary not found at /usr/local/bin/numa. Run: sudo cp target/release/numa /usr/local/bin/numa".to_string()); - } + ensure_binary_installed()?; let unit = include_str!("../numa.service"); std::fs::write(SYSTEMD_UNIT, unit) @@ -517,12 +584,17 @@ fn install_service_linux() -> Result<(), String> { #[cfg(target_os = "linux")] fn uninstall_service_linux() -> Result<(), String> { - let _ = run_systemctl(&["stop", "numa"]); - let _ = run_systemctl(&["disable", "numa"]); + if let Err(e) = run_systemctl(&["stop", "numa"]) { + eprintln!(" warning: {}", e); + } + if let Err(e) = run_systemctl(&["disable", "numa"]) { + eprintln!(" warning: {}", e); + } - if std::path::Path::new(SYSTEMD_UNIT).exists() { - std::fs::remove_file(SYSTEMD_UNIT) - .map_err(|e| format!("failed to remove {}: {}", SYSTEMD_UNIT, e))?; + if let Err(e) = std::fs::remove_file(SYSTEMD_UNIT) { + if e.kind() != std::io::ErrorKind::NotFound { + return Err(format!("failed to remove {}: {}", SYSTEMD_UNIT, e)); + } } let _ = run_systemctl(&["daemon-reload"]);