mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 20:16:15 -08:00
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>
This commit is contained in:
1 parent
92127a81ac
commit
b0a7554dc0
6 files changed
+110
-22
No files matched your search
@@ -9,6 +9,7 @@ const mockEmbeddedMpvService = {
|
||||
prepareAddon: jest.fn(),
|
||||
getSupport: jest.fn(),
|
||||
willProbeLinuxMpvExecutable: jest.fn(() => false),
|
||||
forgetMissingLinuxMpvExecutable: jest.fn(),
|
||||
setPaused: jest.fn(),
|
||||
};
|
||||
const mockSessionOptions = {
|
||||
@@ -23,9 +24,14 @@ jest.mock('../services/embedded-mpv-native.service', () => ({
|
||||
jest.mock('../services/embedded-mpv-session-options', () => ({
|
||||
readEmbeddedMpvSessionOptions: () => mockSessionOptions,
|
||||
}));
|
||||
const mockWaitForLoginShellPath = jest.fn(() => Promise.resolve());
|
||||
const mockWaitForLoginShellPath = jest.fn(() => Promise.resolve(true));
|
||||
let settleLookup: () => void = () => undefined;
|
||||
const mockLookupSettled = new Promise<void>((resolve) => {
|
||||
settleLookup = resolve;
|
||||
});
|
||||
jest.mock('../startup/login-shell-path', () => ({
|
||||
waitForLoginShellPath: () => mockWaitForLoginShellPath(),
|
||||
whenLoginShellPathSettled: () => mockLookupSettled,
|
||||
}));
|
||||
|
||||
import { ipcMain } from 'electron';
|
||||
@@ -77,9 +83,9 @@ describe('EmbeddedMpvEvents IPC handlers', () => {
|
||||
mockEmbeddedMpvService.willProbeLinuxMpvExecutable.mockReturnValue(
|
||||
true
|
||||
);
|
||||
let settle: () => void = () => undefined;
|
||||
let settle: (settled: boolean) => void = () => undefined;
|
||||
mockWaitForLoginShellPath.mockReturnValueOnce(
|
||||
new Promise<void>((resolve) => {
|
||||
new Promise<boolean>((resolve) => {
|
||||
settle = resolve;
|
||||
})
|
||||
);
|
||||
@@ -99,11 +105,37 @@ describe('EmbeddedMpvEvents IPC handlers', () => {
|
||||
mockEmbeddedMpvService.prepareAddon
|
||||
).not.toHaveBeenCalled();
|
||||
|
||||
settle();
|
||||
settle(true);
|
||||
await expect(support).resolves.toEqual({ supported: true });
|
||||
expect(
|
||||
mockEmbeddedMpvService.forgetMissingLinuxMpvExecutable
|
||||
).not.toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
|
||||
it('re-probes a missing mpv once a lookup that ran out finally answers', async () => {
|
||||
mockEmbeddedMpvService.willProbeLinuxMpvExecutable.mockReturnValue(
|
||||
true
|
||||
);
|
||||
mockWaitForLoginShellPath.mockResolvedValueOnce(false);
|
||||
mockEmbeddedMpvService.getSupport.mockReturnValue({
|
||||
supported: false,
|
||||
});
|
||||
|
||||
await expect(
|
||||
getIpcMainHandler(EMBEDDED_MPV_SUPPORT)({})
|
||||
).resolves.toEqual({ supported: false });
|
||||
expect(
|
||||
mockEmbeddedMpvService.forgetMissingLinuxMpvExecutable
|
||||
).not.toHaveBeenCalled();
|
||||
|
||||
settleLookup();
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(
|
||||
mockEmbeddedMpvService.forgetMissingLinuxMpvExecutable
|
||||
).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not wait when no probe runs, nor for session calls', async () => {
|
||||
mockEmbeddedMpvService.getSupport.mockReturnValue({
|
||||
supported: true,
|
||||
|
||||
@@ -33,7 +33,10 @@ import {
|
||||
embeddedMpvNativeService,
|
||||
} from '../services/embedded-mpv-native.service';
|
||||
import { readEmbeddedMpvSessionOptions } from '../services/embedded-mpv-session-options';
|
||||
import { waitForLoginShellPath } from '../startup/login-shell-path';
|
||||
import {
|
||||
waitForLoginShellPath,
|
||||
whenLoginShellPathSettled,
|
||||
} from '../startup/login-shell-path';
|
||||
|
||||
export default class EmbeddedMpvEvents {
|
||||
static bootstrapEmbeddedMpvEvents(): Electron.IpcMain {
|
||||
@@ -71,10 +74,19 @@ function handleEmbeddedMpv<Args extends unknown[]>(
|
||||
* login shell PATH. Only that probe waits; nothing else here spawns by name.
|
||||
*/
|
||||
async function afterLoginShellPathIfProbing<T>(check: () => T): Promise<T> {
|
||||
if (getService().willProbeLinuxMpvExecutable()) {
|
||||
await waitForLoginShellPath();
|
||||
if (
|
||||
!getService().willProbeLinuxMpvExecutable() ||
|
||||
(await waitForLoginShellPath())
|
||||
) {
|
||||
return check();
|
||||
}
|
||||
return check();
|
||||
// The lookup ran out of budget, so this probe sees the inherited PATH.
|
||||
// Once the shell does answer, a "missing" result is probed again.
|
||||
const result = check();
|
||||
void whenLoginShellPathSettled().then(() =>
|
||||
getService().forgetMissingLinuxMpvExecutable()
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
handleEmbeddedMpv(EMBEDDED_MPV_SUPPORT, () =>
|
||||
|
||||
@@ -394,6 +394,25 @@ describe('EmbeddedMpvNativeService power blocker', () => {
|
||||
expect(service.willProbeLinuxMpvExecutable()).toBe(false);
|
||||
});
|
||||
|
||||
it('probes a missing mpv again after it is forgotten, but keeps a found one', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
process.env.DISPLAY = ':0';
|
||||
delete process.env.WAYLAND_DISPLAY;
|
||||
mockSpawnSync.mockClear();
|
||||
mockSpawnSync.mockReturnValue({ status: 1 });
|
||||
mockRuntimeUsable();
|
||||
|
||||
expect(service.getSupport().supported).toBe(false);
|
||||
service.forgetMissingLinuxMpvExecutable();
|
||||
expect(service.willProbeLinuxMpvExecutable()).toBe(true);
|
||||
|
||||
mockSpawnSync.mockReturnValue({ status: 0 });
|
||||
service.getSupport();
|
||||
expect(mockSpawnSync).toHaveBeenCalledTimes(2);
|
||||
service.forgetMissingLinuxMpvExecutable();
|
||||
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';
|
||||
|
||||
@@ -393,6 +393,17 @@ export class EmbeddedMpvNativeService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a cached "mpv missing" result, so the next support check probes
|
||||
* again (a found mpv stays cached). For a probe that ran before the
|
||||
* login shell PATH arrived.
|
||||
*/
|
||||
forgetMissingLinuxMpvExecutable(): void {
|
||||
if (this.cachedLinuxMpvExecutableReason) {
|
||||
this.cachedLinuxMpvExecutableReason = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getSupport(): EmbeddedMpvSupport {
|
||||
if (!SUPPORTED_EMBEDDED_MPV_PLATFORMS.has(process.platform)) {
|
||||
return {
|
||||
|
||||
@@ -84,12 +84,10 @@ describe('login shell PATH', () => {
|
||||
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.toBeUndefined();
|
||||
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.toBeUndefined();
|
||||
await expect(stuck.waitForLoginShellPath(60_000)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('lets a launch retried within the budget wait for the PATH again', async () => {
|
||||
@@ -110,15 +108,15 @@ describe('login shell PATH', () => {
|
||||
// ...a retry still waits, and sees the PATH once the shell answers.
|
||||
const retry = module.waitForLoginShellPath();
|
||||
resolveShell('/opt/homebrew/bin');
|
||||
await retry;
|
||||
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.toBeUndefined();
|
||||
await expect(loadModule().waitForLoginShellPath(60_000)).resolves.toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to the paths fix-path used when the shell reports none', async () => {
|
||||
|
||||
@@ -39,30 +39,46 @@ const loginShellPathSettled: Promise<void> =
|
||||
export const LOGIN_SHELL_PATH_WAIT_LIMIT_MS = 10_000;
|
||||
let loginShellPathDeadline: number | null = null;
|
||||
|
||||
let loginShellPathHasSettled = process.platform === 'win32';
|
||||
const settledOutcome = loginShellPathSettled.then(() => {
|
||||
loginShellPathHasSettled = true;
|
||||
return true;
|
||||
});
|
||||
|
||||
/**
|
||||
* Resolves once the login shell PATH lookup has finished (successfully or
|
||||
* not), or when its budget is spent; immediately on Windows. Before the
|
||||
* lookup is scheduled, a wait lasts at most `limitMs`.
|
||||
* lookup is scheduled, a wait lasts at most `limitMs`. Resolves to whether
|
||||
* the lookup had finished: false means the caller runs on the inherited
|
||||
* PATH and should not keep a negative result.
|
||||
*/
|
||||
export function waitForLoginShellPath(
|
||||
limitMs = LOGIN_SHELL_PATH_WAIT_LIMIT_MS
|
||||
): Promise<void> {
|
||||
): Promise<boolean> {
|
||||
if (loginShellPathHasSettled) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
const remainingMs =
|
||||
loginShellPathDeadline === null
|
||||
? limitMs
|
||||
: Math.min(limitMs, loginShellPathDeadline - Date.now());
|
||||
if (remainingMs <= 0) {
|
||||
return Promise.resolve();
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
const limit = new Promise<void>((resolve) => {
|
||||
timer = setTimeout(resolve, remainingMs);
|
||||
const limit = new Promise<boolean>((resolve) => {
|
||||
timer = setTimeout(() => resolve(false), remainingMs);
|
||||
});
|
||||
return Promise.race([loginShellPathSettled, limit]).finally(() =>
|
||||
return Promise.race([settledOutcome, limit]).finally(() =>
|
||||
clearTimeout(timer)
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolves when the lookup finishes, however long it takes. */
|
||||
export function whenLoginShellPathSettled(): Promise<void> {
|
||||
return loginShellPathSettled;
|
||||
}
|
||||
|
||||
export type ReadLoginShellPath = () => Promise<string | undefined>;
|
||||
|
||||
const readLoginShellPath: ReadLoginShellPath = async () => {
|
||||
|
||||
Reference in new issue
Block a user