test(packaging): trace Snap EGL probe failures

This commit is contained in:
4gray committed 2026-07-18 13:21:35 +02:00
1 parent a575af78bf
commit 318fc32d91
10 files changed
+166 -9

No files matched your search

+3
View File
@@ -1003,6 +1003,9 @@ jobs:
'$1 == "shared-memory" && $2 == "iptvnator:shared-memory" && $3 == ":shared-memory" { found=1 } END { exit !found }'
xvfb-run -a env \
LIBGL_ALWAYS_SOFTWARE=1 \
IPTVNATOR_TRACE_PLAYER=1 \
EGL_LOG_LEVEL=debug \
LIBGL_DEBUG=verbose \
__EGL_VENDOR_LIBRARY_FILENAMES=/tmp/hostile-egl-vendor.json \
GBM_BACKEND=/tmp/hostile-gbm \
MESA_LOADER_DRIVER_OVERRIDE=/tmp/hostile-dri \
+8 -3
View File
@@ -70,7 +70,7 @@ IPTVNATOR_TRACE_STARTUP=1 nx serve electron-backend
- `IPTVNATOR_TRACE_DB=1` traces DB worker requests and request-scoped DB events
- `IPTVNATOR_TRACE_SQL=1` traces SQLite statements in the main process and DB worker
- `IPTVNATOR_TRACE_WINDOW=1` traces BrowserWindow lifecycle and unresponsive events
- `IPTVNATOR_TRACE_PLAYER=1` traces external-player launch/reuse/polling debug output
- `IPTVNATOR_TRACE_PLAYER=1` traces external-player activity and bounded Embedded MPV runtime-probe stderr
- `IPTVNATOR_TRACE_RENDERER_CONSOLE=1` mirrors renderer console output into the Electron terminal
- GPU/compositor debugging:
@@ -280,8 +280,13 @@ Key files:
keeps top-level reason `helper-probe-failed`; `helperReason` is present only
for an exact protocol-v1 line carrying a fixed allowlisted reason, and its
optional `helperDetail` must be 1–1024 printable ASCII characters. Invalid
detail suppresses both helper fields. Any loader failure remains a stable
native-view fallback, never a flag-enabled success.
detail suppresses both helper fields. With `IPTVNATOR_TRACE_PLAYER=1`, a
non-empty helper stderr capture is emitted separately as one JSON-escaped
stderr line whose `stderr` field is limited to 16,384 characters and whose
`truncated` field is always explicit; trace-write failure cannot change the
capability result. Installed-Snap CI enables Mesa EGL/GL diagnostics through
this bounded channel. Any loader failure remains a stable native-view
fallback, never a flag-enabled success.
- In the exact packaged Flatpak `/app` context, reconstruct only Freedesktop
Platform 24.08's immutable `__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS`; its GL
extension loader path comes from the sandbox cache. Flatpak CI must invoke
+8 -3
View File
@@ -127,7 +127,7 @@ Useful narrower flags:
- `IPTVNATOR_TRACE_DB=1` traces DB worker requests and DB progress events
- `IPTVNATOR_TRACE_SQL=1` traces SQLite statements in both main and worker connections
- `IPTVNATOR_TRACE_WINDOW=1` traces BrowserWindow navigation/load lifecycle
- `IPTVNATOR_TRACE_PLAYER=1` traces external-player launch/reuse/polling debug output
- `IPTVNATOR_TRACE_PLAYER=1` traces external-player activity and bounded Embedded MPV runtime-probe stderr
- `IPTVNATOR_TRACE_RENDERER_CONSOLE=1` mirrors renderer console logs into the Electron terminal
For GPU/compositor debugging:
@@ -671,8 +671,13 @@ engine` (restart required) or
helper exit keeps top-level reason `helper-probe-failed`; `helperReason` is
present only for an exact protocol-v1 line carrying a fixed allowlisted
reason, and its optional `helperDetail` must be 1–1024 printable ASCII
characters. Invalid detail suppresses both helper fields. The exact packaged
Flatpak `/app` context reconstructs only Freedesktop Platform 24.08's immutable
characters. Invalid detail suppresses both helper fields. With
`IPTVNATOR_TRACE_PLAYER=1`, non-empty helper stderr is emitted separately as
one JSON-escaped stderr line with a 16,384-character `stderr` limit and an
explicit `truncated` field; trace-write failure cannot change availability.
Installed-Snap CI enables Mesa EGL/GL diagnostics through this bounded
channel. The exact packaged Flatpak `/app` context reconstructs only
Freedesktop Platform 24.08's immutable
`__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS`; its CI smoke invokes that
application-level probe instead of the helper directly. Bundled
Linux packages carry hash-validated
@@ -253,4 +253,86 @@ describe('embedded-mpv frame-copy helper failures', () => {
});
}
);
it('does not trace helper stderr without the exact player trace flag', () => {
const fixture = createFixture(context.rootDir);
context.spawnRuntimeProbe.mockReturnValue({
status: 1,
signal: null,
stdout: '{"protocol":1,"usable":false,"reason":"gl-context-create-failed"}\n',
stderr: 'libEGL debug output\n',
});
expect(context.createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'helper-probe-failed',
helperReason: 'gl-context-create-failed',
});
expect(context.writeRuntimeProbeStderr).not.toHaveBeenCalled();
});
it('traces helper stderr as one JSON-escaped line when player tracing is enabled', () => {
const fixture = createFixture(context.rootDir);
const helperStderr =
'libEGL warning: vendor "mesa"\nfailed path: C:\\driver';
context.spawnRuntimeProbe.mockReturnValue({
status: 1,
signal: null,
stdout: '{"protocol":1,"usable":false,"reason":"gl-context-create-failed"}\n',
stderr: helperStderr,
});
expect(
context.createProbe({
env: {
PATH: '/usr/bin',
IPTVNATOR_TRACE_PLAYER: '1',
},
})(fixture.helperPath)
).toEqual({
usable: false,
reason: 'helper-probe-failed',
helperReason: 'gl-context-create-failed',
});
expect(context.writeRuntimeProbeStderr).toHaveBeenCalledWith(
`${JSON.stringify({
event: 'embedded-mpv-helper-runtime-probe-stderr',
stderr: helperStderr,
truncated: false,
})}\n`
);
expect(context.writeRuntimeProbeStderr).toHaveBeenCalledTimes(1);
expect(
context.writeRuntimeProbeStderr.mock.calls[0][0].split('\n')
).toHaveLength(2);
});
it('bounds traced helper stderr to 16384 characters and reports truncation', () => {
const fixture = createFixture(context.rootDir);
const helperStderr = `${'x'.repeat(16_384)}discarded`;
context.spawnRuntimeProbe.mockReturnValue({
status: 1,
signal: null,
stdout: '{"protocol":1,"usable":false,"reason":"gl-context-create-failed"}\n',
stderr: helperStderr,
});
context.createProbe({
env: {
PATH: '/usr/bin',
IPTVNATOR_TRACE_PLAYER: '1',
},
})(fixture.helperPath);
const trace = JSON.parse(
context.writeRuntimeProbeStderr.mock.calls[0][0]
);
expect(trace).toEqual({
event: 'embedded-mpv-helper-runtime-probe-stderr',
stderr: 'x'.repeat(16_384),
truncated: true,
});
expect(trace.stderr).toHaveLength(16_384);
expect(context.writeRuntimeProbeStderr).toHaveBeenCalledTimes(1);
});
});
@@ -30,6 +30,9 @@ import {
const HELPER_RUNTIME_PROBE_FAILURE_REASON_ALLOWLIST = new Set<string>(
Object.values(EMBEDDED_MPV_HELPER_RUNTIME_PROBE_FAILURE_REASONS)
);
const HELPER_RUNTIME_PROBE_STDERR_LIMIT = 16_384;
const HELPER_RUNTIME_PROBE_STDERR_EVENT =
'embedded-mpv-helper-runtime-probe-stderr';
interface ParsedFailedProbe {
helperReason: EmbeddedMpvHelperRuntimeProbeFailureReason;
@@ -130,6 +133,31 @@ function parseSuccessfulProbe(
};
}
function traceHelperProbeStderr(
stderr: unknown,
dependencies: EmbeddedMpvFrameCopyRuntimeDependencies
): void {
if (
dependencies.env.IPTVNATOR_TRACE_PLAYER !== '1' ||
typeof stderr !== 'string' ||
stderr.length === 0
) {
return;
}
const boundedStderr = stderr.slice(0, HELPER_RUNTIME_PROBE_STDERR_LIMIT);
const output = `${JSON.stringify({
event: HELPER_RUNTIME_PROBE_STDERR_EVENT,
stderr: boundedStderr,
truncated: stderr.length > boundedStderr.length,
})}\n`;
try {
dependencies.writeStderr(output);
} catch {
// Opt-in diagnostics must never change the fail-closed probe result.
}
}
function runHelperProbe(
runtimePackage: ValidatedPackage,
dependencies: EmbeddedMpvFrameCopyRuntimeDependencies
@@ -157,6 +185,7 @@ function runHelperProbe(
} catch {
return failure('helper-probe-spawn-error');
}
traceHelperProbeStderr(result.stderr, dependencies);
if (
result.error &&
@@ -207,6 +236,9 @@ export function createEmbeddedMpvFrameCopyRuntimeProbe(
env: process.env,
fileSystem: defaultFileSystem,
spawnSync: nodeSpawnSync,
writeStderr: (output) => {
nodeFileSystem.writeSync(process.stderr.fd, output);
},
...overrides,
};
const resultCache = new Map<string, EmbeddedMpvFrameCopyRuntimeResult>();
@@ -18,6 +18,7 @@ import { SUCCESS_OUTPUT } from './runtime.spec-data';
export interface RuntimeTestContext {
rootDir: string;
spawnRuntimeProbe: jest.Mock;
writeRuntimeProbeStderr: jest.Mock<void, [string]>;
fileSystem: EmbeddedMpvFrameCopyRuntimeDependencies['fileSystem'];
createProbe(
overrides?: Partial<EmbeddedMpvFrameCopyRuntimeDependencies>
@@ -33,6 +34,7 @@ export function createRuntimeTestContext(): RuntimeTestContext {
stdout: SUCCESS_OUTPUT,
stderr: '',
}));
const writeRuntimeProbeStderr = jest.fn<void, [string]>();
const fileSystem: EmbeddedMpvFrameCopyRuntimeDependencies['fileSystem'] = {
accessSync: jest.fn((filePath: string, mode: number) =>
accessSync(filePath, mode)
@@ -44,6 +46,7 @@ export function createRuntimeTestContext(): RuntimeTestContext {
const context: RuntimeTestContext = {
rootDir,
spawnRuntimeProbe,
writeRuntimeProbeStderr,
fileSystem,
createProbe(
overrides: Partial<EmbeddedMpvFrameCopyRuntimeDependencies> = {}
@@ -59,6 +62,7 @@ export function createRuntimeTestContext(): RuntimeTestContext {
},
fileSystem: context.fileSystem,
spawnSync: context.spawnRuntimeProbe as typeof spawnSync,
writeStderr: context.writeRuntimeProbeStderr,
...overrides,
});
},
@@ -71,6 +71,7 @@ export interface EmbeddedMpvFrameCopyRuntimeDependencies {
env: NodeJS.ProcessEnv;
fileSystem: EmbeddedMpvFrameCopyRuntimeFileSystem;
spawnSync: typeof nodeSpawnSync;
writeStderr(output: string): void;
}
export interface RuntimeFile {
+10 -1
View File
@@ -282,7 +282,12 @@ allowlisted helper reason as `helperReason`. An optional `helperDetail` is
copied only from the same exact line when it contains 1–1024 printable ASCII
characters; an invalid detail rejects both helper fields. Malformed,
multi-line, wrong-protocol, or unknown failure output never reaches either
field.
field. When `IPTVNATOR_TRACE_PLAYER=1`, the probe also emits non-empty captured
helper stderr separately as one JSON line. JSON escaping keeps embedded
newlines on that single line, the `stderr` field is limited to the first 16,384
characters, and the `truncated` boolean is always present. A missing flag,
empty capture, or trace-writer failure produces no trace and never changes the
cached availability result or the application diagnostic's stdout protocol.
The startup probe and every playback helper session use the same sanitized
loader environment selected by the validated manifest's cached `runtimeMode`.
@@ -304,6 +309,10 @@ GL extension `add-ld-path` is supplied through the sandbox loader cache, so no
ambient `LD_LIBRARY_PATH` is needed. Flatpak CI runs the application-level
`--embedded-mpv-runtime-probe`; direct helper execution is only a package
layout check and cannot substitute for the real gate.
The installed-Snap smoke enables `IPTVNATOR_TRACE_PLAYER=1`,
`EGL_LOG_LEVEL=debug`, and `LIBGL_DEBUG=verbose`, so GLVND/Mesa loader failures
remain observable through the bounded stderr record while the same hostile
ambient-path assertions and fail-closed application gate stay active.
Inside a genuine Snap mount, filtered absolute `SNAP_LIBRARY_PATH` entries below
`/var/lib/snapd/lib/gl` follow `native/lib`. The exact
`$SNAP/graphics/usr/lib/x86_64-linux-gnu` content-provider roots come next,
+8 -2
View File
@@ -236,14 +236,20 @@ application diagnostic retains `helper-probe-failed` as the top-level reason
for nonzero helper exits and adds `helperReason` only when the helper emitted
one exact protocol-v1 line with a fixed allowlisted reason. Its optional
`helperDetail` is restricted to 1–1024 printable ASCII characters; invalid
detail suppresses both helper fields. The
detail suppresses both helper fields. With `IPTVNATOR_TRACE_PLAYER=1`, non-empty
captured helper stderr is written separately as one JSON-escaped stderr line:
its `stderr` field contains at most the first 16,384 characters and its
`truncated` boolean is always explicit. Empty captures, disabled tracing, and
trace-writer failures do not emit a record or alter availability. The
installed-Snap probe therefore tests the private shared-memory confinement
needed by playback rather than only loader and graphics startup.
Packaging CI invokes the same gate through
`snap run iptvnator --embedded-mpv-runtime-probe`. This packaging-only
application switch runs before BrowserWindow startup, emits one availability
JSON line, and returns zero only for a usable runtime; it never directly loads
libmpv in Electron.
libmpv in Electron. The installed-Snap smoke adds `EGL_LOG_LEVEL=debug` and
`LIBGL_DEBUG=verbose` under that bounded trace channel to expose GLVND/Mesa
loader failures without weakening the hostile-environment gate.
Electron Builder excludes `electron-backend/native{,/**/*}` from `app.asar`.
Only `afterPack` writes the profile-normalized
@@ -709,6 +709,16 @@ test('Snap verifier preserves fail-closed status while exposing captured diagnos
assert.ok(gnomeConnectionAssertion < sharedMemoryConnectionAssertion);
assert.ok(sharedMemoryConnectionAssertion < successfulRuntimeProbe);
assert.match(snapStep, /snap run iptvnator --embedded-mpv-runtime-probe/);
for (const traceSelector of [
'IPTVNATOR_TRACE_PLAYER=1',
'EGL_LOG_LEVEL=debug',
'LIBGL_DEBUG=verbose',
]) {
assert.match(
snapStep.slice(secondGraphicsConnect),
new RegExp(traceSelector)
);
}
for (const hostileOverride of [
'__EGL_VENDOR_LIBRARY_FILENAMES',
'GBM_BACKEND',