mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
fix(packaging): improve Linux runtime smoke diagnostics
This commit is contained in:
1 parent
33573ceee4
commit
4302aa4058
11 files changed
+179
-11
No files matched your search
@@ -333,6 +333,7 @@ jobs:
|
||||
sudo apt-get install --no-install-recommends -y \
|
||||
appstream \
|
||||
binutils \
|
||||
dbus-daemon \
|
||||
flatpak \
|
||||
flatpak-builder \
|
||||
libarchive-tools \
|
||||
@@ -1087,7 +1088,7 @@ jobs:
|
||||
grep -q '\''readlink -f "$SCRIPT_PATH"'\'' "${LAUNCHER_PATH}"
|
||||
grep -q '\''exec "$SCRIPT_DIR/iptvnator.bin"'\'' "${LAUNCHER_PATH}"
|
||||
'
|
||||
xvfb-run -a flatpak run \
|
||||
xvfb-run -a dbus-run-session -- flatpak run \
|
||||
--env=LIBGL_ALWAYS_SOFTWARE=1 \
|
||||
com.fourgray.iptvnator \
|
||||
--embedded-mpv-runtime-probe
|
||||
|
||||
@@ -276,8 +276,10 @@ Key files:
|
||||
disconnected provider returns `snap-graphics-provider-unavailable` before
|
||||
helper spawn. The packaging-only `--embedded-mpv-runtime-probe` app switch
|
||||
runs the complete cached manifest/hash/helper gate before BrowserWindow
|
||||
startup and exits with one availability JSON line. Any loader failure remains
|
||||
a stable native-view fallback, never a flag-enabled success.
|
||||
startup and exits with one availability JSON line. A nonzero helper exit
|
||||
keeps top-level reason `helper-probe-failed`; `helperReason` is present only
|
||||
for an exact protocol-v1 line carrying a fixed allowlisted reason. 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
|
||||
|
||||
@@ -667,10 +667,13 @@ engine` (restart required) or
|
||||
disconnected provider returns `snap-graphics-provider-unavailable` before
|
||||
helper spawn. The packaging-only
|
||||
`--embedded-mpv-runtime-probe` app switch runs the complete packaged gate
|
||||
before BrowserWindow startup and emits one availability JSON line. 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
|
||||
before BrowserWindow startup and emits one availability JSON line. A nonzero
|
||||
helper exit keeps top-level reason `helper-probe-failed`; `helperReason` is
|
||||
present only for an exact protocol-v1 line carrying a fixed allowlisted
|
||||
reason. 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
|
||||
`embedded-mpv-notices.json`, `THIRD_PARTY_NOTICES.txt`, and `licenses/**`.
|
||||
CI caches the staged runtime plus immutable source inputs, never finished
|
||||
|
||||
@@ -11,5 +11,6 @@ export type {
|
||||
EmbeddedMpvFrameCopyRuntimeFileSystem,
|
||||
EmbeddedMpvFrameCopyRuntimeMode,
|
||||
EmbeddedMpvFrameCopyRuntimeResult,
|
||||
EmbeddedMpvHelperRuntimeProbeFailureReason,
|
||||
} from './embedded-mpv-frame-copy-runtime/types';
|
||||
export type { LinuxFrameCopyHelperLaunchFileSystem } from './embedded-mpv-frame-copy-runtime/helper-launch';
|
||||
+74
@@ -123,4 +123,78 @@ describe('embedded-mpv frame-copy helper failures', () => {
|
||||
expect.objectContaining({ usable: false, reason })
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'mpv-create-failed',
|
||||
'mpv-initialize-failed',
|
||||
'gl-context-create-failed',
|
||||
'gl-context-bind-failed',
|
||||
'mpv-render-context-failed',
|
||||
'shared-memory-create-failed',
|
||||
'shared-memory-initialize-failed',
|
||||
])('preserves the allowlisted helper reason %s', (helperReason) => {
|
||||
const fixture = createFixture(context.rootDir);
|
||||
context.spawnRuntimeProbe.mockReturnValue({
|
||||
status: 1,
|
||||
signal: null,
|
||||
stdout: `${JSON.stringify({
|
||||
protocol: 1,
|
||||
usable: false,
|
||||
reason: helperReason,
|
||||
...(helperReason === 'mpv-create-failed'
|
||||
? {}
|
||||
: {
|
||||
detail: 'diagnostic detail is intentionally not propagated',
|
||||
}),
|
||||
})}\n`,
|
||||
stderr: '',
|
||||
});
|
||||
|
||||
expect(context.createProbe()(fixture.helperPath)).toEqual({
|
||||
usable: false,
|
||||
reason: 'helper-probe-failed',
|
||||
helperReason,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['malformed JSON', 'not-json\n'],
|
||||
[
|
||||
'multiple lines',
|
||||
'{"protocol":1,"usable":false,"reason":"mpv-create-failed"}\nignored\n',
|
||||
],
|
||||
[
|
||||
'wrong protocol',
|
||||
'{"protocol":2,"usable":false,"reason":"mpv-create-failed"}\n',
|
||||
],
|
||||
[
|
||||
'wrong usable value',
|
||||
'{"protocol":1,"usable":true,"reason":"mpv-create-failed"}\n',
|
||||
],
|
||||
[
|
||||
'non-allowlisted reason',
|
||||
'{"protocol":1,"usable":false,"reason":"loader-injected"}\n',
|
||||
],
|
||||
[
|
||||
'unexpected field',
|
||||
'{"protocol":1,"usable":false,"reason":"mpv-create-failed","extra":true}\n',
|
||||
],
|
||||
[
|
||||
'invalid detail',
|
||||
'{"protocol":1,"usable":false,"reason":"mpv-create-failed","detail":1}\n',
|
||||
],
|
||||
])('does not propagate a helper reason from %s', (_label, stdout) => {
|
||||
const fixture = createFixture(context.rootDir);
|
||||
context.spawnRuntimeProbe.mockReturnValue({
|
||||
status: 1,
|
||||
signal: null,
|
||||
stdout,
|
||||
stderr: '',
|
||||
});
|
||||
|
||||
expect(context.createProbe()(fixture.helperPath)).toEqual({
|
||||
usable: false,
|
||||
reason: 'helper-probe-failed',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,11 +8,13 @@ import {
|
||||
} from './contracts';
|
||||
import { createLinuxFrameCopyHelperLaunch } from './helper-launch';
|
||||
import { validatePackage } from './package-validator';
|
||||
import { EMBEDDED_MPV_HELPER_RUNTIME_PROBE_FAILURE_REASONS } from './types';
|
||||
import type {
|
||||
EmbeddedMpvFrameCopyManifestContract,
|
||||
EmbeddedMpvFrameCopyRuntimeDependencies,
|
||||
EmbeddedMpvFrameCopyRuntimeFileSystem,
|
||||
EmbeddedMpvFrameCopyRuntimeResult,
|
||||
EmbeddedMpvHelperRuntimeProbeFailureReason,
|
||||
ValidManifest,
|
||||
ValidatedPackage,
|
||||
} from './types';
|
||||
@@ -25,6 +27,45 @@ import {
|
||||
isValidationFailure,
|
||||
} from './validation-primitives';
|
||||
|
||||
const HELPER_RUNTIME_PROBE_FAILURE_REASON_ALLOWLIST = new Set<string>(
|
||||
Object.values(EMBEDDED_MPV_HELPER_RUNTIME_PROBE_FAILURE_REASONS)
|
||||
);
|
||||
|
||||
function parseFailedProbeReason(
|
||||
stdout: unknown
|
||||
): EmbeddedMpvHelperRuntimeProbeFailureReason | null {
|
||||
if (typeof stdout !== 'string' || !/^[^\r\n]+\n$/.test(stdout)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(stdout.slice(0, -1));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!isObject(parsed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasDetail = Object.prototype.hasOwnProperty.call(parsed, 'detail');
|
||||
const fields = hasDetail
|
||||
? ['detail', 'protocol', 'reason', 'usable']
|
||||
: ['protocol', 'reason', 'usable'];
|
||||
if (
|
||||
!hasExactFields(parsed, fields) ||
|
||||
parsed.protocol !== RUNTIME_PROBE_PROTOCOL ||
|
||||
parsed.usable !== false ||
|
||||
typeof parsed.reason !== 'string' ||
|
||||
!HELPER_RUNTIME_PROBE_FAILURE_REASON_ALLOWLIST.has(parsed.reason) ||
|
||||
(hasDetail &&
|
||||
(typeof parsed.detail !== 'string' || parsed.detail.length === 0))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return parsed.reason as EmbeddedMpvHelperRuntimeProbeFailureReason;
|
||||
}
|
||||
|
||||
function parseSuccessfulProbe(
|
||||
stdout: unknown,
|
||||
manifest: ValidManifest
|
||||
@@ -116,7 +157,14 @@ function runHelperProbe(
|
||||
return failure('helper-probe-signaled');
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
return failure('helper-probe-failed');
|
||||
const helperReason = parseFailedProbeReason(result.stdout);
|
||||
return helperReason
|
||||
? {
|
||||
usable: false,
|
||||
reason: 'helper-probe-failed',
|
||||
helperReason,
|
||||
}
|
||||
: failure('helper-probe-failed');
|
||||
}
|
||||
return parseSuccessfulProbe(result.stdout, runtimePackage.manifest);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import type { spawnSync as nodeSpawnSync } from 'child_process';
|
||||
import type * as nodeFileSystem from 'fs';
|
||||
|
||||
export const EMBEDDED_MPV_HELPER_RUNTIME_PROBE_FAILURE_REASONS = {
|
||||
MPV_CREATE_FAILED: 'mpv-create-failed',
|
||||
MPV_INITIALIZE_FAILED: 'mpv-initialize-failed',
|
||||
GL_CONTEXT_CREATE_FAILED: 'gl-context-create-failed',
|
||||
GL_CONTEXT_BIND_FAILED: 'gl-context-bind-failed',
|
||||
MPV_RENDER_CONTEXT_FAILED: 'mpv-render-context-failed',
|
||||
SHARED_MEMORY_CREATE_FAILED: 'shared-memory-create-failed',
|
||||
SHARED_MEMORY_INITIALIZE_FAILED: 'shared-memory-initialize-failed',
|
||||
} as const;
|
||||
|
||||
export type EmbeddedMpvHelperRuntimeProbeFailureReason =
|
||||
(typeof EMBEDDED_MPV_HELPER_RUNTIME_PROBE_FAILURE_REASONS)[keyof typeof EMBEDDED_MPV_HELPER_RUNTIME_PROBE_FAILURE_REASONS];
|
||||
|
||||
export type EmbeddedMpvFrameCopyRuntimeFailureReason =
|
||||
| 'unsupported-platform'
|
||||
| 'unsupported-architecture'
|
||||
@@ -41,6 +54,7 @@ export type EmbeddedMpvFrameCopyRuntimeResult =
|
||||
| {
|
||||
usable: false;
|
||||
reason: EmbeddedMpvFrameCopyRuntimeFailureReason;
|
||||
helperReason?: EmbeddedMpvHelperRuntimeProbeFailureReason;
|
||||
};
|
||||
|
||||
export interface EmbeddedMpvFrameCopyRuntimeFileSystem {
|
||||
|
||||
@@ -86,10 +86,11 @@ describe('embedded MPV runtime diagnostic', () => {
|
||||
expect(harness.continueStartup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prints the unavailable reason as one JSON line, exits nonzero, and skips startup', () => {
|
||||
it('prints the unavailable helper reason as one JSON line, exits nonzero, and skips startup', () => {
|
||||
const availability: FrameCopyRuntimeAvailability = {
|
||||
usable: false,
|
||||
reason: 'helper-probe-failed',
|
||||
helperReason: 'gl-context-create-failed',
|
||||
};
|
||||
const harness = createHarness(availability);
|
||||
|
||||
@@ -100,7 +101,7 @@ describe('embedded MPV runtime diagnostic', () => {
|
||||
|
||||
expect(harness.getRuntimeAvailability).toHaveBeenCalledTimes(1);
|
||||
expect(harness.writeStdout).toHaveBeenCalledWith(
|
||||
'{"usable":false,"reason":"helper-probe-failed"}\n'
|
||||
'{"usable":false,"reason":"helper-probe-failed","helperReason":"gl-context-create-failed"}\n'
|
||||
);
|
||||
expect(harness.writeStdout).toHaveBeenCalledTimes(1);
|
||||
expect(harness.exit).toHaveBeenCalledWith(1);
|
||||
|
||||
@@ -275,7 +275,11 @@ named `/impv-fc-runtime-probe-<pid>`. It never opens media or enters the media
|
||||
or command loops. Shared-memory creation/mapping and header-initialization
|
||||
failures emit the stable helper reasons `shared-memory-create-failed` and
|
||||
`shared-memory-initialize-failed`. The probe must emit exactly one protocol-v1
|
||||
JSON line and return zero.
|
||||
JSON line and return zero. When the helper exits nonzero with an otherwise
|
||||
exact failure line, the application availability diagnostic keeps the
|
||||
fail-closed top-level reason `helper-probe-failed` and may add only the
|
||||
allowlisted helper reason as `helperReason`; malformed, multi-line,
|
||||
wrong-protocol, or unknown failure output never reaches that field.
|
||||
|
||||
The startup probe and every playback helper session use the same sanitized
|
||||
loader environment selected by the validated manifest's cached `runtimeMode`.
|
||||
|
||||
@@ -232,6 +232,9 @@ shared-memory ring named `/impv-fc-runtime-probe-<pid>`. It does not open media
|
||||
or enter media/command loops. A timeout, loader failure, malformed protocol,
|
||||
missing file, hash mismatch, unusable graphics path, or shm lifecycle failure
|
||||
returns a stable reason and keeps the BrowserWindow sandbox enabled. The
|
||||
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. 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
|
||||
|
||||
@@ -518,6 +518,23 @@ test('Linux CI verifies every package family and exercises intended environments
|
||||
assert.doesNotMatch(buildWorkflow, /\bldd\b/);
|
||||
});
|
||||
|
||||
test('Flatpak application runtime probe runs under an isolated D-Bus session', () => {
|
||||
const installStep = workflowStep('Install Linux system dependencies');
|
||||
const flatpakVerificationStep = workflowStep(
|
||||
'Verify Flatpak payload, launcher, and sandboxed runtime'
|
||||
);
|
||||
|
||||
assert.match(
|
||||
flatpakVerificationStep,
|
||||
/xvfb-run -a dbus-run-session -- flatpak run\s+\\\s+--env=LIBGL_ALWAYS_SOFTWARE=1\s+\\\s+com\.fourgray\.iptvnator\s+\\\s+--embedded-mpv-runtime-probe/
|
||||
);
|
||||
assert.match(installStep, /^\s+dbus-daemon\s+\\$/m);
|
||||
assert.match(
|
||||
flatpakVerificationStep,
|
||||
/xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1\s+\\\s+flatpak run --command=sh com\.fourgray\.iptvnator/
|
||||
);
|
||||
});
|
||||
|
||||
test('foreign DEB CI explicitly selects both marker-only ARM architectures', () => {
|
||||
const foreignDebStep = workflowStep(
|
||||
'Make marker-only foreign-architecture DEB packages'
|
||||
|
||||
Reference in new issue
Block a user