Skip to content

Runaway bash processes: infinite loop in get_workspace_dir when realpath fails #18

@phil

Description

@phil

Summary

Observed bash processes occasionally using >15% CPU indefinitely, with no obvious trigger. Traced to an infinite loop in scripts/helpers.sh:get_workspace_dir that fires from the tmux status bar commands (workspace.sh, status.sh) every status-interval.

Root cause

scripts/helpers.sh:46-63:

current_dir=$(realpath "$current_dir")
while [[ "$current_dir" != "/" ]]; do
    if [[ -d "$current_dir/.devcontainer" ]]; then break; fi
    current_dir=$(dirname "$current_dir")
done

The loop only terminates when current_dir == "/". Two situations make that unreachable:

  1. realpath fails (deleted directory, broken symlink, symlink cycle) → prints nothing → current_dir="".
  2. dirname "" returns ., and dirname "." returns .. Since . != /, the loop spins forever at ~100% CPU.

Verified on macOS:

$ dirname ""
.
$ dirname .
.
$ realpath /nonexistent
(empty, exit 1)

Because #(...) fires on every status-interval (default 15s), a pane whose pane_current_path is no longer valid (deleted worktree, unmounted volume, cleaned-up branch directory) will start an orphaned spinning bash. Subsequent intervals spawn more, so CPU usage stacks over time.

Reproduction

mkdir /tmp/gone && cd /tmp/gone && rmdir ../gone
# wait ~15s for the next status refresh in this pane
ps -eo pid,pcpu,command | awk '$2+0 > 5'

Contributing factors (not the CPU cause, but worth noting)

  • No timeouts on docker, docker compose, or devcontainer read-configuration. A hung Docker daemon stalls status refreshes (idle, not CPU-burning).
  • devcontainer read-configuration (Node.js) runs at least twice per refresh — there is already a memoization TODO at helpers.sh:77.

Suggested fixes

  1. Guard the loop — exit when current_dir is empty, does not start with /, or stops shrinking between iterations. This is the actual CPU bug.
  2. Add a depth cap as belt-and-braces.
  3. Wrap docker / devcontainer calls in a timeout (BSD timeout is not default on macOS — gtimeout from coreutils, or a bash read -t wrapper).
  4. Memoize devcontainer read-configuration per status refresh (the existing TODO).

Investigation tips for anyone else seeing this

# find runaway bash
ps -eo pid,pcpu,etime,command | awk '$2+0 > 5'

# confirm it is plugin-related
ps -o pid,ppid,command -p <PID>

# profile what it is actually doing (macOS)
sample <PID> 3 -f /tmp/sample.txt
# look for repeated frames in dirname / get_workspace_dir

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions