diff --git a/src/main/runtime/rpc/methods/host-capabilities.test.ts b/src/main/runtime/rpc/methods/host-capabilities.test.ts index 64ff48e69a2..8e2f15cd25c 100644 --- a/src/main/runtime/rpc/methods/host-capabilities.test.ts +++ b/src/main/runtime/rpc/methods/host-capabilities.test.ts @@ -3,15 +3,31 @@ import { RpcDispatcher } from '../dispatcher' import type { RpcRequest } from '../core' import type { OrcaRuntimeService } from '../../orca-runtime' -const { isPwshAvailable, isWslAvailable, listWslDistros, isGitBashAvailable } = vi.hoisted(() => ({ +const { + isPwshAvailable, + isPwshAvailableAsync, + isWslAvailable, + isWslAvailableAsync, + listWslDistros, + listWslDistrosAsync, + isGitBashAvailable +} = vi.hoisted(() => ({ isPwshAvailable: vi.fn(), + isPwshAvailableAsync: vi.fn(), isWslAvailable: vi.fn(), + isWslAvailableAsync: vi.fn(), listWslDistros: vi.fn(), + listWslDistrosAsync: vi.fn(), isGitBashAvailable: vi.fn() })) -vi.mock('../../../pwsh', () => ({ isPwshAvailable })) -vi.mock('../../../wsl', () => ({ isWslAvailable, listWslDistros })) +vi.mock('../../../pwsh', () => ({ isPwshAvailable, isPwshAvailableAsync })) +vi.mock('../../../wsl', () => ({ + isWslAvailable, + isWslAvailableAsync, + listWslDistros, + listWslDistrosAsync +})) vi.mock('../../../git-bash', () => ({ isGitBashAvailable })) import { HOST_CAPABILITY_METHODS } from './host-capabilities' @@ -23,15 +39,18 @@ function makeRequest(method: string, params?: unknown): RpcRequest { describe('host capability RPC methods', () => { beforeEach(() => { isPwshAvailable.mockReset() + isPwshAvailableAsync.mockReset() isWslAvailable.mockReset() + isWslAvailableAsync.mockReset() listWslDistros.mockReset() + listWslDistrosAsync.mockReset() isGitBashAvailable.mockReset() }) it('reports Windows shell capability probes through explicit methods', async () => { - isPwshAvailable.mockReturnValue(true) - isWslAvailable.mockReturnValue(true) - listWslDistros.mockReturnValue(['Ubuntu']) + isPwshAvailableAsync.mockResolvedValue(true) + isWslAvailableAsync.mockResolvedValue(true) + listWslDistrosAsync.mockResolvedValue(['Ubuntu']) isGitBashAvailable.mockReturnValue(true) const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService const dispatcher = new RpcDispatcher({ runtime, methods: HOST_CAPABILITY_METHODS }) @@ -55,4 +74,25 @@ describe('host capability RPC methods', () => { result: true }) }) + + // Why: web/mobile clients reach these over the relay, so a sync probe would stall the + // desktop main event loop on execFileSync wsl.exe/pwsh.exe for up to 5s per call. + it('answers the Windows shell capability methods without a blocking spawn', async () => { + isPwshAvailableAsync.mockResolvedValue(false) + isWslAvailableAsync.mockResolvedValue(false) + listWslDistrosAsync.mockResolvedValue([]) + const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService + const dispatcher = new RpcDispatcher({ runtime, methods: HOST_CAPABILITY_METHODS }) + + await dispatcher.dispatch(makeRequest('host.wsl.isAvailable')) + await dispatcher.dispatch(makeRequest('host.wsl.listDistros')) + await dispatcher.dispatch(makeRequest('host.pwsh.isAvailable')) + + expect(isWslAvailableAsync).toHaveBeenCalledTimes(1) + expect(listWslDistrosAsync).toHaveBeenCalledTimes(1) + expect(isPwshAvailableAsync).toHaveBeenCalledTimes(1) + expect(isWslAvailable).not.toHaveBeenCalled() + expect(listWslDistros).not.toHaveBeenCalled() + expect(isPwshAvailable).not.toHaveBeenCalled() + }) }) diff --git a/src/main/runtime/rpc/methods/host-capabilities.ts b/src/main/runtime/rpc/methods/host-capabilities.ts index be5e13dc2f0..afa1af88474 100644 --- a/src/main/runtime/rpc/methods/host-capabilities.ts +++ b/src/main/runtime/rpc/methods/host-capabilities.ts @@ -1,6 +1,6 @@ import { defineMethod, type RpcMethod } from '../core' -import { isPwshAvailable } from '../../../pwsh' -import { isWslAvailable, listWslDistros } from '../../../wsl' +import { isPwshAvailableAsync } from '../../../pwsh' +import { isWslAvailableAsync, listWslDistrosAsync } from '../../../wsl' import { isGitBashAvailable } from '../../../git-bash' export const HOST_CAPABILITY_METHODS: RpcMethod[] = [ @@ -9,20 +9,22 @@ export const HOST_CAPABILITY_METHODS: RpcMethod[] = [ params: null, handler: async () => ({ platform: process.platform }) }), + // Why: paired web/mobile clients route capability reads here, so a sync probe would + // execFileSync wsl.exe/pwsh.exe on this host's main event loop for up to 5s per call. defineMethod({ name: 'host.wsl.isAvailable', params: null, - handler: async () => isWslAvailable() + handler: async () => isWslAvailableAsync() }), defineMethod({ name: 'host.wsl.listDistros', params: null, - handler: async () => listWslDistros() + handler: async () => listWslDistrosAsync() }), defineMethod({ name: 'host.pwsh.isAvailable', params: null, - handler: async () => isPwshAvailable() + handler: async () => isPwshAvailableAsync() }), defineMethod({ name: 'host.gitBash.isAvailable', diff --git a/src/relay/preflight-handler.test.ts b/src/relay/preflight-handler.test.ts index 80bc2ba490a..06886180624 100644 --- a/src/relay/preflight-handler.test.ts +++ b/src/relay/preflight-handler.test.ts @@ -5,13 +5,17 @@ const { execFileAsyncMock } = vi.hoisted(() => ({ execFileAsyncMock: vi.fn() })) -const { isPwshAvailableMock, isWslAvailableMock, listWslDistrosMock, isGitBashAvailableMock } = - vi.hoisted(() => ({ - isPwshAvailableMock: vi.fn(), - isWslAvailableMock: vi.fn(), - listWslDistrosMock: vi.fn(), - isGitBashAvailableMock: vi.fn() - })) +const { + isPwshAvailableAsyncMock, + isWslAvailableAsyncMock, + listWslDistrosAsyncMock, + isGitBashAvailableMock +} = vi.hoisted(() => ({ + isPwshAvailableAsyncMock: vi.fn(), + isWslAvailableAsyncMock: vi.fn(), + listWslDistrosAsyncMock: vi.fn(), + isGitBashAvailableMock: vi.fn() +})) vi.mock('child_process', () => { const execFileWithPromisify = Object.assign(vi.fn(), { @@ -20,10 +24,10 @@ vi.mock('child_process', () => { return { execFile: execFileWithPromisify } }) -vi.mock('../main/pwsh', () => ({ isPwshAvailable: isPwshAvailableMock })) +vi.mock('../main/pwsh', () => ({ isPwshAvailableAsync: isPwshAvailableAsyncMock })) vi.mock('../main/wsl', () => ({ - isWslAvailable: isWslAvailableMock, - listWslDistros: listWslDistrosMock + isWslAvailableAsync: isWslAvailableAsyncMock, + listWslDistrosAsync: listWslDistrosAsyncMock })) vi.mock('../main/git-bash', () => ({ isGitBashAvailable: isGitBashAvailableMock })) @@ -61,9 +65,9 @@ function fishLookupArgs(command: string): string[] { beforeEach(() => { execFileAsyncMock.mockReset() - isPwshAvailableMock.mockReset() - isWslAvailableMock.mockReset() - listWslDistrosMock.mockReset() + isPwshAvailableAsyncMock.mockReset() + isWslAvailableAsyncMock.mockReset() + listWslDistrosAsyncMock.mockReset() isGitBashAvailableMock.mockReset() }) @@ -319,9 +323,9 @@ describe('PreflightHandler', () => { configurable: true, value: 'win32' }) - isWslAvailableMock.mockReturnValue(true) - listWslDistrosMock.mockReturnValue(['Ubuntu']) - isPwshAvailableMock.mockReturnValue(true) + isWslAvailableAsyncMock.mockResolvedValue(true) + listWslDistrosAsyncMock.mockResolvedValue(['Ubuntu']) + isPwshAvailableAsyncMock.mockResolvedValue(true) isGitBashAvailableMock.mockReturnValue(true) const requestHandlers = new Map) => Promise>() diff --git a/src/relay/preflight-handler.ts b/src/relay/preflight-handler.ts index 101c796137c..a703b26847e 100644 --- a/src/relay/preflight-handler.ts +++ b/src/relay/preflight-handler.ts @@ -4,8 +4,8 @@ import { promisify } from 'node:util' import path, { win32 } from 'node:path' import type { RelayDispatcher } from './dispatcher' import { buildRelayCommandEnv } from './relay-command-env' -import { isPwshAvailable } from '../main/pwsh' -import { isWslAvailable, listWslDistros } from '../main/wsl' +import { isPwshAvailableAsync } from '../main/pwsh' +import { isWslAvailableAsync, listWslDistrosAsync } from '../main/wsl' import { isGitBashAvailable } from '../main/git-bash' import { buildPosixCommandPathLookupScript } from '../shared/posix-command-path-lookup' @@ -101,11 +101,11 @@ export class PreflightHandler { hostPlatform: NodeJS.Platform | null }> { const [wslAvailable, pwshAvailable, gitBashAvailable] = await Promise.all([ - Promise.resolve(isWslAvailable()).catch(() => false), - Promise.resolve(isPwshAvailable()).catch(() => false), + isWslAvailableAsync().catch(() => false), + isPwshAvailableAsync().catch(() => false), Promise.resolve(isGitBashAvailable()).catch(() => false) ]) - const wslDistros = wslAvailable ? await Promise.resolve(listWslDistros()).catch(() => []) : [] + const wslDistros = wslAvailable ? await listWslDistrosAsync().catch(() => []) : [] return { wslAvailable, wslDistros,