mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
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>
This commit is contained in:
1 parent
0589156941
commit
92127a81ac
2 files changed
+45
-16
No files matched your search
@@ -79,18 +79,41 @@ describe('login shell PATH', () => {
|
||||
resolveShell('/opt/homebrew/bin');
|
||||
await wait;
|
||||
expect(process.env.PATH).toBe('/opt/homebrew/bin');
|
||||
});
|
||||
|
||||
// A shell that never returns cannot block a launch forever.
|
||||
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));
|
||||
await expect(stuck.waitForLoginShellPath(5)).resolves.toBeUndefined();
|
||||
// Later spawns do not wait out the limit again (the test would time
|
||||
stuck.scheduleDeferredFixPath(() => new Promise(() => undefined), 20);
|
||||
await expect(stuck.waitForLoginShellPath()).resolves.toBeUndefined();
|
||||
// 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();
|
||||
});
|
||||
|
||||
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 retry;
|
||||
expect(process.env.PATH).toBe('/opt/homebrew/bin');
|
||||
});
|
||||
|
||||
it('lets spawns go immediately on Windows', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
await expect(
|
||||
|
||||
@@ -30,29 +30,33 @@ const loginShellPathSettled: Promise<void> =
|
||||
settleLoginShellPath = resolve;
|
||||
});
|
||||
|
||||
/** Upper bound for a waiting spawn when the login shell never returns. */
|
||||
/**
|
||||
* 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;
|
||||
// Set once a wait has run out: a shell that hung that long is not waited
|
||||
// for again, so one broken profile delays one launch, not every launch.
|
||||
let loginShellPathWaitExpired = false;
|
||||
let loginShellPathDeadline: number | null = null;
|
||||
|
||||
/**
|
||||
* Resolves once the login shell PATH lookup has finished (successfully or
|
||||
* not), at the latest after `limitMs`; immediately on Windows and after a
|
||||
* previous wait has run out.
|
||||
* 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> {
|
||||
if (loginShellPathWaitExpired) {
|
||||
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(() => {
|
||||
loginShellPathWaitExpired = true;
|
||||
resolve();
|
||||
}, limitMs);
|
||||
timer = setTimeout(resolve, remainingMs);
|
||||
});
|
||||
return Promise.race([loginShellPathSettled, limit]).finally(() =>
|
||||
clearTimeout(timer)
|
||||
@@ -81,13 +85,15 @@ export async function hydratePathFromLoginShell(
|
||||
}
|
||||
|
||||
export function scheduleDeferredFixPath(
|
||||
readPath: ReadLoginShellPath = readLoginShellPath
|
||||
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(() => {
|
||||
|
||||
Reference in new issue
Block a user