pestphp / pestphp/pest

[Bug]: browser plugin — Playwright server survives the parent on SIGTERM (the signal CI uses to cancel)

Open
#1,825 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
PHP
Stars
11.7k
Forks
538
Avg merge
4d 11h
Merged PRs (30d)
8

Description

Filed here because pestphp/pest-plugin-browser has issues disabled.
Pest: v5.0.3 · Plugin: pest-plugin-browser v5.0.0 · PHP: 8.5.8 · OS: macOS

Summary

The Playwright node server survives its parent process on SIGTERM, which is the signal CI jobs and container runtimes use to cancel a run. Each cancelled run leaves a playwright run-server process behind holding its port and its memory; a machine that cancels runs regularly accumulates them.

SIGINT is fine — that path already cleans up. It is specifically SIGTERM that leaks, and that is the one a cancelled CI job sends.

Measurement

ServerManager::instance()->playwright() + start(), then the parent is signalled. Counting playwright run-server processes before and after (v5.0.0, macOS, PHP 8.5.8):

signal parent alive after server before server after
SIGINT no 1 0
SIGTERM no 1 1
SIGKILL no 1 1 (expected — uncatchable)
Why

The only path that stops the server is Plugin::terminate()PlaywrightNpmServer::stop(), plus Symfony's Process destructor. PHP CLI runs the shutdown sequence for SIGINT, so both fire. For SIGTERM it terminates without running shutdown functions or destructors, so neither fires and the child is orphaned.

There is no pcntl_signal() handler anywhere in src/ — I grepped for SIGTERM / SIGINT / SIGHUP and the plugin registers none. So this is not a race or a timing window; the signal simply has no handler.

Suggested direction

Register a SIGTERM (and SIGHUP) handler when pcntl is available, running the same teardown terminate() already does, and re-raise the default. pcntl is not always present, so the handler needs to be conditional — but where it is absent the behavior is exactly what happens today, so this is strictly an improvement.

I did not open a PR for this one because the right place to hook it (the plugin's terminate(), the server class itself, or a small signal facade) is a structural call I would rather leave to you. Happy to write it if you name the shape you want.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the SIGINT, SIGTERM, and SIGKILL behavior described for ServerManager::instance()->playwright()->start(), then inspect Plugin::terminate() and PlaywrightNpmServer::stop() in src/. Check the existing process teardown path and the absence of signal handling. Done means SIGTERM and SIGHUP clean up the server when pcntl is available, while SIGKILL remains uncatchable and behavior without pcntl is unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.