Skip to content

fix(daemon): clean up child after startup timeout#774

Open
PierrunoYT wants to merge 6 commits into
Gitlawb:mainfrom
PierrunoYT:fix/issue-770-daemon-startup-cleanup
Open

fix(daemon): clean up child after startup timeout#774
PierrunoYT wants to merge 6 commits into
Gitlawb:mainfrom
PierrunoYT:fix/issue-770-daemon-startup-cleanup

Conversation

@PierrunoYT

@PierrunoYT PierrunoYT commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • retain ownership of the detached daemon child until readiness succeeds
  • perform a final readiness probe at the timeout boundary
  • terminate and reap the exact child when startup times out
  • cover timeout cleanup, successful release, and boundary readiness with subprocess tests

Validation

  • go test ./internal/cli -count=1
  • go vet ./internal/cli
  • git diff --check

Closes #770

Summary by CodeRabbit

  • Bug Fixes

    • Improved detached daemon startup reliability by detecting readiness, handling timeouts, and cleaning up failed processes.
    • Prevented lingering or zombie processes when terminating commands and process trees.
    • Improved error reporting for daemon startup failures, including relevant log locations.
  • Tests

    • Added coverage for daemon readiness, timeout handling, process cleanup, and cross-platform termination behavior.

Copilot AI review requested due to automatic review settings July 20, 2026 13:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a startup-timeout failure mode in zero daemon start where the CLI could detach from the spawned daemon before readiness is confirmed, allowing a timed-out start attempt to leave a live background daemon behind.

Changes:

  • Refactors detached daemon startup to retain ownership of the child process until readiness succeeds, including a final readiness probe at the timeout boundary.
  • Adds timeout cleanup logic to terminate and reap the launched child on readiness timeout.
  • Adds subprocess-based tests covering timeout cleanup, successful release, and the timeout-boundary readiness probe.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
