use crate::commands::deploy::config::{DeploymentConfig, DeploymentTarget};
use crate::commands::deploy::state::DeploymentState;
use crate::commands::deploy::templates;
use anyhow::{Context, Result, anyhow};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
pub async fn deploy(
project_root: &Path,
config: &DeploymentConfig,
_force_rebuild: bool,
) -> Result<DeploymentState> {
println!("Starting macOS launchd deployment...");
if config.target != DeploymentTarget::Daemon {
return Err(anyhow!("Invalid deployment target for launchd"));
}
if !cfg!(target_os = "macos") {
return Err(anyhow!("launchd deployment is only available on macOS"));
}
let project_name = project_root
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("audb-app");
let service_label = format!("com.audb.{}", project_name);
let port = config.port.unwrap_or(8080);
if is_service_loaded(&service_label)? {
println!("Warning: Service '{}' is already loaded", service_label);
println!("Stopping existing service...");
let _ = stop_service(&service_label);
}
println!("Building release binary...");
build_release_binary(project_root)?;
let binary_path = get_binary_path(project_root, project_name)?;
if !binary_path.exists() {
return Err(anyhow!(
"Binary not found at {}. Build may have failed.",
binary_path.display()
));
}
println!("Binary built at: {}", binary_path.display());
println!("Generating launchd plist...");
let plist_content = templates::generate_launchd_plist(&service_label, &binary_path, config);
let plist_path = get_plist_path(&service_label)?;
if let Some(parent) = plist_path.parent() {
fs::create_dir_all(parent).context("Failed to create LaunchAgents directory")?;
}
fs::write(&plist_path, plist_content)
.with_context(|| format!("Failed to write plist to {}", plist_path.display()))?;
println!("Plist created at: {}", plist_path.display());
println!("Loading service into launchd...");
load_service(&service_label, &plist_path)?;
println!("Enabling service...");
enable_service(&service_label)?;
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
let status = get_service_status(&service_label)?;
if !status.running {
println!("Warning: Service loaded but not running. Check logs for errors.");
println!("Logs: /tmp/{}.log", service_label);
println!("Errors: /tmp/{}.err", service_label);
} else {
println!("Service is running with PID: {}", status.pid.unwrap_or(0));
}
let state = DeploymentState {
target: config.target,
deployed_at: chrono::Utc::now(),
service_label: service_label.clone(),
project_name: project_name.to_string(),
};
state.save(project_root)?;
println!("\nDeployment successful!");
println!("Service label: {}", service_label);
println!("Port: {}", port);
println!("Plist: {}", plist_path.display());
println!("\nView logs with: au deploy logs");
println!("Check status with: au deploy status");
println!("Stop with: au deploy stop");
println!("\nManual commands:");
println!(" launchctl list | grep {}", service_label);
println!(" tail -f /tmp/{}.log", service_label);
Ok(state)
}
fn build_release_binary(project_root: &Path) -> Result<()> {
let status = Command::new("cargo")
.arg("build")
.arg("--release")
.current_dir(project_root)
.status()
.context("Failed to run cargo build")?;
if !status.success() {
return Err(anyhow!("Cargo build failed"));
}
Ok(())
}
fn get_binary_path(project_root: &Path, project_name: &str) -> Result<PathBuf> {
let mut current = project_root;
let mut workspace_root = None;
while let Some(parent) = current.parent() {
let cargo_toml = parent.join("Cargo.toml");
if cargo_toml.exists() {
if let Ok(content) = fs::read_to_string(&cargo_toml) {
if content.contains("[workspace]") {
workspace_root = Some(parent);
break;
}
}
}
current = parent;
}
if let Some(workspace) = workspace_root {
let workspace_binary = workspace.join("target").join("release").join(project_name);
if workspace_binary.exists() {
return Ok(workspace_binary);
}
}
let project_binary = project_root
.join("target")
.join("release")
.join(project_name);
Ok(project_binary)
}
fn get_plist_path(service_label: &str) -> Result<PathBuf> {
let home = std::env::var("HOME").context("HOME environment variable not set")?;
Ok(PathBuf::from(home)
.join("Library")
.join("LaunchAgents")
.join(format!("{}.plist", service_label)))
}
fn is_service_loaded(service_label: &str) -> Result<bool> {
let output = Command::new("launchctl")
.arg("list")
.output()
.context("Failed to run launchctl list")?;
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(stdout.contains(service_label))
}
fn load_service(service_label: &str, plist_path: &Path) -> Result<()> {
let uid = get_user_id()?;
let domain = format!("gui/{}", uid);
let status = Command::new("launchctl")
.arg("bootstrap")
.arg(&domain)
.arg(plist_path)
.status()
.context("Failed to bootstrap service")?;
if !status.success() {
return Err(anyhow!("Failed to bootstrap service '{}'", service_label));
}
Ok(())
}
fn enable_service(service_label: &str) -> Result<()> {
let uid = get_user_id()?;
let service_target = format!("gui/{}/{}", uid, service_label);
let status = Command::new("launchctl")
.arg("enable")
.arg(&service_target)
.status()
.context("Failed to enable service")?;
if !status.success() {
println!("Note: Service may already be enabled");
}
Ok(())
}
fn get_user_id() -> Result<String> {
let output = Command::new("id")
.arg("-u")
.output()
.context("Failed to get user ID")?;
if !output.status.success() {
return Err(anyhow!("Failed to get user ID"));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
pub async fn stop(project_root: &Path) -> Result<()> {
let state = DeploymentState::load(project_root).context("No deployment state found")?;
println!("Stopping launchd service '{}'...", state.service_label);
stop_service(&state.service_label)?;
let plist_path = get_plist_path(&state.service_label)?;
if plist_path.exists() {
fs::remove_file(&plist_path)
.with_context(|| format!("Failed to remove plist at {}", plist_path.display()))?;
println!("Removed plist: {}", plist_path.display());
}
println!("Service stopped and unloaded");
let state_path = project_root.join(".audb").join("deploy.toml");
if state_path.exists() {
fs::remove_file(&state_path).context("Failed to remove state file")?;
}
Ok(())
}
fn stop_service(service_label: &str) -> Result<()> {
let uid = get_user_id()?;
let service_target = format!("gui/{}/{}", uid, service_label);
let _ = Command::new("launchctl")
.arg("disable")
.arg(&service_target)
.status();
let _ = Command::new("launchctl")
.arg("bootout")
.arg(&service_target)
.status();
Ok(())
}
pub async fn status(project_root: &Path) -> Result<()> {
let state = DeploymentState::load(project_root).context("No deployment state found")?;
println!("Service: {}", state.service_label);
println!("Project: {}", state.project_name);
println!(
"Deployed at: {}",
state.deployed_at.format("%Y-%m-%d %H:%M:%S UTC")
);
let status = get_service_status(&state.service_label)?;
if status.exists {
println!("Status: Loaded");
if status.running {
println!("Running: Yes");
if let Some(pid) = status.pid {
println!("PID: {}", pid);
}
} else {
println!("Running: No");
if let Some(ref error) = status.error {
println!("Error: {}", error);
}
}
} else {
println!("Status: Not loaded");
}
println!("\nLog files:");
println!(" stdout: /tmp/{}.log", state.service_label);
println!(" stderr: /tmp/{}.err", state.service_label);
Ok(())
}
fn get_service_status(service_label: &str) -> Result<ServiceStatus> {
let output = Command::new("launchctl")
.arg("list")
.arg(service_label)
.output()
.context("Failed to run launchctl list")?;
if !output.status.success() {
return Ok(ServiceStatus {
exists: false,
running: false,
pid: None,
error: None,
});
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut pid = None;
let mut exit_status = None;
for line in stdout.lines() {
if line.contains("\"PID\"") {
if let Some(value) = line.split('=').nth(1) {
if let Ok(p) = value.trim().trim_end_matches(';').parse::<u32>() {
pid = Some(p);
}
}
}
if line.contains("\"LastExitStatus\"") {
if let Some(value) = line.split('=').nth(1) {
if let Ok(code) = value.trim().trim_end_matches(';').parse::<i32>() {
exit_status = Some(code);
}
}
}
}
let running = pid.is_some();
let error = if !running {
exit_status.map(|code| format!("Last exit status: {}", code))
} else {
None
};
Ok(ServiceStatus {
exists: true,
running,
pid,
error,
})
}
pub async fn logs(project_root: &Path, follow: bool, tail: Option<String>) -> Result<()> {
let state = DeploymentState::load(project_root).context("No deployment state found")?;
let log_path = format!("/tmp/{}.log", state.service_label);
let err_path = format!("/tmp/{}.err", state.service_label);
println!("Showing logs for service '{}'...\n", state.service_label);
if follow {
let mut cmd = Command::new("tail");
cmd.arg("-f");
if let Some(n) = tail {
cmd.arg("-n").arg(n);
}
cmd.arg(&log_path);
let status = cmd.status().context("Failed to tail log file")?;
if !status.success() {
return Err(anyhow!("Failed to follow logs"));
}
} else {
let n = tail.unwrap_or_else(|| "100".to_string());
if PathBuf::from(&log_path).exists() {
println!("=== stdout ({}) ===", log_path);
let status = Command::new("tail")
.arg("-n")
.arg(&n)
.arg(&log_path)
.status()
.context("Failed to read log file")?;
if !status.success() {
println!("Warning: Could not read stdout log");
}
println!();
}
if PathBuf::from(&err_path).exists() {
println!("=== stderr ({}) ===", err_path);
let status = Command::new("tail")
.arg("-n")
.arg(&n)
.arg(&err_path)
.status()
.context("Failed to read error log")?;
if !status.success() {
println!("Warning: Could not read stderr log");
}
}
}
Ok(())
}
pub async fn restart(project_root: &Path) -> Result<()> {
let state = DeploymentState::load(project_root).context("No deployment state found")?;
println!("Restarting service '{}'...", state.service_label);
let uid = get_user_id()?;
let service_target = format!("gui/{}/{}", uid, state.service_label);
let status = Command::new("launchctl")
.arg("kickstart")
.arg("-k") .arg(&service_target)
.status()
.context("Failed to restart service")?;
if !status.success() {
return Err(anyhow!("Failed to restart service"));
}
println!("Service restarted successfully");
Ok(())
}
pub async fn cleanup(project_root: &Path) -> Result<()> {
println!("Cleaning up deployment...");
let service_label = if let Ok(state) = DeploymentState::load(project_root) {
state.service_label.clone()
} else {
let project_name = project_root
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("audb-app");
format!("com.audb.{}", project_name)
};
println!("Stopping service '{}'...", service_label);
let _ = stop_service(&service_label);
let plist_path = get_plist_path(&service_label)?;
if plist_path.exists() {
fs::remove_file(&plist_path)
.with_context(|| format!("Failed to remove plist at {}", plist_path.display()))?;
println!("Removed plist: {}", plist_path.display());
}
let state_path = project_root.join(".audb").join("deploy.toml");
if state_path.exists() {
fs::remove_file(&state_path).context("Failed to remove state file")?;
println!("Removed state file");
}
println!("Cleanup complete!");
Ok(())
}
#[derive(Debug)]
struct ServiceStatus {
exists: bool,
running: bool,
pid: Option<u32>,
error: Option<String>,
}