Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 9 additions & 17 deletions app/arcbox-cli/src/commands/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! - stop a running daemon (`abctl daemon stop`)
//! - inspect daemon status (`abctl daemon status`)

use anyhow::{Context, Result, bail};
use anyhow::{bail, Context, Result};
use arcbox_constants::{
container_network::ContainerNetwork,
paths::{ArcboxProfile, HostLayout},
Expand All @@ -20,9 +20,12 @@ use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, SystemTime};
use tokio::time::{Instant, sleep, timeout};
use tokio::time::{sleep, timeout, Instant};
use tracing::warn;

#[path = "resolve_daemon.rs"]
mod resolve_daemon;

/// Arguments for the daemon command.
#[derive(Debug, Args)]
pub struct DaemonArgs {
Expand Down Expand Up @@ -397,19 +400,8 @@ impl SpawnLock {

fn resolve_daemon_binary() -> Result<PathBuf> {
let current_exe = std::env::current_exe().context("Failed to resolve current executable")?;

if let Some(parent) = current_exe.parent() {
let sibling = parent.join("arcbox-daemon");
if sibling.is_file() {
return Ok(sibling);
}
}

if let Some(path) = find_in_path("arcbox-daemon") {
return Ok(path);
}

bail!("Failed to locate `arcbox-daemon` next to `abctl` or in PATH");
resolve_daemon::resolve_daemon_binary_from(&current_exe, find_in_path)
.map_err(anyhow::Error::msg)
}

fn find_in_path(binary: &str) -> Option<PathBuf> {
Expand Down Expand Up @@ -561,8 +553,8 @@ fn send_sigterm(pid: i32) -> Result<()> {
#[cfg(test)]
mod tests {
use super::{
DaemonAction, DaemonArgs, SpawnLock, build_daemon_args, read_lock_file,
wait_for_lock_handoff,
build_daemon_args, read_lock_file, wait_for_lock_handoff, DaemonAction, DaemonArgs,
SpawnLock,
};
use arcbox_constants::paths::ArcboxProfile;
use std::ffi::OsString;
Expand Down
124 changes: 124 additions & 0 deletions app/arcbox-cli/src/commands/resolve_daemon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//! Locate `arcbox-daemon` next to `abctl`, following Homebrew cask symlinks.

use std::path::{Path, PathBuf};

const LOCATE_ERR: &str = "Failed to locate `arcbox-daemon` next to `abctl` or in PATH";

pub(crate) fn resolve_daemon_binary_from(
current_exe: &Path,
path_lookup: impl FnOnce(&str) -> Option<PathBuf>,
) -> Result<PathBuf, String> {
if let Some(path) = daemon_sibling_file(current_exe) {
return Ok(path);
}

// Homebrew cask PATH-links only `abctl`, so current_exe() is that
// symlink and the sibling lives next to the real binary.
if let Ok(resolved) = current_exe.canonicalize() {
if let Some(path) = daemon_sibling_file(&resolved) {
return Ok(path);
}
}

if let Some(path) = path_lookup("arcbox-daemon") {
return Ok(path);
}

Err(LOCATE_ERR.to_string())
}

fn daemon_sibling_file(exe: &Path) -> Option<PathBuf> {
let sibling = exe.parent()?.join("arcbox-daemon");
sibling.is_file().then_some(sibling)
}

#[cfg(test)]
mod tests {
use super::resolve_daemon_binary_from;
use std::fs;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

fn scratch() -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time")
.as_nanos();
let dir = std::env::temp_dir().join(format!("arcbox-resolve-daemon-{nanos}"));
fs::create_dir_all(&dir).expect("scratch dir");
dir
}

fn touch(path: &Path) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create parent dirs");
}
fs::write(path, []).expect("touch file");
}

#[test]
fn follows_brew_cask_symlink_to_app_bundle_sibling() {
let dir = scratch();
let app_bin = dir
.join("ArcBox.app")
.join("Contents")
.join("MacOS")
.join("bin");
let brew_bin = dir.join("opt").join("homebrew").join("bin");
let real_abctl = app_bin.join("abctl");
let real_daemon = app_bin.join("arcbox-daemon");
let brew_abctl = brew_bin.join("abctl");

touch(&real_abctl);
touch(&real_daemon);
fs::create_dir_all(&brew_bin).expect("create brew bin");
symlink(&real_abctl, &brew_abctl).expect("symlink brew abctl");

let found = resolve_daemon_binary_from(&brew_abctl, |_| None)
.expect("should find daemon beside the resolved abctl");
assert_eq!(found, real_daemon);
let _ = fs::remove_dir_all(&dir);
}

#[test]
fn uses_direct_sibling() {
let dir = scratch();
let bin = dir.join("bin");
let abctl = bin.join("abctl");
let daemon = bin.join("arcbox-daemon");
touch(&abctl);
touch(&daemon);

let found =
resolve_daemon_binary_from(&abctl, |_| None).expect("should find daemon next to abctl");
assert_eq!(found, daemon);
let _ = fs::remove_dir_all(&dir);
}

#[test]
fn falls_back_to_path() {
let dir = scratch();
let brew_abctl = dir.join("opt").join("homebrew").join("bin").join("abctl");
let path_daemon = dir.join("elsewhere").join("arcbox-daemon");
touch(&brew_abctl);
touch(&path_daemon);

let found = resolve_daemon_binary_from(&brew_abctl, |_| Some(path_daemon.clone()))
.expect("should use PATH when no sibling exists");
assert_eq!(found, path_daemon);
let _ = fs::remove_dir_all(&dir);
}

#[test]
fn errors_when_missing() {
let dir = scratch();
let abctl = dir.join("bin").join("abctl");
touch(&abctl);

let err = resolve_daemon_binary_from(&abctl, |_| None)
.expect_err("missing sibling and PATH must fail");
assert!(err.contains("Failed to locate `arcbox-daemon` next to `abctl` or in PATH"));
let _ = fs::remove_dir_all(&dir);
}
}