Files
iptvnator/apps/electron-backend/src/app/startup/login-shell-path.ts
T
4grayandClaude Opus 5.5 92127a81ac 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>
2026-10-03 08:58:27 +02:00

108 lines
3.9 KiB
TypeScript

import { traceStartupPhase } from '../services/debug-trace';
/**
* Update process.env.PATH from the user's interactive login shell so that
* spawned external players (MPV/VLC) can be resolved by binary name when the
* app was started from Finder or a desktop launcher.
*
* The shell (`$SHELL -ilc env`) starts asynchronously. Its start-up cost
* depends on the user's shell profile and was about 1-2 s with a typical
* zsh setup; the synchronous `fix-path` used before blocked the main thread
* for all of it, right when the database worker's `ready` message and the
* renderer's first IPC calls were waiting, which delayed the first card of
* the launch journey by the same amount. The resulting PATH, and the
* fallback when the shell reports none, are the ones `fix-path` 5 produced.
*
* Runs after window creation and IPC handler registration. Idempotent:
* subsequent calls are no-ops. shell-path is imported here, on demand, so
* its module evaluation stays off the launch path as well.
*
* Handlers that spawn a binary by bare name (external players, the Linux
* `mpv --version` check of embedded MPV) await `waitForLoginShellPath()`
* first, so they see the same PATH the blocking lookup guaranteed.
*/
let loginShellPathScheduled = false;
let settleLoginShellPath: () => void = () => undefined;
const loginShellPathSettled: Promise<void> =
process.platform === 'win32'
? Promise.resolve()
: new Promise((resolve) => {
settleLoginShellPath = resolve;
});
/**
* Time budget of the lookup, counted from its start. A shell that has not
* answered by then is treated as hung: waits end at this shared deadline,
* so a broken profile cannot delay every launch by the full budget, while a
* launch retried within the budget still waits for the PATH.
*/
export const LOGIN_SHELL_PATH_WAIT_LIMIT_MS = 10_000;
let loginShellPathDeadline: number | null = null;
/**
* 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`.
*/
export function waitForLoginShellPath(
limitMs = LOGIN_SHELL_PATH_WAIT_LIMIT_MS
): Promise<void> {
const remainingMs =
loginShellPathDeadline === null
? limitMs
: Math.min(limitMs, loginShellPathDeadline - Date.now());
if (remainingMs <= 0) {
return Promise.resolve();
}
let timer: NodeJS.Timeout | undefined;
const limit = new Promise<void>((resolve) => {
timer = setTimeout(resolve, remainingMs);
});
return Promise.race([loginShellPathSettled, limit]).finally(() =>
clearTimeout(timer)
);
}
export type ReadLoginShellPath = () => Promise<string | undefined>;
const readLoginShellPath: ReadLoginShellPath = async () => {
const { shellPath } = await import('shell-path');
return shellPath();
};
export async function hydratePathFromLoginShell(
readPath: ReadLoginShellPath = readLoginShellPath
): Promise<void> {
const shellPath = await readPath();
process.env.PATH =
shellPath ||
[
'./node_modules/.bin',
'/.nodebrew/current/bin',
'/usr/local/bin',
process.env.PATH,
].join(':');
}
export function scheduleDeferredFixPath(
readPath: ReadLoginShellPath = readLoginShellPath,
budgetMs = LOGIN_SHELL_PATH_WAIT_LIMIT_MS
): void {
if (loginShellPathScheduled || process.platform === 'win32') {
return;
}
loginShellPathScheduled = true;
loginShellPathDeadline = Date.now() + budgetMs;
setImmediate(() => {
hydratePathFromLoginShell(readPath)
.then(() => {
traceStartupPhase('fix-path:done');
})
.catch((error) => {
console.warn('Login shell PATH lookup failed:', error);
})
.finally(() => settleLoginShellPath());
});
}