diff --git a/AGENTS.md b/AGENTS.md index f25f47536..5397757d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -282,13 +282,14 @@ 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. 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. + detail suppresses both helper fields. Every probe uses an explicit 16 MiB + per-stream child-capture ceiling independent of tracing. 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 diff --git a/CLAUDE.md b/CLAUDE.md index 3f5e5db1d..a7691ada6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -673,9 +673,10 @@ 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. 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 + characters. Invalid detail suppresses both helper fields. Every probe uses + an explicit 16 MiB per-stream child-capture ceiling independent of tracing. + 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 diff --git a/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/contracts.ts b/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/contracts.ts index 2058d9c1e..d9eaac5da 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/contracts.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/contracts.ts @@ -12,7 +12,8 @@ export const FRAME_COPY_ADDON_NAME = 'embedded_mpv.node'; export const FRAME_COPY_READER_NAME = 'embedded_mpv_frame_reader.node'; export const FRAME_COPY_HELPER_NAME = 'iptvnator_mpv_helper'; export const RUNTIME_PROBE_PROTOCOL = 1; -export const { RUNTIME_PROBE_TIMEOUT_MS } = runtimeProbeContract; +export const { RUNTIME_PROBE_MAX_BUFFER_BYTES, RUNTIME_PROBE_TIMEOUT_MS } = + runtimeProbeContract; export const VERSIONED_LIBMPV_PATTERN = /^libmpv\.so\.\d+(?:\.\d+)*$/; export const SAFE_RUNTIME_NAME_PATTERN = /^[A-Za-z0-9_+.-]+$/; export const SHARED_LIBRARY_PATTERN = /\.so(?:\.\d+)*$/; diff --git a/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/probe-orchestration.spec.ts b/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/probe-orchestration.spec.ts index a9ecad0d3..6b6a0f610 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/probe-orchestration.spec.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/probe-orchestration.spec.ts @@ -90,6 +90,7 @@ describe('embedded-mpv frame-copy runtime probe orchestration', () => { timeout: 3000, killSignal: 'SIGKILL', windowsHide: true, + maxBuffer: 16 * 1024 * 1024, env: { PATH: '/usr/bin', LIBGL_ALWAYS_SOFTWARE: '1', @@ -171,6 +172,11 @@ describe('embedded-mpv frame-copy runtime probe orchestration', () => { }); const virtualSnapRoot = '/snap/iptvnator/42'; + const linuxTriplet = 'x86_64-linux-gnu'; + const snapLibraries = (...relativePaths: string[]): string[] => + relativePaths.map((relativePath) => + path.join(virtualSnapRoot, relativePath) + ); const virtualNativeDir = path.join( virtualSnapRoot, 'resources', @@ -231,6 +237,8 @@ describe('embedded-mpv frame-copy runtime probe orchestration', () => { virtualSnapRoot, 'gnome-platform' ), + SNAP_LIBRARY_PATH: + '/var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl/nvidia', }, fileSystem: virtualFileSystem, }); @@ -248,9 +256,28 @@ describe('embedded-mpv frame-copy runtime probe orchestration', () => { expect.objectContaining({ env: expect.objectContaining({ SNAP: virtualSnapRoot, - LD_LIBRARY_PATH: expect.stringContaining( - path.join(virtualNativeDir, 'lib') - ), + LD_LIBRARY_PATH: [ + path.join(virtualNativeDir, 'lib'), + '/var/lib/snapd/lib/gl', + '/var/lib/snapd/lib/gl/nvidia', + ...snapLibraries( + `graphics/usr/lib/${linuxTriplet}`, + `graphics/usr/lib/${linuxTriplet}/vdpau` + ), + '/usr/lib/x86_64-linux-gnu', + ...snapLibraries( + `gnome-platform/lib/${linuxTriplet}`, + `gnome-platform/usr/lib/${linuxTriplet}`, + `gnome-platform/usr/lib/${linuxTriplet}/mesa`, + `gnome-platform/usr/lib/${linuxTriplet}/mesa-egl`, + `gnome-platform/usr/lib/${linuxTriplet}/dri`, + `gnome-platform/usr/lib/${linuxTriplet}/pulseaudio`, + 'lib', + 'usr/lib', + `lib/${linuxTriplet}`, + `usr/lib/${linuxTriplet}` + ), + ].join(':'), }), }) ); diff --git a/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/probe.ts b/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/probe.ts index 229756efb..2b9ad7e2d 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/probe.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime/probe.ts @@ -3,6 +3,7 @@ import * as nodeFileSystem from 'fs'; import path from 'path'; import { RUNTIME_MANIFEST_NAME, + RUNTIME_PROBE_MAX_BUFFER_BYTES, RUNTIME_PROBE_PROTOCOL, RUNTIME_PROBE_TIMEOUT_MS, } from './contracts'; @@ -180,6 +181,7 @@ function runHelperProbe( timeout: RUNTIME_PROBE_TIMEOUT_MS, killSignal: 'SIGKILL', windowsHide: true, + maxBuffer: RUNTIME_PROBE_MAX_BUFFER_BYTES, env: launch.env, }); } catch { diff --git a/docs/architecture/embedded-mpv-native.md b/docs/architecture/embedded-mpv-native.md index 156a249e4..40b380854 100644 --- a/docs/architecture/embedded-mpv-native.md +++ b/docs/architecture/embedded-mpv-native.md @@ -282,12 +282,15 @@ 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. 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. +field. Every probe uses the same explicit 16 MiB per-stream child-capture +ceiling, independent of tracing, so verbose diagnostics do not fall back to +Node's smaller implicit buffer. 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`. diff --git a/tools/embedded-mpv/README.md b/tools/embedded-mpv/README.md index 0c16cc216..9038d0c68 100644 --- a/tools/embedded-mpv/README.md +++ b/tools/embedded-mpv/README.md @@ -239,12 +239,14 @@ 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. 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 +detail suppresses both helper fields. Every probe has the same explicit 16 MiB +per-stream child-capture ceiling, regardless of tracing. 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 diff --git a/tools/embedded-mpv/runtime-probe-contract.cjs b/tools/embedded-mpv/runtime-probe-contract.cjs index 22667c10b..28e8bde7d 100644 --- a/tools/embedded-mpv/runtime-probe-contract.cjs +++ b/tools/embedded-mpv/runtime-probe-contract.cjs @@ -1,4 +1,5 @@ const RUNTIME_PROBE_CONTRACT = Object.freeze({ + RUNTIME_PROBE_MAX_BUFFER_BYTES: 16 * 1024 * 1024, RUNTIME_PROBE_TIMEOUT_MS: 3000, }); diff --git a/tools/embedded-mpv/runtime-probe-contract.d.cts b/tools/embedded-mpv/runtime-probe-contract.d.cts index a4601b29e..545231e21 100644 --- a/tools/embedded-mpv/runtime-probe-contract.d.cts +++ b/tools/embedded-mpv/runtime-probe-contract.d.cts @@ -1,4 +1,5 @@ declare const RUNTIME_PROBE_CONTRACT: Readonly<{ + readonly RUNTIME_PROBE_MAX_BUFFER_BYTES: 16777216; readonly RUNTIME_PROBE_TIMEOUT_MS: 3000; }>; diff --git a/tools/packaging/verify-linux-frame-copy-runtime.mjs b/tools/packaging/verify-linux-frame-copy-runtime.mjs index 13acaac61..fd332abfe 100644 --- a/tools/packaging/verify-linux-frame-copy-runtime.mjs +++ b/tools/packaging/verify-linux-frame-copy-runtime.mjs @@ -23,6 +23,7 @@ const { resolveLinuxFrameCopyProfile, } = require('./linux-frame-copy-profile.cjs'); const { + RUNTIME_PROBE_MAX_BUFFER_BYTES, RUNTIME_PROBE_TIMEOUT_MS, } = require('../embedded-mpv/runtime-probe-contract.cjs'); @@ -71,6 +72,7 @@ function defaultRunCommand(command, args, options = {}) { stdio: 'pipe', timeout: options.timeout, killSignal: options.killSignal, + maxBuffer: options.maxBuffer, windowsHide: true, }); } @@ -1611,6 +1613,7 @@ export function verifyExtractedLinuxFrameCopyRuntime({ encoding: 'utf8', env: probeEnvironment, killSignal: 'SIGKILL', + maxBuffer: RUNTIME_PROBE_MAX_BUFFER_BYTES, timeout: RUNTIME_PROBE_TIMEOUT_MS, windowsHide: true, } diff --git a/tools/packaging/verify-linux-frame-copy-runtime.test.mjs b/tools/packaging/verify-linux-frame-copy-runtime.test.mjs index e26671edf..8e195a95d 100644 --- a/tools/packaging/verify-linux-frame-copy-runtime.test.mjs +++ b/tools/packaging/verify-linux-frame-copy-runtime.test.mjs @@ -91,7 +91,7 @@ const DEB_SYSTEM_PACKAGE_DEPENDENCIES = [ 'libgbm1', ]; -test('uses the shared frozen 3000 ms runtime probe timeout contract', async () => { +test('uses the shared frozen runtime probe resource contract', async () => { assert.equal( fs.existsSync(runtimeProbeContractUrl), true, @@ -108,10 +108,18 @@ test('uses the shared frozen 3000 ms runtime probe timeout contract', async () = ); assert.equal(Object.isFrozen(runtimeProbeContract), true); assert.equal(runtimeProbeContract.RUNTIME_PROBE_TIMEOUT_MS, 3000); + assert.equal( + runtimeProbeContract.RUNTIME_PROBE_MAX_BUFFER_BYTES, + 16 * 1024 * 1024 + ); assert.match( fs.readFileSync(runtimeProbeContractTypesUrl, 'utf8'), /readonly RUNTIME_PROBE_TIMEOUT_MS: 3000/ ); + assert.match( + fs.readFileSync(runtimeProbeContractTypesUrl, 'utf8'), + /readonly RUNTIME_PROBE_MAX_BUFFER_BYTES: 16777216/ + ); const verifierSource = fs.readFileSync(verifierUrl, 'utf8'); assert.match( @@ -133,7 +141,7 @@ test('uses the shared frozen 3000 ms runtime probe timeout contract', async () = ); assert.match( backendContractsSource, - /export const \{ RUNTIME_PROBE_TIMEOUT_MS \} = runtimeProbeContract/ + /export const \{\s*RUNTIME_PROBE_MAX_BUFFER_BYTES,\s*RUNTIME_PROBE_TIMEOUT_MS,?\s*\}\s*=\s*runtimeProbeContract/ ); assert.doesNotMatch( backendContractsSource, @@ -784,6 +792,7 @@ test('validates an x64 system payload and executes one bounded helper probe', () encoding: 'utf8', env: { PATH: '/usr/bin' }, killSignal: 'SIGKILL', + maxBuffer: 16 * 1024 * 1024, timeout: 3000, windowsHide: true, });