Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

playwright-launch-guard

Enforce safe Chromium launch options across every Playwright / Puppeteer script in a project — without editing a single call site.

プロジェクト内のすべての Playwright / Puppeteer 起動に、安全な起動オプションと同時起動数の上限を強制する。呼び出し側のコードは1行も書き換えない。

English日本語


English

The problem

A browser-automation project grows to 50–100 scripts. Somewhere in a wiki, or in a shell helper function, is the rule: "always pass --disable-gpu and friends."

Nobody does. We audited ours and found the flags in 1 script out of 100+ — the "mandatory" list lived in a PowerShell function, which a Node script can never call, so the mitigation had never been applied anywhere. On top of that, nothing capped how many browsers could exist at once, so headless Chromium instances accumulated across a working day until the desktop stopped responding to input and needed a reboot.

Telling people to remember a flag does not work. Enforce it in the runtime instead.

What it does

Preloaded with --require, it hooks Module._load and wraps chromium.launch, launchPersistentContext and the Puppeteer-shaped launch, so every launch in every script gets:

Safe args injected --disable-gpu, --disable-gpu-compositing, --disable-software-rasterizer, --disable-dev-shm-usage, --renderer-process-limit=4, and others. Args you already set explicitly are respected.
Concurrency cap A PID-based lockfile pool in the temp dir. New launches queue instead of piling up. Stale locks from dead processes are reclaimed automatically.
Headless by default headless: false is rewritten unless PWGUARD_ALLOW_HEADED=1. On a machine a human also uses, a window appearing steals the foreground.
Off-screen placement stripped --window-position=-10000,-10000 makes document.visibilityState 'hidden', after which SPAs stop handling clicks and stop running timers. It is a trap; the guard removes it.
Guaranteed cleanup Slots released on exit, SIGINT, SIGTERM, uncaughtException, and on browser close.

Install

# One-off
NODE_OPTIONS="--require /abs/path/to/pw-guard.cjs" node your-script.js

# Persistent (Windows, user-level)
setx NODE_OPTIONS "--require C:/abs/path/to/pw-guard.cjs"

⚠️ On Windows, write the path with forward slashes. With backslashes, Node reads \U, \t etc. as escapes and fails with Cannot find module 'C:Usersdir...' — which breaks every node and npm invocation on the machine until you fix it. We shipped that bug and had to unpick it live.

When NODE_OPTIONS is not enough

Two real failure modes:

  1. Shells that were already open when you set the variable never see it. Long-lived terminals and background workers silently run unguarded.
  2. Absolute-path requiresrequire('C:/work/my-project/node_modules/playwright') — which hooks matching only the bare specifier will miss. (This guard handles that case, but package-level patching removes the question entirely.)

So there is a second, environment-independent route: append a loader line to each package's own index.js.

node apply-pkg-patch.mjs --root /abs/path/to/project          # apply (keeps .orig-backup)
node apply-pkg-patch.mjs --root /abs/path/to/project --check  # report only
node apply-pkg-patch.mjs --root /abs/path/to/project --revert # restore

Re-run after any npm install — installing overwrites the patched file.

Verify

NODE_OPTIONS="--require /abs/path/to/pw-guard.cjs" node selftest.cjs

It reads chrome://version and prints whether each flag actually reached the browser process. Check, don't assume — assuming is exactly how the 1-in-100 situation happened.

Options

Env var Default Meaning
PWGUARD_DISABLE 1 disables the guard entirely
PWGUARD_MAX 3 Max concurrent automation browsers
PWGUARD_WAIT_MS 120000 Queue timeout; after this it proceeds anyway rather than deadlocking
PWGUARD_KEEP 1 = this browser is parked for a human; exempt from the cap and stamped with --pwguard-keep so an external reaper can spare it
PWGUARD_ALLOW_HEADED 1 permits headless: false
PWGUARD_SCOPE Substring of cwd/argv[1]; the guard only engages inside it. Use this when NODE_OPTIONS is set machine-wide so it does not load inside npm, editors, or unrelated projects.
PWGUARD_LOG %TEMP%/pw_guard.log Log path

A note on the concurrency cap