internal/cli/daemon.go Moves detached start into a start+await helper, adds readiness polling with a boundary check, and adds termination/reap cleanup on timeout.
internal/cli/daemon_test.go Adds subprocess helpers and tests to validate timeout cleanup, readiness-boundary behavior, and successful release behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/cli/daemon.go Outdated
Comment on lines +212 to +216
func terminateAndReapDaemonProcess(cmd *exec.Cmd) error {
killErr := cmd.Process.Kill()
waitErr := cmd.Wait()
var exitErr *exec.ExitError
if waitErr == nil || errors.As(waitErr, &exitErr) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 27c9bd3. Timeout cleanup now uses a coordinated background.TerminateCommand path instead of leader-only Process.Kill:

  • POSIX captures and signals the process group before Wait can reap the leader, then reaps concurrently while preserving escalation/liveness checks.
  • Windows uses taskkill /T, always reaps the leader, and preserves tree-termination failures if fallback leader termination is needed.
  • Added POSIX coverage for an exited leader with a surviving child and Windows coverage for the exited-leader reap race.
  • Daemon subprocess tests now configure the same child-process-group invariant as production.

Validated on Windows and WSL/Linux, plus go vet ./internal/background ./internal/cli.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The detached daemon startup path now retains process ownership until readiness succeeds, terminates and reaps timed-out children, and performs zombie-aware process-group liveness checks. Tests cover timeout boundaries, cleanup, release behavior, POSIX process trees, and Windows reaping.

Changes

Detached daemon process lifecycle

Layer / File(s) Summary
Cross-platform command termination
internal/execution/..., internal/background/...
Adds bounded command termination and reaping, zombie-aware Unix process-group detection, and POSIX/Windows lifecycle tests.
Detached startup orchestration
internal/cli/daemon.go
Moves readiness polling into helpers, performs a final timeout-boundary check, releases ready processes, and terminates/reaps failed starts.
Process lifecycle validation
internal/cli/daemon_test.go
Tests readiness boundaries, timeout termination and reaping, successful ready-child release, and detached test helpers.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant DaemonProcess
  participant TerminateCommand
  CLI->>DaemonProcess: start and retain process handle
  CLI->>DaemonProcess: poll readiness
  DaemonProcess-->>CLI: ready or timeout
  CLI->>DaemonProcess: release after readiness
  CLI->>TerminateCommand: terminate and reap after timeout
Loading

Possibly related PRs

Suggested reviewers: vasanthdev2004, kevincodex1, gnanam1990

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes retain the child until readiness, re-check at timeout, terminate/reap on failure, and release only after success; tests cover the key cases.
Out of Scope Changes check ✅ Passed The extra process-management and test updates support the same cleanup/reaping behavior and do not introduce unrelated functional scope.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: cleaning up the detached daemon child when startup times out.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/cli/daemon_test.go (1)

144-160: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Good coverage of timeout-kill-reap, timeout-termination, and successful release paths.

Solid regression coverage for the behavior described in the PR (deadline-boundary readiness, termination+reaping of a hanging child, release of a ready child). One minor observation: all three tests duplicate the same exec.Command(os.Args[0], "-test.run=TestDaemonDetachedChildProcess") + env setup; consider extracting a small newHangingDaemonHelper(t *testing.T) *exec.Cmd to cut the repetition, but this is optional.

Also applies to: 162-171, 173-192

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/daemon_test.go` around lines 144 - 160, Optionally reduce
duplication across the daemon process tests, including
TestTerminateAndReapDaemonProcess and the related tests, by extracting the
shared exec.Command and ZERO_TEST_DAEMON_DETACHED_CHILD environment setup into a
helper such as newHangingDaemonHelper(t *testing.T). Use that helper wherever
the detached child process is started, preserving each test’s existing
assertions and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/cli/daemon_test.go`:
- Around line 144-160: Optionally reduce duplication across the daemon process
tests, including TestTerminateAndReapDaemonProcess and the related tests, by
extracting the shared exec.Command and ZERO_TEST_DAEMON_DETACHED_CHILD
environment setup into a helper such as newHangingDaemonHelper(t *testing.T).
Use that helper wherever the detached child process is started, preserving each
test’s existing assertions and behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3f62b5b4-f2b4-4e99-a7c5-91b81df6b580

📥 Commits

Reviewing files that changed from the base of the PR and between da9fb50 and 2f4d837.

📒 Files selected for processing (2)
  • internal/cli/daemon.go
  • internal/cli/daemon_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 20, 2026
@PierrunoYT

Copy link
Copy Markdown
Contributor Author

Fixed the macOS smoke failure in f73194f. The process-group cleanup succeeded, but macOS can retain an orphaned child briefly as a zombie; kill(pid, 0) still succeeds for zombies, so the regression test incorrectly classified the dead child as running. The POSIX assertion now accepts either ESRCH or Z process state. Revalidated internal/background on WSL/Linux and internal/background + internal/cli on Windows.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
internal/background/process_posix.go (1)

173-180: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared cmd.Wait() error classification instead of duplicating it 3x.

The same var exitErr *exec.ExitError; if waitErr != nil && !errors.As(waitErr, &exitErr) { return fmt.Errorf("reap process: %w", waitErr) } block is duplicated verbatim across both platform files. As per coding guidelines, "Prefer one cross-platform function with small conditional checks over duplicated platform-specific helpers when behavior can remain unified" — this logic is platform-independent and belongs in one shared helper (e.g. in terminate.go).

  • internal/background/process_posix.go#L173-L180: keep as the canonical helper, or replace its body with a call to a new shared classifyWaitError(err error) error.
  • internal/background/process_posix.go#L163-L166: replace inline block with a call to the shared helper.
  • internal/background/process_windows.go#L37-L41: replace inline block with a call to the same shared helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/background/process_posix.go` around lines 173 - 180, Extract the
platform-independent wait-error classification into one shared helper, such as
classifyWaitError, in the shared background package. In
internal/background/process_posix.go lines 173-180, retain the existing behavior
in that helper or delegate waitForTerminatedCommand to it; replace the inline
classification at lines 163-166 and in internal/background/process_windows.go
lines 37-41 with calls to the shared helper, preserving the existing handling of
*exec.ExitError and wrapped reap-process errors.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/background/process_posix.go`:
- Around line 107-122: Update terminateCommand so the non-process-gone error
branch after syscall.Getpgid also calls waitForTerminatedCommand before
returning, ensuring the child is reaped on every error path while preserving the
existing error handling for processGoneError.

In `@internal/background/process_windows.go`:
- Around line 31-52: Update terminateCommand to reap cmd with a bounded wait,
mirroring the timeout-based approach used by the POSIX terminateCommand: perform
cmd.Wait() asynchronously and select between its result and the existing
termination timeout. Return a timeout error when the process cannot be reaped in
time, while preserving the current taskkill, fallback-kill, and wait-error
handling for completed waits.

---

Nitpick comments:
In `@internal/background/process_posix.go`:
- Around line 173-180: Extract the platform-independent wait-error
classification into one shared helper, such as classifyWaitError, in the shared
background package. In internal/background/process_posix.go lines 173-180,
retain the existing behavior in that helper or delegate waitForTerminatedCommand
to it; replace the inline classification at lines 163-166 and in
internal/background/process_windows.go lines 37-41 with calls to the shared
helper, preserving the existing handling of *exec.ExitError and wrapped
reap-process errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7291e022-8dba-44b4-b1a4-45dce9dd5ef2

📥 Commits

Reviewing files that changed from the base of the PR and between 2f4d837 and 27c9bd3.

📒 Files selected for processing (7)
  • internal/background/process_posix.go
  • internal/background/process_posix_test.go
  • internal/background/process_windows.go
  • internal/background/process_windows_test.go
  • internal/background/terminate.go
  • internal/cli/daemon.go
  • internal/cli/daemon_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/cli/daemon.go
  • internal/cli/daemon_test.go

Comment thread internal/background/process_posix.go Outdated
Comment thread internal/background/process_windows.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/background/process_posix_test.go (1)

153-162: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Lift the exit condition into the for loop.

The loops use an unidiomatic for { if condition { break } } pattern. As flagged by static analysis, lifting the check into the loop condition improves readability and clarifies the intent.

  • internal/background/process_posix_test.go#L153-L162: Lift !processStopped(childPID) into the loop condition and remove the internal break.
  • internal/background/process_posix_test.go#L196-L205: Lift !processStopped(childPID) into the loop condition and remove the internal break.
♻️ Proposed refactor
-	for {
-		if processStopped(childPID) {
-			break
-		}
+	for !processStopped(childPID) {
		if time.Now().After(deadline) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/background/process_posix_test.go` around lines 153 - 162, Update
both polling loops in internal/background/process_posix_test.go at lines 153-162
and 196-205 to use !processStopped(childPID) as the for-loop condition, remove
the internal break, and preserve the existing deadline timeout, kill, failure,
and sleep behavior.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/background/process_posix_test.go`:
- Around line 153-162: Update both polling loops in
internal/background/process_posix_test.go at lines 153-162 and 196-205 to use
!processStopped(childPID) as the for-loop condition, remove the internal break,
and preserve the existing deadline timeout, kill, failure, and sleep behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 66bb37f6-1461-4153-901a-27df32e5a8e3

📥 Commits

Reviewing files that changed from the base of the PR and between 27c9bd3 and f73194f.

📒 Files selected for processing (1)
  • internal/background/process_posix_test.go

@PierrunoYT

Copy link
Copy Markdown
Contributor Author

Fixed the remaining macOS CI race in 4cb866e. On Darwin, Getpgid can return ESRCH after the configured process-group leader exits even though descendants still remain in that group. TerminateCommand now uses the launch-time Setpgid/Pgid configuration (which guarantees PGID == leader PID) rather than trying to rediscover it after exit, so the descendants are still signalled. Validated internal/background 10x on Linux, internal/background + internal/cli and vet on Windows, and Darwin cross-compilation.

@PierrunoYT

Copy link
Copy Markdown
Contributor Author

Addressed the remaining CodeRabbit feedback in 3ed4571: shared wait-error classification, bounded Windows reaping, fallback termination plus bounded reaping on non-ESRCH Getpgid failures, and the two staticcheck loop cleanups. Validated background/CLI tests and vet on Windows, background tests 10x plus vet on Linux, and Darwin cross-compilation.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 20, 2026

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Resolve the current merge conflict before this can be merged
    GitHub currently reports this PR as mergeable: false with merge state dirty. Please rebase or otherwise resolve the conflict against the current base, then rerun the affected checks so the reviewed diff is mergeable.

  • [P2] Do not count zombie descendants as a surviving process group
    internal/background/process_posix.go:158
    alive uses kill(-pgid, 0) == nil, but that call succeeds for zombie processes. The newly added test explicitly treats a Z process state as stopped because an orphaned child can remain a zombie temporarily. When a timed-out daemon's group contains such a child, this helper waits through both grace periods, sends SIGKILL to an already-dead group, and finally returns process group <pid> did not exit after SIGKILL. That makes zero daemon start report a cleanup failure even though the tree has stopped (and can make the new macOS scenario flaky). Use liveness semantics that distinguish zombies from running descendants, or otherwise accept the already-dead state before declaring cleanup failed.

…n-startup-cleanup

main's Gitlawb#781 unified process lifecycle into internal/execution, reducing
internal/background's platform files to wrappers over
execution.TerminateProcessTree. This branch had grown its own POSIX/Windows
killers there, so the conflict resolves to main's wrappers and the branch's
substance moves onto the unified path:

- background.TerminateCommand keeps the capability the daemon needs (stop the
  tree AND reap the leader, which TerminateProcess cannot do for a caller that
  still owns the exec.Cmd) but is now cross-platform: it calls terminateProcess
  and then bounds cmd.Wait, instead of duplicating a second platform-specific
  killer. That also settles the earlier review note about duplicating the
  Wait-error classification per platform.
- The zombie-liveness fix moves to internal/execution/process_unix.go, which is
  where the flawed check now lives.

Zombie fix: kill(2) with signal 0 succeeds for a process waiting to be reaped, so
a group whose only remaining member was a zombie sat through both grace periods,
took a pointless SIGKILL, and was then reported as "did not exit after SIGKILL" —
a spurious cleanup failure whose timing depended on when the reaper ran, which is
exactly the macOS flake risk. signalTargetRunning now consults process states
(via ps, which both Linux and Darwin provide, and only after the cheap kill check
says something is still there) and treats an all-zombie group as exited. If ps is
unavailable the previous conservative answer stands.

Tests: process_unix_test.go covers the zombie group (it must be seen as exited,
and TerminateProcessTree must succeed rather than report the SIGKILL error) plus a
live-group positive control so the tolerance cannot degrade into ignoring real
processes.
@PierrunoYT

Copy link
Copy Markdown
Contributor Author

Merged current main (315cd63) — the PR is MERGEABLE again — and fixed the zombie-liveness finding at its new home.

[P1] Resolve the merge conflict — done, and it was not a mechanical resolution: main's #781 unified process lifecycle into internal/execution, so internal/background's platform files are now thin wrappers over execution.TerminateProcessTree. This branch had grown its own POSIX and Windows killers there, which #781 supersedes. The conflict therefore resolves to main's wrappers, and the branch's substance moves onto the unified path:

  • background.TerminateCommand keeps the capability the daemon actually needs — stop the tree and reap the leader, which TerminateProcess cannot do for a caller that still owns the exec.Cmd — but it is now cross-platform: signal via terminateProcess, then a bounded cmd.Wait. Signalling still happens before Wait, since Wait releases the leader PID that the group lookup needs. As a bonus this settles CodeRabbit's earlier note about the per-platform duplication of the Wait-error classification: there is now one path, not two.
  • internal/cli/daemon.go keeps the start → poll-readiness → terminate-and-reap flow, and its cleanup goes through background.TerminateCommand, which also closes @copilot's thread about cmd.Process.Kill() bypassing the background termination invariant.

[P2] Do not count zombie descendants as a surviving process group — fixed, in internal/execution/process_unix.go, which is where the kill(target, 0) check lives after #781. signalTargetRunning now distinguishes a process that is running from one merely waiting to be reaped:

  • The cheap kill(2) probe still gates everything, so the common path (group gone → ESRCH) costs nothing extra.
  • Only when that says "something is still there" does it consult process states via ps -A -o pid=,pgid=,stat= — available on both Linux and Darwin, so no /proc dependency — and an all-zombie group counts as exited.
  • If ps cannot run or its output is unparseable, the previous conservative answer stands rather than silently declaring success.

That removes exactly the failure you described: a timed-out daemon whose group held a zombie no longer waits through both grace periods, SIGKILLs a dead group, and reports process group <pid> did not exit after SIGKILL for a tree that had stopped — including the macOS timing flake.

Testsinternal/execution/process_unix_test.go:

  • TestTerminateProcessTreeTreatsZombieGroupAsExited starts a child in its own group, lets it exit without reaping it (the exact state a terminated tree passes through), waits until ps reports Z, then requires signalTargetRunning to report not-running and TerminateProcessTree to return nil. Under the old check this is precisely the "did not exit after SIGKILL" error.
  • TestTerminateProcessTreeStopsRunningGroup is the positive control, so the zombie tolerance cannot degrade into ignoring real processes.

Validation (Windows host, Go 1.26.5): go build ./..., go vet ./..., go test ./... -count=1 all pass, including internal/background, internal/cli, and internal/execution; GOOS=linux|darwin builds plus vets of the touched packages are clean; gofmt clean. The lint baseline (unused,ineffassign,staticcheck) reports 0 issues for internal/execution/... and internal/background/....

Being explicit about one limit: the new POSIX test is compile-verified here but cannot execute on this Windows host, so the ubuntu/macOS CI jobs are what will run the zombie scenario — the case whose timing you flagged as flaky. If it misbehaves there I will follow up rather than leave it.

@jatmn — ready for another look, especially the decision to move the fix into internal/execution rather than keeping a parallel implementation in internal/background. I can't use the reviewer-request button on this repo, hence the mention.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

@PierrunoYT I’ll review the updated merge resolution and unified process-lifecycle path, with particular attention to termination/reaping correctness, zombie handling, CLI timeout behavior, and POSIX/Windows coverage.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@PierrunoYT
PierrunoYT requested a review from jatmn July 25, 2026 14:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/execution/process_unix.go (1)

147-162: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Non-ESRCH Getpgid errors are silently discarded.

When syscall.Getpgid(pid) fails with something other than ESRCH, the function falls through and returns (pid, nil) without surfacing the error, unlike the explicitly-documented ESRCH branch. Worth at least a comment noting this is an intentional conservative fallback (signal the individual pid), so future readers don't mistake it for an oversight.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/execution/process_unix.go` around lines 147 - 162, Update
processSignalTarget to document the intentional conservative fallback when
syscall.Getpgid returns a non-ESRCH error: retain the individual pid target and
return successfully, while preserving the existing ESRCH handling and
process-group behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/execution/process_unix.go`:
- Around line 104-117: Update signalTargetRunning to distinguish process groups
from individual processes: call processGroupHasRunningMember only for negative
target values, and for positive targets perform a per-process zombie check using
processIsZombie or the equivalent production helper. Preserve the existing
syscall liveness check and ensure positive raw PIDs never use their numeric
value as a process-group ID.

---

Nitpick comments:
In `@internal/execution/process_unix.go`:
- Around line 147-162: Update processSignalTarget to document the intentional
conservative fallback when syscall.Getpgid returns a non-ESRCH error: retain the
individual pid target and return successfully, while preserving the existing
ESRCH handling and process-group behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5448f12d-28c9-4663-b99f-a96fd1f3ee01

📥 Commits

Reviewing files that changed from the base of the PR and between 4cb866e and 315cd63.

📒 Files selected for processing (4)
  • internal/background/process_posix_test.go
  • internal/background/terminate.go
  • internal/execution/process_unix.go
  • internal/execution/process_unix_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/background/terminate.go
  • internal/background/process_posix_test.go

Comment on lines +104 to +117
func signalTargetRunning(target int) bool {
if syscall.Kill(target, syscall.Signal(0)) != nil {
return false
}
pgid := target
if pgid < 0 {
pgid = -pgid
}
running, ok := processGroupHasRunningMember(pgid)
if !ok {
return true
}
return running
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Positive target is treated as a pgid even when it isn't one.

processSignalTarget returns a positive target (the raw pid, not a group id) whenever Getpgid reports the process isn't its own group leader, or when it fails with a non-ESRCH error (that error is silently dropped too). signalTargetRunning then does pgid := target unconditionally and searches ps for members whose actual pgid equals this value — but for a non-leader pid, that's not its real group id, just its own pid number. The ok=false fallback usually masks this, but if some unrelated process group's real pgid happens to numerically match this pid, the zombie-check on that unrelated group's state gets applied to this target's liveness determination, which could make TerminateProcessTree conclude the target is gone (and skip SIGKILL) while it's actually still running.

Consider branching on the sign of target: use processGroupHasRunningMember only when target < 0 (a real group), and use a per-pid zombie check (like processIsZombie in the test file) when target > 0.

Sketch of the fix
 func signalTargetRunning(target int) bool {
 	if syscall.Kill(target, syscall.Signal(0)) != nil {
 		return false
 	}
-	pgid := target
-	if pgid < 0 {
-		pgid = -pgid
-	}
-	running, ok := processGroupHasRunningMember(pgid)
-	if !ok {
-		return true
-	}
-	return running
+	if target < 0 {
+		running, ok := processGroupHasRunningMember(-target)
+		if !ok {
+			return true
+		}
+		return running
+	}
+	zombie, ok := processIsZombie(target)
+	if !ok {
+		return true
+	}
+	return !zombie
 }

Also applies to: 147-162

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/execution/process_unix.go` around lines 104 - 117, Update
signalTargetRunning to distinguish process groups from individual processes:
call processGroupHasRunningMember only for negative target values, and for
positive targets perform a per-process zombie check using processIsZombie or the
equivalent production helper. Preserve the existing syscall liveness check and
ensure positive raw PIDs never use their numeric value as a process-group ID.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(daemon): startup timeout can abandon detached child

3 participants