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,1674 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Manager
|
|
3
|
+
* Manages development servers (any language/framework) - start, stop, restart, and monitor
|
|
4
|
+
* Supports multiple runner types: native (spawn), docker, docker-compose
|
|
5
|
+
* Persists state to .cdp-tools/servers.json for recovery and auto-run
|
|
6
|
+
* Logs to .cdp-tools/logs/<server-id>/ for cross-MCP access (native runner only)
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as net from 'net';
|
|
10
|
+
import { debugLog } from './debug-logger.js';
|
|
11
|
+
import { getOutputPath } from './helpers/paths.js';
|
|
12
|
+
import { atomicWriteFile } from './atomic-write.js';
|
|
13
|
+
import { configManager } from './config.js';
|
|
14
|
+
import { ServerFileWatcher } from './server-watcher.js';
|
|
15
|
+
import { serverClaims } from './server-claims.js';
|
|
16
|
+
import { detectRunnerType, createRunner, resetDockerCaches, NativeRunner, DockerRunner, DockerComposeRunner, } from './runners/index.js';
|
|
17
|
+
/**
|
|
18
|
+
* Patterns for commands that auto-restart their own process on file changes
|
|
19
|
+
* (Node's --watch, nodemon, etc.). Pairing one of these with an attached CDP
|
|
20
|
+
* debugger on the same process is a known-bad combination: the auto-restart
|
|
21
|
+
* supervisor races with a paused (frozen) process, which can produce
|
|
22
|
+
* EADDRINUSE crash-loops and ambiguous "failed but still listening" states,
|
|
23
|
+
* since the restart happens entirely outside anything cdp-tools tracks.
|
|
24
|
+
*/
|
|
25
|
+
const AUTO_RESTART_PATTERNS = [
|
|
26
|
+
{ name: '--watch', regex: /(^|\s)--watch(\b|=)/ },
|
|
27
|
+
{ name: 'nodemon', regex: /\bnodemon\b/ },
|
|
28
|
+
{ name: 'tsx watch', regex: /\btsx\s+watch\b/ },
|
|
29
|
+
{ name: 'ts-node-dev', regex: /\bts-node-dev\b/ },
|
|
30
|
+
{ name: 'node-dev', regex: /\bnode-dev\b/ },
|
|
31
|
+
{ name: 'bun --hot/--watch', regex: /\bbun\b.*(--hot\b|--watch\b)/ },
|
|
32
|
+
{ name: 'deno run --watch', regex: /\bdeno\s+run\b.*--watch\b/ },
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Returns the matched auto-restart pattern name if `command` looks like it
|
|
36
|
+
* self-restarts on file changes, or null otherwise.
|
|
37
|
+
*/
|
|
38
|
+
export function detectAutoRestartCommand(command) {
|
|
39
|
+
for (const { name, regex } of AUTO_RESTART_PATTERNS) {
|
|
40
|
+
if (regex.test(command)) {
|
|
41
|
+
return name;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Extract the Node inspector port from a `--inspect`/`--inspect-brk` flag in
|
|
48
|
+
* a command string, if present. Falls back to Node's default port (9229)
|
|
49
|
+
* when the flag is given with no explicit port. Does not handle a separate
|
|
50
|
+
* `--inspect-port=` flag overriding a bare `--inspect`'s port - a rare
|
|
51
|
+
* combination not worth the extra complexity here.
|
|
52
|
+
*/
|
|
53
|
+
export function extractInspectorPort(command) {
|
|
54
|
+
const match = command.match(/--inspect(?:-brk)?(?:=(?:[\w.]+:)?(\d+))?/);
|
|
55
|
+
if (!match) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return match[1] ? parseInt(match[1], 10) : 9229;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Validate server ID to prevent security issues.
|
|
62
|
+
* Server IDs are used in container names, file paths, and project names.
|
|
63
|
+
* Only alphanumeric characters, dashes, and underscores are allowed.
|
|
64
|
+
*/
|
|
65
|
+
function validateServerId(id) {
|
|
66
|
+
if (!id || id.length === 0) {
|
|
67
|
+
throw new Error('Server ID cannot be empty');
|
|
68
|
+
}
|
|
69
|
+
if (id.length > 64) {
|
|
70
|
+
throw new Error('Server ID cannot exceed 64 characters');
|
|
71
|
+
}
|
|
72
|
+
// Only allow alphanumeric, dash, underscore
|
|
73
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
74
|
+
throw new Error(`Invalid server ID "${id}". Server IDs can only contain letters, numbers, dashes, and underscores.`);
|
|
75
|
+
}
|
|
76
|
+
// Cannot start with a dash (would cause issues with CLI args)
|
|
77
|
+
if (id.startsWith('-')) {
|
|
78
|
+
throw new Error('Server ID cannot start with a dash');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Port Monitor - monitors ports using persistent TCP connections
|
|
83
|
+
*/
|
|
84
|
+
export class PortMonitor {
|
|
85
|
+
ports = new Map();
|
|
86
|
+
onFailureCallback;
|
|
87
|
+
getIntervalForLevel;
|
|
88
|
+
// Refcount so multiple paused CDP connections can share one PortMonitor safely:
|
|
89
|
+
// monitoring only actually stops on the first pause and only actually resumes
|
|
90
|
+
// once every pause has a matching resume.
|
|
91
|
+
pauseCount = 0;
|
|
92
|
+
constructor(getIntervalForLevel) {
|
|
93
|
+
// Default intervals if no config provided
|
|
94
|
+
this.getIntervalForLevel = getIntervalForLevel ?? ((level) => {
|
|
95
|
+
switch (level) {
|
|
96
|
+
case 'block': return 1000;
|
|
97
|
+
case 'error': return 2000;
|
|
98
|
+
case 'inform': return 5000;
|
|
99
|
+
default: return 2000;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
onFailure(callback) {
|
|
104
|
+
this.onFailureCallback = callback;
|
|
105
|
+
}
|
|
106
|
+
async startMonitoring(port, level, description, interval) {
|
|
107
|
+
const existing = this.ports.get(port);
|
|
108
|
+
if (existing) {
|
|
109
|
+
existing.level = level;
|
|
110
|
+
existing.description = description;
|
|
111
|
+
existing.interval = interval;
|
|
112
|
+
if (existing.status === 'down' && existing.acknowledged) {
|
|
113
|
+
existing.acknowledged = false;
|
|
114
|
+
}
|
|
115
|
+
// Self-heal: if this port's monitor is dormant (no active socket and no
|
|
116
|
+
// pending reconnect - e.g. left stuck by an imbalanced pause/resume),
|
|
117
|
+
// reconnect it rather than silently staying dead.
|
|
118
|
+
if (!existing.socket && !existing.reconnectTimer) {
|
|
119
|
+
this.connectToPort(port);
|
|
120
|
+
}
|
|
121
|
+
await debugLog('PortMonitor', `Updated monitoring for port ${port}: level=${level}, interval=${interval ?? 'default'}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const monitored = {
|
|
125
|
+
port,
|
|
126
|
+
level,
|
|
127
|
+
description,
|
|
128
|
+
interval,
|
|
129
|
+
status: 'connecting',
|
|
130
|
+
socket: null,
|
|
131
|
+
acknowledged: false,
|
|
132
|
+
};
|
|
133
|
+
this.ports.set(port, monitored);
|
|
134
|
+
await debugLog('PortMonitor', `Starting monitoring for port ${port} (level: ${level}, interval: ${interval ?? 'default'})`);
|
|
135
|
+
this.connectToPort(port);
|
|
136
|
+
}
|
|
137
|
+
connectToPort(port) {
|
|
138
|
+
const monitored = this.ports.get(port);
|
|
139
|
+
if (!monitored)
|
|
140
|
+
return;
|
|
141
|
+
if (monitored.reconnectTimer) {
|
|
142
|
+
clearTimeout(monitored.reconnectTimer);
|
|
143
|
+
monitored.reconnectTimer = undefined;
|
|
144
|
+
}
|
|
145
|
+
if (monitored.socket) {
|
|
146
|
+
monitored.socket.removeAllListeners();
|
|
147
|
+
monitored.socket.destroy();
|
|
148
|
+
monitored.socket = null;
|
|
149
|
+
}
|
|
150
|
+
if (monitored.status !== 'up') {
|
|
151
|
+
monitored.status = 'connecting';
|
|
152
|
+
}
|
|
153
|
+
const socket = new net.Socket();
|
|
154
|
+
socket.setTimeout(5000);
|
|
155
|
+
let hadError = false;
|
|
156
|
+
socket.on('connect', async () => {
|
|
157
|
+
const wasDown = monitored.status === 'down' || monitored.status === 'connecting';
|
|
158
|
+
monitored.status = 'up';
|
|
159
|
+
monitored.socket = socket;
|
|
160
|
+
monitored.failedAt = undefined;
|
|
161
|
+
monitored.acknowledged = false;
|
|
162
|
+
if (wasDown) {
|
|
163
|
+
await debugLog('PortMonitor', `Port ${port} is UP`);
|
|
164
|
+
}
|
|
165
|
+
socket.destroy();
|
|
166
|
+
});
|
|
167
|
+
socket.on('close', async () => {
|
|
168
|
+
monitored.socket = null;
|
|
169
|
+
if (!hadError) {
|
|
170
|
+
this.scheduleReconnect(port);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
socket.on('error', async (err) => {
|
|
174
|
+
hadError = true;
|
|
175
|
+
const wasUp = monitored.status === 'up';
|
|
176
|
+
if (err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT' || err.code === 'ENOTFOUND') {
|
|
177
|
+
if (wasUp || monitored.status === 'connecting') {
|
|
178
|
+
monitored.status = 'down';
|
|
179
|
+
monitored.failedAt = monitored.failedAt || new Date();
|
|
180
|
+
monitored.socket = null;
|
|
181
|
+
if (wasUp) {
|
|
182
|
+
await debugLog('PortMonitor', `Port ${port} went DOWN: ${err.message}`);
|
|
183
|
+
if (this.onFailureCallback) {
|
|
184
|
+
this.onFailureCallback(port, monitored.level);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
await debugLog('PortMonitor', `Port ${port} is down: ${err.message}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
await debugLog('PortMonitor', `Port ${port} connection error (ignoring): ${err.message}`);
|
|
194
|
+
}
|
|
195
|
+
this.scheduleReconnect(port);
|
|
196
|
+
});
|
|
197
|
+
socket.on('timeout', () => {
|
|
198
|
+
hadError = true;
|
|
199
|
+
socket.destroy();
|
|
200
|
+
if (monitored.status === 'connecting' || monitored.status === 'up') {
|
|
201
|
+
const wasUp = monitored.status === 'up';
|
|
202
|
+
monitored.status = 'down';
|
|
203
|
+
monitored.failedAt = monitored.failedAt || new Date();
|
|
204
|
+
if (wasUp) {
|
|
205
|
+
debugLog('PortMonitor', `Port ${port} went DOWN: connection timeout`);
|
|
206
|
+
if (this.onFailureCallback) {
|
|
207
|
+
this.onFailureCallback(port, monitored.level);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
this.scheduleReconnect(port);
|
|
212
|
+
});
|
|
213
|
+
socket.connect(port, 'localhost');
|
|
214
|
+
}
|
|
215
|
+
scheduleReconnect(port) {
|
|
216
|
+
const monitored = this.ports.get(port);
|
|
217
|
+
if (!monitored || monitored.reconnectTimer)
|
|
218
|
+
return;
|
|
219
|
+
// Use per-port interval if set, otherwise get from config based on level
|
|
220
|
+
const interval = monitored.interval ?? this.getIntervalForLevel(monitored.level);
|
|
221
|
+
monitored.reconnectTimer = setTimeout(() => {
|
|
222
|
+
monitored.reconnectTimer = undefined;
|
|
223
|
+
if (this.ports.has(port)) {
|
|
224
|
+
this.connectToPort(port);
|
|
225
|
+
}
|
|
226
|
+
}, interval);
|
|
227
|
+
}
|
|
228
|
+
async stopMonitoring(port) {
|
|
229
|
+
const monitored = this.ports.get(port);
|
|
230
|
+
if (!monitored)
|
|
231
|
+
return false;
|
|
232
|
+
if (monitored.socket) {
|
|
233
|
+
monitored.socket.removeAllListeners();
|
|
234
|
+
monitored.socket.destroy();
|
|
235
|
+
}
|
|
236
|
+
if (monitored.reconnectTimer) {
|
|
237
|
+
clearTimeout(monitored.reconnectTimer);
|
|
238
|
+
}
|
|
239
|
+
this.ports.delete(port);
|
|
240
|
+
await debugLog('PortMonitor', `Stopped monitoring port ${port}`);
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
async acknowledgeFailure(port) {
|
|
244
|
+
const monitored = this.ports.get(port);
|
|
245
|
+
if (!monitored || monitored.status !== 'down')
|
|
246
|
+
return false;
|
|
247
|
+
monitored.acknowledged = true;
|
|
248
|
+
await debugLog('PortMonitor', `Acknowledged failure for port ${port}`);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
getFailedPorts() {
|
|
252
|
+
const failed = [];
|
|
253
|
+
for (const [port, monitored] of this.ports) {
|
|
254
|
+
if (monitored.status === 'down' && !monitored.acknowledged && monitored.failedAt) {
|
|
255
|
+
failed.push({
|
|
256
|
+
port,
|
|
257
|
+
level: monitored.level,
|
|
258
|
+
description: monitored.description,
|
|
259
|
+
failedAt: monitored.failedAt,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return failed;
|
|
264
|
+
}
|
|
265
|
+
getFailedPortsByLevel(level) {
|
|
266
|
+
return this.getFailedPorts().filter(p => p.level === level);
|
|
267
|
+
}
|
|
268
|
+
hasBlockingFailures() {
|
|
269
|
+
return this.getFailedPortsByLevel('block').length > 0;
|
|
270
|
+
}
|
|
271
|
+
isMonitoring(port) {
|
|
272
|
+
return this.ports.has(port);
|
|
273
|
+
}
|
|
274
|
+
getStatus(port) {
|
|
275
|
+
if (port !== undefined) {
|
|
276
|
+
const monitored = this.ports.get(port);
|
|
277
|
+
if (!monitored)
|
|
278
|
+
return [];
|
|
279
|
+
return [{
|
|
280
|
+
port: monitored.port,
|
|
281
|
+
level: monitored.level,
|
|
282
|
+
description: monitored.description,
|
|
283
|
+
interval: monitored.interval,
|
|
284
|
+
status: monitored.status,
|
|
285
|
+
failedAt: monitored.failedAt,
|
|
286
|
+
acknowledged: monitored.acknowledged,
|
|
287
|
+
}];
|
|
288
|
+
}
|
|
289
|
+
return Array.from(this.ports.values()).map(m => ({
|
|
290
|
+
port: m.port,
|
|
291
|
+
level: m.level,
|
|
292
|
+
description: m.description,
|
|
293
|
+
interval: m.interval,
|
|
294
|
+
status: m.status,
|
|
295
|
+
failedAt: m.failedAt,
|
|
296
|
+
acknowledged: m.acknowledged,
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
299
|
+
getPersistedState() {
|
|
300
|
+
return Array.from(this.ports.values()).map(m => ({
|
|
301
|
+
port: m.port,
|
|
302
|
+
level: m.level,
|
|
303
|
+
description: m.description,
|
|
304
|
+
interval: m.interval,
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
async restoreFromState(ports) {
|
|
308
|
+
for (const p of ports) {
|
|
309
|
+
await this.startMonitoring(p.port, p.level, p.description, p.interval);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async stopAll() {
|
|
313
|
+
for (const port of this.ports.keys()) {
|
|
314
|
+
await this.stopMonitoring(port);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Pause all port monitoring (e.g., when paused at a breakpoint)
|
|
319
|
+
* Stops reconnection attempts but preserves port state
|
|
320
|
+
*/
|
|
321
|
+
pauseMonitoring() {
|
|
322
|
+
this.pauseCount++;
|
|
323
|
+
if (this.pauseCount > 1) {
|
|
324
|
+
// Another connection already has monitoring paused; nothing new to do.
|
|
325
|
+
debugLog('PortMonitor', `Port monitoring pause count now ${this.pauseCount}`);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
for (const monitored of this.ports.values()) {
|
|
329
|
+
// Clear any pending reconnect timers
|
|
330
|
+
if (monitored.reconnectTimer) {
|
|
331
|
+
clearTimeout(monitored.reconnectTimer);
|
|
332
|
+
monitored.reconnectTimer = undefined;
|
|
333
|
+
}
|
|
334
|
+
// Close any active socket connections
|
|
335
|
+
if (monitored.socket) {
|
|
336
|
+
monitored.socket.removeAllListeners();
|
|
337
|
+
monitored.socket.destroy();
|
|
338
|
+
monitored.socket = null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
debugLog('PortMonitor', 'Port monitoring paused');
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Resume all port monitoring (e.g., when resuming from a breakpoint)
|
|
345
|
+
* Restarts connection attempts for all monitored ports, once every
|
|
346
|
+
* outstanding pauseMonitoring() call has a matching resumeMonitoring().
|
|
347
|
+
*/
|
|
348
|
+
resumeMonitoring() {
|
|
349
|
+
if (this.pauseCount === 0) {
|
|
350
|
+
// Unbalanced resume (shouldn't normally happen) - nothing to do.
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
this.pauseCount--;
|
|
354
|
+
if (this.pauseCount > 0) {
|
|
355
|
+
debugLog('PortMonitor', `Port monitoring still paused by ${this.pauseCount} other connection(s)`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
for (const port of this.ports.keys()) {
|
|
359
|
+
this.connectToPort(port);
|
|
360
|
+
}
|
|
361
|
+
debugLog('PortMonitor', 'Port monitoring resumed');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// ============================================================================
|
|
365
|
+
// Server Manager
|
|
366
|
+
// ============================================================================
|
|
367
|
+
export class ServerManager {
|
|
368
|
+
servers = new Map();
|
|
369
|
+
portMonitor = null;
|
|
370
|
+
pendingStartups = new Map();
|
|
371
|
+
/** Mutex to serialize saveState calls - prevents concurrent file writes */
|
|
372
|
+
saveMutex = Promise.resolve();
|
|
373
|
+
/** Injected from index.ts - decouples ServerManager from ConnectionManager directly */
|
|
374
|
+
pauseChecker = null;
|
|
375
|
+
/** Watch-mode restart coordination, keyed by serverId - see WatchRestartState */
|
|
376
|
+
watchState = new Map();
|
|
377
|
+
/** Ownership claims - who may stop a shared dev server (see server-claims.ts) */
|
|
378
|
+
claims;
|
|
379
|
+
constructor(claimsStore = serverClaims) {
|
|
380
|
+
this.claims = claimsStore;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Set the function used to check whether a CDP connection at a given
|
|
384
|
+
* inspector port is currently paused at a breakpoint. Mirrors the existing
|
|
385
|
+
* setChromeLauncher() injection pattern.
|
|
386
|
+
*/
|
|
387
|
+
setPauseChecker(fn) {
|
|
388
|
+
this.pauseChecker = fn;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Get the port monitor instance (lazy-initialized)
|
|
392
|
+
* Must be called after configManager.load() for config values to be used
|
|
393
|
+
*/
|
|
394
|
+
getPortMonitor() {
|
|
395
|
+
if (!this.portMonitor) {
|
|
396
|
+
// Lazy init - ensures config is loaded before we read intervals
|
|
397
|
+
this.portMonitor = new PortMonitor((level) => configManager.getIntervalForLevel(level));
|
|
398
|
+
}
|
|
399
|
+
return this.portMonitor;
|
|
400
|
+
}
|
|
401
|
+
getServersFilePath(global) {
|
|
402
|
+
return getOutputPath('servers.json', { global: global ?? false });
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Initialize - load state and recover/start auto-run servers
|
|
406
|
+
* Loads from both local (project) and global (~/.cdp-tools/) storage
|
|
407
|
+
*/
|
|
408
|
+
async initialize() {
|
|
409
|
+
const recovered = [];
|
|
410
|
+
const started = [];
|
|
411
|
+
const failed = [];
|
|
412
|
+
const monitoredPorts = [];
|
|
413
|
+
// Load from both local and global storage
|
|
414
|
+
const localPersisted = await this.loadState(false);
|
|
415
|
+
const globalPersisted = await this.loadState(true);
|
|
416
|
+
// Combine servers from both sources (local servers marked with global=false, global with global=true)
|
|
417
|
+
const allServers = [
|
|
418
|
+
...localPersisted.servers.map(s => ({ ...s, global: false })),
|
|
419
|
+
...globalPersisted.servers.map(s => ({ ...s, global: true })),
|
|
420
|
+
];
|
|
421
|
+
// Restore monitored ports from local config
|
|
422
|
+
if (localPersisted.monitoredPorts.length > 0) {
|
|
423
|
+
await this.getPortMonitor().restoreFromState(localPersisted.monitoredPorts);
|
|
424
|
+
monitoredPorts.push(...localPersisted.monitoredPorts.map(p => p.port));
|
|
425
|
+
await debugLog('ServerManager', `Restored ${monitoredPorts.length} monitored ports`);
|
|
426
|
+
}
|
|
427
|
+
// Restore pending startups from both local and global
|
|
428
|
+
const allPendingStartups = [
|
|
429
|
+
...localPersisted.pendingStartups,
|
|
430
|
+
...globalPersisted.pendingStartups,
|
|
431
|
+
];
|
|
432
|
+
for (const persisted of allPendingStartups) {
|
|
433
|
+
const pending = {
|
|
434
|
+
serverId: persisted.serverId,
|
|
435
|
+
startedAt: new Date(persisted.startedAt),
|
|
436
|
+
timeoutAt: new Date(persisted.timeoutAt),
|
|
437
|
+
acknowledged: persisted.acknowledged,
|
|
438
|
+
reason: persisted.reason,
|
|
439
|
+
};
|
|
440
|
+
this.pendingStartups.set(persisted.serverId, pending);
|
|
441
|
+
await debugLog('ServerManager', `Restored pending startup: ${persisted.serverId}`);
|
|
442
|
+
}
|
|
443
|
+
// Reset Docker caches before processing servers - ensures we check Docker availability
|
|
444
|
+
// fresh on each startup, catching cases where Docker stopped since last run
|
|
445
|
+
resetDockerCaches();
|
|
446
|
+
// Read before the loop below claims anything: a server we are about to
|
|
447
|
+
// claim during recovery would otherwise look owned by us and never be
|
|
448
|
+
// collected, no matter how long ago its real owners died (issue #139).
|
|
449
|
+
const abandoned = this.findAbandonedServerIds();
|
|
450
|
+
const collected = [];
|
|
451
|
+
for (const server of allServers) {
|
|
452
|
+
const runnerType = server.type || 'native';
|
|
453
|
+
const runner = createRunner(runnerType, server.id);
|
|
454
|
+
// Restore runner state using the interface method (pass PersistedRunnerState directly)
|
|
455
|
+
runner.restore(server);
|
|
456
|
+
const isRunning = await runner.isRunning();
|
|
457
|
+
if (isRunning) {
|
|
458
|
+
const managed = {
|
|
459
|
+
id: server.id,
|
|
460
|
+
runner,
|
|
461
|
+
autoRun: server.autoRun,
|
|
462
|
+
monitorPort: server.monitorPort,
|
|
463
|
+
global: server.global,
|
|
464
|
+
watch: server.watch,
|
|
465
|
+
watchPaths: server.watchPaths,
|
|
466
|
+
};
|
|
467
|
+
this.servers.set(server.id, managed);
|
|
468
|
+
// The process survived, but any watcher was tied to the previous
|
|
469
|
+
// ServerManager instance (e.g. before devharness's own supervisor
|
|
470
|
+
// restarted it) - re-establish it now.
|
|
471
|
+
if (server.watch) {
|
|
472
|
+
this.startWatcher(managed, server.watchPaths && server.watchPaths.length > 0 ? server.watchPaths : [server.cwd]);
|
|
473
|
+
}
|
|
474
|
+
// For native runner, init cursor to EOF (native-specific method)
|
|
475
|
+
if (runner.type === 'native' && 'initializeCursorToEOF' in runner) {
|
|
476
|
+
await runner.initializeCursorToEOF();
|
|
477
|
+
}
|
|
478
|
+
// Every session that ever claimed this server is gone, so nobody is
|
|
479
|
+
// coming back for it - this is the closed-window leak, collected at
|
|
480
|
+
// the first opportunity rather than left running until reboot.
|
|
481
|
+
// Another window on this project may be using it even though it never
|
|
482
|
+
// claimed it - only the last session out collects.
|
|
483
|
+
if (abandoned.get(server.global ?? false)?.has(server.id) &&
|
|
484
|
+
!this.claims.hasOtherLiveSessionIn(server.cwd)) {
|
|
485
|
+
try {
|
|
486
|
+
await debugLog('ServerClaims', `Collecting abandoned server ${server.id}`);
|
|
487
|
+
await this.stopServer(server.id);
|
|
488
|
+
collected.push(server.id);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
await debugLog('ServerClaims', `Failed to collect ${server.id}: ${error}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// Reattaching is use: this session now depends on the server, so it
|
|
496
|
+
// takes a claim alongside whoever else already holds one.
|
|
497
|
+
await this.claims.claim(server.id, server.cwd, server.global ?? false);
|
|
498
|
+
recovered.push(server.id);
|
|
499
|
+
await debugLog('ServerManager', `Recovered running server: ${server.id} (${runnerType}, global=${server.global})`);
|
|
500
|
+
}
|
|
501
|
+
else if (server.autoRun) {
|
|
502
|
+
// Not running but autoRun - restart
|
|
503
|
+
let success = false;
|
|
504
|
+
const maxRetries = 3;
|
|
505
|
+
for (let attempt = 1; attempt <= maxRetries && !success; attempt++) {
|
|
506
|
+
try {
|
|
507
|
+
if (server.port) {
|
|
508
|
+
await this.waitForPortRelease(server.port, 5);
|
|
509
|
+
}
|
|
510
|
+
await this.startServer({
|
|
511
|
+
command: server.command,
|
|
512
|
+
cwd: server.cwd,
|
|
513
|
+
id: server.id,
|
|
514
|
+
autoRun: true,
|
|
515
|
+
runner: runnerType,
|
|
516
|
+
monitorPort: server.monitorPort,
|
|
517
|
+
global: server.global,
|
|
518
|
+
watch: server.watch,
|
|
519
|
+
watchPaths: server.watchPaths,
|
|
520
|
+
});
|
|
521
|
+
started.push(server.id);
|
|
522
|
+
success = true;
|
|
523
|
+
await debugLog('ServerManager', `Auto-started server: ${server.id} (attempt ${attempt})`);
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
await debugLog('ServerManager', `Auto-start attempt ${attempt}/${maxRetries} failed for ${server.id}: ${err}`);
|
|
527
|
+
if (attempt < maxRetries) {
|
|
528
|
+
await new Promise(resolve => setTimeout(resolve, 500 * Math.pow(2, attempt - 1)));
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (!success) {
|
|
533
|
+
failed.push(server.id);
|
|
534
|
+
// Still add to map so it can be manually restarted
|
|
535
|
+
this.servers.set(server.id, {
|
|
536
|
+
id: server.id,
|
|
537
|
+
runner,
|
|
538
|
+
autoRun: server.autoRun,
|
|
539
|
+
monitorPort: server.monitorPort,
|
|
540
|
+
global: server.global,
|
|
541
|
+
watch: server.watch,
|
|
542
|
+
watchPaths: server.watchPaths,
|
|
543
|
+
});
|
|
544
|
+
// Create a pending startup failure to block tools until acknowledged
|
|
545
|
+
const now = new Date();
|
|
546
|
+
this.pendingStartups.set(server.id, {
|
|
547
|
+
serverId: server.id,
|
|
548
|
+
startedAt: now,
|
|
549
|
+
timeoutAt: now, // Already timed out
|
|
550
|
+
acknowledged: false,
|
|
551
|
+
reason: 'died', // Failed to start = died
|
|
552
|
+
});
|
|
553
|
+
await debugLog('ServerManager', `Created blocking failure for autoRun server: ${server.id}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
// Not running and not autoRun - keep config for manual restart
|
|
558
|
+
this.servers.set(server.id, {
|
|
559
|
+
id: server.id,
|
|
560
|
+
runner,
|
|
561
|
+
autoRun: false,
|
|
562
|
+
monitorPort: server.monitorPort,
|
|
563
|
+
global: server.global,
|
|
564
|
+
watch: server.watch,
|
|
565
|
+
watchPaths: server.watchPaths,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Process restored pending startups
|
|
570
|
+
// For each pending startup, check if server is still running and if timeout has passed
|
|
571
|
+
for (const [serverId, pending] of this.pendingStartups) {
|
|
572
|
+
const managed = this.servers.get(serverId);
|
|
573
|
+
if (!managed) {
|
|
574
|
+
// Server no longer exists, remove pending state
|
|
575
|
+
this.pendingStartups.delete(serverId);
|
|
576
|
+
await debugLog('ServerManager', `Removed pending startup for non-existent server: ${serverId}`);
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const isRunning = await managed.runner.isRunning();
|
|
580
|
+
const now = new Date();
|
|
581
|
+
if (!isRunning) {
|
|
582
|
+
// Server died - set reason, but respect existing acknowledgment
|
|
583
|
+
pending.reason = 'died';
|
|
584
|
+
// Only reset acknowledged if it wasn't already acknowledged
|
|
585
|
+
// (user already dealt with this failure before MCP restart)
|
|
586
|
+
await debugLog('ServerManager', `Pending startup server died: ${serverId} (acknowledged: ${pending.acknowledged})`);
|
|
587
|
+
}
|
|
588
|
+
else if (now >= pending.timeoutAt) {
|
|
589
|
+
// Timeout already passed - set reason, but respect existing acknowledgment
|
|
590
|
+
pending.reason = 'timeout';
|
|
591
|
+
await debugLog('ServerManager', `Pending startup already timed out: ${serverId} (acknowledged: ${pending.acknowledged})`);
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
// Still within timeout window - resume background detection
|
|
595
|
+
const remainingMs = pending.timeoutAt.getTime() - now.getTime();
|
|
596
|
+
await debugLog('ServerManager', `Resuming port detection for ${serverId}, ${Math.round(remainingMs / 1000)}s remaining`);
|
|
597
|
+
this.resumePortDetection(serverId, remainingMs);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (collected.length > 0) {
|
|
601
|
+
console.error(`[cdp-tools] Collected ${collected.length} abandoned dev server(s): ${collected.join(', ')}`);
|
|
602
|
+
}
|
|
603
|
+
await this.saveState();
|
|
604
|
+
return { recovered, started, failed, monitoredPorts, collected };
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Resume port detection for a server after MCP restart
|
|
608
|
+
* @param serverId The server to resume detection for
|
|
609
|
+
* @param remainingMs Time remaining until timeout
|
|
610
|
+
*/
|
|
611
|
+
resumePortDetection(serverId, remainingMs) {
|
|
612
|
+
// Run detection in background (don't await)
|
|
613
|
+
this.detectPortInBackgroundWithTimeout(serverId, remainingMs);
|
|
614
|
+
}
|
|
615
|
+
async loadState(global) {
|
|
616
|
+
const filePath = this.getServersFilePath(global);
|
|
617
|
+
try {
|
|
618
|
+
if (fs.existsSync(filePath)) {
|
|
619
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
620
|
+
const data = JSON.parse(content);
|
|
621
|
+
// Handle migration from old format (no runner type)
|
|
622
|
+
const servers = (data.servers || []).map((s) => ({
|
|
623
|
+
...s,
|
|
624
|
+
type: s.type || 'native',
|
|
625
|
+
global: global ?? false,
|
|
626
|
+
}));
|
|
627
|
+
return {
|
|
628
|
+
servers,
|
|
629
|
+
monitoredPorts: data.monitoredPorts || [],
|
|
630
|
+
pendingStartups: data.pendingStartups || [],
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
catch (err) {
|
|
635
|
+
await debugLog('ServerManager', `Failed to load state from ${filePath}: ${err}`);
|
|
636
|
+
}
|
|
637
|
+
return { servers: [], monitoredPorts: [], pendingStartups: [] };
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Save state to disk - serialized through mutex to prevent concurrent writes
|
|
641
|
+
*/
|
|
642
|
+
async saveState() {
|
|
643
|
+
// Queue behind any pending save to serialize all writes
|
|
644
|
+
this.saveMutex = this.saveMutex.then(() => this.doSaveState()).catch(() => { });
|
|
645
|
+
return this.saveMutex;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Internal save implementation - uses atomic writes to prevent corruption
|
|
649
|
+
*/
|
|
650
|
+
async doSaveState() {
|
|
651
|
+
// Split servers by global flag
|
|
652
|
+
const localServers = [];
|
|
653
|
+
const globalServers = [];
|
|
654
|
+
for (const [id, managed] of this.servers) {
|
|
655
|
+
const runner = managed.runner;
|
|
656
|
+
const status = await runner.getStatus();
|
|
657
|
+
let containerId;
|
|
658
|
+
if (runner instanceof DockerRunner) {
|
|
659
|
+
containerId = runner.getContainerId() ?? undefined;
|
|
660
|
+
}
|
|
661
|
+
const serverData = {
|
|
662
|
+
type: runner.type,
|
|
663
|
+
id,
|
|
664
|
+
command: this.getRunnerCommand(runner),
|
|
665
|
+
cwd: this.getRunnerCwd(runner),
|
|
666
|
+
pid: status.pid,
|
|
667
|
+
pidStartedAt: status.pidStartedAt,
|
|
668
|
+
containerId,
|
|
669
|
+
port: status.port,
|
|
670
|
+
autoRun: managed.autoRun,
|
|
671
|
+
startedAt: status.startedAt?.toISOString() ?? new Date().toISOString(),
|
|
672
|
+
monitorPort: managed.monitorPort,
|
|
673
|
+
watch: managed.watch,
|
|
674
|
+
watchPaths: managed.watchPaths,
|
|
675
|
+
};
|
|
676
|
+
if (managed.global) {
|
|
677
|
+
globalServers.push(serverData);
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
localServers.push(serverData);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const monitoredPorts = this.getPortMonitor().getPersistedState();
|
|
684
|
+
// Convert pending startups to persisted format, split by server's global flag
|
|
685
|
+
const localPendingStartups = [];
|
|
686
|
+
const globalPendingStartups = [];
|
|
687
|
+
for (const [serverId, pending] of this.pendingStartups) {
|
|
688
|
+
const managed = this.servers.get(serverId);
|
|
689
|
+
const persisted = {
|
|
690
|
+
serverId: pending.serverId,
|
|
691
|
+
startedAt: pending.startedAt.toISOString(),
|
|
692
|
+
timeoutAt: pending.timeoutAt.toISOString(),
|
|
693
|
+
acknowledged: pending.acknowledged,
|
|
694
|
+
reason: pending.reason,
|
|
695
|
+
};
|
|
696
|
+
if (managed?.global) {
|
|
697
|
+
globalPendingStartups.push(persisted);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
localPendingStartups.push(persisted);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// Save local servers to project directory (atomic write prevents corruption)
|
|
704
|
+
const localPath = this.getServersFilePath(false);
|
|
705
|
+
await atomicWriteFile(localPath, JSON.stringify({
|
|
706
|
+
version: 4,
|
|
707
|
+
updatedAt: new Date().toISOString(),
|
|
708
|
+
servers: localServers,
|
|
709
|
+
monitoredPorts, // Port monitoring stays in local config
|
|
710
|
+
pendingStartups: localPendingStartups,
|
|
711
|
+
}, null, 2));
|
|
712
|
+
// Save global servers to ~/.cdp-tools/ (atomic write prevents corruption)
|
|
713
|
+
const globalPath = this.getServersFilePath(true);
|
|
714
|
+
await atomicWriteFile(globalPath, JSON.stringify({
|
|
715
|
+
version: 4,
|
|
716
|
+
updatedAt: new Date().toISOString(),
|
|
717
|
+
servers: globalServers,
|
|
718
|
+
monitoredPorts: [], // Global doesn't track port monitoring
|
|
719
|
+
pendingStartups: globalPendingStartups,
|
|
720
|
+
}, null, 2));
|
|
721
|
+
}
|
|
722
|
+
getRunnerCommand(runner) {
|
|
723
|
+
if (runner instanceof NativeRunner)
|
|
724
|
+
return runner.getCommand();
|
|
725
|
+
if (runner instanceof DockerRunner)
|
|
726
|
+
return runner.getCommand();
|
|
727
|
+
if (runner instanceof DockerComposeRunner)
|
|
728
|
+
return runner.getCommand();
|
|
729
|
+
return '';
|
|
730
|
+
}
|
|
731
|
+
getRunnerCwd(runner) {
|
|
732
|
+
if (runner instanceof NativeRunner)
|
|
733
|
+
return runner.getCwd();
|
|
734
|
+
if (runner instanceof DockerRunner)
|
|
735
|
+
return runner.getCwd();
|
|
736
|
+
if (runner instanceof DockerComposeRunner)
|
|
737
|
+
return runner.getCwd();
|
|
738
|
+
return '';
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Find the managed server whose command's --inspect/--inspect-brk port
|
|
742
|
+
* matches `port` - i.e. the server a CDP debugger connection at that port
|
|
743
|
+
* actually belongs to. This is deliberately NOT the server's own detected
|
|
744
|
+
* app/service port (e.g. an HTTP port parsed from "listening on port
|
|
745
|
+
* 3000") - a Node process's inspector port and its own app port are
|
|
746
|
+
* normally different numbers, so matching on the app port would almost
|
|
747
|
+
* never find anything real.
|
|
748
|
+
*/
|
|
749
|
+
async getManagedServerByInspectorPort(port) {
|
|
750
|
+
return this.findManagedServerByInspectorPortSync(port);
|
|
751
|
+
}
|
|
752
|
+
/** Sync core of getManagedServerByInspectorPort() - reused by watch-restart lookups, which run in a sync pre-execution hot path (checkBreakpointPause) and can't await. */
|
|
753
|
+
findManagedServerByInspectorPortSync(port) {
|
|
754
|
+
for (const managed of this.servers.values()) {
|
|
755
|
+
const command = this.getRunnerCommand(managed.runner);
|
|
756
|
+
if (extractInspectorPort(command) === port) {
|
|
757
|
+
return { id: managed.id, command };
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
async isPortInUse(port) {
|
|
763
|
+
return new Promise((resolve) => {
|
|
764
|
+
const socket = new net.Socket();
|
|
765
|
+
socket.setTimeout(300);
|
|
766
|
+
socket.on('connect', () => {
|
|
767
|
+
socket.destroy();
|
|
768
|
+
resolve(true);
|
|
769
|
+
});
|
|
770
|
+
socket.on('timeout', () => {
|
|
771
|
+
socket.destroy();
|
|
772
|
+
resolve(false);
|
|
773
|
+
});
|
|
774
|
+
socket.on('error', () => {
|
|
775
|
+
socket.destroy();
|
|
776
|
+
resolve(false);
|
|
777
|
+
});
|
|
778
|
+
socket.connect(port, 'localhost');
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Find PIDs of processes listening on a port using lsof
|
|
783
|
+
*/
|
|
784
|
+
async findProcessesOnPort(port) {
|
|
785
|
+
return new Promise((resolve) => {
|
|
786
|
+
const { exec } = require('child_process');
|
|
787
|
+
exec(`lsof -ti :${port}`, (error, stdout) => {
|
|
788
|
+
if (error || !stdout.trim()) {
|
|
789
|
+
resolve([]);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const pids = stdout.trim().split('\n').map(p => parseInt(p, 10)).filter(p => !isNaN(p) && p > 0);
|
|
793
|
+
resolve(pids);
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Get the working directory of a process by PID
|
|
799
|
+
*/
|
|
800
|
+
async getProcessCwd(pid) {
|
|
801
|
+
return new Promise((resolve) => {
|
|
802
|
+
const { exec } = require('child_process');
|
|
803
|
+
// Use lsof to get the cwd of the process (works on macOS and Linux)
|
|
804
|
+
exec(`lsof -p ${pid} | grep cwd | awk '{print $NF}'`, (error, stdout) => {
|
|
805
|
+
if (error || !stdout.trim()) {
|
|
806
|
+
resolve(null);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
resolve(stdout.trim());
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Kill orphan processes holding a port, but only if they're in the expected directory
|
|
815
|
+
* Returns: { killed: PIDs killed, foreign: PIDs that are from a different directory }
|
|
816
|
+
*/
|
|
817
|
+
async killOrphanProcessesOnPort(port, expectedCwd, expectedPid) {
|
|
818
|
+
const pids = await this.findProcessesOnPort(port);
|
|
819
|
+
const killed = [];
|
|
820
|
+
const foreign = [];
|
|
821
|
+
for (const pid of pids) {
|
|
822
|
+
// Skip if this is the expected process
|
|
823
|
+
if (expectedPid && pid === expectedPid)
|
|
824
|
+
continue;
|
|
825
|
+
// Check if process is running from the expected directory
|
|
826
|
+
const processCwd = await this.getProcessCwd(pid);
|
|
827
|
+
if (!processCwd || !processCwd.startsWith(expectedCwd)) {
|
|
828
|
+
// Process is from a different directory - don't kill it
|
|
829
|
+
await debugLog('ServerManager', `Process ${pid} on port ${port} is from different directory (${processCwd}), not killing`);
|
|
830
|
+
foreign.push(pid);
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
await debugLog('ServerManager', `Killing orphan process ${pid} on port ${port} (cwd: ${processCwd})`);
|
|
835
|
+
process.kill(pid, 'SIGTERM');
|
|
836
|
+
killed.push(pid);
|
|
837
|
+
// Wait briefly then force kill if needed
|
|
838
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
839
|
+
try {
|
|
840
|
+
process.kill(pid, 0); // Check if still alive
|
|
841
|
+
process.kill(pid, 'SIGKILL');
|
|
842
|
+
await debugLog('ServerManager', `Force killed orphan process ${pid}`);
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
// Process already dead
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
catch (err) {
|
|
849
|
+
await debugLog('ServerManager', `Failed to kill process ${pid}: ${err}`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return { killed, foreign };
|
|
853
|
+
}
|
|
854
|
+
async waitForPortRelease(port, maxAttempts = 10) {
|
|
855
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
856
|
+
const inUse = await this.isPortInUse(port);
|
|
857
|
+
if (!inUse) {
|
|
858
|
+
await debugLog('ServerManager', `Port ${port} released after ${attempt + 1} attempts`);
|
|
859
|
+
return true;
|
|
860
|
+
}
|
|
861
|
+
const delay = Math.min(100 * Math.pow(2, attempt), 3000);
|
|
862
|
+
await debugLog('ServerManager', `Port ${port} still in use, waiting ${delay}ms (attempt ${attempt + 1}/${maxAttempts})`);
|
|
863
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
864
|
+
}
|
|
865
|
+
await debugLog('ServerManager', `Port ${port} still in use after ${maxAttempts} attempts`);
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Start a server
|
|
870
|
+
*/
|
|
871
|
+
async startServer(options) {
|
|
872
|
+
const { command, cwd, id: serverId, autoRun, env, port, monitorPort, global: isGlobal, watch, watchPaths } = options;
|
|
873
|
+
// Validate server ID for security (prevents command injection via container names)
|
|
874
|
+
validateServerId(serverId);
|
|
875
|
+
// Auto-detect runner type if not specified
|
|
876
|
+
const runnerType = options.runner ?? detectRunnerType(command);
|
|
877
|
+
// Validate cwd exists
|
|
878
|
+
if (!fs.existsSync(cwd)) {
|
|
879
|
+
throw new Error(`Working directory does not exist: ${cwd}`);
|
|
880
|
+
}
|
|
881
|
+
// Check if already running
|
|
882
|
+
const existing = this.servers.get(serverId);
|
|
883
|
+
if (existing) {
|
|
884
|
+
const isRunning = await existing.runner.isRunning();
|
|
885
|
+
if (isRunning) {
|
|
886
|
+
throw new Error(`Server "${serverId}" is already running. Stop it first or use restart.`);
|
|
887
|
+
}
|
|
888
|
+
this.servers.delete(serverId);
|
|
889
|
+
}
|
|
890
|
+
// Check port availability
|
|
891
|
+
const existingStatus = existing ? await existing.runner.getStatus() : null;
|
|
892
|
+
const portToCheck = port || existingStatus?.port;
|
|
893
|
+
if (portToCheck) {
|
|
894
|
+
const inUse = await this.isPortInUse(portToCheck);
|
|
895
|
+
if (inUse) {
|
|
896
|
+
throw new Error(`Port ${portToCheck} is already in use.`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
await debugLog('ServerManager', `Starting server: ${command} (runner: ${runnerType}, global: ${isGlobal ?? false})`);
|
|
900
|
+
// Create and start runner
|
|
901
|
+
const runner = createRunner(runnerType, serverId);
|
|
902
|
+
// Set global flag on native runner for log storage location
|
|
903
|
+
if (runner instanceof NativeRunner) {
|
|
904
|
+
runner.setGlobal(isGlobal ?? false);
|
|
905
|
+
}
|
|
906
|
+
const result = await runner.start({ command, cwd, id: serverId, env, port });
|
|
907
|
+
const resolvedWatchPaths = watchPaths && watchPaths.length > 0 ? watchPaths : [cwd];
|
|
908
|
+
const managed = {
|
|
909
|
+
id: serverId,
|
|
910
|
+
runner,
|
|
911
|
+
autoRun: autoRun ?? false,
|
|
912
|
+
monitorPort: monitorPort ?? true, // Default to monitoring server port
|
|
913
|
+
global: isGlobal ?? false,
|
|
914
|
+
watch: watch ?? false,
|
|
915
|
+
watchPaths: resolvedWatchPaths,
|
|
916
|
+
};
|
|
917
|
+
this.servers.set(serverId, managed);
|
|
918
|
+
if (watch) {
|
|
919
|
+
this.startWatcher(managed, resolvedWatchPaths);
|
|
920
|
+
}
|
|
921
|
+
const autoRestartMatch = detectAutoRestartCommand(command);
|
|
922
|
+
if (autoRestartMatch) {
|
|
923
|
+
await debugLog('ServerManager', `Server ${serverId} command looks auto-restarting (matched: ${autoRestartMatch}) - flagging as risky to pair with an attached breakpoint debugger`);
|
|
924
|
+
}
|
|
925
|
+
// Create pending startup entry for timeout tracking
|
|
926
|
+
const now = new Date();
|
|
927
|
+
const timeoutMs = 30000; // 30 seconds
|
|
928
|
+
this.pendingStartups.set(serverId, {
|
|
929
|
+
serverId,
|
|
930
|
+
startedAt: now,
|
|
931
|
+
timeoutAt: new Date(now.getTime() + timeoutMs),
|
|
932
|
+
acknowledged: false,
|
|
933
|
+
});
|
|
934
|
+
// Start port detection in background
|
|
935
|
+
this.detectPortInBackgroundWithTimeout(serverId, timeoutMs);
|
|
936
|
+
await this.saveState();
|
|
937
|
+
// Starting a server is what makes this session its owner. Without a claim
|
|
938
|
+
// nothing can tell it apart from a server another window is using, so
|
|
939
|
+
// nobody may ever stop it (issue #139).
|
|
940
|
+
await this.claims.claim(serverId, cwd, isGlobal ?? false);
|
|
941
|
+
await debugLog('ServerManager', `Server started: ${serverId} (${runnerType}, PID: ${result.pid})`);
|
|
942
|
+
let autoRestartWarning;
|
|
943
|
+
if (autoRestartMatch && watch) {
|
|
944
|
+
autoRestartWarning = `Command matches "${autoRestartMatch}" AND cdp-tools' own watch mode is also enabled for this server - that's redundant and risky, since both could try to restart the process at once. Recommend removing "${autoRestartMatch}" from the command and relying on cdp-tools' watch mode instead, which already coordinates safely with an attached breakpoint debugger.`;
|
|
945
|
+
}
|
|
946
|
+
else if (autoRestartMatch) {
|
|
947
|
+
autoRestartWarning = `Command matches "${autoRestartMatch}", which auto-restarts its own process on file changes. Attaching a CDP debugger and pausing at a breakpoint while this is running is a known-bad combination (can cause EADDRINUSE crash-loops and ambiguous failed-but-still-listening states) - the restart happens entirely outside anything cdp-tools tracks. Prefer disabling auto-restart and using cdp-tools' own watch mode instead (server({ action: 'start', watch: true })), which coordinates safely with a paused debugger, or call server({ action: 'restart' }) explicitly.`;
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
id: serverId,
|
|
951
|
+
pid: result.pid,
|
|
952
|
+
runnerType,
|
|
953
|
+
containerId: result.containerId,
|
|
954
|
+
autoRestartWarning,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Background port detection with timeout management
|
|
959
|
+
* @param serverId The server to detect port for
|
|
960
|
+
* @param timeoutMs How long to wait before triggering blocking (default 30s)
|
|
961
|
+
*/
|
|
962
|
+
async detectPortInBackgroundWithTimeout(serverId, timeoutMs = 30000) {
|
|
963
|
+
const managed = this.servers.get(serverId);
|
|
964
|
+
if (!managed)
|
|
965
|
+
return;
|
|
966
|
+
const iterations = Math.ceil(timeoutMs / 1000);
|
|
967
|
+
const checkInterval = 1000; // Check every second
|
|
968
|
+
for (let i = 0; i < iterations; i++) {
|
|
969
|
+
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
970
|
+
const current = this.servers.get(serverId);
|
|
971
|
+
if (!current) {
|
|
972
|
+
// Server was removed, clean up pending state
|
|
973
|
+
this.pendingStartups.delete(serverId);
|
|
974
|
+
await this.saveState();
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
// Check if server is still running
|
|
978
|
+
const isRunning = await current.runner.isRunning();
|
|
979
|
+
if (!isRunning) {
|
|
980
|
+
// Server died during startup - trigger blocking
|
|
981
|
+
const pending = this.pendingStartups.get(serverId);
|
|
982
|
+
if (pending) {
|
|
983
|
+
pending.reason = 'died';
|
|
984
|
+
pending.acknowledged = false;
|
|
985
|
+
await this.saveState();
|
|
986
|
+
await debugLog('ServerManager', `Server died during startup: ${serverId}`);
|
|
987
|
+
}
|
|
988
|
+
return; // Exit detection, blocking will be triggered by getPendingStartupFailures
|
|
989
|
+
}
|
|
990
|
+
// Check if port already in status
|
|
991
|
+
const status = await current.runner.getStatus();
|
|
992
|
+
if (status.port) {
|
|
993
|
+
// Port already detected - remove from pending and set up monitoring
|
|
994
|
+
this.pendingStartups.delete(serverId);
|
|
995
|
+
if (current.monitorPort && !this.getPortMonitor().isMonitoring(status.port)) {
|
|
996
|
+
await this.getPortMonitor().startMonitoring(status.port, 'block', `Server: ${serverId}`);
|
|
997
|
+
await debugLog('ServerManager', `Auto-started monitoring for port ${status.port}`);
|
|
998
|
+
}
|
|
999
|
+
await this.saveState();
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
// Try to detect port from logs
|
|
1003
|
+
const port = await current.runner.detectPort();
|
|
1004
|
+
if (port) {
|
|
1005
|
+
// Port detected - remove from pending and set up monitoring
|
|
1006
|
+
this.pendingStartups.delete(serverId);
|
|
1007
|
+
await debugLog('ServerManager', `Detected port ${port} for ${serverId}`);
|
|
1008
|
+
if (current.monitorPort && !this.getPortMonitor().isMonitoring(port)) {
|
|
1009
|
+
await this.getPortMonitor().startMonitoring(port, 'block', `Server: ${serverId}`);
|
|
1010
|
+
await debugLog('ServerManager', `Auto-started monitoring for port ${port}`);
|
|
1011
|
+
}
|
|
1012
|
+
await this.saveState();
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
// Timeout reached without port detection - trigger blocking
|
|
1017
|
+
const pending = this.pendingStartups.get(serverId);
|
|
1018
|
+
if (pending && !pending.reason) {
|
|
1019
|
+
pending.reason = 'timeout';
|
|
1020
|
+
pending.acknowledged = false;
|
|
1021
|
+
await this.saveState();
|
|
1022
|
+
await debugLog('ServerManager', `Port detection timeout for ${serverId}`);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* @deprecated Use detectPortInBackgroundWithTimeout instead
|
|
1027
|
+
*/
|
|
1028
|
+
async detectPortInBackground(serverId) {
|
|
1029
|
+
await this.detectPortInBackgroundWithTimeout(serverId, 30000);
|
|
1030
|
+
}
|
|
1031
|
+
// ============================================================================
|
|
1032
|
+
// Pending Startup Management
|
|
1033
|
+
// ============================================================================
|
|
1034
|
+
/**
|
|
1035
|
+
* Get pending startup failures that should trigger blocking
|
|
1036
|
+
* Returns only startups that have timed out or died and are not acknowledged
|
|
1037
|
+
*/
|
|
1038
|
+
getPendingStartupFailures() {
|
|
1039
|
+
const failures = [];
|
|
1040
|
+
for (const [, pending] of this.pendingStartups) {
|
|
1041
|
+
// Only return failures (has reason) that are not acknowledged
|
|
1042
|
+
if (pending.reason && !pending.acknowledged) {
|
|
1043
|
+
failures.push({
|
|
1044
|
+
serverId: pending.serverId,
|
|
1045
|
+
startedAt: pending.startedAt,
|
|
1046
|
+
reason: pending.reason,
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return failures;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Check if a server has a pending startup (regardless of state)
|
|
1054
|
+
*/
|
|
1055
|
+
hasPendingStartup(serverId) {
|
|
1056
|
+
return this.pendingStartups.has(serverId);
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Get pending startup status for a server
|
|
1060
|
+
*/
|
|
1061
|
+
getPendingStartup(serverId) {
|
|
1062
|
+
return this.pendingStartups.get(serverId);
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Acknowledge a pending startup failure
|
|
1066
|
+
* Clears the blocking state and starts background health monitoring
|
|
1067
|
+
*/
|
|
1068
|
+
async acknowledgeStartup(serverId) {
|
|
1069
|
+
const pending = this.pendingStartups.get(serverId);
|
|
1070
|
+
if (!pending) {
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
// Clear pending state - no longer blocking
|
|
1074
|
+
this.pendingStartups.delete(serverId);
|
|
1075
|
+
await this.saveState();
|
|
1076
|
+
await debugLog('ServerManager', `Acknowledged pending startup: ${serverId}`);
|
|
1077
|
+
// Start background health monitoring
|
|
1078
|
+
// This will detect port (and set up monitoring) or detect server death (and re-block)
|
|
1079
|
+
this.monitorServerHealth(serverId);
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Monitor server health after acknowledgment
|
|
1084
|
+
* Checks every 5 seconds for:
|
|
1085
|
+
* - Port detection (sets up port monitoring when found)
|
|
1086
|
+
* - Server death (re-triggers blocking)
|
|
1087
|
+
* Runs until port is found, server dies, or server is removed
|
|
1088
|
+
*/
|
|
1089
|
+
async monitorServerHealth(serverId) {
|
|
1090
|
+
const checkInterval = 5000; // Check every 5 seconds
|
|
1091
|
+
while (true) {
|
|
1092
|
+
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
1093
|
+
const managed = this.servers.get(serverId);
|
|
1094
|
+
if (!managed) {
|
|
1095
|
+
// Server was removed - clean exit
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
// Check if server is still running
|
|
1099
|
+
const isRunning = await managed.runner.isRunning();
|
|
1100
|
+
if (!isRunning) {
|
|
1101
|
+
// Server died - create blocking entry
|
|
1102
|
+
const now = new Date();
|
|
1103
|
+
this.pendingStartups.set(serverId, {
|
|
1104
|
+
serverId,
|
|
1105
|
+
startedAt: now,
|
|
1106
|
+
timeoutAt: now,
|
|
1107
|
+
acknowledged: false,
|
|
1108
|
+
reason: 'died',
|
|
1109
|
+
});
|
|
1110
|
+
await this.saveState();
|
|
1111
|
+
await debugLog('ServerManager', `Server died (health check): ${serverId}`);
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
// Try to detect port if not already monitoring
|
|
1115
|
+
const status = await managed.runner.getStatus();
|
|
1116
|
+
let port = status.port ?? undefined;
|
|
1117
|
+
if (!port) {
|
|
1118
|
+
port = (await managed.runner.detectPort()) ?? undefined;
|
|
1119
|
+
}
|
|
1120
|
+
if (port && managed.monitorPort && !this.getPortMonitor().isMonitoring(port)) {
|
|
1121
|
+
// Port found - set up monitoring, then port monitor takes over
|
|
1122
|
+
await this.getPortMonitor().startMonitoring(port, 'block', `Server: ${serverId}`);
|
|
1123
|
+
await debugLog('ServerManager', `Health check found port ${port} - monitoring started for ${serverId}`);
|
|
1124
|
+
await this.saveState();
|
|
1125
|
+
return; // Port monitor will handle it from here
|
|
1126
|
+
}
|
|
1127
|
+
// If port is being monitored, we can stop health checking
|
|
1128
|
+
if (port && this.getPortMonitor().isMonitoring(port)) {
|
|
1129
|
+
await debugLog('ServerManager', `Port ${port} already monitored, stopping health check for ${serverId}`);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Extend the startup timeout by another 30 seconds
|
|
1136
|
+
* Resets the acknowledged flag and reason, resumes detection
|
|
1137
|
+
*/
|
|
1138
|
+
async extendStartupTimeout(serverId) {
|
|
1139
|
+
const pending = this.pendingStartups.get(serverId);
|
|
1140
|
+
if (!pending) {
|
|
1141
|
+
return false;
|
|
1142
|
+
}
|
|
1143
|
+
// Check if server is still running
|
|
1144
|
+
const managed = this.servers.get(serverId);
|
|
1145
|
+
if (!managed) {
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
const isRunning = await managed.runner.isRunning();
|
|
1149
|
+
if (!isRunning) {
|
|
1150
|
+
// Server died, can't extend
|
|
1151
|
+
pending.reason = 'died';
|
|
1152
|
+
pending.acknowledged = false;
|
|
1153
|
+
await this.saveState();
|
|
1154
|
+
return false;
|
|
1155
|
+
}
|
|
1156
|
+
// Reset timeout
|
|
1157
|
+
const now = new Date();
|
|
1158
|
+
const timeoutMs = 30000;
|
|
1159
|
+
pending.timeoutAt = new Date(now.getTime() + timeoutMs);
|
|
1160
|
+
pending.acknowledged = false;
|
|
1161
|
+
pending.reason = undefined;
|
|
1162
|
+
await this.saveState();
|
|
1163
|
+
await debugLog('ServerManager', `Extended startup timeout for ${serverId}`);
|
|
1164
|
+
// Resume background detection
|
|
1165
|
+
this.detectPortInBackgroundWithTimeout(serverId, timeoutMs);
|
|
1166
|
+
return true;
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Remove pending startup entry (called when server is stopped or removed)
|
|
1170
|
+
*/
|
|
1171
|
+
removePendingStartup(serverId) {
|
|
1172
|
+
this.pendingStartups.delete(serverId);
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Stop a server
|
|
1176
|
+
*/
|
|
1177
|
+
async stopServer(serverId) {
|
|
1178
|
+
const managed = this.servers.get(serverId);
|
|
1179
|
+
if (!managed) {
|
|
1180
|
+
throw new Error(`Server "${serverId}" not found. Use list action to see servers.`);
|
|
1181
|
+
}
|
|
1182
|
+
// Clean up pending startup state
|
|
1183
|
+
this.removePendingStartup(serverId);
|
|
1184
|
+
// Stop watching and drop any queued/in-flight watch-restart state - a
|
|
1185
|
+
// leaked watcher could otherwise fire a restart on an already-stopped
|
|
1186
|
+
// server.
|
|
1187
|
+
this.stopWatcher(managed);
|
|
1188
|
+
const isRunning = await managed.runner.isRunning();
|
|
1189
|
+
if (!isRunning) {
|
|
1190
|
+
await this.saveState();
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
await debugLog('ServerManager', `Stopping server ${serverId}`);
|
|
1194
|
+
await managed.runner.stop();
|
|
1195
|
+
this.claims.release(serverId, managed.global ?? false);
|
|
1196
|
+
await this.saveState();
|
|
1197
|
+
await debugLog('ServerManager', `Server ${serverId} stopped`);
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Restart a server
|
|
1201
|
+
* Reloads config from persisted state to pick up any manual edits (e.g., port changes)
|
|
1202
|
+
*/
|
|
1203
|
+
async restartServer(serverId) {
|
|
1204
|
+
const managed = this.servers.get(serverId);
|
|
1205
|
+
if (!managed) {
|
|
1206
|
+
throw new Error(`Server "${serverId}" not found.`);
|
|
1207
|
+
}
|
|
1208
|
+
// Reload config from persisted state to pick up any manual edits
|
|
1209
|
+
const persistedConfig = await this.reloadServerConfig(serverId);
|
|
1210
|
+
// Use persisted config values if available, otherwise fall back to in-memory
|
|
1211
|
+
const command = persistedConfig?.command ?? this.getRunnerCommand(managed.runner);
|
|
1212
|
+
const cwd = persistedConfig?.cwd ?? this.getRunnerCwd(managed.runner);
|
|
1213
|
+
const port = persistedConfig?.port;
|
|
1214
|
+
const autoRun = persistedConfig?.autoRun ?? managed.autoRun;
|
|
1215
|
+
const monitorPort = persistedConfig?.monitorPort ?? managed.monitorPort;
|
|
1216
|
+
const runnerType = persistedConfig?.type ?? managed.runner.type;
|
|
1217
|
+
const watch = persistedConfig?.watch ?? managed.watch ?? false;
|
|
1218
|
+
const watchPaths = persistedConfig?.watchPaths ?? managed.watchPaths;
|
|
1219
|
+
// Get current status for the old port (to clean up if different from new port)
|
|
1220
|
+
const currentStatus = await managed.runner.getStatus();
|
|
1221
|
+
const oldPort = currentStatus.port;
|
|
1222
|
+
await this.stopServer(serverId);
|
|
1223
|
+
// Wait for old port to be released (if server was running)
|
|
1224
|
+
if (oldPort) {
|
|
1225
|
+
let released = await this.waitForPortRelease(oldPort, 3);
|
|
1226
|
+
if (!released) {
|
|
1227
|
+
// Kill any orphan processes holding the port, but only if they're from the same directory
|
|
1228
|
+
const { killed, foreign } = await this.killOrphanProcessesOnPort(oldPort, cwd, currentStatus.pid);
|
|
1229
|
+
if (foreign.length > 0 && oldPort === port) {
|
|
1230
|
+
// Foreign process on the port we want to use - error
|
|
1231
|
+
throw new Error(`Port ${oldPort} is in use by another application (PID: ${foreign.join(', ')}). Stop that process first or use a different port.`);
|
|
1232
|
+
}
|
|
1233
|
+
if (killed.length > 0) {
|
|
1234
|
+
await debugLog('ServerManager', `Killed ${killed.length} orphan process(es) on port ${oldPort}: ${killed.join(', ')}`);
|
|
1235
|
+
released = await this.waitForPortRelease(oldPort, 5);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
// Only error if we couldn't release the port AND it's the same port we want to use
|
|
1239
|
+
if (!released && oldPort === port) {
|
|
1240
|
+
throw new Error(`Port ${oldPort} is still in use after stopping server.`);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
else {
|
|
1244
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1245
|
+
}
|
|
1246
|
+
// Check if new port (from config) is available
|
|
1247
|
+
if (port && port !== oldPort) {
|
|
1248
|
+
const newPortInUse = await this.isPortInUse(port);
|
|
1249
|
+
if (newPortInUse) {
|
|
1250
|
+
const pids = await this.findProcessesOnPort(port);
|
|
1251
|
+
throw new Error(`Port ${port} is already in use (PID: ${pids.join(', ')}). Choose a different port.`);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return await this.startServer({
|
|
1255
|
+
command,
|
|
1256
|
+
cwd,
|
|
1257
|
+
id: serverId,
|
|
1258
|
+
autoRun,
|
|
1259
|
+
runner: runnerType,
|
|
1260
|
+
monitorPort,
|
|
1261
|
+
watch,
|
|
1262
|
+
watchPaths,
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
getOrCreateWatchState(serverId) {
|
|
1266
|
+
let state = this.watchState.get(serverId);
|
|
1267
|
+
if (!state) {
|
|
1268
|
+
state = { restarting: false, restartQueued: false };
|
|
1269
|
+
this.watchState.set(serverId, state);
|
|
1270
|
+
}
|
|
1271
|
+
return state;
|
|
1272
|
+
}
|
|
1273
|
+
startWatcher(managed, watchPaths) {
|
|
1274
|
+
managed.watcher?.stop();
|
|
1275
|
+
managed.watcher = new ServerFileWatcher({
|
|
1276
|
+
paths: watchPaths,
|
|
1277
|
+
onChange: () => {
|
|
1278
|
+
void this.requestWatchRestart(managed.id);
|
|
1279
|
+
},
|
|
1280
|
+
});
|
|
1281
|
+
managed.watcher.start();
|
|
1282
|
+
}
|
|
1283
|
+
stopWatcher(managed) {
|
|
1284
|
+
managed.watcher?.stop();
|
|
1285
|
+
managed.watcher = undefined;
|
|
1286
|
+
this.watchState.delete(managed.id);
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Single entry point for every watch-triggered restart (file-change,
|
|
1290
|
+
* resume-triggered auto-fire, and its own re-entrant re-check once an
|
|
1291
|
+
* in-flight restart finishes). Always re-checks pause state fresh rather
|
|
1292
|
+
* than trusting a cached value - see issue #88.
|
|
1293
|
+
*/
|
|
1294
|
+
async requestWatchRestart(serverId) {
|
|
1295
|
+
const managed = this.servers.get(serverId);
|
|
1296
|
+
if (!managed) {
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const state = this.getOrCreateWatchState(serverId);
|
|
1300
|
+
if (state.restarting) {
|
|
1301
|
+
// Queue one more pass for after the in-flight restart finishes, rather
|
|
1302
|
+
// than piggybacking on it - the file that triggered THIS call might not
|
|
1303
|
+
// be reflected in whatever the in-flight restart already read from disk.
|
|
1304
|
+
state.restartQueued = true;
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
const command = this.getRunnerCommand(managed.runner);
|
|
1308
|
+
const inspectorPort = extractInspectorPort(command);
|
|
1309
|
+
const isPaused = inspectorPort !== null && this.pauseChecker ? this.pauseChecker(inspectorPort) : false;
|
|
1310
|
+
if (isPaused) {
|
|
1311
|
+
state.pendingRestart = { queuedAt: new Date() };
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
try {
|
|
1315
|
+
await this.performGuardedRestart(serverId);
|
|
1316
|
+
}
|
|
1317
|
+
catch (err) {
|
|
1318
|
+
await debugLog('ServerManager', `Watch-triggered restart of ${serverId} failed: ${err}`);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Explicit, forced restart (bypasses the pause-check entirely - the caller
|
|
1323
|
+
* has already decided to end any paused debug session). Shares the same
|
|
1324
|
+
* in-flight guard as requestWatchRestart(): if a watch-triggered restart is
|
|
1325
|
+
* already running, piggybacks on its result instead of racing it.
|
|
1326
|
+
*/
|
|
1327
|
+
async forceRestart(serverId) {
|
|
1328
|
+
const state = this.getOrCreateWatchState(serverId);
|
|
1329
|
+
state.pendingRestart = undefined;
|
|
1330
|
+
if (state.restarting && state.inFlight) {
|
|
1331
|
+
return state.inFlight;
|
|
1332
|
+
}
|
|
1333
|
+
return await this.performGuardedRestart(serverId);
|
|
1334
|
+
}
|
|
1335
|
+
/** Shared restart guard used by both requestWatchRestart() and forceRestart(). */
|
|
1336
|
+
async performGuardedRestart(serverId) {
|
|
1337
|
+
const state = this.getOrCreateWatchState(serverId);
|
|
1338
|
+
state.restarting = true;
|
|
1339
|
+
state.pendingRestart = undefined;
|
|
1340
|
+
const promise = this.restartServer(serverId).finally(() => {
|
|
1341
|
+
state.restarting = false;
|
|
1342
|
+
state.inFlight = undefined;
|
|
1343
|
+
if (state.restartQueued) {
|
|
1344
|
+
state.restartQueued = false;
|
|
1345
|
+
void this.requestWatchRestart(serverId);
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
state.inFlight = promise;
|
|
1349
|
+
return promise;
|
|
1350
|
+
}
|
|
1351
|
+
/** For checkBreakpointPause's lookup (via a paused connection's inspector port). */
|
|
1352
|
+
getPendingRestartByInspectorPort(port) {
|
|
1353
|
+
const managed = this.findManagedServerByInspectorPortSync(port);
|
|
1354
|
+
if (!managed) {
|
|
1355
|
+
return null;
|
|
1356
|
+
}
|
|
1357
|
+
const state = this.watchState.get(managed.id);
|
|
1358
|
+
return state?.pendingRestart ? { serverId: managed.id, queuedAt: state.pendingRestart.queuedAt } : null;
|
|
1359
|
+
}
|
|
1360
|
+
/** Discard a queued watch-restart without performing it - keep debugging. */
|
|
1361
|
+
cancelPendingRestart(serverId) {
|
|
1362
|
+
const state = this.watchState.get(serverId);
|
|
1363
|
+
if (!state?.pendingRestart) {
|
|
1364
|
+
return false;
|
|
1365
|
+
}
|
|
1366
|
+
state.pendingRestart = undefined;
|
|
1367
|
+
return true;
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Re-check a port's pause state and let its queued watch-restart (if any)
|
|
1371
|
+
* fire now that the debugger has resumed. Call this after any resume that
|
|
1372
|
+
* un-pauses a connection - requestWatchRestart() only re-fires on the next
|
|
1373
|
+
* file change otherwise, so without this hook a restart deferred by a
|
|
1374
|
+
* pause would sit queued forever if no further edits happen.
|
|
1375
|
+
*/
|
|
1376
|
+
retryPendingRestartByInspectorPort(port) {
|
|
1377
|
+
const managed = this.findManagedServerByInspectorPortSync(port);
|
|
1378
|
+
if (managed && this.watchState.get(managed.id)?.pendingRestart) {
|
|
1379
|
+
void this.requestWatchRestart(managed.id);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Set autoRun flag
|
|
1384
|
+
*/
|
|
1385
|
+
async setAutoRun(serverId, autoRun) {
|
|
1386
|
+
const managed = this.servers.get(serverId);
|
|
1387
|
+
if (!managed) {
|
|
1388
|
+
throw new Error(`Server "${serverId}" not found.`);
|
|
1389
|
+
}
|
|
1390
|
+
managed.autoRun = autoRun;
|
|
1391
|
+
await this.saveState();
|
|
1392
|
+
await debugLog('ServerManager', `Set autoRun=${autoRun} for ${serverId}`);
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Get status of servers
|
|
1396
|
+
*/
|
|
1397
|
+
async getStatus(serverId) {
|
|
1398
|
+
const formatUptime = (startedAt) => {
|
|
1399
|
+
const seconds = Math.floor((Date.now() - startedAt.getTime()) / 1000);
|
|
1400
|
+
if (seconds < 60)
|
|
1401
|
+
return `${seconds}s`;
|
|
1402
|
+
if (seconds < 3600)
|
|
1403
|
+
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
1404
|
+
const hours = Math.floor(seconds / 3600);
|
|
1405
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
1406
|
+
return `${hours}h ${minutes}m`;
|
|
1407
|
+
};
|
|
1408
|
+
const getServerStatus = async (managed) => {
|
|
1409
|
+
const status = await managed.runner.getStatus();
|
|
1410
|
+
return {
|
|
1411
|
+
id: managed.id,
|
|
1412
|
+
command: this.getRunnerCommand(managed.runner),
|
|
1413
|
+
cwd: this.getRunnerCwd(managed.runner),
|
|
1414
|
+
pid: status.pid,
|
|
1415
|
+
containerId: status.containerId,
|
|
1416
|
+
startedAt: status.startedAt ?? new Date(),
|
|
1417
|
+
uptime: status.startedAt ? formatUptime(status.startedAt) : '0s',
|
|
1418
|
+
port: status.port,
|
|
1419
|
+
running: status.running,
|
|
1420
|
+
autoRun: managed.autoRun,
|
|
1421
|
+
runnerType: managed.runner.type,
|
|
1422
|
+
global: managed.global ?? false,
|
|
1423
|
+
watch: managed.watch ?? false,
|
|
1424
|
+
};
|
|
1425
|
+
};
|
|
1426
|
+
if (serverId) {
|
|
1427
|
+
const managed = this.servers.get(serverId);
|
|
1428
|
+
if (!managed)
|
|
1429
|
+
return [];
|
|
1430
|
+
return [await getServerStatus(managed)];
|
|
1431
|
+
}
|
|
1432
|
+
const results = [];
|
|
1433
|
+
for (const managed of this.servers.values()) {
|
|
1434
|
+
results.push(await getServerStatus(managed));
|
|
1435
|
+
}
|
|
1436
|
+
return results;
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Get log stats (for native runner only)
|
|
1440
|
+
*/
|
|
1441
|
+
getLogStats() {
|
|
1442
|
+
const stats = [];
|
|
1443
|
+
for (const [serverId, managed] of this.servers) {
|
|
1444
|
+
if (managed.runner instanceof NativeRunner) {
|
|
1445
|
+
const runnerStats = managed.runner.getLogStats();
|
|
1446
|
+
stats.push({
|
|
1447
|
+
serverId,
|
|
1448
|
+
newStdout: runnerStats.newStdout,
|
|
1449
|
+
newStderr: runnerStats.newStderr,
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
else {
|
|
1453
|
+
stats.push({
|
|
1454
|
+
serverId,
|
|
1455
|
+
newStdout: 0,
|
|
1456
|
+
newStderr: 0,
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return stats;
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Get logs from a server
|
|
1464
|
+
*/
|
|
1465
|
+
async getLogs(serverId, options = {}) {
|
|
1466
|
+
const managed = this.servers.get(serverId);
|
|
1467
|
+
if (!managed) {
|
|
1468
|
+
throw new Error(`Server "${serverId}" not found`);
|
|
1469
|
+
}
|
|
1470
|
+
const { type = 'all', lines } = options;
|
|
1471
|
+
return await managed.runner.getLogs({ type, lines });
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Clear logs (native runner only)
|
|
1475
|
+
*/
|
|
1476
|
+
async clearLogs(serverId) {
|
|
1477
|
+
const managed = this.servers.get(serverId);
|
|
1478
|
+
if (!managed) {
|
|
1479
|
+
throw new Error(`Server "${serverId}" not found`);
|
|
1480
|
+
}
|
|
1481
|
+
if (managed.runner instanceof NativeRunner) {
|
|
1482
|
+
return await managed.runner.clearLogs();
|
|
1483
|
+
}
|
|
1484
|
+
throw new Error(`clearLogs is only supported for native runner, not ${managed.runner.type}`);
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Get log access info (file paths for native, commands for docker)
|
|
1488
|
+
*/
|
|
1489
|
+
getLogAccess(serverId) {
|
|
1490
|
+
const managed = this.servers.get(serverId);
|
|
1491
|
+
if (!managed) {
|
|
1492
|
+
throw new Error(`Server "${serverId}" not found`);
|
|
1493
|
+
}
|
|
1494
|
+
if (managed.runner.getLogAccess) {
|
|
1495
|
+
return managed.runner.getLogAccess();
|
|
1496
|
+
}
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Stop every server this session owns outright - one no live session other
|
|
1501
|
+
* than this one claims. Used when the session is going away for good (an
|
|
1502
|
+
* idle suspend, or a client that closed), which is the only time it is safe
|
|
1503
|
+
* to take a dev server down.
|
|
1504
|
+
*
|
|
1505
|
+
* A server another window is still using is left alone, and so is one whose
|
|
1506
|
+
* ownership cannot be established: over-stopping destroys running work,
|
|
1507
|
+
* under-stopping leaves a process for the next `initialize()` to collect.
|
|
1508
|
+
*/
|
|
1509
|
+
async stopOwnedServers() {
|
|
1510
|
+
const stopped = [];
|
|
1511
|
+
const keptForOthers = [];
|
|
1512
|
+
for (const [serverId, managed] of this.servers) {
|
|
1513
|
+
const isGlobal = managed.global ?? false;
|
|
1514
|
+
try {
|
|
1515
|
+
if (!(await managed.runner.isRunning()))
|
|
1516
|
+
continue;
|
|
1517
|
+
const serverCwd = this.getRunnerCwd(managed.runner) ?? process.cwd();
|
|
1518
|
+
if (!this.claims.mayStop(serverId, serverCwd, isGlobal)) {
|
|
1519
|
+
keptForOthers.push(serverId);
|
|
1520
|
+
// Drop only OUR claim: the server keeps running for whoever else
|
|
1521
|
+
// holds one, and stays collectable once they are gone too.
|
|
1522
|
+
this.claims.release(serverId, isGlobal);
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
await this.stopServer(serverId);
|
|
1526
|
+
stopped.push(serverId);
|
|
1527
|
+
}
|
|
1528
|
+
catch (error) {
|
|
1529
|
+
await debugLog('ServerManager', `Failed to release ${serverId}: ${error}`);
|
|
1530
|
+
keptForOthers.push(serverId);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return { stopped, keptForOthers };
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Server ids whose every claim was dead, per storage scope, with those dead
|
|
1537
|
+
* claims deleted as a side effect.
|
|
1538
|
+
*
|
|
1539
|
+
* Must be read BEFORE this session claims anything during recovery -
|
|
1540
|
+
* otherwise our own fresh claim makes an orphan look owned, and nothing is
|
|
1541
|
+
* ever collected.
|
|
1542
|
+
*
|
|
1543
|
+
* A server with no claim file at all is absent from this: it predates
|
|
1544
|
+
* claims, or was started outside cdp-tools, and neither is ours to kill.
|
|
1545
|
+
*/
|
|
1546
|
+
findAbandonedServerIds() {
|
|
1547
|
+
const byScope = new Map();
|
|
1548
|
+
for (const isGlobal of [false, true]) {
|
|
1549
|
+
const { removed, unclaimedServerIds } = this.claims.collectDeadClaims(isGlobal);
|
|
1550
|
+
if (removed > 0) {
|
|
1551
|
+
void debugLog('ServerClaims', `Removed ${removed} dead claim(s) (global=${isGlobal})`);
|
|
1552
|
+
}
|
|
1553
|
+
byScope.set(isGlobal, new Set(unclaimedServerIds));
|
|
1554
|
+
}
|
|
1555
|
+
return byScope;
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Stop all servers
|
|
1559
|
+
*/
|
|
1560
|
+
async stopAll() {
|
|
1561
|
+
const stopped = [];
|
|
1562
|
+
for (const serverId of this.servers.keys()) {
|
|
1563
|
+
try {
|
|
1564
|
+
await this.stopServer(serverId);
|
|
1565
|
+
stopped.push(serverId);
|
|
1566
|
+
}
|
|
1567
|
+
catch (error) {
|
|
1568
|
+
await debugLog('ServerManager', `Failed to stop ${serverId}: ${error}`);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
return stopped;
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Remove a server from config
|
|
1575
|
+
*/
|
|
1576
|
+
async removeServer(serverId) {
|
|
1577
|
+
const managed = this.servers.get(serverId);
|
|
1578
|
+
if (!managed) {
|
|
1579
|
+
throw new Error(`Server "${serverId}" not found.`);
|
|
1580
|
+
}
|
|
1581
|
+
const isRunning = await managed.runner.isRunning();
|
|
1582
|
+
if (isRunning) {
|
|
1583
|
+
await this.stopServer(serverId);
|
|
1584
|
+
}
|
|
1585
|
+
else {
|
|
1586
|
+
// stopServer() (which also closes the watcher) is only called above
|
|
1587
|
+
// when running - make sure a leaked watcher/pending state can't
|
|
1588
|
+
// outlive removal either way.
|
|
1589
|
+
this.stopWatcher(managed);
|
|
1590
|
+
}
|
|
1591
|
+
this.servers.delete(serverId);
|
|
1592
|
+
await this.saveState();
|
|
1593
|
+
await debugLog('ServerManager', `Server ${serverId} removed from config`);
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Get running server IDs
|
|
1597
|
+
*/
|
|
1598
|
+
async getRunningServerIds() {
|
|
1599
|
+
const running = [];
|
|
1600
|
+
for (const [id, managed] of this.servers) {
|
|
1601
|
+
if (await managed.runner.isRunning()) {
|
|
1602
|
+
running.push(id);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return running;
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Cleanup - refresh status of all servers (no longer removes stopped servers)
|
|
1609
|
+
* Stopped servers remain in config and can be manually restarted
|
|
1610
|
+
*/
|
|
1611
|
+
async cleanup() {
|
|
1612
|
+
// Pick up servers another session started after this one loaded its state.
|
|
1613
|
+
// Without this a second editor window can neither see nor stop them - its
|
|
1614
|
+
// map was built once at startup - and, more importantly, it never claims
|
|
1615
|
+
// them, so it has no say in whether they are collected.
|
|
1616
|
+
await this.adoptNewlyPersistedServers();
|
|
1617
|
+
// Servers are only removed via the explicit 'remove' action
|
|
1618
|
+
return 0;
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Add running servers that appeared in persisted state since this session
|
|
1622
|
+
* loaded, claiming each one: seeing a server is enough to depend on it.
|
|
1623
|
+
*/
|
|
1624
|
+
async adoptNewlyPersistedServers() {
|
|
1625
|
+
let persisted;
|
|
1626
|
+
try {
|
|
1627
|
+
const [local, global] = await Promise.all([this.loadState(false), this.loadState(true)]);
|
|
1628
|
+
persisted = [
|
|
1629
|
+
...local.servers.map(server => ({ ...server, global: false })),
|
|
1630
|
+
...global.servers.map(server => ({ ...server, global: true })),
|
|
1631
|
+
];
|
|
1632
|
+
}
|
|
1633
|
+
catch {
|
|
1634
|
+
return; // Unreadable state is not worth failing a list over.
|
|
1635
|
+
}
|
|
1636
|
+
for (const server of persisted) {
|
|
1637
|
+
if (this.servers.has(server.id))
|
|
1638
|
+
continue;
|
|
1639
|
+
try {
|
|
1640
|
+
const runner = createRunner(server.type || 'native', server.id);
|
|
1641
|
+
runner.restore(server);
|
|
1642
|
+
if (!(await runner.isRunning()))
|
|
1643
|
+
continue;
|
|
1644
|
+
this.servers.set(server.id, {
|
|
1645
|
+
id: server.id,
|
|
1646
|
+
runner,
|
|
1647
|
+
autoRun: server.autoRun,
|
|
1648
|
+
monitorPort: server.monitorPort,
|
|
1649
|
+
global: server.global,
|
|
1650
|
+
watch: server.watch,
|
|
1651
|
+
watchPaths: server.watchPaths,
|
|
1652
|
+
});
|
|
1653
|
+
await this.claims.claim(server.id, server.cwd, server.global);
|
|
1654
|
+
await debugLog('ServerManager', `Adopted server started elsewhere: ${server.id}`);
|
|
1655
|
+
}
|
|
1656
|
+
catch (error) {
|
|
1657
|
+
await debugLog('ServerManager', `Could not adopt ${server.id}: ${error}`);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Reload a server's config from persisted state (for picking up manual edits)
|
|
1663
|
+
*/
|
|
1664
|
+
async reloadServerConfig(serverId) {
|
|
1665
|
+
const localPersisted = await this.loadState(false);
|
|
1666
|
+
const globalPersisted = await this.loadState(true);
|
|
1667
|
+
const allServers = [
|
|
1668
|
+
...localPersisted.servers.map(s => ({ ...s, global: false })),
|
|
1669
|
+
...globalPersisted.servers.map(s => ({ ...s, global: true })),
|
|
1670
|
+
];
|
|
1671
|
+
return allServers.find(s => s.id === serverId) || null;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
//# sourceMappingURL=server-manager.js.map
|