fix(shell): bound PATH repair fallbacks#831
fix(shell): bound PATH repair fallbacks#831Chrono-byte wants to merge 3 commits intopingdotgg:mainfrom
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR refactors PATH “repair” during startup on macOS and Linux by centralizing login-shell PATH resolution behind a shared resolver with a bounded set of shell candidates and invocation modes, to avoid excessive synchronous probing.
Changes:
- Add bounded shell candidate selection (
defaultShellCandidates) and a multi-shell resolver (resolvePathFromLoginShells) inpackages/shared. - Update desktop + server startup PATH repair to use the shared resolver and enable Linux support.
- Expand unit tests for the new fallback behavior and candidate selection.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| packages/shared/src/shell.ts | Adds login-shell PATH resolution fallbacks and shared candidate/resolution helpers. |
| packages/shared/src/shell.test.ts | Adds tests for the new fallback behavior and default candidate sets. |
| apps/server/src/os-jank.ts | Switches server PATH repair to the shared bounded resolver; enables Linux. |
| apps/desktop/src/fixPath.ts | Switches desktop PATH repair to the shared bounded resolver; enables Linux. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/shared/src/shell.ts
Outdated
| type ShellPathResolveErrorReporter = (shell: string, error: unknown) => void; | ||
|
|
||
| const defaultShellPathErrorReporter: ShellPathResolveErrorReporter | undefined = | ||
| process.env.T3CODE_DEBUG_SHELL_PATH === "1" | ||
| ? (shell, error) => { | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| console.warn(`[shell] PATH resolution failed for ${shell}: ${message}`); | ||
| } | ||
| : undefined; | ||
|
|
||
| export function resolvePathFromLoginShells( | ||
| shells: ReadonlyArray<string>, | ||
| execFile: ExecFileSyncLike = execFileSync, | ||
| onError: ShellPathResolveErrorReporter | undefined = defaultShellPathErrorReporter, | ||
| ): string | undefined { | ||
| for (const shell of shells) { | ||
| try { | ||
| const result = readPathFromLoginShell(shell, execFile); | ||
| if (result) { | ||
| return result; | ||
| } | ||
| } catch (error) { | ||
| onError?.(shell, error); | ||
| // Try next shell candidate. | ||
| } | ||
| } |
There was a problem hiding this comment.
resolvePathFromLoginShells's onError hook (and the T3CODE_DEBUG_SHELL_PATH logging) is effectively unreachable for normal shell failures because readPathFromLoginShell swallows all execFile errors internally. If the intent is to report per-shell failures, either let errors bubble up from readPathFromLoginShell (after trying its arg fallbacks) or invoke the reporter from within readPathFromLoginShell when each mode fails.
| export function fixPath(): void { | ||
| if (process.platform !== "darwin") return; | ||
| if (process.platform !== "darwin" && process.platform !== "linux") return; | ||
|
|
||
| try { | ||
| const shell = process.env.SHELL ?? "/bin/zsh"; | ||
| const result = readPathFromLoginShell(shell); | ||
| if (result) { | ||
| process.env.PATH = result; | ||
| } | ||
| } catch { | ||
| // Silently ignore — keep default PATH | ||
| const shells = defaultShellCandidates(); | ||
|
|
||
| const resolvedPath = resolvePathFromLoginShells(shells); | ||
| if (resolvedPath) { | ||
| process.env.PATH = resolvedPath; | ||
| } |
There was a problem hiding this comment.
On darwin/linux this runs a synchronous PATH repair that can block startup for up to timeout * argModes * shellCandidates (currently 5000ms * 2 * up to 3 = 30s on macOS; 20s on Linux). If the PR goal is to cap worst-case startup cost, consider lowering the per-invocation timeout and/or adding an overall deadline / gating the repair to cases where PATH looks incomplete.
apps/server/src/os-jank.ts
Outdated
|
|
||
| const resolvedPath = resolvePathFromLoginShells(shells); |
There was a problem hiding this comment.
defaultShellCandidates() includes process.env.SHELL, which means on Linux this server startup path repair may execute whatever binary is pointed to by SHELL. If this code can run in environments where SHELL is not trusted/controlled (e.g., service managers, container entrypoints), consider ignoring process.env.SHELL here or validating/allowlisting shell paths before executing them.
| const resolvedPath = resolvePathFromLoginShells(shells); | |
| const allowedShells = new Set<string>([ | |
| "/bin/bash", | |
| "/usr/bin/bash", | |
| "/bin/zsh", | |
| "/usr/bin/zsh", | |
| "/bin/sh", | |
| "/usr/bin/sh", | |
| "/bin/dash", | |
| "/usr/bin/dash", | |
| "/bin/fish", | |
| "/usr/bin/fish", | |
| ]); | |
| const filteredShells = shells.filter((shell) => allowedShells.has(shell)); | |
| if (filteredShells.length === 0) return; | |
| const resolvedPath = resolvePathFromLoginShells(filteredShells); |
| export function fixPath(): void { | ||
| if (process.platform !== "darwin") return; | ||
| if (process.platform !== "darwin" && process.platform !== "linux") return; | ||
|
|
||
| try { | ||
| const shell = process.env.SHELL ?? "/bin/zsh"; | ||
| const result = readPathFromLoginShell(shell); | ||
| if (result) { | ||
| process.env.PATH = result; | ||
| } | ||
| } catch { | ||
| // Keep inherited PATH if shell lookup fails. | ||
| const result = resolvePathFromLoginShells(defaultShellCandidates()); | ||
| if (result) { | ||
| process.env.PATH = result; | ||
| } |
There was a problem hiding this comment.
This runs a synchronous PATH repair on Linux as well as macOS; with the current 5s timeout and two invocation modes per shell, Electron main startup can block for up to ~30s in the worst case. Consider reducing the timeout and/or short-circuiting when PATH is already populated enough to resolve required binaries.
packages/shared/src/shell.test.ts
Outdated
| it("returns the first resolved PATH from the provided shells", () => { | ||
| const execFile = vi.fn< | ||
| ( | ||
| file: string, | ||
| args: ReadonlyArray<string>, | ||
| options: { encoding: "utf8"; timeout: number }, | ||
| ) => string | ||
| >((file) => { | ||
| if (file === "/bin/zsh") { | ||
| throw new Error("zsh unavailable"); | ||
| } | ||
| return "__T3CODE_PATH_START__\n/a:/b\n__T3CODE_PATH_END__\n"; | ||
| }); | ||
|
|
||
| const result = resolvePathFromLoginShells(["/bin/zsh", "/bin/bash"], execFile); | ||
| expect(result).toBe("/a:/b"); | ||
| expect(execFile).toHaveBeenCalledTimes(2); | ||
| }); |
There was a problem hiding this comment.
The resolvePathFromLoginShells test undercounts how many execFile calls occur. readPathFromLoginShell now tries both -ilc and -lc per shell candidate, so a shell that always throws (like /bin/zsh in this mock) will be invoked twice before moving on. Update the expectation (or the mock) so the call count matches the two-mode fallback behavior.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
What Changed
Tightened desktop/server PATH repair on macOS and Linux by moving recovery through a shared resolver with bounded fallbacks.
Why
Packaged launches can start with an incomplete PATH, which breaks downstream process resolution. We need to recover a login-shell PATH, but doing that by probing too many shells synchronously can stall startup. This change keeps the recovery behavior while capping worst-case startup cost.
Note
Bound PATH repair fallbacks with multi-shell candidates and a platform gate
fixPathandos-jank.tswith a shared, cross-platform flow from@t3tools/shared/shell.shouldRepairPathto gate repair by platform (darwin/linux only) and PATH contents (e.g. missing/opt/homebrew/binon macOS).resolvePathFromLoginShellswhich iteratesdefaultShellCandidates()within a 2,000ms global deadline, falling back from-ilcto-lcper shell at 750ms each.Macroscope summarized e488a7f.