fix: stop workspace tracker when work directory is deleted (#472)
This commit is contained in:
@@ -39,6 +39,12 @@ func (wt *WorkspaceTracker) pollGitChanges(ctx context.Context) {
|
||||
case <-wt.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
// Stop polling if the work directory was deleted (worktree removed)
|
||||
if !wt.workDirExists() {
|
||||
wt.logger.Warn("work directory no longer exists, stopping git polling",
|
||||
zap.String("workDir", wt.workDir))
|
||||
return
|
||||
}
|
||||
wt.checkGitChanges(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,16 @@ func (wt *WorkspaceTracker) monitorLoop(ctx context.Context) {
|
||||
// Quick state check using mtime + diff-files
|
||||
currentState, err := wt.getWorkspaceState(ctx)
|
||||
if err != nil {
|
||||
// Git command failed - skip this cycle and retry on next tick.
|
||||
// Check if the work directory has been deleted (e.g., worktree removed
|
||||
// after task was archived or PR branch was deleted). If so, stop the
|
||||
// tracker to avoid spamming warnings every poll cycle.
|
||||
if !wt.workDirExists() {
|
||||
wt.logger.Warn("work directory no longer exists, stopping workspace tracker",
|
||||
zap.String("workDir", wt.workDir))
|
||||
return
|
||||
}
|
||||
|
||||
// Git command failed for another reason - skip this cycle and retry on next tick.
|
||||
// Don't update lastState so the change will be detected when git recovers.
|
||||
wt.logger.Warn("failed to get workspace state, will retry next cycle",
|
||||
zap.Error(err))
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWorkspaceTracker_StopsWhenWorkDirDeleted(t *testing.T) {
|
||||
isolateTestGitEnv(t)
|
||||
|
||||
repoDir, cleanup := setupTestRepo(t)
|
||||
defer cleanup()
|
||||
|
||||
log := newTestLogger(t)
|
||||
wt := NewWorkspaceTracker(repoDir, log)
|
||||
wt.gitPollInterval = 100 * time.Millisecond
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
wt.Start(ctx)
|
||||
|
||||
// Delete the work directory to simulate worktree removal
|
||||
if err := os.RemoveAll(repoDir); err != nil {
|
||||
t.Fatalf("failed to remove workdir: %v", err)
|
||||
}
|
||||
|
||||
// Both monitorLoop and pollGitChanges should exit within a few poll cycles
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wt.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Both goroutines exited — success
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("workspace tracker goroutines did not stop after workdir was deleted")
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,15 @@ func resolveGitIndexPath(workDir string) string {
|
||||
return indexPath
|
||||
}
|
||||
|
||||
// workDirExists checks whether the workspace directory still exists on disk.
|
||||
// Used to detect deleted worktrees and stop polling loops gracefully.
|
||||
// The workDir is validated at construction time via resolveExistingWorkDir,
|
||||
// so this only checks for subsequent deletion (e.g., worktree cleanup).
|
||||
func (wt *WorkspaceTracker) workDirExists() bool {
|
||||
_, err := os.Stat(wt.workDir) //nolint:gosec // workDir is validated at construction via resolveExistingWorkDir
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
// Start begins monitoring the workspace using polling (no fsnotify).
|
||||
// File changes are detected via git status polling, which is efficient and
|
||||
// doesn't consume file descriptors like fsnotify/kqueue does on macOS.
|
||||
|
||||
Reference in New Issue
Block a user