From d65091ef47efcb025808da1fbcd42b91a68a1205 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Mon, 13 Jul 2026 22:57:40 -0700 Subject: [PATCH] fix(ssh): keep Windows CLI when the launcher compiler is missing The remote CLI installer deleted the legacy orca.cmd bridge before it confirmed csc.exe existed, so a minimal Windows host without the .NET compiler lost its existing CLI command and got nothing back. Move the legacy-shim removal to run only after the compile and the launcher-existence guard both pass. Missing-compiler and compile-failure paths now exit non-zero while leaving the existing orca.cmd untouched; a successful upgrade still clears the unsafe %* bridge (orca.exe already shadows orca.cmd via PATHEXT). No %* forwarding is restored. Co-authored-by: Orca --- src/main/ssh/ssh-remote-cli-launcher.test.ts | 77 ++++++++++++++++++-- src/main/ssh/ssh-remote-cli-launcher.ts | 6 +- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/src/main/ssh/ssh-remote-cli-launcher.test.ts b/src/main/ssh/ssh-remote-cli-launcher.test.ts index c9f9c0865de..62aa7bec660 100644 --- a/src/main/ssh/ssh-remote-cli-launcher.test.ts +++ b/src/main/ssh/ssh-remote-cli-launcher.test.ts @@ -1,6 +1,6 @@ import { Buffer } from 'node:buffer' import { spawnSync } from 'node:child_process' -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { describe, expect, it } from 'vitest' @@ -23,15 +23,18 @@ function decodePowerShellCommand(command: string): string { } describe('SSH remote Orca CLI launcher', () => { - it('compiles a native Windows launcher without a cmd.exe argument bridge', () => { - const hostPlatform = getRemoteHostPlatform('win32-x64') - const plan = createRemoteCliInstallPlan({ + function windowsInstallPlan(): ReturnType { + return createRemoteCliInstallPlan({ binDir: 'C:/Users/me user/.orca-relay/bin', relayDir: 'C:/Users/me user/.orca-remote/relay-v1', nodePath: 'C:/Program Files/nodejs/node.exe', sockPath: '\\\\.\\pipe\\orca-relay-123', - hostPlatform + hostPlatform: getRemoteHostPlatform('win32-x64') }) + } + + it('compiles a native Windows launcher without a cmd.exe argument bridge', () => { + const plan = windowsInstallPlan() expect(plan.launcherPath).toBe('C:/Users/me user/.orca-relay/bin/orca.exe') expect(plan.files).toHaveLength(1) @@ -54,7 +57,24 @@ describe('SSH remote Orca CLI launcher', () => { expect(compileScript).toContain('/out:orca.exe') expect(compileScript).toContain('C:/Users/me user/.orca-relay/bin/orca-launcher.cs') expect(compileScript).toContain('C:/Users/me user/.orca-relay/bin/orca.cmd') - expect(compileScript.indexOf('orca.cmd')).toBeLessThan(compileScript.indexOf('csc.exe')) + }) + + it('removes the legacy orca.cmd only after every compile guard has passed', () => { + const script = decodePowerShellCommand(windowsInstallPlan().postWriteCommands[0] ?? '') + const legacyShimRemoval = + "Remove-Item -LiteralPath 'C:/Users/me user/.orca-relay/bin/orca.cmd' -Force -ErrorAction SilentlyContinue" + // Why: a host missing csc.exe or failing the compile must keep its existing + // CLI, so every fail-closed guard precedes the legacy %* shim removal. + const guards = [ + "if (-not $compiler) { Write-Error 'Unable to find the .NET Framework C# compiler required for the Orca SSH CLI launcher.'; exit 1 }", + 'if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }', + "if (-not (Test-Path -LiteralPath 'C:/Users/me user/.orca-relay/bin/orca.exe' -PathType Leaf))" + ] + expect(script).toContain(legacyShimRemoval) + for (const guard of guards) { + expect(script).toContain(guard) + expect(script.indexOf(guard)).toBeLessThan(script.indexOf(legacyShimRemoval)) + } }) itWindows('preserves a multiline argument through the compiled remote launcher', () => { @@ -128,6 +148,51 @@ describe('SSH remote Orca CLI launcher', () => { } }) + itWindows('preserves the existing orca.cmd when the compiler is missing', () => { + const root = mkdtempSync(join(tmpdir(), 'orca remote cli ')) + try { + const binDir = join(root, 'bin').replaceAll('\\', '/') + mkdirSync(binDir, { recursive: true }) + const legacyShimPath = join(binDir, 'orca.cmd') + writeFileSync(legacyShimPath, '@echo legacy orca cli\r\n', 'utf8') + + const plan = createRemoteCliInstallPlan({ + binDir, + relayDir: join(root, 'relay').replaceAll('\\', '/'), + nodePath: process.execPath, + sockPath: '\\\\.\\pipe\\orca-relay-test', + hostPlatform: getRemoteHostPlatform('win32-x64') + }) + for (const file of plan.files) { + mkdirSync(dirname(file.path), { recursive: true }) + writeFileSync(file.path, file.contents, 'utf8') + } + + const encoded = plan.postWriteCommands[0]?.match(/-EncodedCommand\s+(\S+)/)?.[1] + expect(encoded).toBeTruthy() + // Point WINDIR at a directory with no csc.exe so compiler discovery fails. + const compile = spawnSync( + 'powershell.exe', + [ + '-NoProfile', + '-NonInteractive', + '-ExecutionPolicy', + 'Bypass', + '-EncodedCommand', + encoded! + ], + { encoding: 'utf8', env: { ...process.env, WINDIR: root, SystemRoot: root } } + ) + + expect(compile.status).not.toBe(0) + expect(existsSync(legacyShimPath), 'existing orca.cmd must survive a failed install').toBe( + true + ) + } finally { + rmSync(root, { recursive: true, force: true }) + } + }) + it('keeps the POSIX launcher as an argv-preserving shell exec', () => { const plan = createRemoteCliInstallPlan({ binDir: '/home/me/.orca-relay/bin', diff --git a/src/main/ssh/ssh-remote-cli-launcher.ts b/src/main/ssh/ssh-remote-cli-launcher.ts index a246ce9c9b8..4122e0dc09d 100644 --- a/src/main/ssh/ssh-remote-cli-launcher.ts +++ b/src/main/ssh/ssh-remote-cli-launcher.ts @@ -169,9 +169,6 @@ function createWindowsLauncherCompileCommand( .join(' ') return powerShellCommand( [ - // Why: an upgrade must not leave the old %* batch bridge callable when - // compiler discovery fails; losing the convenience CLI is safer. - `Remove-Item -LiteralPath ${powerShellLiteral(legacyShimPath)} -Force -ErrorAction SilentlyContinue`, `Set-Location -ErrorAction Stop -LiteralPath ${powerShellLiteral(binDir)}`, '$windowsDirectory = if ($env:WINDIR) { $env:WINDIR } else { $env:SystemRoot }', `$compilerCandidates = @((Join-Path $windowsDirectory 'Microsoft.NET\\Framework64\\v4.0.30319\\csc.exe'), (Join-Path $windowsDirectory 'Microsoft.NET\\Framework\\v4.0.30319\\csc.exe'))`, @@ -180,6 +177,9 @@ function createWindowsLauncherCompileCommand( `& $compiler ${compilerArgs}`, 'if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }', `if (-not (Test-Path -LiteralPath ${powerShellLiteral(launcherPath)} -PathType Leaf)) { Write-Error 'The Orca SSH CLI launcher compiler produced no executable.'; exit 1 }`, + // Why: remove the legacy %* bridge only after a successful compile, so a + // host missing csc.exe keeps its existing CLI (orca.exe shadows orca.cmd). + `Remove-Item -LiteralPath ${powerShellLiteral(legacyShimPath)} -Force -ErrorAction SilentlyContinue`, `Remove-Item -LiteralPath ${powerShellLiteral(sourcePath)} -Force` ].join('; ') )