mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
* perf(electron): look up the login shell PATH without blocking the main thread fix-path ran $SHELL -ilc env synchronously right after the first load. With a typical zsh profile that held the main thread for 1-2 s, while the database worker's ready message and the renderer's first IPC calls waited, so the launch journey's first card came that much later. Use shell-path's async shellPath() with fix-path's fallback, so the resulting PATH is the same and the main thread stays free. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(perf): name the PR that made the login shell PATH lookup async Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(electron): let bare-name player spawns wait for the login shell PATH With the lookup now asynchronous, an external player launched (or the Linux embedded MPV support check, which runs and caches a bare `mpv --version`) within the first seconds could see the inherited PATH. The OPEN_MPV_PLAYER / OPEN_VLC_PLAYER handlers and every embedded MPV handler now await waitForLoginShellPath() (settled lookup, at most 10 s, immediate on Windows), restoring the guarantee the blocking lookup gave. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * 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> * 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> * fix(electron): stop waiting for a login shell that already timed out once After the first wait for a hung `$SHELL -ilc env` runs out, later bare-name player launches and Linux mpv probes proceed at once instead of each waiting the full limit again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(electron): bound login shell PATH waits by the lookup's own budget Replace the latch on the first expired wait with a deadline set when the lookup starts (10 s). Every wait ends when the lookup settles or the deadline passes: a launch retried while the shell is still within its budget waits for the PATH again, and once the budget is spent no launch waits, so a hung shell still delays at most the first seconds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(electron): re-probe a missing mpv once a late login shell answers When the PATH lookup runs out of budget, the Linux embedded MPV support check probes `mpv --version` with the inherited PATH and caches a missing result for the rest of the session. waitForLoginShellPath() now reports whether the lookup settled; after a timed-out wait the handler forgets a cached "missing" once the lookup finishes, so the next support check probes again with the login shell PATH. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(electron): share one deadline among waits before the PATH lookup starts A bare-name launch that waited before the lookup was scheduled started its own 10 s limit, so while startup was stuck every retry paid the full delay again. The first early wait now sets the shared deadline; the lookup still replaces it with its own budget when it starts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(electron): re-probe mpv after a late login shell PATH either way A Linux mpv probe that ran on the inherited PATH can be wrong in both directions: the login shell PATH may add mpv or drop the directory the inherited one found it in. forgetLinuxMpvExecutableProbe() now clears a found result as well as a missing one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(electron): skip the login shell PATH wait for Flatpak host launches In Flatpak, players start through `flatpak-spawn --host`, which resolves the name with the host's PATH; the sandbox's login shell lookup cannot change it. The launch handlers now decide from the same launch context the player uses: no wait in flatpak-host mode, otherwise only for a bare player name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
179 lines
6.5 KiB
TypeScript
179 lines
6.5 KiB
TypeScript
const shellPath = jest.fn<Promise<string | undefined>, []>();
|
|
const shellPathSync = jest.fn<string | undefined, []>();
|
|
|
|
jest.mock('shell-path', () => ({ shellPath, shellPathSync }));
|
|
jest.mock('../services/debug-trace', () => ({
|
|
traceStartupPhase: jest.fn(),
|
|
}));
|
|
|
|
type LoginShellPathModule = typeof import('./login-shell-path');
|
|
|
|
function loadModule(): LoginShellPathModule {
|
|
let loaded: LoginShellPathModule | undefined;
|
|
jest.isolateModules(() => {
|
|
loaded = jest.requireActual('./login-shell-path');
|
|
});
|
|
return loaded as LoginShellPathModule;
|
|
}
|
|
|
|
async function flushScheduled(): Promise<void> {
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
}
|
|
|
|
describe('login shell PATH', () => {
|
|
const originalPath = process.env.PATH;
|
|
const originalPlatform = process.platform;
|
|
|
|
beforeEach(() => {
|
|
shellPath.mockReset();
|
|
shellPathSync.mockReset();
|
|
process.env.PATH = '/usr/bin:/bin';
|
|
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
});
|
|
|
|
afterAll(() => {
|
|
process.env.PATH = originalPath;
|
|
Object.defineProperty(process, 'platform', {
|
|
value: originalPlatform,
|
|
});
|
|
});
|
|
|
|
it('reads the login shell asynchronously, never through the blocking API', async () => {
|
|
let resolveShell: (path: string) => void = () => undefined;
|
|
shellPath.mockReturnValue(
|
|
new Promise((resolve) => {
|
|
resolveShell = resolve;
|
|
})
|
|
);
|
|
|
|
loadModule().scheduleDeferredFixPath();
|
|
await flushScheduled();
|
|
|
|
// The shell is still starting, and the main thread is free.
|
|
expect(shellPath).toHaveBeenCalledTimes(1);
|
|
expect(shellPathSync).not.toHaveBeenCalled();
|
|
expect(process.env.PATH).toBe('/usr/bin:/bin');
|
|
|
|
resolveShell('/opt/homebrew/bin:/usr/bin:/bin');
|
|
await flushScheduled();
|
|
expect(process.env.PATH).toBe('/opt/homebrew/bin:/usr/bin:/bin');
|
|
});
|
|
|
|
it('lets spawns wait until the lookup settled, at most the limit', async () => {
|
|
let resolveShell: (path: string) => void = () => undefined;
|
|
const module = loadModule();
|
|
module.scheduleDeferredFixPath(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
resolveShell = resolve;
|
|
})
|
|
);
|
|
let waited = false;
|
|
const wait = module.waitForLoginShellPath().then(() => {
|
|
waited = true;
|
|
});
|
|
await flushScheduled();
|
|
expect(waited).toBe(false);
|
|
|
|
resolveShell('/opt/homebrew/bin');
|
|
await wait;
|
|
expect(process.env.PATH).toBe('/opt/homebrew/bin');
|
|
});
|
|
|
|
it('ends every wait at the lookup budget, so a hung shell delays no later launch', async () => {
|
|
const stuck = loadModule();
|
|
stuck.scheduleDeferredFixPath(() => new Promise(() => undefined), 20);
|
|
await expect(stuck.waitForLoginShellPath()).resolves.toBe(false);
|
|
// Past the budget a later launch goes at once (the test would time
|
|
// out on this one otherwise).
|
|
await expect(stuck.waitForLoginShellPath(60_000)).resolves.toBe(false);
|
|
});
|
|
|
|
it('lets waits before the lookup is scheduled share one deadline', async () => {
|
|
jest.useFakeTimers();
|
|
try {
|
|
const module = loadModule();
|
|
const first = module.waitForLoginShellPath();
|
|
await jest.advanceTimersByTimeAsync(6_000);
|
|
const retry = module.waitForLoginShellPath();
|
|
await jest.advanceTimersByTimeAsync(4_000);
|
|
// Both end at the first wait's deadline, not 10 s after the retry.
|
|
await expect(first).resolves.toBe(false);
|
|
await expect(retry).resolves.toBe(false);
|
|
await expect(module.waitForLoginShellPath()).resolves.toBe(false);
|
|
} finally {
|
|
jest.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it('lets a launch retried within the budget wait for the PATH again', async () => {
|
|
let resolveShell: (path: string) => void = () => undefined;
|
|
const module = loadModule();
|
|
module.scheduleDeferredFixPath(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
resolveShell = resolve;
|
|
}),
|
|
60_000
|
|
);
|
|
await flushScheduled();
|
|
// The first launch gives up on its own, shorter limit...
|
|
await module.waitForLoginShellPath(5);
|
|
expect(process.env.PATH).toBe('/usr/bin:/bin');
|
|
|
|
// ...a retry still waits, and sees the PATH once the shell answers.
|
|
const retry = module.waitForLoginShellPath();
|
|
resolveShell('/opt/homebrew/bin');
|
|
await expect(retry).resolves.toBe(true);
|
|
expect(process.env.PATH).toBe('/opt/homebrew/bin');
|
|
});
|
|
|
|
it('lets spawns go immediately on Windows', async () => {
|
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
await expect(loadModule().waitForLoginShellPath(60_000)).resolves.toBe(
|
|
true
|
|
);
|
|
});
|
|
|
|
it('falls back to the paths fix-path used when the shell reports none', async () => {
|
|
await loadModule().hydratePathFromLoginShell(async () => undefined);
|
|
|
|
expect(process.env.PATH).toBe(
|
|
'./node_modules/.bin:/.nodebrew/current/bin:/usr/local/bin:/usr/bin:/bin'
|
|
);
|
|
});
|
|
|
|
it('runs once, and not at all on Windows', async () => {
|
|
const read = jest.fn(async () => '/custom/bin');
|
|
const module = loadModule();
|
|
module.scheduleDeferredFixPath(read);
|
|
module.scheduleDeferredFixPath(read);
|
|
await flushScheduled();
|
|
expect(read).toHaveBeenCalledTimes(1);
|
|
|
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
const windowsRead = jest.fn(async () => '/custom/bin');
|
|
loadModule().scheduleDeferredFixPath(windowsRead);
|
|
await flushScheduled();
|
|
expect(windowsRead).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('keeps the current PATH when the lookup fails', async () => {
|
|
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {
|
|
// Expected failure.
|
|
});
|
|
loadModule().scheduleDeferredFixPath(async () => {
|
|
throw new Error('shell exited');
|
|
});
|
|
await flushScheduled();
|
|
|
|
expect(process.env.PATH).toBe('/usr/bin:/bin');
|
|
expect(warn).toHaveBeenCalledWith(
|
|
'Login shell PATH lookup failed:',
|
|
expect.any(Error)
|
|
);
|
|
warn.mockRestore();
|
|
});
|
|
});
|