fix(search): preserve remote binaries and complete runtime packaging

This commit is contained in:
Neil
2026-09-23 19:57:36 -07:00
parent 11d29feaeb
commit 8d6759a8e7
16 changed files with 374 additions and 82 deletions
+20 -28
View File
@@ -9,14 +9,12 @@
*/
import { fork, spawnSync } from 'node:child_process'
import { build } from 'esbuild'
import { createHash } from 'node:crypto'
import {
chmodSync,
copyFileSync,
existsSync,
cpSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync
} from 'node:fs'
@@ -24,10 +22,10 @@ import { arch, platform, tmpdir } from 'node:os'
import { join } from 'node:path'
import process from 'node:process'
import {
ORCAD_VERSION,
ORCAD_VERSION_FILENAME,
orcadArtifactFilenames
ORCAD_RIPGREP_ARTIFACTS
} from '../../src/shared/orcad-artifacts.ts'
import { computeOrcadFullVersion } from './orcad-artifact-version.mjs'
const ROOT = join(import.meta.dirname, '..', '..')
const OUT_DIR = join(ROOT, 'out', 'orcad')
@@ -83,18 +81,23 @@ copyFileSync(AGENT_BROWSER_SOURCE, AGENT_BROWSER_OUTPUT)
if (process.platform !== 'win32') {
chmodSync(AGENT_BROWSER_OUTPUT, 0o755)
}
// Why: orcad has no Electron resourcesPath; the rg resolver looks under its install root instead.
const RIPGREP_PLATFORM = `${platform()}-${arch()}`
const RIPGREP_NAME = process.platform === 'win32' ? 'rg.exe' : 'rg'
const RIPGREP_OUTPUT_DIR = join(OUT_DIR, 'ripgrep', RIPGREP_PLATFORM)
mkdirSync(RIPGREP_OUTPUT_DIR, { recursive: true })
copyFileSync(
join(ROOT, 'node_modules', '@vscode', 'ripgrep-universal', 'bin', RIPGREP_PLATFORM, RIPGREP_NAME),
join(RIPGREP_OUTPUT_DIR, RIPGREP_NAME)
)
if (process.platform !== 'win32') {
chmodSync(join(RIPGREP_OUTPUT_DIR, RIPGREP_NAME), 0o755)
// Why every platform: an SSH deployment can target a different host than the build machine.
for (const artifact of ORCAD_RIPGREP_ARTIFACTS) {
const [, ripgrepPlatform, ripgrepName] = artifact.split('/')
const outputDir = join(OUT_DIR, 'ripgrep', ripgrepPlatform)
mkdirSync(outputDir, { recursive: true })
const outputPath = join(outputDir, ripgrepName)
copyFileSync(
join(ROOT, 'node_modules', '@vscode', 'ripgrep-universal', 'bin', ripgrepPlatform, ripgrepName),
outputPath
)
if (!ripgrepPlatform.startsWith('win32-')) {
chmodSync(outputPath, 0o755)
}
}
cpSync(join(ROOT, 'resources', 'licenses', 'ripgrep'), join(OUT_DIR, 'ripgrep', 'licenses'), {
recursive: true
})
/** Why one call per child and not one `outdir` build: esbuild mirrors each entry's source
* directory under `outdir`, and both children must land flat beside orcad.js — that is where
@@ -258,18 +261,7 @@ if (graphErrors.length > 0) {
// already-`.install-complete` dir is never re-uploaded. The deploy would silently run stale
// bytes while reporting the new version.
if (process.exitCode !== 1) {
const hash = createHash('sha256')
for (const filename of orcadArtifactFilenames()) {
const artifactPath = join(OUT_DIR, filename)
if (!existsSync(artifactPath)) {
throw new Error(
`orcad declares ${filename} in ORCAD_ARTIFACTS but never emitted it. Add the build ` +
'step, or drop it from src/shared/orcad-artifacts.ts.'
)
}
hash.update(readFileSync(artifactPath))
}
const fullVersion = `${ORCAD_VERSION}+${hash.digest('hex').slice(0, 12)}`
const fullVersion = computeOrcadFullVersion(OUT_DIR)
writeFileSync(join(OUT_DIR, ORCAD_VERSION_FILENAME), fullVersion)
console.log(
`[build-orcad] ok — ${fullVersion}, ${(output.bytes / 1024 / 1024).toFixed(2)} MB, ${Object.keys(output.inputs).length} modules, zero electron and node:sqlite imports.`
+19
View File
@@ -0,0 +1,19 @@
import { createHash } from 'node:crypto'
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { ORCAD_VERSION, orcadArtifactFilenames } from '../../src/shared/orcad-artifacts.ts'
export function computeOrcadFullVersion(artifactDir) {
const hash = createHash('sha256')
for (const filename of orcadArtifactFilenames()) {
const artifactPath = join(artifactDir, filename)
if (!existsSync(artifactPath)) {
throw new Error(
`orcad declares ${filename} in ORCAD_ARTIFACTS but never emitted it. Add the build ` +
'step, or drop it from src/shared/orcad-artifacts.ts.'
)
}
hash.update(readFileSync(artifactPath))
}
return `${ORCAD_VERSION}+${hash.digest('hex').slice(0, 12)}`
}
@@ -0,0 +1,30 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
ORCAD_RIPGREP_ARTIFACTS,
orcadArtifactFilenames
} from '../../src/shared/orcad-artifacts.ts'
import { computeOrcadFullVersion } from './orcad-artifact-version.mjs'
describe('standalone runtime version', () => {
it('changes when a shipped search binary changes and rejects a missing binary', () => {
const dir = mkdtempSync(join(tmpdir(), 'orcad-version-'))
try {
for (const filename of orcadArtifactFilenames()) {
const path = join(dir, filename)
mkdirSync(dirname(path), { recursive: true })
writeFileSync(path, filename)
}
const before = computeOrcadFullVersion(dir)
const binary = join(dir, ORCAD_RIPGREP_ARTIFACTS[0])
writeFileSync(binary, 'updated binary')
expect(computeOrcadFullVersion(dir)).not.toBe(before)
rmSync(binary)
expect(() => computeOrcadFullVersion(dir)).toThrow(ORCAD_RIPGREP_ARTIFACTS[0])
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
})
+2 -1
View File
@@ -1,7 +1,8 @@
# Bundled ripgrep notices
Orca ships prebuilt ripgrep (`rg`) binaries from `@vscode/ripgrep-universal` under
`Resources/ripgrep/`, for local, WSL, and SSH-remote search.
`Resources/ripgrep/` and in the standalone runtime's `ripgrep/` directory,
for local, WSL, and SSH-remote search.
- ripgrep: dual-licensed MIT (`LICENSE-MIT`) or Unlicense (`UNLICENSE`).
- PCRE2, statically linked into ripgrep's `--pcre2` support: BSD (`PCRE2-LICENCE.md`).
@@ -0,0 +1,78 @@
import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { getAppEnvironment, setAppEnvironment } from '../../shared/app-environment'
import { bundledRipgrepBinaryName } from '../../shared/bundled-ripgrep'
import { bundledRipgrepCommand, resetBundledRipgrepPathCacheForTests } from './bundled-ripgrep-path'
import { spawnBundledRipgrep } from './bundled-ripgrep-spawn'
describe('packaged ripgrep execution', () => {
let fixture: string
let binary: string
let workspace: string
const resourcesPath = process.resourcesPath
beforeEach(() => {
resetBundledRipgrepPathCacheForTests()
const source = bundledRipgrepCommand()
fixture = mkdtempSync(join(tmpdir(), 'orca bundled rg '))
const resources = join(fixture, 'resources')
const platform = `${process.platform}-${process.arch}`
binary = join(resources, 'ripgrep', platform, bundledRipgrepBinaryName(platform))
mkdirSync(dirname(binary), { recursive: true })
copyFileSync(source, binary)
workspace = join(fixture, 'folder workspace')
mkdirSync(workspace)
writeFileSync(join(workspace, '日本語 file.txt'), 'needle\n')
// A checkout executable must never replace a missing packaged binary.
writeFileSync(join(workspace, 'rg'), '#!/bin/sh\necho planted\n', { mode: 0o755 })
writeFileSync(join(workspace, 'rg.exe'), 'planted')
Object.defineProperty(process, 'resourcesPath', { configurable: true, value: resources })
setAppEnvironment({
...getAppEnvironment(),
isPackaged: () => true,
getAppPath: () => fixture
})
resetBundledRipgrepPathCacheForTests()
})
afterEach(() => {
Object.defineProperty(process, 'resourcesPath', { configurable: true, value: resourcesPath })
resetBundledRipgrepPathCacheForTests()
rmSync(fixture, { recursive: true, force: true })
})
function search(): Promise<string> {
return new Promise((resolve, reject) => {
const child = spawnBundledRipgrep(['--no-config', '--files-with-matches', 'needle', '.'], {
cwd: workspace,
env: { ...process.env, PATH: '' },
stdio: ['ignore', 'pipe', 'pipe']
})
let output = ''
child.stdout?.setEncoding('utf8').on('data', (chunk: string) => {
output += chunk
})
child.stderr?.resume()
child.once('error', reject)
child.once('close', (code) => {
if (code === 0) {
resolve(output)
} else {
reject(new Error(`ripgrep exited ${code}`))
}
})
})
}
it('searches a folder with no PATH tools and spaces in the install and workspace paths', async () => {
expect(await search()).toContain('日本語 file.txt')
})
it('fails when its binary is missing instead of running the workspace executable', async () => {
rmSync(binary)
resetBundledRipgrepPathCacheForTests()
await expect(search()).rejects.toMatchObject({ code: 'ENOENT' })
})
})
+50
View File
@@ -23,6 +23,7 @@ import { uploadRelayDirectory, writeRelayFile } from './ssh-relay-install-transf
import { deployOrcad, type OrcadDeployOptions } from './orcad-remote-deploy'
import { emptyOrcadActivationRecord, withActivatedVersion } from './orcad-activation-record'
import { getRemoteHostPlatform } from './ssh-remote-platform'
import { finalizeInstall } from './ssh-relay-versioned-install'
import type { SshConnection } from './ssh-connection'
const mockExec = vi.mocked(execCommand)
@@ -150,6 +151,55 @@ describe('deployOrcad', () => {
expect(vi.mocked(uploadRelayDirectory).mock.calls[0][2]).toContain(`orcad-${NEW_VERSION}`)
})
it.each(['linux-arm64', 'darwin-x64'] as const)(
'marks the %s search binary executable before completing the install',
async (platform) => {
scriptHost({ activationRecord: '', readiness: {}, log: [] })
await deployOrcad(
options({
host: getRemoteHostPlatform(platform),
census: { liveSessions: 1, startedSinceActivation: 0 }
})
)
const chmod = mockExec.mock.calls.findIndex(([, command]) =>
String(command).startsWith('chmod 755 ')
)
expect(chmod).toBeGreaterThanOrEqual(0)
expect(mockExec.mock.calls[chmod]?.[1]).toContain(`/ripgrep/${platform}/rg'`)
expect(vi.mocked(uploadRelayDirectory).mock.invocationCallOrder[0]).toBeLessThan(
mockExec.mock.invocationCallOrder[chmod]
)
expect(mockExec.mock.invocationCallOrder[chmod]).toBeLessThan(
vi.mocked(finalizeInstall).mock.invocationCallOrder[0]
)
}
)
it('does not run chmod on a Windows remote', async () => {
scriptHost({ activationRecord: '', readiness: {}, log: [] })
await deployOrcad(
options({
host: getRemoteHostPlatform('win32-x64'),
remoteHome: 'C:/Users/u',
census: { liveSessions: 1, startedSinceActivation: 0 }
})
)
expect(mockExec.mock.calls.some(([, command]) => String(command).startsWith('chmod '))).toBe(
false
)
})
it('leaves an upload incomplete when the remote cannot make search executable', async () => {
mockExec.mockImplementation(async (_conn, command) => {
if (String(command).startsWith('chmod 755 ')) {
throw new Error('chmod failed')
}
return ''
})
await expect(deployOrcad(options())).rejects.toThrow('chmod failed')
expect(vi.mocked(finalizeInstall)).not.toHaveBeenCalled()
})
it('activates a healthy candidate and records the outgoing version as the rollback target', async () => {
const script: HostScript = {
activationRecord: ACTIVE_OLD,
+7 -6
View File
@@ -14,6 +14,7 @@
*/
import type { SshConnection } from './ssh-connection'
import { execCommand } from './ssh-relay-deploy-helpers'
import { shellEscape } from './ssh-connection-utils'
import { ORCAD_INSTALL_MODEL } from './remote-install-model'
import { acquireInstallLock } from './ssh-relay-install-lock'
import { uploadRelayDirectory, writeRelayFile } from './ssh-relay-install-transfers'
@@ -125,15 +126,15 @@ async function installOrcadBundle(
) {
return
}
// Why this needs ripgrep work before it goes live: `build-orcad.mjs` copies only the BUILD
// host's rg into `out/orcad/ripgrep/<platform>/`, and this upload carries that directory
// verbatim. orcad reports isPackaged() === true, so its rg resolver takes the packaged branch
// and returns an absolute path under its own install root with no PATH fallback -- on a remote
// of a different platform that path does not exist and every search fails. Call
// `ensureRemoteBundledRipgrep` here (as the relay deploy does), or ship all six platforms.
await uploadRelayDirectory(options.conn, options.localOrcadDir, remoteDir, options.host, {
signal: options.signal
})
const { host } = options
if (host.os !== 'win32') {
// SFTP creates uploaded files with 0644 even when the source binary is executable.
const binaryPath = joinRemotePath(host, remoteDir, 'ripgrep', host.relayPlatform, 'rg')
await exec(options, `chmod 755 ${shellEscape(binaryPath)}`)
}
await writeRelayFile(
options.conn,
options.host,
+35
View File
@@ -1,4 +1,11 @@
import { execFileSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { ORCAD_RIPGREP_ARTIFACTS } from '../../shared/orcad-artifacts'
import { probeRemoteInstallCompleteCommand } from './ssh-remote-commands'
import { getRemoteHostPlatform } from './ssh-remote-platform'
import {
inventoryRemoteInstallDirs,
@@ -20,6 +27,34 @@ describe('remote install namespace', () => {
expect(remoteInstallDirName(ORCAD_INSTALL_MODEL, '0.1.0+aa')).toBe('orcad-0.1.0+aa')
})
it('requires every shipped search binary in a completed standalone runtime install', () => {
const required = ORCAD_INSTALL_MODEL.requiredArtifacts(false)
expect(required).toEqual(expect.arrayContaining([...ORCAD_RIPGREP_ARTIFACTS]))
expect(ORCAD_INSTALL_MODEL.requiredArtifacts(true)).toEqual(required)
})
it.skipIf(process.platform === 'win32')('rejects an install missing its search binary', () => {
const dir = mkdtempSync(join(tmpdir(), 'orcad-remote-probe-'))
try {
const required = [...ORCAD_INSTALL_MODEL.requiredArtifacts(false), '.install-complete']
for (const filename of required) {
const path = join(dir, filename)
mkdirSync(dirname(path), { recursive: true })
writeFileSync(path, '')
}
const command = probeRemoteInstallCompleteCommand(
getRemoteHostPlatform('linux-x64'),
dir,
required
)
expect(execFileSync('sh', ['-c', command], { encoding: 'utf8' }).trim()).toBe('OK')
rmSync(join(dir, ORCAD_RIPGREP_ARTIFACTS[0]))
expect(execFileSync('sh', ['-c', command], { encoding: 'utf8' }).trim()).toBe('MISSING')
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
it('keeps the relay listing pattern byte-identical to the one it shipped with', () => {
// The literal that was hardcoded in `listRelayBaseDirsCommand` before it was
// parameterized. A drift here changes what an existing host's GC can see.
+16 -16
View File
@@ -79,27 +79,14 @@ describe('ensureRemoteBundledRipgrep', () => {
)
})
// Why this needs a test: nothing else collects this tree -- the relay's version GC only matches
// `relay-*` -- so without it every rg bump left another ~5 MB on every host, forever.
it('collects superseded builds while sparing the current one and live stages', async () => {
execCommandMock.mockResolvedValueOnce('ORCA-RG-PRESENT\n')
await ensureRemoteBundledRipgrep(connection(), LINUX, '/home/me')
const script = execScripts()[0]
expect(script).toContain("! -name 'c0ffee0123456789-linux-x64'")
expect(script).toContain("! -name '.upload-*'")
expect(script).toContain('-mtime +14')
})
it('collects superseded builds on Windows hosts too', async () => {
it('limits Windows cache cleanup to abandoned upload stages', async () => {
execCommandMock.mockResolvedValueOnce('ORCA-RG-PRESENT\n')
await ensureRemoteBundledRipgrep(connection(), WINDOWS, 'C:/Users/me user')
const script = execScripts()[0]
expect(script).toContain("-ne 'c0ffee0123456789-win32-x64'")
expect(script).toContain('AddDays(-14)')
expect(script.match(/Get-ChildItem/g)).toHaveLength(1)
expect(script).toContain("-Filter '.upload-*'")
})
it('skips the upload in one round trip when the binary is already installed', async () => {
@@ -275,6 +262,19 @@ describe.runIf(process.platform !== 'win32').each(SHELLS)(
expect(execFileSync(installed(), { encoding: 'utf-8' })).toContain('ripgrep 15.0.0')
})
it('keeps an old binary usable by a relay pinned to an earlier client build', async () => {
const previous = join(home, '.orca-remote', 'ripgrep', 'previous-linux-x64', 'rg')
mkdirSync(dirname(previous), { recursive: true })
writeFileSync(previous, '#!/bin/sh\necho ripgrep previous\n', { mode: 0o755 })
execFileSync('touch', ['-t', '200001010000', dirname(previous)])
expect(execFileSync(previous, { encoding: 'utf-8' })).toContain('ripgrep previous')
await expect(ensureRemoteBundledRipgrep(connection(), LINUX, home)).resolves.toBe('installed')
await expect(ensureRemoteBundledRipgrep(connection(), LINUX, home)).resolves.toBe('present')
expect(execFileSync(previous, { encoding: 'utf-8' })).toContain('ripgrep previous')
})
it('sweeps an abandoned stage older than an hour but keeps a live one', async () => {
const cache = join(home, '.orca-remote', 'ripgrep')
mkdirSync(join(cache, '.upload-stale', 'payload'), { recursive: true })
+2 -9
View File
@@ -47,10 +47,6 @@ export const REMOTE_RIPGREP_CACHE_DIR_NAME = 'ripgrep'
const UPLOAD_STAGE_PREFIX = '.upload-'
// Why an hour: long enough that no live upload of ~5 MB is still writing, short enough to drain crashes.
const STALE_UPLOAD_STAGE_MINUTES = 60
// Why two weeks and not immediately: a client pinned to an older Orca may still be running against
// the build it uploaded. The only cost of collecting one too early is that client re-uploading
// ~5 MB on its next connect; the cost of never collecting is a permanent leak on every host.
const SUPERSEDED_RIPGREP_DAYS = 14
const PRESENT = 'ORCA-RG-PRESENT'
const STAGED = 'ORCA-RG-STAGED'
const INSTALLED = 'ORCA-RG-INSTALLED'
@@ -196,19 +192,16 @@ export function probeOrStageCommand(
const bin = shellEscape(layout.binaryPath)
const cache = shellEscape(layout.cacheDir)
const sweep = `find ${cache} -mindepth 1 -maxdepth 1 -type d -name '${UPLOAD_STAGE_PREFIX}*' -mmin +${STALE_UPLOAD_STAGE_MINUTES} -exec rm -rf {} + 2>/dev/null`
// Why here and not in the relay's version GC: that GC only matches `relay-*`, so nothing has
// ever collected this tree. Every rg bump would otherwise leave another ~5 MB per host forever.
const collect = `find ${cache} -mindepth 1 -maxdepth 1 -type d ! -name ${shellEscape(layout.entryName)} ! -name '${UPLOAD_STAGE_PREFIX}*' -mtime +${SUPERSEDED_RIPGREP_DAYS} -exec rm -rf {} + 2>/dev/null`
// Installed builds may still serve an older client's live relay; age is not disuse.
const stage = makeRelayUploadStageDirectoryCommand(stageNamespace, host, stageDir)
// Why the sweep runs before the branch, not inside the else: once rg is installed every later
// deploy takes the PRESENT path, so a stage orphaned by a dropped connection would never be
// collected. It stays one exec round trip either way.
return `${sweep}; ${collect}; if ${posixInstalledTest(bin, bytes)}; then echo ${PRESENT}; else ${stage} && echo ${STAGED}; fi`
return `${sweep}; if ${posixInstalledTest(bin, bytes)}; then echo ${PRESENT}; else ${stage} && echo ${STAGED}; fi`
}
return powerShellCommand(
[
`Get-ChildItem -LiteralPath ${powerShellLiteral(layout.cacheDir)} -Directory -Filter '${UPLOAD_STAGE_PREFIX}*' -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt (Get-Date).AddMinutes(-${STALE_UPLOAD_STAGE_MINUTES}) } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue`,
`Get-ChildItem -LiteralPath ${powerShellLiteral(layout.cacheDir)} -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne ${powerShellLiteral(layout.entryName)} -and -not $_.Name.StartsWith('${UPLOAD_STAGE_PREFIX}') -and $_.LastWriteTime -lt (Get-Date).AddDays(-${SUPERSEDED_RIPGREP_DAYS}) } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue`,
`if (${windowsInstalledTest(powerShellLiteral(layout.binaryPath), bytes)}) { '${PRESENT}' } else {`,
`$null = New-Item -ItemType Directory -Force -Path ${powerShellLiteral(joinRemotePath(host, stageDir, 'payload'))} -ErrorAction Stop`,
`'${STAGED}' }`
+9 -14
View File
@@ -103,23 +103,19 @@ export function listFilesWithRg(
let processErrorObserved = false
let unavailableExitObserved = false
let launchFailureCheck: Promise<void> | null = null
// --no-messages: permission-denied noise on the remote (e.g. .ssh,
// root-owned mounts) would otherwise flood stderr.
// cwd: rootPath — root-relative exclude globs like `!packages/app/**`
// are evaluated against rg's working directory, not the absolute
// search target. Without cwd, nested-worktree exclusions silently
// stop working.
// Suppress permission noise; cwd anchors root-relative exclusion globs.
const command = resolveRelayRipgrepCommand()
// Why not spawn a bare name when this is null: on Windows CreateProcessW searches the
// spawn cwd -- the user's repo -- before PATH. "No rg here" is what the chain handles.
if (command === null) {
throw new RipgrepUnavailableError()
}
const env = buildRelayCommandEnv()
let child: ChildProcess
try {
child = spawn(command, ['--no-messages', ...args], {
cwd: rootPath,
env: buildRelayCommandEnv(),
env,
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true
})
@@ -179,19 +175,18 @@ export function listFilesWithRg(
// Why distinguish: RipgrepUnavailableError is what engages the git/readdir chain,
// and that chain cannot help when the root itself is gone.
rejectPass(
(await classifyRipgrepLaunchFailure(rootPath, [command, pathRipgrepCommand()])) ===
'cwd-unreachable'
(await classifyRipgrepLaunchFailure(
rootPath,
[command, pathRipgrepCommand()],
env
)) === 'cwd-unreachable'
? ripgrepMissingCwdError(rootPath)
: new RipgrepUnavailableError()
)
}
)
}
children.push({
child,
isDone: () => passDone,
reject: rejectPass
})
children.push({ child, isDone: () => passDone, reject: rejectPass })
timer = setTimeout(() => {
// Discard residual buffer on abnormal exit — a truncated byte
+7 -3
View File
@@ -126,11 +126,12 @@ export function searchWithRg(
}
// Why a second binding: the closures below capture it, and narrowing does not reach them.
const command: string = resolvedRgCommand
const env = buildRelayCommandEnv()
let child: ReturnType<typeof spawn>
try {
child = spawn(command, rgArgs, {
cwd: rootPath,
env: buildRelayCommandEnv(),
env,
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true
})
@@ -185,8 +186,11 @@ export function searchWithRg(
// ripgrep. The workspace moving would otherwise look like a successful empty scan.
if (settle()) {
reject(
(await classifyRipgrepLaunchFailure(rootPath, [command, pathRipgrepCommand()])) ===
'cwd-unreachable'
(await classifyRipgrepLaunchFailure(
rootPath,
[command, pathRipgrepCommand()],
env
)) === 'cwd-unreachable'
? ripgrepMissingCwdError(rootPath)
: new RipgrepUnavailableError()
)
+42
View File
@@ -0,0 +1,42 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { listFilesWithRg } from './fs-handler-list-files'
import { searchWithRg } from './fs-handler-utils'
import { configureRelayBundledRipgrep } from './relay-bundled-ripgrep'
describe.runIf(process.platform !== 'win32')(
'relay ripgrep launch classification environment',
() => {
let dir: string
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'relay-rg-cwd-env-'))
writeFileSync(join(dir, 'found.txt'), 'needle')
const cargo = join(dir, 'cargo')
mkdirSync(join(cargo, 'bin'), { recursive: true })
writeFileSync(join(cargo, 'bin', 'rg'), '#!/bin/sh\necho found.txt\n', { mode: 0o755 })
vi.stubEnv('PATH', join(dir, 'empty-path'))
vi.stubEnv('CARGO_HOME', cargo)
configureRelayBundledRipgrep(undefined)
})
afterEach(() => {
vi.unstubAllEnvs()
configureRelayBundledRipgrep(undefined)
rmSync(dir, { recursive: true, force: true })
})
it('uses the search PATH to distinguish a moved root from missing ripgrep', async () => {
await expect(listFilesWithRg(dir, [], { maxResults: 10 })).resolves.toContain('found.txt')
const missing = join(dir, 'moved-root')
await expect(listFilesWithRg(missing)).rejects.toThrow(
`Search root is not reachable: ${missing}`
)
await expect(searchWithRg(missing, 'needle', { maxResults: 10 })).rejects.toThrow(
`Search root is not reachable: ${missing}`
)
})
}
)
+29
View File
@@ -0,0 +1,29 @@
import { readdirSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { BUNDLED_RIPGREP_PLATFORMS, bundledRipgrepBinaryName } from './bundled-ripgrep'
import {
ORCAD_RIPGREP_ARTIFACTS,
ORCAD_RIPGREP_LICENSE_ARTIFACTS,
orcadArtifactFilenames
} from './orcad-artifacts'
describe('standalone runtime artifacts', () => {
it('ships search binaries for every SSH host and includes them in the install identity', () => {
const expected = BUNDLED_RIPGREP_PLATFORMS.map(
(platform) => `ripgrep/${platform}/${bundledRipgrepBinaryName(platform)}`
)
expect(ORCAD_RIPGREP_ARTIFACTS).toEqual(expected)
expect(orcadArtifactFilenames()).toEqual(expect.arrayContaining(expected))
})
it('ships the binary redistribution notices with every install', () => {
const sourceDir = join(__dirname, '../../resources/licenses/ripgrep')
expect(ORCAD_RIPGREP_LICENSE_ARTIFACTS.map((path) => path.split('/').at(-1)).sort()).toEqual(
readdirSync(sourceDir).sort()
)
expect(orcadArtifactFilenames()).toEqual(
expect.arrayContaining([...ORCAD_RIPGREP_LICENSE_ARTIFACTS])
)
})
})
+23 -1
View File
@@ -11,6 +11,26 @@
export const ORCAD_VERSION = '0.1.0'
// Kept here because build-orcad.mjs imports this manifest directly under Node type stripping.
export const ORCAD_RIPGREP_ARTIFACTS = [
'ripgrep/linux-x64/rg',
'ripgrep/linux-arm64/rg',
'ripgrep/darwin-x64/rg',
'ripgrep/darwin-arm64/rg',
'ripgrep/win32-x64/rg.exe',
'ripgrep/win32-arm64/rg.exe'
] as const
export const ORCAD_RIPGREP_LICENSE_ARTIFACTS = [
'ripgrep/licenses/JEMALLOC-COPYING',
'ripgrep/licenses/LICENSE-MIT',
'ripgrep/licenses/LLVM-LIBUNWIND-LICENSE.TXT',
'ripgrep/licenses/MUSL-COPYRIGHT',
'ripgrep/licenses/PCRE2-LICENCE.md',
'ripgrep/licenses/README.md',
'ripgrep/licenses/UNLICENSE'
] as const
export type OrcadArtifact = {
filename: string
/**
@@ -26,7 +46,9 @@ export const ORCAD_ARTIFACTS: readonly OrcadArtifact[] = [
// Forked so a native @parcel/watcher fault kills the child, not the server.
{ filename: 'parcel-watcher-process-entry.js' },
// Forked so PTYs outlive the runtime process; its absence makes every restart destructive.
{ filename: 'daemon-entry.js' }
{ filename: 'daemon-entry.js' },
...ORCAD_RIPGREP_ARTIFACTS.map((filename) => ({ filename })),
...ORCAD_RIPGREP_LICENSE_ARTIFACTS.map((filename) => ({ filename }))
]
/** Written after the artifacts, so it is never an input to its own hash. */
+5 -4
View File
@@ -77,12 +77,12 @@ export async function isRipgrepSpawnCwdUsable(cwd: string): Promise<boolean> {
}
}
function probeRipgrepVersion(command: string): Promise<boolean> {
function probeRipgrepVersion(command: string, env: NodeJS.ProcessEnv): Promise<boolean> {
return new Promise((resolve) => {
let child: ChildProcess
try {
// windowsHide: a probe must never flash a console window on Windows.
child = spawn(command, ['--version'], { stdio: 'ignore', windowsHide: true })
child = spawn(command, ['--version'], { env, stdio: 'ignore', windowsHide: true })
} catch {
resolve(false)
return
@@ -128,13 +128,14 @@ function probeRipgrepVersion(command: string): Promise<boolean> {
*/
export async function classifyRipgrepLaunchFailure(
cwd: string,
candidates: readonly (string | null)[]
candidates: readonly (string | null)[],
env: NodeJS.ProcessEnv
): Promise<'cwd-unreachable' | 'ripgrep-unavailable'> {
if (await isRipgrepSpawnCwdUsable(cwd)) {
return 'ripgrep-unavailable'
}
for (const command of new Set(candidates.filter((entry) => entry !== null))) {
if (await probeRipgrepVersion(command)) {
if (await probeRipgrepVersion(command, env)) {
return 'cwd-unreachable'
}
}