prismcast 1.10.2 → 1.11.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/dist/app.d.ts +6 -3
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +140 -86
- package/dist/app.js.map +1 -1
- package/dist/browser/blockedPage.d.ts +80 -0
- package/dist/browser/blockedPage.d.ts.map +1 -0
- package/dist/browser/blockedPage.js +209 -0
- package/dist/browser/blockedPage.js.map +1 -0
- package/dist/browser/browserSupervisor.d.ts +80 -0
- package/dist/browser/browserSupervisor.d.ts.map +1 -0
- package/dist/browser/browserSupervisor.js +186 -0
- package/dist/browser/browserSupervisor.js.map +1 -0
- package/dist/browser/cdp.d.ts +1 -0
- package/dist/browser/cdp.d.ts.map +1 -0
- package/dist/browser/cdp.js +6 -5
- package/dist/browser/cdp.js.map +1 -1
- package/dist/browser/channelSelection.d.ts +45 -14
- package/dist/browser/channelSelection.d.ts.map +1 -0
- package/dist/browser/channelSelection.js +81 -18
- package/dist/browser/channelSelection.js.map +1 -1
- package/dist/browser/consent.d.ts +152 -0
- package/dist/browser/consent.d.ts.map +1 -0
- package/dist/browser/consent.js +418 -0
- package/dist/browser/consent.js.map +1 -0
- package/dist/browser/display.d.ts +1 -0
- package/dist/browser/display.d.ts.map +1 -0
- package/dist/browser/hlsPlaylistObserver.d.ts +55 -0
- package/dist/browser/hlsPlaylistObserver.d.ts.map +1 -0
- package/dist/browser/hlsPlaylistObserver.js +132 -0
- package/dist/browser/hlsPlaylistObserver.js.map +1 -0
- package/dist/browser/index.d.ts +67 -40
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +508 -300
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/launchGovernor.d.ts +89 -0
- package/dist/browser/launchGovernor.d.ts.map +1 -0
- package/dist/browser/launchGovernor.js +96 -0
- package/dist/browser/launchGovernor.js.map +1 -0
- package/dist/browser/login.d.ts +10 -2
- package/dist/browser/login.d.ts.map +1 -0
- package/dist/browser/login.js +36 -5
- package/dist/browser/login.js.map +1 -1
- package/dist/browser/manifestInterceptor.d.ts +102 -49
- package/dist/browser/manifestInterceptor.d.ts.map +1 -0
- package/dist/browser/manifestInterceptor.js +219 -237
- package/dist/browser/manifestInterceptor.js.map +1 -1
- package/dist/browser/pageStaleness.d.ts +30 -0
- package/dist/browser/pageStaleness.d.ts.map +1 -0
- package/dist/browser/pageStaleness.js +76 -0
- package/dist/browser/pageStaleness.js.map +1 -0
- package/dist/browser/precaching.d.ts +95 -3
- package/dist/browser/precaching.d.ts.map +1 -0
- package/dist/browser/precaching.js +499 -65
- package/dist/browser/precaching.js.map +1 -1
- package/dist/browser/tabNetworkObserver.d.ts +49 -0
- package/dist/browser/tabNetworkObserver.d.ts.map +1 -0
- package/dist/browser/tabNetworkObserver.js +144 -0
- package/dist/browser/tabNetworkObserver.js.map +1 -0
- package/dist/browser/tuning/cache.d.ts +61 -0
- package/dist/browser/tuning/cache.d.ts.map +1 -0
- package/dist/browser/tuning/cache.js +62 -0
- package/dist/browser/tuning/cache.js.map +1 -0
- package/dist/browser/tuning/comcastPolymer.d.ts +3 -2
- package/dist/browser/tuning/comcastPolymer.d.ts.map +1 -0
- package/dist/browser/tuning/comcastPolymer.js +79 -102
- package/dist/browser/tuning/comcastPolymer.js.map +1 -1
- package/dist/browser/tuning/cox.d.ts +1 -0
- package/dist/browser/tuning/cox.d.ts.map +1 -0
- package/dist/browser/tuning/directv.d.ts +1 -0
- package/dist/browser/tuning/directv.d.ts.map +1 -0
- package/dist/browser/tuning/directv.js +52 -46
- package/dist/browser/tuning/directv.js.map +1 -1
- package/dist/browser/tuning/fox.d.ts +1 -0
- package/dist/browser/tuning/fox.d.ts.map +1 -0
- package/dist/browser/tuning/fox.js +13 -12
- package/dist/browser/tuning/fox.js.map +1 -1
- package/dist/browser/tuning/gridSearch.d.ts +68 -0
- package/dist/browser/tuning/gridSearch.d.ts.map +1 -0
- package/dist/browser/tuning/gridSearch.js +85 -0
- package/dist/browser/tuning/gridSearch.js.map +1 -0
- package/dist/browser/tuning/hbo.d.ts +1 -0
- package/dist/browser/tuning/hbo.d.ts.map +1 -0
- package/dist/browser/tuning/hbo.js +73 -45
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/tuning/hulu.d.ts +1 -0
- package/dist/browser/tuning/hulu.d.ts.map +1 -0
- package/dist/browser/tuning/hulu.js +128 -173
- package/dist/browser/tuning/hulu.js.map +1 -1
- package/dist/browser/tuning/shared.d.ts +75 -5
- package/dist/browser/tuning/shared.d.ts.map +1 -0
- package/dist/browser/tuning/shared.js +216 -6
- package/dist/browser/tuning/shared.js.map +1 -1
- package/dist/browser/tuning/sling.d.ts +1 -0
- package/dist/browser/tuning/sling.d.ts.map +1 -0
- package/dist/browser/tuning/sling.js +105 -126
- package/dist/browser/tuning/sling.js.map +1 -1
- package/dist/browser/tuning/spectrum.d.ts +1 -0
- package/dist/browser/tuning/spectrum.d.ts.map +1 -0
- package/dist/browser/tuning/spectrum.js +80 -125
- package/dist/browser/tuning/spectrum.js.map +1 -1
- package/dist/browser/tuning/thumbnailRow.d.ts +1 -0
- package/dist/browser/tuning/thumbnailRow.d.ts.map +1 -0
- package/dist/browser/tuning/thumbnailRow.js +8 -2
- package/dist/browser/tuning/thumbnailRow.js.map +1 -1
- package/dist/browser/tuning/tileClick.d.ts +1 -0
- package/dist/browser/tuning/tileClick.d.ts.map +1 -0
- package/dist/browser/tuning/tileClick.js +3 -2
- package/dist/browser/tuning/tileClick.js.map +1 -1
- package/dist/browser/tuning/xfinity.d.ts +1 -0
- package/dist/browser/tuning/xfinity.d.ts.map +1 -0
- package/dist/browser/tuning/youtubeTv.d.ts +1 -0
- package/dist/browser/tuning/youtubeTv.d.ts.map +1 -0
- package/dist/browser/tuning/youtubeTv.js +74 -127
- package/dist/browser/tuning/youtubeTv.js.map +1 -1
- package/dist/browser/video.d.ts +54 -14
- package/dist/browser/video.d.ts.map +1 -0
- package/dist/browser/video.js +296 -138
- package/dist/browser/video.js.map +1 -1
- package/dist/channels/index.d.ts +1 -0
- package/dist/channels/index.d.ts.map +1 -0
- package/dist/channels/index.js +12 -9
- package/dist/channels/index.js.map +1 -1
- package/dist/config/channelForm.d.ts +15 -1
- package/dist/config/channelForm.d.ts.map +1 -0
- package/dist/config/channelForm.js +28 -12
- package/dist/config/channelForm.js.map +1 -1
- package/dist/config/consistencyProbe.d.ts +1 -0
- package/dist/config/consistencyProbe.d.ts.map +1 -0
- package/dist/config/consistencyProbe.js +27 -21
- package/dist/config/consistencyProbe.js.map +1 -1
- package/dist/config/health.d.ts +41 -8
- package/dist/config/health.d.ts.map +1 -0
- package/dist/config/health.js +189 -38
- package/dist/config/health.js.map +1 -1
- package/dist/config/index.d.ts +55 -3
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +320 -69
- package/dist/config/index.js.map +1 -1
- package/dist/config/paths.d.ts +10 -9
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +11 -11
- package/dist/config/paths.js.map +1 -1
- package/dist/config/persistence.context.d.ts +1 -0
- package/dist/config/persistence.context.d.ts.map +1 -0
- package/dist/config/persistence.d.ts +13 -10
- package/dist/config/persistence.d.ts.map +1 -0
- package/dist/config/persistence.js +40 -21
- package/dist/config/persistence.js.map +1 -1
- package/dist/config/presets.d.ts +1 -0
- package/dist/config/presets.d.ts.map +1 -0
- package/dist/config/profiles.d.ts +22 -15
- package/dist/config/profiles.d.ts.map +1 -0
- package/dist/config/profiles.js +81 -47
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/providerLineups.d.ts +47 -0
- package/dist/config/providerLineups.d.ts.map +1 -0
- package/dist/config/providerLineups.js +137 -0
- package/dist/config/providerLineups.js.map +1 -0
- package/dist/config/reactivity.d.ts +84 -0
- package/dist/config/reactivity.d.ts.map +1 -0
- package/dist/config/reactivity.js +226 -0
- package/dist/config/reactivity.js.map +1 -0
- package/dist/config/servicePacks.d.ts +18 -2
- package/dist/config/servicePacks.d.ts.map +1 -0
- package/dist/config/servicePacks.js +53 -11
- package/dist/config/servicePacks.js.map +1 -1
- package/dist/config/services.d.ts +21 -15
- package/dist/config/services.d.ts.map +1 -0
- package/dist/config/services.js +35 -24
- package/dist/config/services.js.map +1 -1
- package/dist/config/sites.d.ts +6 -4
- package/dist/config/sites.d.ts.map +1 -0
- package/dist/config/sites.js +15 -11
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.d.ts +129 -30
- package/dist/config/userChannels.d.ts.map +1 -0
- package/dist/config/userChannels.js +333 -68
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +14 -9
- package/dist/config/userConfig.d.ts.map +1 -0
- package/dist/config/userConfig.js +87 -25
- package/dist/config/userConfig.js.map +1 -1
- package/dist/config/userProfiles.d.ts +10 -6
- package/dist/config/userProfiles.d.ts.map +1 -0
- package/dist/config/userProfiles.js +64 -39
- package/dist/config/userProfiles.js.map +1 -1
- package/dist/hdhr/channelMap.d.ts +1 -0
- package/dist/hdhr/channelMap.d.ts.map +1 -0
- package/dist/hdhr/deviceId.d.ts +1 -0
- package/dist/hdhr/deviceId.d.ts.map +1 -0
- package/dist/hdhr/discover.d.ts +1 -0
- package/dist/hdhr/discover.d.ts.map +1 -0
- package/dist/hdhr/discover.js +48 -47
- package/dist/hdhr/discover.js.map +1 -1
- package/dist/hdhr/getHandlers.d.ts +29 -0
- package/dist/hdhr/getHandlers.d.ts.map +1 -0
- package/dist/hdhr/getHandlers.js +196 -0
- package/dist/hdhr/getHandlers.js.map +1 -0
- package/dist/hdhr/identity.d.ts +36 -0
- package/dist/hdhr/identity.d.ts.map +1 -0
- package/dist/hdhr/identity.js +51 -0
- package/dist/hdhr/identity.js.map +1 -0
- package/dist/hdhr/index.d.ts +22 -4
- package/dist/hdhr/index.d.ts.map +1 -0
- package/dist/hdhr/index.js +229 -54
- package/dist/hdhr/index.js.map +1 -1
- package/dist/hdhr/protocol.d.ts +75 -0
- package/dist/hdhr/protocol.d.ts.map +1 -0
- package/dist/hdhr/protocol.js +264 -0
- package/dist/hdhr/protocol.js.map +1 -0
- package/dist/hdhr/tunerState.d.ts +24 -0
- package/dist/hdhr/tunerState.d.ts.map +1 -0
- package/dist/hdhr/tunerState.js +67 -0
- package/dist/hdhr/tunerState.js.map +1 -0
- package/dist/hdhr/udp.d.ts +43 -0
- package/dist/hdhr/udp.d.ts.map +1 -0
- package/dist/hdhr/udp.js +277 -0
- package/dist/hdhr/udp.js.map +1 -0
- package/dist/identity.d.ts +1 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +13 -11
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -12
- package/dist/index.js.map +1 -1
- package/dist/native/codecInference.d.ts +1 -0
- package/dist/native/codecInference.d.ts.map +1 -0
- package/dist/native/codecInference.js +50 -5
- package/dist/native/codecInference.js.map +1 -1
- package/dist/native/decrypt.d.ts +6 -2
- package/dist/native/decrypt.d.ts.map +1 -0
- package/dist/native/decrypt.js +15 -8
- package/dist/native/decrypt.js.map +1 -1
- package/dist/native/index.d.ts +40 -17
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +281 -120
- package/dist/native/index.js.map +1 -1
- package/dist/native/probe.d.ts +114 -18
- package/dist/native/probe.d.ts.map +1 -0
- package/dist/native/probe.js +452 -115
- package/dist/native/probe.js.map +1 -1
- package/dist/native/proxy.d.ts +57 -5
- package/dist/native/proxy.d.ts.map +1 -0
- package/dist/native/proxy.js +604 -170
- package/dist/native/proxy.js.map +1 -1
- package/dist/native/tokenExpiry.d.ts +1 -0
- package/dist/native/tokenExpiry.d.ts.map +1 -0
- package/dist/native/tokenExpiry.js +1 -1
- package/dist/routes/assets.d.ts +1 -0
- package/dist/routes/assets.d.ts.map +1 -0
- package/dist/routes/assets.js +4 -2
- package/dist/routes/assets.js.map +1 -1
- package/dist/routes/auth.d.ts +1 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +0 -1
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/cdp.d.ts +5 -5
- package/dist/routes/cdp.d.ts.map +1 -0
- package/dist/routes/cdp.js +59 -20
- package/dist/routes/cdp.js.map +1 -1
- package/dist/routes/channels.d.ts +1 -0
- package/dist/routes/channels.d.ts.map +1 -0
- package/dist/routes/channels.js +2 -1
- package/dist/routes/channels.js.map +1 -1
- package/dist/routes/clientActions.d.ts +98 -0
- package/dist/routes/clientActions.d.ts.map +1 -0
- package/dist/routes/clientActions.js +113 -0
- package/dist/routes/clientActions.js.map +1 -0
- package/dist/routes/components.d.ts +5 -3
- package/dist/routes/components.d.ts.map +1 -0
- package/dist/routes/components.js +36 -39
- package/dist/routes/components.js.map +1 -1
- package/dist/routes/config/channels/endpoints/browse.d.ts +1 -0
- package/dist/routes/config/channels/endpoints/browse.d.ts.map +1 -0
- package/dist/routes/config/channels/endpoints/browse.js +23 -10
- package/dist/routes/config/channels/endpoints/browse.js.map +1 -1
- package/dist/routes/config/channels/endpoints/bulk.d.ts +1 -0
- package/dist/routes/config/channels/endpoints/bulk.d.ts.map +1 -0
- package/dist/routes/config/channels/endpoints/bulk.js +5 -3
- package/dist/routes/config/channels/endpoints/bulk.js.map +1 -1
- package/dist/routes/config/channels/endpoints/crud.d.ts +1 -0
- package/dist/routes/config/channels/endpoints/crud.d.ts.map +1 -0
- package/dist/routes/config/channels/endpoints/crud.js +85 -93
- package/dist/routes/config/channels/endpoints/crud.js.map +1 -1
- package/dist/routes/config/channels/endpoints/importExport.d.ts +1 -0
- package/dist/routes/config/channels/endpoints/importExport.d.ts.map +1 -0
- package/dist/routes/config/channels/endpoints/importExport.js +6 -3
- package/dist/routes/config/channels/endpoints/importExport.js.map +1 -1
- package/dist/routes/config/channels/endpoints/predefined.d.ts +1 -0
- package/dist/routes/config/channels/endpoints/predefined.d.ts.map +1 -0
- package/dist/routes/config/channels/endpoints/prefs.d.ts +1 -0
- package/dist/routes/config/channels/endpoints/prefs.d.ts.map +1 -0
- package/dist/routes/config/channels/endpoints/prefs.js +2 -1
- package/dist/routes/config/channels/endpoints/prefs.js.map +1 -1
- package/dist/routes/config/channels/endpoints/service.d.ts +2 -1
- package/dist/routes/config/channels/endpoints/service.d.ts.map +1 -0
- package/dist/routes/config/channels/endpoints/service.js +3 -1
- package/dist/routes/config/channels/endpoints/service.js.map +1 -1
- package/dist/routes/config/channels/endpoints/tags.d.ts +1 -0
- package/dist/routes/config/channels/endpoints/tags.d.ts.map +1 -0
- package/dist/routes/config/channels/endpoints/tags.js +13 -7
- package/dist/routes/config/channels/endpoints/tags.js.map +1 -1
- package/dist/routes/config/channels/healthBridge.d.ts +24 -0
- package/dist/routes/config/channels/healthBridge.d.ts.map +1 -0
- package/dist/routes/config/channels/healthBridge.js +71 -0
- package/dist/routes/config/channels/healthBridge.js.map +1 -0
- package/dist/routes/config/channels/http/handler.d.ts +1 -0
- package/dist/routes/config/channels/http/handler.d.ts.map +1 -0
- package/dist/routes/config/channels/http/playlistHint.d.ts +2 -1
- package/dist/routes/config/channels/http/playlistHint.d.ts.map +1 -0
- package/dist/routes/config/channels/http/playlistHint.js +16 -2
- package/dist/routes/config/channels/http/playlistHint.js.map +1 -1
- package/dist/routes/config/channels/http/serviceWarning.d.ts +4 -2
- package/dist/routes/config/channels/http/serviceWarning.d.ts.map +1 -0
- package/dist/routes/config/channels/http/serviceWarning.js +3 -2
- package/dist/routes/config/channels/http/serviceWarning.js.map +1 -1
- package/dist/routes/config/channels/index.d.ts +1 -0
- package/dist/routes/config/channels/index.d.ts.map +1 -0
- package/dist/routes/config/channels/setup.d.ts +1 -0
- package/dist/routes/config/channels/setup.d.ts.map +1 -0
- package/dist/routes/config/channels/table.d.ts +3 -3
- package/dist/routes/config/channels/table.d.ts.map +1 -0
- package/dist/routes/config/channels/table.js +148 -107
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/http/envelope.d.ts +1 -0
- package/dist/routes/config/http/envelope.d.ts.map +1 -0
- package/dist/routes/config/index.d.ts +35 -3
- package/dist/routes/config/index.d.ts.map +1 -0
- package/dist/routes/config/index.js +51 -2
- package/dist/routes/config/index.js.map +1 -1
- package/dist/routes/config/services.d.ts +5 -4
- package/dist/routes/config/services.d.ts.map +1 -0
- package/dist/routes/config/services.js +57 -43
- package/dist/routes/config/services.js.map +1 -1
- package/dist/routes/config/settings.d.ts +1 -0
- package/dist/routes/config/settings.d.ts.map +1 -0
- package/dist/routes/config/settings.js +98 -49
- package/dist/routes/config/settings.js.map +1 -1
- package/dist/routes/debug.d.ts +1 -0
- package/dist/routes/debug.d.ts.map +1 -0
- package/dist/routes/debug.js +277 -139
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/health.d.ts +39 -1
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +33 -27
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/hls.d.ts +1 -0
- package/dist/routes/hls.d.ts.map +1 -0
- package/dist/routes/icons.d.ts +1 -0
- package/dist/routes/icons.d.ts.map +1 -0
- package/dist/routes/icons.js +1 -1
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/logs.d.ts +1 -0
- package/dist/routes/logs.d.ts.map +1 -0
- package/dist/routes/logs.js +14 -4
- package/dist/routes/logs.js.map +1 -1
- package/dist/routes/mpegts.d.ts +1 -0
- package/dist/routes/mpegts.d.ts.map +1 -0
- package/dist/routes/play.d.ts +1 -0
- package/dist/routes/play.d.ts.map +1 -0
- package/dist/routes/playlist.d.ts +1 -0
- package/dist/routes/playlist.d.ts.map +1 -0
- package/dist/routes/playlist.js +7 -2
- package/dist/routes/playlist.js.map +1 -1
- package/dist/routes/root/content.d.ts +8 -5
- package/dist/routes/root/content.d.ts.map +1 -0
- package/dist/routes/root/content.js +29 -26
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/index.d.ts +2 -1
- package/dist/routes/root/index.d.ts.map +1 -0
- package/dist/routes/root/index.js +26 -21
- package/dist/routes/root/index.js.map +1 -1
- package/dist/routes/root/scripts/channels.d.ts +6 -0
- package/dist/routes/root/scripts/channels.d.ts.map +1 -0
- package/dist/routes/root/scripts/channels.js +197 -118
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/clientEscape.d.ts +22 -0
- package/dist/routes/root/scripts/clientEscape.d.ts.map +1 -0
- package/dist/routes/root/scripts/clientEscape.js +41 -0
- package/dist/routes/root/scripts/clientEscape.js.map +1 -0
- package/dist/routes/root/scripts/clientUrl.d.ts +23 -0
- package/dist/routes/root/scripts/clientUrl.d.ts.map +1 -0
- package/dist/routes/root/scripts/clientUrl.js +44 -0
- package/dist/routes/root/scripts/clientUrl.js.map +1 -0
- package/dist/routes/root/scripts/config.d.ts +6 -0
- package/dist/routes/root/scripts/config.d.ts.map +1 -0
- package/dist/routes/root/scripts/config.js +110 -24
- package/dist/routes/root/scripts/config.js.map +1 -1
- package/dist/routes/root/scripts/index.d.ts +1 -0
- package/dist/routes/root/scripts/index.d.ts.map +1 -0
- package/dist/routes/root/scripts/shared.d.ts +1 -0
- package/dist/routes/root/scripts/shared.d.ts.map +1 -0
- package/dist/routes/root/scripts/shared.js +112 -26
- package/dist/routes/root/scripts/shared.js.map +1 -1
- package/dist/routes/root/scripts/status.d.ts +1 -0
- package/dist/routes/root/scripts/status.d.ts.map +1 -0
- package/dist/routes/root/scripts/status.handlers.d.ts +23 -33
- package/dist/routes/root/scripts/status.handlers.d.ts.map +1 -0
- package/dist/routes/root/scripts/status.handlers.js +46 -92
- package/dist/routes/root/scripts/status.handlers.js.map +1 -1
- package/dist/routes/root/scripts/status.js +21 -8
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/routes/root/styles.d.ts +1 -0
- package/dist/routes/root/styles.d.ts.map +1 -0
- package/dist/routes/root/styles.js +7 -4
- package/dist/routes/root/styles.js.map +1 -1
- package/dist/routes/services.d.ts +13 -1
- package/dist/routes/services.d.ts.map +1 -0
- package/dist/routes/services.js +81 -68
- package/dist/routes/services.js.map +1 -1
- package/dist/routes/sse.d.ts +1 -0
- package/dist/routes/sse.d.ts.map +1 -0
- package/dist/routes/sse.js +1 -1
- package/dist/routes/sse.js.map +1 -1
- package/dist/routes/streams.d.ts +2 -1
- package/dist/routes/streams.d.ts.map +1 -0
- package/dist/routes/streams.js +6 -4
- package/dist/routes/streams.js.map +1 -1
- package/dist/routes/theme.d.ts +3 -2
- package/dist/routes/theme.d.ts.map +1 -0
- package/dist/routes/theme.js +6 -5
- package/dist/routes/theme.js.map +1 -1
- package/dist/routes/ui.d.ts +3 -1
- package/dist/routes/ui.d.ts.map +1 -0
- package/dist/routes/ui.js +16 -4
- package/dist/routes/ui.js.map +1 -1
- package/dist/routes/upgrade.d.ts +1 -0
- package/dist/routes/upgrade.d.ts.map +1 -0
- package/dist/routes/upgrade.js +6 -2
- package/dist/routes/upgrade.js.map +1 -1
- package/dist/service/commands.context.d.ts +1 -0
- package/dist/service/commands.context.d.ts.map +1 -0
- package/dist/service/commands.context.js +4 -3
- package/dist/service/commands.context.js.map +1 -1
- package/dist/service/commands.d.ts +1 -0
- package/dist/service/commands.d.ts.map +1 -0
- package/dist/service/commands.js +10 -13
- package/dist/service/commands.js.map +1 -1
- package/dist/service/generators.context.d.ts +1 -0
- package/dist/service/generators.context.d.ts.map +1 -0
- package/dist/service/generators.context.js +1 -1
- package/dist/service/generators.context.js.map +1 -1
- package/dist/service/generators.d.ts +4 -3
- package/dist/service/generators.d.ts.map +1 -0
- package/dist/service/generators.js +111 -31
- package/dist/service/generators.js.map +1 -1
- package/dist/service/index.d.ts +1 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/streaming/captureLock.d.ts +44 -0
- package/dist/streaming/captureLock.d.ts.map +1 -0
- package/dist/streaming/captureLock.js +95 -0
- package/dist/streaming/captureLock.js.map +1 -0
- package/dist/streaming/captureSession.d.ts +31 -0
- package/dist/streaming/captureSession.d.ts.map +1 -0
- package/dist/streaming/captureSession.js +54 -0
- package/dist/streaming/captureSession.js.map +1 -0
- package/dist/streaming/clients.d.ts +1 -0
- package/dist/streaming/clients.d.ts.map +1 -0
- package/dist/streaming/codec.d.ts +1 -0
- package/dist/streaming/codec.d.ts.map +1 -0
- package/dist/streaming/fmp4Segmenter.d.ts +29 -2
- package/dist/streaming/fmp4Segmenter.d.ts.map +1 -0
- package/dist/streaming/fmp4Segmenter.js +79 -26
- package/dist/streaming/fmp4Segmenter.js.map +1 -1
- package/dist/streaming/hls.d.ts +18 -7
- package/dist/streaming/hls.d.ts.map +1 -0
- package/dist/streaming/hls.js +220 -120
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.d.ts +1 -0
- package/dist/streaming/hlsResume.d.ts.map +1 -0
- package/dist/streaming/hlsResume.js +1 -1
- package/dist/streaming/hlsResume.js.map +1 -1
- package/dist/streaming/hlsSegments.d.ts +59 -2
- package/dist/streaming/hlsSegments.d.ts.map +1 -0
- package/dist/streaming/hlsSegments.js +147 -17
- package/dist/streaming/hlsSegments.js.map +1 -1
- package/dist/streaming/lifecycle.d.ts +4 -1
- package/dist/streaming/lifecycle.d.ts.map +1 -0
- package/dist/streaming/lifecycle.js +65 -85
- package/dist/streaming/lifecycle.js.map +1 -1
- package/dist/streaming/monitor.d.ts +6 -4
- package/dist/streaming/monitor.d.ts.map +1 -0
- package/dist/streaming/monitor.js +343 -145
- package/dist/streaming/monitor.js.map +1 -1
- package/dist/streaming/mp4Parser.d.ts +5 -4
- package/dist/streaming/mp4Parser.d.ts.map +1 -0
- package/dist/streaming/mp4Parser.js +36 -13
- package/dist/streaming/mp4Parser.js.map +1 -1
- package/dist/streaming/mpegts.d.ts +16 -0
- package/dist/streaming/mpegts.d.ts.map +1 -0
- package/dist/streaming/mpegts.js +61 -17
- package/dist/streaming/mpegts.js.map +1 -1
- package/dist/streaming/playlistBuilder.d.ts +1 -0
- package/dist/streaming/playlistBuilder.d.ts.map +1 -0
- package/dist/streaming/preroll.d.ts +32 -5
- package/dist/streaming/preroll.d.ts.map +1 -0
- package/dist/streaming/preroll.js +70 -32
- package/dist/streaming/preroll.js.map +1 -1
- package/dist/streaming/pretune.d.ts +26 -1
- package/dist/streaming/pretune.d.ts.map +1 -0
- package/dist/streaming/pretune.js +22 -24
- package/dist/streaming/pretune.js.map +1 -1
- package/dist/streaming/pretuneTimers.d.ts +24 -0
- package/dist/streaming/pretuneTimers.d.ts.map +1 -0
- package/dist/streaming/pretuneTimers.js +56 -0
- package/dist/streaming/pretuneTimers.js.map +1 -0
- package/dist/streaming/recovery.d.ts +139 -5
- package/dist/streaming/recovery.d.ts.map +1 -0
- package/dist/streaming/recovery.js +160 -16
- package/dist/streaming/recovery.js.map +1 -1
- package/dist/streaming/registry.d.ts +27 -9
- package/dist/streaming/registry.d.ts.map +1 -0
- package/dist/streaming/registry.js +21 -3
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +157 -29
- package/dist/streaming/setup.d.ts.map +1 -0
- package/dist/streaming/setup.js +1034 -458
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.d.ts +21 -1
- package/dist/streaming/showInfo.d.ts.map +1 -0
- package/dist/streaming/showInfo.js +31 -16
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +10 -10
- package/dist/streaming/statusEmitter.d.ts.map +1 -0
- package/dist/streaming/statusEmitter.js +5 -8
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/channels.d.ts +16 -4
- package/dist/types/channels.d.ts.map +1 -0
- package/dist/types/channels.js +4 -2
- package/dist/types/channels.js.map +1 -1
- package/dist/types/config.d.ts +16 -5
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/profiles.d.ts +6 -4
- package/dist/types/profiles.d.ts.map +1 -0
- package/dist/types/selection.d.ts +31 -3
- package/dist/types/selection.d.ts.map +1 -0
- package/dist/types/selection.js +4 -2
- package/dist/types/selection.js.map +1 -1
- package/dist/types/shared.d.ts +1 -0
- package/dist/types/shared.d.ts.map +1 -0
- package/dist/types/streaming.d.ts +8 -0
- package/dist/types/streaming.d.ts.map +1 -0
- package/dist/upgrade/commands.context.d.ts +1 -0
- package/dist/upgrade/commands.context.d.ts.map +1 -0
- package/dist/upgrade/commands.context.js +14 -10
- package/dist/upgrade/commands.context.js.map +1 -1
- package/dist/upgrade/commands.d.ts +8 -15
- package/dist/upgrade/commands.d.ts.map +1 -0
- package/dist/upgrade/commands.js +27 -28
- package/dist/upgrade/commands.js.map +1 -1
- package/dist/upgrade/detection.context.d.ts +1 -0
- package/dist/upgrade/detection.context.d.ts.map +1 -0
- package/dist/upgrade/detection.context.js +4 -1
- package/dist/upgrade/detection.context.js.map +1 -1
- package/dist/upgrade/detection.d.ts +9 -7
- package/dist/upgrade/detection.d.ts.map +1 -0
- package/dist/upgrade/detection.js +23 -54
- package/dist/upgrade/detection.js.map +1 -1
- package/dist/upgrade/index.d.ts +2 -0
- package/dist/upgrade/index.d.ts.map +1 -0
- package/dist/upgrade/index.js +1 -0
- package/dist/upgrade/index.js.map +1 -1
- package/dist/upgrade/lifecycle.context.d.ts +20 -0
- package/dist/upgrade/lifecycle.context.d.ts.map +1 -0
- package/dist/upgrade/lifecycle.context.js +47 -0
- package/dist/upgrade/lifecycle.context.js.map +1 -0
- package/dist/upgrade/lifecycle.d.ts +72 -0
- package/dist/upgrade/lifecycle.d.ts.map +1 -0
- package/dist/upgrade/lifecycle.js +175 -0
- package/dist/upgrade/lifecycle.js.map +1 -0
- package/dist/upgrade/pathHandle.d.ts +30 -0
- package/dist/upgrade/pathHandle.d.ts.map +1 -0
- package/dist/upgrade/pathHandle.js +102 -0
- package/dist/upgrade/pathHandle.js.map +1 -0
- package/dist/utils/bootSession.context.d.ts +16 -0
- package/dist/utils/bootSession.context.d.ts.map +1 -0
- package/dist/utils/bootSession.context.js +60 -0
- package/dist/utils/bootSession.context.js.map +1 -0
- package/dist/utils/bootSession.d.ts +17 -0
- package/dist/utils/bootSession.d.ts.map +1 -0
- package/dist/utils/bootSession.js +20 -0
- package/dist/utils/bootSession.js.map +1 -0
- package/dist/utils/chromeFetch.d.ts +1 -0
- package/dist/utils/chromeFetch.d.ts.map +1 -0
- package/dist/utils/cliOutput.d.ts +1 -0
- package/dist/utils/cliOutput.d.ts.map +1 -0
- package/dist/utils/clock.d.ts +5 -4
- package/dist/utils/clock.d.ts.map +1 -0
- package/dist/utils/clock.js +19 -16
- package/dist/utils/clock.js.map +1 -1
- package/dist/utils/debugFilter.d.ts +9 -1
- package/dist/utils/debugFilter.d.ts.map +1 -0
- package/dist/utils/debugFilter.js +60 -33
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/utils/delay.d.ts +48 -15
- package/dist/utils/delay.d.ts.map +1 -0
- package/dist/utils/delay.js +73 -20
- package/dist/utils/delay.js.map +1 -1
- package/dist/utils/errors.d.ts +32 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +45 -4
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/evaluate.d.ts +1 -0
- package/dist/utils/evaluate.d.ts.map +1 -0
- package/dist/utils/evaluate.js +29 -23
- package/dist/utils/evaluate.js.map +1 -1
- package/dist/utils/ffmpeg.context.d.ts +1 -0
- package/dist/utils/ffmpeg.context.d.ts.map +1 -0
- package/dist/utils/ffmpeg.context.js.map +1 -1
- package/dist/utils/ffmpeg.d.ts +29 -7
- package/dist/utils/ffmpeg.d.ts.map +1 -0
- package/dist/utils/ffmpeg.js +32 -14
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/fileLogger.d.ts +3 -1
- package/dist/utils/fileLogger.d.ts.map +1 -0
- package/dist/utils/fileLogger.js +64 -29
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/format.d.ts +14 -1
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +19 -5
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logEmitter.d.ts +1 -0
- package/dist/utils/logEmitter.d.ts.map +1 -0
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +10 -6
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/m3u.d.ts +3 -2
- package/dist/utils/m3u.d.ts.map +1 -0
- package/dist/utils/m3u.js +4 -3
- package/dist/utils/m3u.js.map +1 -1
- package/dist/utils/markup.d.ts +13 -1
- package/dist/utils/markup.d.ts.map +1 -0
- package/dist/utils/markup.js +28 -4
- package/dist/utils/markup.js.map +1 -1
- package/dist/utils/memo.d.ts +3 -2
- package/dist/utils/memo.d.ts.map +1 -0
- package/dist/utils/memo.js +3 -3
- package/dist/utils/morganStream.d.ts +1 -0
- package/dist/utils/morganStream.d.ts.map +1 -0
- package/dist/utils/network.d.ts +1 -0
- package/dist/utils/network.d.ts.map +1 -0
- package/dist/utils/network.js +2 -2
- package/dist/utils/pid.d.ts +3 -25
- package/dist/utils/pid.d.ts.map +1 -0
- package/dist/utils/pid.js +10 -49
- package/dist/utils/pid.js.map +1 -1
- package/dist/utils/platform.d.ts +3 -1
- package/dist/utils/platform.d.ts.map +1 -0
- package/dist/utils/platform.js +2 -1
- package/dist/utils/platform.js.map +1 -1
- package/dist/utils/processInspector.context.d.ts +8 -0
- package/dist/utils/processInspector.context.d.ts.map +1 -0
- package/dist/utils/processInspector.context.js +106 -0
- package/dist/utils/processInspector.context.js.map +1 -0
- package/dist/utils/processInspector.d.ts +55 -0
- package/dist/utils/processInspector.d.ts.map +1 -0
- package/dist/utils/processInspector.js +127 -0
- package/dist/utils/processInspector.js.map +1 -0
- package/dist/utils/retry.d.ts +25 -2
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +33 -6
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/runtimeIdentity.context.d.ts +7 -0
- package/dist/utils/runtimeIdentity.context.d.ts.map +1 -0
- package/dist/utils/runtimeIdentity.context.js +43 -0
- package/dist/utils/runtimeIdentity.context.js.map +1 -0
- package/dist/utils/runtimeIdentity.d.ts +98 -0
- package/dist/utils/runtimeIdentity.d.ts.map +1 -0
- package/dist/utils/runtimeIdentity.js +181 -0
- package/dist/utils/runtimeIdentity.js.map +1 -0
- package/dist/utils/sanitize.d.ts +1 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/streamContext.d.ts +2 -1
- package/dist/utils/streamContext.d.ts.map +1 -0
- package/dist/utils/streamContext.js +1 -1
- package/dist/utils/timing.d.ts +2 -1
- package/dist/utils/timing.d.ts.map +1 -0
- package/dist/utils/timing.js +0 -5
- package/dist/utils/timing.js.map +1 -1
- package/dist/utils/version.d.ts +1 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +47 -13
- package/dist/utils/version.js.map +1 -1
- package/package.json +26 -20
package/dist/browser/index.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { LOG, cancellableTimeout, clearPidFile, delay, evaluateWithAbort, formatError, isProcessRunning, readPidFile, startTimer, writePidFile } from "../utils/index.js";
|
|
1
|
+
import { LOG, boundedWait, delay, evaluateWithAbort, formatError, isProcessRunning, listProcesses, realClock, startTimer } from "../utils/index.js";
|
|
3
2
|
import { clearLoginState, isLoginModeActive, setBrowserAccessors } from "./login.js";
|
|
4
3
|
import { getAllStreams, getStreamCount } from "../streaming/registry.js";
|
|
5
|
-
import { getChromeDataDir,
|
|
4
|
+
import { getChromeDataDir, getDataDir, getExtensionDir } from "../config/paths.js";
|
|
6
5
|
import { getEffectivePreset, getPresetViewport } from "../config/presets.js";
|
|
7
6
|
import { getExtensionPage, getStream, launch } from "puppeteer-stream";
|
|
7
|
+
import { getGpuCapabilities, setBrowserChrome, setGpuCapabilities, setMaxSupportedViewport } from "./display.js";
|
|
8
8
|
import { resizeAndMinimizeWindow, unminimizeWindow } from "./cdp.js";
|
|
9
9
|
import { CONFIG } from "../config/index.js";
|
|
10
10
|
import { clearChannelSelectionCaches } from "./channelSelection.js";
|
|
11
|
+
import { createBrowserSupervisor } from "./browserSupervisor.js";
|
|
11
12
|
import { emitSystemStatusChanged } from "../streaming/statusEmitter.js";
|
|
13
|
+
import { evaluateStalePages } from "./pageStaleness.js";
|
|
12
14
|
import fs from "node:fs";
|
|
13
15
|
import path from "node:path";
|
|
14
16
|
import { launch as puppeteerLaunch } from "puppeteer-core";
|
|
@@ -19,34 +21,101 @@ const { promises: fsPromises } = fs;
|
|
|
19
21
|
/* Global variables maintain the application's runtime state across all operations. We minimize global state where possible, but some values must be shared across
|
|
20
22
|
* the application lifecycle:
|
|
21
23
|
*
|
|
22
|
-
* -
|
|
23
|
-
*
|
|
24
|
+
* - supervisor: The browser capture-readiness supervisor. It is the single source of truth for the shared Chrome instance and its lifecycle (absent / launching /
|
|
25
|
+
* ready / degraded / trialing), so all streaming sessions use one Chrome process via supervisor.acquire(). It holds one discriminated-union lifecycle state that
|
|
26
|
+
* captures the browser reference, its launch timestamp, and whether a launch is in flight, and routes every relaunch through one loop-safe governor.
|
|
24
27
|
*
|
|
25
|
-
* -
|
|
26
|
-
*
|
|
28
|
+
* - currentChromeVersion: The one piece of per-browser metadata the adapter holds directly, captured when the browser becomes ready and surfaced by the
|
|
29
|
+
* health endpoint.
|
|
27
30
|
*
|
|
28
|
-
* Stream tracking and ID generation live in streaming/registry.ts for unified stream management across all output types (HLS, MPEG-TS, etc.).
|
|
31
|
+
* Stream tracking and ID generation live in streaming/registry.ts for unified stream management across all output types (HLS, MPEG-TS, etc.). Filesystem path
|
|
32
|
+
* resolution for persistent data (the Chrome profile and the streaming extension files) is centralized in config/paths.ts, whose getters create the data directory on
|
|
33
|
+
* startup if it does not exist; it is resolved on demand rather than held as module state here.
|
|
29
34
|
*/
|
|
30
|
-
// The
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
// The PID of the Chrome process launched by Puppeteer. Tracked in memory for fast access and persisted to a PID file on disk so that orphaned Chrome processes
|
|
34
|
-
// can be cleaned up after a crash or container restart without relying on Unix-only tools like pkill/pgrep.
|
|
35
|
-
let chromePid = null;
|
|
36
|
-
// Tracks whether this process has taken ownership of Chrome cleanup by running killStaleChrome() during startup. The exit handler checks this flag to avoid
|
|
37
|
-
// killing Chrome that belongs to another running PrismCast instance - e.g., when a duplicate instance is rejected by the instance guard and exits before
|
|
38
|
-
// killStaleChrome() runs in the startup sequence.
|
|
39
|
-
let ownsChromeCleanup = false;
|
|
40
|
-
// The Chrome version string (e.g., "Chrome/144.0.7559.110") captured when the browser launches. Cleared when the browser disconnects. Used by the
|
|
41
|
-
// health endpoint to report the active Chrome version.
|
|
35
|
+
// The Chrome version string (e.g., "Chrome/144.0.7559.110") captured when the browser becomes capture-ready in launchReadyBrowser. Cleared when the browser
|
|
36
|
+
// disconnects or is closed. This is the one piece of per-browser metadata the adapter holds directly; the browser instance, its launch timestamp, and the
|
|
37
|
+
// launch mutex live inside the supervisor's lifecycle state, which is the single source of truth for "what is the browser doing, and is it usable?".
|
|
42
38
|
let currentChromeVersion = null;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
39
|
+
/* The browser relaunch governor's escalating cooldown ladder: 5 minutes, then 15, then 60. This is the escalation SHAPE - a design constant - rather than an
|
|
40
|
+
* operational tolerance, so it stays in code while the scalar tolerances (failure threshold, window, health hold) are operator-tunable via CONFIG.recovery. Each
|
|
41
|
+
* successive trip cools down for the next-longer rung; the final rung is the ceiling.
|
|
42
|
+
*/
|
|
43
|
+
const RELAUNCH_COOLDOWN_LADDER_MS = [5 * 60 * 1000, 15 * 60 * 1000, 60 * 60 * 1000];
|
|
44
|
+
/* How long Chrome is given to exit after SIGTERM, and then after SIGKILL. The escalation is SIGTERM-first so Chrome can flush its profile databases (LevelDB,
|
|
45
|
+
* extension state, session storage) instead of having them corrupted by an immediate kill, and the SIGTERM window is generous because containerized environments
|
|
46
|
+
* with software rendering and shared CPU may need all of it. Every path that signals Chrome - the orderly close of a running instance and the startup sweep of
|
|
47
|
+
* stale ones - shares this pair, so the escalation behaves identically wherever it runs.
|
|
48
|
+
*/
|
|
49
|
+
const TERM_WAIT_MS = 5000;
|
|
50
|
+
const KILL_WAIT_MS = 2000;
|
|
51
|
+
/* The worst case a browser teardown can take before the Chrome process is certainly gone. The supervisor's closing state hands this to requests that arrive
|
|
52
|
+
* mid-drain as their retry horizon, so it is derived from the waits above rather than restated: the bound cannot drift from what the teardown actually allows.
|
|
53
|
+
*/
|
|
54
|
+
const BROWSER_TEARDOWN_DRAIN_BOUND_MS = TERM_WAIT_MS + KILL_WAIT_MS;
|
|
55
|
+
/**
|
|
56
|
+
* Builds the browser relaunch governor's policy from live configuration. The supervisor's policy port is a getter, so this is read fresh at each governor decision -
|
|
57
|
+
* an operator's change to the recovery.relaunch* settings takes effect without reconstructing the supervisor (and a deferred config reload that restarts the server
|
|
58
|
+
* applies it too). The scalar tolerances come from CONFIG.recovery (conservative, biased eager-for-the-first-failure: the first failures cost no cooldown; only
|
|
59
|
+
* repeated failures within the window trip the escalating cooldown); the cooldown ladder is the fixed escalation shape above.
|
|
60
|
+
* @returns The current launch governor policy.
|
|
61
|
+
*/
|
|
62
|
+
function buildRelaunchPolicy() {
|
|
63
|
+
return {
|
|
64
|
+
cooldownLadderMs: RELAUNCH_COOLDOWN_LADDER_MS,
|
|
65
|
+
failureThreshold: CONFIG.recovery.relaunchFailureThreshold,
|
|
66
|
+
failureWindowMs: CONFIG.recovery.relaunchFailureWindow,
|
|
67
|
+
healthHoldMs: CONFIG.recovery.relaunchHealthHold
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/* The one browser capture-readiness supervisor for the process lifetime. It owns the lifecycle state (absent/launching/ready/degraded/trialing) that unifies the
|
|
71
|
+
* browser reference, launch promise, and launch timestamp, and routes every relaunch through one loop-safe governor. The adapter injects the impure
|
|
72
|
+
* ports: launchReadyBrowser (spawn Chrome and run the readiness gate), closeBrowserInstance (teardown), realClock.now (time), buildRelaunchPolicy (live config
|
|
73
|
+
* bounds), and onSupervisorStateChange (the loud degraded alarm and the recovery notice). All browser access flows through it: getCurrentBrowser is acquire(); the
|
|
74
|
+
* non-launching reads derive from current() and currentLaunchTime(). The injected ports are hoisted function declarations, so referencing them here is safe even
|
|
75
|
+
* though they are defined further down the module.
|
|
76
|
+
*/
|
|
77
|
+
const supervisor = createBrowserSupervisor({ close: closeBrowserInstance, launch: launchReadyBrowser, now: realClock.now, onStateChange: onSupervisorStateChange,
|
|
78
|
+
policy: buildRelaunchPolicy });
|
|
79
|
+
/**
|
|
80
|
+
* Observes supervisor lifecycle transitions purely for operator-visible signals; it never affects the transition (the supervisor treats it as best-effort, so a
|
|
81
|
+
* throwing logger cannot corrupt the lifecycle). It raises the loud degraded alarm when the relaunch governor trips, an info notice when capture readiness is
|
|
82
|
+
* restored after a degraded period, and emits the SSE system status whenever a ready browser is published.
|
|
83
|
+
* @param next - The state being entered.
|
|
84
|
+
* @param previous - The state being left.
|
|
85
|
+
*/
|
|
86
|
+
function onSupervisorStateChange(next, previous) {
|
|
87
|
+
// The governor just tripped: relaunches are paused while the browser's capture system cools down. We log loudly at ERROR with the cooldown horizon so the
|
|
88
|
+
// condition is never invisible - the enforceable form of "it is impossible to be silently un-tunable."
|
|
89
|
+
if ((next.kind === "degraded") && (previous.kind !== "degraded")) {
|
|
90
|
+
const cooldownMinutes = Math.max(1, Math.round((next.until - realClock.now()) / 60000));
|
|
91
|
+
LOG.error("The browser capture system has degraded and the relaunch governor has tripped: %s Relaunches are paused for approximately %d minute(s) while it " +
|
|
92
|
+
"cools down; new stream requests will receive a 503 back-off until it recovers.", next.reason, cooldownMinutes);
|
|
93
|
+
}
|
|
94
|
+
else if ((next.kind === "ready") && ((previous.kind === "trialing") || (previous.kind === "degraded"))) {
|
|
95
|
+
// Capture readiness was restored by a successful trial after a degraded period. The ordinary first launch (absent -> launching -> ready) is intentionally
|
|
96
|
+
// silent; only a recovery from trialing/degraded is worth an operator notice.
|
|
97
|
+
LOG.info("The browser capture system has recovered and is serving captures again.");
|
|
98
|
+
}
|
|
99
|
+
// The browser's connectivity is part of the SSE system status, so emit when a ready browser is published. Readiness-loss emits are owned by handleBrowserDisconnect
|
|
100
|
+
// (genuine disconnect) and the shutdown path; emitSystemStatusChanged dedupes, so a redundant emit is a cheap no-op.
|
|
101
|
+
if (next.kind === "ready") {
|
|
102
|
+
void emitCurrentSystemStatus();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/* The capture-readiness probe (capability tier of the launch gate). Null until streaming/setup.ts injects the real getStream probe at module load, which the import
|
|
106
|
+
* order guarantees runs before any launch: app.ts imports the streaming layer, whose module bodies evaluate during import resolution, before startServer's warm-up.
|
|
107
|
+
* launchReadyBrowser refuses to publish a browser if it is somehow still null (see the call site), rather than serving an unverified one.
|
|
108
|
+
*/
|
|
109
|
+
let captureProbe = null;
|
|
110
|
+
/**
|
|
111
|
+
* Injects the capture-readiness probe used as the capability tier of the launch gate. Called once from streaming/setup.ts at module load (which always precedes any
|
|
112
|
+
* launch, since the streaming layer is imported during server startup). Separating the wiring from the call keeps browser/index.ts free of a streaming-layer import.
|
|
113
|
+
* @param probe - The probe to run against a freshly-launched browser; it resolves when the browser can capture and rejects otherwise (or exits the process for the
|
|
114
|
+
* unrecoverable stale-mutex case).
|
|
115
|
+
*/
|
|
116
|
+
export function setCaptureProbe(probe) {
|
|
117
|
+
captureProbe = probe;
|
|
118
|
+
}
|
|
50
119
|
// The stale page cleanup interval handle, stored so we can clear it during graceful shutdown. The interval periodically checks for browser pages that are not
|
|
51
120
|
// associated with active streams and closes them to prevent resource exhaustion.
|
|
52
121
|
let stalePageCleanupInterval = null;
|
|
@@ -59,7 +128,7 @@ const BROWSER_MAX_AGE = 6 * 60 * 60 * 1000;
|
|
|
59
128
|
// Duration of the quiet period (zero streams) required before executing the restart (5 minutes).
|
|
60
129
|
const BROWSER_RESTART_QUIET_PERIOD = 5 * 60 * 1000;
|
|
61
130
|
// How often to check whether the browser qualifies for a restart (30 seconds).
|
|
62
|
-
const BROWSER_RESTART_CHECK_INTERVAL =
|
|
131
|
+
const BROWSER_RESTART_CHECK_INTERVAL = 30000;
|
|
63
132
|
// Timer handle for the quiet period countdown. When set, the browser has exceeded BROWSER_MAX_AGE and we are waiting for BROWSER_RESTART_QUIET_PERIOD to
|
|
64
133
|
// elapse with zero active streams. Cancelled if a stream starts during the quiet period.
|
|
65
134
|
let restartQuietTimer = null;
|
|
@@ -97,10 +166,20 @@ const managedPageIds = new Set();
|
|
|
97
166
|
// Map from page ID to timestamp when a page was first observed as potentially stale (not associated with an active stream). Pages must remain in this state for
|
|
98
167
|
// the configured grace period before being closed. This prevents race conditions where pages are briefly untracked during initialization or cleanup transitions.
|
|
99
168
|
const potentiallyStalePages = new Map();
|
|
169
|
+
/* Set of IDs for pages that stream setup created and whose ownership the stream registry does not yet record. Setup writes the registry's page reference only once
|
|
170
|
+
* it completes, which on a slow tune is long enough for the cleanup walk to see the page as unowned and close it out from under the setup driving it; membership
|
|
171
|
+
* here exempts the page from staleness for exactly that window. An entry leaves the set when unregisterManagedPage releases the page as setup tears it down, when
|
|
172
|
+
* the cleanup walk sees the registry record the ownership the mark stood in for, and when clearPageTracking wipes the collections at the end of a browser session.
|
|
173
|
+
*/
|
|
174
|
+
const inFlightSetupPageIds = new Set();
|
|
100
175
|
// Login mode management. State and functions live in login.ts; re-exported here so existing consumers don't need import path changes. clearLoginState,
|
|
101
176
|
// isLoginModeActive, and setBrowserAccessors are imported above; the first two for internal use, setBrowserAccessors for one-time initialization below.
|
|
102
177
|
export { clearLoginState, isLoginModeActive };
|
|
103
|
-
export { endLoginMode, getLoginPage, getLoginStatus, startLoginMode } from "./login.js";
|
|
178
|
+
export { endLoginMode, getLoginPage, getLoginStatus, setLoginModeEndObserver, startLoginMode } from "./login.js";
|
|
179
|
+
// Re-export the supervisor's acquire() rejection classes through the browser surface so the stream-setup layer can map them to a 503 back-off without reaching into
|
|
180
|
+
// the supervisor module directly. Both signal a transient "retry me" condition: BrowserUnavailableError while the relaunch governor is cooling, BrowserSupersededError
|
|
181
|
+
// when a launch was abandoned mid-flight by a readiness-loss.
|
|
182
|
+
export { BrowserSupersededError, BrowserUnavailableError } from "./browserSupervisor.js";
|
|
104
183
|
// Inject browser accessors into the login module. This breaks the circular dependency (login needs getBrowserInstance/minimizeBrowserWindow, index needs login
|
|
105
184
|
// functions) using the same setter/getter pattern as setChromeUserAgent in chromeFetch.ts. Function declarations are hoisted, so both accessors are available here.
|
|
106
185
|
setBrowserAccessors({ getBrowserInstance, minimizeBrowserWindow });
|
|
@@ -109,9 +188,12 @@ setBrowserAccessors({ getBrowserInstance, minimizeBrowserWindow });
|
|
|
109
188
|
*/
|
|
110
189
|
export async function emitCurrentSystemStatus() {
|
|
111
190
|
let pageCount = 0;
|
|
191
|
+
// The published browser, or null when the supervisor is not in its ready state. Connectivity and page count derive from it - there is no separate browser
|
|
192
|
+
// reference to consult.
|
|
193
|
+
const browser = supervisor.current();
|
|
112
194
|
try {
|
|
113
|
-
if (
|
|
114
|
-
const pages = await
|
|
195
|
+
if (browser?.connected) {
|
|
196
|
+
const pages = await browser.pages();
|
|
115
197
|
pageCount = pages.length;
|
|
116
198
|
}
|
|
117
199
|
}
|
|
@@ -121,7 +203,7 @@ export async function emitCurrentSystemStatus() {
|
|
|
121
203
|
const memUsage = process.memoryUsage();
|
|
122
204
|
const status = {
|
|
123
205
|
browser: {
|
|
124
|
-
connected: !!
|
|
206
|
+
connected: !!browser && browser.connected,
|
|
125
207
|
pageCount
|
|
126
208
|
},
|
|
127
209
|
memory: {
|
|
@@ -143,14 +225,20 @@ export async function emitCurrentSystemStatus() {
|
|
|
143
225
|
* Each registered page receives a unique ID that persists for the page's lifetime. This ID is used for comparison and staleness tracking, avoiding potential
|
|
144
226
|
* issues with Page object reference identity.
|
|
145
227
|
* @param page - The Puppeteer Page to register.
|
|
228
|
+
* @param options - Registration options. Set inFlightSetup when stream setup owns the page but has not yet recorded that ownership in the stream registry, so the
|
|
229
|
+
* stale page cleanup never closes a page whose stream is still being established.
|
|
146
230
|
*/
|
|
147
|
-
export function registerManagedPage(page) {
|
|
231
|
+
export function registerManagedPage(page, options = {}) {
|
|
148
232
|
// Generate a unique ID for this page.
|
|
149
233
|
const pageId = "page-" + String(++managedPageIdCounter);
|
|
150
234
|
// Associate the Page object with its ID.
|
|
151
235
|
pageToId.set(page, pageId);
|
|
152
236
|
// Track the ID as managed.
|
|
153
237
|
managedPageIds.add(pageId);
|
|
238
|
+
// Exempt the page from staleness for as long as its stream setup is in flight.
|
|
239
|
+
if (options.inFlightSetup) {
|
|
240
|
+
inFlightSetupPageIds.add(pageId);
|
|
241
|
+
}
|
|
154
242
|
}
|
|
155
243
|
/**
|
|
156
244
|
* Unregisters a page from PrismCast's management. This should be called when a page is being closed intentionally (during stream cleanup). Unregistering prevents the
|
|
@@ -163,6 +251,8 @@ export function unregisterManagedPage(page) {
|
|
|
163
251
|
managedPageIds.delete(pageId);
|
|
164
252
|
// Also remove from potentially stale tracking since we're intentionally closing it.
|
|
165
253
|
potentiallyStalePages.delete(pageId);
|
|
254
|
+
// Release any in-flight setup mark. The page is leaving our management, so the exemption it carried has nothing left to protect.
|
|
255
|
+
inFlightSetupPageIds.delete(pageId);
|
|
166
256
|
// Note: We don't delete from pageToId because WeakMap handles cleanup automatically when the Page is garbage collected.
|
|
167
257
|
}
|
|
168
258
|
}
|
|
@@ -174,6 +264,18 @@ export function unregisterManagedPage(page) {
|
|
|
174
264
|
function getManagedPageId(page) {
|
|
175
265
|
return pageToId.get(page);
|
|
176
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Discards every page-tracking collection. The ids they hold are scoped to one browser session's pages, so they mean nothing once that session ends and would
|
|
269
|
+
* otherwise carry into the next one - which matters most for the scheduled restart, where the process lives on across the swap. Every path that ends a PUBLISHED
|
|
270
|
+
* browser session routes through here, which is why the collections are cleared in one place rather than at each of those paths. A launch that is superseded
|
|
271
|
+
* before it is ever published does not, and needs no clear: it never held stream pages, and its readiness-gate probe pages are released at their own registration
|
|
272
|
+
* sites. The WeakMap needs no clear - it releases its entries when the Page objects are collected.
|
|
273
|
+
*/
|
|
274
|
+
function clearPageTracking() {
|
|
275
|
+
managedPageIds.clear();
|
|
276
|
+
potentiallyStalePages.clear();
|
|
277
|
+
inFlightSetupPageIds.clear();
|
|
278
|
+
}
|
|
177
279
|
/**
|
|
178
280
|
* Ensures the data directory exists, creating it if necessary. This should be called during application startup before any operations that depend on the data
|
|
179
281
|
* directory (like browser launch or extension preparation).
|
|
@@ -191,6 +293,37 @@ export async function ensureDataDirectory() {
|
|
|
191
293
|
LOG.error("Failed to create data directory %s: %s.", getDataDir(), formatError(error));
|
|
192
294
|
throw error;
|
|
193
295
|
}
|
|
296
|
+
// Purge on-disk artifacts retired in earlier releases. The data directory is the right boundary for this work - it runs once per startup, after the directory
|
|
297
|
+
// exists, before any subsequent step reads it. Future retirements add a single line to purgeLegacyArtifacts; the call site here does not change.
|
|
298
|
+
await purgeLegacyArtifacts();
|
|
299
|
+
}
|
|
300
|
+
const RETIRED_ARTIFACTS = [
|
|
301
|
+
{
|
|
302
|
+
filename: "chrome.pid",
|
|
303
|
+
replacedBy: "OS process-table discovery in utils/processInspector",
|
|
304
|
+
retiredIn: "1.10.3"
|
|
305
|
+
}
|
|
306
|
+
];
|
|
307
|
+
/**
|
|
308
|
+
* Removes every on-disk artifact in RETIRED_ARTIFACTS. Failures are non-fatal: a missing file is the steady-state expectation (fresh installs and any
|
|
309
|
+
* post-first-run startup) and any other I/O error is logged but does not interrupt startup - the user's running configuration is not at risk from a leftover
|
|
310
|
+
* artifact. The data-directory boundary in ensureDataDirectory is the natural caller: it runs once per startup, after the directory exists, before any
|
|
311
|
+
* subsequent step reads it.
|
|
312
|
+
*/
|
|
313
|
+
async function purgeLegacyArtifacts() {
|
|
314
|
+
for (const artifact of RETIRED_ARTIFACTS) {
|
|
315
|
+
const filePath = path.join(getDataDir(), artifact.filename);
|
|
316
|
+
try {
|
|
317
|
+
// eslint-disable-next-line no-await-in-loop
|
|
318
|
+
await fsPromises.unlink(filePath);
|
|
319
|
+
LOG.debug("browser:lifecycle", "Purged legacy artifact %s (retired in %s, replaced by %s).", filePath, artifact.retiredIn, artifact.replacedBy);
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
if (error.code !== "ENOENT") {
|
|
323
|
+
LOG.warn("Failed to remove legacy artifact %s: %s.", filePath, formatError(error));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
194
327
|
}
|
|
195
328
|
/* These functions handle the Chrome browser lifecycle: startup, cleanup, and instance management. The browser is a shared resource used by all streaming sessions,
|
|
196
329
|
* so careful lifecycle management is essential for reliability. Key considerations:
|
|
@@ -206,30 +339,6 @@ export async function ensureDataDirectory() {
|
|
|
206
339
|
* - Extension initialization: The puppeteer-stream extension needs time after browser launch to inject its recording APIs. We wait for this initialization before
|
|
207
340
|
* attempting to capture streams.
|
|
208
341
|
*/
|
|
209
|
-
/**
|
|
210
|
-
* Persists the Chrome process PID to both the module-level variable (fast, in-memory) and a PID file on disk (survives crashes). The PID file allows the next
|
|
211
|
-
* startup to find and terminate orphaned Chrome processes even if the Node process crashed without cleanup.
|
|
212
|
-
* @param pid - The Chrome process ID to save.
|
|
213
|
-
*/
|
|
214
|
-
function saveChromePid(pid) {
|
|
215
|
-
chromePid = pid;
|
|
216
|
-
writePidFile(getChromePidFilePath(), pid, "Chrome", LOG);
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Loads the Chrome PID from the module-level variable (fast path) or falls back to reading the PID file on disk (crash recovery path). Returns null if no PID
|
|
220
|
-
* is available - either first run or the PID file was already cleaned up.
|
|
221
|
-
* @returns The Chrome process ID, or null if unavailable.
|
|
222
|
-
*/
|
|
223
|
-
function loadChromePid() {
|
|
224
|
-
return chromePid ?? readPidFile(getChromePidFilePath(), "Chrome", LOG);
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Clears the Chrome PID from both the module-level variable and the PID file on disk. Called after successful process cleanup to prevent stale PID reuse.
|
|
228
|
-
*/
|
|
229
|
-
function clearChromePid() {
|
|
230
|
-
chromePid = null;
|
|
231
|
-
clearPidFile(getChromePidFilePath(), "Chrome", LOG);
|
|
232
|
-
}
|
|
233
342
|
/**
|
|
234
343
|
* Synchronous sleep using Atomics.wait(). This is a cross-platform replacement for execSync("sleep N") that works on all platforms without shelling out.
|
|
235
344
|
* Required because killStaleChrome() runs in the synchronous process.on("exit") handler where async operations are not available.
|
|
@@ -239,37 +348,65 @@ function syncSleep(ms) {
|
|
|
239
348
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
240
349
|
}
|
|
241
350
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
351
|
+
* Identifies Chrome processes that this PrismCast instance is responsible for terminating. The filter has two stages: command-line discovery (anything using
|
|
352
|
+
* our profile directory) followed by ownership verification (we spawned it, or its parent is no longer alive). The ownership stage is structural - we do not
|
|
353
|
+
* rely on an in-memory flag; the parent-child relationship in the OS process table IS the ownership proof, which means this function is safe to call from any
|
|
354
|
+
* code path, including from a rejected-duplicate startup's exit handler.
|
|
355
|
+
*
|
|
356
|
+
* Why ownership matters. A duplicate PrismCast that the instance guard rejects must NOT signal Chrome that belongs to the legitimate holder. Process-table
|
|
357
|
+
* discovery lets the OS itself tell us who owns what: a Chrome whose ppid is a live unrelated PID belongs to that parent, not to us.
|
|
358
|
+
* @param processes - The current process table snapshot.
|
|
359
|
+
* @param profileDir - The Chrome user-data-dir to match against.
|
|
360
|
+
* @param ownPid - The current process's PID (anything whose parent is us is ours to kill).
|
|
361
|
+
* @param isProcessAlive - Predicate that returns true when the given PID is currently a live process. Injected so tests do not depend on real PIDs.
|
|
362
|
+
* @returns The PIDs to terminate, in process-table order.
|
|
363
|
+
*/
|
|
364
|
+
export function findChromeProcessesUsingProfile(processes, profileDir, ownPid, isProcessAlive) {
|
|
365
|
+
const target = "--user-data-dir=" + profileDir;
|
|
366
|
+
return processes.filter((p) => {
|
|
367
|
+
// Stage 1: command-line match. Chrome puppeteer launches with --user-data-dir=<path> (equals form, no quotes). We also verify the character following
|
|
368
|
+
// the path is whitespace or end-of-string, otherwise "/x/y" would match "--user-data-dir=/x/yz".
|
|
369
|
+
const idx = p.commandLine.indexOf(target);
|
|
370
|
+
if (idx === -1) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
const after = p.commandLine.charAt(idx + target.length);
|
|
374
|
+
if ((after !== "") && (after !== " ") && (after !== "\t")) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
// Stage 2: ownership verification. Kill if we spawned it (ppid is us) or if its parent is no longer alive (orphaned from a previous PrismCast that died).
|
|
378
|
+
// Skip if its parent is a live unrelated PID - that process owns it, not us.
|
|
379
|
+
return (p.ppid === ownPid) || !isProcessAlive(p.ppid);
|
|
380
|
+
}).map((p) => p.pid);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Ensures a clean slate for browser launch by terminating any stale Chrome processes and removing orphaned profile lock files. Chrome locks its profile
|
|
384
|
+
* directory while running; if a previous instance crashed without releasing the lock, we cannot launch a new browser with the same profile. Discovery is done
|
|
385
|
+
* via the OS process table (utils/processInspector) and filtered to Chrome processes using our profile directory whose ownership belongs to us - either because
|
|
386
|
+
* we spawned them (ppid === process.pid) or because their parent is no longer alive (orphaned from a previous instance).
|
|
246
387
|
*
|
|
247
388
|
* The termination strategy escalates from SIGTERM to SIGKILL. SIGTERM is sent first, giving Chrome up to 5 seconds to flush its profile databases (LevelDB,
|
|
248
389
|
* extension state, session storage) and exit cleanly. If Chrome does not exit, SIGKILL is sent as a fallback. This escalation is critical when called from the
|
|
249
|
-
* process exit handler
|
|
390
|
+
* process exit handler: Chrome may be running normally (e.g., after a capture probe timeout) and an immediate SIGKILL would corrupt its profile databases,
|
|
250
391
|
* poisoning the Docker volume for subsequent container restarts.
|
|
251
392
|
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* process.kill() throws ESRCH, which is caught gracefully.
|
|
393
|
+
* The ownership filter makes this function safe to call from any context, including a rejected-duplicate startup's exit handler: a duplicate that never
|
|
394
|
+
* spawned Chrome will find nothing matching its ownership criteria and signal nothing.
|
|
255
395
|
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
396
|
+
* Called at startup before launching the browser and from the process exit handler as a crash recovery fallback. Safe to call when no stale processes or files
|
|
397
|
+
* exist - the discovery and lock-file cleanup are both no-ops in the empty case.
|
|
258
398
|
*/
|
|
259
399
|
export function killStaleChrome() {
|
|
260
400
|
const profileDir = getChromeDataDir(CONFIG);
|
|
261
|
-
const pid = loadChromePid();
|
|
262
401
|
const POLL_INTERVAL_MS = 200;
|
|
263
|
-
|
|
402
|
+
const pidsToKill = findChromeProcessesUsingProfile(listProcesses(), profileDir, process.pid, isProcessRunning);
|
|
403
|
+
for (const pid of pidsToKill) {
|
|
264
404
|
try {
|
|
265
405
|
// Send SIGTERM first to give Chrome a chance to flush its profile databases (LevelDB, extension state, session storage) before exiting. This is critical
|
|
266
406
|
// when called from the process exit handler - Chrome may be running normally (e.g., after a capture probe timeout) and SIGKILL would corrupt its profile
|
|
267
407
|
// databases, poisoning the Docker volume for subsequent restarts.
|
|
268
408
|
process.kill(pid, "SIGTERM");
|
|
269
409
|
LOG.debug("browser:lifecycle", "Sent SIGTERM to Chrome process %d.", pid);
|
|
270
|
-
// Wait up to 5 seconds for Chrome to flush its databases and exit after SIGTERM. Containerized environments with software rendering and shared CPU may
|
|
271
|
-
// need the full window.
|
|
272
|
-
const TERM_WAIT_MS = 5000;
|
|
273
410
|
if (!waitForChromeExit(pid, TERM_WAIT_MS, POLL_INTERVAL_MS)) {
|
|
274
411
|
// SIGTERM didn't work. Escalate to SIGKILL. Orphaned Chrome processes (from a crashed parent or previous container) may not respond to SIGTERM.
|
|
275
412
|
LOG.debug("browser:lifecycle", "Chrome did not exit after SIGTERM. Escalating to SIGKILL.");
|
|
@@ -279,7 +416,6 @@ export function killStaleChrome() {
|
|
|
279
416
|
catch (_error) {
|
|
280
417
|
// ESRCH - Chrome exited between the poll check and the kill call.
|
|
281
418
|
}
|
|
282
|
-
const KILL_WAIT_MS = 2000;
|
|
283
419
|
if (!waitForChromeExit(pid, KILL_WAIT_MS, POLL_INTERVAL_MS)) {
|
|
284
420
|
LOG.warn("Chrome process %d did not exit after %dms of signal escalation. Proceeding anyway.", pid, TERM_WAIT_MS + KILL_WAIT_MS);
|
|
285
421
|
}
|
|
@@ -292,20 +428,9 @@ export function killStaleChrome() {
|
|
|
292
428
|
LOG.warn("Failed to signal Chrome process %d: %s.", pid, formatError(error));
|
|
293
429
|
}
|
|
294
430
|
}
|
|
295
|
-
clearChromePid();
|
|
296
431
|
}
|
|
297
432
|
// Remove stale lock and port files left behind by an unclean Chrome exit.
|
|
298
433
|
cleanStaleProfileFiles(profileDir);
|
|
299
|
-
// Mark this process as owning Chrome cleanup. The exit handler checks this flag to avoid killing Chrome that belongs to another running instance.
|
|
300
|
-
ownsChromeCleanup = true;
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Returns whether this process has taken ownership of Chrome cleanup by running killStaleChrome() during startup. Used by the exit handler to avoid killing
|
|
304
|
-
* Chrome that belongs to another running PrismCast instance.
|
|
305
|
-
* @returns True if killStaleChrome() has run in this process.
|
|
306
|
-
*/
|
|
307
|
-
export function canCleanupChrome() {
|
|
308
|
-
return ownsChromeCleanup;
|
|
309
434
|
}
|
|
310
435
|
/**
|
|
311
436
|
* Polls until the Chrome process with the given PID has exited, or the timeout expires. Uses process.kill(pid, 0) to check process existence - throws ESRCH
|
|
@@ -381,6 +506,12 @@ export function getExecutablePath() {
|
|
|
381
506
|
}
|
|
382
507
|
throw new Error("No Chrome installation found. Set CHROME_BIN environment variable.");
|
|
383
508
|
}
|
|
509
|
+
/* Chrome extension ID for puppeteer-stream's bundled capture extension. This is the deterministic ID Chrome assigns based on the extension's public key, mirrored
|
|
510
|
+
* from their dist at node_modules/puppeteer-stream/dist/PuppeteerStream.js (the extensionId constant). We pass it below via --allowlisted-extension-id as a
|
|
511
|
+
* defensive duplicate of the flag puppeteer-stream's own launch() call already re-adds; see the "--allowlisted-extension-id" entry in buildLaunchOptions for
|
|
512
|
+
* the full rationale.
|
|
513
|
+
*/
|
|
514
|
+
const PUPPETEER_STREAM_EXTENSION_ID = "jjndjgheafjngoipoacpjgeicjeomjli";
|
|
384
515
|
/**
|
|
385
516
|
* Assembles the configuration options for launching Chrome with Puppeteer. These options are critical for reliable streaming:
|
|
386
517
|
*
|
|
@@ -397,6 +528,13 @@ export function buildLaunchOptions() {
|
|
|
397
528
|
* --allow-running-insecure-content: Some streaming sites serve mixed HTTP/HTTPS content. Without this flag, the browser blocks HTTP resources on HTTPS
|
|
398
529
|
* pages, which can break video players that load some assets over HTTP.
|
|
399
530
|
*
|
|
531
|
+
* --allowlisted-extension-id=<extension-id>: Restores Chrome's global allowlist for puppeteer-stream's capture extension. puppeteer-stream's own launch()
|
|
532
|
+
* call already re-adds this exact flag (see the addToArgs call for extensionId in node_modules/puppeteer-stream/dist/PuppeteerStream.js), so this entry
|
|
533
|
+
* is a defensive duplicate: if a future puppeteer-stream release drops the flag again in favor of granting activeTab via a synthetic keystroke,
|
|
534
|
+
* CDP-synthesized keystrokes do not satisfy chrome.commands under automation (the renderer sees the event but the browser-process accelerator dispatcher
|
|
535
|
+
* does not), so capture would be denied at the API level (see github.com/Flam3rboy/puppeteer-stream/issues/206) without this fallback in place. Confirm
|
|
536
|
+
* whether this duplicate is still warranted whenever the pinned puppeteer-stream version changes.
|
|
537
|
+
*
|
|
400
538
|
* --autoplay-policy=no-user-gesture-required: Allows video and audio to play without requiring a user click first. Essential for automated streaming
|
|
401
539
|
* since we cannot simulate genuine user interaction for autoplay policy purposes.
|
|
402
540
|
*
|
|
@@ -428,6 +566,7 @@ export function buildLaunchOptions() {
|
|
|
428
566
|
*/
|
|
429
567
|
args: [
|
|
430
568
|
"--allow-running-insecure-content",
|
|
569
|
+
"--allowlisted-extension-id=" + PUPPETEER_STREAM_EXTENSION_ID,
|
|
431
570
|
"--autoplay-policy=no-user-gesture-required",
|
|
432
571
|
"--disable-background-media-suspend",
|
|
433
572
|
"--disable-background-networking",
|
|
@@ -489,9 +628,11 @@ export function buildLaunchOptions() {
|
|
|
489
628
|
* @returns The launched browser instance.
|
|
490
629
|
*/
|
|
491
630
|
async function launchWithCustomArgs(opts) {
|
|
492
|
-
// When running as a packaged executable (process.pkg is set by the pkg bundler), we need to replace the extension paths.
|
|
493
|
-
//
|
|
494
|
-
//
|
|
631
|
+
// When running as a packaged executable (process.pkg is set by the pkg bundler), we need to replace the extension paths. puppeteer-stream points
|
|
632
|
+
// opts.enableExtensions at its own node_modules-relative extension directory, which puppeteer-core installs via a CDP browser.installExtension() call after
|
|
633
|
+
// Chrome starts, rather than via --load-extension/--disable-extensions-except CLI flags - so that path still resolves inside node_modules, which does not
|
|
634
|
+
// exist at that location in the packaged executable. We route around it with Chrome's own native unpacked-extension flags, pointing them at our extracted
|
|
635
|
+
// extension files instead.
|
|
495
636
|
if (process.pkg) {
|
|
496
637
|
const extensionPath = getExtensionDir(CONFIG);
|
|
497
638
|
// Remove any existing extension arguments and add our own pointing to the extracted extension.
|
|
@@ -524,11 +665,13 @@ function formatGpuSuffix(gpu) {
|
|
|
524
665
|
return " (software rendering)";
|
|
525
666
|
}
|
|
526
667
|
/**
|
|
527
|
-
* Detects the maximum supported viewport dimensions based on the user's display
|
|
528
|
-
* determine the largest viewport we can use for video capture
|
|
668
|
+
* Detects the maximum supported viewport dimensions based on the user's display, and probes GPU hardware-encoding capabilities. The viewport half measures the
|
|
669
|
+
* available screen space and subtracts browser chrome to determine the largest viewport we can use for video capture; the GPU half queries CDP
|
|
670
|
+
* SystemInfo.getInfo for renderer identity and H.264/HEVC/AV1 hardware encoding support, falling back to a MediaRecorder capability probe where the CDP data
|
|
671
|
+
* is incomplete.
|
|
529
672
|
*
|
|
530
|
-
* The detection uses a temporary page (or existing page if available) to evaluate screen dimensions
|
|
531
|
-
* use by the preset system when determining effective viewport.
|
|
673
|
+
* The detection uses a temporary page (or existing page if available) to evaluate screen dimensions and GPU capabilities via JavaScript and CDP. Both results
|
|
674
|
+
* are cached in the display module for use by the preset system when determining effective viewport and capture codec.
|
|
532
675
|
* @param browser - The browser instance to use for detection.
|
|
533
676
|
*/
|
|
534
677
|
async function detectDisplayDimensions(browser) {
|
|
@@ -682,152 +825,176 @@ async function detectDisplayDimensions(browser) {
|
|
|
682
825
|
}
|
|
683
826
|
}
|
|
684
827
|
/**
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
* 2. Stopping all health monitors (they would fail trying to check page state)
|
|
692
|
-
* 3. Removing all entries from activeStreams (the pages are gone)
|
|
693
|
-
*
|
|
694
|
-
* This ensures streams fail gracefully rather than hanging indefinitely trying to use closed pages.
|
|
828
|
+
* Relinquishes the current browser's capture readiness and tears down everything that depended on it. This is the single source of truth for "the published browser
|
|
829
|
+
* is no longer usable" - shared by the disconnect handler (the browser crashed) and by invalidateBrowser (the browser is alive but capture-dead). It drops the
|
|
830
|
+
* supervisor's readiness first (which supersedes any launch in flight, clears the governor's health anchor, and transitions the lifecycle to absent so the next
|
|
831
|
+
* request relaunches through the gate and governor), clears the adapter-held metadata and caches, ends login mode, terminates every active stream (they were
|
|
832
|
+
* capturing on the now-unusable browser), and emits status. Callers log the specific cause; the alive-but-dead caller additionally closes the Chrome instance.
|
|
833
|
+
* @param streamTerminationReason - The reason recorded against each terminated stream, for the stream-end logs.
|
|
695
834
|
*/
|
|
696
|
-
function
|
|
697
|
-
//
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
835
|
+
function relinquishBrowserReadiness(streamTerminationReason) {
|
|
836
|
+
// Drop readiness first: supersede any in-flight launch, clear the governor's health anchor, and move the lifecycle to absent.
|
|
837
|
+
supervisor.noteReadinessLost();
|
|
838
|
+
// Clear the adapter-held Chrome version and the cached user agent so stale values are not served before the next ready browser. We do not track Chrome's PID
|
|
839
|
+
// directly - killStaleChrome discovers orphans via the OS process table on the next startup, which removes a class of state that could go stale.
|
|
701
840
|
currentChromeVersion = null;
|
|
702
841
|
setChromeUserAgent(null);
|
|
703
|
-
// Cancel any pending restart quiet timer since the browser is
|
|
842
|
+
// Cancel any pending scheduled-restart quiet timer since the browser this readiness applied to is gone.
|
|
704
843
|
if (restartQuietTimer) {
|
|
705
844
|
clearTimeout(restartQuietTimer);
|
|
706
845
|
restartQuietTimer = null;
|
|
707
846
|
}
|
|
708
|
-
// Clear all channel selection caches. Cached state (guide row positions, discovered page URLs)
|
|
847
|
+
// Clear all channel selection caches. Cached state (guide row positions, discovered page URLs) belongs to the old browser session.
|
|
709
848
|
clearChannelSelectionCaches();
|
|
710
|
-
//
|
|
711
|
-
//
|
|
849
|
+
// End login mode if it was active. We use clearLoginState() rather than endLoginMode() because the browser may already be gone and we do not want to attempt any
|
|
850
|
+
// browser operations (page close, window minimize).
|
|
712
851
|
if (clearLoginState() && !gracefulShutdownInProgress) {
|
|
713
|
-
LOG.info("Login mode ended due to browser
|
|
852
|
+
LOG.info("Login mode ended due to browser readiness loss.");
|
|
714
853
|
}
|
|
715
|
-
//
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
// Clean up all active streams using the authoritative terminateStream function for consistent cleanup. This is kept even during graceful shutdown as a defensive
|
|
720
|
-
// measure - terminateStream() is idempotent, so if streams were already terminated by the caller, this harmlessly iterates an empty array.
|
|
721
|
-
const streams = getAllStreams();
|
|
722
|
-
for (const streamInfo of streams) {
|
|
723
|
-
terminateStream(streamInfo.id, streamInfo.info.storeKey, "browser disconnect");
|
|
854
|
+
// Terminate every active stream using the authoritative terminateStream for consistent cleanup. Kept even during graceful shutdown as a defensive measure -
|
|
855
|
+
// terminateStream() is safe to call more than once, so if streams were already terminated by the caller, this harmlessly iterates an empty array.
|
|
856
|
+
for (const streamInfo of getAllStreams()) {
|
|
857
|
+
terminateStream(streamInfo.id, streamInfo.info.storeKey, streamTerminationReason);
|
|
724
858
|
}
|
|
859
|
+
// The session those streams captured on is over, so whatever page tracking survived their termination belongs to a browser that is gone.
|
|
860
|
+
clearPageTracking();
|
|
725
861
|
// Emit system status after stream cleanup. Skip during graceful shutdown since no clients are listening and the process is exiting.
|
|
726
862
|
if (!gracefulShutdownInProgress) {
|
|
727
863
|
void emitCurrentSystemStatus();
|
|
728
864
|
}
|
|
729
865
|
}
|
|
730
866
|
/**
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
* - Serializing concurrent callers so only one launch occurs at a time
|
|
736
|
-
* - Waiting for the puppeteer-stream extension to initialize
|
|
737
|
-
* - Setting up disconnect handlers for crash recovery
|
|
738
|
-
* @returns The browser instance.
|
|
739
|
-
* @throws If the browser cannot be launched.
|
|
867
|
+
* Handles browser disconnection events by relinquishing readiness and terminating all active streams. Called when the browser crashes, is closed externally, or
|
|
868
|
+
* otherwise loses its connection. It runs only for a genuine, unsolicited disconnect: every intentional teardown removes this listener via closeBrowserInstance
|
|
869
|
+
* first, so a scheduled restart, an orphan close, or an invalidation never reaches here. The browser is already gone, so there is nothing to close - relinquish
|
|
870
|
+
* readiness and the next request relaunches a fresh, gate-verified browser.
|
|
740
871
|
*/
|
|
741
|
-
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
}
|
|
747
|
-
// If a launch is already in progress (e.g., from the restart path or a concurrent stream request), piggyback on that promise instead of starting a second
|
|
748
|
-
// Chrome process. Two concurrent launches with the same profile directory would contend on Chrome's profile lock.
|
|
749
|
-
if (browserLaunchPromise) {
|
|
750
|
-
return browserLaunchPromise;
|
|
751
|
-
}
|
|
752
|
-
// We need to launch a new browser. Store the promise so concurrent callers can piggyback on this launch.
|
|
753
|
-
browserLaunchPromise = launchBrowser();
|
|
754
|
-
try {
|
|
755
|
-
return await browserLaunchPromise;
|
|
872
|
+
function handleBrowserDisconnect() {
|
|
873
|
+
// Announce the unexpected disconnect before tearing down (the message says streams will be terminated, which relinquish then does). Suppressed during a full
|
|
874
|
+
// server shutdown, where closeBrowser() set the flag and the disconnect is intentional.
|
|
875
|
+
if (!gracefulShutdownInProgress) {
|
|
876
|
+
LOG.error("Browser disconnected unexpectedly. All active streams will be terminated.");
|
|
756
877
|
}
|
|
757
|
-
|
|
758
|
-
|
|
878
|
+
relinquishBrowserReadiness("browser disconnect");
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Invalidates a specific browser that is still connected but can no longer capture - a mid-life capture death that no "disconnected" event would surface. This is the
|
|
882
|
+
* single recovery action generalized to the alive-but-incapable case: relinquish readiness and terminate the browser's streams (exactly as a disconnect does),
|
|
883
|
+
* then additionally close the still-running Chrome so a leaked process is not left behind. The lifecycle is already absent after relinquish, so the next request
|
|
884
|
+
* relaunches a fresh, gate-verified browser through the supervisor. Exported for the streaming layer's passive mid-life detector to call once its probe confirms the
|
|
885
|
+
* browser cannot capture. noteReadinessLost stays internal: callers signal intent through this function, never the supervisor directly.
|
|
886
|
+
*
|
|
887
|
+
* The caller passes the exact instance it verified, and we invalidate only if it is still the published browser. The detector's probe runs in the background for
|
|
888
|
+
* seconds, during which the verified browser could have disconnected and been replaced by a fresh relaunch; without this identity guard we would tear down that
|
|
889
|
+
* new, healthy browser on the strength of a probe against the old, dead one.
|
|
890
|
+
* @param browser - The specific browser instance the caller verified as unable to capture.
|
|
891
|
+
* @param reason - A short description of why the browser is being invalidated, for the alarm log.
|
|
892
|
+
*/
|
|
893
|
+
export async function invalidateBrowser(browser, reason) {
|
|
894
|
+
// Only invalidate if this is still the published browser. If it was already superseded (a disconnect plus relaunch raced the caller's probe), there is nothing to
|
|
895
|
+
// do - the readiness loss was handled, and tearing down the current browser would wrongly disrupt a healthy, freshly-relaunched one.
|
|
896
|
+
if (supervisor.current() !== browser) {
|
|
897
|
+
return;
|
|
759
898
|
}
|
|
899
|
+
LOG.error("The browser is connected but can no longer capture (%s). Invalidating it for a governed relaunch.", reason);
|
|
900
|
+
relinquishBrowserReadiness("capture system failure");
|
|
901
|
+
// Readiness was relinquished first, because that is what supersedes an in-flight launch; publishing the teardown synchronously, before any await, then keeps the
|
|
902
|
+
// launch window shut for the whole drain so nothing spawns a second Chrome against the profile lock this one still holds.
|
|
903
|
+
const teardown = closeBrowserInstance(browser);
|
|
904
|
+
supervisor.noteTeardownBegun(teardown, BROWSER_TEARDOWN_DRAIN_BOUND_MS);
|
|
905
|
+
await teardown;
|
|
760
906
|
}
|
|
761
907
|
/**
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
908
|
+
* Provides access to the capture-ready browser, launching one if needed. This is the single gated entry point for all browser access: it delegates to the
|
|
909
|
+
* supervisor's acquire(), which returns the ready browser, joins an in-flight launch (single-flight, so concurrent callers never contend on Chrome's profile lock),
|
|
910
|
+
* lazily launches when absent, or - while the relaunch governor is cooling after repeated failures - rejects fast with a BrowserUnavailableError WITHOUT spawning
|
|
911
|
+
* Chrome (the loop bound). The launch it drives runs the readiness gate, so a returned browser is verified capture-ready, not merely connected.
|
|
912
|
+
* @returns The capture-ready browser instance.
|
|
913
|
+
* @throws BrowserUnavailableError while the governor is cooling, BrowserSupersededError if an in-flight launch was abandoned by a readiness-loss, or the underlying
|
|
914
|
+
* launch error when a launch attempt fails.
|
|
766
915
|
*/
|
|
767
|
-
async function
|
|
916
|
+
export async function getCurrentBrowser() {
|
|
917
|
+
return supervisor.acquire();
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* The supervisor's `launch` port: spawns Chrome, runs the readiness gate, performs post-launch initialization (display detection, version/UA capture, precaching),
|
|
921
|
+
* and resolves ONLY with a capture-ready browser. It builds into a local instance and publishes nothing - the supervisor owns publication and transitions to
|
|
922
|
+
* "ready" only after this resolves. A launch that fails the gate tears down its own Chrome here and throws, so a broken instance is never handed up; the supervisor
|
|
923
|
+
* counts the failure and decides whether to relaunch immediately or cool down. The gate throws rather than logging a failed extension load as a warning and serving
|
|
924
|
+
* the broken browser anyway, so only a verified-capturing instance is ever published.
|
|
925
|
+
* @returns The capture-ready browser instance.
|
|
926
|
+
* @throws If the launch or the readiness gate fails.
|
|
927
|
+
*/
|
|
928
|
+
async function launchReadyBrowser() {
|
|
768
929
|
const browserElapsed = startTimer();
|
|
769
|
-
//
|
|
930
|
+
// The launch function from puppeteer-stream wraps standard Puppeteer launch to inject the streaming extension. We pass our custom launch function that handles
|
|
931
|
+
// packaged-executable extension paths. This happens on first stream request, after a browser crash, during server warmup, or during a governed relaunch.
|
|
932
|
+
const browser = await launch({ launch: launchWithCustomArgs }, buildLaunchOptions());
|
|
770
933
|
try {
|
|
771
|
-
const options = buildLaunchOptions();
|
|
772
|
-
// The launch function from puppeteer-stream wraps standard Puppeteer launch to inject the streaming extension. We pass our custom launch function that
|
|
773
|
-
// handles packaged executable extension paths.
|
|
774
|
-
currentBrowser = await launch({ launch: launchWithCustomArgs }, options);
|
|
775
|
-
// Persist the Chrome PID for cross-platform process cleanup. The PID file survives Node crashes, allowing the next startup to find and terminate orphaned
|
|
776
|
-
// Chrome processes without relying on Unix-only tools like pkill/pgrep.
|
|
777
|
-
const launchedPid = currentBrowser.process()?.pid;
|
|
778
|
-
if (launchedPid) {
|
|
779
|
-
saveChromePid(launchedPid);
|
|
780
|
-
}
|
|
781
|
-
else {
|
|
782
|
-
LOG.warn("Chrome process PID is unavailable. Orphaned process cleanup after a crash will be limited to lock file removal.");
|
|
783
|
-
}
|
|
784
|
-
// Register a handler for browser disconnection. This ensures we clean up properly if the browser crashes or is closed unexpectedly.
|
|
785
|
-
currentBrowser.on("disconnected", handleBrowserDisconnect);
|
|
786
934
|
LOG.debug("timing:browser", "Chrome process spawned. (+%sms)", browserElapsed());
|
|
787
|
-
// Poll for the puppeteer-stream extension to finish initializing
|
|
788
|
-
//
|
|
789
|
-
//
|
|
935
|
+
// Readiness gate, handshake tier (cheap, on-suspicion). Poll for the puppeteer-stream extension to finish initializing - it injects a START_RECORDING function
|
|
936
|
+
// into its options page context, so its presence is the extension's own readiness signal. We poll rather than fixed-delay so the browser is ready as soon as the
|
|
937
|
+
// extension loads (typically 200-500ms). On failure this THROWS rather than warning-and-proceeding: an unregistered extension means chrome.tabs is undefined and
|
|
938
|
+
// every getStream() would hang, so the instance is not capture-ready and must not be published. We reclassify the raw waitForFunction timeout into a
|
|
939
|
+
// capture-infrastructure error carrying "timed out" so the setup layer maps it to a 503 back-off (the same as the capability-tier probe failure), rather than a
|
|
940
|
+
// 500 the client would not back off from - an unregistered extension is a capture-infrastructure fault, and a fresh relaunch usually clears it.
|
|
790
941
|
try {
|
|
791
|
-
const extensionPage = await getExtensionPage(
|
|
942
|
+
const extensionPage = await getExtensionPage(browser);
|
|
792
943
|
await extensionPage.waitForFunction("typeof START_RECORDING === 'function'", { timeout: CONFIG.browser.initTimeout });
|
|
793
944
|
}
|
|
794
|
-
catch {
|
|
795
|
-
|
|
796
|
-
// assertExtensionLoaded() in puppeteer-stream will retry before each capture attempt, so this isn't fatal.
|
|
797
|
-
LOG.warn("Extension did not initialize within %d ms. Streams may need additional time to start.", CONFIG.browser.initTimeout);
|
|
945
|
+
catch (handshakeError) {
|
|
946
|
+
throw new Error("The capture extension handshake timed out after " + String(CONFIG.browser.initTimeout) + " ms.", { cause: handshakeError });
|
|
798
947
|
}
|
|
799
948
|
LOG.debug("timing:browser", "Extension initialized. (+%sms)", browserElapsed());
|
|
800
|
-
//
|
|
801
|
-
//
|
|
802
|
-
|
|
949
|
+
// Readiness gate, capability tier (the authoritative arbiter). Run the injected capture probe - a real getStream against a throwaway page on THIS instance - so
|
|
950
|
+
// "ready" means "really captured," not merely "the extension handshake responded." This predicate must run at every (re)launch:
|
|
951
|
+
// it exercises the exact getStream path that hangs when the extension is unregistered. A probe failure throws, so the supervisor counts the launch failure and the
|
|
952
|
+
// browser is never published; the unrecoverable stale-mutex case exits the process from inside the probe, since a Chrome restart cannot fix a leaked module mutex.
|
|
953
|
+
//
|
|
954
|
+
// If the probe is not wired (the injection point left unset by a refactor - impossible in the normal import order, which always wires it before any launch),
|
|
955
|
+
// we reject the launch rather than publish a handshake-only browser: serving an unverified browser would be the "proceed and hope" path this design
|
|
956
|
+
// eliminates. The supervisor counts the rejected launch and, on repetition, degrades loudly.
|
|
957
|
+
if (!captureProbe) {
|
|
958
|
+
throw new Error("The capture-readiness probe is not wired; refusing to publish a browser whose capture capability was not verified.");
|
|
959
|
+
}
|
|
960
|
+
await captureProbe(browser);
|
|
961
|
+
LOG.debug("timing:browser", "Capture probe complete. (+%sms)", browserElapsed());
|
|
962
|
+
// Detect display dimensions to determine the maximum supported viewport. This must happen before streaming so the preset system can degrade to a smaller preset
|
|
963
|
+
// if needed.
|
|
964
|
+
await detectDisplayDimensions(browser);
|
|
803
965
|
LOG.debug("timing:browser", "Display detection complete. (+%sms)", browserElapsed());
|
|
804
|
-
//
|
|
805
|
-
//
|
|
806
|
-
const chromeVersion = await
|
|
807
|
-
const userAgent = await
|
|
808
|
-
browserLaunchTime = Date.now();
|
|
966
|
+
// Capture the Chrome version and User-Agent. The version is logged for diagnostics (correlating browser behavior changes with specific Chrome releases) and
|
|
967
|
+
// surfaced by the health endpoint; the User-Agent lets server-side fetch() calls to service CDNs match Chrome's identity.
|
|
968
|
+
const chromeVersion = await browser.version();
|
|
969
|
+
const userAgent = await browser.userAgent();
|
|
809
970
|
currentChromeVersion = chromeVersion;
|
|
810
971
|
setChromeUserAgent(userAgent);
|
|
811
972
|
const gpu = getGpuCapabilities();
|
|
812
973
|
const gpuSuffix = gpu ? formatGpuSuffix(gpu) : "";
|
|
813
974
|
LOG.info("Chrome ready: %s%s.", chromeVersion, gpuSuffix);
|
|
814
975
|
LOG.debug("timing:browser", "Browser ready. Total: %sms.", browserElapsed());
|
|
815
|
-
//
|
|
816
|
-
|
|
817
|
-
// Start background precaching of selected service channel lineups. Fire-and-forget - the setTimeout inside startPrecaching() ensures the actual work is fully
|
|
818
|
-
// async and non-blocking.
|
|
976
|
+
// Start background precaching of selected service channel lineups. Fire-and-forget - the setTimeout inside startPrecaching() defers the work until after this
|
|
977
|
+
// launch settles and the supervisor has published the ready browser, so its getCurrentBrowser() resolves immediately rather than re-entering this launch.
|
|
819
978
|
startPrecaching();
|
|
979
|
+
// Arm the disconnect handler only now, as the very last step before the supervisor publishes this browser as ready. It is deliberately NOT armed earlier: during
|
|
980
|
+
// the gate/init window above, a Chrome crash surfaces as a thrown init step (CDP and waitForFunction reject on a dead browser), which the supervisor counts as a
|
|
981
|
+
// launch failure and feeds to the governor - keeping the relaunch loop bounded even if Chrome dies repeatedly during init. Arming the handler earlier would let
|
|
982
|
+
// its noteReadinessLost() bump the supervisor's launch generation and the launch would be treated as superseded (uncounted), defeating the loop bound. There is
|
|
983
|
+
// no gap: every statement from the last await to this return is synchronous, so a disconnect cannot be delivered between this registration and publication.
|
|
984
|
+
browser.on("disconnected", handleBrowserDisconnect);
|
|
985
|
+
return browser;
|
|
820
986
|
}
|
|
821
987
|
catch (error) {
|
|
822
988
|
LOG.error("Failed to launch browser: %s.", formatError(error));
|
|
823
|
-
//
|
|
824
|
-
|
|
825
|
-
|
|
989
|
+
// The gate (or post-launch init) failed. Clear the adapter-held metadata and tear down the Chrome instance we just spawned before propagating, so a failed
|
|
990
|
+
// launch never leaks a process and the next governed relaunch starts from a clean profile. The disconnect handler is not yet armed on this instance (it is armed
|
|
991
|
+
// only on the success path above), so this teardown never re-enters handleBrowserDisconnect - which is exactly what lets the supervisor count this as a launch
|
|
992
|
+
// failure rather than a supersession.
|
|
826
993
|
currentChromeVersion = null;
|
|
827
994
|
setChromeUserAgent(null);
|
|
995
|
+
await closeBrowserInstance(browser);
|
|
828
996
|
throw error;
|
|
829
997
|
}
|
|
830
|
-
return currentBrowser;
|
|
831
998
|
}
|
|
832
999
|
/**
|
|
833
1000
|
* Returns the Chrome version string captured when the browser launched, or null if the browser is not connected.
|
|
@@ -842,14 +1009,15 @@ export function getChromeVersion() {
|
|
|
842
1009
|
* @returns The browser instance, or null if not running.
|
|
843
1010
|
*/
|
|
844
1011
|
export function getBrowserInstance() {
|
|
845
|
-
return
|
|
1012
|
+
return supervisor.current();
|
|
846
1013
|
}
|
|
847
1014
|
/**
|
|
848
1015
|
* Checks if the browser is currently connected and usable. This is a synchronous check that can be used before attempting browser operations.
|
|
849
1016
|
* @returns True if the browser is connected and ready for use, false otherwise.
|
|
850
1017
|
*/
|
|
851
1018
|
export function isBrowserConnected() {
|
|
852
|
-
|
|
1019
|
+
const browser = supervisor.current();
|
|
1020
|
+
return !!browser && browser.connected;
|
|
853
1021
|
}
|
|
854
1022
|
/**
|
|
855
1023
|
* Resizes the browser window to the effective viewport and minimizes it. This function combines viewport sizing with minimization to ensure the window is
|
|
@@ -861,18 +1029,19 @@ export function isBrowserConnected() {
|
|
|
861
1029
|
*/
|
|
862
1030
|
export async function minimizeBrowserWindow() {
|
|
863
1031
|
// Guard against calling this when no browser is running.
|
|
864
|
-
|
|
1032
|
+
const browser = supervisor.current();
|
|
1033
|
+
if (!browser?.connected) {
|
|
865
1034
|
return;
|
|
866
1035
|
}
|
|
867
1036
|
let tempPage = null;
|
|
868
1037
|
let usingTempPage = false;
|
|
869
1038
|
try {
|
|
870
1039
|
// Try to use an existing page first. Creating a new page can cause the window to restore/activate on macOS, which defeats the purpose of minimizing.
|
|
871
|
-
const existingPages = await
|
|
1040
|
+
const existingPages = await browser.pages();
|
|
872
1041
|
let targetPage = existingPages.find((p) => !p.isClosed()) ?? null;
|
|
873
1042
|
// If no existing pages, we must create a temporary one. This is less ideal but necessary to get a CDP session target.
|
|
874
1043
|
if (!targetPage) {
|
|
875
|
-
tempPage = await
|
|
1044
|
+
tempPage = await browser.newPage();
|
|
876
1045
|
usingTempPage = true;
|
|
877
1046
|
// Register the temp page so stale cleanup knows it's ours.
|
|
878
1047
|
registerManagedPage(tempPage);
|
|
@@ -903,16 +1072,17 @@ export async function minimizeBrowserWindow() {
|
|
|
903
1072
|
}
|
|
904
1073
|
}
|
|
905
1074
|
/**
|
|
906
|
-
* Gets all open browser pages (tabs). This is used by the health check endpoint to report page count
|
|
1075
|
+
* Gets all open browser pages (tabs). This is used by the health check endpoint to report page count.
|
|
907
1076
|
* @returns Array of pages, or empty array if the browser is not connected.
|
|
908
1077
|
*/
|
|
909
1078
|
export async function getBrowserPages() {
|
|
910
|
-
// Guard against calling this when no browser is running.
|
|
911
|
-
|
|
1079
|
+
// Guard against calling this when no ready browser is running.
|
|
1080
|
+
const browser = supervisor.current();
|
|
1081
|
+
if (!browser?.connected) {
|
|
912
1082
|
return [];
|
|
913
1083
|
}
|
|
914
1084
|
try {
|
|
915
|
-
return await
|
|
1085
|
+
return await browser.pages();
|
|
916
1086
|
}
|
|
917
1087
|
catch (_error) {
|
|
918
1088
|
// If getting pages fails (browser disconnecting, etc.), return empty array rather than throwing.
|
|
@@ -920,67 +1090,87 @@ export async function getBrowserPages() {
|
|
|
920
1090
|
}
|
|
921
1091
|
}
|
|
922
1092
|
/**
|
|
923
|
-
*
|
|
924
|
-
*
|
|
1093
|
+
* Tears down a specific Chrome instance and is the single teardown primitive: the supervisor's `close` port (for disposing an orphaned superseded launch), the
|
|
1094
|
+
* launch-failure cleanup in launchReadyBrowser, the scheduled-restart teardown in executeBrowserRestart, and the full-server closeBrowser all route through it. It
|
|
1095
|
+
* owns no lifecycle state - the supervisor is the single source of truth for that. It first removes the disconnect listener so this intentional teardown does not
|
|
1096
|
+
* trip handleBrowserDisconnect: the SIGTERM-induced "disconnected" event can arrive after this function returns, and without the removal it could supersede a fresh
|
|
1097
|
+
* launch the caller has already started (the late-disconnect race).
|
|
925
1098
|
*
|
|
926
1099
|
* Chrome termination uses Puppeteer's ChildProcess handle and its `exit` event for detection:
|
|
927
1100
|
*
|
|
928
|
-
* -
|
|
929
|
-
* -
|
|
1101
|
+
* - browser.close() sends CDP Browser.close and waits for WebSocket teardown, which hangs 3-5 seconds even after Chrome exits.
|
|
1102
|
+
* - browser.disconnect() drops the WebSocket instantly but orphans Chrome as a Node child process, creating a zombie that process.kill(pid, 0) cannot detect.
|
|
930
1103
|
* - Synchronous polling (Atomics.wait) blocks the event loop, preventing Node from processing SIGCHLD to reap the child - Chrome becomes a zombie regardless
|
|
931
1104
|
* of how SIGTERM was sent.
|
|
932
1105
|
*
|
|
933
|
-
* Instead, we send SIGTERM through the ChildProcess handle and listen for the `exit` event. This keeps the event loop running so Node can process SIGCHLD and
|
|
934
|
-
*
|
|
1106
|
+
* Instead, we send SIGTERM through the ChildProcess handle and listen for the `exit` event. This keeps the event loop running so Node can process SIGCHLD and reap
|
|
1107
|
+
* Chrome properly. The exit event fires only after the process is fully reaped - no zombies, no polling, no event loop blocking. The await on the exit event is what
|
|
1108
|
+
* lets the caller relaunch immediately afterward without contending on Chrome's profile lock.
|
|
1109
|
+
* @param browser - The Chrome instance to terminate.
|
|
935
1110
|
*/
|
|
936
|
-
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
//
|
|
942
|
-
|
|
943
|
-
browserLaunchTime = null;
|
|
944
|
-
currentChromeVersion = null;
|
|
945
|
-
setChromeUserAgent(null);
|
|
946
|
-
if (!browserRef) {
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
// Send SIGTERM through Puppeteer's ChildProcess handle and wait for the `exit` event. The ChildProcess handle is only available when Puppeteer launched
|
|
950
|
-
// Chrome (not when connecting to an existing browser), but PrismCast always launches Chrome directly.
|
|
951
|
-
const chromeProcess = browserRef.process();
|
|
1111
|
+
async function closeBrowserInstance(browser) {
|
|
1112
|
+
// Remove the disconnect handler before signalling. Every call here is an intentional teardown, so the resulting "disconnected" event must not invoke the
|
|
1113
|
+
// unexpected-disconnect handler - which would clear caches, log an error, and (critically) call noteReadinessLost(), superseding any launch the caller starts next.
|
|
1114
|
+
browser.off("disconnected", handleBrowserDisconnect);
|
|
1115
|
+
// Send SIGTERM through Puppeteer's ChildProcess handle and wait for the `exit` event. The ChildProcess handle is only available when Puppeteer launched Chrome
|
|
1116
|
+
// (not when connecting to an existing browser), but PrismCast always launches Chrome directly.
|
|
1117
|
+
const chromeProcess = browser.process();
|
|
952
1118
|
if (chromeProcess?.pid && !chromeProcess.killed) {
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
// Listen for the exit event before sending the signal. The event fires after the OS reaps the process, so there is no zombie window. Resolves to true so
|
|
956
|
-
// Promise.race can distinguish exit from timeout.
|
|
1119
|
+
// Listen for the exit event before sending the signal. The event fires after the OS reaps the process, so there is no zombie window. The promise only ever
|
|
1120
|
+
// resolves, so a null from either bounded wait below means the exit never came.
|
|
957
1121
|
const { promise: exitPromise, resolve: signalExit } = Promise.withResolvers();
|
|
958
1122
|
chromeProcess.on("exit", () => { signalExit(true); });
|
|
959
1123
|
chromeProcess.kill("SIGTERM");
|
|
960
1124
|
LOG.debug("browser:lifecycle", "Sent SIGTERM to Chrome process %d.", chromeProcess.pid);
|
|
961
|
-
// Wait for Chrome to exit after SIGTERM, with a
|
|
962
|
-
const
|
|
963
|
-
const exitedAfterTerm = await Promise.race([exitPromise, termTimeout.promise]);
|
|
964
|
-
termTimeout.cancel();
|
|
1125
|
+
// Wait for Chrome to exit after SIGTERM, with a bound. If Chrome doesn't exit in time, escalate to SIGKILL.
|
|
1126
|
+
const exitedAfterTerm = await boundedWait(exitPromise, TERM_WAIT_MS);
|
|
965
1127
|
if (!exitedAfterTerm) {
|
|
966
|
-
// SIGTERM didn't work within the
|
|
1128
|
+
// SIGTERM didn't work within the bound. Escalate to SIGKILL. Orphaned Chrome processes (from a crashed parent or previous container) may not
|
|
967
1129
|
// respond to SIGTERM.
|
|
968
1130
|
LOG.debug("browser:lifecycle", "Chrome did not exit after SIGTERM. Escalating to SIGKILL.");
|
|
969
1131
|
chromeProcess.kill("SIGKILL");
|
|
970
|
-
|
|
971
|
-
await
|
|
972
|
-
killTimeout.cancel();
|
|
1132
|
+
// The same exit promise serves the second wait: if it already resolved, this returns its value immediately.
|
|
1133
|
+
await boundedWait(exitPromise, KILL_WAIT_MS);
|
|
973
1134
|
}
|
|
974
1135
|
}
|
|
975
|
-
// Disconnect the Puppeteer WebSocket after Chrome has exited. This cleans up Puppeteer's internal state (event listeners, pending CDP calls) without
|
|
976
|
-
//
|
|
977
|
-
|
|
978
|
-
|
|
1136
|
+
// Disconnect the Puppeteer WebSocket after Chrome has exited. This cleans up Puppeteer's internal state (event listeners, pending CDP calls) without waiting for
|
|
1137
|
+
// the WebSocket close handshake to complete on a dead connection. We catch the rejection: disconnect() on a connection whose underlying transport already died of
|
|
1138
|
+
// an unclean Chrome exit can reject, and an unhandled rejection on this fire-and-forget call would crash the process during an otherwise-successful teardown.
|
|
1139
|
+
if (browser.connected) {
|
|
1140
|
+
browser.disconnect().catch((error) => {
|
|
1141
|
+
LOG.debug("browser:lifecycle", "Ignoring browser disconnect error during teardown: %s.", formatError(error));
|
|
1142
|
+
});
|
|
979
1143
|
}
|
|
980
|
-
//
|
|
981
|
-
|
|
1144
|
+
// Remove stale Chrome profile lock files left behind by the disconnected browser. No per-PID state to clear here - killStaleChrome on the next launch will
|
|
1145
|
+
// discover any leftover Chrome via the OS process table.
|
|
982
1146
|
cleanStaleProfileFiles(getChromeDataDir(CONFIG));
|
|
983
1147
|
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Closes the browser and cleans up resources during full server shutdown. After this call the supervisor reports absent and any subsequent stream request launches
|
|
1150
|
+
* a fresh browser. It retires the current instance from the lifecycle (so an in-flight launch is superseded and the metadata is cleared), then delegates the actual
|
|
1151
|
+
* Chrome teardown to closeBrowserInstance. The graceful-shutdown flag is set so handleBrowserDisconnect, if it runs for any reason, stays quiet.
|
|
1152
|
+
*/
|
|
1153
|
+
export async function closeBrowser() {
|
|
1154
|
+
// Ensure the flag is set so the disconnect handler stays quiet. Normally set earlier by app.ts shutdown(), but set here as a fallback for direct calls.
|
|
1155
|
+
setGracefulShutdown(true);
|
|
1156
|
+
// Capture the ready browser before retiring it from the lifecycle. noteReadinessLost() supersedes any launch in flight and transitions to absent; we then clear
|
|
1157
|
+
// the adapter-held metadata so nothing stale is served.
|
|
1158
|
+
const browser = supervisor.current();
|
|
1159
|
+
supervisor.noteReadinessLost();
|
|
1160
|
+
// The session is ending, so its page ids are spent. The call sits ahead of the early return below so the clear happens whether or not there was a browser to
|
|
1161
|
+
// close.
|
|
1162
|
+
clearPageTracking();
|
|
1163
|
+
currentChromeVersion = null;
|
|
1164
|
+
setChromeUserAgent(null);
|
|
1165
|
+
if (!browser) {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
// Readiness was relinquished first, because that is what supersedes an in-flight launch; publishing the teardown synchronously, before any await, then keeps the
|
|
1169
|
+
// launch window shut for the whole drain so nothing spawns a second Chrome against the profile lock this one still holds.
|
|
1170
|
+
const teardown = closeBrowserInstance(browser);
|
|
1171
|
+
supervisor.noteTeardownBegun(teardown, BROWSER_TEARDOWN_DRAIN_BOUND_MS);
|
|
1172
|
+
await teardown;
|
|
1173
|
+
}
|
|
984
1174
|
/* Over time, browser pages (tabs) may accumulate if cleanup fails during stream termination. This can happen due to race conditions, errors during cleanup, or
|
|
985
1175
|
* edge cases in stream lifecycle management. Each orphaned page consumes memory and may continue running JavaScript, so we periodically clean them up.
|
|
986
1176
|
*
|
|
@@ -996,6 +1186,12 @@ export async function closeBrowser() {
|
|
|
996
1186
|
* briefly untracked during stream initialization or cleanup.
|
|
997
1187
|
*
|
|
998
1188
|
* 4. Minimum page preservation: We always keep at least one page open to prevent Chrome from exiting.
|
|
1189
|
+
*
|
|
1190
|
+
* 5. In-flight setup exemption: Pages whose stream setup is still running (tracked in inFlightSetupPageIds) are never considered stale. The registry records a
|
|
1191
|
+
* stream's page only once setup completes, so without this a slow tune would have its own page closed out from under it.
|
|
1192
|
+
*
|
|
1193
|
+
* The safeguards are expressed as rules in browser/pageStaleness.ts, which decides from a snapshot what to close, track, forget, and unmark. This function is the
|
|
1194
|
+
* I/O shell around that decision: it reads Chrome's page list, applies the decision to the tracking collections, and performs the closes.
|
|
999
1195
|
*/
|
|
1000
1196
|
/**
|
|
1001
1197
|
* Cleans up browser pages that are not associated with active streams. This function runs periodically to catch any pages that were not properly closed during
|
|
@@ -1003,17 +1199,18 @@ export async function closeBrowser() {
|
|
|
1003
1199
|
*
|
|
1004
1200
|
* The cleanup uses a multi-stage filtering process:
|
|
1005
1201
|
* 1. Only consider pages we created (in managedPageIds)
|
|
1006
|
-
* 2. Exclude pages associated with active streams
|
|
1202
|
+
* 2. Exclude pages associated with active streams, and pages whose stream setup is still in flight
|
|
1007
1203
|
* 3. Apply a grace period before closing (to handle race conditions)
|
|
1008
1204
|
* 4. Preserve at least one page to keep the browser alive
|
|
1009
1205
|
*/
|
|
1010
1206
|
export async function cleanupStalePages() {
|
|
1011
|
-
// Guard against calling this when no browser is running.
|
|
1012
|
-
|
|
1207
|
+
// Guard against calling this when no ready browser is running.
|
|
1208
|
+
const browser = supervisor.current();
|
|
1209
|
+
if (!browser?.connected) {
|
|
1013
1210
|
return;
|
|
1014
1211
|
}
|
|
1015
1212
|
try {
|
|
1016
|
-
const pages = await
|
|
1213
|
+
const pages = await browser.pages();
|
|
1017
1214
|
// If there's only one page or fewer, we must preserve it to keep the browser alive. Don't attempt cleanup.
|
|
1018
1215
|
if (pages.length <= 1) {
|
|
1019
1216
|
return;
|
|
@@ -1029,53 +1226,37 @@ export async function cleanupStalePages() {
|
|
|
1029
1226
|
}
|
|
1030
1227
|
}
|
|
1031
1228
|
const now = Date.now();
|
|
1032
|
-
|
|
1033
|
-
//
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
// - It has been stale for longer than the grace period
|
|
1037
|
-
const candidatePages = [];
|
|
1038
|
-
// Track which managed page IDs we've seen in the current browser pages. Used for cleanup of stale tracking data.
|
|
1039
|
-
const currentManagedIds = new Set();
|
|
1229
|
+
// Project the browser's pages into the shape the decision core reads: the managed ids in the browser's own order, with undefined standing in for pages we
|
|
1230
|
+
// did not create, plus a lookup back to the Page objects so the ids it returns can be resolved to something closable.
|
|
1231
|
+
const idToPage = new Map();
|
|
1232
|
+
const pageIds = [];
|
|
1040
1233
|
for (const page of pages) {
|
|
1041
1234
|
const pageId = getManagedPageId(page);
|
|
1042
|
-
|
|
1043
|
-
if (
|
|
1044
|
-
|
|
1235
|
+
pageIds.push(pageId);
|
|
1236
|
+
if (pageId !== undefined) {
|
|
1237
|
+
idToPage.set(pageId, page);
|
|
1045
1238
|
}
|
|
1046
|
-
currentManagedIds.add(pageId);
|
|
1047
|
-
// Skip pages associated with active streams.
|
|
1048
|
-
if (activePageIds.has(pageId)) {
|
|
1049
|
-
// If this page was previously marked as potentially stale, remove it from tracking since it's now active.
|
|
1050
|
-
potentiallyStalePages.delete(pageId);
|
|
1051
|
-
continue;
|
|
1052
|
-
}
|
|
1053
|
-
// This page is potentially stale. Track when we first observed it as such.
|
|
1054
|
-
if (!potentiallyStalePages.has(pageId)) {
|
|
1055
|
-
potentiallyStalePages.set(pageId, now);
|
|
1056
|
-
// Don't close it yet - wait for the grace period.
|
|
1057
|
-
continue;
|
|
1058
|
-
}
|
|
1059
|
-
// Check if the grace period has elapsed.
|
|
1060
|
-
const firstSeenStale = potentiallyStalePages.get(pageId) ?? now;
|
|
1061
|
-
if ((now - firstSeenStale) < gracePeriod) {
|
|
1062
|
-
// Grace period hasn't elapsed yet. Leave this page alone for now.
|
|
1063
|
-
continue;
|
|
1064
|
-
}
|
|
1065
|
-
// This page has been stale for longer than the grace period. It's a candidate for cleanup.
|
|
1066
|
-
candidatePages.push({ page, pageId });
|
|
1067
1239
|
}
|
|
1068
|
-
//
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1240
|
+
// The staleness judgment - clocks, exemptions, the dead-entry sweep, and the preserve-one budget - belongs to the pure core; this function only carries it out.
|
|
1241
|
+
const actions = evaluateStalePages({ activePageIds, gracePeriodMs: CONFIG.recovery.stalePageGracePeriod, inFlightSetupPageIds, now, pageIds,
|
|
1242
|
+
staleFirstSeen: potentiallyStalePages });
|
|
1243
|
+
// Bring the tracking collections in line with the decision before any close runs, so a close that fails cannot leave the bookkeeping half-applied.
|
|
1244
|
+
for (const pageId of actions.forgetTrackedIds) {
|
|
1245
|
+
potentiallyStalePages.delete(pageId);
|
|
1246
|
+
}
|
|
1247
|
+
for (const pageId of actions.startTrackingIds) {
|
|
1248
|
+
potentiallyStalePages.set(pageId, now);
|
|
1249
|
+
}
|
|
1250
|
+
for (const pageId of actions.clearInFlightIds) {
|
|
1251
|
+
inFlightSetupPageIds.delete(pageId);
|
|
1073
1252
|
}
|
|
1074
|
-
// Calculate how many pages we can close while still keeping at least one page open.
|
|
1075
|
-
const maxToClose = Math.max(0, pages.length - 1 - activePageIds.size);
|
|
1076
|
-
const pagesToClose = candidatePages.slice(0, maxToClose);
|
|
1077
1253
|
let closedCount = 0;
|
|
1078
|
-
for (const
|
|
1254
|
+
for (const pageId of actions.closeIds) {
|
|
1255
|
+
// Every id the core returns for closing came from the page list built above, so this resolves; the check is what narrows it to a Page.
|
|
1256
|
+
const page = idToPage.get(pageId);
|
|
1257
|
+
if (!page) {
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1079
1260
|
try {
|
|
1080
1261
|
// Unregister the page before closing to prevent any race with re-registration.
|
|
1081
1262
|
managedPageIds.delete(pageId);
|
|
@@ -1121,16 +1302,29 @@ export function stopStalePageCleanup() {
|
|
|
1121
1302
|
*/
|
|
1122
1303
|
/**
|
|
1123
1304
|
* Checks whether the browser qualifies for an opportunistic restart. Called periodically by the restart check interval. The check skips when any of these
|
|
1124
|
-
* conditions hold: graceful shutdown in progress, login mode active, browser not
|
|
1125
|
-
* quiet timer is cancelled (streams started during the quiet period reset the
|
|
1305
|
+
* conditions hold: graceful shutdown in progress, login mode active, browser not ready, browser age below threshold. On every eligible tick it also drives the
|
|
1306
|
+
* supervisor's health-gated governor reset. If active streams exist, any pending quiet timer is cancelled (streams started during the quiet period reset the
|
|
1307
|
+
* countdown). Otherwise a quiet timer is started if one is not already running.
|
|
1126
1308
|
*/
|
|
1127
1309
|
function checkBrowserRestart() {
|
|
1128
|
-
// Skip if the server is shutting down
|
|
1129
|
-
if (gracefulShutdownInProgress || isLoginModeActive()
|
|
1310
|
+
// Skip if the server is shutting down or login mode is active.
|
|
1311
|
+
if (gracefulShutdownInProgress || isLoginModeActive()) {
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
// Read the ready browser and its launch time from the supervisor. Both are non-null only in the ready state, so a single guard covers "no ready browser." The
|
|
1315
|
+
// launch time is on the supervisor's clock (realClock.now), so age must be measured against the same clock - not Date.now() - or the units would not match.
|
|
1316
|
+
const browser = supervisor.current();
|
|
1317
|
+
const launchTime = supervisor.currentLaunchTime();
|
|
1318
|
+
if (!browser?.connected || (launchTime === null)) {
|
|
1130
1319
|
return;
|
|
1131
1320
|
}
|
|
1321
|
+
// Health-gated governor reset. On every eligible tick, tell the supervisor the browser is still ready; once it has been continuously ready for the policy's
|
|
1322
|
+
// hold, this resets the relaunch governor to its normal state and returns true, so we log the recovery exactly once.
|
|
1323
|
+
if (supervisor.noteSustainedHealth()) {
|
|
1324
|
+
LOG.info("Browser capture readiness has been sustained; the relaunch governor has reset to its normal state.");
|
|
1325
|
+
}
|
|
1132
1326
|
// Skip if the browser has not exceeded the maximum age.
|
|
1133
|
-
const age =
|
|
1327
|
+
const age = realClock.now() - launchTime;
|
|
1134
1328
|
if (age < BROWSER_MAX_AGE) {
|
|
1135
1329
|
return;
|
|
1136
1330
|
}
|
|
@@ -1158,21 +1352,37 @@ function checkBrowserRestart() {
|
|
|
1158
1352
|
async function executeBrowserRestart() {
|
|
1159
1353
|
// Clear the timer handle.
|
|
1160
1354
|
restartQuietTimer = null;
|
|
1161
|
-
// Final guard: re-check all preconditions. Conditions may have changed during the quiet period (e.g., a stream started just before the timer fired, login
|
|
1162
|
-
//
|
|
1163
|
-
|
|
1355
|
+
// Final guard: re-check all preconditions. Conditions may have changed during the quiet period (e.g., a stream started just before the timer fired, login mode
|
|
1356
|
+
// was activated, or the browser disconnected on its own). Reading current()/currentLaunchTime() together keeps the ready-state check and the age source consistent.
|
|
1357
|
+
const browser = supervisor.current();
|
|
1358
|
+
const launchTime = supervisor.currentLaunchTime();
|
|
1359
|
+
if (gracefulShutdownInProgress || isLoginModeActive() || (getStreamCount() > 0) || !browser?.connected || (launchTime === null)) {
|
|
1164
1360
|
LOG.debug("browser:lifecycle", "Browser restart aborted - preconditions no longer met.");
|
|
1165
1361
|
return;
|
|
1166
1362
|
}
|
|
1167
|
-
const age =
|
|
1363
|
+
const age = realClock.now() - launchTime;
|
|
1168
1364
|
const hours = Math.floor(age / 3600000);
|
|
1169
1365
|
const minutes = Math.floor((age % 3600000) / 60000);
|
|
1170
1366
|
LOG.info("Restarting browser for scheduled maintenance (uptime: %sh %sm).", hours, minutes);
|
|
1171
1367
|
try {
|
|
1172
|
-
//
|
|
1173
|
-
|
|
1174
|
-
//
|
|
1175
|
-
|
|
1368
|
+
// Retire the current instance from the lifecycle, tear it down, then acquire a fresh one through the supervisor. We do NOT touch the graceful-shutdown flag (the
|
|
1369
|
+
// server is not shutting down): closeBrowserInstance removes the disconnect listener, which is what makes this intentional teardown quiet. acquire() publishes
|
|
1370
|
+
// "ready" only after the readiness gate passes, so the completion log below is truthful: it verifies capture capability before claiming readiness, not mere liveness.
|
|
1371
|
+
supervisor.noteReadinessLost();
|
|
1372
|
+
// The restart swaps the whole Chrome session inside a living process, so the retiring session's page ids must not carry into the fresh one.
|
|
1373
|
+
clearPageTracking();
|
|
1374
|
+
// Readiness was relinquished first, because that is what supersedes an in-flight launch; publishing the teardown synchronously, before any await, then keeps the
|
|
1375
|
+
// launch window shut for the whole drain so nothing spawns a second Chrome against the profile lock this one still holds.
|
|
1376
|
+
const teardown = closeBrowserInstance(browser);
|
|
1377
|
+
supervisor.noteTeardownBegun(teardown, BROWSER_TEARDOWN_DRAIN_BOUND_MS);
|
|
1378
|
+
await teardown;
|
|
1379
|
+
// The preconditions were checked before the teardown, but a shutdown can begin during the seconds it takes, and relaunching then would spawn Chrome into a
|
|
1380
|
+
// dying process. Re-check on the far side of the await, for the same reason the guard above re-checks on the far side of the quiet period.
|
|
1381
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- the shutdown path sets this while the teardown is awaited; TS cannot see that.
|
|
1382
|
+
if (gracefulShutdownInProgress) {
|
|
1383
|
+
LOG.debug("browser:lifecycle", "Browser restart relaunch declined because shutdown began while the previous instance was closing.");
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1176
1386
|
// Launch a fresh browser instance so it is ready for the next stream request.
|
|
1177
1387
|
await getCurrentBrowser();
|
|
1178
1388
|
// Minimize the new window to reduce GPU usage and desktop clutter.
|
|
@@ -1181,8 +1391,6 @@ async function executeBrowserRestart() {
|
|
|
1181
1391
|
}
|
|
1182
1392
|
catch (error) {
|
|
1183
1393
|
LOG.error("Browser restart failed: %s.", formatError(error));
|
|
1184
|
-
// Ensure the graceful shutdown flag is cleared even on failure so new stream requests can still launch a browser.
|
|
1185
|
-
setGracefulShutdown(false);
|
|
1186
1394
|
}
|
|
1187
1395
|
}
|
|
1188
1396
|
/**
|