fix(electron): wait for the login shell PATH only before the mpv probe

Embedded MPV support and prepare waited on every Linux call, although
getSupport() returns before the bare-name `mpv --version` probe for the
frame-copy engine, native Wayland, a disabled feature or a cached result.
willProbeLinuxMpvExecutable() now gates the wait on the probe actually
running.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-03 08:47:18 +02:00
1 parent dcae8547e3
commit ea7d6f6700
4 files changed
+98 -35

No files matched your search

@@ -6,7 +6,9 @@ jest.mock('electron', () => ({
const mockEmbeddedMpvService = {
createSession: jest.fn(),
prepareAddon: jest.fn(),
getSupport: jest.fn(),
willProbeLinuxMpvExecutable: jest.fn(() => false),
setPaused: jest.fn(),
};
const mockSessionOptions = {
@@ -29,6 +31,7 @@ jest.mock('../startup/login-shell-path', () => ({
import { ipcMain } from 'electron';
import {
EMBEDDED_MPV_CREATE_SESSION,
EMBEDDED_MPV_PREPARE,
EMBEDDED_MPV_SET_PAUSED,
EMBEDDED_MPV_SUPPORT,
} from '@iptvnator/shared/interfaces';
@@ -60,45 +63,55 @@ describe('EmbeddedMpvEvents IPC handlers', () => {
});
describe('support checks and the login shell PATH', () => {
const originalPlatform = process.platform;
afterEach(() => {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
mockWaitForLoginShellPath.mockClear();
});
it('on Linux, checks support only after the lookup settled', async () => {
Object.defineProperty(process, 'platform', { value: 'linux' });
let settle: () => void = () => undefined;
mockWaitForLoginShellPath.mockReturnValueOnce(
new Promise<void>((resolve) => {
settle = resolve;
})
mockEmbeddedMpvService.willProbeLinuxMpvExecutable.mockReset();
mockEmbeddedMpvService.willProbeLinuxMpvExecutable.mockReturnValue(
false
);
mockEmbeddedMpvService.getSupport.mockReturnValue({
supported: true,
});
const support = getIpcMainHandler(EMBEDDED_MPV_SUPPORT)({});
await new Promise<void>((resolve) => setImmediate(resolve));
// Linux caches the result of a bare-name `mpv --version`.
expect(mockEmbeddedMpvService.getSupport).not.toHaveBeenCalled();
settle();
await expect(support).resolves.toEqual({ supported: true });
});
it('elsewhere, and for session calls, never waits', async () => {
Object.defineProperty(process, 'platform', { value: 'darwin' });
it.each([EMBEDDED_MPV_SUPPORT, EMBEDDED_MPV_PREPARE])(
'%s waits for the lookup before the bare-name mpv probe',
async (channel) => {
mockEmbeddedMpvService.willProbeLinuxMpvExecutable.mockReturnValue(
true
);
let settle: () => void = () => undefined;
mockWaitForLoginShellPath.mockReturnValueOnce(
new Promise<void>((resolve) => {
settle = resolve;
})
);
mockEmbeddedMpvService.getSupport.mockReturnValue({
supported: true,
});
mockEmbeddedMpvService.prepareAddon.mockReturnValue({
supported: true,
});
const support = getIpcMainHandler(channel)({});
await new Promise<void>((resolve) => setImmediate(resolve));
expect(
mockEmbeddedMpvService.getSupport
).not.toHaveBeenCalled();
expect(
mockEmbeddedMpvService.prepareAddon
).not.toHaveBeenCalled();
settle();
await expect(support).resolves.toEqual({ supported: true });
}
);
it('does not wait when no probe runs, nor for session calls', async () => {
mockEmbeddedMpvService.getSupport.mockReturnValue({
supported: true,
engine: 'frame-copy',
});
mockEmbeddedMpvService.createSession.mockReturnValue({ id: 's' });
await getIpcMainHandler(EMBEDDED_MPV_SUPPORT)({});
Object.defineProperty(process, 'platform', { value: 'linux' });
await getIpcMainHandler(EMBEDDED_MPV_CREATE_SESSION)(
{},
{ x: 0, y: 0, width: 1, height: 1 }
@@ -66,23 +66,23 @@ function handleEmbeddedMpv<Args extends unknown[]>(
}
/**
* On Linux, support (and prepare, which checks support first) runs
* `mpv --version` by bare name and caches the result, so it must see the
* login shell PATH. Nothing else here spawns by name.
* On Linux, support (and prepare, which checks support first) may run
* `mpv --version` by bare name and cache the result, so it must see the
* login shell PATH. Only that probe waits; nothing else here spawns by name.
*/
async function afterLinuxLoginShellPath<T>(check: () => T): Promise<T> {
if (process.platform === 'linux') {
async function afterLoginShellPathIfProbing<T>(check: () => T): Promise<T> {
if (getService().willProbeLinuxMpvExecutable()) {
await waitForLoginShellPath();
}
return check();
}
handleEmbeddedMpv(EMBEDDED_MPV_SUPPORT, () =>
afterLinuxLoginShellPath(() => getService().getSupport())
afterLoginShellPathIfProbing(() => getService().getSupport())
);
handleEmbeddedMpv(EMBEDDED_MPV_PREPARE, () =>
afterLinuxLoginShellPath(() => getService().prepareAddon())
afterLoginShellPathIfProbing(() => getService().prepareAddon())
);
handleEmbeddedMpv(
@@ -375,6 +375,40 @@ describe('EmbeddedMpvNativeService power blocker', () => {
expect(support.frameCopyAvailable).toBe(true);
});
it('predicts the bare-name mpv probe exactly when getSupport runs it', () => {
Object.defineProperty(process, 'platform', { value: 'linux' });
process.env.DISPLAY = ':0';
delete process.env.WAYLAND_DISPLAY;
mockSpawnSync.mockClear();
mockSpawnSync.mockReturnValue({ status: 0 });
mockRuntimeUsable();
expect(service.willProbeLinuxMpvExecutable()).toBe(true);
service.getSupport();
expect(mockSpawnSync).toHaveBeenCalledWith(
'mpv',
['--version'],
expect.anything()
);
// Cached afterwards: no further probe, so no further wait.
expect(service.willProbeLinuxMpvExecutable()).toBe(false);
});
it('predicts no probe for the frame-copy engine or native Wayland', () => {
Object.defineProperty(process, 'platform', { value: 'linux' });
process.env.DISPLAY = ':0';
process.env.WAYLAND_DISPLAY = 'wayland-0';
mockSpawnSync.mockClear();
mockRuntimeUsable();
expect(service.willProbeLinuxMpvExecutable()).toBe(false);
delete process.env.WAYLAND_DISPLAY;
process.env.IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY = '1';
expect(service.willProbeLinuxMpvExecutable()).toBe(false);
service.getSupport();
expect(mockSpawnSync).not.toHaveBeenCalled();
});
it('keeps frame-copy supported on Linux without a system mpv executable', () => {
// The helper links libmpv itself; the mpv-on-PATH probe only
// binds the native --wid engine.
@@ -377,6 +377,22 @@ export class EmbeddedMpvNativeService {
};
}
/**
* Whether `getSupport()` would now run the uncached Linux bare-name
* `mpv --version` probe, i.e. reach the native-engine checks: no
* frame-copy engine, a usable display server and the feature enabled.
* The IPC layer waits for the login shell PATH only then.
*/
willProbeLinuxMpvExecutable(): boolean {
return (
process.platform === 'linux' &&
this.cachedLinuxMpvExecutableReason === undefined &&
!this.isUnsupportedLinuxDisplayServer() &&
isEmbeddedMpvFeatureEnabled() &&
!this.isFrameCopyEngineActive()
);
}
getSupport(): EmbeddedMpvSupport {
if (!SUPPORTED_EMBEDDED_MPV_PLATFORMS.has(process.platform)) {
return {