use std::path::PathBuf;
use crate::core::paths::FRAMEWORK_DIR_NAME;
pub const DEFAULT_DAEMON_ADDR: &str = "127.0.0.1:7880";
pub const DEFAULT_DAEMON_URL: &str = "http://127.0.0.1:7880";
pub const DEFAULT_CONSOLE_ADDR: &str = "127.0.0.1:7788";
pub const GATEWAY_PATH: &str = "/api/mpm";
pub fn default_daemon_addr() -> std::net::SocketAddr {
DEFAULT_DAEMON_ADDR
.parse()
.expect("DEFAULT_DAEMON_ADDR is a valid SocketAddr literal")
}
pub fn lock_file_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(FRAMEWORK_DIR_NAME)
.join("daemon.lock")
}
pub fn explicit_url_from_env() -> Option<String> {
std::env::var("TRUSTY_MPM_URL")
.ok()
.filter(|v| !v.trim().is_empty())
}
pub fn resolve_daemon_url(explicit: Option<&str>) -> String {
try_resolve_daemon_url(explicit).unwrap_or_else(isolated_refusal_url)
}
pub fn try_resolve_daemon_url(explicit: Option<&str>) -> Result<String, DaemonUrlError> {
if let Some(url) = explicit
&& !url.is_empty()
{
return Ok(url.to_string());
}
if let Some(url) = client_lock_url() {
return Ok(url);
}
if isolated_environment() {
return Err(DaemonUrlError::NoSandboxDaemon);
}
Ok(DEFAULT_DAEMON_URL.to_string())
}
pub fn client_lock_url() -> Option<String> {
let url = read_lock_file_url()?;
if isolated_environment() {
let home = dirs::home_dir();
let passwd_home = crate::core::host_state_gate::passwd_home_dir();
if lock_is_hosts(&url, home.as_deref(), passwd_home.as_deref()) {
return None;
}
}
Some(url)
}
fn lock_is_hosts(
lock_url: &str,
home: Option<&std::path::Path>,
passwd_home: Option<&std::path::Path>,
) -> bool {
if lock_url.trim_end_matches('/') == DEFAULT_DAEMON_URL {
return true;
}
let (Some(home), Some(account)) = (home, passwd_home) else {
return true;
};
let resolve =
|p: &std::path::Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
resolve(home) == resolve(account)
}
pub const ISOLATED_NO_DAEMON_URL: &str = "tm-sandbox-no-daemon://set-TRUSTY_MPM_URL";
pub fn refuse_isolated_no_daemon(url: &str) -> Result<(), DaemonUrlError> {
if url == ISOLATED_NO_DAEMON_URL {
Err(DaemonUrlError::NoSandboxDaemon)
} else {
Ok(())
}
}
fn isolated_refusal_url(err: DaemonUrlError) -> String {
tracing::warn!("{err}");
ISOLATED_NO_DAEMON_URL.to_string()
}
pub fn refuse_daemon_spawn_when_isolated() -> Result<(), DaemonUrlError> {
if isolated_environment() {
Err(DaemonUrlError::NoSandboxDaemon)
} else {
Ok(())
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum DaemonUrlError {
#[error(
"daemon unreachable at explicitly configured URL {url} (probed GET {url}/health); \
refusing to fall back to a different daemon — check the address or drop --url/TRUSTY_MPM_URL \
to use automatic discovery"
)]
Unreachable {
url: String,
},
#[error(
"no daemon reachable for this sandbox: TRUSTY_SANDBOX=1 or TRUSTY_DATA_DIR_OVERRIDE is \
set, and no --url/TRUSTY_MPM_URL, recorded console or live daemon lock names one; \
refusing the host default daemon (127.0.0.1:7880) — start one with \
scripts/sandbox_daemon.sh and pass its URL"
)]
NoSandboxDaemon,
}
pub const EXIT_DAEMON_URL_UNREACHABLE: i32 = crate::core::exit_codes::EXIT_UNAVAILABLE;
pub async fn resolve_daemon_url_probing(
client: &reqwest::Client,
explicit: Option<&str>,
) -> Result<String, DaemonUrlError> {
if let Some(url) = explicit
&& !url.is_empty()
{
return if probe_url(client, url).await {
Ok(url.to_string())
} else {
Err(DaemonUrlError::Unreachable {
url: url.to_string(),
})
};
}
try_resolve_daemon_url(explicit)
}
pub async fn resolve_daemon_url_for_cli(
client: &reqwest::Client,
explicit: Option<&str>,
) -> Result<String, DaemonUrlError> {
if explicit.is_some_and(|url| !url.is_empty()) {
resolve_daemon_url_probing(client, explicit).await
} else {
try_resolve_daemon_url_via_gateway(client, explicit).await
}
}
pub const GATEWAY_PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(500);
pub async fn resolve_daemon_url_via_gateway(
client: &reqwest::Client,
explicit: Option<&str>,
) -> String {
try_resolve_daemon_url_via_gateway(client, explicit)
.await
.unwrap_or_else(isolated_refusal_url)
}
async fn try_resolve_daemon_url_via_gateway(
client: &reqwest::Client,
explicit: Option<&str>,
) -> Result<String, DaemonUrlError> {
let recorded = trusty_common::read_daemon_addr("trusty-console")
.ok()
.flatten();
resolve_daemon_url_via_gateway_from(client, explicit, recorded, DEFAULT_CONSOLE_ADDR).await
}
async fn resolve_daemon_url_via_gateway_from(
client: &reqwest::Client,
explicit: Option<&str>,
recorded: Option<String>,
default_console: &str,
) -> Result<String, DaemonUrlError> {
let console_addr = match recorded_console_addr(recorded) {
Some(addr) => addr,
None if isolated_environment() => return try_resolve_daemon_url(explicit),
None => default_console.to_string(),
};
resolve_daemon_url_via_gateway_inner(client, explicit, &console_addr).await
}
pub fn isolated_environment() -> bool {
use trusty_common::credentials::{SANDBOX_ENV_VAR, sandbox_flag_set};
sandbox_flag_set(std::env::var_os(SANDBOX_ENV_VAR).as_deref())
|| std::env::var_os(trusty_common::DATA_DIR_OVERRIDE_ENV).is_some_and(|v| !v.is_empty())
}
fn recorded_console_addr(recorded: Option<String>) -> Option<String> {
recorded
.map(|a| a.trim().to_string())
.filter(|a| !a.is_empty())
}
pub fn console_addr() -> String {
console_addr_from(
trusty_common::read_daemon_addr("trusty-console")
.ok()
.flatten(),
)
}
fn console_addr_from(recorded: Option<String>) -> String {
recorded_console_addr(recorded).unwrap_or_else(|| DEFAULT_CONSOLE_ADDR.to_string())
}
pub fn console_addr_to_probe() -> Option<String> {
console_addr_to_probe_from(
trusty_common::read_daemon_addr("trusty-console")
.ok()
.flatten(),
isolated_environment(),
)
}
fn console_addr_to_probe_from(recorded: Option<String>, isolated: bool) -> Option<String> {
recorded_console_addr(recorded)
.or_else(|| (!isolated).then(|| DEFAULT_CONSOLE_ADDR.to_string()))
}
pub fn console_base_url() -> String {
format!("http://{}", console_addr())
}
async fn resolve_daemon_url_via_gateway_inner(
client: &reqwest::Client,
explicit: Option<&str>,
console_addr: &str,
) -> Result<String, DaemonUrlError> {
if let Some(url) = explicit
&& !url.is_empty()
{
return Ok(url.to_string());
}
let probe_client = reqwest::Client::builder()
.timeout(GATEWAY_PROBE_TIMEOUT)
.build()
.expect("gateway probe client is infallible");
let gateway_base = format!("http://{console_addr}{GATEWAY_PATH}");
if probe_url(&probe_client, &gateway_base).await {
return Ok(gateway_base);
}
resolve_daemon_url_probing(client, explicit).await
}
async fn probe_url(client: &reqwest::Client, url: &str) -> bool {
use std::time::Duration;
let base = url.trim_end_matches('/');
let probe = client
.get(format!("{base}/health"))
.timeout(Duration::from_millis(500))
.send()
.await;
probe.map(|r| r.status().is_success()).unwrap_or(false)
}
fn read_lock_file_url() -> Option<String> {
super::daemon_identity::read_lock().map(|lock| lock.addr)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn explicit_url_wins_over_everything() {
let result = resolve_daemon_url(Some("http://example.com:9999"));
assert_eq!(result, "http://example.com:9999");
}
#[test]
fn empty_explicit_falls_through() {
let result = resolve_daemon_url(Some(""));
assert!(!result.is_empty());
}
#[test]
#[serial_test::serial]
fn default_returned_when_no_lock_and_no_explicit() {
let _sandbox = EnvVarGuard::unset(SANDBOX_ENV_VAR);
let _data = EnvVarGuard::unset(DATA_DIR_OVERRIDE_ENV);
let result = resolve_daemon_url(None);
assert!(result.starts_with("http"));
}
#[test]
fn default_url_matches_addr() {
assert_eq!(
DEFAULT_DAEMON_URL,
format!("http://{DEFAULT_DAEMON_ADDR}"),
"client default URL and daemon bind addr drifted (issue #1268)"
);
}
#[test]
fn default_addr_parses() {
let addr = default_daemon_addr();
assert_eq!(addr.to_string(), DEFAULT_DAEMON_ADDR);
assert_eq!(addr.port(), 7880);
}
#[test]
fn explicit_value_equal_to_default_wins_immediately() {
let result = resolve_daemon_url(Some(DEFAULT_DAEMON_URL));
assert_eq!(
result, DEFAULT_DAEMON_URL,
"an explicit value equal to the default must win outright, not fall through to the lock file"
);
}
#[test]
fn lock_file_path_is_under_framework_root() {
let path = lock_file_path();
assert!(
path.ends_with(format!("{FRAMEWORK_DIR_NAME}/daemon.lock")),
"lock file path {path:?} is not under the {FRAMEWORK_DIR_NAME} root"
);
assert_eq!(
path.parent().and_then(|p| p.file_name()),
Some(std::ffi::OsStr::new(FRAMEWORK_DIR_NAME))
);
}
#[tokio::test]
async fn probing_resolver_errors_when_explicit_unreachable() {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_millis(200))
.build()
.expect("build client");
let stale = "http://127.0.0.1:1"; let result = resolve_daemon_url_probing(&client, Some(stale)).await;
assert_eq!(
result,
Err(DaemonUrlError::Unreachable {
url: stale.to_string()
}),
"explicit unreachable URL must error, not fall back to a different daemon"
);
}
#[tokio::test]
async fn probing_resolver_wins_when_explicit_reachable() {
use tokio::io::AsyncWriteExt as _;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind listener");
let addr = listener.local_addr().expect("local_addr");
let url = format!("http://{addr}");
tokio::spawn(async move {
if let Ok((mut sock, _)) = listener.accept().await {
let _ = sock
.write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok")
.await;
}
});
let client = reqwest::Client::new();
let result = resolve_daemon_url_probing(&client, Some(&url)).await;
assert_eq!(
result,
Ok(url),
"reachable explicit URL must win over lock file / default"
);
}
#[tokio::test]
#[serial_test::serial]
async fn probing_resolver_implicit_none_falls_back_ok() {
let _sandbox = EnvVarGuard::unset(SANDBOX_ENV_VAR);
let _data = EnvVarGuard::unset(DATA_DIR_OVERRIDE_ENV);
let client = reqwest::Client::new();
let result = resolve_daemon_url_probing(&client, None).await;
let url = result.expect("implicit resolution must never error");
assert!(url.starts_with("http"), "expected HTTP URL, got: {url}");
}
#[test]
fn exit_code_matches_sm_unavailable_convention() {
assert_eq!(EXIT_DAEMON_URL_UNREACHABLE, 75);
}
#[tokio::test]
async fn resolve_for_cli_explicit_unreachable_errors() {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_millis(200))
.build()
.expect("build client");
let stale = "http://127.0.0.1:1";
let result = resolve_daemon_url_for_cli(&client, Some(stale)).await;
assert_eq!(
result,
Err(DaemonUrlError::Unreachable {
url: stale.to_string()
})
);
}
#[tokio::test]
#[serial_test::serial]
async fn resolve_for_cli_implicit_falls_back() {
let _sandbox = EnvVarGuard::unset(SANDBOX_ENV_VAR);
let _data = EnvVarGuard::unset(DATA_DIR_OVERRIDE_ENV);
let client = reqwest::Client::new();
let result = resolve_daemon_url_for_cli(&client, None).await;
let url = result.expect("implicit resolution must never error");
assert!(url.starts_with("http"), "expected HTTP URL, got: {url}");
}
#[tokio::test]
async fn resolve_via_gateway_explicit_override_wins() {
let client = reqwest::Client::new();
let explicit = "http://127.0.0.1:9999";
let result =
resolve_daemon_url_via_gateway_inner(&client, Some(explicit), "127.0.0.1:1").await;
assert_eq!(
result.as_deref(),
Ok(explicit),
"explicit override must win over gateway probe"
);
}
#[tokio::test]
async fn resolve_via_gateway_explicit_override_equal_to_default_still_wins() {
use tokio::io::AsyncWriteExt as _;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind listener");
let addr = listener.local_addr().expect("local_addr");
let hit_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let hit_count_clone = hit_count.clone();
tokio::spawn(async move {
while let Ok((mut sock, _)) = listener.accept().await {
hit_count_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let _ = sock
.write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok")
.await;
}
});
let client = reqwest::Client::new();
let console_addr = addr.to_string();
let result =
resolve_daemon_url_via_gateway_inner(&client, Some(DEFAULT_DAEMON_URL), &console_addr)
.await;
assert_eq!(
result.as_deref(),
Ok(DEFAULT_DAEMON_URL),
"TRUSTY_MPM_URL equal to the default must win outright, never the gateway URL"
);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert_eq!(
hit_count.load(std::sync::atomic::Ordering::SeqCst),
0,
"the gateway/console must never be probed when an explicit override is present"
);
}
#[tokio::test]
async fn resolve_via_gateway_uses_gateway_when_probe_succeeds() {
use tokio::io::{AsyncBufReadExt as _, AsyncWriteExt as _, BufReader};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind listener");
let addr = listener.local_addr().expect("local_addr");
let (path_tx, path_rx) = tokio::sync::oneshot::channel::<String>();
tokio::spawn(async move {
if let Ok((sock, _)) = listener.accept().await {
let (reader, mut writer) = sock.into_split();
let mut lines = BufReader::new(reader).lines();
let req_line = lines.next_line().await.unwrap().unwrap_or_default();
let _ = path_tx.send(req_line);
let _ = writer
.write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok")
.await;
}
});
let client = reqwest::Client::new();
let console_addr = addr.to_string();
let result = resolve_daemon_url_via_gateway_inner(&client, None, &console_addr).await;
let expected_gateway = format!("http://{console_addr}{GATEWAY_PATH}");
assert_eq!(
result,
Ok(expected_gateway),
"gateway URL must be returned when console probe succeeds"
);
let req_line = path_rx.await.expect("request line must be captured");
assert!(
req_line.starts_with("GET /api/mpm/health "),
"gateway probe must target /api/mpm/health; got: {req_line:?}"
);
}
#[tokio::test]
#[serial_test::serial]
async fn resolve_via_gateway_falls_back_to_direct_on_probe_failure() {
let _sandbox = EnvVarGuard::unset(SANDBOX_ENV_VAR);
let _data = EnvVarGuard::unset(DATA_DIR_OVERRIDE_ENV);
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_millis(200))
.build()
.expect("build client");
let dead_console = "127.0.0.1:1";
let result = resolve_daemon_url_via_gateway_inner(&client, None, dead_console)
.await
.expect("outside isolation the direct fallback never errors");
let gateway_url = format!("http://{dead_console}{GATEWAY_PATH}");
assert!(
result.starts_with("http"),
"fallback must still return an HTTP URL, got: {result}"
);
assert_ne!(
result, gateway_url,
"must not return the unreachable gateway URL on probe failure"
);
}
#[tokio::test]
async fn probe_url_trims_trailing_slash() {
use tokio::io::{AsyncBufReadExt as _, AsyncWriteExt as _, BufReader};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind listener");
let addr = listener.local_addr().expect("local_addr");
let (path_tx, path_rx) = tokio::sync::oneshot::channel::<String>();
tokio::spawn(async move {
if let Ok((sock, _)) = listener.accept().await {
let (reader, mut writer) = sock.into_split();
let mut lines = BufReader::new(reader).lines();
let req_line = lines.next_line().await.unwrap().unwrap_or_default();
let _ = path_tx.send(req_line);
let _ = writer
.write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n")
.await;
}
});
let client = reqwest::Client::new();
let url_with_slash = format!("http://{addr}/");
probe_url(&client, &url_with_slash).await;
let req_line = path_rx.await.expect("request line captured");
assert!(
req_line.starts_with("GET /health "),
"probe must request /health (no double slash); got: {req_line:?}"
);
}
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
crate::core::trusty_tools_config::env_test_lock()
}
#[test]
fn explicit_url_from_env_reads_set_var() {
let _g = env_lock();
let prior = std::env::var("TRUSTY_MPM_URL").ok();
unsafe { std::env::set_var("TRUSTY_MPM_URL", "http://127.0.0.1:7880") };
let result = explicit_url_from_env();
match prior {
Some(v) => unsafe { std::env::set_var("TRUSTY_MPM_URL", v) },
None => unsafe { std::env::remove_var("TRUSTY_MPM_URL") },
}
assert_eq!(
result.as_deref(),
Some("http://127.0.0.1:7880"),
"must read TRUSTY_MPM_URL verbatim, even when it equals the default"
);
}
#[test]
fn explicit_url_from_env_treats_empty_as_unset() {
let _g = env_lock();
let prior = std::env::var("TRUSTY_MPM_URL").ok();
unsafe { std::env::set_var("TRUSTY_MPM_URL", " ") };
let result = explicit_url_from_env();
match prior {
Some(v) => unsafe { std::env::set_var("TRUSTY_MPM_URL", v) },
None => unsafe { std::env::remove_var("TRUSTY_MPM_URL") },
}
assert_eq!(
result, None,
"a whitespace-only TRUSTY_MPM_URL must resolve to None, same as unset"
);
}
#[test]
fn explicit_url_from_env_unset_is_none() {
let _g = env_lock();
let prior = std::env::var("TRUSTY_MPM_URL").ok();
unsafe { std::env::remove_var("TRUSTY_MPM_URL") };
let result = explicit_url_from_env();
if let Some(v) = prior {
unsafe { std::env::set_var("TRUSTY_MPM_URL", v) };
}
assert_eq!(result, None, "an unset TRUSTY_MPM_URL must resolve to None");
}
#[test]
fn console_addr_from_prefers_the_recorded_addr() {
assert_eq!(
console_addr_from(Some("127.0.0.1:9911".to_string())),
"127.0.0.1:9911"
);
assert_eq!(
console_addr_from(Some(" 127.0.0.1:9911\n".to_string())),
"127.0.0.1:9911"
);
}
#[test]
fn console_addr_from_falls_back_when_absent_or_blank() {
assert_eq!(console_addr_from(None), DEFAULT_CONSOLE_ADDR);
assert_eq!(console_addr_from(Some(String::new())), DEFAULT_CONSOLE_ADDR);
assert_eq!(
console_addr_from(Some(" \n".to_string())),
DEFAULT_CONSOLE_ADDR
);
}
#[test]
fn console_base_url_wraps_the_resolved_addr() {
let url = console_base_url();
assert!(
url.starts_with("http://"),
"console base URL must be an http:// URL: {url}"
);
assert_eq!(url, format!("http://{}", console_addr()));
}
use crate::secret_source::test_env::EnvVarGuard;
use trusty_common::DATA_DIR_OVERRIDE_ENV;
use trusty_common::credentials::SANDBOX_ENV_VAR;
fn serve_one_ok(listener: tokio::net::TcpListener) {
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
tokio::spawn(async move {
if let Ok((mut sock, _)) = listener.accept().await {
let mut buf = [0u8; 1024];
let _ = sock.read(&mut buf).await;
let _ = sock
.write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok")
.await;
}
});
}
async fn was_contacted(listener: &tokio::net::TcpListener) -> bool {
tokio::time::timeout(std::time::Duration::from_millis(300), listener.accept())
.await
.is_ok()
}
#[tokio::test]
#[serial_test::serial]
async fn gateway_probe_skipped_under_data_dir_override() {
let data = crate::test_support::hermetic_temp_dir();
let _sandbox = EnvVarGuard::unset(SANDBOX_ENV_VAR);
let _data = EnvVarGuard::set(DATA_DIR_OVERRIDE_ENV, data.path());
let recorded = trusty_common::read_daemon_addr("trusty-console")
.ok()
.flatten();
assert_eq!(recorded, None, "the override dir records no console");
let default_console = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind listener");
let addr = default_console
.local_addr()
.expect("local_addr")
.to_string();
let result =
resolve_daemon_url_via_gateway_from(&reqwest::Client::new(), None, recorded, &addr)
.await;
assert_ne!(result, Ok(format!("http://{addr}{GATEWAY_PATH}")));
assert!(
!was_contacted(&default_console).await,
"under {DATA_DIR_OVERRIDE_ENV} the default console must get zero connections"
);
}
#[tokio::test]
#[serial_test::serial]
async fn gateway_probe_skipped_under_sandbox_flag() {
let _data = EnvVarGuard::unset(DATA_DIR_OVERRIDE_ENV);
let _sandbox = EnvVarGuard::set(SANDBOX_ENV_VAR, "1");
let default_console = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind listener");
let addr = default_console
.local_addr()
.expect("local_addr")
.to_string();
let _ =
resolve_daemon_url_via_gateway_from(&reqwest::Client::new(), None, None, &addr).await;
assert!(
!was_contacted(&default_console).await,
"under {SANDBOX_ENV_VAR}=1 the default console must get zero connections"
);
}
#[tokio::test]
#[serial_test::serial]
async fn gateway_probe_uses_recorded_addr_under_data_dir_override() {
let data = crate::test_support::hermetic_temp_dir();
let _sandbox = EnvVarGuard::set(SANDBOX_ENV_VAR, "1");
let _data = EnvVarGuard::set(DATA_DIR_OVERRIDE_ENV, data.path());
let recorded = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind listener");
let addr = recorded.local_addr().expect("local_addr").to_string();
serve_one_ok(recorded);
let result = resolve_daemon_url_via_gateway_from(
&reqwest::Client::new(),
None,
Some(addr.clone()),
"127.0.0.1:1",
)
.await;
assert_eq!(result, Ok(format!("http://{addr}{GATEWAY_PATH}")));
}
#[tokio::test]
#[serial_test::serial]
async fn gateway_probe_uses_default_console_outside_isolation() {
let _data = EnvVarGuard::unset(DATA_DIR_OVERRIDE_ENV);
let _sandbox = EnvVarGuard::unset(SANDBOX_ENV_VAR);
let default_console = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind listener");
let addr = default_console
.local_addr()
.expect("local_addr")
.to_string();
serve_one_ok(default_console);
let result =
resolve_daemon_url_via_gateway_from(&reqwest::Client::new(), None, None, &addr).await;
assert_eq!(result, Ok(format!("http://{addr}{GATEWAY_PATH}")));
}
#[tokio::test]
#[serial_test::serial]
async fn resolve_refuses_default_daemon_under_isolation() {
let home = crate::test_support::hermetic_temp_dir();
let data = crate::test_support::hermetic_temp_dir();
let _home = EnvVarGuard::set("HOME", home.path());
let _sandbox = EnvVarGuard::unset(SANDBOX_ENV_VAR);
let _data = EnvVarGuard::set(DATA_DIR_OVERRIDE_ENV, data.path());
let client = reqwest::Client::new();
assert_ne!(resolve_daemon_url(None), DEFAULT_DAEMON_URL);
assert_ne!(
resolve_daemon_url_via_gateway(&client, None).await,
DEFAULT_DAEMON_URL
);
for result in [
resolve_daemon_url_probing(&client, None).await,
resolve_daemon_url_for_cli(&client, None).await,
] {
let err = result.expect_err("an isolated resolver must refuse, not fall back");
assert!(
err.to_string()
.contains("no daemon reachable for this sandbox"),
"the refusal must say why: {err}"
);
}
}
#[tokio::test]
#[serial_test::serial]
async fn resolve_keeps_default_daemon_outside_isolation() {
let home = crate::test_support::hermetic_temp_dir();
let _home = EnvVarGuard::set("HOME", home.path());
let _sandbox = EnvVarGuard::unset(SANDBOX_ENV_VAR);
let _data = EnvVarGuard::unset(DATA_DIR_OVERRIDE_ENV);
let client = reqwest::Client::new();
assert_eq!(resolve_daemon_url(None), DEFAULT_DAEMON_URL);
assert_eq!(
resolve_daemon_url_probing(&client, None).await,
Ok(DEFAULT_DAEMON_URL.to_string())
);
}
#[tokio::test]
#[serial_test::serial]
async fn isolated_resolver_ignores_a_host_lock_naming_the_default() {
let home = crate::test_support::hermetic_temp_dir();
let data = crate::test_support::hermetic_temp_dir();
let _home = EnvVarGuard::set("HOME", home.path());
let _sandbox = EnvVarGuard::set(SANDBOX_ENV_VAR, "1");
let _data = EnvVarGuard::set(DATA_DIR_OVERRIDE_ENV, data.path());
crate::core::daemon_identity::write_lock_at(&lock_file_path(), DEFAULT_DAEMON_URL, "");
assert_ne!(resolve_daemon_url(None), DEFAULT_DAEMON_URL);
let err = resolve_daemon_url_probing(&reqwest::Client::new(), None)
.await
.expect_err("a host lock is not this sandbox's daemon");
assert!(
err.to_string()
.contains("no daemon reachable for this sandbox"),
"{err}"
);
}
#[test]
#[serial_test::serial]
fn client_lock_url_ignores_a_host_lock_under_isolation() {
let home = crate::test_support::hermetic_temp_dir();
let data = crate::test_support::hermetic_temp_dir();
let _home = EnvVarGuard::set("HOME", home.path());
let _sandbox = EnvVarGuard::unset(SANDBOX_ENV_VAR);
let _data = EnvVarGuard::set(DATA_DIR_OVERRIDE_ENV, data.path());
crate::core::daemon_identity::write_lock_at(&lock_file_path(), DEFAULT_DAEMON_URL, "");
assert_eq!(client_lock_url(), None, "a host lock is not this sandbox's");
}
#[test]
#[serial_test::serial]
fn client_lock_url_is_read_lock_outside_isolation() {
let home = crate::test_support::hermetic_temp_dir();
let _home = EnvVarGuard::set("HOME", home.path());
let _sandbox = EnvVarGuard::unset(SANDBOX_ENV_VAR);
let _data = EnvVarGuard::unset(DATA_DIR_OVERRIDE_ENV);
crate::core::daemon_identity::write_lock_at(&lock_file_path(), DEFAULT_DAEMON_URL, "");
assert_eq!(client_lock_url().as_deref(), Some(DEFAULT_DAEMON_URL));
assert_eq!(
client_lock_url(),
crate::core::daemon_identity::read_lock().map(|lock| lock.addr)
);
}
#[test]
#[serial_test::serial]
fn isolated_resolver_keeps_a_sandbox_home_lock() {
let home = crate::test_support::hermetic_temp_dir();
let data = crate::test_support::hermetic_temp_dir();
let _home = EnvVarGuard::set("HOME", home.path());
let _sandbox = EnvVarGuard::set(SANDBOX_ENV_VAR, "1");
let _data = EnvVarGuard::set(DATA_DIR_OVERRIDE_ENV, data.path());
let sandbox_daemon = "http://127.0.0.1:17881";
crate::core::daemon_identity::write_lock_at(&lock_file_path(), sandbox_daemon, "");
assert_eq!(resolve_daemon_url(None), sandbox_daemon);
}
#[test]
fn host_lock_is_judged_by_address_and_home() {
let sandbox = crate::test_support::hermetic_temp_dir();
let account = crate::test_support::hermetic_temp_dir();
let (sandbox, account) = (Some(sandbox.path()), Some(account.path()));
let other = "http://127.0.0.1:17881";
assert!(!lock_is_hosts(other, sandbox, account));
assert!(lock_is_hosts(other, account, account));
assert!(lock_is_hosts(DEFAULT_DAEMON_URL, sandbox, account));
assert!(lock_is_hosts("http://127.0.0.1:7880/", sandbox, account));
assert!(lock_is_hosts(other, None, account));
assert!(lock_is_hosts(other, sandbox, None));
}
#[test]
fn console_probe_target_is_none_under_isolation() {
assert_eq!(console_addr_to_probe_from(None, true), None);
assert_eq!(
console_addr_to_probe_from(None, false).as_deref(),
Some(DEFAULT_CONSOLE_ADDR)
);
assert_eq!(
console_addr_to_probe_from(Some(" 127.0.0.1:9911\n".into()), true).as_deref(),
Some("127.0.0.1:9911")
);
}
#[test]
#[serial_test::serial]
fn daemon_spawn_refused_only_under_isolation() {
let data = crate::test_support::hermetic_temp_dir();
{
let _sandbox = EnvVarGuard::set(SANDBOX_ENV_VAR, "1");
let _data = EnvVarGuard::unset(DATA_DIR_OVERRIDE_ENV);
assert_eq!(
refuse_daemon_spawn_when_isolated(),
Err(DaemonUrlError::NoSandboxDaemon)
);
}
{
let _sandbox = EnvVarGuard::unset(SANDBOX_ENV_VAR);
let _data = EnvVarGuard::set(DATA_DIR_OVERRIDE_ENV, data.path());
assert_eq!(
refuse_daemon_spawn_when_isolated(),
Err(DaemonUrlError::NoSandboxDaemon)
);
}
let _sandbox = EnvVarGuard::unset(SANDBOX_ENV_VAR);
let _data = EnvVarGuard::unset(DATA_DIR_OVERRIDE_ENV);
assert_eq!(refuse_daemon_spawn_when_isolated(), Ok(()));
}
#[test]
fn isolated_no_daemon_url_maps_back_to_the_refusal() {
assert_eq!(
refuse_isolated_no_daemon(ISOLATED_NO_DAEMON_URL),
Err(DaemonUrlError::NoSandboxDaemon)
);
assert_eq!(refuse_isolated_no_daemon(DEFAULT_DAEMON_URL), Ok(()));
}
#[tokio::test]
async fn isolated_no_daemon_url_is_refused_before_any_io() {
let err = reqwest::Client::new()
.get(format!("{ISOLATED_NO_DAEMON_URL}/health"))
.send()
.await
.expect_err("a non-HTTP scheme is never sent");
assert!(
err.is_builder(),
"refused while building, not on the wire: {err}"
);
assert!(err.to_string().contains(ISOLATED_NO_DAEMON_URL), "{err}");
}
}