Skip to content
Merged
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
8 changes: 7 additions & 1 deletion crates/tracedecay-cli/src/commands/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,14 @@ pub(crate) async fn handle_init(
true,
)?;
handshake.moved_store_adoption = adoption;
// A connectable socket is the whole precondition: `brokered_init` carries
// its own 120 s bootstrap deadline, so a daemon that has not finished
// answering initialize within the one-second reachability probe is still
// the daemon this init must broker through. Requiring the identity proof
// here refused cold starts on CPU-constrained hosts and told the operator
// to start a daemon that was already running.
#[cfg(unix)]
let daemon_available = tracedecay_daemon_control::daemon_reachable();
let daemon_available = tracedecay_daemon_control::daemon_socket_connectable();
#[cfg(not(unix))]
let daemon_available = true;

Expand Down
18 changes: 9 additions & 9 deletions crates/tracedecay-daemon-control/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,15 @@ mod service;

pub use service::{
DaemonProcessProofV1, DaemonServiceSpec, DaemonServiceState, MaintenanceWindowOutcome,
QuiescedDaemonLifecycle, daemon_reachable, default_socket_path, install_service,
install_service_under_lease, installed_service_process_proof, installed_service_socket_path,
installed_service_state, prepare_scoop_package_service, quiesce_installed_service_before_lease,
refresh_installed_service_under_lease_with_state, restore_installed_service_after_update,
restore_scoop_package_service, service_spec, service_spec_with_remote_tls, service_status,
socket_path_or_default, start_service, stop_service, unavailable_daemon_socket_advice,
uninstall_service, verify_installed_service_quiesced_under_lease,
wait_for_installed_service_state, with_exclusive_maintenance_window,
with_quiesced_installed_service,
QuiescedDaemonLifecycle, daemon_reachable, daemon_socket_connectable, default_socket_path,
install_service, install_service_under_lease, installed_service_process_proof,
installed_service_socket_path, installed_service_state, prepare_scoop_package_service,
quiesce_installed_service_before_lease, refresh_installed_service_under_lease_with_state,
restore_installed_service_after_update, restore_scoop_package_service, service_spec,
service_spec_with_remote_tls, service_status, socket_path_or_default, start_service,
stop_service, unavailable_daemon_socket_advice, uninstall_service,
verify_installed_service_quiesced_under_lease, wait_for_installed_service_state,
with_exclusive_maintenance_window, with_quiesced_installed_service,
};

fn current_daemon_client_identity() -> Result<DaemonClientIdentity> {
Expand Down
2 changes: 1 addition & 1 deletion crates/tracedecay-daemon-control/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ mod tests;
#[allow(clippy::expect_used)]
mod update_restore_tests;

pub use probe::{DaemonProcessProofV1, daemon_reachable};
pub use probe::{DaemonProcessProofV1, daemon_reachable, daemon_socket_connectable};
pub use unit_file::installed_service_socket_path;

use probe::{
Expand Down
22 changes: 22 additions & 0 deletions crates/tracedecay-daemon-control/src/service/probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,28 @@ pub fn daemon_reachable() -> bool {
})
}

/// Whether the default socket has a listener, whether or not it named itself
/// inside the reachability probe timeout.
///
/// [`daemon_reachable`] answers "a daemon proved its identity in one second".
/// A cold daemon on a CPU-constrained host answers initialize later than that,
/// and a caller that owns its own deadline must not read that miss as "no
/// daemon is running": the socket is connectable only because a process is
/// accepting on it.
pub fn daemon_socket_connectable() -> bool {
default_socket_path().is_ok_and(|path| {
matches!(
daemon_readiness_probe(
&path,
env!("CARGO_PKG_VERSION"),
DAEMON_REACHABILITY_PROBE_TIMEOUT,
)
.0,
DaemonSocketState::Connectable
)
})
}

/// Probe `socket_path` once and return both the socket observation and the
/// initialize proof. Callers must not connect again to classify liveness.
pub(super) fn observe_daemon_process(
Expand Down
39 changes: 39 additions & 0 deletions crates/tracedecay-daemon-control/src/service/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,45 @@ fn daemon_reachable_requires_an_initialize_answer() {
server.join().expect("join initialize server");
}

#[cfg(unix)]
#[test]
fn daemon_socket_connectable_separates_a_slow_daemon_from_no_daemon() {
let _env_lock = lock_user_data_dir_test_env();
let profile = TempDir::new().expect("profile temp dir");

let missing = profile.path().join("missing.sock");
let missing_guard = EnvVarGuard::set(SOCKET_ENV, &missing);
assert!(
!super::daemon_socket_connectable(),
"a missing socket has no listener to broker through"
);
drop(missing_guard);

let stale = profile.path().join("stale.sock");
drop(UnixListener::bind(&stale).expect("bind stale socket"));
let stale_guard = EnvVarGuard::set(SOCKET_ENV, &stale);
assert!(
!super::daemon_socket_connectable(),
"a socket file whose listener is gone has no listener to broker through"
);
drop(stale_guard);

// The cold-start case: a daemon is accepting but has not answered
// initialize inside the one-second reachability probe.
let silent = profile.path().join("silent.sock");
let _listener = UnixListener::bind(&silent).expect("bind silent socket");
let silent_guard = EnvVarGuard::set(SOCKET_ENV, &silent);
assert!(
!super::daemon_reachable(),
"the identity proof is still absent while the daemon is starting"
);
assert!(
super::daemon_socket_connectable(),
"a daemon that has not answered initialize yet is still a running daemon"
);
drop(silent_guard);
}

#[cfg(unix)]
#[test]
fn daemon_status_reports_the_initialize_proof_not_only_the_socket() {
Expand Down
Loading