runWatchdog monitors lastOutputTime and kills the process if no output is received within the timeout duration. It stops when watchdogDone is closed.
(timeout time.Duration, done <-chan struct{}, fired *atomic.Bool)
| 400 | // runWatchdog monitors lastOutputTime and kills the process if no output is received |
| 401 | // within the timeout duration. It stops when watchdogDone is closed. |
| 402 | func (l *Loop) runWatchdog(timeout time.Duration, done <-chan struct{}, fired *atomic.Bool) { |
| 403 | // Check interval scales with timeout: 1/5 of timeout, clamped to [10ms, 10s] |
| 404 | checkInterval := timeout / 5 |
| 405 | if checkInterval < 10*time.Millisecond { |
| 406 | checkInterval = 10 * time.Millisecond |
| 407 | } |
| 408 | if checkInterval > 10*time.Second { |
| 409 | checkInterval = 10 * time.Second |
| 410 | } |
| 411 | ticker := time.NewTicker(checkInterval) |
| 412 | defer ticker.Stop() |
| 413 | |
| 414 | for { |
| 415 | select { |
| 416 | case <-ticker.C: |
| 417 | l.mu.Lock() |
| 418 | lastOutput := l.lastOutputTime |
| 419 | stopped := l.stopped |
| 420 | l.mu.Unlock() |
| 421 | |
| 422 | if stopped { |
| 423 | return |
| 424 | } |
| 425 | |
| 426 | if time.Since(lastOutput) > timeout { |
| 427 | fired.Store(true) |
| 428 | |
| 429 | // Emit watchdog timeout event |
| 430 | l.mu.Lock() |
| 431 | iter := l.iteration |
| 432 | l.mu.Unlock() |
| 433 | l.events <- Event{ |
| 434 | Type: EventWatchdogTimeout, |
| 435 | Iteration: iter, |
| 436 | Text: fmt.Sprintf("No output for %s, killing hung process", timeout), |
| 437 | } |
| 438 | |
| 439 | // Kill the process |
| 440 | l.mu.Lock() |
| 441 | if l.agentCmd != nil && l.agentCmd.Process != nil { |
| 442 | l.agentCmd.Process.Kill() |
| 443 | } |
| 444 | l.mu.Unlock() |
| 445 | return |
| 446 | } |
| 447 | case <-done: |
| 448 | return |
| 449 | } |
| 450 | } |
| 451 | } |
| 452 | |
| 453 | // processOutput reads stdout line by line, logs it, and parses events. |
| 454 | func (l *Loop) processOutput(r io.Reader) { |