Skip to content

fix: clean up ttyd process on early return from Evaluate (#738)#751

Open
dhruba-datta wants to merge 1 commit into
charmbracelet:mainfrom
dhruba-datta:fix/ttyd-process-leak-738
Open

fix: clean up ttyd process on early return from Evaluate (#738)#751
dhruba-datta wants to merge 1 commit into
charmbracelet:mainfrom
dhruba-datta:fix/ttyd-process-leak-738

Conversation

@dhruba-datta

Copy link
Copy Markdown

Fixes #738.

When Evaluate() returns early — between Start() and the Record() / teardown() path — the ttyd process was left running indefinitely. The deferred cleanup at evaluator.go:45 only called v.close(), which is set to vhs.browser.Close. The terminate() method that kills both browser and ttyd was only reachable via Record()'s own cleanup path, which requires Record() to have started.

Reproduction (from the issue)

echo 'Set Width 0' > /tmp/bad.tape
echo 'Type "hello"' >> /tmp/bad.tape
vhs /tmp/bad.tape
ps aux | grep ttyd          # ttyd still running

Affected code paths

All early returns after Start() succeeds but before Record() begins:

  • Page.Wait fails (evaluator.go:50-53)
  • A SET command fails (evaluator.go:59-63)
  • Video dimensions too small (evaluator.go:78-91)
  • A Hide block command fails (evaluator.go:106-109)
  • Browser launch or page creation fails inside Start() itself, after ttyd has already started — the original Start() returned the error without killing the spawned ttyd.

Changes

Three small changes across two files:

  1. evaluator.go: change defer v.close() to defer v.terminate() so every early return cleans up ttyd as well as the browser.
  2. vhs.go Start(): kill the spawned ttyd if browser launch or page creation fails. Without this, callers cannot reach terminate() because Start() returns before assigning vhs.started = true.
  3. vhs.go terminate(): make idempotent via a terminated flag on the VHS struct, and tolerate nil browser / tty fields. The happy path now calls terminate() twice (once from Record()'s own cleanup goroutine and once from Evaluate()'s deferred call) — the second call is a no-op.

Test coverage

Added vhs_test.go with two regression tests:

  • TestTerminateIdempotent — guards the double-call path.
  • TestTerminateBeforeStartIsNoOp — guards the path where Evaluate() defers terminate() but Start() never succeeded (parser-only error path).

Full go test ./... passes locally on macOS / arm64 (Go 1.26.3).

Fixes charmbracelet#738.

When Evaluate() returns early — between Start() and the
Record()/teardown() path — the ttyd process was left running
indefinitely. The deferred cleanup at evaluator.go:45 only called
v.close(), which is set to vhs.browser.Close. The terminate() method
that kills both browser AND ttyd was only reachable via Record()'s
own cleanup, which requires Record() to have started.

Affected code paths from the issue:
  - Page.Wait fails (evaluator.go:50-53)
  - A SET command fails (evaluator.go:59-63)
  - Video dimensions too small (evaluator.go:78-91)
  - A Hide block command fails (evaluator.go:106-109)
  - Browser launch or page creation fails inside Start() itself,
    after ttyd has already started

Three changes:

  1. evaluator.go: change defer v.close() to defer v.terminate() so
     every early return kills ttyd as well as the browser.

  2. vhs.go Start(): kill ttyd if browser launch or page creation
     fails. Without this, terminate() is never reached because
     Start() returns before assigning vhs.started = true.

  3. vhs.go terminate(): make idempotent via a 'terminated' guard
     on the VHS struct, and tolerate nil browser/tty fields. The
     happy path now calls terminate() twice (Record()'s own cleanup
     plus Evaluate()'s defer) — the second call is a no-op.

Added vhs_test.go with two regression tests covering both new
invariants (idempotent on already-terminated, no-op before Start).
Full test suite passes.
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.

ttyd process leaked on early exit from Evaluate

1 participant