devharness 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/build/atomic-write.d.ts +28 -0
- package/build/atomic-write.d.ts.map +1 -0
- package/build/atomic-write.js +126 -0
- package/build/atomic-write.js.map +1 -0
- package/build/cdp-manager.d.ts +524 -0
- package/build/cdp-manager.d.ts.map +1 -0
- package/build/cdp-manager.js +1828 -0
- package/build/cdp-manager.js.map +1 -0
- package/build/cdp-manager.test.d.ts +6 -0
- package/build/cdp-manager.test.d.ts.map +1 -0
- package/build/cdp-manager.test.js +546 -0
- package/build/cdp-manager.test.js.map +1 -0
- package/build/chrome-launcher.d.ts +400 -0
- package/build/chrome-launcher.d.ts.map +1 -0
- package/build/chrome-launcher.js +1071 -0
- package/build/chrome-launcher.js.map +1 -0
- package/build/chrome-launcher.test.d.ts +11 -0
- package/build/chrome-launcher.test.d.ts.map +1 -0
- package/build/chrome-launcher.test.js +204 -0
- package/build/chrome-launcher.test.js.map +1 -0
- package/build/clickable-cache.d.ts +73 -0
- package/build/clickable-cache.d.ts.map +1 -0
- package/build/clickable-cache.js +90 -0
- package/build/clickable-cache.js.map +1 -0
- package/build/command-recorder.d.ts +401 -0
- package/build/command-recorder.d.ts.map +1 -0
- package/build/command-recorder.js +701 -0
- package/build/command-recorder.js.map +1 -0
- package/build/config.d.ts +390 -0
- package/build/config.d.ts.map +1 -0
- package/build/config.js +826 -0
- package/build/config.js.map +1 -0
- package/build/config.test.d.ts +2 -0
- package/build/config.test.d.ts.map +1 -0
- package/build/config.test.js +112 -0
- package/build/config.test.js.map +1 -0
- package/build/connection-manager.d.ts +159 -0
- package/build/connection-manager.d.ts.map +1 -0
- package/build/connection-manager.js +432 -0
- package/build/connection-manager.js.map +1 -0
- package/build/connection-manager.test.d.ts +11 -0
- package/build/connection-manager.test.d.ts.map +1 -0
- package/build/connection-manager.test.js +114 -0
- package/build/connection-manager.test.js.map +1 -0
- package/build/console-monitor.d.ts +150 -0
- package/build/console-monitor.d.ts.map +1 -0
- package/build/console-monitor.js +454 -0
- package/build/console-monitor.js.map +1 -0
- package/build/dashboard/bundle.js +1 -0
- package/build/dashboard/dashboard-client.d.ts +42 -0
- package/build/dashboard/dashboard-client.d.ts.map +1 -0
- package/build/dashboard/dashboard-client.js +184 -0
- package/build/dashboard/dashboard-client.js.map +1 -0
- package/build/dashboard/dashboard-hub.d.ts +50 -0
- package/build/dashboard/dashboard-hub.d.ts.map +1 -0
- package/build/dashboard/dashboard-hub.js +654 -0
- package/build/dashboard/dashboard-hub.js.map +1 -0
- package/build/dashboard/hub-lock.d.ts +16 -0
- package/build/dashboard/hub-lock.d.ts.map +1 -0
- package/build/dashboard/hub-lock.js +77 -0
- package/build/dashboard/hub-lock.js.map +1 -0
- package/build/dashboard/index.d.ts +25 -0
- package/build/dashboard/index.d.ts.map +1 -0
- package/build/dashboard/index.js +77 -0
- package/build/dashboard/index.js.map +1 -0
- package/build/dashboard/types.d.ts +83 -0
- package/build/dashboard/types.d.ts.map +1 -0
- package/build/dashboard/types.js +6 -0
- package/build/dashboard/types.js.map +1 -0
- package/build/debug-logger.d.ts +85 -0
- package/build/debug-logger.d.ts.map +1 -0
- package/build/debug-logger.js +195 -0
- package/build/debug-logger.js.map +1 -0
- package/build/debugger-aware-wrapper.d.ts +37 -0
- package/build/debugger-aware-wrapper.d.ts.map +1 -0
- package/build/debugger-aware-wrapper.js +128 -0
- package/build/debugger-aware-wrapper.js.map +1 -0
- package/build/dom-change-monitor.d.ts +92 -0
- package/build/dom-change-monitor.d.ts.map +1 -0
- package/build/dom-change-monitor.js +437 -0
- package/build/dom-change-monitor.js.map +1 -0
- package/build/element-collector.d.ts +16 -0
- package/build/element-collector.d.ts.map +1 -0
- package/build/element-collector.js +207 -0
- package/build/element-collector.js.map +1 -0
- package/build/error-helpers.d.ts +43 -0
- package/build/error-helpers.d.ts.map +1 -0
- package/build/error-helpers.js +143 -0
- package/build/error-helpers.js.map +1 -0
- package/build/formatters/console-formatter.d.ts +160 -0
- package/build/formatters/console-formatter.d.ts.map +1 -0
- package/build/formatters/console-formatter.js +338 -0
- package/build/formatters/console-formatter.js.map +1 -0
- package/build/helpers/parser-plugins.d.ts +44 -0
- package/build/helpers/parser-plugins.d.ts.map +1 -0
- package/build/helpers/parser-plugins.js +102 -0
- package/build/helpers/parser-plugins.js.map +1 -0
- package/build/helpers/paths.d.ts +79 -0
- package/build/helpers/paths.d.ts.map +1 -0
- package/build/helpers/paths.js +168 -0
- package/build/helpers/paths.js.map +1 -0
- package/build/helpers/process-liveness.d.ts +24 -0
- package/build/helpers/process-liveness.d.ts.map +1 -0
- package/build/helpers/process-liveness.js +36 -0
- package/build/helpers/process-liveness.js.map +1 -0
- package/build/helpers/process-liveness.test.d.ts +2 -0
- package/build/helpers/process-liveness.test.d.ts.map +1 -0
- package/build/helpers/process-liveness.test.js +40 -0
- package/build/helpers/process-liveness.test.js.map +1 -0
- package/build/helpers/sequence-tree.d.ts +27 -0
- package/build/helpers/sequence-tree.d.ts.map +1 -0
- package/build/helpers/sequence-tree.js +80 -0
- package/build/helpers/sequence-tree.js.map +1 -0
- package/build/helpers/sequence-tree.test.d.ts +2 -0
- package/build/helpers/sequence-tree.test.d.ts.map +1 -0
- package/build/helpers/sequence-tree.test.js +122 -0
- package/build/helpers/sequence-tree.test.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +2007 -0
- package/build/index.js.map +1 -0
- package/build/interaction-recorder.d.ts +214 -0
- package/build/interaction-recorder.d.ts.map +1 -0
- package/build/interaction-recorder.js +2007 -0
- package/build/interaction-recorder.js.map +1 -0
- package/build/issue-tracker.d.ts +107 -0
- package/build/issue-tracker.d.ts.map +1 -0
- package/build/issue-tracker.js +582 -0
- package/build/issue-tracker.js.map +1 -0
- package/build/issue-tracker.test.d.ts +2 -0
- package/build/issue-tracker.test.d.ts.map +1 -0
- package/build/issue-tracker.test.js +88 -0
- package/build/issue-tracker.test.js.map +1 -0
- package/build/launch-chrome-port.test.d.ts +14 -0
- package/build/launch-chrome-port.test.d.ts.map +1 -0
- package/build/launch-chrome-port.test.js +97 -0
- package/build/launch-chrome-port.test.js.map +1 -0
- package/build/log-processor/orchestrator.d.ts +32 -0
- package/build/log-processor/orchestrator.d.ts.map +1 -0
- package/build/log-processor/orchestrator.js +27 -0
- package/build/log-processor/orchestrator.js.map +1 -0
- package/build/logpoint-execution-tracker.d.ts +57 -0
- package/build/logpoint-execution-tracker.d.ts.map +1 -0
- package/build/logpoint-execution-tracker.js +112 -0
- package/build/logpoint-execution-tracker.js.map +1 -0
- package/build/mcp-supervisor.d.ts +3 -0
- package/build/mcp-supervisor.d.ts.map +1 -0
- package/build/mcp-supervisor.js +211 -0
- package/build/mcp-supervisor.js.map +1 -0
- package/build/messages.d.ts +165 -0
- package/build/messages.d.ts.map +1 -0
- package/build/messages.js +574 -0
- package/build/messages.js.map +1 -0
- package/build/network-monitor.d.ts +172 -0
- package/build/network-monitor.d.ts.map +1 -0
- package/build/network-monitor.js +350 -0
- package/build/network-monitor.js.map +1 -0
- package/build/overlays.d.ts +49 -0
- package/build/overlays.d.ts.map +1 -0
- package/build/overlays.js +409 -0
- package/build/overlays.js.map +1 -0
- package/build/persistent-profiles.test.d.ts +15 -0
- package/build/persistent-profiles.test.d.ts.map +1 -0
- package/build/persistent-profiles.test.js +470 -0
- package/build/persistent-profiles.test.js.map +1 -0
- package/build/port-reserver.d.ts +32 -0
- package/build/port-reserver.d.ts.map +1 -0
- package/build/port-reserver.js +118 -0
- package/build/port-reserver.js.map +1 -0
- package/build/puppeteer-manager.d.ts +47 -0
- package/build/puppeteer-manager.d.ts.map +1 -0
- package/build/puppeteer-manager.js +116 -0
- package/build/puppeteer-manager.js.map +1 -0
- package/build/recording-coverage.test.d.ts +12 -0
- package/build/recording-coverage.test.d.ts.map +1 -0
- package/build/recording-coverage.test.js +72 -0
- package/build/recording-coverage.test.js.map +1 -0
- package/build/reference-validator.d.ts +47 -0
- package/build/reference-validator.d.ts.map +1 -0
- package/build/reference-validator.js +86 -0
- package/build/reference-validator.js.map +1 -0
- package/build/reference-validator.test.d.ts +5 -0
- package/build/reference-validator.test.d.ts.map +1 -0
- package/build/reference-validator.test.js +243 -0
- package/build/reference-validator.test.js.map +1 -0
- package/build/replay-cursor.d.ts +27 -0
- package/build/replay-cursor.d.ts.map +1 -0
- package/build/replay-cursor.js +240 -0
- package/build/replay-cursor.js.map +1 -0
- package/build/runners/docker-compose-runner.d.ts +69 -0
- package/build/runners/docker-compose-runner.d.ts.map +1 -0
- package/build/runners/docker-compose-runner.js +438 -0
- package/build/runners/docker-compose-runner.js.map +1 -0
- package/build/runners/docker-runner.d.ts +65 -0
- package/build/runners/docker-runner.d.ts.map +1 -0
- package/build/runners/docker-runner.js +414 -0
- package/build/runners/docker-runner.js.map +1 -0
- package/build/runners/index.d.ts +19 -0
- package/build/runners/index.d.ts.map +1 -0
- package/build/runners/index.js +35 -0
- package/build/runners/index.js.map +1 -0
- package/build/runners/native-runner.d.ts +95 -0
- package/build/runners/native-runner.d.ts.map +1 -0
- package/build/runners/native-runner.js +370 -0
- package/build/runners/native-runner.js.map +1 -0
- package/build/runners/types.d.ts +147 -0
- package/build/runners/types.d.ts.map +1 -0
- package/build/runners/types.js +25 -0
- package/build/runners/types.js.map +1 -0
- package/build/self-restart.d.ts +12 -0
- package/build/self-restart.d.ts.map +1 -0
- package/build/self-restart.js +41 -0
- package/build/self-restart.js.map +1 -0
- package/build/self-restart.test.d.ts +2 -0
- package/build/self-restart.test.d.ts.map +1 -0
- package/build/self-restart.test.js +34 -0
- package/build/self-restart.test.js.map +1 -0
- package/build/server-claims.d.ts +161 -0
- package/build/server-claims.d.ts.map +1 -0
- package/build/server-claims.js +382 -0
- package/build/server-claims.js.map +1 -0
- package/build/server-claims.test.d.ts +2 -0
- package/build/server-claims.test.d.ts.map +1 -0
- package/build/server-claims.test.js +197 -0
- package/build/server-claims.test.js.map +1 -0
- package/build/server-manager.d.ts +438 -0
- package/build/server-manager.d.ts.map +1 -0
- package/build/server-manager.js +1674 -0
- package/build/server-manager.js.map +1 -0
- package/build/server-manager.test.d.ts +17 -0
- package/build/server-manager.test.d.ts.map +1 -0
- package/build/server-manager.test.js +122 -0
- package/build/server-manager.test.js.map +1 -0
- package/build/server-ownership.test.d.ts +2 -0
- package/build/server-ownership.test.d.ts.map +1 -0
- package/build/server-ownership.test.js +238 -0
- package/build/server-ownership.test.js.map +1 -0
- package/build/server-watcher.d.ts +21 -0
- package/build/server-watcher.d.ts.map +1 -0
- package/build/server-watcher.js +105 -0
- package/build/server-watcher.js.map +1 -0
- package/build/session-detector.d.ts +47 -0
- package/build/session-detector.d.ts.map +1 -0
- package/build/session-detector.js +229 -0
- package/build/session-detector.js.map +1 -0
- package/build/session-detector.test.d.ts +9 -0
- package/build/session-detector.test.d.ts.map +1 -0
- package/build/session-detector.test.js +45 -0
- package/build/session-detector.test.js.map +1 -0
- package/build/sourcemap-handler.d.ts +114 -0
- package/build/sourcemap-handler.d.ts.map +1 -0
- package/build/sourcemap-handler.js +479 -0
- package/build/sourcemap-handler.js.map +1 -0
- package/build/supervisor/child-manager.d.ts +43 -0
- package/build/supervisor/child-manager.d.ts.map +1 -0
- package/build/supervisor/child-manager.js +148 -0
- package/build/supervisor/child-manager.js.map +1 -0
- package/build/supervisor/child-manager.test.d.ts +2 -0
- package/build/supervisor/child-manager.test.d.ts.map +1 -0
- package/build/supervisor/child-manager.test.js +107 -0
- package/build/supervisor/child-manager.test.js.map +1 -0
- package/build/supervisor/client-watcher.d.ts +48 -0
- package/build/supervisor/client-watcher.d.ts.map +1 -0
- package/build/supervisor/client-watcher.js +186 -0
- package/build/supervisor/client-watcher.js.map +1 -0
- package/build/supervisor/client-watcher.test.d.ts +2 -0
- package/build/supervisor/client-watcher.test.d.ts.map +1 -0
- package/build/supervisor/client-watcher.test.js +121 -0
- package/build/supervisor/client-watcher.test.js.map +1 -0
- package/build/supervisor/idle-config.d.ts +30 -0
- package/build/supervisor/idle-config.d.ts.map +1 -0
- package/build/supervisor/idle-config.js +76 -0
- package/build/supervisor/idle-config.js.map +1 -0
- package/build/supervisor/idle-config.test.d.ts +2 -0
- package/build/supervisor/idle-config.test.d.ts.map +1 -0
- package/build/supervisor/idle-config.test.js +119 -0
- package/build/supervisor/idle-config.test.js.map +1 -0
- package/build/supervisor/ndjson-reader.d.ts +47 -0
- package/build/supervisor/ndjson-reader.d.ts.map +1 -0
- package/build/supervisor/ndjson-reader.js +75 -0
- package/build/supervisor/ndjson-reader.js.map +1 -0
- package/build/supervisor/ndjson-reader.test.d.ts +2 -0
- package/build/supervisor/ndjson-reader.test.d.ts.map +1 -0
- package/build/supervisor/ndjson-reader.test.js +92 -0
- package/build/supervisor/ndjson-reader.test.js.map +1 -0
- package/build/supervisor/pidfile.d.ts +11 -0
- package/build/supervisor/pidfile.d.ts.map +1 -0
- package/build/supervisor/pidfile.js +29 -0
- package/build/supervisor/pidfile.js.map +1 -0
- package/build/supervisor/pidfile.test.d.ts +8 -0
- package/build/supervisor/pidfile.test.d.ts.map +1 -0
- package/build/supervisor/pidfile.test.js +46 -0
- package/build/supervisor/pidfile.test.js.map +1 -0
- package/build/supervisor/restart-coordinator.d.ts +73 -0
- package/build/supervisor/restart-coordinator.d.ts.map +1 -0
- package/build/supervisor/restart-coordinator.js +300 -0
- package/build/supervisor/restart-coordinator.js.map +1 -0
- package/build/supervisor/restart-coordinator.test.d.ts +2 -0
- package/build/supervisor/restart-coordinator.test.d.ts.map +1 -0
- package/build/supervisor/restart-coordinator.test.js +434 -0
- package/build/supervisor/restart-coordinator.test.js.map +1 -0
- package/build/test-support/fake-execute-tool-call.d.ts +11 -0
- package/build/test-support/fake-execute-tool-call.d.ts.map +1 -0
- package/build/test-support/fake-execute-tool-call.js +19 -0
- package/build/test-support/fake-execute-tool-call.js.map +1 -0
- package/build/tool-error.d.ts +14 -0
- package/build/tool-error.d.ts.map +1 -0
- package/build/tool-error.js +18 -0
- package/build/tool-error.js.map +1 -0
- package/build/tool-response.d.ts +293 -0
- package/build/tool-response.d.ts.map +1 -0
- package/build/tool-response.js +365 -0
- package/build/tool-response.js.map +1 -0
- package/build/tools/assert-tools.d.ts +62 -0
- package/build/tools/assert-tools.d.ts.map +1 -0
- package/build/tools/assert-tools.js +330 -0
- package/build/tools/assert-tools.js.map +1 -0
- package/build/tools/breakpoint-await-abort.test.d.ts +2 -0
- package/build/tools/breakpoint-await-abort.test.d.ts.map +1 -0
- package/build/tools/breakpoint-await-abort.test.js +124 -0
- package/build/tools/breakpoint-await-abort.test.js.map +1 -0
- package/build/tools/breakpoint-tools.d.ts +99 -0
- package/build/tools/breakpoint-tools.d.ts.map +1 -0
- package/build/tools/breakpoint-tools.js +1197 -0
- package/build/tools/breakpoint-tools.js.map +1 -0
- package/build/tools/config-tools.d.ts +58 -0
- package/build/tools/config-tools.d.ts.map +1 -0
- package/build/tools/config-tools.js +180 -0
- package/build/tools/config-tools.js.map +1 -0
- package/build/tools/connection-routing.test.d.ts +12 -0
- package/build/tools/connection-routing.test.d.ts.map +1 -0
- package/build/tools/connection-routing.test.js +206 -0
- package/build/tools/connection-routing.test.js.map +1 -0
- package/build/tools/console-tools.d.ts +84 -0
- package/build/tools/console-tools.d.ts.map +1 -0
- package/build/tools/console-tools.js +241 -0
- package/build/tools/console-tools.js.map +1 -0
- package/build/tools/content-tools.d.ts +74 -0
- package/build/tools/content-tools.d.ts.map +1 -0
- package/build/tools/content-tools.js +582 -0
- package/build/tools/content-tools.js.map +1 -0
- package/build/tools/dashboard-tools.d.ts +38 -0
- package/build/tools/dashboard-tools.d.ts.map +1 -0
- package/build/tools/dashboard-tools.js +123 -0
- package/build/tools/dashboard-tools.js.map +1 -0
- package/build/tools/dom-tools.d.ts +41 -0
- package/build/tools/dom-tools.d.ts.map +1 -0
- package/build/tools/dom-tools.js +277 -0
- package/build/tools/dom-tools.js.map +1 -0
- package/build/tools/download-tools.d.ts +55 -0
- package/build/tools/download-tools.d.ts.map +1 -0
- package/build/tools/download-tools.js +280 -0
- package/build/tools/download-tools.js.map +1 -0
- package/build/tools/execution-tools.d.ts +40 -0
- package/build/tools/execution-tools.d.ts.map +1 -0
- package/build/tools/execution-tools.js +121 -0
- package/build/tools/execution-tools.js.map +1 -0
- package/build/tools/input-abort.test.d.ts +2 -0
- package/build/tools/input-abort.test.d.ts.map +1 -0
- package/build/tools/input-abort.test.js +227 -0
- package/build/tools/input-abort.test.js.map +1 -0
- package/build/tools/input-ambiguous-selector.test.d.ts +2 -0
- package/build/tools/input-ambiguous-selector.test.d.ts.map +1 -0
- package/build/tools/input-ambiguous-selector.test.js +34 -0
- package/build/tools/input-ambiguous-selector.test.js.map +1 -0
- package/build/tools/input-tools.d.ts +168 -0
- package/build/tools/input-tools.d.ts.map +1 -0
- package/build/tools/input-tools.js +1397 -0
- package/build/tools/input-tools.js.map +1 -0
- package/build/tools/inspection-tools.d.ts +112 -0
- package/build/tools/inspection-tools.d.ts.map +1 -0
- package/build/tools/inspection-tools.js +637 -0
- package/build/tools/inspection-tools.js.map +1 -0
- package/build/tools/interpolation.d.ts +30 -0
- package/build/tools/interpolation.d.ts.map +1 -0
- package/build/tools/interpolation.js +143 -0
- package/build/tools/interpolation.js.map +1 -0
- package/build/tools/issues-abort-hygiene.test.d.ts +2 -0
- package/build/tools/issues-abort-hygiene.test.d.ts.map +1 -0
- package/build/tools/issues-abort-hygiene.test.js +79 -0
- package/build/tools/issues-abort-hygiene.test.js.map +1 -0
- package/build/tools/issues-tools.d.ts +98 -0
- package/build/tools/issues-tools.d.ts.map +1 -0
- package/build/tools/issues-tools.js +821 -0
- package/build/tools/issues-tools.js.map +1 -0
- package/build/tools/issues-tools.test.d.ts +8 -0
- package/build/tools/issues-tools.test.d.ts.map +1 -0
- package/build/tools/issues-tools.test.js +184 -0
- package/build/tools/issues-tools.test.js.map +1 -0
- package/build/tools/modal-tools.d.ts +73 -0
- package/build/tools/modal-tools.d.ts.map +1 -0
- package/build/tools/modal-tools.js +161 -0
- package/build/tools/modal-tools.js.map +1 -0
- package/build/tools/network-tools.d.ts +72 -0
- package/build/tools/network-tools.d.ts.map +1 -0
- package/build/tools/network-tools.js +432 -0
- package/build/tools/network-tools.js.map +1 -0
- package/build/tools/page-tools.d.ts +78 -0
- package/build/tools/page-tools.d.ts.map +1 -0
- package/build/tools/page-tools.js +255 -0
- package/build/tools/page-tools.js.map +1 -0
- package/build/tools/plugin-tools.d.ts +16 -0
- package/build/tools/plugin-tools.d.ts.map +1 -0
- package/build/tools/plugin-tools.js +17 -0
- package/build/tools/plugin-tools.js.map +1 -0
- package/build/tools/replay-abort.test.d.ts +2 -0
- package/build/tools/replay-abort.test.d.ts.map +1 -0
- package/build/tools/replay-abort.test.js +259 -0
- package/build/tools/replay-abort.test.js.map +1 -0
- package/build/tools/replay-add-conditional.test.d.ts +2 -0
- package/build/tools/replay-add-conditional.test.d.ts.map +1 -0
- package/build/tools/replay-add-conditional.test.js +205 -0
- package/build/tools/replay-add-conditional.test.js.map +1 -0
- package/build/tools/replay-background-run.test.d.ts +2 -0
- package/build/tools/replay-background-run.test.d.ts.map +1 -0
- package/build/tools/replay-background-run.test.js +176 -0
- package/build/tools/replay-background-run.test.js.map +1 -0
- package/build/tools/replay-capture-variables.test.d.ts +2 -0
- package/build/tools/replay-capture-variables.test.d.ts.map +1 -0
- package/build/tools/replay-capture-variables.test.js +331 -0
- package/build/tools/replay-capture-variables.test.js.map +1 -0
- package/build/tools/replay-conditional-idb-and-launch.test.d.ts +11 -0
- package/build/tools/replay-conditional-idb-and-launch.test.d.ts.map +1 -0
- package/build/tools/replay-conditional-idb-and-launch.test.js +429 -0
- package/build/tools/replay-conditional-idb-and-launch.test.js.map +1 -0
- package/build/tools/replay-conditional-timeout.test.d.ts +14 -0
- package/build/tools/replay-conditional-timeout.test.d.ts.map +1 -0
- package/build/tools/replay-conditional-timeout.test.js +126 -0
- package/build/tools/replay-conditional-timeout.test.js.map +1 -0
- package/build/tools/replay-declare.test.d.ts +2 -0
- package/build/tools/replay-declare.test.d.ts.map +1 -0
- package/build/tools/replay-declare.test.js +106 -0
- package/build/tools/replay-declare.test.js.map +1 -0
- package/build/tools/replay-declared-browser-cleanup.test.d.ts +2 -0
- package/build/tools/replay-declared-browser-cleanup.test.d.ts.map +1 -0
- package/build/tools/replay-declared-browser-cleanup.test.js +103 -0
- package/build/tools/replay-declared-browser-cleanup.test.js.map +1 -0
- package/build/tools/replay-declared-profile.test.d.ts +2 -0
- package/build/tools/replay-declared-profile.test.d.ts.map +1 -0
- package/build/tools/replay-declared-profile.test.js +116 -0
- package/build/tools/replay-declared-profile.test.js.map +1 -0
- package/build/tools/replay-executor.d.ts +556 -0
- package/build/tools/replay-executor.d.ts.map +1 -0
- package/build/tools/replay-executor.js +2347 -0
- package/build/tools/replay-executor.js.map +1 -0
- package/build/tools/replay-folder-save.test.d.ts +2 -0
- package/build/tools/replay-folder-save.test.d.ts.map +1 -0
- package/build/tools/replay-folder-save.test.js +63 -0
- package/build/tools/replay-folder-save.test.js.map +1 -0
- package/build/tools/replay-formatters.d.ts +148 -0
- package/build/tools/replay-formatters.d.ts.map +1 -0
- package/build/tools/replay-formatters.js +856 -0
- package/build/tools/replay-formatters.js.map +1 -0
- package/build/tools/replay-kill-chrome-on-finish.test.d.ts +2 -0
- package/build/tools/replay-kill-chrome-on-finish.test.d.ts.map +1 -0
- package/build/tools/replay-kill-chrome-on-finish.test.js +175 -0
- package/build/tools/replay-kill-chrome-on-finish.test.js.map +1 -0
- package/build/tools/replay-multi-connection.test.d.ts +2 -0
- package/build/tools/replay-multi-connection.test.d.ts.map +1 -0
- package/build/tools/replay-multi-connection.test.js +547 -0
- package/build/tools/replay-multi-connection.test.js.map +1 -0
- package/build/tools/replay-output-and-record-options.test.d.ts +2 -0
- package/build/tools/replay-output-and-record-options.test.d.ts.map +1 -0
- package/build/tools/replay-output-and-record-options.test.js +240 -0
- package/build/tools/replay-output-and-record-options.test.js.map +1 -0
- package/build/tools/replay-production-error-shape.test.d.ts +7 -0
- package/build/tools/replay-production-error-shape.test.d.ts.map +1 -0
- package/build/tools/replay-production-error-shape.test.js +93 -0
- package/build/tools/replay-production-error-shape.test.js.map +1 -0
- package/build/tools/replay-rebase.test.d.ts +2 -0
- package/build/tools/replay-rebase.test.d.ts.map +1 -0
- package/build/tools/replay-rebase.test.js +58 -0
- package/build/tools/replay-rebase.test.js.map +1 -0
- package/build/tools/replay-retry-classification.test.d.ts +9 -0
- package/build/tools/replay-retry-classification.test.d.ts.map +1 -0
- package/build/tools/replay-retry-classification.test.js +57 -0
- package/build/tools/replay-retry-classification.test.js.map +1 -0
- package/build/tools/replay-run-registry.d.ts +55 -0
- package/build/tools/replay-run-registry.d.ts.map +1 -0
- package/build/tools/replay-run-registry.js +65 -0
- package/build/tools/replay-run-registry.js.map +1 -0
- package/build/tools/replay-sequence-reload.test.d.ts +2 -0
- package/build/tools/replay-sequence-reload.test.d.ts.map +1 -0
- package/build/tools/replay-sequence-reload.test.js +82 -0
- package/build/tools/replay-sequence-reload.test.js.map +1 -0
- package/build/tools/replay-socket-scope.test.d.ts +2 -0
- package/build/tools/replay-socket-scope.test.d.ts.map +1 -0
- package/build/tools/replay-socket-scope.test.js +75 -0
- package/build/tools/replay-socket-scope.test.js.map +1 -0
- package/build/tools/replay-step-connection.test.d.ts +2 -0
- package/build/tools/replay-step-connection.test.d.ts.map +1 -0
- package/build/tools/replay-step-connection.test.js +292 -0
- package/build/tools/replay-step-connection.test.js.map +1 -0
- package/build/tools/replay-step-timeout.test.d.ts +2 -0
- package/build/tools/replay-step-timeout.test.d.ts.map +1 -0
- package/build/tools/replay-step-timeout.test.js +204 -0
- package/build/tools/replay-step-timeout.test.js.map +1 -0
- package/build/tools/replay-tags.test.d.ts +2 -0
- package/build/tools/replay-tags.test.d.ts.map +1 -0
- package/build/tools/replay-tags.test.js +99 -0
- package/build/tools/replay-tags.test.js.map +1 -0
- package/build/tools/replay-teardown-and-foreach.test.d.ts +13 -0
- package/build/tools/replay-teardown-and-foreach.test.d.ts.map +1 -0
- package/build/tools/replay-teardown-and-foreach.test.js +318 -0
- package/build/tools/replay-teardown-and-foreach.test.js.map +1 -0
- package/build/tools/replay-tool-name-validation.test.d.ts +2 -0
- package/build/tools/replay-tool-name-validation.test.d.ts.map +1 -0
- package/build/tools/replay-tool-name-validation.test.js +280 -0
- package/build/tools/replay-tool-name-validation.test.js.map +1 -0
- package/build/tools/replay-tools.d.ts +288 -0
- package/build/tools/replay-tools.d.ts.map +1 -0
- package/build/tools/replay-tools.js +2994 -0
- package/build/tools/replay-tools.js.map +1 -0
- package/build/tools/request-abort.test.d.ts +2 -0
- package/build/tools/request-abort.test.d.ts.map +1 -0
- package/build/tools/request-abort.test.js +130 -0
- package/build/tools/request-abort.test.js.map +1 -0
- package/build/tools/request-tools.d.ts +56 -0
- package/build/tools/request-tools.d.ts.map +1 -0
- package/build/tools/request-tools.js +174 -0
- package/build/tools/request-tools.js.map +1 -0
- package/build/tools/screenshot-tools.d.ts +135 -0
- package/build/tools/screenshot-tools.d.ts.map +1 -0
- package/build/tools/screenshot-tools.js +636 -0
- package/build/tools/screenshot-tools.js.map +1 -0
- package/build/tools/server-tools.d.ts +88 -0
- package/build/tools/server-tools.d.ts.map +1 -0
- package/build/tools/server-tools.js +485 -0
- package/build/tools/server-tools.js.map +1 -0
- package/build/tools/source-tools.d.ts +46 -0
- package/build/tools/source-tools.d.ts.map +1 -0
- package/build/tools/source-tools.js +47 -0
- package/build/tools/source-tools.js.map +1 -0
- package/build/tools/storage-tools.d.ts +141 -0
- package/build/tools/storage-tools.d.ts.map +1 -0
- package/build/tools/storage-tools.js +894 -0
- package/build/tools/storage-tools.js.map +1 -0
- package/build/tools/storage-tools.test.d.ts +13 -0
- package/build/tools/storage-tools.test.d.ts.map +1 -0
- package/build/tools/storage-tools.test.js +707 -0
- package/build/tools/storage-tools.test.js.map +1 -0
- package/build/tools/tab-tools.d.ts +42 -0
- package/build/tools/tab-tools.d.ts.map +1 -0
- package/build/tools/tab-tools.js +296 -0
- package/build/tools/tab-tools.js.map +1 -0
- package/build/tools/wait-tools.d.ts +85 -0
- package/build/tools/wait-tools.d.ts.map +1 -0
- package/build/tools/wait-tools.js +236 -0
- package/build/tools/wait-tools.js.map +1 -0
- package/build/tools/wait-tools.test.d.ts +9 -0
- package/build/tools/wait-tools.test.d.ts.map +1 -0
- package/build/tools/wait-tools.test.js +193 -0
- package/build/tools/wait-tools.test.js.map +1 -0
- package/build/types.d.ts +133 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +5 -0
- package/build/types.js.map +1 -0
- package/build/ui-verifier.d.ts +88 -0
- package/build/ui-verifier.d.ts.map +1 -0
- package/build/ui-verifier.js +540 -0
- package/build/ui-verifier.js.map +1 -0
- package/build/utils/abort.d.ts +68 -0
- package/build/utils/abort.d.ts.map +1 -0
- package/build/utils/abort.js +159 -0
- package/build/utils/abort.js.map +1 -0
- package/build/utils/abort.test.d.ts +2 -0
- package/build/utils/abort.test.d.ts.map +1 -0
- package/build/utils/abort.test.js +174 -0
- package/build/utils/abort.test.js.map +1 -0
- package/build/utils/modal-detection-core.d.ts +150 -0
- package/build/utils/modal-detection-core.d.ts.map +1 -0
- package/build/utils/modal-detection-core.js +332 -0
- package/build/utils/modal-detection-core.js.map +1 -0
- package/build/utils/modal-detection-core.test.d.ts +5 -0
- package/build/utils/modal-detection-core.test.d.ts.map +1 -0
- package/build/utils/modal-detection-core.test.js +492 -0
- package/build/utils/modal-detection-core.test.js.map +1 -0
- package/build/utils/modal-detector.d.ts +29 -0
- package/build/utils/modal-detector.d.ts.map +1 -0
- package/build/utils/modal-detector.js +281 -0
- package/build/utils/modal-detector.js.map +1 -0
- package/build/utils/modal-dismissal.d.ts +32 -0
- package/build/utils/modal-dismissal.d.ts.map +1 -0
- package/build/utils/modal-dismissal.js +190 -0
- package/build/utils/modal-dismissal.js.map +1 -0
- package/build/utils/port-check.d.ts +31 -0
- package/build/utils/port-check.d.ts.map +1 -0
- package/build/utils/port-check.js +87 -0
- package/build/utils/port-check.js.map +1 -0
- package/build/utils/port-check.test.d.ts +2 -0
- package/build/utils/port-check.test.d.ts.map +1 -0
- package/build/utils/port-check.test.js +66 -0
- package/build/utils/port-check.test.js.map +1 -0
- package/build/utils/selector-resolver.d.ts +66 -0
- package/build/utils/selector-resolver.d.ts.map +1 -0
- package/build/utils/selector-resolver.js +200 -0
- package/build/utils/selector-resolver.js.map +1 -0
- package/build/validation-helpers.d.ts +41 -0
- package/build/validation-helpers.d.ts.map +1 -0
- package/build/validation-helpers.js +236 -0
- package/build/validation-helpers.js.map +1 -0
- package/build/window-sizing.d.ts +26 -0
- package/build/window-sizing.d.ts.map +1 -0
- package/build/window-sizing.js +87 -0
- package/build/window-sizing.js.map +1 -0
- package/build/window-sizing.test.d.ts +8 -0
- package/build/window-sizing.test.d.ts.map +1 -0
- package/build/window-sizing.test.js +113 -0
- package/build/window-sizing.test.js.map +1 -0
- package/docs/README.md +74 -0
- package/docs/automation.md +562 -0
- package/docs/debugging.md +229 -0
- package/docs/installation.md +133 -0
- package/docs/instructions.md +227 -0
- package/docs/mcp-instructions.md +15 -0
- package/docs/message-system.md +327 -0
- package/docs/messages.md +3480 -0
- package/docs/parser-plugins.md +132 -0
- package/docs/replay.md +1411 -0
- package/docs/troubleshooting.md +272 -0
- package/docs/working_with_llm_notes.md +5 -0
- package/package.json +87 -0
- package/skills/devharness/SKILL.md +195 -0
- package/skills/devharness/references/sequences.md +613 -0
- package/skills/devharness/references/tool-categories.md +99 -0
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome Launcher
|
|
3
|
+
* Utilities for launching Chrome with debugging enabled
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import * as os from 'os';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as net from 'net';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import { randomBytes } from 'crypto';
|
|
11
|
+
import { debugLog } from './debug-logger.js';
|
|
12
|
+
/**
|
|
13
|
+
* Prefix used for all launcher-created temporary Chrome profile directories.
|
|
14
|
+
* The startup sweep only ever considers directories with this prefix.
|
|
15
|
+
*/
|
|
16
|
+
export const EPHEMERAL_PROFILE_PREFIX = 'chrome-debug-profile-';
|
|
17
|
+
/**
|
|
18
|
+
* Legal characters for a named persistent profile (issue 13).
|
|
19
|
+
*
|
|
20
|
+
* Deliberately strict: the name becomes a directory under the profile root, so
|
|
21
|
+
* anything that could escape it (`/`, `..`, leading dot) or confuse the startup
|
|
22
|
+
* sweep is rejected rather than sanitised - silently renaming a profile would
|
|
23
|
+
* hand the caller a different identity than they asked for.
|
|
24
|
+
*/
|
|
25
|
+
export const PROFILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
26
|
+
/** Thrown when a profile name is not a safe single directory segment. */
|
|
27
|
+
export class InvalidProfileNameError extends Error {
|
|
28
|
+
profile;
|
|
29
|
+
constructor(profile) {
|
|
30
|
+
super(`Invalid profile name "${profile}". Use 1-64 characters: letters, digits, dot, dash or underscore, starting with a letter or digit.`);
|
|
31
|
+
this.profile = profile;
|
|
32
|
+
this.name = 'InvalidProfileNameError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Thrown when an operation would disturb a profile a live Chrome is holding. */
|
|
36
|
+
export class ProfileInUseError extends Error {
|
|
37
|
+
profile;
|
|
38
|
+
port;
|
|
39
|
+
constructor(profile, port) {
|
|
40
|
+
super(`Profile "${profile}" is in use by the Chrome running on port ${port}.`);
|
|
41
|
+
this.profile = profile;
|
|
42
|
+
this.port = port;
|
|
43
|
+
this.name = 'ProfileInUseError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Thrown when a profile is held by a Chrome this process did not launch -
|
|
48
|
+
* typically another cdp-tools session on the same machine, because the default
|
|
49
|
+
* persistent profile root (`~/.cdp-tools/profiles`) is global.
|
|
50
|
+
*
|
|
51
|
+
* Detected from the profile's Chrome `SingletonLock`, so we know the holding
|
|
52
|
+
* PID but not which debug port it listens on.
|
|
53
|
+
*
|
|
54
|
+
* LIMITATION: the check is POSIX-only. `SingletonLock` is a symlink on
|
|
55
|
+
* macOS/Linux; on Windows Chrome uses a plain lock file we cannot read a PID
|
|
56
|
+
* from, so this error is never raised there and the cross-session races it
|
|
57
|
+
* guards against remain possible.
|
|
58
|
+
*/
|
|
59
|
+
export class ProfileLockedError extends Error {
|
|
60
|
+
profile;
|
|
61
|
+
pid;
|
|
62
|
+
dir;
|
|
63
|
+
constructor(profile, pid, dir) {
|
|
64
|
+
super(`Profile "${profile}" is locked by a Chrome started by another process (PID ${pid}, ${dir}). ` +
|
|
65
|
+
`Only one live Chrome may hold a profile; quit that browser (or use a different profile name) and retry.`);
|
|
66
|
+
this.profile = profile;
|
|
67
|
+
this.pid = pid;
|
|
68
|
+
this.dir = dir;
|
|
69
|
+
this.name = 'ProfileLockedError';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Validate a persistent profile name, returning it trimmed.
|
|
74
|
+
* @throws InvalidProfileNameError
|
|
75
|
+
*/
|
|
76
|
+
export function normalizeProfileName(name) {
|
|
77
|
+
const trimmed = (name ?? '').trim();
|
|
78
|
+
if (!PROFILE_NAME_PATTERN.test(trimmed) || trimmed.startsWith(EPHEMERAL_PROFILE_PREFIX)) {
|
|
79
|
+
throw new InvalidProfileNameError(name);
|
|
80
|
+
}
|
|
81
|
+
return trimmed;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Decide which port a launchChrome call should use (bug-005).
|
|
85
|
+
*
|
|
86
|
+
* Extracted from the MCP handler so the decision is testable without a browser
|
|
87
|
+
* or an MCP server: src/index.ts calls main() on import, so anything left
|
|
88
|
+
* inline there can only be "tested" by grepping the source.
|
|
89
|
+
*
|
|
90
|
+
* Rules:
|
|
91
|
+
* - An explicit `port` is always honoured, never silently relocated.
|
|
92
|
+
* - `forceNewInstance` must produce a fresh process, so an explicit port that
|
|
93
|
+
* is already occupied is an error rather than a hand-off to whatever is
|
|
94
|
+
* listening; occupancy is only consulted in that case.
|
|
95
|
+
* - `forceNewInstance` without a port picks a known-free port instead of the
|
|
96
|
+
* reserved one, which an existing instance may already be using.
|
|
97
|
+
*/
|
|
98
|
+
export async function resolveLaunchPort(req) {
|
|
99
|
+
const { explicitPort, forceNewInstance, reservedPort } = req;
|
|
100
|
+
if (!forceNewInstance) {
|
|
101
|
+
return { decision: 'use', port: explicitPort ?? reservedPort };
|
|
102
|
+
}
|
|
103
|
+
if (explicitPort !== undefined) {
|
|
104
|
+
if (await req.isPortOccupied(explicitPort)) {
|
|
105
|
+
return { decision: 'forced-port-in-use', port: explicitPort };
|
|
106
|
+
}
|
|
107
|
+
return { decision: 'use', port: explicitPort };
|
|
108
|
+
}
|
|
109
|
+
return { decision: 'use', port: await req.findFreePort() };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Decide whether a launchChrome call may reuse an existing Chrome, given the
|
|
113
|
+
* named profile it asked for (issue 13 / bug: profile pre-check ordering).
|
|
114
|
+
*
|
|
115
|
+
* The ordering this encodes is the whole point: a live instance already running
|
|
116
|
+
* the requested profile is REUSED, exactly as it would be without a profile, so
|
|
117
|
+
* the idempotent `launchChrome({ profile, reference })` "make sure it's up"
|
|
118
|
+
* pattern keeps working. "Profile in use" is only an error when the call would
|
|
119
|
+
* have to put a SECOND Chrome on a profile another instance holds.
|
|
120
|
+
*/
|
|
121
|
+
export function decideProfileReuse(req) {
|
|
122
|
+
const { wantedProfileDir, existing, holderPort } = req;
|
|
123
|
+
if (!wantedProfileDir) {
|
|
124
|
+
return { decision: 'ok' };
|
|
125
|
+
}
|
|
126
|
+
if (!existing) {
|
|
127
|
+
// We would spawn - a holder elsewhere means a second Chrome on one profile.
|
|
128
|
+
return holderPort !== undefined ? { decision: 'in-use', port: holderPort } : { decision: 'ok' };
|
|
129
|
+
}
|
|
130
|
+
if (existing.profileDir === wantedProfileDir) {
|
|
131
|
+
return { decision: 'ok' }; // same profile, same browser - plain reuse
|
|
132
|
+
}
|
|
133
|
+
if (holderPort !== undefined && holderPort !== existing.port) {
|
|
134
|
+
// Reuse is out (wrong profile) and spawning is out (someone holds it).
|
|
135
|
+
return { decision: 'in-use', port: holderPort };
|
|
136
|
+
}
|
|
137
|
+
return { decision: 'mismatch', port: existing.port, actualProfile: existing.profileDir };
|
|
138
|
+
}
|
|
139
|
+
export class ChromeLauncher {
|
|
140
|
+
chromeProcesses = new Map();
|
|
141
|
+
launchLocks = new Map();
|
|
142
|
+
/** profile name -> in-flight launch for that profile (see launch()) */
|
|
143
|
+
profileLaunchLocks = new Map();
|
|
144
|
+
lastCloseEvents = [];
|
|
145
|
+
maxCloseEvents = 10; // Keep last 10 close events
|
|
146
|
+
pendingCloseReason = new Map(); // Track reason before kill
|
|
147
|
+
onExitCallback = null;
|
|
148
|
+
/** port -> profile dir currently in use by the Chrome on that port */
|
|
149
|
+
profileDirs = new Map();
|
|
150
|
+
profileRoot;
|
|
151
|
+
persistentProfileRootOption;
|
|
152
|
+
staleProfileMaxAgeMs;
|
|
153
|
+
/** Resolves once the startup sweep (if any) has finished. Exposed for tests. */
|
|
154
|
+
startupSweep;
|
|
155
|
+
constructor(options = {}) {
|
|
156
|
+
this.profileRoot = options.profileRoot ?? os.tmpdir();
|
|
157
|
+
this.persistentProfileRootOption =
|
|
158
|
+
options.persistentProfileRoot ?? path.join(os.homedir(), '.cdp-tools', 'profiles');
|
|
159
|
+
this.staleProfileMaxAgeMs = options.staleProfileMaxAgeMs ?? 60 * 60 * 1000;
|
|
160
|
+
// Sweep profiles left behind by crashed/killed sessions. Fire-and-forget:
|
|
161
|
+
// a failure here must never prevent the launcher from being usable.
|
|
162
|
+
this.startupSweep = options.sweepStaleProfilesOnStartup === false
|
|
163
|
+
? Promise.resolve([])
|
|
164
|
+
: this.sweepStaleProfiles().catch((error) => {
|
|
165
|
+
debugLog('ChromeLauncher', `Startup profile sweep failed: ${error}`).catch(() => { });
|
|
166
|
+
return [];
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Set a callback to be invoked when any Chrome process exits.
|
|
171
|
+
* Used for cleanup (closing stale connections) and port re-reservation.
|
|
172
|
+
*/
|
|
173
|
+
setOnExitCallback(callback) {
|
|
174
|
+
this.onExitCallback = callback;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Get the Chrome executable path for the current platform
|
|
178
|
+
*/
|
|
179
|
+
getChromePath() {
|
|
180
|
+
const platform = os.platform();
|
|
181
|
+
switch (platform) {
|
|
182
|
+
case 'darwin': // macOS
|
|
183
|
+
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
184
|
+
case 'win32': // Windows
|
|
185
|
+
return 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
|
|
186
|
+
case 'linux':
|
|
187
|
+
// Try common Linux paths
|
|
188
|
+
return '/usr/bin/google-chrome';
|
|
189
|
+
default:
|
|
190
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Create Chrome preferences file that disables password manager popups
|
|
195
|
+
* This is required because command-line flags alone don't reliably disable
|
|
196
|
+
* the "Change your password" leak detection popup
|
|
197
|
+
*/
|
|
198
|
+
createChromePreferences(userDataDir, overwrite = true) {
|
|
199
|
+
const defaultDir = path.join(userDataDir, 'Default');
|
|
200
|
+
const prefsPath = path.join(defaultDir, 'Preferences');
|
|
201
|
+
// Create Default directory if it doesn't exist
|
|
202
|
+
if (!fs.existsSync(defaultDir)) {
|
|
203
|
+
fs.mkdirSync(defaultDir, { recursive: true });
|
|
204
|
+
}
|
|
205
|
+
// Persistent/named profiles (issue 13) only get the seed once. Rewriting
|
|
206
|
+
// Preferences on every launch would throw away everything the profile has
|
|
207
|
+
// accumulated - which is the entire point of a persistent profile.
|
|
208
|
+
if (!overwrite && fs.existsSync(prefsPath)) {
|
|
209
|
+
debugLog('ChromeLauncher', `Keeping existing Chrome preferences at ${prefsPath}`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Chrome preferences to disable password-related popups
|
|
213
|
+
const preferences = {
|
|
214
|
+
credentials_enable_service: false,
|
|
215
|
+
profile: {
|
|
216
|
+
password_manager_enabled: false,
|
|
217
|
+
password_manager_leak_detection: false,
|
|
218
|
+
},
|
|
219
|
+
safebrowsing: {
|
|
220
|
+
enabled: false,
|
|
221
|
+
enhanced: false,
|
|
222
|
+
},
|
|
223
|
+
autofill: {
|
|
224
|
+
profile_enabled: false,
|
|
225
|
+
credit_card_enabled: false,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
fs.writeFileSync(prefsPath, JSON.stringify(preferences, null, 2));
|
|
229
|
+
debugLog('ChromeLauncher', `Created Chrome preferences at ${prefsPath} with password manager disabled`);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Check if a port is already in use using TCP connection
|
|
233
|
+
*/
|
|
234
|
+
async isPortInUse(port) {
|
|
235
|
+
await debugLog('ChromeLauncher', `isPortInUse() checking port ${port}`);
|
|
236
|
+
return new Promise((resolve) => {
|
|
237
|
+
try {
|
|
238
|
+
const socket = new net.Socket();
|
|
239
|
+
socket.setTimeout(500);
|
|
240
|
+
socket.on('connect', () => {
|
|
241
|
+
socket.destroy();
|
|
242
|
+
debugLog('ChromeLauncher', `isPortInUse() port ${port} is in use`).catch(() => { });
|
|
243
|
+
resolve(true);
|
|
244
|
+
});
|
|
245
|
+
socket.on('timeout', () => {
|
|
246
|
+
socket.destroy();
|
|
247
|
+
debugLog('ChromeLauncher', `isPortInUse() timeout checking port ${port}, assuming free`).catch(() => { });
|
|
248
|
+
resolve(false);
|
|
249
|
+
});
|
|
250
|
+
socket.on('error', (err) => {
|
|
251
|
+
socket.destroy();
|
|
252
|
+
if (err.code === 'ECONNREFUSED') {
|
|
253
|
+
// Port is not in use
|
|
254
|
+
debugLog('ChromeLauncher', `isPortInUse() port ${port} is free (ECONNREFUSED)`).catch(() => { });
|
|
255
|
+
resolve(false);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// Other error, assume port is in use to be safe
|
|
259
|
+
debugLog('ChromeLauncher', `isPortInUse() error checking port ${port}: ${err.code}, assuming in use`).catch(() => { });
|
|
260
|
+
resolve(true);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
try {
|
|
264
|
+
socket.connect(port, 'localhost');
|
|
265
|
+
}
|
|
266
|
+
catch (connectError) {
|
|
267
|
+
debugLog('ChromeLauncher', `isPortInUse() socket.connect() threw for port ${port}: ${connectError}`).catch(() => { });
|
|
268
|
+
socket.destroy();
|
|
269
|
+
resolve(true); // Assume in use to be safe
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
debugLog('ChromeLauncher', `isPortInUse() unexpected error for port ${port}: ${error}`).catch(() => { });
|
|
274
|
+
resolve(true); // Assume in use to be safe
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Wait for Chrome debugging port to become ready
|
|
280
|
+
* Polls the /json/version endpoint until Chrome is inspectable
|
|
281
|
+
*/
|
|
282
|
+
async waitForChromeReady(port, maxAttempts = 15) {
|
|
283
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
284
|
+
try {
|
|
285
|
+
// Use a race between fetch and timeout
|
|
286
|
+
const fetchPromise = fetch(`http://localhost:${port}/json/version`);
|
|
287
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 1000));
|
|
288
|
+
const response = await Promise.race([fetchPromise, timeoutPromise]);
|
|
289
|
+
if (response.ok) {
|
|
290
|
+
// Validate it's actually Chrome responding, not the port reserver
|
|
291
|
+
const text = await response.text();
|
|
292
|
+
if (text.trim() === 'chrome-not-running') {
|
|
293
|
+
await debugLog('ChromeLauncher', `Port ${port} is reserved by PortReserver, Chrome failed to bind`);
|
|
294
|
+
throw new Error(`Port ${port} is reserved by another MCP instance. Chrome cannot bind to this port.`);
|
|
295
|
+
}
|
|
296
|
+
// Valid Chrome response
|
|
297
|
+
await debugLog('ChromeLauncher', `Chrome ready on port ${port} after ${i + 1} attempts`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
// Re-throw port reservation errors immediately
|
|
303
|
+
if (error instanceof Error && error.message.includes('reserved by another MCP instance')) {
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
// Chrome not ready yet, continue polling
|
|
307
|
+
// Only log every 5 attempts to reduce noise
|
|
308
|
+
if (i % 5 === 0) {
|
|
309
|
+
await debugLog('ChromeLauncher', `Waiting for Chrome on port ${port} (attempt ${i + 1}/${maxAttempts}) - error: ${error}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Exponential backoff: 500ms + (attempt * 200ms)
|
|
313
|
+
await new Promise(resolve => setTimeout(resolve, 500 + i * 200));
|
|
314
|
+
}
|
|
315
|
+
throw new Error(`Chrome debugging port ${port} failed to become inspectable within timeout. Chrome may have crashed during startup.`);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Launch Chrome with debugging enabled
|
|
319
|
+
* Uses atomic release-and-launch to prevent race conditions
|
|
320
|
+
* Waits for Chrome to actually bind to the port before resolving
|
|
321
|
+
*/
|
|
322
|
+
async launch(port = 9222, url, portReserver, headless = false, extraArgs = [], profileName) {
|
|
323
|
+
if (profileName === undefined) {
|
|
324
|
+
return this.launchOnPort(port, url, portReserver, headless, extraArgs, profileName);
|
|
325
|
+
}
|
|
326
|
+
// Named profiles are serialised by NAME as well as by port (bug-006 follow-up).
|
|
327
|
+
// The per-port lock does not help here: two launches for the same profile on
|
|
328
|
+
// different ports both pass the "is this profile held?" guard while the first
|
|
329
|
+
// Chrome is still spawning (findPortForProfile requires isRunning(), which is
|
|
330
|
+
// false during that window). Chrome then hands the second process off to the
|
|
331
|
+
// first singleton and it exits, surfacing as a generic spawn failure.
|
|
332
|
+
const name = normalizeProfileName(profileName);
|
|
333
|
+
for (let i = 0; i < 50; i++) {
|
|
334
|
+
const inFlight = this.profileLaunchLocks.get(name);
|
|
335
|
+
if (!inFlight)
|
|
336
|
+
break;
|
|
337
|
+
await debugLog('ChromeLauncher', `Another launch is in progress for profile "${name}", waiting...`);
|
|
338
|
+
// Its failure is its caller's problem, not ours - we just need it settled.
|
|
339
|
+
const result = await inFlight.then(r => r, () => undefined);
|
|
340
|
+
if (result && result.port === port && this.isRunning(port)) {
|
|
341
|
+
// Same port, same profile: this is the per-port hand-off case, so give
|
|
342
|
+
// both callers the one launch instead of "already running on port X".
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const launchPromise = this.launchOnPort(port, url, portReserver, headless, extraArgs, name);
|
|
347
|
+
this.profileLaunchLocks.set(name, launchPromise);
|
|
348
|
+
try {
|
|
349
|
+
return await launchPromise;
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
if (this.profileLaunchLocks.get(name) === launchPromise) {
|
|
353
|
+
this.profileLaunchLocks.delete(name);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Port-scoped half of launch(): the per-port lock, the "already running" and
|
|
359
|
+
* profile-ownership guards, and the spawn itself.
|
|
360
|
+
*/
|
|
361
|
+
async launchOnPort(port, url, portReserver, headless = false, extraArgs = [], profileName) {
|
|
362
|
+
await debugLog('ChromeLauncher', `launch() called with port ${port}, portReserver=${!!portReserver}, isReserved=${portReserver?.isReserved()}`);
|
|
363
|
+
// CRITICAL: Check if another launch is in progress for this port
|
|
364
|
+
// This prevents race conditions where two launch() calls happen simultaneously
|
|
365
|
+
const existingLaunch = this.launchLocks.get(port);
|
|
366
|
+
if (existingLaunch) {
|
|
367
|
+
await debugLog('ChromeLauncher', `Another launch is in progress for port ${port}, waiting for it to complete...`);
|
|
368
|
+
return existingLaunch; // Return the same promise, so both callers wait for the same launch
|
|
369
|
+
}
|
|
370
|
+
// Use isRunning() to verify the process is actually alive (handles external kills)
|
|
371
|
+
if (this.isRunning(port)) {
|
|
372
|
+
throw new Error(`Chrome is already running on port ${port}. Use killChrome() to stop it first, or specify a different port.`);
|
|
373
|
+
}
|
|
374
|
+
// Create a promise for this launch and store it in the lock map
|
|
375
|
+
// This prevents concurrent launches on the same port
|
|
376
|
+
// A named profile can only be held by one live Chrome - a second launch on
|
|
377
|
+
// the same user-data-dir is handed off to the first process and ours exits.
|
|
378
|
+
if (profileName !== undefined) {
|
|
379
|
+
const name = normalizeProfileName(profileName);
|
|
380
|
+
const holder = this.findPortForProfile(name);
|
|
381
|
+
if (holder !== undefined) {
|
|
382
|
+
throw new ProfileInUseError(name, holder);
|
|
383
|
+
}
|
|
384
|
+
// ...and the same profile may be held by a Chrome belonging to ANOTHER
|
|
385
|
+
// cdp-tools session (the persistent profile root is global by default),
|
|
386
|
+
// which our own maps know nothing about. Without this the launch "works",
|
|
387
|
+
// Chrome hands off to the existing singleton, and the caller gets an
|
|
388
|
+
// unexplained spawn failure. POSIX-only - see ProfileLockedError.
|
|
389
|
+
const lockPid = await this.findProfileLockHolder(name);
|
|
390
|
+
if (lockPid !== undefined) {
|
|
391
|
+
throw new ProfileLockedError(name, lockPid, this.getPersistentProfilePath(name));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const launchPromise = this.performLaunch(port, url, portReserver, headless, extraArgs, profileName);
|
|
395
|
+
this.launchLocks.set(port, launchPromise);
|
|
396
|
+
try {
|
|
397
|
+
const result = await launchPromise;
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
// Always clean up the lock when done (success or failure)
|
|
402
|
+
this.launchLocks.delete(port);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Internal method that performs the actual Chrome launch
|
|
407
|
+
* Separated from launch() to allow mutex/locking logic
|
|
408
|
+
*/
|
|
409
|
+
async performLaunch(port, url, portReserver, headless = false, extraArgs = [], profileName) {
|
|
410
|
+
await debugLog('ChromeLauncher', `performLaunch() starting for port ${port}`);
|
|
411
|
+
// Check if port is in use by something OTHER than our port reserver
|
|
412
|
+
// This prevents multiple Chrome instances from being launched on the same port
|
|
413
|
+
let isPortInUse;
|
|
414
|
+
try {
|
|
415
|
+
isPortInUse = await this.isPortInUse(port);
|
|
416
|
+
await debugLog('ChromeLauncher', `performLaunch() isPortInUse check completed: ${isPortInUse}`);
|
|
417
|
+
}
|
|
418
|
+
catch (portCheckError) {
|
|
419
|
+
await debugLog('ChromeLauncher', `performLaunch() isPortInUse check failed: ${portCheckError}`);
|
|
420
|
+
throw new Error(`Failed to check port availability: ${portCheckError}`);
|
|
421
|
+
}
|
|
422
|
+
const isOurReservation = portReserver && portReserver.isReserved() && portReserver.getPort() === port;
|
|
423
|
+
if (isPortInUse && !isOurReservation) {
|
|
424
|
+
// Port is in use by something else (not our reservation)
|
|
425
|
+
throw new Error(`Port ${port} is already in use by another process or MCP instance. Please choose a different port.`);
|
|
426
|
+
}
|
|
427
|
+
if (!isPortInUse && !isOurReservation) {
|
|
428
|
+
// Port is free but not reserved by us - this is fine, we can use it
|
|
429
|
+
await debugLog('ChromeLauncher', `Port ${port} is free and not reserved, proceeding with launch`);
|
|
430
|
+
}
|
|
431
|
+
const chromePath = this.getChromePath();
|
|
432
|
+
// Unique by construction: the port can only be held by one live Chrome at a
|
|
433
|
+
// time, and the random suffix covers same-millisecond relaunches on the same
|
|
434
|
+
// port. Using Date.now() alone let two concurrent launches on *different*
|
|
435
|
+
// ports share a profile (launchLocks only serialises per-port) - bug-006.
|
|
436
|
+
const profile = this.createProfileRecord(port, profileName);
|
|
437
|
+
const userDataDir = profile.dir;
|
|
438
|
+
this.profileDirs.set(port, profile);
|
|
439
|
+
await debugLog('ChromeLauncher', `Using ${profile.ephemeral ? 'ephemeral' : 'persistent'} profile ${userDataDir} for port ${port}`);
|
|
440
|
+
if (!profile.ephemeral) {
|
|
441
|
+
// First use of a named profile: the directory does not exist yet.
|
|
442
|
+
await fs.promises.mkdir(userDataDir, { recursive: true });
|
|
443
|
+
}
|
|
444
|
+
// Create Chrome preferences file to disable password manager popups
|
|
445
|
+
// This is more reliable than command-line flags alone
|
|
446
|
+
this.createChromePreferences(userDataDir, profile.ephemeral);
|
|
447
|
+
const args = [
|
|
448
|
+
`--remote-debugging-port=${port}`,
|
|
449
|
+
`--user-data-dir=${userDataDir}`,
|
|
450
|
+
'--no-first-run',
|
|
451
|
+
'--no-default-browser-check',
|
|
452
|
+
'--disable-popup-blocking',
|
|
453
|
+
'--disable-prompt-on-repost',
|
|
454
|
+
'--disable-background-timer-throttling',
|
|
455
|
+
'--disable-backgrounding-occluded-windows',
|
|
456
|
+
'--disable-renderer-backgrounding',
|
|
457
|
+
// Disable password manager and related popups that can block automation
|
|
458
|
+
'--password-store=basic',
|
|
459
|
+
'--use-mock-keychain',
|
|
460
|
+
'--disable-save-password-bubble',
|
|
461
|
+
'--disable-features=PasswordLeakDetection,PasswordCheck,PasswordImport,PasswordManagerOnboarding',
|
|
462
|
+
];
|
|
463
|
+
// Add headless mode if requested (prevents focus stealing)
|
|
464
|
+
if (headless) {
|
|
465
|
+
args.push('--headless=new'); // Use new headless mode
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
args.push('--start-minimized'); // Launch minimized to reduce focus stealing
|
|
469
|
+
}
|
|
470
|
+
// Pass-through: any extra Chrome flags from the launchChrome call (extraArgs)
|
|
471
|
+
// and/or the CDP_TOOLS_EXTRA_CHROME_ARGS env var (space-separated). Merged
|
|
472
|
+
// after the managed defaults and before the URL (Chrome treats the trailing
|
|
473
|
+
// positional arg as the page to open). Lets callers enable things like a fake
|
|
474
|
+
// camera (--use-fake-device-for-media-stream) without patching this file.
|
|
475
|
+
const envExtra = (process.env.CDP_TOOLS_EXTRA_CHROME_ARGS || '').split(/\s+/).filter(Boolean);
|
|
476
|
+
const passthrough = [...extraArgs, ...envExtra];
|
|
477
|
+
if (passthrough.length) {
|
|
478
|
+
await debugLog('ChromeLauncher', `Passing through extra Chrome args: ${passthrough.join(' ')}`);
|
|
479
|
+
args.push(...passthrough);
|
|
480
|
+
}
|
|
481
|
+
if (url) {
|
|
482
|
+
args.push(url);
|
|
483
|
+
}
|
|
484
|
+
// Track whether we released the reservation (so we can re-reserve on failure)
|
|
485
|
+
let didReleaseReservation = false;
|
|
486
|
+
try {
|
|
487
|
+
// ATOMIC OPERATION: Release port reservation immediately before spawning Chrome
|
|
488
|
+
// This minimizes the race condition window to just a few milliseconds
|
|
489
|
+
if (isOurReservation) {
|
|
490
|
+
await debugLog('ChromeLauncher', `Releasing port reservation for ${port} immediately before spawn`);
|
|
491
|
+
await portReserver.release();
|
|
492
|
+
didReleaseReservation = true;
|
|
493
|
+
await debugLog('ChromeLauncher', `Port ${port} released, spawning Chrome immediately...`);
|
|
494
|
+
}
|
|
495
|
+
await debugLog('ChromeLauncher', `Spawning Chrome process on port ${port}...`);
|
|
496
|
+
const chromeProcess = spawn(chromePath, args, {
|
|
497
|
+
stdio: 'ignore',
|
|
498
|
+
});
|
|
499
|
+
const pid = chromeProcess.pid;
|
|
500
|
+
await debugLog('ChromeLauncher', `Chrome process spawned with PID ${pid}`);
|
|
501
|
+
// CRITICAL FIX: Add to tracking map IMMEDIATELY after spawn to prevent orphans
|
|
502
|
+
// This ensures the process is tracked even if waitForChromeReady fails
|
|
503
|
+
this.chromeProcesses.set(port, chromeProcess);
|
|
504
|
+
await debugLog('ChromeLauncher', `Added Chrome process (PID: ${pid}) to tracking map for port ${port}`);
|
|
505
|
+
// Set up auto-cleanup when process exits (BEFORE waitForChromeReady)
|
|
506
|
+
// This ensures cleanup happens even if the launch fails later
|
|
507
|
+
chromeProcess.once('exit', (code, signal) => {
|
|
508
|
+
debugLog('ChromeLauncher', `Chrome process on port ${port} (PID: ${pid}) exited (code: ${code}, signal: ${signal}), removing from tracking`);
|
|
509
|
+
this.chromeProcesses.delete(port);
|
|
510
|
+
// Record the close event with reason
|
|
511
|
+
const pendingReason = this.pendingCloseReason.get(port);
|
|
512
|
+
this.pendingCloseReason.delete(port);
|
|
513
|
+
let reason;
|
|
514
|
+
if (pendingReason) {
|
|
515
|
+
reason = pendingReason;
|
|
516
|
+
}
|
|
517
|
+
else if (signal) {
|
|
518
|
+
reason = 'signal';
|
|
519
|
+
}
|
|
520
|
+
else if (code !== 0 && code !== null) {
|
|
521
|
+
reason = 'crash';
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
reason = 'external'; // Closed externally (user closed browser, etc.)
|
|
525
|
+
}
|
|
526
|
+
const closeEvent = this.recordCloseEvent(port, pid || -1, reason, code, signal);
|
|
527
|
+
// Remove the throwaway profile now that its Chrome is gone. Covers
|
|
528
|
+
// external closes and crashes as well as killInstance() - bug-007.
|
|
529
|
+
this.removeProfileDir(port, profile).catch(() => { });
|
|
530
|
+
// Invoke the exit callback if set (for port re-reservation)
|
|
531
|
+
if (this.onExitCallback) {
|
|
532
|
+
debugLog('ChromeLauncher', `Invoking onExit callback for port ${port}`);
|
|
533
|
+
// Don't await - fire and forget to avoid blocking exit handler
|
|
534
|
+
Promise.resolve(this.onExitCallback(closeEvent)).catch((err) => {
|
|
535
|
+
debugLog('ChromeLauncher', `onExit callback error: ${err}`);
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
// Handle process errors and unexpected exits
|
|
540
|
+
let processExited = false;
|
|
541
|
+
const exitHandler = () => {
|
|
542
|
+
processExited = true;
|
|
543
|
+
};
|
|
544
|
+
chromeProcess.once('exit', exitHandler);
|
|
545
|
+
chromeProcess.once('error', exitHandler);
|
|
546
|
+
// Wait for Chrome to actually start and bind to the port
|
|
547
|
+
await debugLog('ChromeLauncher', `Waiting for Chrome to become ready on port ${port}...`);
|
|
548
|
+
try {
|
|
549
|
+
await this.waitForChromeReady(port);
|
|
550
|
+
}
|
|
551
|
+
catch (waitError) {
|
|
552
|
+
await debugLog('ChromeLauncher', `waitForChromeReady failed: ${waitError}`);
|
|
553
|
+
// Re-reserve the port if we released it (cleanup after failure)
|
|
554
|
+
if (didReleaseReservation && portReserver) {
|
|
555
|
+
try {
|
|
556
|
+
await debugLog('ChromeLauncher', `Re-reserving port ${port} after Chrome launch failure`);
|
|
557
|
+
await portReserver.reserve(port);
|
|
558
|
+
await debugLog('ChromeLauncher', `Successfully re-reserved port ${port}`);
|
|
559
|
+
}
|
|
560
|
+
catch (reserveError) {
|
|
561
|
+
await debugLog('ChromeLauncher', `Failed to re-reserve port ${port}: ${reserveError}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// Clean up if Chrome failed to start - use SIGKILL for immediate termination
|
|
565
|
+
if (chromeProcess && !chromeProcess.killed && pid) {
|
|
566
|
+
await debugLog('ChromeLauncher', `Force killing orphaned Chrome process (PID: ${pid})`);
|
|
567
|
+
try {
|
|
568
|
+
chromeProcess.kill('SIGKILL');
|
|
569
|
+
// Wait a moment to ensure kill completes
|
|
570
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
571
|
+
}
|
|
572
|
+
catch (killError) {
|
|
573
|
+
await debugLog('ChromeLauncher', `Failed to kill orphaned process: ${killError}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
// Remove from tracking map since we're about to throw
|
|
577
|
+
// (exit handler will also remove it when process dies, but this is defensive)
|
|
578
|
+
this.chromeProcesses.delete(port);
|
|
579
|
+
await this.removeProfileDir(port, profile);
|
|
580
|
+
throw waitError;
|
|
581
|
+
}
|
|
582
|
+
// Check if process exited during startup
|
|
583
|
+
if (processExited) {
|
|
584
|
+
// Remove from tracking since process is dead
|
|
585
|
+
this.chromeProcesses.delete(port);
|
|
586
|
+
throw new Error('Chrome process exited unexpectedly during startup');
|
|
587
|
+
}
|
|
588
|
+
// Remove temporary exit handler now that Chrome is confirmed running
|
|
589
|
+
// (The permanent exit handler was already set up earlier)
|
|
590
|
+
chromeProcess.removeListener('exit', exitHandler);
|
|
591
|
+
chromeProcess.removeListener('error', exitHandler);
|
|
592
|
+
await debugLog('ChromeLauncher', `Chrome successfully started on port ${port} with PID ${pid}`);
|
|
593
|
+
return { port, pid: pid || -1 };
|
|
594
|
+
}
|
|
595
|
+
catch (error) {
|
|
596
|
+
await debugLog('ChromeLauncher', `Failed to launch Chrome: ${error}`);
|
|
597
|
+
// Re-reserve the port if we released it (cleanup after any failure)
|
|
598
|
+
if (didReleaseReservation && portReserver) {
|
|
599
|
+
try {
|
|
600
|
+
await debugLog('ChromeLauncher', `Re-reserving port ${port} after outer catch block failure`);
|
|
601
|
+
await portReserver.reserve(port);
|
|
602
|
+
await debugLog('ChromeLauncher', `Successfully re-reserved port ${port}`);
|
|
603
|
+
}
|
|
604
|
+
catch (reserveError) {
|
|
605
|
+
await debugLog('ChromeLauncher', `Failed to re-reserve port ${port}: ${reserveError}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
throw new Error(`Failed to launch Chrome: ${error}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Check if a process with the given PID is actually running
|
|
613
|
+
* This handles the case where Chrome is killed externally (e.g., Activity Monitor, kill command)
|
|
614
|
+
*/
|
|
615
|
+
isProcessAlive(pid) {
|
|
616
|
+
try {
|
|
617
|
+
// process.kill with signal 0 doesn't kill the process, just checks if it exists
|
|
618
|
+
process.kill(pid, 0);
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
// ESRCH = No such process, EPERM = exists but no permission (still alive)
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Check if Chrome is running on a specific port, or if any Chrome instance is running
|
|
628
|
+
* This verifies the process is actually alive, not just tracked
|
|
629
|
+
*/
|
|
630
|
+
isRunning(port) {
|
|
631
|
+
if (port !== undefined) {
|
|
632
|
+
const chromeProcess = this.chromeProcesses.get(port);
|
|
633
|
+
if (!chromeProcess || chromeProcess.killed) {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
// Verify the process is actually alive (handles external kills)
|
|
637
|
+
const pid = chromeProcess.pid;
|
|
638
|
+
if (!pid || !this.isProcessAlive(pid)) {
|
|
639
|
+
// Process is dead but we didn't know - clean up tracking
|
|
640
|
+
this.chromeProcesses.delete(port);
|
|
641
|
+
this.recordCloseEvent(port, pid || -1, 'external', null, null);
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
// Check if any Chrome instance is running - verify each one
|
|
647
|
+
for (const [p, chromeProcess] of this.chromeProcesses.entries()) {
|
|
648
|
+
const pid = chromeProcess.pid;
|
|
649
|
+
if (pid && this.isProcessAlive(pid)) {
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
// Clean up dead process
|
|
654
|
+
this.chromeProcesses.delete(p);
|
|
655
|
+
this.recordCloseEvent(p, pid || -1, 'external', null, null);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Get all running Chrome ports
|
|
662
|
+
*/
|
|
663
|
+
getRunningPorts() {
|
|
664
|
+
return Array.from(this.chromeProcesses.keys());
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Kill Chrome process(es)
|
|
668
|
+
* If port is specified, kills only that instance. Otherwise kills all instances.
|
|
669
|
+
* First attempts graceful shutdown with SIGTERM, then force kills with SIGKILL if needed
|
|
670
|
+
*/
|
|
671
|
+
async kill(port) {
|
|
672
|
+
if (port !== undefined) {
|
|
673
|
+
// Kill specific instance
|
|
674
|
+
await this.killInstance(port);
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
// Kill all instances
|
|
678
|
+
const ports = Array.from(this.chromeProcesses.keys());
|
|
679
|
+
await debugLog('ChromeLauncher', `Killing all Chrome instances (${ports.length} total)`);
|
|
680
|
+
for (const p of ports) {
|
|
681
|
+
await this.killInstance(p);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Kill a specific Chrome instance by port
|
|
687
|
+
*/
|
|
688
|
+
async killInstance(port) {
|
|
689
|
+
// Snapshot the profile we are killing *now*: a relaunch on this port can
|
|
690
|
+
// land between profileDirs.set() and chromeProcesses.set() in
|
|
691
|
+
// performLaunch(), and deleting whatever profile happens to be tracked by
|
|
692
|
+
// the time we get around to it would wipe the new launch's fresh dir.
|
|
693
|
+
// Passing the snapshot as `expected` makes every deletion below a no-op
|
|
694
|
+
// once someone else owns the port.
|
|
695
|
+
const profile = this.profileDirs.get(port);
|
|
696
|
+
const chromeProcess = this.chromeProcesses.get(port);
|
|
697
|
+
if (!chromeProcess || chromeProcess.killed) {
|
|
698
|
+
// Nothing to kill, but a profile may still be tracked (e.g. the process
|
|
699
|
+
// was reaped externally) - make sure it does not leak.
|
|
700
|
+
if (profile)
|
|
701
|
+
await this.removeProfileDir(port, profile);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const pid = chromeProcess.pid;
|
|
705
|
+
if (!pid) {
|
|
706
|
+
this.chromeProcesses.delete(port);
|
|
707
|
+
if (profile)
|
|
708
|
+
await this.removeProfileDir(port, profile);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
await debugLog('ChromeLauncher', `Killing Chrome process on port ${port} (PID: ${pid})`);
|
|
712
|
+
// Try graceful shutdown first (SIGTERM)
|
|
713
|
+
try {
|
|
714
|
+
chromeProcess.kill('SIGTERM');
|
|
715
|
+
await debugLog('ChromeLauncher', `Sent SIGTERM to Chrome on port ${port} (PID: ${pid})`);
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
await debugLog('ChromeLauncher', `Failed to send SIGTERM: ${error}`);
|
|
719
|
+
}
|
|
720
|
+
// Wait 500ms for graceful shutdown, then force kill if needed
|
|
721
|
+
await new Promise((resolve) => {
|
|
722
|
+
const timeout = setTimeout(() => {
|
|
723
|
+
try {
|
|
724
|
+
// Check if process still exists using signal 0 (doesn't actually kill)
|
|
725
|
+
process.kill(pid, 0);
|
|
726
|
+
// Process still exists, force kill
|
|
727
|
+
debugLog('ChromeLauncher', `Chrome on port ${port} didn't exit gracefully, sending SIGKILL (PID: ${pid})`);
|
|
728
|
+
try {
|
|
729
|
+
chromeProcess.kill('SIGKILL');
|
|
730
|
+
debugLog('ChromeLauncher', `Sent SIGKILL to Chrome on port ${port} (PID: ${pid})`);
|
|
731
|
+
}
|
|
732
|
+
catch (killError) {
|
|
733
|
+
debugLog('ChromeLauncher', `Failed to send SIGKILL: ${killError}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
// Process already dead (signal 0 threw error)
|
|
738
|
+
debugLog('ChromeLauncher', `Chrome on port ${port} exited gracefully (PID: ${pid})`);
|
|
739
|
+
}
|
|
740
|
+
resolve();
|
|
741
|
+
}, 500);
|
|
742
|
+
// If process exits before timeout, clear the timeout
|
|
743
|
+
chromeProcess.once('exit', () => {
|
|
744
|
+
clearTimeout(timeout);
|
|
745
|
+
debugLog('ChromeLauncher', `Chrome process on port ${port} exited (PID: ${pid})`);
|
|
746
|
+
resolve();
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
this.chromeProcesses.delete(port);
|
|
750
|
+
// Chrome is gone (or was force-killed) - drop its throwaway profile,
|
|
751
|
+
// unless a relaunch has already claimed this port (see snapshot above).
|
|
752
|
+
if (profile)
|
|
753
|
+
await this.removeProfileDir(port, profile);
|
|
754
|
+
await debugLog('ChromeLauncher', `Chrome cleanup complete for port ${port}`);
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Build the profile record for a launch on `port`.
|
|
758
|
+
*
|
|
759
|
+
* The name embeds the port (only one live Chrome can hold a debug port, so
|
|
760
|
+
* this alone separates concurrent launches) plus a timestamp and 4 random
|
|
761
|
+
* bytes (so sequential relaunches on the same port within one millisecond
|
|
762
|
+
* still differ). Naming on Date.now() alone let same-millisecond launches on
|
|
763
|
+
* different ports share a profile - bug-006.
|
|
764
|
+
*
|
|
765
|
+
* With `profileName` (issue 13) the record is a stable directory under the
|
|
766
|
+
* persistent profile root and is marked `ephemeral: false`, which every
|
|
767
|
+
* cleanup path skips. The port is deliberately NOT part of a named profile's
|
|
768
|
+
* directory: the profile is the identity, the port is just where this run of
|
|
769
|
+
* it happens to listen.
|
|
770
|
+
*/
|
|
771
|
+
createProfileRecord(port, profileName) {
|
|
772
|
+
if (profileName !== undefined) {
|
|
773
|
+
return { dir: this.getPersistentProfilePath(profileName), ephemeral: false };
|
|
774
|
+
}
|
|
775
|
+
const name = `${EPHEMERAL_PROFILE_PREFIX}p${port}-${Date.now()}-${randomBytes(4).toString('hex')}`;
|
|
776
|
+
return { dir: path.join(this.profileRoot, name), ephemeral: true };
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Directory named persistent profiles live under (see
|
|
780
|
+
* `chrome.persistentProfileRoot`; defaults to `~/.cdp-tools/profiles`).
|
|
781
|
+
*/
|
|
782
|
+
getPersistentProfileRoot() {
|
|
783
|
+
return typeof this.persistentProfileRootOption === 'function'
|
|
784
|
+
? this.persistentProfileRootOption()
|
|
785
|
+
: this.persistentProfileRootOption;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Absolute path of a named persistent profile. Does not create it.
|
|
789
|
+
* @throws InvalidProfileNameError
|
|
790
|
+
*/
|
|
791
|
+
getPersistentProfilePath(profileName) {
|
|
792
|
+
return path.join(this.getPersistentProfileRoot(), normalizeProfileName(profileName));
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Port of the live Chrome currently holding a named profile, if any.
|
|
796
|
+
* @throws InvalidProfileNameError
|
|
797
|
+
*/
|
|
798
|
+
findPortForProfile(profileName) {
|
|
799
|
+
const dir = this.getPersistentProfilePath(profileName);
|
|
800
|
+
for (const [port, record] of this.profileDirs.entries()) {
|
|
801
|
+
if (record.dir === dir && this.isRunning(port)) {
|
|
802
|
+
return port;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return undefined;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* PID of a live Chrome holding a named persistent profile, if the profile's
|
|
809
|
+
* SingletonLock says so. Unlike findPortForProfile() this sees Chromes we did
|
|
810
|
+
* NOT launch (other cdp-tools sessions sharing the global profile root, or a
|
|
811
|
+
* Chrome started by hand on the same user-data-dir).
|
|
812
|
+
*
|
|
813
|
+
* LIMITATION: POSIX-only. It reads the SingletonLock symlink target, which
|
|
814
|
+
* Windows Chrome does not create - there it always reports "not locked", so
|
|
815
|
+
* callers must treat a negative result as "no evidence of a holder" rather
|
|
816
|
+
* than proof the profile is free.
|
|
817
|
+
*
|
|
818
|
+
* @throws InvalidProfileNameError
|
|
819
|
+
*/
|
|
820
|
+
async findProfileLockHolder(profileName) {
|
|
821
|
+
const dir = this.getPersistentProfilePath(profileName);
|
|
822
|
+
return this.readProfileLockPid(dir);
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Names of persistent profiles that exist on disk.
|
|
826
|
+
*/
|
|
827
|
+
async listPersistentProfiles() {
|
|
828
|
+
try {
|
|
829
|
+
const entries = await fs.promises.readdir(this.getPersistentProfileRoot(), { withFileTypes: true });
|
|
830
|
+
return entries.filter(e => e.isDirectory()).map(e => e.name).sort();
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
return [];
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Wipe a named persistent profile and recreate it empty, so the next launch
|
|
838
|
+
* starts from a clean browser (no cookies, no IndexedDB, no enrolment).
|
|
839
|
+
*
|
|
840
|
+
* Refuses while a Chrome we launched still holds the profile: deleting a
|
|
841
|
+
* live user-data-dir corrupts the running browser and the deletion would be
|
|
842
|
+
* partly undone as Chrome flushes state back out on exit. Kill that instance
|
|
843
|
+
* (`killChrome({ port })`) first.
|
|
844
|
+
*
|
|
845
|
+
* Also refuses when the profile's Chrome SingletonLock names a live PID we
|
|
846
|
+
* did not launch. The default profile root is global (`~/.cdp-tools/profiles`),
|
|
847
|
+
* so another cdp-tools session may be running this exact profile; without the
|
|
848
|
+
* lock check this call would rm -rf a live browser's user-data-dir and destroy
|
|
849
|
+
* the very identity persistent profiles exist to preserve. That check is
|
|
850
|
+
* POSIX-only (see ProfileLockedError) - on Windows this remains unguarded.
|
|
851
|
+
*
|
|
852
|
+
* @throws InvalidProfileNameError | ProfileInUseError | ProfileLockedError
|
|
853
|
+
*/
|
|
854
|
+
async resetPersistentProfile(profileName) {
|
|
855
|
+
const name = normalizeProfileName(profileName);
|
|
856
|
+
const holder = this.findPortForProfile(name);
|
|
857
|
+
if (holder !== undefined) {
|
|
858
|
+
throw new ProfileInUseError(name, holder);
|
|
859
|
+
}
|
|
860
|
+
const lockPid = await this.findProfileLockHolder(name);
|
|
861
|
+
if (lockPid !== undefined) {
|
|
862
|
+
throw new ProfileLockedError(name, lockPid, this.getPersistentProfilePath(name));
|
|
863
|
+
}
|
|
864
|
+
const dir = this.getPersistentProfilePath(name);
|
|
865
|
+
const existed = fs.existsSync(dir);
|
|
866
|
+
await fs.promises.rm(dir, { recursive: true, force: true });
|
|
867
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
868
|
+
await debugLog('ChromeLauncher', `Reset persistent profile "${name}" at ${dir} (existed: ${existed})`);
|
|
869
|
+
return { profile: name, path: dir, existed };
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Profile directory currently tracked for a port (if any).
|
|
873
|
+
*/
|
|
874
|
+
getProfileDir(port) {
|
|
875
|
+
return this.profileDirs.get(port)?.dir;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* All tracked profiles, keyed by port. Persistent profiles are included but
|
|
879
|
+
* are never deleted by the launcher.
|
|
880
|
+
*/
|
|
881
|
+
getProfiles() {
|
|
882
|
+
return new Map(this.profileDirs);
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Stop tracking a port's profile and, if it is ephemeral, delete it from disk.
|
|
886
|
+
*
|
|
887
|
+
* `expected` guards against a late exit event from a previous Chrome deleting
|
|
888
|
+
* the freshly created profile of a relaunch on the same port: if the tracked
|
|
889
|
+
* record is no longer the one we started with, nothing is touched.
|
|
890
|
+
*/
|
|
891
|
+
async removeProfileDir(port, expected) {
|
|
892
|
+
const record = this.profileDirs.get(port);
|
|
893
|
+
if (!record) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (expected && record !== expected) {
|
|
897
|
+
// A newer launch owns this port now - leave its profile alone.
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
this.profileDirs.delete(port);
|
|
901
|
+
if (!record.ephemeral) {
|
|
902
|
+
// Persistent/named profile (issue 13) - tracking only, never delete.
|
|
903
|
+
await debugLog('ChromeLauncher', `Keeping persistent profile ${record.dir} for port ${port}`);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
try {
|
|
907
|
+
await fs.promises.rm(record.dir, { recursive: true, force: true });
|
|
908
|
+
await debugLog('ChromeLauncher', `Removed ephemeral profile ${record.dir} for port ${port}`);
|
|
909
|
+
}
|
|
910
|
+
catch (error) {
|
|
911
|
+
await debugLog('ChromeLauncher', `Failed to remove profile ${record.dir}: ${error}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Delete ephemeral profile directories left behind by previous sessions
|
|
916
|
+
* (crashes, SIGKILL, machine restart). Returns the directories removed.
|
|
917
|
+
*
|
|
918
|
+
* Conservative on purpose - a directory is skipped when:
|
|
919
|
+
* - it is younger than staleProfileMaxAgeMs (another MCP instance may be
|
|
920
|
+
* launching Chrome into it right now), or
|
|
921
|
+
* - its Chrome SingletonLock names a PID that is still alive, or
|
|
922
|
+
* - it is currently tracked by this launcher.
|
|
923
|
+
*/
|
|
924
|
+
async sweepStaleProfiles() {
|
|
925
|
+
const removed = [];
|
|
926
|
+
let entries;
|
|
927
|
+
try {
|
|
928
|
+
entries = await fs.promises.readdir(this.profileRoot, { withFileTypes: true });
|
|
929
|
+
}
|
|
930
|
+
catch (error) {
|
|
931
|
+
await debugLog('ChromeLauncher', `Profile sweep could not read ${this.profileRoot}: ${error}`);
|
|
932
|
+
return removed;
|
|
933
|
+
}
|
|
934
|
+
const inUse = new Set(Array.from(this.profileDirs.values()).map(r => r.dir));
|
|
935
|
+
const now = Date.now();
|
|
936
|
+
for (const entry of entries) {
|
|
937
|
+
if (!entry.isDirectory() || !entry.name.startsWith(EPHEMERAL_PROFILE_PREFIX)) {
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
const dir = path.join(this.profileRoot, entry.name);
|
|
941
|
+
if (inUse.has(dir)) {
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
try {
|
|
945
|
+
const stats = await fs.promises.stat(dir);
|
|
946
|
+
if (now - stats.mtimeMs < this.staleProfileMaxAgeMs) {
|
|
947
|
+
continue; // Too fresh - may belong to a launch in progress elsewhere
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
catch {
|
|
951
|
+
continue; // Vanished or unreadable - leave it alone
|
|
952
|
+
}
|
|
953
|
+
if (await this.isProfileLocked(dir)) {
|
|
954
|
+
await debugLog('ChromeLauncher', `Profile sweep skipping ${dir} - still locked by a live Chrome`);
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
try {
|
|
958
|
+
await fs.promises.rm(dir, { recursive: true, force: true });
|
|
959
|
+
removed.push(dir);
|
|
960
|
+
}
|
|
961
|
+
catch (error) {
|
|
962
|
+
await debugLog('ChromeLauncher', `Profile sweep failed to remove ${dir}: ${error}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (removed.length) {
|
|
966
|
+
await debugLog('ChromeLauncher', `Profile sweep removed ${removed.length} stale profile(s)`);
|
|
967
|
+
}
|
|
968
|
+
return removed;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Is a profile directory still held by a running Chrome?
|
|
972
|
+
* Chrome writes a SingletonLock symlink whose target is "<hostname>-<pid>".
|
|
973
|
+
* Unreadable/absent lock means "not locked" (Windows uses a plain lockfile).
|
|
974
|
+
*/
|
|
975
|
+
async isProfileLocked(dir) {
|
|
976
|
+
return (await this.readProfileLockPid(dir)) !== undefined;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* PID named by a profile directory's Chrome SingletonLock, if that process is
|
|
980
|
+
* still alive. POSIX-only: on Windows the lock is a plain file with no
|
|
981
|
+
* readable PID, so this always returns undefined ("no evidence of a holder").
|
|
982
|
+
*/
|
|
983
|
+
async readProfileLockPid(dir) {
|
|
984
|
+
try {
|
|
985
|
+
const target = await fs.promises.readlink(path.join(dir, 'SingletonLock'));
|
|
986
|
+
const pid = Number(target.split('-').pop());
|
|
987
|
+
return Number.isInteger(pid) && pid > 0 && this.isProcessAlive(pid) ? pid : undefined;
|
|
988
|
+
}
|
|
989
|
+
catch {
|
|
990
|
+
return undefined;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Reset the launcher state (useful if Chrome was closed externally)
|
|
995
|
+
*/
|
|
996
|
+
reset(port) {
|
|
997
|
+
// Note: profile dirs are only untracked here, never deleted - reset() is a
|
|
998
|
+
// "forget my state" escape hatch and the Chrome holding the profile may
|
|
999
|
+
// still be alive. Anything orphaned this way is picked up by the startup
|
|
1000
|
+
// sweep on the next run.
|
|
1001
|
+
if (port !== undefined) {
|
|
1002
|
+
this.chromeProcesses.delete(port);
|
|
1003
|
+
this.profileDirs.delete(port);
|
|
1004
|
+
}
|
|
1005
|
+
else {
|
|
1006
|
+
this.chromeProcesses.clear();
|
|
1007
|
+
this.profileDirs.clear();
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Record a Chrome close event
|
|
1012
|
+
*/
|
|
1013
|
+
recordCloseEvent(port, pid, reason, exitCode, signal) {
|
|
1014
|
+
const event = {
|
|
1015
|
+
port,
|
|
1016
|
+
pid,
|
|
1017
|
+
reason,
|
|
1018
|
+
timestamp: new Date(),
|
|
1019
|
+
exitCode,
|
|
1020
|
+
signal,
|
|
1021
|
+
};
|
|
1022
|
+
this.lastCloseEvents.push(event);
|
|
1023
|
+
// Keep only the last N events
|
|
1024
|
+
if (this.lastCloseEvents.length > this.maxCloseEvents) {
|
|
1025
|
+
this.lastCloseEvents.shift();
|
|
1026
|
+
}
|
|
1027
|
+
debugLog('ChromeLauncher', `Recorded close event: port=${port}, pid=${pid}, reason=${reason}, exitCode=${exitCode}, signal=${signal}`);
|
|
1028
|
+
return event;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Get the last close events
|
|
1032
|
+
*/
|
|
1033
|
+
getLastCloseEvents() {
|
|
1034
|
+
return [...this.lastCloseEvents];
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Get Chrome launcher status for all instances
|
|
1038
|
+
* Verifies each process is actually alive and cleans up dead ones
|
|
1039
|
+
*/
|
|
1040
|
+
getStatus() {
|
|
1041
|
+
const instances = [];
|
|
1042
|
+
const deadPorts = [];
|
|
1043
|
+
for (const [port, chromeProcess] of this.chromeProcesses.entries()) {
|
|
1044
|
+
const pid = chromeProcess.pid || -1;
|
|
1045
|
+
const alive = pid > 0 && this.isProcessAlive(pid);
|
|
1046
|
+
if (!alive) {
|
|
1047
|
+
deadPorts.push(port);
|
|
1048
|
+
}
|
|
1049
|
+
instances.push({ port, pid, running: alive });
|
|
1050
|
+
}
|
|
1051
|
+
// Clean up dead processes
|
|
1052
|
+
for (const port of deadPorts) {
|
|
1053
|
+
const chromeProcess = this.chromeProcesses.get(port);
|
|
1054
|
+
const pid = chromeProcess?.pid || -1;
|
|
1055
|
+
this.chromeProcesses.delete(port);
|
|
1056
|
+
this.recordCloseEvent(port, pid, 'external', null, null);
|
|
1057
|
+
}
|
|
1058
|
+
// Return only alive instances
|
|
1059
|
+
return {
|
|
1060
|
+
instances: instances.filter(i => i.running),
|
|
1061
|
+
lastCloseEvents: this.lastCloseEvents
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Set the pending close reason for a port (call before killing)
|
|
1066
|
+
*/
|
|
1067
|
+
setPendingCloseReason(port, reason) {
|
|
1068
|
+
this.pendingCloseReason.set(port, reason);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
//# sourceMappingURL=chrome-launcher.js.map
|