From 5af784a77e8de99e1fd1a22f0e276855af1ca09b Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:59:34 -0700 Subject: [PATCH] fix(preflight): ship worker and preserve CLI liveness --- .../command-path-directory-worker-client.ts | 2 +- ...nd-path-directory-worker.test-fixtures.mjs | 41 +++++++++++++++++-- .../ipc/command-path-directory-worker.test.ts | 38 +++++++++++++++++ src/main/ipc/command-path-directory.ts | 29 ++++++++----- src/main/ipc/command-path-resolver.test.ts | 22 +++++++++- src/shared/orcad-artifacts.ts | 1 + 6 files changed, 116 insertions(+), 17 deletions(-) diff --git a/src/main/ipc/command-path-directory-worker-client.ts b/src/main/ipc/command-path-directory-worker-client.ts index 2cc8fbee971..63ad146799d 100644 --- a/src/main/ipc/command-path-directory-worker-client.ts +++ b/src/main/ipc/command-path-directory-worker-client.ts @@ -66,8 +66,8 @@ export class DetectionDirectoryWorkerClient { this.host.clearIdleTimer() const id = this.nextId++ return new Promise((resolve) => { + // Keep plain-Node CLI invocations alive until their listings settle. const timer = setTimeout(() => this.dispose(), REQUEST_TIMEOUT_MS) - timer.unref?.() this.pending.set(id, { resolve, timer }) const request: DetectionDirectoryRequest = { id, dir } try { diff --git a/src/main/ipc/command-path-directory-worker.test-fixtures.mjs b/src/main/ipc/command-path-directory-worker.test-fixtures.mjs index dd799f52cb5..e287008cd30 100644 --- a/src/main/ipc/command-path-directory-worker.test-fixtures.mjs +++ b/src/main/ipc/command-path-directory-worker.test-fixtures.mjs @@ -2,7 +2,7 @@ import fs from 'node:fs' import fsp from 'node:fs/promises' import { registerHooks, syncBuiltinESMExports } from 'node:module' import path from 'node:path' -import { workerData } from 'node:worker_threads' +import { Worker, workerData } from 'node:worker_threads' // Run unbuilt production TS in Node workers, including extensionless source imports. registerHooks({ @@ -22,7 +22,7 @@ registerHooks({ } }) -if (workerData.blockedDirectory) { +if (workerData?.blockedDirectory) { const originalOpen = fsp.opendir fsp.opendir = (directory, options) => directory === workerData.blockedDirectory @@ -31,7 +31,7 @@ if (workerData.blockedDirectory) { syncBuiltinESMExports() } -if (workerData.unknownDirectory) { +if (workerData?.unknownDirectory) { const { dir, names, counters: buffer, holdMetadata } = workerData.unknownDirectory const counters = new Int32Array(buffer) const originalOpen = fsp.opendir @@ -77,4 +77,37 @@ if (workerData.unknownDirectory) { syncBuiltinESMExports() } -await import(workerData.entry) +if (workerData) { + await import(workerData.entry) +} else { + const { transformSync } = await import('esbuild') + registerHooks({ + load(url, context, nextLoad) { + if (url.endsWith('.ts')) { + return { + format: 'module', + source: transformSync(fs.readFileSync(new URL(url), 'utf8'), { + loader: 'ts', + format: 'esm', + target: 'esnext' + }).code, + shortCircuit: true + } + } + return nextLoad(url, context) + } + }) + const { DetectionDirectoryWorkerClient } = + await import('./command-path-directory-worker-client.ts') + const client = new DetectionDirectoryWorkerClient( + () => + new Worker(new URL(import.meta.url), { + workerData: { + entry: new URL('./command-path-directory-worker-entry.ts', import.meta.url).href + } + }) + ) + void client.read(process.argv[2]).then((names) => { + console.log(JSON.stringify(names === null ? null : [...names])) + }) +} diff --git a/src/main/ipc/command-path-directory-worker.test.ts b/src/main/ipc/command-path-directory-worker.test.ts index aa95b49eafc..7c51f6edb5d 100644 --- a/src/main/ipc/command-path-directory-worker.test.ts +++ b/src/main/ipc/command-path-directory-worker.test.ts @@ -4,6 +4,7 @@ import { tmpdir } from 'node:os' import path from 'node:path' import { pathToFileURL } from 'node:url' import { Worker } from 'node:worker_threads' +import { runProcess } from '@orca/process-host' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as DirectoryWorkerClient from './command-path-directory-worker-client' import { DetectionDirectoryWorkerClient } from './command-path-directory-worker-client' @@ -42,6 +43,18 @@ afterEach(async () => { }) describe('detection directory worker boundary', () => { + it('keeps a plain Node CLI alive for pending listings and lets it exit while idle', async () => { + await writeFile(path.join(dir, 'found'), '') + const result = await runProcess({ + program: process.execPath, + args: [fixture, dir], + env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }, + timeoutMs: 5_000 + }) + expect(result.code, result.stderr).toBe(0) + expect(result.stdout.trim()).toBe(JSON.stringify([['found', 'found']])) + }) + it('keeps real unknown-Dirent metadata off the caller and closes the bounded listing before candidate fallback', async () => { const names = Array.from({ length: 129 }, (_, index) => `unrelated-${index}`) await Promise.all(names.map((name) => writeFile(path.join(dir, name), ''))) @@ -85,6 +98,31 @@ describe('detection directory worker boundary', () => { await expect(value.read(path.join(dir, 'missing'))).resolves.toEqual(new Map()) }) + it('still finds a stable executable when an unknown Dirent disappears during iteration', async () => { + await writeFile(path.join(dir, 'found'), '') + await chmod(path.join(dir, 'found'), 0o755) + const counters = new Int32Array(new SharedArrayBuffer(5 * Int32Array.BYTES_PER_ELEMENT)) + client( + () => + new Worker(fixture, { + workerData: { + entry, + unknownDirectory: { + dir, + names: ['deleted-neighbor', 'found'], + counters: counters.buffer + } + } + }) + ) + await expect( + findCommandsOnLocalPath(['found', 'b', 'c', 'd', 'e'], { env: { PATH: dir }, cwd: dir }) + ).resolves.toEqual(new Set(['found'])) + expect(Atomics.load(counters, 0)).toBe(1) + expect(Atomics.load(counters, 1)).toBe(1) + expect(Atomics.load(counters, 2)).toBe(1) + }) + it('uses async executable probes when the worker cannot start', async () => { await writeFile(path.join(dir, 'found'), '') await chmod(path.join(dir, 'found'), 0o755) diff --git a/src/main/ipc/command-path-directory.ts b/src/main/ipc/command-path-directory.ts index 74179e8e2ed..547f10788e7 100644 --- a/src/main/ipc/command-path-directory.ts +++ b/src/main/ipc/command-path-directory.ts @@ -1,3 +1,4 @@ +import type { Dir } from 'node:fs' import { opendir } from 'node:fs/promises' const MAX_DETECTION_DIRECTORY_ENTRIES = 128 @@ -7,8 +8,22 @@ export function directoryNameKey(name: string): string { } export async function readDetectionDirectory(dir: string): Promise | null> { + let directory: Dir + try { + directory = await opendir(dir, { bufferSize: MAX_DETECTION_DIRECTORY_ENTRIES + 1 }) + } catch (error) { + if ( + error && + typeof error === 'object' && + 'code' in error && + (error.code === 'ENOENT' || error.code === 'ENOTDIR') + ) { + return new Map() + } + // Searchable-but-unreadable directories still admit stat on known names. + return null + } try { - const directory = await opendir(dir, { bufferSize: MAX_DETECTION_DIRECTORY_ENTRIES + 1 }) const names = new Map() let entries = 0 for await (const entry of directory) { @@ -23,16 +38,8 @@ export async function readDetectionDirectory(dir: string): Promise ({ + readDetectionDirectoryViaWorker: vi.fn( + (await import('./command-path-directory')).readDetectionDirectory + ) +})) + +import { readDetectionDirectoryViaWorker } from './command-path-directory-worker-client' import { findCommandsOnLocalPath, isCommandOnLocalPath, @@ -313,6 +322,17 @@ describe('findCommandsOnLocalPath', () => { ).resolves.toEqual(new Set(['mixed'])) }) + it('probes exact host filenames when the directory worker is unavailable', async () => { + vi.mocked(readDetectionDirectoryViaWorker).mockResolvedValueOnce(null) + await expect( + findCommandsOnLocalPath(['tool', 'missing'], { + platform: 'win32', + env: { Path: dir, PATHEXT: '.CMD' }, + cwd: dir + }) + ).resolves.toEqual(new Set(['tool'])) + }) + it('matches extensionless executable names checked by the agent fallback', async () => { await expect( findCommandsOnLocalPath(['literal-cli'], { diff --git a/src/shared/orcad-artifacts.ts b/src/shared/orcad-artifacts.ts index aab78185079..030919164a6 100644 --- a/src/shared/orcad-artifacts.ts +++ b/src/shared/orcad-artifacts.ts @@ -227,6 +227,7 @@ export const ORCAD_ARTIFACTS: readonly OrcadArtifact[] = [ { filename: 'daemon-entry.js' }, { filename: 'profile-state-writer-worker-entry.js' }, { filename: 'profile-state-backup-worker-entry.js' }, + { filename: 'command-path-directory-worker-entry.js' }, // Worker thread that reads other apps' SQLite (the OpenCode binder and history) off the event loop. { filename: ORCAD_FOREIGN_SQLITE_READER_ENTRY }, { filename: ORCAD_PORT_SCAN_COMMAND_WORKER_ENTRY },