From ca7574bc5f7ba05fb318294a00f2915604783b5d Mon Sep 17 00:00:00 2001 From: Martin Becze Date: Sat, 20 Jun 2026 20:45:57 +0200 Subject: [PATCH 1/5] fix: preserve existing EXIT trap from ides shellHook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ides shellHook sets `trap _ides_leave EXIT` which clobbers any existing EXIT trap. This breaks direnv (and any other tool that relies on EXIT traps) because direnv uses its EXIT trap to capture the environment state after .envrc finishes. With the trap overridden, direnv captures nothing — no PATH, no env vars — making the nix dev shell invisible. Fix: save the existing EXIT trap before setting ours, and chain to it from a wrapper function. Closes #1 --- lib/build.nix | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/build.nix b/lib/build.nix index 3dfc027..5215ae5 100644 --- a/lib/build.nix +++ b/lib/build.nix @@ -351,7 +351,14 @@ ides leave --token "$IDES_LEASE_TOKEN" fi } - trap _ides_leave EXIT + # Preserve any existing EXIT trap (e.g. direnv's + # internal trap) by wrapping it with _ides_leave. + _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 '' else ""; From ed7c098644d0cda268002486cdaec08e10a850e5 Mon Sep 17 00:00:00 2001 From: Martin Becze Date: Sat, 20 Jun 2026 21:54:20 +0200 Subject: [PATCH 2/5] fix: support direnv by walking up to real shell PID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When ides' shellHook runs inside direnv, the bash subprocess that evaluates .envrc exits immediately after evaluation. This causes the EXIT trap to fire, releasing the lease and stopping all services. Fix: detect direnv via DIRENV_DIR/DIRENV_DIFF env vars. When detected: 1. Walk up the process tree (bash → direnv → real shell) to find the user's actual shell PID for the lease, so the lease stays alive. 2. Skip the EXIT-trap lease cleanup — the daemon's prune_dead will release the lease and stop services when the real shell exits. 3. Still chain to any existing EXIT trap (e.g. direnv's env capture). In normal (non-direnv) shells, behavior is unchanged. --- lib/build.nix | 52 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/lib/build.nix b/lib/build.nix index 5215ae5..ac809c6 100644 --- a/lib/build.nix +++ b/lib/build.nix @@ -338,7 +338,29 @@ leaseRun = if monitorEnabled then '' - _IDES_ENTER_OUTPUT="$(ides enter --kind shell --root "$PWD" --pid $$)" + # 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 --kind shell --root "$PWD" --pid "$_IDES_SHELL_PID")" _IDES_ENTER_STATUS=$? if [ "$_IDES_ENTER_STATUS" -ne 0 ]; then unset _IDES_ENTER_OUTPUT @@ -351,14 +373,26 @@ ides leave --token "$IDES_LEASE_TOKEN" fi } - # Preserve any existing EXIT trap (e.g. direnv's - # internal trap) by wrapping it with _ides_leave. - _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 + 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 '' else ""; From 7a68032264de5504ae27ab58f5cd3a396ad4ac42 Mon Sep 17 00:00:00 2001 From: Martin Becze Date: Sun, 21 Jun 2026 10:14:37 +0200 Subject: [PATCH 3/5] fix: retry ides enter and don't fail shell on error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ides enter can fail on first run when the daemon is still starting up (connection reset). Add retry logic (5 attempts, 200ms apart). Also, don't exit the shell if ides enter ultimately fails — just skip lease management. The user can run `ides run` manually. --- lib/build.nix | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/build.nix b/lib/build.nix index ac809c6..1d97e17 100644 --- a/lib/build.nix +++ b/lib/build.nix @@ -360,12 +360,23 @@ _IDES_SHELL_PID="''${_IDES_SHELL_PID:-$_IDES_PARENT_PID}" fi fi - _IDES_ENTER_OUTPUT="$(ides enter --kind shell --root "$PWD" --pid "$_IDES_SHELL_PID")" - _IDES_ENTER_STATUS=$? + _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() { From 689435bf87052233d3d2294c34d5c15894c98bb0 Mon Sep 17 00:00:00 2001 From: Martin Becze Date: Sun, 21 Jun 2026 12:43:24 +0200 Subject: [PATCH 4/5] fix: don't exit daemon on startup when pruning stale leases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The daemon's serve() loop checks if active==0 && pruned>0 to detect when all shells have exited (pruned dead leases → stop services → exit). But on startup, stale lease files from previous runs would trigger this condition immediately, causing the daemon to call systemd::down() and exit before any client could connect. Fix: skip the prune-and-exit check on the first iteration. The daemon will still exit after 10 seconds of idle time if no leases are created. --- lucius/src/daemon.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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}")), From 4825db88540301bf511f779aa1fe357e0708ac3c Mon Sep 17 00:00:00 2001 From: Martin Becze Date: Sun, 21 Jun 2026 12:47:56 +0200 Subject: [PATCH 5/5] fix: close if/fi for ides enter retry logic --- lib/build.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/build.nix b/lib/build.nix index 1d97e17..f8766a8 100644 --- a/lib/build.nix +++ b/lib/build.nix @@ -404,6 +404,7 @@ } trap _ides_leave_wrapper EXIT fi + fi '' else "";