mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 00:03:08 +00:00
fix(startup): use non-interactive login shell for PATH probe (#5657)
The startup PATH probe spawned the user's login shell with -ilc. The interactive (-i) flag forces sourcing of interactive rc init (compinit, oh-my-zsh, completion generators, nvm/sdkman) that forks many nested execs. On managed macOS machines with Endpoint Security agents (Jamf Protect / CrowdStrike), each nested exec awaits an ES_EVENT_TYPE_AUTH_EXEC verdict; a stalled verdict wedges the spawn in uninterruptible kernel sleep (U state) that SIGKILL cannot reap, hanging Orca startup with no window until reboot. Switch to login-but-non-interactive (-lc). PATH lives in login profiles and unguarded rc exports, both still sourced under -lc, so the captured PATH stays complete while the subprocess-spawning interactive init that triggers the AUTH_EXEC wedge is skipped. The 5s timeout + SIGKILL is kept as defense-in-depth (it cannot reap a U-state process, so avoiding the wedge via -lc is the real fix). Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -133,6 +133,46 @@ describe('hydrateShellPath', () => {
|
||||
expect(result).toEqual({ segments: [], ok: false, failureReason: 'empty_path' })
|
||||
})
|
||||
|
||||
// Why: the `-i` (interactive) flag forces sourcing of interactive rc init
|
||||
// (compinit, oh-my-zsh, completion generators) that forks nested execs and
|
||||
// wedges startup in uninterruptible sleep under macOS Endpoint Security
|
||||
// agents (#5657). The PATH probe must spawn login-but-non-interactive (`-lc`).
|
||||
it('spawns the shell login-but-non-interactive (-lc, never -ilc) for the PATH probe', async () => {
|
||||
const proc = createMockShellProcess()
|
||||
spawnMock.mockReturnValue(proc)
|
||||
|
||||
const resultPromise = hydrateShellPath({ shellOverride: '/bin/zsh', force: true })
|
||||
|
||||
expect(spawnMock).toHaveBeenCalledTimes(1)
|
||||
const [spawnedShell, spawnArgs] = spawnMock.mock.calls[0]
|
||||
expect(spawnedShell).toBe('/bin/zsh')
|
||||
expect(spawnArgs[0]).toBe('-lc')
|
||||
expect(spawnArgs).not.toContain('-ilc')
|
||||
|
||||
// Emit a PATH containing version-manager dirs and confirm it still parses,
|
||||
// proving login profiles + unguarded rc exports remain captured under -lc.
|
||||
const path = [
|
||||
'/Users/tester/.cargo/bin',
|
||||
'/Users/tester/.pyenv/shims',
|
||||
'/Users/tester/.volta/bin',
|
||||
'/opt/homebrew/bin'
|
||||
].join(delimiter)
|
||||
proc.stdout.emit('data', Buffer.from(`__ORCA_SHELL_PATH__${path}__ORCA_SHELL_PATH__`))
|
||||
proc.emit('close')
|
||||
|
||||
const result = await resultPromise
|
||||
expect(result).toEqual({
|
||||
segments: [
|
||||
'/Users/tester/.cargo/bin',
|
||||
'/Users/tester/.pyenv/shims',
|
||||
'/Users/tester/.volta/bin',
|
||||
'/opt/homebrew/bin'
|
||||
],
|
||||
ok: true,
|
||||
failureReason: 'none'
|
||||
})
|
||||
})
|
||||
|
||||
it('cleans up shell listeners when hydration times out', async () => {
|
||||
vi.useFakeTimers()
|
||||
const proc = createMockShellProcess()
|
||||
|
||||
@@ -81,14 +81,21 @@ function spawnShellAndReadPath(shell: string): Promise<HydrationResult> {
|
||||
return new Promise((resolve) => {
|
||||
// Why: printing $PATH between delimiters is resilient to rc-file banners,
|
||||
// MOTDs, and `echo` invocations that shells like fish print unprompted.
|
||||
// `-ilc` runs the shell as a login+interactive so both .profile/.zprofile
|
||||
// and .bashrc/.zshrc are sourced — matches what `which` in Terminal sees.
|
||||
// Why: login-but-non-interactive (`-lc`, not `-ilc`). PATH lives in login
|
||||
// profiles (.zprofile/.zlogin/.profile/.bash_profile) and unguarded rc
|
||||
// exports, all still sourced under `-lc`, so the captured PATH stays
|
||||
// complete. The dropped `-i` flag is what matters: interactive init
|
||||
// (compinit, oh-my-zsh, `source <(tool completion ...)`, nvm/sdkman) forks
|
||||
// many nested execs that, under macOS Endpoint Security agents (Jamf
|
||||
// Protect / CrowdStrike), each await an ES_EVENT_TYPE_AUTH_EXEC verdict; a
|
||||
// stalled verdict wedges the spawn in uninterruptible kernel sleep (`U`),
|
||||
// which SIGKILL cannot reap — hanging startup until reboot (#5657).
|
||||
const command = `printf '%s' '${DELIMITER}'; printf '%s' "$PATH"; printf '%s' '${DELIMITER}'`
|
||||
let finished = false
|
||||
let stdout = ''
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const child = spawn(shell, ['-ilc', command], {
|
||||
const child = spawn(shell, ['-lc', command], {
|
||||
// Why: inherit current env so the shell sees the same baseline, then let
|
||||
// it layer its own rc files on top. Do NOT forward stdio — some shells
|
||||
// (oh-my-zsh setups, powerlevel10k) print a lot to stderr on startup,
|
||||
@@ -119,7 +126,9 @@ function spawnShellAndReadPath(shell: string): Promise<HydrationResult> {
|
||||
timer = setTimeout(() => {
|
||||
// Why: slow rc files (corporate env setup, nvm eager init) can exceed
|
||||
// our budget. Kill the shell and fall back to process.env rather than
|
||||
// blocking the Agents pane indefinitely.
|
||||
// blocking the Agents pane indefinitely. Defense-in-depth only — SIGKILL
|
||||
// cannot reap a child stuck in `U` (uninterruptible) state, so the real
|
||||
// fix for the Endpoint Security wedge is the non-interactive `-lc` above.
|
||||
try {
|
||||
child.kill('SIGKILL')
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user