prismcast 1.13.0 → 1.14.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 +88 -6
- package/dist/app.js +287 -154
- package/dist/app.js.map +1 -1
- package/dist/browser/cdp.d.ts +6 -17
- package/dist/browser/cdp.js +6 -57
- package/dist/browser/cdp.js.map +1 -1
- package/dist/browser/index.d.ts +106 -23
- package/dist/browser/index.js +292 -225
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/precaching.d.ts +25 -11
- package/dist/browser/precaching.js +245 -97
- package/dist/browser/precaching.js.map +1 -1
- package/dist/browser/tabCapture.js +3 -10
- package/dist/browser/tabCapture.js.map +1 -1
- package/dist/browser/tuning/comcastPolymer.js +13 -14
- package/dist/browser/tuning/comcastPolymer.js.map +1 -1
- package/dist/browser/tuning/hbo.js +34 -17
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/video.d.ts +1 -1
- package/dist/browser/video.js +3 -3
- package/dist/channels/index.js +5 -5
- package/dist/channels/index.js.map +1 -1
- package/dist/cliHelp.d.ts +11 -0
- package/dist/cliHelp.js +148 -0
- package/dist/cliHelp.js.map +1 -0
- package/dist/config/channelForm.js +5 -4
- package/dist/config/channelForm.js.map +1 -1
- package/dist/config/consistencyProbe.d.ts +2 -4
- package/dist/config/consistencyProbe.js +8 -51
- package/dist/config/consistencyProbe.js.map +1 -1
- package/dist/config/index.d.ts +107 -55
- package/dist/config/index.js +499 -267
- package/dist/config/index.js.map +1 -1
- package/dist/config/paths.d.ts +13 -8
- package/dist/config/paths.js +21 -25
- package/dist/config/paths.js.map +1 -1
- package/dist/config/persistence.d.ts +57 -11
- package/dist/config/persistence.js +127 -44
- package/dist/config/persistence.js.map +1 -1
- package/dist/config/reactivity.d.ts +60 -36
- package/dist/config/reactivity.js +89 -116
- package/dist/config/reactivity.js.map +1 -1
- package/dist/config/services.d.ts +61 -26
- package/dist/config/services.js +137 -68
- package/dist/config/services.js.map +1 -1
- package/dist/config/streamSettings.d.ts +24 -0
- package/dist/config/streamSettings.js +17 -0
- package/dist/config/streamSettings.js.map +1 -0
- package/dist/config/userChannels.d.ts +60 -52
- package/dist/config/userChannels.js +138 -188
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +224 -61
- package/dist/config/userConfig.js +351 -134
- package/dist/config/userConfig.js.map +1 -1
- package/dist/hdhr/discover.d.ts +7 -1
- package/dist/hdhr/discover.js +12 -6
- package/dist/hdhr/discover.js.map +1 -1
- package/dist/hdhr/index.d.ts +51 -10
- package/dist/hdhr/index.js +108 -112
- package/dist/hdhr/index.js.map +1 -1
- package/dist/hdhr/udp.d.ts +11 -4
- package/dist/hdhr/udp.js +25 -20
- package/dist/hdhr/udp.js.map +1 -1
- package/dist/index.js +16 -143
- package/dist/index.js.map +1 -1
- package/dist/native/index.d.ts +1 -1
- package/dist/native/index.js +4 -3
- package/dist/native/index.js.map +1 -1
- package/dist/native/proxy.d.ts +1 -1
- package/dist/native/proxy.js +36 -28
- package/dist/native/proxy.js.map +1 -1
- package/dist/routes/cdp.d.ts +7 -6
- package/dist/routes/cdp.js +9 -8
- package/dist/routes/cdp.js.map +1 -1
- package/dist/routes/components.d.ts +23 -5
- package/dist/routes/components.js +21 -4
- package/dist/routes/components.js.map +1 -1
- package/dist/routes/config/channels/endpoints/browse.js +39 -29
- package/dist/routes/config/channels/endpoints/browse.js.map +1 -1
- package/dist/routes/config/channels/endpoints/bulk.js +2 -6
- package/dist/routes/config/channels/endpoints/bulk.js.map +1 -1
- package/dist/routes/config/channels/endpoints/predefined.js +5 -15
- package/dist/routes/config/channels/endpoints/predefined.js.map +1 -1
- package/dist/routes/config/channels/endpoints/prefs.js +7 -6
- package/dist/routes/config/channels/endpoints/prefs.js.map +1 -1
- package/dist/routes/config/channels/endpoints/service.js +6 -5
- package/dist/routes/config/channels/endpoints/service.js.map +1 -1
- package/dist/routes/config/channels/endpoints/tags.js +4 -16
- package/dist/routes/config/channels/endpoints/tags.js.map +1 -1
- package/dist/routes/config/channels/table.d.ts +2 -1
- package/dist/routes/config/channels/table.js +11 -13
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/http/envelope.d.ts +6 -3
- package/dist/routes/config/http/envelope.js +18 -5
- package/dist/routes/config/http/envelope.js.map +1 -1
- package/dist/routes/config/index.d.ts +21 -13
- package/dist/routes/config/index.js +47 -34
- package/dist/routes/config/index.js.map +1 -1
- package/dist/routes/config/settings.d.ts +43 -6
- package/dist/routes/config/settings.js +225 -125
- package/dist/routes/config/settings.js.map +1 -1
- package/dist/routes/config/vocabulary.d.ts +24 -0
- package/dist/routes/config/vocabulary.js +33 -0
- package/dist/routes/config/vocabulary.js.map +1 -0
- package/dist/routes/debug.js +28 -13
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/root/content.js +33 -26
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/scripts/channels.js +23 -11
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/config.d.ts +1 -1
- package/dist/routes/root/scripts/config.js +127 -79
- package/dist/routes/root/scripts/config.js.map +1 -1
- package/dist/routes/root/scripts/shared.js +34 -23
- package/dist/routes/root/scripts/shared.js.map +1 -1
- package/dist/routes/root/scripts/status.handlers.d.ts +8 -13
- package/dist/routes/root/scripts/status.handlers.js +35 -21
- package/dist/routes/root/scripts/status.handlers.js.map +1 -1
- package/dist/routes/root/styles.js +13 -16
- package/dist/routes/root/styles.js.map +1 -1
- package/dist/routes/services.js +12 -16
- package/dist/routes/services.js.map +1 -1
- package/dist/routes/theme.js +12 -12
- package/dist/routes/theme.js.map +1 -1
- package/dist/routes/ui.d.ts +4 -3
- package/dist/routes/ui.js +17 -13
- package/dist/routes/ui.js.map +1 -1
- package/dist/service/generators.js +12 -9
- package/dist/service/generators.js.map +1 -1
- package/dist/streaming/captureSession.d.ts +4 -4
- package/dist/streaming/captureSession.js +6 -7
- package/dist/streaming/captureSession.js.map +1 -1
- package/dist/streaming/codec.d.ts +12 -2
- package/dist/streaming/codec.js +40 -28
- package/dist/streaming/codec.js.map +1 -1
- package/dist/streaming/fmp4Segmenter.d.ts +14 -4
- package/dist/streaming/fmp4Segmenter.js +99 -79
- package/dist/streaming/fmp4Segmenter.js.map +1 -1
- package/dist/streaming/hls.d.ts +21 -14
- package/dist/streaming/hls.js +148 -122
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.d.ts +36 -13
- package/dist/streaming/hlsResume.js +68 -37
- package/dist/streaming/hlsResume.js.map +1 -1
- package/dist/streaming/monitor.d.ts +5 -3
- package/dist/streaming/monitor.js +47 -37
- package/dist/streaming/monitor.js.map +1 -1
- package/dist/streaming/mp4Parser.d.ts +29 -4
- package/dist/streaming/mp4Parser.js +49 -7
- package/dist/streaming/mp4Parser.js.map +1 -1
- package/dist/streaming/playlistBuilder.d.ts +1 -2
- package/dist/streaming/playlistBuilder.js +2 -3
- package/dist/streaming/playlistBuilder.js.map +1 -1
- package/dist/streaming/preroll.d.ts +43 -16
- package/dist/streaming/preroll.js +112 -62
- package/dist/streaming/preroll.js.map +1 -1
- package/dist/streaming/pretune.d.ts +2 -2
- package/dist/streaming/pretune.js +11 -7
- package/dist/streaming/pretune.js.map +1 -1
- package/dist/streaming/recovery.d.ts +29 -2
- package/dist/streaming/recovery.js +43 -4
- package/dist/streaming/recovery.js.map +1 -1
- package/dist/streaming/registry.d.ts +13 -10
- package/dist/streaming/registry.js +6 -5
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +29 -20
- package/dist/streaming/setup.js +181 -170
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.d.ts +24 -12
- package/dist/streaming/showInfo.js +120 -113
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +15 -10
- package/dist/streaming/statusEmitter.js +24 -20
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/config.d.ts +24 -13
- package/dist/types/config.js +5 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/index.d.ts +3 -2
- package/dist/types/index.js +2 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/profiles.d.ts +2 -2
- package/dist/types/selection.d.ts +12 -10
- package/dist/types/streaming.d.ts +12 -5
- package/dist/types/streaming.js +12 -5
- package/dist/types/streaming.js.map +1 -1
- package/dist/utils/debugFilter.d.ts +3 -2
- package/dist/utils/debugFilter.js +7 -5
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/{hdhr → utils}/deviceId.d.ts +1 -1
- package/dist/{hdhr → utils}/deviceId.js +11 -29
- package/dist/utils/deviceId.js.map +1 -0
- package/dist/utils/errors.d.ts +7 -0
- package/dist/utils/errors.js +12 -3
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/fileLogger.d.ts +23 -12
- package/dist/utils/fileLogger.js +73 -41
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/httpErrors.d.ts +13 -0
- package/dist/utils/httpErrors.js +22 -0
- package/dist/utils/httpErrors.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.d.ts +9 -8
- package/dist/utils/logger.js +64 -20
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/markup.js +8 -3
- package/dist/utils/markup.js.map +1 -1
- package/dist/utils/plainObject.d.ts +6 -0
- package/dist/utils/plainObject.js +18 -0
- package/dist/utils/plainObject.js.map +1 -0
- package/dist/utils/runtimeIdentity.context.js +1 -29
- package/dist/utils/runtimeIdentity.context.js.map +1 -1
- package/dist/utils/runtimeIdentity.d.ts +17 -6
- package/dist/utils/runtimeIdentity.js +50 -17
- package/dist/utils/runtimeIdentity.js.map +1 -1
- package/package.json +5 -5
- package/dist/hdhr/deviceId.js.map +0 -1
package/dist/browser/index.js
CHANGED
|
@@ -3,10 +3,10 @@ import { TimerRegistry, systemClock } from "homebridge-plugin-utils";
|
|
|
3
3
|
import { clearLoginState, isLoginModeActive, setLoginDeps } from "./login.js";
|
|
4
4
|
import { getAllStreams, getStreamCount, hasActiveCaptureStreams, hasEstablishedStreams, isCaptureIdentity } from "../streaming/registry.js";
|
|
5
5
|
import { getCachedTabId, installStrayOpenTabReaper, onTabActivation } from "./tabSelection.js";
|
|
6
|
-
import { getChromeDataDir, getDataDir
|
|
6
|
+
import { getChromeDataDir, getDataDir } from "../config/paths.js";
|
|
7
7
|
import { getExtensionPage, launch } from "puppeteer-stream";
|
|
8
8
|
import { getGpuCapabilities, setGpuCapabilities } from "./display.js";
|
|
9
|
-
import { minimizeWindow, readWindowPlacement,
|
|
9
|
+
import { minimizeWindow, readWindowPlacement, unminimizeWindow, withCDPSession } from "./cdp.js";
|
|
10
10
|
import { CONFIG } from "../config/index.js";
|
|
11
11
|
import { EXTENSION_READY_EXPRESSION } from "./tabCapture.js";
|
|
12
12
|
import { clearChannelSelectionCaches } from "./channelSelection.js";
|
|
@@ -18,21 +18,22 @@ import fs from "node:fs";
|
|
|
18
18
|
import { getPresetViewport } from "../config/presets.js";
|
|
19
19
|
import path from "node:path";
|
|
20
20
|
import { launch as puppeteerLaunch } from "puppeteer-core";
|
|
21
|
+
import { registerConfigChangeHandler } from "../config/reactivity.js";
|
|
21
22
|
import { startPrecaching } from "./precaching.js";
|
|
22
23
|
const { promises: fsPromises } = fs;
|
|
23
24
|
/* Global variables maintain the application's runtime state across all operations. We minimize global state where possible, but some values must be shared across
|
|
24
25
|
* the application lifecycle:
|
|
25
26
|
*
|
|
26
|
-
* - supervisor: The browser capture-readiness supervisor. It is the single source of truth for the shared Chrome instance and its lifecycle (
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* - supervisor: The browser capture-readiness supervisor. It is the single source of truth for the shared Chrome instance and its lifecycle (every phase from
|
|
28
|
+
* absent to closing), so all streaming sessions use one Chrome process via supervisor.acquire(). It holds one discriminated-union lifecycle state that captures
|
|
29
|
+
* the browser reference, its launch timestamp, and whether a launch is in flight, and routes every relaunch through one loop-safe governor.
|
|
29
30
|
*
|
|
30
31
|
* - currentChromeVersion: The one piece of per-browser metadata the adapter holds directly, captured when the browser becomes ready and surfaced by the
|
|
31
32
|
* health endpoint.
|
|
32
33
|
*
|
|
33
34
|
* Stream tracking and ID generation live in streaming/registry.ts for unified stream management across all output types (HLS, MPEG-TS, etc.). Filesystem path
|
|
34
|
-
* resolution for persistent data (the Chrome profile and the streaming extension files) is centralized in config/paths.ts
|
|
35
|
-
*
|
|
35
|
+
* resolution for persistent data (the Chrome profile and the streaming extension files) is centralized in config/paths.ts and resolved on demand rather than held
|
|
36
|
+
* as module state here; ensureDataDirectory() below creates the data directory at startup.
|
|
36
37
|
*/
|
|
37
38
|
// The Chrome version string (e.g., "Chrome/144.0.7559.110") captured when the browser becomes capture-ready in launchReadyBrowser. Cleared when the browser
|
|
38
39
|
// 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
|
|
@@ -56,8 +57,8 @@ const KILL_WAIT_MS = 2000;
|
|
|
56
57
|
const BROWSER_TEARDOWN_DRAIN_BOUND_MS = TERM_WAIT_MS + KILL_WAIT_MS;
|
|
57
58
|
/**
|
|
58
59
|
* 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 -
|
|
59
|
-
* an operator's change to the recovery.relaunch* settings takes effect
|
|
60
|
-
*
|
|
60
|
+
* an operator's change to the recovery.relaunch* settings takes effect at the next governor decision without reconstructing the supervisor. The scalar
|
|
61
|
+
* tolerances come from CONFIG.recovery (conservative, biased eager-for-the-first-failure: the first failures cost no cooldown; only
|
|
61
62
|
* repeated failures within the window trip the escalating cooldown); the cooldown ladder is the fixed escalation shape above.
|
|
62
63
|
* @returns The current launch governor policy.
|
|
63
64
|
*/
|
|
@@ -69,12 +70,12 @@ function buildRelaunchPolicy() {
|
|
|
69
70
|
healthHoldMs: CONFIG.recovery.relaunchHealthHold
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
|
-
/* The one browser capture-readiness supervisor for the process lifetime. It owns the lifecycle state (
|
|
73
|
-
* browser reference, launch promise, and launch timestamp, and routes every relaunch through one loop-safe governor. The adapter
|
|
74
|
-
* ports: launchReadyBrowser (spawn Chrome and run the readiness gate), closeBrowserInstance (teardown), systemClock.now (time),
|
|
75
|
-
* bounds), and onSupervisorStateChange (the loud degraded alarm and the recovery notice). All browser access flows through it:
|
|
76
|
-
* non-launching reads derive from current() and currentLaunchTime(). The injected ports are hoisted function declarations, so
|
|
77
|
-
* though they are defined further down the module.
|
|
73
|
+
/* The one browser capture-readiness supervisor for the process lifetime. It owns the lifecycle state (one discriminated union covering every phase from absent to
|
|
74
|
+
* closing) that unifies the browser reference, launch promise, and launch timestamp, and routes every relaunch through one loop-safe governor. The adapter
|
|
75
|
+
* injects the impure ports: launchReadyBrowser (spawn Chrome and run the readiness gate), closeBrowserInstance (teardown), systemClock.now (time),
|
|
76
|
+
* buildRelaunchPolicy (live config bounds), and onSupervisorStateChange (the loud degraded alarm and the recovery notice). All browser access flows through it:
|
|
77
|
+
* getCurrentBrowser is acquire(); the non-launching reads derive from current() and currentLaunchTime(). The injected ports are hoisted function declarations, so
|
|
78
|
+
* referencing them here is safe even though they are defined further down the module.
|
|
78
79
|
*/
|
|
79
80
|
const supervisor = createBrowserSupervisor({ close: closeBrowserInstance, launch: launchReadyBrowser, now: () => systemClock.now(),
|
|
80
81
|
onStateChange: onSupervisorStateChange, policy: buildRelaunchPolicy });
|
|
@@ -121,7 +122,8 @@ function onSupervisorStateChange(next, previous) {
|
|
|
121
122
|
let captureProbe = null;
|
|
122
123
|
/**
|
|
123
124
|
* 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
|
|
124
|
-
* launch, since the streaming layer is imported during server startup). Separating the wiring from the call keeps browser/index.ts
|
|
125
|
+
* launch, since the streaming layer is imported during server startup). Separating the wiring from the call keeps browser/index.ts from importing
|
|
126
|
+
* streaming/setup.ts, which already imports this module, so the dependency stays one-directional.
|
|
125
127
|
* @param probe - The probe to run against a freshly-launched browser; it resolves when the browser can capture and rejects when it cannot.
|
|
126
128
|
*/
|
|
127
129
|
export function setCaptureProbe(probe) {
|
|
@@ -136,7 +138,7 @@ export function setCaptureProbe(probe) {
|
|
|
136
138
|
let streamTerminator = null;
|
|
137
139
|
/**
|
|
138
140
|
* Injects the authoritative stream terminator used when browser readiness is lost. Called once from streaming/lifecycle.ts at module load. Separating the wiring
|
|
139
|
-
* from the call keeps browser/index.ts
|
|
141
|
+
* from the call keeps browser/index.ts from importing streaming/lifecycle.ts, which already imports this module, so the dependency stays one-directional.
|
|
140
142
|
* @param terminator - The function that tears down one stream and all of the resources it owns.
|
|
141
143
|
*/
|
|
142
144
|
export function setStreamTerminator(terminator) {
|
|
@@ -144,10 +146,8 @@ export function setStreamTerminator(terminator) {
|
|
|
144
146
|
}
|
|
145
147
|
// The identity of the periodic sweep on the stale-page owner's registry.
|
|
146
148
|
const STALE_PAGE_SWEEP_KEY = "sweep";
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
*/
|
|
150
|
-
let stalePageTimers = null;
|
|
149
|
+
// The running stale-page sweep. Null until started, and again once stopped.
|
|
150
|
+
let stalePageSweep = null;
|
|
151
151
|
/* Opportunistic browser restart state. Chrome accumulates memory pressure, GPU process issues, and general flakiness over multi-hour sessions with continuous
|
|
152
152
|
* media playback. We proactively restart Chrome after it has been running for BROWSER_MAX_AGE, waiting for a quiet period with zero active streams before
|
|
153
153
|
* executing the restart. After the restart, a fresh browser is launched immediately so it is ready for the next stream request.
|
|
@@ -175,8 +175,9 @@ let restartTimers = null;
|
|
|
175
175
|
function cancelRestartQuietTimer() {
|
|
176
176
|
restartTimers?.clear(RESTART_QUIET_KEY);
|
|
177
177
|
}
|
|
178
|
-
//
|
|
179
|
-
//
|
|
178
|
+
// The process-wide graceful-shutdown state. app.ts shutdown() sets it early, and closeBrowser() sets it as a fallback for direct calls. While it is set, the
|
|
179
|
+
// disconnect path stays quiet (no error log and no status emit) and the restart paths stand down; stream termination still runs. This prevents false
|
|
180
|
+
// "unexpected disconnect" errors during graceful shutdown.
|
|
180
181
|
let gracefulShutdownInProgress = false;
|
|
181
182
|
/**
|
|
182
183
|
* Returns true if graceful shutdown is in progress.
|
|
@@ -241,7 +242,7 @@ export function pickCarrierPage(pages) {
|
|
|
241
242
|
return pages.find(isCarrierPage) ?? null;
|
|
242
243
|
}
|
|
243
244
|
/* Which window Chrome's own tables call the shared one, keyed on the browser that owns it. The value belongs to CDP's window table and is never comparable with
|
|
244
|
-
* the capture extension's chrome.windows ids: those are two independent tables, and this is the only place
|
|
245
|
+
* the capture extension's chrome.windows ids: those are two independent tables, and this is the only place the shared window's identity is recorded.
|
|
245
246
|
*
|
|
246
247
|
* Keyed on the browser and never cleared, for the reason the own-window mark above is: the identity is a fact about how that browser was created, an entry dies
|
|
247
248
|
* with the browser holding it, and a relaunch records its own. An id that outlived its browser is exactly what would place a capture tab in the wrong window
|
|
@@ -277,7 +278,7 @@ async function readPageWindow(page) {
|
|
|
277
278
|
* Records which window the shared one is, reading it from a page that sits in it.
|
|
278
279
|
*
|
|
279
280
|
* This is the one way an identity is written. The launch path calls it once the launch-era pages exist, so every browser this process publishes carries the
|
|
280
|
-
* answer before anything asks for a capture tab
|
|
281
|
+
* answer before anything asks for a capture tab.
|
|
281
282
|
* @param browser - The browser whose shared window is being recorded.
|
|
282
283
|
* @param page - A page that sits in that window.
|
|
283
284
|
*/
|
|
@@ -341,6 +342,16 @@ export { endLoginMode, getLoginPage, getLoginStatus, setLoginModeEndObserver, st
|
|
|
341
342
|
// captures and is waiting for its streams to end. The purpose and impairment types cross the same boundary, so a caller declares what it needs the browser for and
|
|
342
343
|
// reads the mark without importing the supervisor.
|
|
343
344
|
export { BrowserCaptureImpairedError, BrowserSupersededError, BrowserUnavailableError } from "./browserSupervisor.js";
|
|
345
|
+
/**
|
|
346
|
+
* Thrown when the browser launched but its capture system failed a launch gate: the capture extension never became ready, or the capture probe found no
|
|
347
|
+
* working capture across its attempts. setupStream answers it as a capture-infrastructure failure by its type, so its message is free to state what happened.
|
|
348
|
+
*/
|
|
349
|
+
export class CaptureLaunchError extends Error {
|
|
350
|
+
constructor(message, options) {
|
|
351
|
+
super(message, options);
|
|
352
|
+
this.name = "CaptureLaunchError";
|
|
353
|
+
}
|
|
354
|
+
}
|
|
344
355
|
/* The one window-presentation executor for the process lifetime. Its collaborators all live in this module: the registry predicate that says whether capture is
|
|
345
356
|
* reading the compositor, login mode's own flag, the shutdown gate, the two CDP primitives, and a page resolver built on the supervisor.
|
|
346
357
|
*
|
|
@@ -406,6 +417,20 @@ export async function syncWindowVisibility(page) {
|
|
|
406
417
|
// same setter/getter pattern as setChromeUserAgent in chromeFetch.ts. Both accessors are hoisted function declarations, so this call reads them at module
|
|
407
418
|
// evaluation time regardless of where they sit in the file. No clock is supplied, so login runs on the system clock the port defaults to.
|
|
408
419
|
setLoginDeps({ getBrowserInstance, syncWindowVisibility });
|
|
420
|
+
/**
|
|
421
|
+
* Composes the system status the SSE bus carries. A browser reads as connected only when one is published and its connection is live, and the memory readings
|
|
422
|
+
* carry only the figures the status reports.
|
|
423
|
+
* @param facts - The browser and capture state and the process and registry readings, read at the moment the status is composed.
|
|
424
|
+
* @returns The system status.
|
|
425
|
+
*/
|
|
426
|
+
export function composeSystemStatus(facts) {
|
|
427
|
+
return {
|
|
428
|
+
browser: { captureImpaired: facts.captureImpaired, connected: facts.browser?.connected ?? false, pageCount: facts.pageCount },
|
|
429
|
+
memory: { heapUsed: facts.memory.heapUsed, rss: facts.memory.rss },
|
|
430
|
+
streams: { active: facts.streams.active, limit: facts.streams.limit },
|
|
431
|
+
uptime: facts.uptime
|
|
432
|
+
};
|
|
433
|
+
}
|
|
409
434
|
/**
|
|
410
435
|
* Computes the current system status and emits it to SSE subscribers. Called when browser state changes significantly or when streams are added/removed.
|
|
411
436
|
*/
|
|
@@ -423,36 +448,20 @@ export async function emitCurrentSystemStatus() {
|
|
|
423
448
|
catch (_error) {
|
|
424
449
|
// Ignore errors getting page count.
|
|
425
450
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
*/
|
|
434
|
-
captureImpaired: supervisor.captureImpairment() !== null,
|
|
435
|
-
connected: !!browser && browser.connected,
|
|
436
|
-
pageCount
|
|
437
|
-
},
|
|
438
|
-
memory: {
|
|
439
|
-
heapUsed: memUsage.heapUsed,
|
|
440
|
-
rss: memUsage.rss
|
|
441
|
-
},
|
|
442
|
-
streams: {
|
|
443
|
-
active: getStreamCount(),
|
|
444
|
-
limit: CONFIG.streaming.maxConcurrentStreams
|
|
445
|
-
},
|
|
446
|
-
uptime: process.uptime()
|
|
447
|
-
};
|
|
448
|
-
emitSystemStatusChanged(status);
|
|
451
|
+
/* The capture-impairment mark is read here, at compose time, beside the connectivity the composer reads, rather than as a snapshot taken before the page-count
|
|
452
|
+
* await. This function is called un-awaited from many sites, so two calls can be in flight around the instant a mark lands; a pre-mark snapshot settling after
|
|
453
|
+
* the mark's own emit would broadcast false over the cached true and the dedupe would honor it. Reading here makes every emit describe the state at the moment
|
|
454
|
+
* consumers receive it, so the later of two racing emits is also the truer one.
|
|
455
|
+
*/
|
|
456
|
+
emitSystemStatusChanged(composeSystemStatus({ browser, captureImpaired: supervisor.captureImpairment() !== null, memory: process.memoryUsage(), pageCount,
|
|
457
|
+
streams: { active: getStreamCount(), limit: CONFIG.streaming.maxConcurrentStreams }, uptime: process.uptime() }));
|
|
449
458
|
}
|
|
450
459
|
/**
|
|
451
460
|
* Registers a page as managed by PrismCast. This should be called immediately after creating a page via browser.newPage(). Registered pages are tracked for stale
|
|
452
461
|
* page cleanup, while unregistered pages (manually opened, site popups, etc.) are left alone.
|
|
453
462
|
*
|
|
454
|
-
* Each registered page receives a unique ID that persists for the page's lifetime
|
|
455
|
-
*
|
|
463
|
+
* Each registered page receives a unique ID that persists for the page's lifetime, read back through the Page reference by getManagedPageId. The staleness map,
|
|
464
|
+
* the in-flight set and the stale page cleanup's decision core all work on this stable string key rather than on Page objects.
|
|
456
465
|
* @param page - The Puppeteer Page to register.
|
|
457
466
|
* @param options - Registration options. Set inFlight when an operation owns the page for its duration but nothing the stale page cleanup reads records that
|
|
458
467
|
* ownership - a stream setup that has not yet written its page into the registry, or a discovery walk whose page belongs to no stream at all - so the cleanup
|
|
@@ -508,11 +517,7 @@ function clearPageTracking() {
|
|
|
508
517
|
}
|
|
509
518
|
/**
|
|
510
519
|
* Ensures the data directory exists, creating it if necessary. This should be called during application startup before any operations that depend on the data
|
|
511
|
-
* directory
|
|
512
|
-
*
|
|
513
|
-
* The data directory stores:
|
|
514
|
-
* - Chrome profile data (cookies, local storage, session state)
|
|
515
|
-
* - Extension files (when running as a packaged executable)
|
|
520
|
+
* directory, browser launch above all, because the directory holds the Chrome profile data (cookies, local storage, session state).
|
|
516
521
|
*/
|
|
517
522
|
export async function ensureDataDirectory() {
|
|
518
523
|
try {
|
|
@@ -524,7 +529,7 @@ export async function ensureDataDirectory() {
|
|
|
524
529
|
throw error;
|
|
525
530
|
}
|
|
526
531
|
// 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
|
|
527
|
-
// exists, before any subsequent step reads it. Future retirements add
|
|
532
|
+
// exists, before any subsequent step reads it. Future retirements add an entry to RETIRED_ARTIFACTS; neither purgeLegacyArtifacts nor this call site changes.
|
|
528
533
|
await purgeLegacyArtifacts();
|
|
529
534
|
}
|
|
530
535
|
const RETIRED_ARTIFACTS = [
|
|
@@ -577,6 +582,38 @@ async function purgeLegacyArtifacts() {
|
|
|
577
582
|
function syncSleep(ms) {
|
|
578
583
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
579
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* Reports whether a process's command line runs Chrome on the given profile directory: the first stage of the kill filter and of the holder test, so they agree
|
|
587
|
+
* on which processes are on the profile.
|
|
588
|
+
* @param commandLine - The process's command line as the OS reports it.
|
|
589
|
+
* @param profileDir - The Chrome user-data-dir to match against.
|
|
590
|
+
* @returns True when the command line carries the profile flag for exactly that directory.
|
|
591
|
+
*/
|
|
592
|
+
function usesProfile(commandLine, profileDir) {
|
|
593
|
+
const target = "--user-data-dir=" + profileDir;
|
|
594
|
+
// Chrome puppeteer launches with --user-data-dir=<path> (equals form, no quotes). We also verify the character following the path is whitespace or
|
|
595
|
+
// end-of-string, otherwise "/x/y" would match "--user-data-dir=/x/yz".
|
|
596
|
+
const idx = commandLine.indexOf(target);
|
|
597
|
+
if (idx === -1) {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
const after = commandLine.charAt(idx + target.length);
|
|
601
|
+
return (after === "") || (after === " ") || (after === "\t");
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Reports whether a Chrome process on our profile is ours to terminate: the ownership stage of the kill filter, and the stage the holder test reads to decide
|
|
605
|
+
* which Chrome the sweep spares, so the sweep never keeps the files of a Chrome it ends nor removes those of one it spares. The parent-child relationship in the
|
|
606
|
+
* OS process table is the ownership proof, so no in-memory flag is consulted.
|
|
607
|
+
* @param entry - The process-table row of a Chrome process on our profile.
|
|
608
|
+
* @param ownPid - The current process's PID.
|
|
609
|
+
* @param isProcessAlive - Predicate that returns true when the given PID is currently a live process.
|
|
610
|
+
* @returns True when we spawned the process or its parent is no longer alive.
|
|
611
|
+
*/
|
|
612
|
+
function isOwnedChrome(entry, ownPid, isProcessAlive) {
|
|
613
|
+
// Ours if we spawned it (ppid is us) or if its parent is no longer alive (orphaned from a previous PrismCast that died). A live unrelated parent owns it, not
|
|
614
|
+
// us.
|
|
615
|
+
return (entry.ppid === ownPid) || !isProcessAlive(entry.ppid);
|
|
616
|
+
}
|
|
580
617
|
/**
|
|
581
618
|
* Identifies Chrome processes that this PrismCast instance is responsible for terminating. The filter has two stages: command-line discovery (anything using
|
|
582
619
|
* 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
|
|
@@ -592,22 +629,25 @@ function syncSleep(ms) {
|
|
|
592
629
|
* @returns The PIDs to terminate, in process-table order.
|
|
593
630
|
*/
|
|
594
631
|
export function findChromeProcessesUsingProfile(processes, profileDir, ownPid, isProcessAlive) {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
632
|
+
return processes.filter((p) => usesProfile(p.commandLine, profileDir) && isOwnedChrome(p, ownPid, isProcessAlive)).map((p) => p.pid);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Finds a live Chrome outside this process that holds our profile directory. A Chrome holds the profile when it roots a tree of processes on the profile and the
|
|
636
|
+
* kill filter's ownership stage spares it, its parent alive and not this process. The test reads the root because Chrome's helpers carry the profile flag with
|
|
637
|
+
* the Chrome main as their parent, so a helper's parent, the main, is a live process that is not this one, and every helper of this process's own Chrome would
|
|
638
|
+
* otherwise read as a holder. It reads the ownership stage rather than a liveness test of its own so it stays in step with the kill filter: the sweep never keeps
|
|
639
|
+
* the files of a Chrome it ends nor removes those of one it spares. A helper whose parent lacks the profile flag reads as a root, so that error keeps the lock files
|
|
640
|
+
* rather than removing them, and the next sweep with no Chrome alive removes them.
|
|
641
|
+
* @param processes - The current process table snapshot: the sweep's scan from before its kill loop, or the teardown's scan once its Chrome has exited.
|
|
642
|
+
* @param profileDir - The Chrome user-data-dir to match against.
|
|
643
|
+
* @param ownPid - The current process's PID (a Chrome whose parent is us is ours, never a holder).
|
|
644
|
+
* @param isProcessAlive - Predicate that returns true when the given PID is currently a live process. Injected so tests do not depend on real PIDs.
|
|
645
|
+
* @returns The PID of the first holder in process-table order, or null when no live Chrome outside this process holds the profile.
|
|
646
|
+
*/
|
|
647
|
+
export function findProfileHolder(processes, profileDir, ownPid, isProcessAlive) {
|
|
648
|
+
const onProfile = processes.filter((p) => usesProfile(p.commandLine, profileDir));
|
|
649
|
+
const profilePids = new Set(onProfile.map((p) => p.pid));
|
|
650
|
+
return onProfile.find((p) => !profilePids.has(p.ppid) && !isOwnedChrome(p, ownPid, isProcessAlive))?.pid ?? null;
|
|
611
651
|
}
|
|
612
652
|
/**
|
|
613
653
|
* Ensures a clean slate for browser launch by terminating any stale Chrome processes and removing orphaned profile lock files. Chrome locks its profile
|
|
@@ -620,8 +660,9 @@ export function findChromeProcessesUsingProfile(processes, profileDir, ownPid, i
|
|
|
620
660
|
* process exit handler: Chrome may be running normally (e.g., after a capture probe timeout) and an immediate SIGKILL would corrupt its profile databases,
|
|
621
661
|
* poisoning the Docker volume for subsequent container restarts.
|
|
622
662
|
*
|
|
623
|
-
* The ownership filter
|
|
624
|
-
* spawned Chrome
|
|
663
|
+
* The ownership filter and the holder test together make this function safe to call from any context, including a rejected-duplicate startup's exit handler: a
|
|
664
|
+
* duplicate that never spawned Chrome finds nothing matching its ownership criteria, signals nothing, and leaves the profile's lock files to the live Chrome
|
|
665
|
+
* that holds them.
|
|
625
666
|
*
|
|
626
667
|
* 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
|
|
627
668
|
* exist - the discovery and lock-file cleanup are both no-ops in the empty case.
|
|
@@ -629,7 +670,10 @@ export function findChromeProcessesUsingProfile(processes, profileDir, ownPid, i
|
|
|
629
670
|
export function killStaleChrome() {
|
|
630
671
|
const profileDir = getChromeDataDir(CONFIG);
|
|
631
672
|
const POLL_INTERVAL_MS = 200;
|
|
632
|
-
|
|
673
|
+
// The kill filter and the lock-file removal's holder test read the one scan taken before the kill loop, because a scan after it would read the helpers of each
|
|
674
|
+
// Chrome the loop ends, adopted by a live process once their main exits, as live holders.
|
|
675
|
+
const processes = listProcesses();
|
|
676
|
+
const pidsToKill = findChromeProcessesUsingProfile(processes, profileDir, process.pid, isProcessRunning);
|
|
633
677
|
for (const pid of pidsToKill) {
|
|
634
678
|
try {
|
|
635
679
|
// Send SIGTERM first to give Chrome a chance to flush its profile databases (LevelDB, extension state, session storage) before exiting. This is critical
|
|
@@ -652,15 +696,13 @@ export function killStaleChrome() {
|
|
|
652
696
|
}
|
|
653
697
|
}
|
|
654
698
|
catch (error) {
|
|
655
|
-
// ESRCH means the process
|
|
656
|
-
// previous container's PID namespace.
|
|
699
|
+
// ESRCH means the process exited between the process-table scan above and the signal, which is a benign race.
|
|
657
700
|
if (error.code !== "ESRCH") {
|
|
658
701
|
LOG.warn("Failed to signal Chrome process %d: %s.", pid, formatError(error));
|
|
659
702
|
}
|
|
660
703
|
}
|
|
661
704
|
}
|
|
662
|
-
|
|
663
|
-
cleanStaleProfileFiles(profileDir);
|
|
705
|
+
cleanStaleProfileFiles(processes, profileDir);
|
|
664
706
|
}
|
|
665
707
|
/**
|
|
666
708
|
* 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
|
|
@@ -682,12 +724,23 @@ function waitForChromeExit(pid, timeoutMs, pollIntervalMs) {
|
|
|
682
724
|
return !isProcessRunning(pid);
|
|
683
725
|
}
|
|
684
726
|
/**
|
|
685
|
-
* Removes
|
|
686
|
-
* (container kill, SIGKILL, crash) leaves them behind. Stale lock files prevent Chrome from acquiring the
|
|
687
|
-
* Puppeteer connection.
|
|
727
|
+
* Removes the profile's lock files and the DevTools port file unless a live Chrome outside this process holds the profile. Chrome writes these while running and
|
|
728
|
+
* removes them on clean shutdown, but an unclean exit (container kill, SIGKILL, crash) leaves them behind. Stale lock files prevent Chrome from acquiring the
|
|
729
|
+
* profile, and a stale DevToolsActivePort can confuse the Puppeteer connection. This is the one removal of those files, so each caller, the stale-Chrome sweep and
|
|
730
|
+
* the teardown primitive, reaches them through the holder test, and none removes them from under a live holder.
|
|
731
|
+
* @param processes - The process table the holder test reads: the sweep's scan from before its kill loop, or the teardown's scan once its Chrome has exited.
|
|
688
732
|
* @param profileDir - The Chrome user data directory path.
|
|
689
733
|
*/
|
|
690
|
-
function cleanStaleProfileFiles(profileDir) {
|
|
734
|
+
export function cleanStaleProfileFiles(processes, profileDir) {
|
|
735
|
+
const holder = findProfileHolder(processes, profileDir, process.pid, isProcessRunning);
|
|
736
|
+
/* The lock and port files belong to whichever Chrome holds the profile, so they are stale only when no live Chrome outside this process holds it. Removing
|
|
737
|
+
* them from under a live holder, such as the instance a refused second start found, would let the next Chrome launched on the profile start beside the
|
|
738
|
+
* running one instead of meeting its singleton.
|
|
739
|
+
*/
|
|
740
|
+
if (holder !== null) {
|
|
741
|
+
LOG.debug("browser:lifecycle", "Kept the profile lock files of live Chrome process %d.", holder);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
691
744
|
// Chrome's profile lock mechanism uses three symlinks: SingletonLock (hostname-PID pair), SingletonCookie (numeric verification token), and SingletonSocket
|
|
692
745
|
// (path to the IPC socket). All three must be removed for Chrome to acquire a fresh lock. DevToolsActivePort contains the debugging port from the previous
|
|
693
746
|
// session and is irrelevant when launching a new browser instance.
|
|
@@ -893,9 +946,9 @@ export function buildLaunchOptions() {
|
|
|
893
946
|
executablePath: getExecutablePath(),
|
|
894
947
|
/* PrismCast owns process signals, so the launcher's own listeners are off. Left on, the puppeteer launcher installs handlers for SIGHUP, SIGINT, and SIGTERM
|
|
895
948
|
* on the node process and, on SIGTERM or SIGHUP, ends Chrome's entire process group with SIGKILL - and on SIGINT it kills the group and then exits the process
|
|
896
|
-
* outright.
|
|
897
|
-
* and forfeits whatever it had pending: a window placement inside its save debounce, its session state. With the listeners off,
|
|
898
|
-
* own
|
|
949
|
+
* outright. Each of them races the shutdown handler app.ts installs for the same signal and preempts closeBrowserInstance's SIGTERM ladder below, so Chrome
|
|
950
|
+
* gets no shutdown at all and forfeits whatever it had pending: a window placement inside its save debounce, its session state. With the listeners off, every
|
|
951
|
+
* one of these signals reaches PrismCast's own handlers, the graceful path runs, and Chrome exits through the ladder with its pending writes committed.
|
|
899
952
|
*/
|
|
900
953
|
handleSIGHUP: false,
|
|
901
954
|
handleSIGINT: false,
|
|
@@ -929,8 +982,8 @@ export function buildLaunchOptions() {
|
|
|
929
982
|
"--enable-blink-features=IdleDetection",
|
|
930
983
|
"--mute-audio"
|
|
931
984
|
],
|
|
932
|
-
// Use pipe mode for browser communication instead of WebSocket. Pipe mode is faster and more reliable, especially under load. It
|
|
933
|
-
//
|
|
985
|
+
// Use pipe mode for browser communication instead of WebSocket. Pipe mode is faster and more reliable, especially under load. It runs the DevTools Protocol
|
|
986
|
+
// over a dedicated pair of pipes (file descriptors 3 and 4) rather than a network socket.
|
|
934
987
|
pipe: true,
|
|
935
988
|
// Persistent user data directory for Chrome profile. This directory stores cookies, local storage, and other session data. By persisting this across
|
|
936
989
|
// restarts, sites remember login state and don't require re-authentication.
|
|
@@ -981,6 +1034,32 @@ export async function emulateCaptureSurface(page) {
|
|
|
981
1034
|
export async function emulateLayoutSurface(page) {
|
|
982
1035
|
return declareSurface(page, NATIVE_DENSITY);
|
|
983
1036
|
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Re-issues a capture page's own standing device-metrics override. Chrome composes the capture of a selected tab from the window's fitted presentation rather than
|
|
1039
|
+
* from the emulated surface, and re-sending the page's standing override is what moves the composition back to the emulated surface, while a capture already
|
|
1040
|
+
* composing that surface is left exactly as it was. Callable at any time, from anywhere, at any frequency, because the values sent are the ones Puppeteer has
|
|
1041
|
+
* already declared on this page - nothing about the page's emulation changes.
|
|
1042
|
+
*
|
|
1043
|
+
* The re-issue goes through the page's own emulation session, the one Puppeteer keeps on the page's primary target, because the override is only as durable as
|
|
1044
|
+
* the session that holds it and Chrome restores the window's view size when a session that declared one detaches.
|
|
1045
|
+
* @param page - The page to re-affirm. A page carrying no explicitly declared density is left alone.
|
|
1046
|
+
* @throws Whatever the viewport setter rejects with. Each trigger site decides whether that matters to it.
|
|
1047
|
+
*/
|
|
1048
|
+
export async function reaffirmCaptureSurface(page) {
|
|
1049
|
+
/* An explicitly declared, positive density is precisely the mark of a capture page, which is what makes this function safe to fire at any page from any trigger.
|
|
1050
|
+
* A page PrismCast has not emulated carries no viewport at all and falls out on the first test; a page emulated for layout declares the display's own density
|
|
1051
|
+
* through Chrome's disable value of 0, which the positive test excludes.
|
|
1052
|
+
*/
|
|
1053
|
+
const viewport = page.viewport();
|
|
1054
|
+
if (!viewport) {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
const deviceScaleFactor = viewport.deviceScaleFactor;
|
|
1058
|
+
if ((typeof deviceScaleFactor !== "number") || !(deviceScaleFactor > 0)) {
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
await page.setViewport(viewport);
|
|
1062
|
+
}
|
|
984
1063
|
/**
|
|
985
1064
|
* Derives the creation bounds for a window that should open exactly where an existing window rests. The four numbers always travel, so the new window lands on
|
|
986
1065
|
* the same frame. The state travels only when it is "maximized", because that is the one state Chrome saves alongside the frame and a maximized window mirrored
|
|
@@ -1164,27 +1243,6 @@ export function healActivatedCaptureTab(tabId, deps = { getAllStreams, getCached
|
|
|
1164
1243
|
onTabActivation((tabId) => {
|
|
1165
1244
|
healActivatedCaptureTab(tabId);
|
|
1166
1245
|
});
|
|
1167
|
-
/**
|
|
1168
|
-
* Custom launch function that modifies Chrome arguments when running as a packaged executable. The packaged version cannot load extensions from node_modules
|
|
1169
|
-
* (which is bundled inside the executable), so we point the extension paths to our extracted extension files in the data directory.
|
|
1170
|
-
* @param opts - The launch options to modify.
|
|
1171
|
-
* @returns The launched browser instance.
|
|
1172
|
-
*/
|
|
1173
|
-
async function launchWithCustomArgs(opts) {
|
|
1174
|
-
// When running as a packaged executable (process.pkg is set by the pkg bundler), we need to replace the extension paths. puppeteer-stream points
|
|
1175
|
-
// opts.enableExtensions at its own node_modules-relative extension directory, which puppeteer-core installs via a CDP browser.installExtension() call after
|
|
1176
|
-
// Chrome starts, rather than via --load-extension/--disable-extensions-except CLI flags - so that path still resolves inside node_modules, which does not
|
|
1177
|
-
// 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
|
|
1178
|
-
// extension files instead.
|
|
1179
|
-
if (process.pkg) {
|
|
1180
|
-
const extensionPath = getExtensionDir(CONFIG);
|
|
1181
|
-
// Remove any existing extension arguments and add our own pointing to the extracted extension.
|
|
1182
|
-
opts.args = (opts.args ?? [])
|
|
1183
|
-
.filter((arg) => !arg.startsWith("--load-extension=") && !arg.startsWith("--disable-extensions-except="))
|
|
1184
|
-
.concat(["--disable-extensions-except=" + extensionPath, "--load-extension=" + extensionPath]);
|
|
1185
|
-
}
|
|
1186
|
-
return puppeteerLaunch(opts);
|
|
1187
|
-
}
|
|
1188
1246
|
/**
|
|
1189
1247
|
* Formats the GPU capabilities into a human-readable suffix for the "Chrome ready" log line. The renderer string is already cleaned (ANGLE wrapper and Metal
|
|
1190
1248
|
* prefix stripped) at detection time, so this function uses it directly and appends hardware-accelerated codec names in brackets when available.
|
|
@@ -1249,7 +1307,8 @@ async function detectBrowserCapabilities(browser) {
|
|
|
1249
1307
|
return ext ? String(gl.getParameter(ext.UNMASKED_RENDERER_WEBGL)) : String(gl.getParameter(gl.RENDERER));
|
|
1250
1308
|
});
|
|
1251
1309
|
// Extract the meaningful GPU name. ANGLE wraps the actual GPU identity: "ANGLE (Vendor, GPU Name, API Version)". The GPU name is the second
|
|
1252
|
-
// comma-separated field.
|
|
1310
|
+
// comma-separated field. A non-ANGLE renderer string is used as is, and the device string from CDP is used only when WebGL reports the masked
|
|
1311
|
+
// "WebKit WebGL" or nothing at all.
|
|
1253
1312
|
let renderer = webglRenderer;
|
|
1254
1313
|
const anglePart = /^ANGLE \([^,]+, ([^,]+)/.exec(webglRenderer)?.[1];
|
|
1255
1314
|
if (anglePart) {
|
|
@@ -1373,7 +1432,7 @@ function relinquishBrowserReadiness(streamTerminationReason) {
|
|
|
1373
1432
|
*/
|
|
1374
1433
|
function handleBrowserDisconnect() {
|
|
1375
1434
|
// Announce the unexpected disconnect before tearing down (the message says streams will be terminated, which relinquish then does). Suppressed during a full
|
|
1376
|
-
// server shutdown, where closeBrowser() set the flag and the disconnect is intentional.
|
|
1435
|
+
// server shutdown, where app.ts shutdown() (or closeBrowser() as a fallback) set the flag and the disconnect is intentional.
|
|
1377
1436
|
if (!gracefulShutdownInProgress) {
|
|
1378
1437
|
LOG.error("Browser disconnected unexpectedly. All active streams will be terminated.");
|
|
1379
1438
|
}
|
|
@@ -1434,23 +1493,36 @@ async function launchReadyBrowser() {
|
|
|
1434
1493
|
// Seed the profile's extension developer-mode flag before Chrome reads the profile. Chrome loads the unpacked capture extension only when the flag is set, and
|
|
1435
1494
|
// this is the one function every launch passes through, so it is also the only place that runs before the first-ever launch on a fresh install.
|
|
1436
1495
|
seedProfilePreferences(getChromeDataDir(CONFIG));
|
|
1437
|
-
// The launch function from puppeteer-stream wraps standard Puppeteer launch to inject the streaming extension. We
|
|
1438
|
-
//
|
|
1439
|
-
|
|
1496
|
+
// The launch function from puppeteer-stream wraps a standard Puppeteer launch to inject the streaming extension. We hand it puppeteer-core's own launch as its
|
|
1497
|
+
// launch port, so the browser comes from the puppeteer-core this module depends on rather than whichever copy puppeteer-stream resolves for itself. This happens
|
|
1498
|
+
// on first stream request, after a browser crash, during server warmup, or during a governed relaunch.
|
|
1499
|
+
const browser = await launch({ launch: puppeteerLaunch }, buildLaunchOptions());
|
|
1440
1500
|
try {
|
|
1441
1501
|
LOG.debug("timing:browser", "Chrome process spawned. (+%sms)", browserElapsed());
|
|
1442
|
-
//
|
|
1443
|
-
//
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1502
|
+
// The init timeout is live, so it is read once per launch: the error below then names the bound the wait applied, and a save made during a handshake applies
|
|
1503
|
+
// at the next launch.
|
|
1504
|
+
const initTimeout = CONFIG.browser.initTimeout;
|
|
1505
|
+
/* Readiness gate, handshake tier (cheap, on-suspicion). Poll for the puppeteer-stream extension to finish initializing - it injects a START_RECORDING function
|
|
1506
|
+
* 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
|
|
1507
|
+
* the extension loads (typically 200-500ms). On failure this THROWS rather than warning-and-proceeding: an unregistered extension means chrome.tabs is undefined
|
|
1508
|
+
* and every capture acquisition would hang, so the instance is not capture-ready and must not be published.
|
|
1509
|
+
*
|
|
1510
|
+
* Each step throws a CaptureLaunchError whose message states what that step saw, with the underlying error as its cause. The type, not the wording, is what
|
|
1511
|
+
* earns the 503 back-off at the setup layer (the same answer the capability-tier probe failure gets), rather than a 500 the client would not back off from - an
|
|
1512
|
+
* unregistered extension is a capture-infrastructure fault, and a fresh relaunch usually clears it.
|
|
1513
|
+
*/
|
|
1514
|
+
let extensionPage;
|
|
1448
1515
|
try {
|
|
1449
|
-
|
|
1450
|
-
await extensionPage.waitForFunction(EXTENSION_READY_EXPRESSION, { timeout: CONFIG.browser.initTimeout });
|
|
1516
|
+
extensionPage = await getExtensionPage(browser);
|
|
1451
1517
|
}
|
|
1452
|
-
catch (
|
|
1453
|
-
throw new
|
|
1518
|
+
catch (error) {
|
|
1519
|
+
throw new CaptureLaunchError("The capture extension page did not open.", { cause: error });
|
|
1520
|
+
}
|
|
1521
|
+
try {
|
|
1522
|
+
await extensionPage.waitForFunction(EXTENSION_READY_EXPRESSION, { timeout: initTimeout });
|
|
1523
|
+
}
|
|
1524
|
+
catch (error) {
|
|
1525
|
+
throw new CaptureLaunchError("The capture extension did not become ready within " + String(initTimeout) + " ms.", { cause: error });
|
|
1454
1526
|
}
|
|
1455
1527
|
LOG.debug("timing:browser", "Extension initialized. (+%sms)", browserElapsed());
|
|
1456
1528
|
/* The tab the window rests on. It is created once and selected by construction because nothing asks otherwise, so whatever the window shows when nothing has
|
|
@@ -1578,8 +1650,9 @@ export async function getBrowserPages() {
|
|
|
1578
1650
|
*
|
|
1579
1651
|
* Chrome termination uses Puppeteer's ChildProcess handle and its `exit` event for detection:
|
|
1580
1652
|
*
|
|
1581
|
-
* - browser.close()
|
|
1582
|
-
*
|
|
1653
|
+
* - browser.close() first runs puppeteer-stream's pass that closes every page but the extension's capture page and queries the extension's tabs, then sends CDP
|
|
1654
|
+
* Browser.close and waits for Chrome's process to exit, with no bound of its own on that wait.
|
|
1655
|
+
* - browser.disconnect() drops the DevTools connection instantly but orphans Chrome as a Node child process, creating a zombie that process.kill(pid, 0) cannot detect.
|
|
1583
1656
|
* - Synchronous polling (Atomics.wait) blocks the event loop, preventing Node from processing SIGCHLD to reap the child - Chrome becomes a zombie regardless
|
|
1584
1657
|
* of how SIGTERM was sent.
|
|
1585
1658
|
*
|
|
@@ -1605,25 +1678,27 @@ async function closeBrowserInstance(browser) {
|
|
|
1605
1678
|
// Wait for Chrome to exit after SIGTERM, with a bound. If Chrome doesn't exit in time, escalate to SIGKILL.
|
|
1606
1679
|
const exitedAfterTerm = await boundedWait(exitPromise, TERM_WAIT_MS);
|
|
1607
1680
|
if (!exitedAfterTerm) {
|
|
1608
|
-
// SIGTERM didn't work within the bound. Escalate to SIGKILL.
|
|
1609
|
-
// respond to SIGTERM.
|
|
1681
|
+
// SIGTERM didn't work within the bound. Escalate to SIGKILL. A wedged or hung Chrome can fail to exit on SIGTERM within the bound.
|
|
1610
1682
|
LOG.debug("browser:lifecycle", "Chrome did not exit after SIGTERM. Escalating to SIGKILL.");
|
|
1611
1683
|
chromeProcess.kill("SIGKILL");
|
|
1612
1684
|
// The same exit promise serves the second wait: if it already resolved, this returns its value immediately.
|
|
1613
1685
|
await boundedWait(exitPromise, KILL_WAIT_MS);
|
|
1614
1686
|
}
|
|
1615
1687
|
}
|
|
1616
|
-
// Disconnect the
|
|
1617
|
-
//
|
|
1688
|
+
// Disconnect Puppeteer's DevTools connection (the CDP pipe) after Chrome has exited. This cleans up Puppeteer's internal state (event listeners, pending CDP
|
|
1689
|
+
// calls) without waiting for an orderly close on a dead connection. We catch the rejection: disconnect() on a connection whose underlying transport already died of
|
|
1618
1690
|
// 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.
|
|
1619
1691
|
if (browser.connected) {
|
|
1620
1692
|
browser.disconnect().catch((error) => {
|
|
1621
1693
|
LOG.debug("browser:lifecycle", "Ignoring browser disconnect error during teardown: %s.", formatError(error));
|
|
1622
1694
|
});
|
|
1623
1695
|
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1696
|
+
/* The lock files go through the holder test on a scan taken here, once this process's Chrome has exited, because only a scan after the exit can see a Chrome
|
|
1697
|
+
* another instance launched on a shared profile in its place. This process's own Chrome never reads as that holder: a main still running after the bounded
|
|
1698
|
+
* waits is ours by the ownership test, and its helpers are not roots. A helper still shutting down after its main exited has been adopted by a live process
|
|
1699
|
+
* and reads as a holder, so that teardown keeps the files, the adopted-orphan limit the sweep shares, and the next startup sweep with no Chrome alive removes them.
|
|
1700
|
+
*/
|
|
1701
|
+
cleanStaleProfileFiles(listProcesses(), getChromeDataDir(CONFIG));
|
|
1627
1702
|
}
|
|
1628
1703
|
/**
|
|
1629
1704
|
* Closes the browser and cleans up resources during full server shutdown. After this call the supervisor reports absent and any subsequent stream request launches
|
|
@@ -1659,8 +1734,8 @@ export async function closeBrowser() {
|
|
|
1659
1734
|
* 1. Only managed pages: We only consider pages that PrismCast created (tracked in managedPageIds). Pages opened manually by the user for debugging, or pages opened
|
|
1660
1735
|
* by streaming sites (OAuth popups, etc.) are left alone.
|
|
1661
1736
|
*
|
|
1662
|
-
* 2.
|
|
1663
|
-
*
|
|
1737
|
+
* 2. Managed page IDs: Each managed page is assigned a string ID ("page-" plus a counter) by registerManagedPage and read back through the Page reference by
|
|
1738
|
+
* getManagedPageId. The staleness map, the in-flight set and the decision core all work on these stable string keys rather than on Page objects.
|
|
1664
1739
|
*
|
|
1665
1740
|
* 3. Grace period: Pages must be observed as potentially stale for a configurable grace period before being closed. This handles race conditions where pages are
|
|
1666
1741
|
* briefly untracked during stream initialization or cleanup.
|
|
@@ -1756,32 +1831,62 @@ export async function cleanupStalePages(now) {
|
|
|
1756
1831
|
}
|
|
1757
1832
|
}
|
|
1758
1833
|
catch (error) {
|
|
1759
|
-
// Cleanup failure is not critical - log
|
|
1834
|
+
// Cleanup failure is not critical - log it at debug level and let the next interval retry.
|
|
1760
1835
|
LOG.debug("browser:lifecycle", "Stale page cleanup failed: %s.", formatError(error));
|
|
1761
1836
|
}
|
|
1762
1837
|
}
|
|
1838
|
+
/**
|
|
1839
|
+
* Arms the stale-page sweep's interval on the owner's registry: cleanupStalePages at the owner's clock reading, every interval. Arming under the sweep's key
|
|
1840
|
+
* replaces an interval already armed there, so a re-arm leaves one sweep, on the new cadence from the moment it is armed.
|
|
1841
|
+
* @param sweep - The running stale-page owner.
|
|
1842
|
+
* @param interval - Milliseconds between sweeps.
|
|
1843
|
+
*/
|
|
1844
|
+
function armStalePageSweep(sweep, interval) {
|
|
1845
|
+
sweep.timers.setInterval(STALE_PAGE_SWEEP_KEY, () => { void cleanupStalePages(sweep.clock.now()); }, interval);
|
|
1846
|
+
}
|
|
1763
1847
|
/**
|
|
1764
1848
|
* Starts the periodic stale page cleanup. This should be called once during server startup, after the browser is initialized. The sweep runs indefinitely until
|
|
1765
|
-
* stopStalePageCleanup() is called (typically during graceful shutdown)
|
|
1849
|
+
* stopStalePageCleanup() is called (typically during graceful shutdown), and a saved interval re-arms it through applyStalePageCleanupChanges(). A second start
|
|
1850
|
+
* while one is running changes nothing.
|
|
1766
1851
|
* @param clock - The clock the sweep's interval arms on and whose reading each sweep judges staleness against, so the cadence and the judgment share one time
|
|
1767
1852
|
* source. Defaults to the system clock.
|
|
1768
1853
|
*/
|
|
1769
1854
|
export function startStalePageCleanup(clock = systemClock) {
|
|
1770
|
-
if (
|
|
1855
|
+
if (stalePageSweep) {
|
|
1771
1856
|
return;
|
|
1772
1857
|
}
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
1858
|
+
const sweep = { clock, timers: new TimerRegistry({ clock }) };
|
|
1859
|
+
armStalePageSweep(sweep, CONFIG.recovery.stalePageCleanupInterval);
|
|
1860
|
+
stalePageSweep = sweep;
|
|
1776
1861
|
}
|
|
1777
1862
|
/**
|
|
1778
1863
|
* Stops the periodic stale page cleanup. This should be called during graceful shutdown to prevent the sweep from running after we've started shutting down the
|
|
1779
1864
|
* browser and streams. Disposing the registry drains the interval, and a stop with nothing running is a no-op.
|
|
1780
1865
|
*/
|
|
1781
1866
|
export function stopStalePageCleanup() {
|
|
1782
|
-
|
|
1783
|
-
|
|
1867
|
+
stalePageSweep?.timers.dispose();
|
|
1868
|
+
stalePageSweep = null;
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Re-arms the running stale-page sweep at a saved cleanup interval. The re-arm restarts the cadence from the save, so the next sweep runs one full interval after
|
|
1872
|
+
* it, and the grace period needs nothing here, because each sweep already reads it when it runs. The handler reads the running sweep and never creates one, so a
|
|
1873
|
+
* save before the boot's start or after the stop arms nothing, and a save can never resurrect a stopped sweep. The interval comes from the candidate, because
|
|
1874
|
+
* the reconcile commits CONFIG only after its handlers run. Re-arming cannot fail, so the handler refuses nothing.
|
|
1875
|
+
* @param _changes - The change to the cleanup interval; the candidate carries the interval, so the handler reads that instead.
|
|
1876
|
+
* @param next - The candidate running configuration.
|
|
1877
|
+
* @returns No rejections.
|
|
1878
|
+
*/
|
|
1879
|
+
export async function applyStalePageCleanupChanges(_changes, next) {
|
|
1880
|
+
if (stalePageSweep) {
|
|
1881
|
+
const interval = next.recovery.stalePageCleanupInterval;
|
|
1882
|
+
armStalePageSweep(stalePageSweep, interval);
|
|
1883
|
+
LOG.debug("browser:lifecycle", "Stale page cleanup re-armed at %d ms.", interval);
|
|
1884
|
+
}
|
|
1885
|
+
return [];
|
|
1784
1886
|
}
|
|
1887
|
+
// Module-load side effect: register the handler once per process, as every config-change handler registers, so it is in place before the first save can reach
|
|
1888
|
+
// the reconcile.
|
|
1889
|
+
registerConfigChangeHandler("recovery.stalePageCleanupInterval", applyStalePageCleanupChanges);
|
|
1785
1890
|
/**
|
|
1786
1891
|
* Decides whether the browser may be torn down for the given cause, from facts read at the call. Each cause asks a different question of the same registry, so
|
|
1787
1892
|
* the decision is stated once here rather than spelled out at each guard.
|
|
@@ -1814,6 +1919,23 @@ export function isBrowserIdleForRestart(cause, facts) {
|
|
|
1814
1919
|
function readRestartFacts() {
|
|
1815
1920
|
return { establishedStreams: hasEstablishedStreams(), inFlightPages: inFlightPageIds.size, streamCount: getStreamCount() };
|
|
1816
1921
|
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Decides what a restart check's tick does about the maintenance quiet period. A browser younger than BROWSER_MAX_AGE needs no maintenance, so the tick leaves
|
|
1924
|
+
* the countdown as it stands. An old browser with streams registered waits for them: a countdown already running is cancelled, so a stream that starts during
|
|
1925
|
+
* the quiet period resets it, and with no countdown running there is nothing to do. An old browser with an empty registry starts the countdown, unless one is
|
|
1926
|
+
* already running.
|
|
1927
|
+
* @param facts - The browser's age, the registry's size and the countdown's state, read at the tick.
|
|
1928
|
+
* @returns The action the tick takes.
|
|
1929
|
+
*/
|
|
1930
|
+
export function decideRestartQuietPeriod(facts) {
|
|
1931
|
+
if (facts.age < BROWSER_MAX_AGE) {
|
|
1932
|
+
return "skip";
|
|
1933
|
+
}
|
|
1934
|
+
if (facts.streamCount > 0) {
|
|
1935
|
+
return facts.quietPending ? "cancel" : "skip";
|
|
1936
|
+
}
|
|
1937
|
+
return facts.quietPending ? "skip" : "arm";
|
|
1938
|
+
}
|
|
1817
1939
|
/**
|
|
1818
1940
|
* Relaunches a browser that can no longer start captures, as soon as nothing depends on it. Every trigger routes here - the mark itself, each stream termination,
|
|
1819
1941
|
* and the periodic restart check as the backstop for a moment when the other two could not act (login mode above all) - so the decision lives in one place rather
|
|
@@ -1839,18 +1961,17 @@ export async function restartBrowserIfImpairedAndIdle() {
|
|
|
1839
1961
|
/**
|
|
1840
1962
|
* Checks whether the browser qualifies for a restart. Called periodically by the restart check interval. The check skips when any of these conditions hold:
|
|
1841
1963
|
* graceful shutdown in progress, login mode active, browser not ready. A marked browser is handed to the impairment path and the tick ends there. Otherwise the
|
|
1842
|
-
* tick drives the supervisor's health-gated governor reset and
|
|
1843
|
-
* active streams exist (streams started during the quiet period reset the countdown), and otherwise start a quiet timer if one is not already running.
|
|
1964
|
+
* tick drives the supervisor's health-gated governor reset and carries out what decideRestartQuietPeriod decides about the maintenance quiet period.
|
|
1844
1965
|
* @param timers - The restart owner's registry, whose quiet period this check arms and reads. Handed in by the interval closure, so the check never reads the
|
|
1845
1966
|
* nullable module binding and needs no null branch of its own.
|
|
1967
|
+
* @param clock - The clock the check was started with, which the browser's age is measured on.
|
|
1846
1968
|
*/
|
|
1847
|
-
function checkBrowserRestart(timers) {
|
|
1969
|
+
function checkBrowserRestart(timers, clock) {
|
|
1848
1970
|
// Skip if the server is shutting down or login mode is active.
|
|
1849
1971
|
if (gracefulShutdownInProgress || isLoginModeActive()) {
|
|
1850
1972
|
return;
|
|
1851
1973
|
}
|
|
1852
|
-
// 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."
|
|
1853
|
-
// launch time is read from the supervisor's clock (systemClock.now), so age is measured against that same clock rather than against any other time source.
|
|
1974
|
+
// 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."
|
|
1854
1975
|
const browser = supervisor.current();
|
|
1855
1976
|
const launchTime = supervisor.currentLaunchTime();
|
|
1856
1977
|
if (!browser?.connected || (launchTime === null)) {
|
|
@@ -1870,31 +1991,32 @@ function checkBrowserRestart(timers) {
|
|
|
1870
1991
|
if (supervisor.noteSustainedHealth()) {
|
|
1871
1992
|
LOG.info("Browser capture readiness has been sustained; the relaunch governor has reset to its normal state.");
|
|
1872
1993
|
}
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1994
|
+
/* The age is measured on the clock this check was started with. The supervisor stamps the launch time on the system clock, which is the clock production
|
|
1995
|
+
* starts the check with, so in production the launch time and the reading it is subtracted from come from the same source.
|
|
1996
|
+
*/
|
|
1997
|
+
const action = decideRestartQuietPeriod({ age: clock.now() - launchTime, quietPending: timers.has(RESTART_QUIET_KEY), streamCount: getStreamCount() });
|
|
1998
|
+
switch (action) {
|
|
1999
|
+
case "arm": {
|
|
2000
|
+
LOG.debug("browser:lifecycle", "Browser uptime exceeds threshold. Quiet period started - restart will proceed if no streams start within %s minutes.", Math.round(BROWSER_RESTART_QUIET_PERIOD / 60000));
|
|
2001
|
+
timers.setTimeout(RESTART_QUIET_KEY, () => {
|
|
2002
|
+
void executeBrowserRestart("maintenance");
|
|
2003
|
+
}, BROWSER_RESTART_QUIET_PERIOD);
|
|
2004
|
+
break;
|
|
2005
|
+
}
|
|
2006
|
+
case "cancel": {
|
|
1881
2007
|
LOG.debug("browser:lifecycle", "Browser restart quiet period cancelled - streams are active.");
|
|
2008
|
+
cancelRestartQuietTimer();
|
|
2009
|
+
break;
|
|
2010
|
+
}
|
|
2011
|
+
case "skip": {
|
|
2012
|
+
break;
|
|
1882
2013
|
}
|
|
1883
|
-
cancelRestartQuietTimer();
|
|
1884
|
-
return;
|
|
1885
|
-
}
|
|
1886
|
-
// No active streams and the browser is old enough. Start the quiet timer if one is not already running.
|
|
1887
|
-
if (!timers.has(RESTART_QUIET_KEY)) {
|
|
1888
|
-
LOG.debug("browser:lifecycle", "Browser uptime exceeds threshold. Quiet period started - restart will proceed if no streams start within %s minutes.", Math.round(BROWSER_RESTART_QUIET_PERIOD / 60000));
|
|
1889
|
-
timers.setTimeout(RESTART_QUIET_KEY, () => {
|
|
1890
|
-
void executeBrowserRestart("maintenance");
|
|
1891
|
-
}, BROWSER_RESTART_QUIET_PERIOD);
|
|
1892
2014
|
}
|
|
1893
2015
|
}
|
|
1894
2016
|
/**
|
|
1895
|
-
* Executes a browser restart: a final guard check, then the current instance is retired and torn down and a fresh one is launched in its place.
|
|
1896
|
-
*
|
|
1897
|
-
* @param cause - Why the restart is running, for the announcement.
|
|
2017
|
+
* Executes a browser restart: a final guard check, then the current instance is retired and torn down and a fresh one is launched in its place. The guard's
|
|
2018
|
+
* idleness test and the log line that announces the restart both depend on the cause; the teardown and relaunch sequence is shared by every cause.
|
|
2019
|
+
* @param cause - Why the restart is running, for the guard's idleness test and the announcement.
|
|
1898
2020
|
*/
|
|
1899
2021
|
async function executeBrowserRestart(cause) {
|
|
1900
2022
|
// Retire any pending quiet period. For a maintenance restart this is the timer that just fired, and clearing a fired handle costs nothing. For an impairment
|
|
@@ -1960,14 +2082,14 @@ async function executeBrowserRestart(cause) {
|
|
|
1960
2082
|
/**
|
|
1961
2083
|
* Starts the periodic browser restart eligibility check. This should be called once during server startup, after the browser is initialized. The check runs
|
|
1962
2084
|
* indefinitely until stopBrowserRestartChecking() is called (typically during graceful shutdown). A second start while one is running changes nothing.
|
|
1963
|
-
* @param clock - The clock the check's interval and the quiet period it arms both run on. Defaults to the system clock.
|
|
2085
|
+
* @param clock - The clock the check's interval and the quiet period it arms both run on, and the browser's age is measured on. Defaults to the system clock.
|
|
1964
2086
|
*/
|
|
1965
2087
|
export function startBrowserRestartChecking(clock = systemClock) {
|
|
1966
2088
|
if (restartTimers) {
|
|
1967
2089
|
return;
|
|
1968
2090
|
}
|
|
1969
2091
|
const timers = new TimerRegistry({ clock });
|
|
1970
|
-
timers.setInterval(RESTART_CHECK_KEY, () => { checkBrowserRestart(timers); }, BROWSER_RESTART_CHECK_INTERVAL);
|
|
2092
|
+
timers.setInterval(RESTART_CHECK_KEY, () => { checkBrowserRestart(timers, clock); }, BROWSER_RESTART_CHECK_INTERVAL);
|
|
1971
2093
|
restartTimers = timers;
|
|
1972
2094
|
}
|
|
1973
2095
|
/**
|
|
@@ -1978,61 +2100,6 @@ export function stopBrowserRestartChecking() {
|
|
|
1978
2100
|
restartTimers?.dispose();
|
|
1979
2101
|
restartTimers = null;
|
|
1980
2102
|
}
|
|
1981
|
-
/* When running as a packaged executable (created by the `pkg` tool), the application is bundled into a single binary. Node modules like puppeteer-stream are
|
|
1982
|
-
* included in the bundle, but Chrome cannot load extensions from within the packaged binary - it needs actual files on the filesystem.
|
|
1983
|
-
*
|
|
1984
|
-
* To solve this, we extract the puppeteer-stream extension files to the application's data directory during startup. This happens only when process.pkg is
|
|
1985
|
-
* defined (indicating we're running as a packaged executable).
|
|
1986
|
-
*
|
|
1987
|
-
* The extracted files are:
|
|
1988
|
-
* - background.js: The extension's service worker that handles media capture
|
|
1989
|
-
* - manifest.json: The extension manifest declaring permissions and capabilities
|
|
1990
|
-
* - options.html/options.js: Extension options page (not used by our automation, but required by the manifest)
|
|
1991
|
-
*/
|
|
1992
|
-
/**
|
|
1993
|
-
* Extracts the Puppeteer Stream extension files when running as a packaged executable. This copies the extension files from within the packaged binary to the
|
|
1994
|
-
* filesystem where Chrome can load them.
|
|
1995
|
-
*
|
|
1996
|
-
* When running from source (not packaged), this function does nothing - puppeteer-stream can load the extension directly from node_modules.
|
|
1997
|
-
* @throws If extension extraction fails.
|
|
1998
|
-
*/
|
|
1999
|
-
export async function prepareExtension() {
|
|
2000
|
-
// Only needed when running as a packaged executable.
|
|
2001
|
-
if (!process.pkg) {
|
|
2002
|
-
return;
|
|
2003
|
-
}
|
|
2004
|
-
try {
|
|
2005
|
-
// The extension files are extracted to the extension directory within the data directory (ensured to exist before this function is called).
|
|
2006
|
-
const out = getExtensionDir(CONFIG);
|
|
2007
|
-
// Create the extension directory if it doesn't exist.
|
|
2008
|
-
try {
|
|
2009
|
-
await fsPromises.mkdir(out, { recursive: true });
|
|
2010
|
-
}
|
|
2011
|
-
catch (error) {
|
|
2012
|
-
LOG.error("Failed to create extension directory: %s.", formatError(error));
|
|
2013
|
-
throw error;
|
|
2014
|
-
}
|
|
2015
|
-
// The extension files that need to be extracted. These are the files from puppeteer-stream's extension directory.
|
|
2016
|
-
const files = ["background.js", "manifest.json", "options.html", "options.js"];
|
|
2017
|
-
for (const file of files) {
|
|
2018
|
-
try {
|
|
2019
|
-
// Copy each file from the packaged location (relative to the executable) to the data directory. The source path assumes the executable is in the
|
|
2020
|
-
// same directory as node_modules (which is how pkg packages the application).
|
|
2021
|
-
// eslint-disable-next-line no-await-in-loop
|
|
2022
|
-
await fsPromises.copyFile(path.join(path.dirname(process.execPath), "node_modules", "puppeteer-stream", "extension", file), path.join(out, file));
|
|
2023
|
-
}
|
|
2024
|
-
catch (error) {
|
|
2025
|
-
LOG.error("Failed to copy extension file %s: %s.", file, formatError(error));
|
|
2026
|
-
throw error;
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
LOG.debug("browser:lifecycle", "Extension files prepared successfully.");
|
|
2030
|
-
}
|
|
2031
|
-
catch (error) {
|
|
2032
|
-
LOG.error("Extension preparation failed: %s.", formatError(error));
|
|
2033
|
-
throw error;
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
2103
|
/* Re-export the capture acquisition for the streaming module. The browser directory owns every conversation with puppeteer-stream - index.ts launches it and
|
|
2037
2104
|
* tabCapture.ts speaks its extension's protocol - so no other layer imports the library, and the streaming layer asks this module for a capture the same way it
|
|
2038
2105
|
* asks it for a browser.
|