#![allow(dead_code, unused_imports, unused_variables, clippy::ptr_arg)]
use super::{
PlatformUpgradeHandler, UpgradeConfig, UpgradeError, UpgradeResult,
platform::BasePlatformHandler,
};
use anyhow::Result;
use std::path::PathBuf;
use std::process::Command;
use tracing::{debug, info, warn};
pub struct MacOSUpgradeHandler {
base: BasePlatformHandler,
}
impl MacOSUpgradeHandler {
pub fn new(config: &UpgradeConfig) -> Result<Self> {
let base = BasePlatformHandler::new(config)?;
Ok(Self { base })
}
async fn install_app_bundle(&self, package_path: &PathBuf) -> UpgradeResult<()> {
info!("Installing macOS App Bundle: {:?}", package_path);
if !self.is_valid_app_bundle(package_path)? {
return Err(UpgradeError::InvalidPackage(
"Invalid macOS App Bundle".to_string(),
));
}
if self.base.config.require_signatures {
self.verify_code_signature(package_path).await?;
}
let install_dir = PathBuf::from("/Applications");
let app_name = package_path
.file_name()
.ok_or_else(|| UpgradeError::InvalidPackage("Invalid app bundle name".to_string()))?;
let target_path = install_dir.join(app_name);
self.quit_application().await?;
if target_path.exists() {
self.remove_app_bundle(&target_path).await?;
}
self.copy_app_bundle(package_path, &target_path).await?;
self.update_launch_services().await?;
info!("macOS App Bundle installation completed");
Ok(())
}
async fn install_homebrew(&self, package_name: &str) -> UpgradeResult<()> {
info!("Installing via Homebrew: {}", package_name);
if !self.is_homebrew_installed() {
return Err(UpgradeError::InstallationFailed(
"Homebrew not installed".to_string(),
));
}
let output = Command::new("brew")
.arg("update")
.output()
.map_err(|e| UpgradeError::InstallationFailed(e.to_string()))?;
if !output.status.success() {
warn!(
"Homebrew update failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let output = Command::new("brew")
.args(["upgrade", package_name])
.output()
.map_err(|e| UpgradeError::InstallationFailed(e.to_string()))?;
if !output.status.success() {
let output = Command::new("brew")
.args(["install", package_name])
.output()
.map_err(|e| UpgradeError::InstallationFailed(e.to_string()))?;
if !output.status.success() {
return Err(UpgradeError::InstallationFailed(format!(
"Homebrew installation failed: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
}
info!("Homebrew installation completed");
Ok(())
}
async fn verify_code_signature(&self, app_path: &PathBuf) -> UpgradeResult<()> {
debug!("Verifying code signature for: {:?}", app_path);
let output = Command::new("codesign")
.args(["--verify", "--deep", "--strict"])
.arg(app_path)
.output()
.map_err(|e| UpgradeError::VerificationFailed(e.to_string()))?;
if !output.status.success() {
return Err(UpgradeError::VerificationFailed(format!(
"Code signature verification failed: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
let output = Command::new("codesign")
.args(["-dv", "--verbose=4"])
.arg(app_path)
.output()
.map_err(|e| UpgradeError::VerificationFailed(e.to_string()))?;
let signature_info = String::from_utf8_lossy(&output.stderr);
debug!("Code signature info: {}", signature_info);
if signature_info.contains("Developer ID Application")
|| signature_info.contains("Mac App Store")
{
debug!("Code signature verification passed");
Ok(())
} else {
Err(UpgradeError::VerificationFailed(
"Untrusted code signature".to_string(),
))
}
}
fn is_valid_app_bundle(&self, path: &PathBuf) -> UpgradeResult<bool> {
if !path.exists() || !path.is_dir() {
return Ok(false);
}
let extension = path.extension().and_then(|ext| ext.to_str());
if extension != Some("app") {
return Ok(false);
}
let contents_dir = path.join("Contents");
let info_plist = contents_dir.join("Info.plist");
let macos_dir = contents_dir.join("MacOS");
Ok(contents_dir.exists() && info_plist.exists() && macos_dir.exists())
}
async fn quit_application(&self) -> UpgradeResult<()> {
debug!("Attempting to quit application gracefully");
let output = Command::new("osascript")
.args(["-e", "tell application \"Inferno\" to quit"])
.output();
if let Ok(output) = output {
if output.status.success() {
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
return Ok(());
}
}
let output = Command::new("pkill").args(["-f", "Inferno"]).output();
if let Ok(output) = output {
if output.status.success() {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
}
}
Ok(())
}
async fn remove_app_bundle(&self, app_path: &PathBuf) -> UpgradeResult<()> {
debug!("Removing existing app bundle: {:?}", app_path);
let output = Command::new("rm")
.args(["-rf", app_path.to_str().unwrap()])
.output()
.map_err(|e| UpgradeError::InstallationFailed(e.to_string()))?;
if !output.status.success() {
return Err(UpgradeError::InstallationFailed(format!(
"Failed to remove existing app bundle: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
Ok(())
}
async fn copy_app_bundle(&self, source: &PathBuf, target: &PathBuf) -> UpgradeResult<()> {
debug!("Copying app bundle from {:?} to {:?}", source, target);
let output = Command::new("cp")
.args(["-R", source.to_str().unwrap(), target.to_str().unwrap()])
.output()
.map_err(|e| UpgradeError::InstallationFailed(e.to_string()))?;
if !output.status.success() {
return Err(UpgradeError::InstallationFailed(format!(
"Failed to copy app bundle: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
Ok(())
}
async fn update_launch_services(&self) -> UpgradeResult<()> {
debug!("Updating Launch Services database");
let output = Command::new("/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister")
.args(["-kill", "-r", "-domain", "local", "-domain", "system", "-domain", "user"])
.output();
if let Ok(output) = output {
if !output.status.success() {
warn!(
"Launch Services update failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
} else {
warn!("Could not update Launch Services database");
}
Ok(())
}
fn is_homebrew_installed(&self) -> bool {
Command::new("brew")
.arg("--version")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
fn get_app_info(&self, app_path: &PathBuf) -> UpgradeResult<AppBundleInfo> {
let info_plist = app_path.join("Contents/Info.plist");
let output = Command::new("plutil")
.args(["-convert", "json", "-o", "-"])
.arg(&info_plist)
.output()
.map_err(|e| UpgradeError::InvalidPackage(e.to_string()))?;
if !output.status.success() {
return Err(UpgradeError::InvalidPackage(
"Failed to read Info.plist".to_string(),
));
}
let json_str = String::from_utf8(output.stdout)
.map_err(|e| UpgradeError::InvalidPackage(e.to_string()))?;
let info: serde_json::Value = serde_json::from_str(&json_str)
.map_err(|e| UpgradeError::InvalidPackage(e.to_string()))?;
Ok(AppBundleInfo {
bundle_identifier: info["CFBundleIdentifier"]
.as_str()
.unwrap_or("unknown")
.to_string(),
bundle_version: info["CFBundleVersion"]
.as_str()
.unwrap_or("unknown")
.to_string(),
bundle_name: info["CFBundleName"]
.as_str()
.unwrap_or("unknown")
.to_string(),
})
}
async fn check_system_updates(&self) -> UpgradeResult<()> {
debug!("Checking for macOS system updates");
let output = Command::new("softwareupdate")
.args(["--list", "--no-scan"])
.output()
.map_err(|e| UpgradeError::Internal(e.to_string()))?;
if output.status.success() {
let update_list = String::from_utf8_lossy(&output.stdout);
if update_list.contains("restart") {
warn!(
"System updates requiring restart are available. Consider installing them after the upgrade."
);
}
}
Ok(())
}
async fn manage_gatekeeper(&self, disable: bool) -> UpgradeResult<()> {
if disable {
debug!("Temporarily disabling Gatekeeper");
let output = Command::new("sudo")
.args(["spctl", "--master-disable"])
.output();
if let Ok(output) = output {
if !output.status.success() {
warn!(
"Failed to disable Gatekeeper: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}
} else {
debug!("Re-enabling Gatekeeper");
let output = Command::new("sudo")
.args(["spctl", "--master-enable"])
.output();
if let Ok(output) = output {
if !output.status.success() {
warn!(
"Failed to re-enable Gatekeeper: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}
}
Ok(())
}
}
#[async_trait::async_trait]
impl PlatformUpgradeHandler for MacOSUpgradeHandler {
fn supports_seamless_upgrade(&self) -> bool {
self.base.supports_seamless_upgrade()
}
async fn prepare_for_upgrade(&self) -> Result<()> {
info!("Preparing macOS system for upgrade");
let _stopped = self.base.stop_services().await?;
self.check_system_updates().await?;
if !self.base.config.require_signatures {
self.manage_gatekeeper(true).await?;
}
Ok(())
}
async fn install_update(&self, package_path: &PathBuf) -> Result<()> {
let extension = package_path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
match extension.to_lowercase().as_str() {
"app" => {
self.install_app_bundle(package_path).await?;
}
"pkg" => {
let output = Command::new("installer")
.args(["-pkg", package_path.to_str().unwrap(), "-target", "/"])
.output()
.map_err(|e| anyhow::anyhow!("PKG installation failed: {}", e))?;
if !output.status.success() {
return Err(anyhow::anyhow!(
"PKG installation failed: {}",
String::from_utf8_lossy(&output.stderr)
));
}
}
"tar" | "tgz" | "tar.gz" => {
self.base.install_self_extractor(package_path).await?;
}
_ => {
return Err(anyhow::anyhow!(
"Unsupported package format for macOS: {}",
extension
));
}
}
Ok(())
}
async fn restart_application(&self) -> Result<()> {
info!("Restarting application on macOS");
let current_exe = std::env::current_exe()?;
let _child = Command::new("nohup")
.arg(¤t_exe)
.arg("serve") .spawn()?;
Ok(())
}
async fn verify_installation(&self) -> Result<bool> {
self.base.verify_installation().await.map_err(Into::into)
}
async fn cleanup_after_upgrade(&self) -> Result<()> {
if !self.base.config.require_signatures {
self.manage_gatekeeper(false).await?;
}
self.base.cleanup_after_upgrade().await?;
Ok(())
}
fn requires_elevated_privileges(&self) -> bool {
self.base.requires_elevated_privileges()
}
fn get_installation_directory(&self) -> PathBuf {
self.base.get_installation_directory()
}
fn get_backup_directory(&self) -> PathBuf {
self.base.get_backup_directory()
}
}
#[derive(Debug, Clone)]
struct AppBundleInfo {
bundle_identifier: String,
bundle_version: String,
bundle_name: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_macos_handler_creation() {
let config = UpgradeConfig::default();
let handler = MacOSUpgradeHandler::new(&config);
#[cfg(target_os = "macos")]
{
assert!(handler.is_ok());
let handler = handler.unwrap();
assert!(handler.supports_seamless_upgrade());
}
#[cfg(not(target_os = "macos"))]
{
println!("macOS handler test skipped on non-macOS platform");
}
}
#[test]
fn test_homebrew_detection() {
let config = UpgradeConfig::default();
#[cfg(target_os = "macos")]
{
if let Ok(handler) = MacOSUpgradeHandler::new(&config) {
let has_homebrew = handler.is_homebrew_installed();
println!("Homebrew installed: {}", has_homebrew);
}
}
}
}