We first set it to 1, on the theory that accumulated Chromium instances were exhausting GDI/USER handles. Measurement did not support that: with a parked browser open, GDI was 1,180 and USER 2,231, against a warning threshold of 8,000. Serialising every browser task just made the pipeline slow. 3 is bounded without being the bottleneck — tune it, and measure before you believe a theory about your own freezes.

Requirements: Node 14+. No dependencies. Works with Playwright and Puppeteer.


日本語

何を解決するか

ブラウザ自動化のスクリプトが50〜100本に増えると、「起動時には必ず --disable-gpu などを付けること」というルールは 誰も守らなくなります

実際に監査したところ、100本以上あるスクリプトのうち、フラグが付いていたのは 1本だけ でした。「必須」とされていた引数リストは PowerShell の関数の中にあり、Node のスクリプトからは原理的に呼べません。つまり対策はどこにも適用されていませんでした。加えて同時起動数の上限が無かったため、headless の Chromium が一日かけて積み上がり、最終的にデスクトップが入力を受け付けなくなって再起動が必要になりました。

「気をつけて付けてね」では直りません。実行時に強制するのが本ツールです。

動作

--require でプリロードすると Module._load をフックし、chromium.launch / launchPersistentContext / Puppeteer 形式の launch をラップします。結果、全スクリプトの全起動に対して次が効きます。

  • 安全な引数を自動注入--disable-gpu ほか。呼び出し側が明示指定した引数は尊重)
  • 同時起動数の上限(tmp のロックファイル方式。超過分は待ち行列に入る。死んだプロセスのロックは自動回収)
  • headless を既定に強制PWGUARD_ALLOW_HEADED=1 のときのみ画面付きを許可)
  • 画面外配置 --window-position の除去visibilityStatehidden になり SPA がクリックを処理しなくなるため)
  • 終了時のロック解放を保証

導入

NODE_OPTIONS="--require /abs/path/to/pw-guard.cjs" node your-script.js

⚠️ Windows ではパスをスラッシュ / で書いてください。 バックスラッシュだと Node がエスケープとして解釈し、Cannot find module 'C:Usersdir...' となってそのマシン上の node と npm がすべて壊れます(実際に踏みました)。

NODE_OPTIONS だけでは足りない場合

変数を設定する前から起動していたシェルには届きません。常駐ターミナルやバックグラウンドのワーカーは、気付かないままガード無しで走ります。 ② require('C:/work/my-project/node_modules/playwright') のような絶対パス require は、ベア名だけを見るフックでは取りこぼします。

そこで環境変数に依存しない経路として、各パッケージの index.js に読み込み行を追記する方法を用意しています。

node apply-pkg-patch.mjs --root /abs/path/to/project          # 適用(.orig-backup を残す)
node apply-pkg-patch.mjs --root /abs/path/to/project --check  # 確認のみ
node apply-pkg-patch.mjs --root /abs/path/to/project --revert # 復旧

npm install すると追記は消えるので、その都度やり直してください。

検証

NODE_OPTIONS="--require /abs/path/to/pw-guard.cjs" node selftest.cjs

chrome://version を読み、各フラグが実際にブラウザプロセスへ届いたかを出力します。「付いているはず」で済ませないこと。それが 100本中1本という事態の原因でした。

同時起動上限について

当初は「Chromium の蓄積がハンドルを枯渇させている」という仮説から 1 にしていました。しかし実測では、常駐ブラウザ1台がある状態で GDI 1,180 / USER 2,231(警告閾値 8,000)と、仮説を支持しませんでした。全ブラウザ処理を直列化した結果、パイプライン全体が遅くなっただけでした。3 は上限として機能しつつ律速にならない値です。自分のマシンのフリーズについては、仮説を信じる前に必ず実測してください。

要件: Node 14 以上。依存パッケージなし。Playwright / Puppeteer 両対応。


MIT License. Built by AI Jidoka Lab (Taku)https://setlog-app.github.io/ai-jidoka-lab/

About

Enforce safe Chromium launch args and a concurrency cap across every Playwright/Puppeteer script in a project - without editing a single call site.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages