mirror of
https://github.com/stablyai/orca.git
synced 2026-11-05 19:47:27 +00:00
fix(preflight): ship worker and preserve CLI liveness
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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]))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<Map<string, string> | 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<string, string>()
|
||||
let entries = 0
|
||||
for await (const entry of directory) {
|
||||
@@ -23,16 +38,8 @@ export async function readDetectionDirectory(dir: string): Promise<Map<string, s
|
||||
names.set(directoryNameKey(entry.name), entry.name)
|
||||
}
|
||||
return names
|
||||
} 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.
|
||||
} catch {
|
||||
// A vanished entry during Dirent conversion cannot prove the directory empty.
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { access, chmod, mkdir, mkdtemp, readdir, rm, symlink, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Exercise Windows filename folding through source listings on case-sensitive hosts.
|
||||
vi.mock('./command-path-directory-worker-client', async () => ({
|
||||
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'], {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user