diff --git a/lib/build.nix b/lib/build.nix index 3dfc027..f8766a8 100644 --- a/lib/build.nix +++ b/lib/build.nix @@ -338,12 +338,45 @@ leaseRun = if monitorEnabled then '' - _IDES_ENTER_OUTPUT="$(ides enter --kind shell --root "$PWD" --pid $$)" - _IDES_ENTER_STATUS=$? + # Detect if we're running inside direnv's ephemeral + # bash subprocess. direnv runs .envrc in a bash that + # exits immediately after evaluation, which would + # release the lease and stop services. When direnv + # is detected, we: + # 1. Walk up the process tree to find the real shell + # PID (fish/zsh/bash) for the lease, so the lease + # stays alive for the lifetime of the user's shell. + # 2. Skip the EXIT-trap cleanup — the daemon's + # prune_dead will release the lease and stop + # services when the real shell exits. + _IDES_SHELL_PID=$$ + _IDES_IN_DIRENV=0 + if [ -n "''${DIRENV_DIR:-}" ] || [ -n "''${DIRENV_DIFF:-}" ]; then + _IDES_IN_DIRENV=1 + # Walk up: $$ (bash) → direnv → real shell + _IDES_PARENT_PID=$(ps --no-header -o ppid:1 "$$" 2>/dev/null | tr -d ' ') + if [ -n "$_IDES_PARENT_PID" ]; then + _IDES_SHELL_PID=$(ps --no-header -o ppid:1 "$_IDES_PARENT_PID" 2>/dev/null | tr -d ' ') + _IDES_SHELL_PID="''${_IDES_SHELL_PID:-$_IDES_PARENT_PID}" + fi + fi + _IDES_ENTER_OUTPUT="" + _IDES_ENTER_STATUS=1 + for _ in 1 2 3 4 5; do + _IDES_ENTER_OUTPUT="$(ides enter --kind shell --root "$PWD" --pid "$_IDES_SHELL_PID" 2>/dev/null)" + _IDES_ENTER_STATUS=$? + if [ "$_IDES_ENTER_STATUS" -eq 0 ]; then + break + fi + sleep 0.2 + done if [ "$_IDES_ENTER_STATUS" -ne 0 ]; then unset _IDES_ENTER_OUTPUT - return "$_IDES_ENTER_STATUS" 2>/dev/null || exit "$_IDES_ENTER_STATUS" - fi + # Don't fail the entire shell if ides enter fails — + # services just won't be managed. The user can run + # `ides run` manually. + _IDES_IN_DIRENV=0 + else export IDES_LEASE_TOKEN="$_IDES_ENTER_OUTPUT" unset _IDES_ENTER_OUTPUT _IDES_ENTER_STATUS _ides_leave() { @@ -351,7 +384,27 @@ ides leave --token "$IDES_LEASE_TOKEN" fi } - trap _ides_leave EXIT + if [ "$_IDES_IN_DIRENV" -eq 1 ]; then + # Inside direnv: don't release the lease on EXIT. + # The daemon's prune_dead will clean up when the + # real shell (whose PID we registered) exits. + # Still chain to any existing EXIT trap (e.g. + # direnv's internal trap for env capture). + _ides_prev_exit_trap=$(trap -p EXIT | sed "s/^trap -- '//; s/' EXIT$//") + if [ -n "$_ides_prev_exit_trap" ]; then + eval "$_ides_prev_exit_trap" + fi + else + # Normal shell: preserve any existing EXIT trap and + # chain _ides_leave into it. + _ides_prev_exit_trap=$(trap -p EXIT | sed "s/^trap -- '//; s/' EXIT$//") + _ides_leave_wrapper() { + _ides_leave + [ -n "''${_ides_prev_exit_trap:-}" ] && eval "$_ides_prev_exit_trap" + } + trap _ides_leave_wrapper EXIT + fi + fi '' else ""; diff --git a/lucius/src/daemon.rs b/lucius/src/daemon.rs index 84a83ba..16fa861 100644 --- a/lucius/src/daemon.rs +++ b/lucius/src/daemon.rs @@ -175,6 +175,7 @@ pub fn serve(manifest: &Manifest, manifest_path: &Path) -> Result<()> { .map_err(|err| format!("failed to set daemon socket nonblocking: {err}"))?; let mut idle_since = Instant::now(); + let mut first_iteration = true; loop { match listener.accept() { Ok((stream, _)) => { @@ -186,13 +187,17 @@ pub fn serve(manifest: &Manifest, manifest_path: &Path) -> Result<()> { Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => { let pruned = leases::prune_dead(&manifest.set_id)?; let active = leases::active_count(&manifest.set_id)?; - if active == 0 && pruned > 0 { + // Only exit if we pruned dead leases AND this isn't the first + // iteration. On startup, stale leases from previous runs may + // exist; pruning them shouldn't cause an immediate exit. + if !first_iteration && active == 0 && pruned > 0 { systemd::down(manifest, &[])?; break; } if active == 0 && idle_since.elapsed() > Duration::from_secs(10) { break; } + first_iteration = false; thread::sleep(Duration::from_millis(100)); } Err(err) => return Err(format!("failed to accept daemon client: {err}")),