mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
fix(electron): wait for the login shell PATH only for bare-name spawns
External players wait only when they resolve to a bare name (no configured path, no well-known install found); a path to an executable starts at once. Embedded MPV waits only on Linux and only for support and prepare, which run the cached bare-name `mpv --version` check; sessions and controls never wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
565becf6dd
commit
dcae8547e3
4 files changed
+150
-53
No files matched your search
@@ -59,22 +59,53 @@ describe('EmbeddedMpvEvents IPC handlers', () => {
|
||||
mockEmbeddedMpvService.setPaused.mockReset();
|
||||
});
|
||||
|
||||
it('checks support only after the login shell PATH lookup settled', async () => {
|
||||
let settle: () => void = () => undefined;
|
||||
mockWaitForLoginShellPath.mockReturnValueOnce(
|
||||
new Promise<void>((resolve) => {
|
||||
settle = resolve;
|
||||
})
|
||||
);
|
||||
mockEmbeddedMpvService.getSupport.mockReturnValue({ supported: true });
|
||||
describe('support checks and the login shell PATH', () => {
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
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();
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
mockWaitForLoginShellPath.mockClear();
|
||||
});
|
||||
|
||||
settle();
|
||||
await expect(support).resolves.toEqual({ supported: true });
|
||||
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.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' });
|
||||
mockEmbeddedMpvService.getSupport.mockReturnValue({
|
||||
supported: true,
|
||||
});
|
||||
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 }
|
||||
);
|
||||
|
||||
expect(mockWaitForLoginShellPath).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a session with the options read from the settings mirror', async () => {
|
||||
|
||||
@@ -57,9 +57,6 @@ function handleEmbeddedMpv<Args extends unknown[]>(
|
||||
): void {
|
||||
ipcMain.handle(channel, async (_event, ...args: unknown[]) => {
|
||||
try {
|
||||
// Linux support checks run `mpv --version` by bare name and
|
||||
// cache the result, so they must see the login shell PATH.
|
||||
await waitForLoginShellPath();
|
||||
return await handler(...(args as Args));
|
||||
} catch (error) {
|
||||
console.error(`[Embedded MPV] ${channel} handler failed:`, error);
|
||||
@@ -68,9 +65,25 @@ function handleEmbeddedMpv<Args extends unknown[]>(
|
||||
});
|
||||
}
|
||||
|
||||
handleEmbeddedMpv(EMBEDDED_MPV_SUPPORT, () => getService().getSupport());
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
async function afterLinuxLoginShellPath<T>(check: () => T): Promise<T> {
|
||||
if (process.platform === 'linux') {
|
||||
await waitForLoginShellPath();
|
||||
}
|
||||
return check();
|
||||
}
|
||||
|
||||
handleEmbeddedMpv(EMBEDDED_MPV_PREPARE, () => getService().prepareAddon());
|
||||
handleEmbeddedMpv(EMBEDDED_MPV_SUPPORT, () =>
|
||||
afterLinuxLoginShellPath(() => getService().getSupport())
|
||||
);
|
||||
|
||||
handleEmbeddedMpv(EMBEDDED_MPV_PREPARE, () =>
|
||||
afterLinuxLoginShellPath(() => getService().prepareAddon())
|
||||
);
|
||||
|
||||
handleEmbeddedMpv(
|
||||
EMBEDDED_MPV_CREATE_SESSION,
|
||||
|
||||
@@ -467,46 +467,79 @@ describe('openVlcPlayer', () => {
|
||||
});
|
||||
|
||||
describe('external player launch handlers', () => {
|
||||
const pathKeys: Record<string, string> = {
|
||||
OPEN_MPV_PLAYER: MPV_PLAYER_PATH,
|
||||
OPEN_VLC_PLAYER: VLC_PLAYER_PATH,
|
||||
};
|
||||
|
||||
function configurePlayerPath(channel: string, playerPath: string): void {
|
||||
(store.get as unknown as jest.Mock).mockImplementation(
|
||||
(key: string, fallback?: unknown) =>
|
||||
key === pathKeys[channel] ? playerPath : fallback
|
||||
);
|
||||
}
|
||||
|
||||
async function launchUntilSpawn(
|
||||
channel: string,
|
||||
wait: Promise<void>,
|
||||
afterLaunch: () => Promise<void>
|
||||
): Promise<void> {
|
||||
(waitForLoginShellPath as jest.Mock).mockReturnValueOnce(wait);
|
||||
const proc = createMockChildProcess();
|
||||
(spawn as unknown as jest.Mock).mockReturnValue(proc);
|
||||
const consoleErrorSpy = jest
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined);
|
||||
try {
|
||||
const launch = Promise.resolve(
|
||||
getIpcMainHandler(channel)(
|
||||
{},
|
||||
'https://example.com/live.m3u8',
|
||||
'Live'
|
||||
)
|
||||
).catch(() => undefined);
|
||||
await afterLaunch();
|
||||
proc.emit('error', new Error('spawn ENOENT'));
|
||||
await launch;
|
||||
} finally {
|
||||
consoleErrorSpy.mockRestore();
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(spawn as unknown as jest.Mock).mockReset();
|
||||
(store.get as unknown as jest.Mock).mockImplementation(
|
||||
(_key: string, fallback?: unknown) => fallback
|
||||
);
|
||||
(waitForLoginShellPath as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
it.each(['OPEN_MPV_PLAYER', 'OPEN_VLC_PLAYER'])(
|
||||
'%s spawns only after the login shell PATH lookup settled',
|
||||
'%s spawns a bare player name only after the login shell PATH lookup settled',
|
||||
async (channel) => {
|
||||
configurePlayerPath(channel, 'player-on-shell-path');
|
||||
let settle: () => void = () => undefined;
|
||||
(waitForLoginShellPath as jest.Mock).mockReturnValueOnce(
|
||||
new Promise<void>((resolve) => {
|
||||
settle = resolve;
|
||||
})
|
||||
);
|
||||
const proc = createMockChildProcess();
|
||||
(spawn as unknown as jest.Mock).mockReturnValue(proc);
|
||||
const consoleErrorSpy = jest
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined);
|
||||
|
||||
try {
|
||||
const launch = Promise.resolve(
|
||||
getIpcMainHandler(channel)(
|
||||
{},
|
||||
'https://example.com/live.m3u8',
|
||||
'Live'
|
||||
)
|
||||
).catch(() => undefined);
|
||||
const wait = new Promise<void>((resolve) => {
|
||||
settle = resolve;
|
||||
});
|
||||
await launchUntilSpawn(channel, wait, async () => {
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(spawn).not.toHaveBeenCalled();
|
||||
|
||||
settle();
|
||||
await waitForSpawnCallCount(1);
|
||||
proc.emit('error', new Error('spawn ENOENT'));
|
||||
await launch;
|
||||
} finally {
|
||||
consoleErrorSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it.each(['OPEN_MPV_PLAYER', 'OPEN_VLC_PLAYER'])(
|
||||
'%s starts a configured executable path without waiting',
|
||||
async (channel) => {
|
||||
configurePlayerPath(channel, '/opt/players/bin/player');
|
||||
await launchUntilSpawn(
|
||||
channel,
|
||||
new Promise<void>(() => undefined),
|
||||
async () => {
|
||||
await waitForSpawnCallCount(1);
|
||||
expect(waitForLoginShellPath).not.toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,12 @@ import {
|
||||
store,
|
||||
VLC_PLAYER_PATH,
|
||||
} from '../services/store.service';
|
||||
import { normalizePlayerPathForStore } from './external-player-launch-context';
|
||||
import {
|
||||
getDefaultMpvPath,
|
||||
getDefaultVlcPath,
|
||||
normalizeCustomPlayerPath,
|
||||
normalizePlayerPathForStore,
|
||||
} from './external-player-launch-context';
|
||||
import {
|
||||
externalPlayerSessions,
|
||||
traceExternalPlayer,
|
||||
@@ -35,6 +40,17 @@ export {
|
||||
parseVlcRcPlaybackState,
|
||||
} from './vlc-session.service';
|
||||
|
||||
/**
|
||||
* A player resolved to a bare name (no configured path, no well-known
|
||||
* install found) is looked up through PATH, so it waits for the login shell
|
||||
* PATH; a path to an executable starts right away.
|
||||
*/
|
||||
async function waitForPathIfBareName(playerPath: string): Promise<void> {
|
||||
if (!/[\\/]/.test(playerPath)) {
|
||||
await waitForLoginShellPath();
|
||||
}
|
||||
}
|
||||
|
||||
export default class PlayerEvents {
|
||||
static bootstrapPlayerEvents(): Electron.IpcMain {
|
||||
return ipcMain;
|
||||
@@ -55,8 +71,10 @@ ipcMain.handle(
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) => {
|
||||
// A bare player name resolves through the login shell PATH.
|
||||
await waitForLoginShellPath();
|
||||
await waitForPathIfBareName(
|
||||
normalizeCustomPlayerPath(store.get(MPV_PLAYER_PATH)) ??
|
||||
getDefaultMpvPath()
|
||||
);
|
||||
return openMpvPlayer({
|
||||
url,
|
||||
title,
|
||||
@@ -100,8 +118,10 @@ ipcMain.handle(
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) => {
|
||||
// A bare player name resolves through the login shell PATH.
|
||||
await waitForLoginShellPath();
|
||||
await waitForPathIfBareName(
|
||||
normalizeCustomPlayerPath(store.get(VLC_PLAYER_PATH)) ??
|
||||
getDefaultVlcPath()
|
||||
);
|
||||
return openVlcPlayer({
|
||||
url,
|
||||
title,
|
||||
|
||||
Reference in new issue
Block a user