mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 16:03:53 +00:00
fix: resolve daemon shell before spawning
This commit is contained in:
@@ -9,10 +9,12 @@ const {
|
||||
spawnMock,
|
||||
isPwshAvailableMock,
|
||||
validateWorkingDirectoryMock,
|
||||
resolveUnixShellPathMock,
|
||||
resolveAgentForegroundProcessMock
|
||||
} = vi.hoisted(() => ({
|
||||
spawnMock: vi.fn(),
|
||||
isPwshAvailableMock: vi.fn(),
|
||||
resolveUnixShellPathMock: vi.fn((shellPath: string) => shellPath),
|
||||
resolveAgentForegroundProcessMock: vi.fn(),
|
||||
validateWorkingDirectoryMock: vi.fn((cwd: string) => {
|
||||
if (cwd.includes('definitely-missing')) {
|
||||
@@ -51,6 +53,7 @@ vi.mock('../providers/local-pty-utils', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof LocalPtyUtils>()
|
||||
return {
|
||||
...actual,
|
||||
resolveUnixShellPath: resolveUnixShellPathMock,
|
||||
validateWorkingDirectory: validateWorkingDirectoryMock
|
||||
}
|
||||
})
|
||||
@@ -116,6 +119,8 @@ describe('createPtySubprocess', () => {
|
||||
async (_pid: number, fallbackProcess: string | null) => fallbackProcess
|
||||
)
|
||||
validateWorkingDirectoryMock.mockClear()
|
||||
resolveUnixShellPathMock.mockReset()
|
||||
resolveUnixShellPathMock.mockImplementation((shellPath: string) => shellPath)
|
||||
isPwshAvailableMock.mockReturnValue(false)
|
||||
previousUserDataPath = process.env.ORCA_USER_DATA_PATH
|
||||
previousPowerlevelWizardDisable = process.env[POWERLEVEL10K_WIZARD_DISABLE_ENV]
|
||||
@@ -181,6 +186,41 @@ describe('createPtySubprocess', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('resolves a missing Unix default before spawning node-pty', () => {
|
||||
const proc = mockPtyProcess()
|
||||
spawnMock.mockReturnValue(proc)
|
||||
resolveUnixShellPathMock.mockReturnValue('/bin/sh')
|
||||
const platform = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
const previousShell = process.env.SHELL
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' })
|
||||
delete process.env.SHELL
|
||||
|
||||
try {
|
||||
createPtySubprocess({ sessionId: 'test', cols: 80, rows: 24, env: {} })
|
||||
|
||||
expect(resolveUnixShellPathMock).toHaveBeenCalledWith('/bin/zsh')
|
||||
expect(spawnMock).toHaveBeenCalledWith(
|
||||
'/bin/sh',
|
||||
['-l'],
|
||||
expect.objectContaining({ env: expect.objectContaining({ SHELL: '/bin/sh' }) })
|
||||
)
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
'[daemon/pty] Preferred shell "/bin/zsh" is unavailable, fell back to "/bin/sh"'
|
||||
)
|
||||
} finally {
|
||||
warn.mockRestore()
|
||||
if (platform) {
|
||||
Object.defineProperty(process, 'platform', platform)
|
||||
}
|
||||
if (previousShell === undefined) {
|
||||
delete process.env.SHELL
|
||||
} else {
|
||||
process.env.SHELL = previousShell
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('uses bundled ConPTY for native Windows daemon terminals', () => {
|
||||
const proc = mockPtyProcess()
|
||||
spawnMock.mockReturnValue(proc)
|
||||
|
||||
@@ -14,6 +14,7 @@ import { isValidPtySize, normalizePtySize } from './daemon-pty-size'
|
||||
import {
|
||||
ensureNodePtySpawnHelperExecutable,
|
||||
getNodePtySpawnHelperCandidates,
|
||||
resolveUnixShellPath,
|
||||
validateWorkingDirectory
|
||||
} from '../providers/local-pty-utils'
|
||||
import { wrapShellSpawnForMacosTccAttribution } from '../providers/macos-tcc-login-shell'
|
||||
@@ -728,6 +729,14 @@ export function createPtySubprocess(opts: PtySubprocessOptions): SubprocessHandl
|
||||
addOrcaWslInteropEnv(env)
|
||||
}
|
||||
} else {
|
||||
const preferredShellPath = shellPath
|
||||
shellPath = resolveUnixShellPath(shellPath)
|
||||
if (shellPath !== preferredShellPath) {
|
||||
env.SHELL = shellPath
|
||||
console.warn(
|
||||
`[daemon/pty] Preferred shell "${preferredShellPath}" is unavailable, fell back to "${shellPath}"`
|
||||
)
|
||||
}
|
||||
// Why: relay-side launch modes can ask for host defaults to stay scrubbed
|
||||
// even after environment normalization above.
|
||||
for (const key of opts.envToDelete ?? []) {
|
||||
|
||||
@@ -33,7 +33,11 @@ vi.mock('./macos-tcc-login-shell', () => ({
|
||||
wrapShellSpawnForMacosTccAttribution: wrapSpawnMock
|
||||
}))
|
||||
|
||||
import { spawnShellWithFallback, validateWorkingDirectory } from './local-pty-utils'
|
||||
import {
|
||||
resolveUnixShellPath,
|
||||
spawnShellWithFallback,
|
||||
validateWorkingDirectory
|
||||
} from './local-pty-utils'
|
||||
|
||||
const WSL_UNC_DIR = '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo'
|
||||
const NATIVE_DIR = 'C:\\Users\\jin\\repo'
|
||||
@@ -102,6 +106,40 @@ describe('validateWorkingDirectory', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveUnixShellPath', () => {
|
||||
beforeEach(() => {
|
||||
existsSyncMock.mockReset()
|
||||
accessSyncMock.mockReset()
|
||||
})
|
||||
|
||||
it('preserves non-absolute shells for execvp PATH and cwd resolution', () => {
|
||||
expect(resolveUnixShellPath('bash')).toBe('bash')
|
||||
expect(resolveUnixShellPath('./bin/custom-shell')).toBe('./bin/custom-shell')
|
||||
expect(existsSyncMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('selects an executable fallback before node-pty can fork a missing shell', () => {
|
||||
existsSyncMock.mockImplementation((path) => path === '/bin/sh')
|
||||
accessSyncMock.mockReturnValue(undefined)
|
||||
|
||||
expect(resolveUnixShellPath('/missing/zsh')).toBe('/bin/sh')
|
||||
expect(existsSyncMock.mock.calls.map(([path]) => path)).toEqual([
|
||||
'/missing/zsh',
|
||||
'/bin/zsh',
|
||||
'/bin/bash',
|
||||
'/bin/sh'
|
||||
])
|
||||
})
|
||||
|
||||
it('reports every attempted shell when no executable candidate exists', () => {
|
||||
existsSyncMock.mockReturnValue(false)
|
||||
|
||||
expect(() => resolveUnixShellPath('/missing/zsh')).toThrow(
|
||||
'No executable Unix shell found (tried: /missing/zsh, /bin/zsh, /bin/bash, /bin/sh)'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('spawnShellWithFallback macOS TCC login wrapping', () => {
|
||||
let origPlatform: PropertyDescriptor | undefined
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { basename, join } from 'node:path'
|
||||
import { basename, isAbsolute, join } from 'node:path'
|
||||
import { existsSync, accessSync, statSync, chmodSync, constants as fsConstants } from 'node:fs'
|
||||
import type * as pty from 'node-pty'
|
||||
import { isWslUncPath } from '../../shared/wsl-paths'
|
||||
@@ -7,6 +7,8 @@ import { wrapShellSpawnForMacosTccAttribution } from './macos-tcc-login-shell'
|
||||
|
||||
let didEnsureSpawnHelperExecutable = false
|
||||
|
||||
const UNIX_SHELL_FALLBACKS = ['/bin/zsh', '/bin/bash', '/bin/sh'] as const
|
||||
|
||||
function toUnpackedAsarPath(candidate: string): string {
|
||||
return candidate
|
||||
.replace(/app\.asar([/\\])/, 'app.asar.unpacked$1')
|
||||
@@ -46,6 +48,25 @@ export function getShellValidationError(shellPath: string): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an absolute Unix shell before node-pty forks. Bare commands and
|
||||
* relative paths stay untouched so execvp can resolve them against PATH or cwd.
|
||||
*/
|
||||
export function resolveUnixShellPath(shellPath: string): string {
|
||||
if (!isAbsolute(shellPath)) {
|
||||
return shellPath
|
||||
}
|
||||
const candidates = [
|
||||
shellPath,
|
||||
...UNIX_SHELL_FALLBACKS.filter((candidate) => candidate !== shellPath)
|
||||
]
|
||||
const resolved = candidates.find((candidate) => getShellValidationError(candidate) === null)
|
||||
if (resolved) {
|
||||
return resolved
|
||||
}
|
||||
throw new Error(`No executable Unix shell found (tried: ${candidates.join(', ')})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the node-pty spawn-helper binary has the executable bit set.
|
||||
*
|
||||
@@ -252,7 +273,7 @@ export function spawnShellWithFallback(params: ShellSpawnParams): ShellSpawnResu
|
||||
|
||||
// Try fallback shells on Unix
|
||||
if (process.platform !== 'win32') {
|
||||
const fallbackShells = ['/bin/zsh', '/bin/bash', '/bin/sh'].filter((s) => s !== shellPath)
|
||||
const fallbackShells = UNIX_SHELL_FALLBACKS.filter((candidate) => candidate !== shellPath)
|
||||
for (const fallback of fallbackShells) {
|
||||
if (getShellValidationError(fallback)) {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user