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/app.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
*
|
|
3
3
|
* app.ts: Express application builder for PrismCast.
|
|
4
4
|
*/
|
|
5
|
-
import { CONFIG, displayConfiguration, initializeConfiguration,
|
|
6
|
-
import { LOG, boundedWait, claim, createMorganStream, formatError, formatTimestamp, getCurrentPattern, getPackageVersion, isDebugLogging, release, resolveFFmpegPath, setConsoleLogging, startUpdateChecking, stopUpdateChecking } from "./utils/index.js";
|
|
7
|
-
import { closeBrowser, ensureDataDirectory, getCurrentBrowser,
|
|
5
|
+
import { CONFIG, displayConfiguration, initializeConfiguration, validateConfiguration } from "./config/index.js";
|
|
6
|
+
import { LOG, assertNever, boundedWait, claim, createMorganStream, formatError, formatTimestamp, getCurrentPattern, getPackageVersion, handleRequestError, isDebugLogging, release, resolveFFmpegPath, setConsoleLogging, startUpdateChecking, stopUpdateChecking } from "./utils/index.js";
|
|
7
|
+
import { closeBrowser, ensureDataDirectory, getCurrentBrowser, isGracefulShutdown, killStaleChrome, setGracefulShutdown, setLoginModeEndObserver, startBrowserRestartChecking, startStalePageCleanup, stopBrowserRestartChecking, stopStalePageCleanup, syncWindowVisibility } from "./browser/index.js";
|
|
8
8
|
import { ensureAllMigrated, snapshotAllForRelease } from "./config/persistence.js";
|
|
9
9
|
import { flushHealthStateNow, loadHealthState } from "./config/health.js";
|
|
10
10
|
import { getAllStreams, isCaptureIdentity } from "./streaming/registry.js";
|
|
11
11
|
import { getDebugEnv, getLogFilePath, getServerPidFilePath } from "./config/paths.js";
|
|
12
|
-
import { initializeFileLogger, shutdownFileLogger } from "./utils/fileLogger.js";
|
|
12
|
+
import { initializeFileLogger, setMaxLogSize, shutdownFileLogger } from "./utils/fileLogger.js";
|
|
13
13
|
import { loadResumeState, saveResumeState } from "./streaming/hlsResume.js";
|
|
14
14
|
import { revalidateDomainAuth, stopPrecaching } from "./browser/precaching.js";
|
|
15
15
|
import { startHdhrServer, stopHdhrServer } from "./hdhr/index.js";
|
|
@@ -25,6 +25,7 @@ import { initializeUserProfiles } from "./config/userProfiles.js";
|
|
|
25
25
|
import { installHealthBridge } from "./routes/config/channels/healthBridge.js";
|
|
26
26
|
import { loadProviderLineups } from "./config/providerLineups.js";
|
|
27
27
|
import morgan from "morgan";
|
|
28
|
+
import { registerConfigChangeHandler } from "./config/reactivity.js";
|
|
28
29
|
import { runConsistencyProbeAtStartup } from "./config/consistencyProbe.js";
|
|
29
30
|
import { setupRoutes } from "./routes/index.js";
|
|
30
31
|
import { systemClock } from "homebridge-plugin-utils";
|
|
@@ -43,8 +44,8 @@ let server = null;
|
|
|
43
44
|
// shutdown, exactly as the file logger's drain bound does for a wedged filesystem operation.
|
|
44
45
|
const SERVER_CLOSE_BOUND_MS = 5000;
|
|
45
46
|
/* The background-service teardown stack. Each long-lived background service (stale-page sweep, browser-restart watchdog, idle cleanup, show-info/pretune polling,
|
|
46
|
-
* update check) registers its stop here at the moment it starts in
|
|
47
|
-
* started without also being torn down. An AsyncDisposableStack is used (rather than the synchronous DisposableStack) so the teardown awaits each registered stop -
|
|
47
|
+
* update check) registers its stop here at the moment it starts in startBackgroundServices(); graceful shutdown disposes the stack wholesale, so a future service cannot
|
|
48
|
+
* be started without also being torn down. An AsyncDisposableStack is used (rather than the synchronous DisposableStack) so the teardown awaits each registered stop -
|
|
48
49
|
* today's registered stops are all synchronous, but a future service whose stop must complete (e.g. flushing to disk) would be silently fire-and-forgotten by a
|
|
49
50
|
* sync stack.
|
|
50
51
|
*/
|
|
@@ -72,10 +73,45 @@ function stopIdleCleanup() {
|
|
|
72
73
|
idleCleanupInterval = null;
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Closes the main HTTP server, graceful shutdown's step for the listener the boot's tail bound. Connections are destroyed first because the status and log SSE
|
|
78
|
+
* streams (routes/streams.ts and routes/logs.ts) hold their sockets open with a heartbeat, and the close waits on every one of them; the CDP proxy's WebSocket
|
|
79
|
+
* clients have already closed themselves with 1001 in response to the browser disconnect shutdown awaits before this step. An in-flight request ends with the
|
|
80
|
+
* process, because the exit follows immediately. The server reference is taken and cleared before the close, so a close that runs again finds no server.
|
|
81
|
+
* @param clock - The clock the close bound runs on, the system clock in production and a test clock in a row that crosses the bound.
|
|
82
|
+
* @returns A promise that resolves once the server closes or its bound lapses, and at once when no server is bound.
|
|
76
83
|
*/
|
|
84
|
+
export async function closeMainServer(clock = systemClock) {
|
|
85
|
+
const current = server;
|
|
86
|
+
server = null;
|
|
87
|
+
if (!current) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// The outer catch keeps a close that throws from rejecting, because shutdown runs unawaited from its signal handler and a rejection here would skip the exit
|
|
91
|
+
// the shutdown ends with.
|
|
92
|
+
try {
|
|
93
|
+
const { promise: closed, resolve } = Promise.withResolvers();
|
|
94
|
+
current.close((error) => {
|
|
95
|
+
if (error) {
|
|
96
|
+
LOG.error("The HTTP server reported an error while closing during shutdown.", { error: formatError(error) });
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
LOG.info("HTTP server closed successfully.");
|
|
100
|
+
}
|
|
101
|
+
resolve(true);
|
|
102
|
+
});
|
|
103
|
+
current.closeAllConnections();
|
|
104
|
+
if ((await boundedWait(closed, SERVER_CLOSE_BOUND_MS, { clock })) === null) {
|
|
105
|
+
LOG.warn("The HTTP server did not close within its bound, so the shutdown continues.", { boundMs: SERVER_CLOSE_BOUND_MS });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
LOG.error("The HTTP server could not be closed during shutdown.", { error: formatError(error) });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
77
112
|
/**
|
|
78
|
-
* Sets up signal handlers for graceful shutdown. When SIGINT or SIGTERM is received, we close all streams, the browser
|
|
113
|
+
* Sets up signal handlers for graceful shutdown. When SIGHUP, SIGINT or SIGTERM is received, we close all active streams, the browser and the HTTP server before
|
|
114
|
+
* exiting, so every resource is released cleanly.
|
|
79
115
|
*/
|
|
80
116
|
function setupGracefulShutdown() {
|
|
81
117
|
let shutdownInProgress = false;
|
|
@@ -89,12 +125,14 @@ function setupGracefulShutdown() {
|
|
|
89
125
|
// Set the graceful shutdown flag early so that page close errors are suppressed during stream termination.
|
|
90
126
|
setGracefulShutdown(true);
|
|
91
127
|
// Tear down the background services. HDHR is torn down first and awaited so its HTTP and UDP sockets fully release before the rest of shutdown proceeds. The
|
|
92
|
-
// long-lived pollers are disposed wholesale via the AsyncDisposableStack they registered on at startup. The
|
|
93
|
-
// separately - its graceful-shutdown guard has already neutralized it via
|
|
128
|
+
// long-lived pollers are disposed wholesale via the AsyncDisposableStack they registered on at startup. The precache scheduler, whose cycles browser launches
|
|
129
|
+
// and saves request rather than the boot starting them, is cancelled separately - its graceful-shutdown guard has already neutralized it via
|
|
130
|
+
// setGracefulShutdown above, and this clears its pending timers.
|
|
94
131
|
await stopHdhrServer();
|
|
95
132
|
await backgroundServices?.disposeAsync();
|
|
96
133
|
stopPrecaching();
|
|
97
|
-
// Terminate all streams. terminateStream()
|
|
134
|
+
// Terminate all streams. terminateStream() runs every per-stream cleanup except the page close, which it skips during graceful shutdown because
|
|
135
|
+
// closeBrowser() below closes every page.
|
|
98
136
|
const streams = getAllStreams();
|
|
99
137
|
// Collect resume state from active streams before termination destroys the segmenters. Each entry captures sequence numbers and timestamps so the next startup
|
|
100
138
|
// can seed from them, preventing HLS sequence resets that confuse Channels DVR. The segment index is decremented by one so the resumed playlist still includes
|
|
@@ -106,6 +144,7 @@ function setupGracefulShutdown() {
|
|
|
106
144
|
if (stream.info.storeKey && segmenter) {
|
|
107
145
|
resumeEntries.push({
|
|
108
146
|
channelName: stream.info.storeKey,
|
|
147
|
+
discontinuityCount: segmenter.getDiscontinuityCount(),
|
|
109
148
|
initSegment: segmenter.getInitSegment(),
|
|
110
149
|
initVersion: segmenter.getInitVersion(),
|
|
111
150
|
segmentIndex: Math.max(0, segmenter.getSegmentIndex() - 1),
|
|
@@ -123,32 +162,8 @@ function setupGracefulShutdown() {
|
|
|
123
162
|
await closeBrowser();
|
|
124
163
|
// Release the server instance slot so the next startup does not see a stale identity file.
|
|
125
164
|
releaseInstanceSlot();
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
* waits on every one of them; the CDP proxy's WebSocket clients have already closed themselves with 1001 in response to the browser disconnect above. An
|
|
129
|
-
* in-flight request ends with the process either way, because the exit follows immediately.
|
|
130
|
-
*/
|
|
131
|
-
try {
|
|
132
|
-
if (server) {
|
|
133
|
-
const { promise: closed, resolve } = Promise.withResolvers();
|
|
134
|
-
server.close((error) => {
|
|
135
|
-
if (error) {
|
|
136
|
-
LOG.error("Error closing server during shutdown: %s.", formatError(error));
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
LOG.info("HTTP server closed successfully.");
|
|
140
|
-
}
|
|
141
|
-
resolve(true);
|
|
142
|
-
});
|
|
143
|
-
server.closeAllConnections();
|
|
144
|
-
if ((await boundedWait(closed, SERVER_CLOSE_BOUND_MS)) === null) {
|
|
145
|
-
LOG.warn("The HTTP server did not close within %sms. Continuing the shutdown.", SERVER_CLOSE_BOUND_MS);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
LOG.error("Error closing server during shutdown: %s.", formatError(error));
|
|
151
|
-
}
|
|
165
|
+
// Close the HTTP server, awaited so its own line lands while the file logger is still open, which the step after it ends.
|
|
166
|
+
await closeMainServer();
|
|
152
167
|
// Shut down file logger if in use. Awaited because shutdown drains the outstanding write chain before its final synchronous flush, and the process exit below
|
|
153
168
|
// would otherwise cut that drain short.
|
|
154
169
|
if (!usingConsoleLogging) {
|
|
@@ -156,6 +171,11 @@ function setupGracefulShutdown() {
|
|
|
156
171
|
}
|
|
157
172
|
process.exit(0);
|
|
158
173
|
}
|
|
174
|
+
// A hangup asks the process to end as surely as an interrupt or a termination does, when the terminal or the session that started PrismCast goes away, and its
|
|
175
|
+
// default action would end the process without this teardown or the exit handler, so it runs the same shutdown.
|
|
176
|
+
process.on("SIGHUP", () => {
|
|
177
|
+
void shutdown();
|
|
178
|
+
});
|
|
159
179
|
process.on("SIGINT", () => {
|
|
160
180
|
void shutdown();
|
|
161
181
|
});
|
|
@@ -183,8 +203,8 @@ const FILTERED_SKIP_PATTERNS = ["/logs", "/health", "/favicon", "/logo.png", "/l
|
|
|
183
203
|
// Streaming and management endpoints, always logged in filtered mode because they mark what the server is actually doing.
|
|
184
204
|
const FILTERED_IMPORTANT_PATTERNS = ["/stream", "/streams", "/config", "/playlist", "/debug"];
|
|
185
205
|
/**
|
|
186
|
-
* Records a request's arrival time.
|
|
187
|
-
* than from whenever
|
|
206
|
+
* Records a request's arrival time. The request logger's middleware calls it as a request arrives, before handing the request to morgan, so every reader of the
|
|
207
|
+
* elapsed time measures from the request's arrival rather than from whenever morgan happened to see it.
|
|
188
208
|
* @param req - The incoming request.
|
|
189
209
|
* @param _res - The response, unused.
|
|
190
210
|
* @param next - The next middleware in the chain.
|
|
@@ -261,26 +281,74 @@ export function skipInFilteredMode(request) {
|
|
|
261
281
|
// Skip successful requests to the root landing page, and log everything else.
|
|
262
282
|
return request.url === "/";
|
|
263
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Decides whether the request log skips a request at the given level: "none" skips every request, "all" logs every one, and "errors" and "filtered" apply their
|
|
286
|
+
* own rules. The switch is exhaustive, and no running level reaches its default arm, because every configuration the server builds corrects a level it does not
|
|
287
|
+
* recognize.
|
|
288
|
+
* @param options - The level in force, the elapsed milliseconds (null when unstamped), whether a Retry-After header is present, the response status, and the
|
|
289
|
+
* request URL.
|
|
290
|
+
* @returns True when the request should not be logged.
|
|
291
|
+
*/
|
|
292
|
+
export function skipRequestLog(options) {
|
|
293
|
+
switch (options.level) {
|
|
294
|
+
case "all": {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
case "errors": {
|
|
298
|
+
return skipInErrorsMode(options);
|
|
299
|
+
}
|
|
300
|
+
case "filtered": {
|
|
301
|
+
return skipInFilteredMode(options);
|
|
302
|
+
}
|
|
303
|
+
case "none": {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
default: {
|
|
307
|
+
assertNever(options.level);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
264
311
|
/* The token is defined once for the process, at module load. morgan's token registry is global to the module, so registering inside the app builder would re-register
|
|
265
312
|
* on every build - harmless today because the definition is constant, but a registration whose count tracks how often the app is assembled is the kind of coupling
|
|
266
313
|
* that only shows up once a second build carries different state.
|
|
267
314
|
*/
|
|
268
315
|
morgan.token("elapsed", elapsedMillis);
|
|
269
|
-
|
|
270
|
-
|
|
316
|
+
// The request log line. Its elapsed token reads the arrival stamp stampRequestStart records.
|
|
317
|
+
const REQUEST_LOG_FORMAT = ":method :url from :remote-addr responded :status in :elapsed ms.";
|
|
318
|
+
/**
|
|
319
|
+
* Builds the request logger: one morgan logger behind one middleware, each reading the configured HTTP log level per request, so a saved level applies from the
|
|
320
|
+
* next request. Whether a request is observed is decided when it arrives: under "none" the middleware hands it on at once, at the cost of one property read, and
|
|
321
|
+
* the request is never stamped or seen by morgan. Whether an observed request is logged is decided by the level in force when it finishes, because morgan makes
|
|
322
|
+
* its skip decision as the response completes. So a long-lived stream that arrived under "none" is never logged, and one that arrived under another level is
|
|
323
|
+
* judged by the level in force at its close.
|
|
324
|
+
* @param stream - The stream morgan writes each line to.
|
|
325
|
+
* @returns The request-logging middleware.
|
|
271
326
|
*/
|
|
327
|
+
export function createRequestLogger(stream) {
|
|
328
|
+
const logger = morgan(REQUEST_LOG_FORMAT, {
|
|
329
|
+
skip: (req, res) => skipRequestLog({
|
|
330
|
+
elapsedMs: elapsedMillisValue(req),
|
|
331
|
+
hasRetryAfter: res.getHeader("Retry-After") !== undefined,
|
|
332
|
+
level: CONFIG.logging.httpLogLevel,
|
|
333
|
+
statusCode: res.statusCode,
|
|
334
|
+
url: req.originalUrl || req.url
|
|
335
|
+
}),
|
|
336
|
+
stream
|
|
337
|
+
});
|
|
338
|
+
return (req, res, next) => {
|
|
339
|
+
if (CONFIG.logging.httpLogLevel === "none") {
|
|
340
|
+
next();
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
stampRequestStart(req, res, () => { logger(req, res, next); });
|
|
344
|
+
};
|
|
345
|
+
}
|
|
272
346
|
/**
|
|
273
|
-
* Creates and configures the Express application with all middleware and routes.
|
|
347
|
+
* Creates and configures the Express application with all middleware and routes. It is a step of its own, so the boot's tail takes it as an injected step and a
|
|
348
|
+
* test substitutes a stub for it.
|
|
274
349
|
* @returns The configured Express application.
|
|
275
350
|
*/
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
await prepareExtension();
|
|
279
|
-
}
|
|
280
|
-
catch (error) {
|
|
281
|
-
LOG.error("Cannot build app without extension: %s.", formatError(error));
|
|
282
|
-
throw error;
|
|
283
|
-
}
|
|
351
|
+
function buildApp() {
|
|
284
352
|
const app = express();
|
|
285
353
|
// Trust proxy headers (X-Forwarded-Proto, X-Forwarded-Host) so that req.protocol and req.hostname reflect what the client actually used when accessing through
|
|
286
354
|
// a reverse proxy. This ensures playlist URLs match the client's connection.
|
|
@@ -298,55 +366,20 @@ async function buildApp() {
|
|
|
298
366
|
// Add body parsing middleware for form submissions (configuration page).
|
|
299
367
|
app.use(express.urlencoded({ extended: true }));
|
|
300
368
|
app.use(express.json());
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
const morganFormat = ":method :url from :remote-addr responded :status in :elapsed ms.";
|
|
305
|
-
const morganStream = createMorganStream();
|
|
306
|
-
// Stamp each request's arrival before morgan sees it, so the elapsed token and the filtered mode's slow-request rule read the same start time.
|
|
307
|
-
app.use(stampRequestStart);
|
|
308
|
-
if (CONFIG.logging.httpLogLevel === "errors") {
|
|
309
|
-
// Log requests with 4xx or 5xx status codes, but skip 404s for common browser asset requests.
|
|
310
|
-
app.use(morgan(morganFormat, {
|
|
311
|
-
skip: (req, res) => skipInErrorsMode({
|
|
312
|
-
hasRetryAfter: res.getHeader("Retry-After") !== undefined,
|
|
313
|
-
statusCode: res.statusCode,
|
|
314
|
-
url: req.originalUrl || req.url
|
|
315
|
-
}),
|
|
316
|
-
stream: morganStream
|
|
317
|
-
}));
|
|
318
|
-
}
|
|
319
|
-
else if (CONFIG.logging.httpLogLevel === "filtered") {
|
|
320
|
-
// Log important requests while skipping high-frequency polling endpoints. We always log errors, slow requests, and critical endpoints.
|
|
321
|
-
app.use(morgan(morganFormat, {
|
|
322
|
-
skip: (req, res) => skipInFilteredMode({
|
|
323
|
-
elapsedMs: elapsedMillisValue(req),
|
|
324
|
-
statusCode: res.statusCode,
|
|
325
|
-
url: req.originalUrl || req.url
|
|
326
|
-
}),
|
|
327
|
-
stream: morganStream
|
|
328
|
-
}));
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
// Log all requests.
|
|
332
|
-
app.use(morgan(morganFormat, { stream: morganStream }));
|
|
333
|
-
}
|
|
334
|
-
}
|
|
369
|
+
// Log HTTP requests at the configured level, which the logger reads per request. Its output goes through the morgan stream adapter, which formats timestamps
|
|
370
|
+
// the same way in console and file logging modes.
|
|
371
|
+
app.use(createRequestLogger(createMorganStream()));
|
|
335
372
|
// Set up all HTTP endpoints.
|
|
336
373
|
setupRoutes(app);
|
|
337
|
-
//
|
|
338
|
-
app.use(
|
|
339
|
-
LOG.error("Unhandled error in request: %s.", formatError(err));
|
|
340
|
-
if (!res.headersSent) {
|
|
341
|
-
res.status(500).send("Internal server error");
|
|
342
|
-
}
|
|
343
|
-
});
|
|
374
|
+
// The request error handler goes last, so it receives what every route above raises.
|
|
375
|
+
app.use(handleRequestError);
|
|
344
376
|
return app;
|
|
345
377
|
}
|
|
346
378
|
/* PrismCast instance guard. Only one server instance should run at a time - a second instance would launch a competing Chrome process, bind to the same port,
|
|
347
|
-
* and corrupt shared state. The guard delegates to the runtime-identity primitive in utils/runtimeIdentity.ts, which composes PID liveness
|
|
348
|
-
*
|
|
349
|
-
* transparently: the next claim() overwrites them. A held-live record (same boot, alive PID
|
|
379
|
+
* and corrupt shared state. The guard delegates to the runtime-identity primitive in utils/runtimeIdentity.ts, which composes PID liveness, the current boot
|
|
380
|
+
* session ID and the writer's command-line fingerprint to classify the on-disk identity file. Stale records from reboots, container restarts, crashes, and
|
|
381
|
+
* ungraceful shutdowns are recovered transparently: the next claim() overwrites them. A held-live record (same boot, alive PID, and that PID not proven by its
|
|
382
|
+
* command-line fingerprint to be another program) is the only state that rejects a startup.
|
|
350
383
|
*
|
|
351
384
|
* Ownership is verified structurally inside release(): the function reads the on-disk record and only removes the file when its PID matches this process's
|
|
352
385
|
* PID. The file content is itself the source of truth, so a rejected duplicate startup's exit handler cannot accidentally delete the legitimate holder's file.
|
|
@@ -417,12 +450,123 @@ async function listenMainServer(app) {
|
|
|
417
450
|
});
|
|
418
451
|
return promise;
|
|
419
452
|
}
|
|
420
|
-
|
|
421
|
-
*
|
|
453
|
+
/**
|
|
454
|
+
* Applies a saved log size limit to the open log file. The composition root owns the handler because it wires the file logger, and the file logger, a utility
|
|
455
|
+
* the configuration layer builds on, cannot import that layer back. The limit comes from the candidate, because the reconcile commits CONFIG only after its
|
|
456
|
+
* handlers run. The size check is started rather than awaited: the save needs only the limit, which the setter assigns before its first await, so a save never
|
|
457
|
+
* waits on log-file I/O.
|
|
458
|
+
* @param _changes - The change to the log size limit; the candidate carries the limit, so the handler reads that instead.
|
|
459
|
+
* @param next - The candidate running configuration.
|
|
460
|
+
* @returns No rejections.
|
|
422
461
|
*/
|
|
462
|
+
export async function applyLogSizeChanges(_changes, next) {
|
|
463
|
+
void setMaxLogSize(next.logging.maxSize);
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
466
|
+
// 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
|
|
467
|
+
// the reconcile.
|
|
468
|
+
registerConfigChangeHandler("logging.maxSize", applyLogSizeChanges);
|
|
469
|
+
// The boot's tail.
|
|
423
470
|
/**
|
|
424
|
-
*
|
|
425
|
-
*
|
|
471
|
+
* Starts the background services, each on the teardown stack graceful shutdown disposes. The boot's tail calls it through its injected steps, so a test records
|
|
472
|
+
* its place in the boot's order without starting a real service.
|
|
473
|
+
*/
|
|
474
|
+
function startBackgroundServices() {
|
|
475
|
+
// Start the background services and register each one's stop on an AsyncDisposableStack the moment it starts, so graceful shutdown can dispose them all wholesale
|
|
476
|
+
// and a future service cannot be started without also being torn down. These are order-independent background loops/timers, so LIFO disposal order is immaterial.
|
|
477
|
+
// HDHR is intentionally not a member (it is torn down first in shutdown so its sockets release before the rest), and the precache scheduler stays off the stack
|
|
478
|
+
// as well: browser launches and saves request its cycles, and shutdown stops it separately.
|
|
479
|
+
backgroundServices = new AsyncDisposableStack();
|
|
480
|
+
startStalePageCleanup();
|
|
481
|
+
backgroundServices.defer(stopStalePageCleanup);
|
|
482
|
+
startBrowserRestartChecking();
|
|
483
|
+
backgroundServices.defer(stopBrowserRestartChecking);
|
|
484
|
+
startIdleCleanup();
|
|
485
|
+
backgroundServices.defer(stopIdleCleanup);
|
|
486
|
+
startShowInfoPolling();
|
|
487
|
+
backgroundServices.defer(stopShowInfoPolling);
|
|
488
|
+
startPretunePolling();
|
|
489
|
+
backgroundServices.defer(stopPretunePolling);
|
|
490
|
+
startUpdateChecking(getPackageVersion());
|
|
491
|
+
backgroundServices.defer(stopUpdateChecking);
|
|
492
|
+
}
|
|
493
|
+
export const defaultBootServicesDeps = {
|
|
494
|
+
attachCdpUpgradeHandler,
|
|
495
|
+
buildApp,
|
|
496
|
+
isGracefulShutdown,
|
|
497
|
+
listenMainServer,
|
|
498
|
+
startBackgroundServices,
|
|
499
|
+
startHdhrServer
|
|
500
|
+
};
|
|
501
|
+
/**
|
|
502
|
+
* Reports whether shutdown has begun, logging the skip line when it has. It reads the graceful-shutdown state the browser module holds, which the signal handler
|
|
503
|
+
* sets before its first await and nothing in production clears, and keeps no flag of its own. Each caller returns when it answers true, so the line logs at most
|
|
504
|
+
* once per boot.
|
|
505
|
+
*
|
|
506
|
+
* Shutdown never waits on the boot, so the boot reaches a check only while shutdown is still awaiting its own steps, and where the signal lands decides what the boot has
|
|
507
|
+
* armed. A signal before setupGracefulShutdown(), the data directory creation included, meets no handler of ours, and the platform's default
|
|
508
|
+
* action ends the process with nothing armed. A signal during the release boot coordinator, the configuration load, the logger, the FFmpeg check,
|
|
509
|
+
* the stores or the consistency probe runs shutdown while the stack and the listener are absent, so its disposal and its close have nothing to
|
|
510
|
+
* act on, and with no browser to wait for, its exit ends the boot before the first check. A signal during the browser warm-up makes shutdown's
|
|
511
|
+
* closeBrowser() supersede the launch in flight: the launch rejects when it settles, the warm-up rethrows, and the entry's catch, finding shutdown begun, leaves the exit
|
|
512
|
+
* to shutdown, whose own exit usually comes first because the launch has seconds of work left, so the boot reaches no check whichever exit lands first. A signal during
|
|
513
|
+
* the preroll or the window sync finds a ready browser, so shutdown waits for Chrome's exit, within the browser module's BROWSER_TEARDOWN_DRAIN_BOUND_MS; the window sync
|
|
514
|
+
* returns at once when shutdown has begun, so a boot whose preroll finishes inside that wait reaches the first check, which stops it before the stack is armed, and a
|
|
515
|
+
* boot whose preroll outlasts the wait is ended by the exit. The build is synchronous and a bind on an address crosses no event-loop turn, so between arming the stack
|
|
516
|
+
* and starting HDHomeRun the one await a signal can land in is a hostname host's lookup during the bind: shutdown reads the listener only after its own awaits, so the
|
|
517
|
+
* bind completes and shutdown closes the listener, and the check after the bind stops the boot before HDHomeRun starts. A signal during the HDHomeRun start lets
|
|
518
|
+
* shutdown's stop run before the start finishes binding, and the sockets it binds last until the exit. A signal after the boot finished meets no check and shutdown
|
|
519
|
+
* tears everything down; a repeated signal is absorbed by the handler's own guard; and a process restart discards the state, so the next boot starts with it unset.
|
|
520
|
+
* @param deps - The boot's steps, whose shutdown reader the check calls.
|
|
521
|
+
* @returns True when shutdown has begun, so the caller arms nothing further.
|
|
522
|
+
*/
|
|
523
|
+
function shutdownBegan(deps) {
|
|
524
|
+
if (!deps.isGracefulShutdown()) {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
LOG.info("Shutdown began during startup, so the remaining startup steps are skipped.");
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Arms what the server runs for the life of the process, in order: the background services, the Express application and its listener, the CDP upgrade handler,
|
|
532
|
+
* and HDHomeRun emulation. The signal handlers are installed well before the boot reaches this point, so a signal can run shutdown against a background stack, a
|
|
533
|
+
* listener and an HDHomeRun surface that do not exist yet, and a boot that armed them afterwards would leave them running in a process that is shutting down. The
|
|
534
|
+
* boot therefore reads the shutdown state before it arms the stack, and again after the bind, the await a later arming step follows, and arms nothing once
|
|
535
|
+
* shutdown has begun. The check after the bind decides only where that await yields to the event loop - a hostname host's lookup - and elsewhere the first check
|
|
536
|
+
* decides. The limit is an await already in flight when the signal lands: it completes, and the process exit at the end of shutdown ends whatever it started.
|
|
537
|
+
* @param deps - The boot's steps: the real ones in production, and recording stubs in a test.
|
|
538
|
+
* @returns A promise that resolves once the boot's tail completes or stops for a shutdown, and rejects when the build or the bind fails.
|
|
539
|
+
*/
|
|
540
|
+
export async function startBootServices(deps = defaultBootServicesDeps) {
|
|
541
|
+
if (shutdownBegan(deps)) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
deps.startBackgroundServices();
|
|
545
|
+
// Build and start Express application.
|
|
546
|
+
try {
|
|
547
|
+
const app = deps.buildApp();
|
|
548
|
+
// Bind the main HTTP server, detecting bind success or failure through explicit events rather than the unreliable listen callback. A bind failure throws out
|
|
549
|
+
// of here so the catch below surfaces it and the process exits cleanly rather than lingering with no HTTP surface.
|
|
550
|
+
server = await deps.listenMainServer(app);
|
|
551
|
+
// Attach the CDP proxy upgrade handler once the underlying http.Server exists. The handler is gated on the `cdp` debug category at request time, so it sits
|
|
552
|
+
// dormant until the user enables CDP via /debug. We attach unconditionally so the toggle takes effect without requiring a restart.
|
|
553
|
+
deps.attachCdpUpgradeHandler(server);
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
// Covers both a buildApp failure and a listenMainServer bind failure; the latter already logged the precise, operator-actionable line before rejecting.
|
|
557
|
+
LOG.error("Failed to build or start the HTTP server: %s.", formatError(error));
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
if (shutdownBegan(deps)) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
// Start HDHomeRun emulation server if enabled. This runs independently of the main server and handles EADDRINUSE gracefully without affecting PrismCast's
|
|
564
|
+
// primary functionality.
|
|
565
|
+
await deps.startHdhrServer();
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Initializes the server and starts it: we validate configuration, clean up stale Chrome processes and warm up the browser instance, then hand the rest of the
|
|
569
|
+
* boot to startBootServices, which arms the background services, builds the Express application and binds its listener, and starts HDHomeRun emulation.
|
|
426
570
|
* @param parsedArgs - Parsed command-line arguments containing flags and override values.
|
|
427
571
|
*/
|
|
428
572
|
export async function startServer(parsedArgs) {
|
|
@@ -456,6 +600,18 @@ export async function startServer(parsedArgs) {
|
|
|
456
600
|
cliOverrides[configPath] = parsedArgs[argKey];
|
|
457
601
|
}
|
|
458
602
|
}
|
|
603
|
+
/* Claim the server instance slot before the boot touches anything a running instance owns: the stores the release coordinator snapshots and migrates, the
|
|
604
|
+
* configuration file the configuration load corrects, the Chrome profile and the port. Without the claim first, a second start against a data directory a live
|
|
605
|
+
* instance holds writes those stores before its claim refuses it, and a newer release started beside an older one migrates the older one's files out from
|
|
606
|
+
* under it. The claim depends on nothing the configuration produces, since the data directory's path is fixed before the server starts, so it can stand this
|
|
607
|
+
* early. The directory is created first because the identity file lives there, and creating it also removes the artifacts retired releases left behind,
|
|
608
|
+
* which no release since their retirement writes. The signal handlers follow the claim directly, so the boot never holds the slot without the shutdown that
|
|
609
|
+
* releases it on a signal; shutdown skips each surface the boot has not built yet. A failure later in the boot, the coordinator's or the configuration's
|
|
610
|
+
* included, exits through the exit handler, whose releaseInstanceSlot() removes the identity file only when this process wrote it.
|
|
611
|
+
*/
|
|
612
|
+
await ensureDataDirectory();
|
|
613
|
+
claimInstanceSlot();
|
|
614
|
+
setupGracefulShutdown();
|
|
459
615
|
// Release boot coordinator: snapshot every persistence-managed file before any reads or migrations run, then apply any pending schema migrations across all
|
|
460
616
|
// stores. Both operations are safe to run more than once within a release (snapshots skip when the labeled copy already exists; ensureMigrated skips when the
|
|
461
617
|
// file is at the current schema version). Running them up front guarantees a restore point exists for every file at the start of every release boot, even if a
|
|
@@ -473,10 +629,6 @@ export async function startServer(parsedArgs) {
|
|
|
473
629
|
try {
|
|
474
630
|
await initializeConfiguration(cliOverrides);
|
|
475
631
|
validateConfiguration();
|
|
476
|
-
// Write any startup capture coercion back to disk so the on-disk state matches the coerced live CONFIG. This keeps a config file that holds an unsupported
|
|
477
|
-
// capture value (native mode) from staying divergent forever, which would otherwise make the reload-validation path reject every later save on the phantom
|
|
478
|
-
// capture diff. A no-op unless validateConfiguration actually coerced something.
|
|
479
|
-
await persistCoercedConfig();
|
|
480
632
|
await initializeUserProfiles();
|
|
481
633
|
validateProfiles();
|
|
482
634
|
}
|
|
@@ -484,13 +636,6 @@ export async function startServer(parsedArgs) {
|
|
|
484
636
|
LOG.error(formatError(error));
|
|
485
637
|
process.exit(1);
|
|
486
638
|
}
|
|
487
|
-
setupGracefulShutdown();
|
|
488
|
-
// Ensure the data directory exists before any operations that depend on it.
|
|
489
|
-
await ensureDataDirectory();
|
|
490
|
-
// Claim the server instance slot before launching Chrome or binding the port. This must come after ensureDataDirectory() since the identity file lives there.
|
|
491
|
-
// claimInstanceSlot() exits with a precise diagnostic when a live holder is detected; on success the identity file holds our PID and boot session ID. If
|
|
492
|
-
// startup subsequently fails, the exit handler calls releaseInstanceSlot() to remove the stale file.
|
|
493
|
-
claimInstanceSlot();
|
|
494
639
|
// Initialize file logger if not using console logging. Initialization follows configuration loading, which resolves the log file path and the size cap, and
|
|
495
640
|
// ensureDataDirectory(), which creates the parent directory. Every line logged before this point is held by the logger and written by its first flush, so
|
|
496
641
|
// the startup messages above are under no ordering constraint.
|
|
@@ -512,16 +657,15 @@ export async function startServer(parsedArgs) {
|
|
|
512
657
|
LOG.info("Debug logging enabled for all categories.");
|
|
513
658
|
}
|
|
514
659
|
}
|
|
515
|
-
// Check FFmpeg availability
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
523
|
-
LOG.info("Using FFmpeg at: %s.", ffmpegPath);
|
|
660
|
+
// Check FFmpeg availability on every boot, because FFmpeg capture is always in effect: the configuration corrects any other capture mode as it is built. This
|
|
661
|
+
// must be after file logger initialization so the log message is captured.
|
|
662
|
+
const ffmpegPath = await resolveFFmpegPath();
|
|
663
|
+
if (!ffmpegPath) {
|
|
664
|
+
LOG.error("FFmpeg is not available. FFmpeg capture mode requires FFmpeg to be installed and in the system PATH.");
|
|
665
|
+
LOG.error("Install FFmpeg and start PrismCast again.");
|
|
666
|
+
process.exit(1);
|
|
524
667
|
}
|
|
668
|
+
LOG.info("Using FFmpeg at: %s.", ffmpegPath);
|
|
525
669
|
// Load user channels from channels.json in the data directory if it exists.
|
|
526
670
|
await initializeUserChannels();
|
|
527
671
|
// Load persisted health state (channel health + domain auth) from health.json.
|
|
@@ -536,8 +680,8 @@ export async function startServer(parsedArgs) {
|
|
|
536
680
|
// can clear the flag without waiting for the next precache cycle. Composition-root wiring - login.ts stays free of discovery knowledge, and revalidateDomainAuth
|
|
537
681
|
// never rejects, so voiding the promise is safe.
|
|
538
682
|
setLoginModeEndObserver((url) => void revalidateDomainAuth(url));
|
|
539
|
-
// Run the cross-store consistency probe now that every store is loaded.
|
|
540
|
-
//
|
|
683
|
+
// Run the cross-store consistency probe now that every store is loaded. It logs each foreign-key-style inconsistency spanning multiple stores that an operator
|
|
684
|
+
// must act on, such as a variant whose canonical is missing, and changes nothing. Errors do not block startup.
|
|
541
685
|
await runConsistencyProbeAtStartup();
|
|
542
686
|
// Load HLS resume state from the previous shutdown. This seeds sequence numbers so streams resume forward instead of resetting to 0.
|
|
543
687
|
loadResumeState(systemClock.now());
|
|
@@ -560,40 +704,29 @@ export async function startServer(parsedArgs) {
|
|
|
560
704
|
// GPU probe wants an environment representative of the one capture runs in and the capture probe acquires a real capture, both of which want the window as the
|
|
561
705
|
// launch left it.
|
|
562
706
|
await syncWindowVisibility();
|
|
563
|
-
//
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
// of here so the catch below surfaces it and the process exits cleanly rather than lingering with no HTTP surface.
|
|
585
|
-
server = await listenMainServer(app);
|
|
586
|
-
// Attach the CDP proxy upgrade handler once the underlying http.Server exists. The handler is gated on the `cdp` debug category at request time, so it sits
|
|
587
|
-
// dormant until the user enables CDP via /debug. We attach unconditionally so the toggle takes effect without requiring a restart.
|
|
588
|
-
attachCdpUpgradeHandler(server);
|
|
589
|
-
}
|
|
590
|
-
catch (error) {
|
|
591
|
-
// Covers both a buildApp failure and a listenMainServer bind failure; the latter already logged the precise, operator-actionable line before rejecting.
|
|
592
|
-
LOG.error("Failed to build or start the HTTP server: %s.", formatError(error));
|
|
593
|
-
throw error;
|
|
707
|
+
// The boot's tail arms what runs for the life of the process: the background services, the HTTP server and HDHomeRun emulation.
|
|
708
|
+
await startBootServices();
|
|
709
|
+
}
|
|
710
|
+
export const defaultStartupFailureDeps = {
|
|
711
|
+
exit: (code) => {
|
|
712
|
+
process.exit(code);
|
|
713
|
+
},
|
|
714
|
+
isGracefulShutdown
|
|
715
|
+
};
|
|
716
|
+
/**
|
|
717
|
+
* Ends a startup that failed, the entry point's catch for startServer. A failure that lands once shutdown has begun is one shutdown caused, such as the browser
|
|
718
|
+
* warm-up's launch that shutdown superseded, so it is shutdown's to end: shutdown owns the exit, and a second exit racing it here would cut its drain short with the
|
|
719
|
+
* wrong code, so the handler logs the superseded startup and leaves the exit to it. The entry point's unhandled-rejection handler reads the same state for the
|
|
720
|
+
* same reason. Any other failure is fatal, and the process exits with a failure code so a process manager sees it.
|
|
721
|
+
* @param error - The error startServer rejected with.
|
|
722
|
+
* @param deps - The shutdown reader and the exit: the real ones in production, and recording stubs in a test.
|
|
723
|
+
*/
|
|
724
|
+
export function handleStartupFailure(error, deps = defaultStartupFailureDeps) {
|
|
725
|
+
if (deps.isGracefulShutdown()) {
|
|
726
|
+
LOG.info("Startup was superseded by shutdown, so the process exits through shutdown.");
|
|
727
|
+
return;
|
|
594
728
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
await startHdhrServer();
|
|
729
|
+
LOG.error("Fatal startup error occurred.", { error: formatError(error) });
|
|
730
|
+
deps.exit(1);
|
|
598
731
|
}
|
|
599
732
|
//# sourceMappingURL=app.js.map
|