Skip to content
Open
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
41 changes: 41 additions & 0 deletions packages/happy-app/sources/utils/worktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,43 @@ export async function listWorktrees(
return worktrees;
}

/**
* Kill any process whose working directory is inside the worktree.
*
* `git worktree remove --force` deletes the directory even while a process is
* still cwd'd into it — most often a happy session that got orphaned when its
* daemon restarted. That orphan then lives on inside a now-deleted directory,
* holding its server socket open and re-reporting the session as `active`
* forever, so the session can never be archived from the app. We therefore
* stop those processes *before* removing the directory.
*
* Portable across the machine's OS: Linux reads /proc/<pid>/cwd, macOS falls
* back to lsof (resolved by absolute path so a stripped PATH still finds it).
* The worktree path is canonicalised (pwd -P) so a symlinked parent still
* matches, and the match is anchored to the worktree dir or a subdir of it —
* never a sibling that merely shares a name prefix. SIGTERM lets the happy CLI
* shut its session down cleanly. Entirely best-effort: a failure here must
* never block the worktree removal itself.
*/
async function killProcessesInWorktree(
machineId: string,
worktreePath: string,
cwd: string
): Promise<void> {
const script = [
`WT='${worktreePath}'`,
`WT=$(cd "$WT" 2>/dev/null && pwd -P || printf %s "$WT")`,
`if [ -d /proc ]; then`,
` for d in /proc/[0-9]*; do c=$(readlink "$d/cwd" 2>/dev/null) || continue; case "$c" in "$WT"|"$WT"/*) kill "$(basename "$d")" 2>/dev/null || true;; esac; done`,
`else`,
` L=$(command -v lsof || echo /usr/sbin/lsof)`,
` "$L" -d cwd -Fpn 2>/dev/null | awk -v wt="$WT" '/^p/{pid=substr($0,2)} /^n/{p=substr($0,2); if(p==wt||index(p,wt"/")==1)print pid}' | while read -r pid; do kill "$pid" 2>/dev/null || true; done`,
`fi`,
`true`,
].join('\n');
await machineBash(machineId, script, cwd).catch(() => {});
}

export async function removeWorktree(
machineId: string,
worktreePath: string
Expand All @@ -161,6 +198,10 @@ export async function removeWorktree(
}
const basePath = worktreePath.slice(0, idx);

// Stop processes still running inside the worktree before --force deletes
// it out from under them (see killProcessesInWorktree).
await killProcessesInWorktree(machineId, worktreePath, basePath);

const result = await machineBash(
machineId,
`git worktree remove ${worktreePath} --force`,
Expand Down
Loading