feat(orcad): deploy, activate and roll back orcad on Windows SSH hosts (W3) (#24563)

* feat(orcad): deploy, activate and roll back orcad on Windows SSH hosts (W3)

* test(orcad): exhaustive op switch in the Windows lifecycle fake

* test(ssh): narrow the Windows host-cell descriptor by lane before building a relay cell

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-02 02:43:45 -07:00
committed by GitHub
co-authored by m4air
parent ef01867ae4
commit 9292d18f65
28 changed files with 1034 additions and 82 deletions
+2 -1
View File
@@ -33,6 +33,7 @@ on:
- 'src/shared/windows-breakaway-launch*.ts'
- '!src/**/*.test.ts'
- 'src/main/ssh/ssh-relay-windows-host-lane.test.ts'
- 'src/main/ssh/orcad-windows-host-lane.test.ts'
- 'config/ci/windows-ssh-provider/**'
- '.github/workflows/ssh-windows-hosts.yml'
workflow_dispatch:
@@ -118,7 +119,7 @@ jobs:
$receipts=Join-Path $pwd '.build/ssh-windows-host-receipts'
New-Item -ItemType Directory -Force -Path $receipts | Out-Null
$cells=@($env:CELLS -split ',' | ForEach-Object {$_.Trim()} | Where-Object {$_})
if(-not $cells.Count){$cells=@('pinned-cmd','pinned-powershell','legacy-opt-out')}
if(-not $cells.Count){$cells=@('pinned-cmd','pinned-powershell','legacy-opt-out','orcad-cmd','orcad-powershell')}
$archive=''
if('${{ matrix.server }}' -eq 'preview'){
$archive=Join-Path $env:RUNNER_TEMP 'preview-${{ matrix.archive }}'
@@ -6,11 +6,11 @@ param(
[Parameter(Mandatory=$true)][hashtable]$Context,
[Parameter(Mandatory=$true)][ValidateSet('win32-arm64','win32-x64')][string]$Target,
[Parameter(Mandatory=$true)][string]$ReceiptRoot,
[ValidateSet('pinned-cmd','pinned-powershell','legacy-opt-out')][string[]]$Cells=@('pinned-cmd','pinned-powershell','legacy-opt-out')
[ValidateSet('pinned-cmd','pinned-powershell','legacy-opt-out','orcad-cmd','orcad-powershell')][string[]]$Cells=@('pinned-cmd','pinned-powershell','legacy-opt-out','orcad-cmd','orcad-powershell')
)
$ErrorActionPreference='Stop'
if($env:GITHUB_ACTIONS -ne 'true' -or $env:ORCA_ISOLATED_SSH_CI -ne '1'){throw 'Disposable CI only'}
$shells=@{'pinned-cmd'='cmd';'pinned-powershell'='powershell';'legacy-opt-out'='cmd'}
$shells=@{'pinned-cmd'='cmd';'pinned-powershell'='powershell';'legacy-opt-out'='cmd';'orcad-cmd'='cmd';'orcad-powershell'='powershell'}
if($Context.accounts.Count -lt $Cells.Count){throw 'Each cell needs its own private account'}
if(-not $Context.forbiddenToolLog){throw 'Run the provisioning with -HiddenTools so toolchain calls are logged'}
$openSshKey='HKLM:\SOFTWARE\OpenSSH'
@@ -78,11 +78,13 @@ try {
@{cell=$cell;target=$Target;host='127.0.0.1';port=[int]$Context.port;username=$account.name;identityFile=$Context.identityFile;home=$account.home;forbiddenToolLog=$Context.forbiddenToolLog;receipt=(Join-Path $ReceiptRoot "$cell.json")} | ConvertTo-Json | Set-Content -LiteralPath $descriptor -Encoding utf8NoBOM
$env:ORCA_RUN_SSH_WINDOWS_HOST='1';$env:ORCA_SSH_WINDOWS_HOST_CELL=$descriptor
Write-Host "Windows host cell $cell ($Target, DefaultShell $shell, account $($account.name))"
& node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/ssh/ssh-relay-windows-host-lane.test.ts --reporter=verbose 2>&1 | Tee-Object -FilePath (Join-Path $ReceiptRoot "$cell.log")
# orcad cells deploy managed orcad instead of the relay; same account and descriptor shape.
$lane=if($cell.StartsWith('orcad-')){'src/main/ssh/orcad-windows-host-lane.test.ts'}else{'src/main/ssh/ssh-relay-windows-host-lane.test.ts'}
& node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts $lane --reporter=verbose 2>&1 | Tee-Object -FilePath (Join-Path $ReceiptRoot "$cell.log")
# Why global: under the workflow's GetNewClosure callback, bare $LASTEXITCODE reads a stale captured copy.
$code=$global:LASTEXITCODE
# The relay's own log is the only record of why it closed a client.
foreach($log in @(Get-ChildItem -Path (Join-Path $account.home '.orca-remote\relay-*\relay*.log') -File -ErrorAction SilentlyContinue)){Copy-Item -LiteralPath $log.FullName -Destination (Join-Path $ReceiptRoot "$cell.$($log.Directory.Name).$($log.Name)")}
foreach($log in @(Get-ChildItem -Path (Join-Path $account.home '.orca-remote\relay-*\relay*.log'),(Join-Path $account.home '.orca-remote\orcad-*\orcad.log') -File -ErrorAction SilentlyContinue)){Copy-Item -LiteralPath $log.FullName -Destination (Join-Path $ReceiptRoot "$cell.$($log.Directory.Name).$($log.Name)")}
if(Test-Path -LiteralPath $Context.forbiddenToolLog){Copy-Item -LiteralPath $Context.forbiddenToolLog -Destination (Join-Path $ReceiptRoot "$cell.forbidden-tool-calls.log")}
$summary.Add(@{cell=$cell;shell=$shell;account=$account.name;exitCode=$code})
if($code -ne 0){$failed.Add($cell)}
@@ -2,7 +2,7 @@
# -HiddenTools: the private accounts are denied every machine PATH directory holding one of these
# executables, and their own PATH carries logging shims for them, so SSH sessions have no host toolchain.
# -HostCellProbe receives a context hashtable (accounts, port, keys, shim log) once provisioning passes.
param([Parameter(Mandatory=$true)][string]$Receipt,[string]$Archive,[Parameter(Mandatory=$true)][ValidateSet('arm64','x64')][string]$Arch,[ValidateSet('preview','inbox')][string]$Server='preview',[scriptblock]$ProductionRouteProbe,[ValidateRange(1,4)][int]$Accounts=1,[string[]]$HiddenTools=@(),[scriptblock]$HostCellProbe)
param([Parameter(Mandatory=$true)][string]$Receipt,[string]$Archive,[Parameter(Mandatory=$true)][ValidateSet('arm64','x64')][string]$Arch,[ValidateSet('preview','inbox')][string]$Server='preview',[scriptblock]$ProductionRouteProbe,[ValidateRange(1,5)][int]$Accounts=1,[string[]]$HiddenTools=@(),[scriptblock]$HostCellProbe)
$ErrorActionPreference = 'Stop'
$target=@{arm64=@{os='Arm64';folder='OpenSSH-ARM64';machine='0xAA64';archive='698c6aec31c1dd0fb996206e8741f4531a97355686b5431ef347d531b07fcd42'};x64=@{os='X64';folder='OpenSSH-Win64';machine='0x8664';archive='23f50f3458c4c5d0b12217c6a5ddfde0137210a30fa870e98b29827f7b43aba5'}}[$Arch]
$scopeServer=if($Server -eq 'inbox'){'Windows inbox OpenSSH.Server capability binaries'}else{'Microsoft Win32-OpenSSH 10.0.0.0p2-Preview'}
@@ -4,7 +4,8 @@ import { describe, expect, it } from 'vitest'
import { parse } from 'yaml'
import {
WINDOWS_FORBIDDEN_TOOLS,
WINDOWS_HOST_CELL_IDS
WINDOWS_HOST_CELL_IDS,
WINDOWS_ORCAD_CELL_IDS
} from '../../src/main/ssh/ssh-windows-host-cells.ts'
const projectDir = resolve(import.meta.dirname, '../..')
@@ -70,14 +71,16 @@ describe('SSH Windows-host workflow', () => {
it('defaults to every cell the TypeScript lane knows', () => {
const defaults = /\{\$cells=@\(([^)]*)\)\}/.exec(runStep.run)?.[1]
expect(defaults?.split(',').map((id) => id.trim().replaceAll("'", ''))).toEqual([
...WINDOWS_HOST_CELL_IDS
...WINDOWS_HOST_CELL_IDS,
...WINDOWS_ORCAD_CELL_IDS
])
const invoker = readFileSync(
join(projectDir, 'config/ci/windows-ssh-provider/invoke-pinned-relay-cells.ps1'),
'utf8'
)
for (const id of WINDOWS_HOST_CELL_IDS) {
for (const id of [...WINDOWS_HOST_CELL_IDS, ...WINDOWS_ORCAD_CELL_IDS]) {
expect(invoker).toContain(`'${id}'`)
}
expect(invoker).toContain('src/main/ssh/orcad-windows-host-lane.test.ts')
})
})
+17 -2
View File
@@ -15,7 +15,8 @@ import { acquireInstallLock, RELAY_INSTALL_LOCK_NAME } from './ssh-relay-install
import { probeInstallLockExistsCommand } from './ssh-relay-install-lock-commands'
import { RELAY_REMOTE_DIR } from './relay-protocol'
import { removeRemoteFileCommand, removeRemoteTreeCommand } from './ssh-remote-commands'
import { joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform'
import { orcadRemoteBaseDir, orcadWindowsHostOpCommand } from './orcad-remote-windows-node'
import { isWindowsRemoteHost, joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform'
import {
ORCAD_ACTIVATION_TRANSACTION_DIRNAME,
ORCAD_ACTIVATION_TRANSACTION_FILENAME
@@ -147,8 +148,22 @@ function releaseActivationFence(
// Journal first: a release cut short must leave a lock without a journal, never the reverse.
const journal = joinRemotePath(options.host, lockRoot, ORCAD_ACTIVATION_TRANSACTION_FILENAME)
// Why no signal: a cancelled run must still be able to drop a fence it proved unnecessary.
const target = withoutAbortSignal(options)
if (isWindowsRemoteHost(options.host)) {
// `&&` does not parse under a PowerShell DefaultShell, so the two steps are two execs.
const baseDir = orcadRemoteBaseDir(options.host, options.remoteHome)
return execOrcadRemote(
target,
orcadWindowsHostOpCommand(options.host, baseDir, 'remove-file', [journal])
).then(() =>
execOrcadRemote(
target,
orcadWindowsHostOpCommand(options.host, baseDir, 'remove-tree', [lockRoot])
).then(() => undefined)
)
}
return execOrcadRemote(
withoutAbortSignal(options),
target,
`${removeRemoteFileCommand(options.host, journal)} && ${removeRemoteTreeCommand(options.host, lockRoot)}`
).then(() => undefined)
}
+14 -3
View File
@@ -6,6 +6,7 @@
* when the change provably carries no terminals — the slot exposed RPC, so a pre-launch census
* cannot vouch for it.
*/
import { orcadRemoteBaseDir } from './orcad-remote-windows-node'
import type { ServeReadiness } from '../server/serve-readiness'
import { RELAY_REMOTE_DIR } from './relay-protocol'
import { ORCAD_STATE_SNAPSHOT_DIR } from './orcad-activation-record'
@@ -98,7 +99,12 @@ async function decideChangedStateRestore(
// Read-only, so a lost answer is just "changed".
const comparison = await execOrcadRemote(
options,
compareOrcadStateSnapshotCommand(options.host, options.userDataDir, snapshotDir)
compareOrcadStateSnapshotCommand(
options.host,
options.userDataDir,
snapshotDir,
orcadRemoteBaseDir(options.host, options.remoteHome)
)
).catch(() => '')
if (orcadSnapshotIsUnchanged(comparison)) {
return 'unchanged'
@@ -143,9 +149,14 @@ async function restoreState(options: OrcadSlotOptions, state: OrcadSnapshotVerdi
? restoreOrcadStateSnapshotCommand(
options.host,
options.userDataDir,
orcadSnapshotPath(options, state.dirName)
orcadSnapshotPath(options, state.dirName),
orcadRemoteBaseDir(options.host, options.remoteHome)
)
: clearOrcadStateSnapshotMembersCommand(
options.host,
options.userDataDir,
orcadRemoteBaseDir(options.host, options.remoteHome)
)
: clearOrcadStateSnapshotMembersCommand(options.host, options.userDataDir)
const restored = parseOrcadSnapshotRestore(await execOrcadRemote(options, command))
if (restored !== 'restored') {
throw new Error(`The prelaunch state could not be restored (${restored}).`)
@@ -9,7 +9,8 @@ import { ORCAD_LOCK_FILE_NAME } from '../orcad/orcad-instance-lock'
import { PRIMARY_RUNTIME_METADATA_FILE } from '../../shared/runtime-bootstrap'
import { shellEscape } from './ssh-connection-utils'
import { selectOrcadSlotRuntimeCommand } from './orcad-remote-runtime'
import { joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform'
import { isWindowsRemoteHost, joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform'
import { orcadWindowsBaseDir, orcadWindowsHostOpCommand } from './orcad-remote-windows-node'
const OWNER_RECORD_MAX_BYTES = 64 * 1024
@@ -24,6 +25,15 @@ export function initialOrcadActivationAdmissionCommand(
remoteInstallDir: string,
legacyNodePath: string
): string {
if (isWindowsRemoteHost(host)) {
// Windows has no O_NOFOLLOW; the host script refuses links by lstat instead.
return orcadWindowsHostOpCommand(
host,
orcadWindowsBaseDir(host, remoteInstallDir),
'owner-admission',
[userDataDir]
)
}
const owners = [ORCAD_LOCK_FILE_NAME, PRIMARY_RUNTIME_METADATA_FILE].map((name) => ({
name,
path: joinRemotePath(host, userDataDir, name)
+7 -1
View File
@@ -5,6 +5,7 @@
* the host for `recoverInterruptedOrcadActivation` to finish or undo it. A run that ends with
* the host provably back on one slot drops the fence; one that cannot prove it keeps it.
*/
import { orcadRemoteBaseDir } from './orcad-remote-windows-node'
import { randomUUID } from 'node:crypto'
import type { OrcadDeployOptions, OrcadDeployResult } from './orcad-remote-deploy'
import { isUnconfirmedSshCommandTermination } from './ssh-relay-deploy-helpers'
@@ -176,7 +177,12 @@ export async function activateInstalledOrcad(
const capture = parseOrcadSnapshotCapture(
await execOrcadRemote(
options,
captureOrcadStateSnapshotCommand(options.host, options.userDataDir, snapshotDir)
captureOrcadStateSnapshotCommand(
options.host,
options.userDataDir,
snapshotDir,
orcadRemoteBaseDir(options.host, options.remoteHome)
)
).catch((error: unknown) => {
if (isUnconfirmedSshCommandTermination(error)) {
throw error
+6 -3
View File
@@ -4,11 +4,12 @@ import type { SshTarget } from '../../shared/ssh-types'
import type { OrcadActivationRecord } from './orcad-activation-record'
import { readOrcadActivationRecord } from './orcad-activation-record-store'
import { resolveOrcadDeploymentTarget } from './orcad-deployment-target'
import { assertPosixOrcadHost } from './orcad-remote-host-support'
import { prepareWindowsOrcadHost } from './orcad-windows-host-preparation'
import { execOrcadRemote } from './orcad-remote-runtime-control'
import type { SshConnection } from './ssh-connection'
import { readRemoteHomeCommand } from './ssh-remote-commands'
import {
isWindowsRemoteHost,
joinRemotePath,
normalizeRemoteHome,
validateRemoteHome,
@@ -35,8 +36,6 @@ export async function resolveOrcadRemoteContext(
if (!host) {
throw new Error('This SSH host platform is not supported by managed orcad.')
}
// Why first: every lifecycle step after this is POSIX-only, so refuse before probing further.
assertPosixOrcadHost(host)
const remote = { conn: connection, host, signal }
const remoteHome = normalizeRemoteHome(
await execOrcadRemote(remote, readRemoteHomeCommand(host)),
@@ -46,6 +45,10 @@ export async function resolveOrcadRemoteContext(
throw new Error(`Remote home is not a valid path: ${remoteHome.slice(0, 100)}`)
}
const serverTarget = await resolveOrcadDeploymentTarget({ conn: connection, host, signal })
if (isWindowsRemoteHost(host)) {
// Every Windows host op, the activation record read included, runs on the pinned node.exe.
await prepareWindowsOrcadHost({ conn: connection, host, remoteHome, serverTarget, signal })
}
const activationRecord = await readOrcadActivationRecord({ ...remote, remoteHome })
return {
activationRecord,
-2
View File
@@ -14,7 +14,6 @@ import { readOrcadActivationRecord } from './orcad-activation-record-store'
import type { OrcadActivationVerdict } from './orcad-activation-gate'
import type { OrcadTerminalCensus } from './orcad-update-plan'
import type { RemoteHostPlatform } from './ssh-remote-platform'
import { assertPosixOrcadHost } from './orcad-remote-host-support'
import { installOrcadBundle } from './orcad-remote-install'
import { getAppEnvironment } from '../../shared/app-environment'
import type { ServerTarget } from '../../shared/node-runtime-pin'
@@ -62,7 +61,6 @@ export type OrcadDeployResult =
/** Activate on a healthy verdict; retain changed candidate state for explicit recovery. */
export async function deployOrcad(input: OrcadDeployOptions): Promise<OrcadDeployResult> {
assertPosixOrcadHost(input.host)
const target =
input.target ??
(input.localOrcadDir
+4 -5
View File
@@ -3,9 +3,8 @@
* discovered at runtime.
*
* The install transaction is host-agnostic — it is the relay's, and the relay runs on
* Windows. Launch, readiness, liveness, stop, build hash and record files have Windows
* branches; snapshots, the deploy and rollback drivers, preflight and managed stop do not yet,
* so the managed lifecycle still refuses Windows hosts at context resolution.
* Windows. Deploy, rollback and their recovery run on Windows through the host script
* (`orcad-windows-host-script.ts`); managed stop, decommission and GC do not yet, and refuse.
*/
import { isWindowsRemoteHost, type RemoteHostPlatform } from './ssh-remote-platform'
@@ -14,8 +13,8 @@ export class OrcadRemoteLaunchUnsupportedError extends Error {
constructor(hostLabel: string) {
super(
`Deploying orcad to a ${hostLabel} host is not implemented. The install transaction is ` +
'host-agnostic, but state snapshots, preflight, rollback and managed stop are ' +
'POSIX-only. Use the relay for this host.'
'host-agnostic, but managed stop, decommission and version GC are POSIX-only. ' +
'Use the relay for this host.'
)
this.name = 'OrcadRemoteLaunchUnsupportedError'
}
@@ -0,0 +1,235 @@
/**
* The deploy and rollback drivers end to end against a Windows host whose every host op is answered by a
* fake: no POSIX command, no `-EncodedCommand` and no PowerShell hop on the orcad path.
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as RecordFile from './orcad-remote-record-file'
vi.mock('./ssh-relay-deploy-helpers', () => ({
execCommand: vi.fn(),
isUnconfirmedSshCommandTermination: () => false
}))
vi.mock('./ssh-relay-install-lock', () => ({
acquireInstallLock: vi.fn().mockResolvedValue(undefined),
RELAY_INSTALL_LOCK_NAME: '.install-lock'
}))
vi.mock('./ssh-relay-install-transfers', () => ({
uploadRelayDirectory: vi.fn().mockResolvedValue(undefined),
writeRelayFile: vi.fn().mockResolvedValue(undefined)
}))
vi.mock('./orcad-remote-record-file', async (importOriginal) => ({
...(await importOriginal<typeof RecordFile>()),
writeAtomicOrcadRemoteRecord: vi.fn().mockResolvedValue(undefined)
}))
vi.mock('./orcad-remote-node-runtime', () => ({
ensureRemoteOrcadNodeRuntime: vi.fn().mockResolvedValue(undefined)
}))
vi.mock('./orcad-local-build-hash', () => ({
computeLocalOrcadBuildHash: () => 'abc123def4567890'
}))
vi.mock('./ssh-relay-versioned-install', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
readLocalFullVersion: () => '0.2.0+bb0100000000',
isRemoteInstallComplete: vi.fn().mockResolvedValue(false),
finalizeInstall: vi.fn().mockResolvedValue(undefined),
abandonInstall: vi.fn().mockResolvedValue(undefined)
}))
import { execCommand } from './ssh-relay-deploy-helpers'
import { deployOrcad, type OrcadDeployOptions } from './orcad-remote-deploy'
import { rollbackOrcad } from './orcad-remote-rollback'
import { emptyOrcadActivationRecord } from './orcad-activation-record'
import { writeAtomicOrcadRemoteRecord } from './orcad-remote-record-file'
import { getRemoteHostPlatform } from './ssh-remote-platform'
import { NODE_RUNTIME_PIN } from '../../shared/node-runtime-pin'
const mockExec = vi.mocked(execCommand)
const host = getRemoteHostPlatform('win32-x64')
const VERSION = '0.2.0+bb0100000000'
const SLOT_NODE = 'C:\\Users\\u\\.orca-remote\\runtimes\\node-ab\\node.exe'
const encoded = (marker: string, value: string): string =>
`${marker} ${Buffer.from(value).toString('base64')}\r\n`
const TARGET = '0.1.0+aa01'
function readyLine(version = VERSION): string {
return `${JSON.stringify({
type: 'orca_server_ready',
runtimeId: 'r1',
boundEndpoint: 'ws://127.0.0.1:7777',
advertisedEndpoint: null,
managedWslCliReconciliation: 'settled',
pairing: { available: false, reason: 'disabled_by_operator', guidance: 'n/a' },
health: {
buildHash: 'abc123def4567890',
buildVersion: version,
nodeVersion: NODE_RUNTIME_PIN.version,
nodeAbi: '137',
platform: 'win32',
arch: 'x64',
pid: 4242,
stopRequests: 1,
terminalDaemon: {
state: 'live',
ownsFreshSessions: true,
pid: 2,
buildVersion: version,
entryPath: 'C:/x/daemon-entry.js',
protocolVersion: 3,
selfTest: { ok: true, coverage: 'handshake', verdict: 'healthy', durationMs: 5 }
}
}
})}\n`
}
function scriptWindowsHost(log: string[], activeRecord: string | null = null): void {
mockExec.mockImplementation(async (_conn, command: string) => {
const text = String(command)
log.push(text)
const op = /\.js ([a-z-]+)(?: |$)/u.exec(text)?.[1] ?? ''
switch (op) {
case 'record-read':
return activeRecord && text.includes('orcad-active.json')
? encoded('__ORCAD_RECORD_PRESENT__', activeRecord)
: '__ORCAD_RECORD_ABSENT__\r\n'
case 'build-hash':
return '__ORCAD_BUILD_HASH__ abc123def4567890\r\n'
case 'owner-admission':
return 'CLEAR'
case 'slot-runtime':
return encoded('__ORCAD_RUNTIME__', SLOT_NODE)
case 'readiness-wait':
return encoded('__ORCAD_READINESS__', readyLine(text.includes(TARGET) ? TARGET : VERSION))
case 'stop':
return 'STOPPED'
case 'snapshot-probe':
return 'PRESENT'
case 'snapshot-restore':
return 'RESTORED'
case 'state-newest-mtime':
return 'UNKNOWN'
case 'snapshot-capture':
return 'CAPTURED'
case 'remove-file':
case 'remove-tree':
return ''
default:
break
}
if (text.includes('--orcad-profile-state-preflight')) {
return JSON.stringify({
type: 'orca_profile_state_ready',
nonce: text.match(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/)?.[0],
runtime: 'node',
runtimeVersion: NODE_RUNTIME_PIN.version,
sqliteVersion: '3.51.0',
artifactVersion: VERSION,
revision: 1
})
}
if (text.includes('--windows-breakaway-launch')) {
return 'ORCA_ORCAD_LAUNCH {"method":"breakaway","pid":4242,"inJob":false}\r\n'
}
// The relay's shared install fence is the one pre-existing PowerShell site left on this path.
if (text.startsWith('powershell.exe ')) {
return 'OPEN'
}
throw new Error(`unexpected Windows command: ${text}`)
})
}
function options(): OrcadDeployOptions {
return {
conn: Object.assign(Object.create(null), { writeFile: vi.fn().mockResolvedValue(undefined) }),
host,
remoteHome: 'C:/Users/u',
localOrcadDir: '/local/out/orcad',
target: 'win32-x64',
nodePath: 'C:/host/node.exe',
userDataDir: 'C:/Users/u/.orca',
bindHost: '127.0.0.1',
port: 7777,
census: { liveSessions: 0, startedSinceActivation: 0, daemonProtocolVersion: 3 },
readinessTimeoutMs: 1_000,
sleep: async () => {},
now: () => new Date('2026-10-02T00:00:00.000Z')
}
}
beforeEach(() => {
mockExec.mockReset()
vi.mocked(writeAtomicOrcadRemoteRecord).mockClear()
})
describe('deployOrcad on a Windows host', () => {
it('activates a first install through node.exe host ops only', async () => {
const log: string[] = []
scriptWindowsHost(log)
const result = await deployOrcad(options())
expect(result).toMatchObject({ outcome: 'installed-and-activated', fullVersion: VERSION })
const orcadOps = log.filter((command) => !command.startsWith('powershell.exe '))
expect(orcadOps.length).toBeGreaterThan(0)
for (const command of orcadOps) {
expect(command).not.toMatch(/EncodedCommand|nohup|kill |head -c|tar |\bsh -c\b/u)
}
const ops = orcadOps.map((command) => /\.js ([a-z-]+)/u.exec(command)?.[1] ?? command)
expect(ops).toContain('owner-admission')
expect(ops).toContain('snapshot-capture')
expect(ops.indexOf('slot-runtime')).toBeLessThan(
ops.findIndex((entry) => entry.includes('--windows-breakaway-launch'))
)
expect(ops).toContain('readiness-wait')
const record = vi
.mocked(writeAtomicOrcadRemoteRecord)
.mock.calls.find(([, path]) => path.endsWith('orcad-active.json'))
expect(JSON.parse(record?.[2] ?? '{}')).toMatchObject({ active: VERSION })
})
it('rolls back by stop request, directory snapshot and node.exe launch', async () => {
const log: string[] = []
const record = {
...emptyOrcadActivationRecord(),
active: VERSION,
previous: TARGET,
activatedAt: '2026-10-01T00:00:00.000Z',
snapshot: {
dirName: `pre-${VERSION}-1000`,
takenBeforeVersion: VERSION,
readableByVersion: TARGET,
takenAt: '2026-10-01T00:00:00.000Z'
}
}
scriptWindowsHost(log, JSON.stringify(record))
const { localOrcadDir: _dir, target: _target, force: _force, ...base } = options()
const result = await rollbackOrcad({
...base,
record,
targetBuildHash: 'abc123def4567890',
targetDaemonProtocol: { protocolVersion: 3, previousProtocolVersions: [1, 2] }
})
expect(result).toMatchObject({ outcome: 'rolled-back', target: TARGET })
const ops = log.map((command) => /\.js ([a-z-]+)/u.exec(command)?.[1] ?? command)
// Stop before any state is touched; the target starts only after its state is back.
expect(ops.indexOf('stop')).toBeLessThan(ops.indexOf('snapshot-restore'))
expect(ops).toEqual([
'record-read',
'record-read',
'snapshot-probe',
'state-newest-mtime',
'build-hash',
'stop',
'snapshot-capture',
'snapshot-restore',
'slot-runtime',
'--windows-breakaway-launch',
'readiness-wait',
'record-read',
'remove-file',
'remove-tree'
])
for (const command of log) {
expect(command).not.toMatch(/EncodedCommand|kill |tar |nohup/u)
}
})
})
+50 -6
View File
@@ -7,10 +7,17 @@ import {
parseOrcadProfilePreflight
} from '../../shared/orcad-profile-preflight'
import { assertPosixOrcadHost } from './orcad-remote-host-support'
import {
orcadWindowsBaseDir,
orcadWindowsHostOpCommand,
orcadWindowsNodeCommandLine,
readOrcadWindowsEncodedAnswer
} from './orcad-remote-windows-node'
import { ORCAD_WINDOWS_RUNTIME_MARKER } from './orcad-windows-host-script'
import { orcadNodeSlotRuntimeCommand } from './orcad-remote-runtime'
import { execCommand } from './ssh-relay-deploy-helpers'
import { shellEscape } from './ssh-connection-utils'
import { joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform'
import { isWindowsRemoteHost, joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform'
import type { SshConnection } from './ssh-connection'
export function orcadProfilePreflightCommand(
@@ -40,10 +47,47 @@ export async function preflightInstalledOrcad(options: {
signal?: AbortSignal
}): Promise<void> {
const nonce = randomUUID()
const output = await execCommand(
options.conn,
orcadProfilePreflightCommand(options.host, options.remoteInstallDir, nonce),
{ signal: options.signal, timeoutMs: ORCAD_PROFILE_PREFLIGHT_TIMEOUT_MS }
)
const command = isWindowsRemoteHost(options.host)
? await windowsOrcadProfilePreflightCommand(options, nonce)
: orcadProfilePreflightCommand(options.host, options.remoteInstallDir, nonce)
const output = await execCommand(options.conn, command, {
signal: options.signal,
timeoutMs: ORCAD_PROFILE_PREFLIGHT_TIMEOUT_MS,
wrapCommand: !isWindowsRemoteHost(options.host)
})
parseOrcadProfilePreflight(output, nonce, ORCAD_NODE_RUNTIME_IDENTITY, options.fullVersion)
}
/**
* Windows: resolve the slot's node.exe, then run its `orcad.js` with plain argv. No
* ORCA_BACKGROUND_LAUNCH here: orcad opens no window, and argv cannot set environment.
*/
async function windowsOrcadProfilePreflightCommand(
options: {
conn: SshConnection
host: RemoteHostPlatform
remoteInstallDir: string
signal?: AbortSignal
},
nonce: string
): Promise<string> {
const { host, remoteInstallDir } = options
const runtime = readOrcadWindowsEncodedAnswer(
await execCommand(
options.conn,
orcadWindowsHostOpCommand(host, orcadWindowsBaseDir(host, remoteInstallDir), 'slot-runtime', [
remoteInstallDir
]),
{ signal: options.signal, wrapCommand: false }
),
ORCAD_WINDOWS_RUNTIME_MARKER
)
if (!runtime) {
throw new Error('The Windows host did not name the runtime this orcad slot needs.')
}
return orcadWindowsNodeCommandLine(runtime, [
joinRemotePath(host, remoteInstallDir, 'orcad.js'),
ORCAD_PROFILE_PREFLIGHT_FLAG,
nonce
])
}
+21 -3
View File
@@ -7,9 +7,11 @@ vi.mock('./ssh-relay-deploy-helpers', () => ({
}))
vi.mock('./ssh-remote-platform-detection', () => ({ detectRemoteHostPlatform: vi.fn() }))
vi.mock('./orcad-windows-host-preparation', () => ({ prepareWindowsOrcadHost: vi.fn() }))
import { execCommand } from './ssh-relay-deploy-helpers'
import { detectRemoteHostPlatform } from './ssh-remote-platform-detection'
import { prepareWindowsOrcadHost } from './orcad-windows-host-preparation'
import { resolveOrcadRemoteContext } from './orcad-remote-context'
import { getRemoteHostPlatform } from './ssh-remote-platform'
import type { SshConnection } from './ssh-connection'
@@ -234,10 +236,26 @@ describe('readiness parsing bounds', () => {
})
describe('orcad remote context', () => {
it('refuses a Windows host before probing anything else', async () => {
it('prepares a Windows host (runtime, host script) before reading the activation record', async () => {
vi.mocked(detectRemoteHostPlatform).mockResolvedValueOnce(windows)
const order: string[] = []
vi.mocked(prepareWindowsOrcadHost).mockImplementationOnce(async () => {
order.push('prepare')
})
mockExec.mockImplementation(async (_conn, command: string) => {
if (command.includes('record-read')) {
order.push('record')
return '__ORCAD_RECORD_ABSENT__\r\n'
}
return 'C:\\Users\\u\r\n'
})
const sshTarget = { id: 't', label: 't', host: 'h', port: 22, username: 'u' }
await expect(resolveOrcadRemoteContext(sshTarget, conn)).rejects.toThrow()
expect(mockExec).not.toHaveBeenCalled()
const context = await resolveOrcadRemoteContext(sshTarget, conn)
expect(context).toMatchObject({ serverTarget: 'win32-x64', remoteHome: 'C:/Users/u' })
expect(vi.mocked(prepareWindowsOrcadHost).mock.calls[0]?.[0]).toMatchObject({
remoteHome: 'C:/Users/u',
serverTarget: 'win32-x64'
})
expect(order).toEqual(['prepare', 'record'])
})
})
-2
View File
@@ -22,7 +22,6 @@ import { readOrcadActivationRecord } from './orcad-activation-record-store'
import { sameOrcadActivationRecord } from './orcad-activation-transaction'
import { readOrcadActivationTransaction } from './orcad-activation-transaction-store'
import type { RemoteHostPlatform } from './ssh-remote-platform'
import { assertPosixOrcadHost } from './orcad-remote-host-support'
import {
resolveOrcadActivationReadinessTimeout,
withOrcadActivationLock
@@ -58,7 +57,6 @@ export type OrcadRollbackResult =
| { outcome: 'failed'; code: string; reason: string }
export async function rollbackOrcad(input: OrcadRollbackOptions): Promise<OrcadRollbackResult> {
assertPosixOrcadHost(input.host)
const options = {
...input,
readinessTimeoutMs: resolveOrcadActivationReadinessTimeout(
@@ -16,6 +16,7 @@ import {
} from '../../shared/orcad-artifacts'
import { pinnedNodeRuntimeAsset, type NodeRuntimeTarget } from '../../shared/node-runtime-pin'
import type { SshConnection } from './ssh-connection'
import { RELAY_REMOTE_DIR } from './relay-protocol'
import { joinRemotePath, remoteDirname, type RemoteHostPlatform } from './ssh-remote-platform'
import { powerShellLiteral } from './ssh-remote-powershell'
import {
@@ -73,6 +74,11 @@ export function orcadWindowsNodeCommandLine(executable: string, args: readonly s
return `powershell.exe -NoProfile -NonInteractive -Command "${inner}"`
}
/** `~/.orca-remote` from the remote home. */
export function orcadRemoteBaseDir(host: RemoteHostPlatform, remoteHome: string): string {
return joinRemotePath(host, remoteHome, RELAY_REMOTE_DIR)
}
/** `~/.orca-remote`, the parent of every slot and of the runtime store. */
export function orcadWindowsBaseDir(host: RemoteHostPlatform, slotDir: string): string {
return remoteDirname(slotDir.replace(/\/+$/u, ''), host)
+18 -4
View File
@@ -1,4 +1,5 @@
/** The locked, journaled half of `rollbackOrcad`. */
import { orcadRemoteBaseDir } from './orcad-remote-windows-node'
import { randomUUID } from 'node:crypto'
import type { OrcadRollbackOptions, OrcadRollbackResult } from './orcad-remote-rollback'
import { isUnconfirmedSshCommandTermination } from './ssh-relay-deploy-helpers'
@@ -58,7 +59,14 @@ async function stateWritesSinceActivation(options: OrcadRollbackOptions): Promis
return null
}
const newest = parseNewestStateMtimeSeconds(
await execOrEmpty(options, newestStateMtimeCommand(options.host, options.userDataDir))
await execOrEmpty(
options,
newestStateMtimeCommand(
options.host,
options.userDataDir,
orcadRemoteBaseDir(options.host, options.remoteHome)
)
)
)
return newest === null ? null : newest >= activatedAtSeconds
}
@@ -73,7 +81,11 @@ export async function rollbackOrcadLocked(
? parseOrcadSnapshotPresence(
await execOrEmpty(
options,
probeOrcadStateSnapshotCommand(options.host, orcadSnapshotPath(options, snapshot.dirName))
probeOrcadStateSnapshotCommand(
options.host,
orcadSnapshotPath(options, snapshot.dirName),
orcadRemoteBaseDir(options.host, options.remoteHome)
)
)
)
: 'absent'
@@ -145,7 +157,8 @@ export async function rollbackOrcadLocked(
captureOrcadStateSnapshotCommand(
options.host,
options.userDataDir,
orcadSnapshotPath(options, transaction.rescue.dirName)
orcadSnapshotPath(options, transaction.rescue.dirName),
orcadRemoteBaseDir(options.host, options.remoteHome)
)
)
)
@@ -169,7 +182,8 @@ export async function rollbackOrcadLocked(
restoreOrcadStateSnapshotCommand(
options.host,
options.userDataDir,
orcadSnapshotPath(options, snapshot.dirName)
orcadSnapshotPath(options, snapshot.dirName),
orcadRemoteBaseDir(options.host, options.remoteHome)
)
)
)
@@ -0,0 +1,22 @@
/** What the pre-activation snapshot holds; shared by the POSIX commands and the Windows host script. */
/**
* Root-relative paths a rollback needs restored. Everything else under the data root is
* either regenerable, or owned by a process that survives the rollback.
*/
export const ORCAD_SNAPSHOT_MEMBERS = [
'orca-profile-index.json',
// Pre-profiles layout; still read as a migration source.
'orca-data.json',
'profiles',
// Cross-profile SQLite moves must survive an orcad rollback too.
'profile-move-intents'
] as const
/** Never captured and never restored — see `orcad-state-snapshot.ts`. */
export const ORCAD_SNAPSHOT_EXCLUDED = ['daemon', 'logs'] as const
export const ORCAD_STATE_RESTORE_STAGE_DIRNAME = '.orcad-state-restore-stage'
/** Windows keeps the snapshot as a directory copy, where POSIX keeps `state.tar`. */
export const ORCAD_WINDOWS_SNAPSHOT_STATE_DIRNAME = 'state'
+33 -8
View File
@@ -105,14 +105,39 @@ describe('detecting writes since activation', () => {
})
describe('Windows hosts', () => {
const BASE = 'C:/Users/u/.orca-remote'
it.each([
['capture', () => captureOrcadStateSnapshotCommand(windows, ROOT, SNAP)],
['restore', () => restoreOrcadStateSnapshotCommand(windows, ROOT, SNAP)],
['clear', () => clearOrcadStateSnapshotMembersCommand(windows, ROOT)],
['presence', () => probeOrcadStateSnapshotCommand(windows, SNAP)],
['compare', () => compareOrcadStateSnapshotCommand(windows, ROOT, SNAP)],
['mtime', () => newestStateMtimeCommand(windows, ROOT)]
])('refuses %s rather than emitting a POSIX command', (_label, build) => {
expect(build).toThrow('orcad to a Windows host is not implemented')
[
'capture',
'snapshot-capture',
(base?: string) => captureOrcadStateSnapshotCommand(windows, ROOT, SNAP, base)
],
[
'restore',
'snapshot-restore',
(base?: string) => restoreOrcadStateSnapshotCommand(windows, ROOT, SNAP, base)
],
[
'clear',
'snapshot-clear',
(base?: string) => clearOrcadStateSnapshotMembersCommand(windows, ROOT, base)
],
[
'presence',
'snapshot-probe',
(base?: string) => probeOrcadStateSnapshotCommand(windows, SNAP, base)
],
[
'compare',
'snapshot-compare',
(base?: string) => compareOrcadStateSnapshotCommand(windows, ROOT, SNAP, base)
],
['mtime', 'state-newest-mtime', (base?: string) => newestStateMtimeCommand(windows, ROOT, base)]
])('%s runs the host script op %s with node.exe, never a POSIX command', (_label, op, build) => {
const command = build(BASE)
expect(command).toContain(` ${op} `)
expect(command).toMatch(/^C:\\Users\\u\\\.orca-remote\\runtimes\\node-[0-9a-f]+\\node\.exe /u)
expect(command).not.toMatch(/tar |find |diff |EncodedCommand|powershell/u)
expect(() => build()).toThrow('~/.orca-remote')
})
})
+64 -30
View File
@@ -16,24 +16,15 @@
* massacre the daemon exists to prevent.
*/
import { shellEscape } from './ssh-connection-utils'
import { joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform'
import { assertPosixOrcadHost as assertPosixHost } from './orcad-remote-host-support'
import { isWindowsRemoteHost, joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform'
import type { OrcadWindowsHostStateOp } from './orcad-windows-host-state-ops'
import {
ORCAD_SNAPSHOT_MEMBERS,
ORCAD_STATE_RESTORE_STAGE_DIRNAME
} from './orcad-state-snapshot-members'
import { orcadWindowsHostOpCommand } from './orcad-remote-windows-node'
/**
* Root-relative paths a rollback needs restored. Everything else under the data root is
* either regenerable, or owned by a process that survives the rollback.
*/
export const ORCAD_SNAPSHOT_MEMBERS = [
'orca-profile-index.json',
// Pre-profiles layout; still read as a migration source.
'orca-data.json',
'profiles',
// Cross-profile SQLite moves must survive an orcad rollback too.
'profile-move-intents'
] as const
/** Never captured and never restored — see the module comment. */
export const ORCAD_SNAPSHOT_EXCLUDED = ['daemon', 'logs'] as const
export { ORCAD_SNAPSHOT_EXCLUDED, ORCAD_SNAPSHOT_MEMBERS } from './orcad-state-snapshot-members'
/**
* The member names go into the command unquoted (see `captureOrcadStateSnapshotCommand`), so
@@ -47,7 +38,23 @@ function assertPlainMemberName(member: string): string {
return member
}
const RESTORE_STAGE_DIRNAME = '.orcad-state-restore-stage'
const RESTORE_STAGE_DIRNAME = ORCAD_STATE_RESTORE_STAGE_DIRNAME
/** The Windows host-script op, or null on POSIX; `baseDir` is `~/.orca-remote`. */
function windowsStateCommand(
host: RemoteHostPlatform,
baseDir: string | undefined,
op: OrcadWindowsHostStateOp,
args: string[]
): string | null {
if (!isWindowsRemoteHost(host)) {
return null
}
if (!baseDir) {
throw new Error('Windows orcad state commands need the ~/.orca-remote directory')
}
return orcadWindowsHostOpCommand(host, baseDir, op, args)
}
function noSymlinkedStateCommand(path: string): string {
return `links=$(find ${path} -type l -print) && [ -z "$links" ]`
@@ -75,9 +82,13 @@ export function orcadRollbackRescueDirName(fullVersion: string, takenAtMs: numbe
export function captureOrcadStateSnapshotCommand(
host: RemoteHostPlatform,
userDataDir: string,
snapshotDir: string
snapshotDir: string,
baseDir?: string
): string {
assertPosixHost(host)
const windows = windowsStateCommand(host, baseDir, 'snapshot-capture', [userDataDir, snapshotDir])
if (windows) {
return windows
}
const root = shellEscape(userDataDir)
const dir = shellEscape(snapshotDir)
const archive = shellEscape(joinRemotePath(host, snapshotDir, 'state.tar'))
@@ -118,9 +129,13 @@ export function parseOrcadSnapshotCapture(output: string): OrcadSnapshotCapture
export function probeOrcadStateSnapshotCommand(
host: RemoteHostPlatform,
snapshotDir: string
snapshotDir: string,
baseDir?: string
): string {
assertPosixHost(host)
const windows = windowsStateCommand(host, baseDir, 'snapshot-probe', [snapshotDir])
if (windows) {
return windows
}
const archive = shellEscape(joinRemotePath(host, snapshotDir, 'state.tar'))
return `test -f ${archive} && echo PRESENT || echo ABSENT`
}
@@ -151,9 +166,13 @@ export function parseOrcadSnapshotPresence(output: string): OrcadSnapshotPresenc
export function restoreOrcadStateSnapshotCommand(
host: RemoteHostPlatform,
userDataDir: string,
snapshotDir: string
snapshotDir: string,
baseDir?: string
): string {
assertPosixHost(host)
const windows = windowsStateCommand(host, baseDir, 'snapshot-restore', [userDataDir, snapshotDir])
if (windows) {
return windows
}
const root = shellEscape(userDataDir)
const archive = shellEscape(joinRemotePath(host, snapshotDir, 'state.tar'))
const stage = shellEscape(joinRemotePath(host, userDataDir, RESTORE_STAGE_DIRNAME))
@@ -183,9 +202,13 @@ export function restoreOrcadStateSnapshotCommand(
/** Restore an originally empty state root after a candidate populated it. */
export function clearOrcadStateSnapshotMembersCommand(
host: RemoteHostPlatform,
userDataDir: string
userDataDir: string,
baseDir?: string
): string {
assertPosixHost(host)
const windows = windowsStateCommand(host, baseDir, 'snapshot-clear', [userDataDir])
if (windows) {
return windows
}
const root = shellEscape(userDataDir)
const removals = ORCAD_SNAPSHOT_MEMBERS.map(
(member) => `rm -rf ${root}/${shellEscape(member)}`
@@ -207,9 +230,13 @@ export function parseOrcadSnapshotRestore(output: string): OrcadSnapshotRestore
export function compareOrcadStateSnapshotCommand(
host: RemoteHostPlatform,
userDataDir: string,
snapshotDir: string
snapshotDir: string,
baseDir?: string
): string {
assertPosixHost(host)
const windows = windowsStateCommand(host, baseDir, 'snapshot-compare', [userDataDir, snapshotDir])
if (windows) {
return windows
}
const root = shellEscape(userDataDir)
const dir = shellEscape(snapshotDir)
const archive = shellEscape(joinRemotePath(host, snapshotDir, 'state.tar'))
@@ -246,8 +273,15 @@ export function orcadSnapshotIsUnchanged(output: string): boolean {
* caller compares; an `UNKNOWN` becomes `null`, which `assessOrcadRollback` treats as "yes,
* assume writes".
*/
export function newestStateMtimeCommand(host: RemoteHostPlatform, userDataDir: string): string {
assertPosixHost(host)
export function newestStateMtimeCommand(
host: RemoteHostPlatform,
userDataDir: string,
baseDir?: string
): string {
const windows = windowsStateCommand(host, baseDir, 'state-newest-mtime', [userDataDir])
if (windows) {
return windows
}
const root = shellEscape(userDataDir)
const paths = ORCAD_SNAPSHOT_MEMBERS.map((member) => `${root}/${shellEscape(member)}`).join(' ')
return [
@@ -0,0 +1,112 @@
// Managed orcad on a real Win32-OpenSSH host, one cell per DefaultShell: resolve the context
// (pinned node.exe, host script), deploy and activate, prove readiness and liveness, stop through
// the slot's request file, and prove exit. config/ci/windows-ssh-provider/invoke-pinned-relay-cells.ps1
// provisions the account and runs this file for `orcad-*` cells; ssh-windows-hosts.yml runs that.
//
// Run: ORCA_RUN_SSH_WINDOWS_HOST=1 ORCA_SSH_WINDOWS_HOST_CELL=<descriptor.json> pnpm test <this file>
import { randomUUID } from 'node:crypto'
import { writeFileSync } from 'node:fs'
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
vi.mock('electron', () => ({ app: { getAppPath: () => process.cwd() } }))
import { SshConnection } from './ssh-connection'
import {
connectHostileHost,
installHostileHostAppEnvironment
} from './ssh-hostile-host-test-harness'
import { resolveOrcadRemoteContext } from './orcad-remote-context'
import { deployOrcad } from './orcad-remote-deploy'
import { orcadLivenessProbeCommand, parseOrcadLiveness } from './orcad-remote-launch'
import { orcadSlotDir, stopOrcadSlot, type OrcadSlotOptions } from './orcad-recovery-slot'
import { execOrcadRemote } from './orcad-remote-runtime-control'
import {
isWindowsOrcadCellId,
readWindowsHostCellDescriptor,
windowsHostSshTarget
} from './ssh-windows-host-cells'
const RUN = process.env.ORCA_RUN_SSH_WINDOWS_HOST === '1'
const CELL_TIMEOUT_MS = 20 * 60_000
/** Orcad's own host ops and launches: these must never take a PowerShell hop. */
function isOrcadHostCommand(command: string): boolean {
return /orcad-host-script-[0-9a-f]{16}\.js|[\\/]orcad\.js /u.test(command)
}
describe.runIf(RUN)('managed orcad on a Windows OpenSSH host', () => {
let cleanupAppEnvironment: (() => void) | null = null
beforeAll(() => {
cleanupAppEnvironment = installHostileHostAppEnvironment()
})
afterAll(() => {
cleanupAppEnvironment?.()
})
it(
'deploys, serves, stops by request and exits',
async () => {
const descriptor = readWindowsHostCellDescriptor(process.env.ORCA_SSH_WINDOWS_HOST_CELL ?? '')
if (!isWindowsOrcadCellId(descriptor.cell)) {
throw new Error(`${descriptor.cell} runs in ssh-relay-windows-host-lane.test.ts`)
}
const sshTarget = windowsHostSshTarget(
descriptor,
{ id: descriptor.cell, remoteRuntime: 'pinned-node' },
randomUUID()
)
const exec = vi.spyOn(SshConnection.prototype, 'exec')
const receipt: Record<string, unknown> = { cell: descriptor.cell, target: descriptor.target }
let conn: SshConnection | null = null
try {
conn = await connectHostileHost(sshTarget)
const context = await resolveOrcadRemoteContext(sshTarget, conn)
expect(context.host.os).toBe('win32')
const options: OrcadSlotOptions = {
conn,
host: context.host,
remoteHome: context.remoteHome,
nodePath: 'node',
userDataDir: context.userDataDir,
bindHost: '127.0.0.1',
port: 0
}
const deployed = await deployOrcad({
...options,
target: context.serverTarget,
census: { liveSessions: 0, startedSinceActivation: 0, daemonProtocolVersion: null }
})
receipt.deploy = deployed
expect(deployed.outcome).toBe('installed-and-activated')
const slotDir = orcadSlotDir(options, deployed.fullVersion)
const liveness = async () =>
parseOrcadLiveness(
await execOrcadRemote(options, orcadLivenessProbeCommand(options.host, slotDir))
)
expect(await liveness()).toBe('LIVE')
receipt.stop = await stopOrcadSlot(options, slotDir, false)
expect(receipt.stop).toBe('stopped')
expect(await liveness()).toBe('DEAD')
const commands = exec.mock.calls.map(([command]) => String(command))
const orcadCommands = commands.filter(isOrcadHostCommand)
receipt.orcadCommands = orcadCommands.length
receipt.powershellCommands = commands.filter((command) =>
/^powershell\.exe /iu.test(command)
).length
expect(orcadCommands.length).toBeGreaterThan(0)
for (const command of orcadCommands) {
expect(command).not.toMatch(/EncodedCommand/u)
}
receipt.passed = true
} finally {
exec.mockRestore()
await conn?.disconnect().catch(() => {})
writeFileSync(descriptor.receipt, `${JSON.stringify(receipt, null, 2)}\n`)
}
},
CELL_TIMEOUT_MS
)
})
@@ -0,0 +1,40 @@
/**
* What every managed-orcad operation on a Windows host needs before its first host op: this
* client's pinned node.exe in the runtime store, and the host script it runs.
*
* Both are idempotent: a present runtime costs one probe and nothing is uploaded, and the host
* script is content-addressed, so rewriting it changes nothing a running orcad depends on.
*/
import { join } from 'node:path'
import { getAppEnvironment } from '../../shared/app-environment'
import type { ServerTarget } from '../../shared/node-runtime-pin'
import { ensureRemoteOrcadNodeRuntime } from './orcad-remote-node-runtime'
import { installOrcadWindowsHostScript, orcadRemoteBaseDir } from './orcad-remote-windows-node'
import { materializeNodeRuntimeArchive } from './pinned-runtime-materializer'
import type { SshConnection } from './ssh-connection'
import { joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform'
export async function prepareWindowsOrcadHost(options: {
conn: SshConnection
host: RemoteHostPlatform
remoteHome: string
serverTarget: ServerTarget
signal?: AbortSignal
}): Promise<void> {
const baseDir = orcadRemoteBaseDir(options.host, options.remoteHome)
await ensureRemoteOrcadNodeRuntime({
conn: options.conn,
host: options.host,
// Only its parent is read: the runtime store sits beside the slots.
slotDir: joinRemotePath(options.host, baseDir, 'orcad-host'),
target: options.serverTarget,
archivePath: () =>
materializeNodeRuntimeArchive(
options.serverTarget,
join(getAppEnvironment().getPath('userData'), 'orcad-artifacts'),
{ signal: options.signal }
),
signal: options.signal
})
await installOrcadWindowsHostScript(options, baseDir)
}
+12
View File
@@ -23,6 +23,10 @@ import {
ORCAD_STOP_REQUEST_FILENAME,
ORCAD_STOP_REQUESTS_CAPABILITY
} from '../../shared/orcad-stop-request'
import {
ORCAD_WINDOWS_HOST_STATE_OPS,
type OrcadWindowsHostStateOp
} from './orcad-windows-host-state-ops'
import {
ORCAD_READINESS_FILENAME,
ORCAD_READINESS_MAX_BYTES,
@@ -48,6 +52,8 @@ export type OrcadWindowsHostOp =
| 'record-publish'
| 'slot-runtime'
| 'remove-file'
| 'remove-tree'
| OrcadWindowsHostStateOp
const text = JSON.stringify
@@ -187,6 +193,11 @@ const ops = {
answer('')
},
'remove-tree'(target) {
fs.rmSync(target, { recursive: true, force: true, maxRetries: 5 })
answer('')
},
// The node.exe a slot's marker names; with clear-stop-request, also drops a stale request.
'slot-runtime'(slotDir, mode) {
let sha
@@ -202,6 +213,7 @@ const ops = {
}
}
${ORCAD_WINDOWS_HOST_STATE_OPS}
const run = Object.hasOwn(ops, op) ? ops[op] : null
if (!run) {
process.stderr.write('unknown orcad host op: ' + String(op) + '\\n')
@@ -0,0 +1,123 @@
/**
* Runs the Windows host script's state ops under this machine's node, against real files:
* the snapshot a rollback depends on, its comparison and restore, and owner admission.
*/
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
utimesSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { runProcess } from '../../shared/child-process/run-process'
import { ORCAD_WINDOWS_HOST_SCRIPT, type OrcadWindowsHostOp } from './orcad-windows-host-script'
let dir = ''
let root = ''
let snapshot = ''
let script = ''
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'orcad-win-state-'))
root = join(dir, '.orca')
snapshot = join(dir, '.orca-remote', 'orcad-snapshots', 'pre-0.2.0+bb01-1')
script = join(dir, 'orcad-host-script.js')
writeFileSync(script, ORCAD_WINDOWS_HOST_SCRIPT)
mkdirSync(join(root, 'profiles', 'p1'), { recursive: true })
mkdirSync(join(root, 'daemon'), { recursive: true })
writeFileSync(join(root, 'orca-profile-index.json'), '{"v":"before"}')
writeFileSync(join(root, 'profiles', 'p1', 'orca-data.json'), '{"repos":"before"}')
writeFileSync(join(root, 'daemon', 'daemon.sock.token'), 'live-daemon-token')
})
afterEach(() => {
rmSync(dir, { recursive: true, force: true })
})
async function op(name: OrcadWindowsHostOp, ...args: string[]): Promise<string> {
const result = await runProcess({ program: process.execPath, args: [script, name, ...args] })
expect(result.code, result.stderr).toBe(0)
return result.stdout.trim()
}
describe('Windows snapshot ops', () => {
it('captures, proves unchanged, then restores the members and never the daemon', async () => {
expect(await op('snapshot-probe', snapshot)).toBe('ABSENT')
expect(await op('snapshot-capture', root, snapshot)).toBe('CAPTURED')
expect(await op('snapshot-probe', snapshot)).toBe('PRESENT')
expect(await op('snapshot-compare', root, snapshot)).toBe('UNCHANGED')
writeFileSync(join(root, 'profiles', 'p1', 'orca-data.json'), '{"repos":"after"}')
writeFileSync(join(root, 'profiles', 'p1', 'added.json'), '{}')
writeFileSync(join(root, 'daemon', 'daemon.sock.token'), 'rotated-token')
expect(await op('snapshot-compare', root, snapshot)).toBe('CHANGED')
expect(await op('snapshot-restore', root, snapshot)).toBe('RESTORED')
expect(readFileSync(join(root, 'profiles', 'p1', 'orca-data.json'), 'utf8')).toBe(
'{"repos":"before"}'
)
expect(existsSync(join(root, 'profiles', 'p1', 'added.json'))).toBe(false)
expect(readFileSync(join(root, 'daemon', 'daemon.sock.token'), 'utf8')).toBe('rotated-token')
expect(existsSync(join(root, '.orcad-state-restore-stage'))).toBe(false)
expect(await op('snapshot-compare', root, snapshot)).toBe('UNCHANGED')
})
it('reports EMPTY rather than fabricating a snapshot of a root with no state', async () => {
const empty = join(dir, 'empty-root')
mkdirSync(empty)
expect(await op('snapshot-capture', empty, snapshot)).toBe('EMPTY')
expect(await op('snapshot-probe', snapshot)).toBe('ABSENT')
})
it('fails closed on a link inside captured state', async () => {
symlinkSync(join(dir), join(root, 'profiles', 'escape'), 'junction')
expect(await op('snapshot-capture', root, snapshot)).toBe('FAILED')
expect(await op('snapshot-probe', snapshot)).toBe('ABSENT')
})
it('answers MISSING, UNKNOWN and FAILED rather than guessing', async () => {
expect(await op('snapshot-restore', root, snapshot)).toBe('MISSING')
expect(await op('snapshot-compare', root, snapshot)).toBe('UNKNOWN')
expect(await op('snapshot-compare', join(dir, 'no-root'), snapshot)).toBe('UNKNOWN')
})
it('clears the members of a root that started empty, leaving the daemon', async () => {
expect(await op('snapshot-clear', root)).toBe('RESTORED')
expect(existsSync(join(root, 'profiles'))).toBe(false)
expect(existsSync(join(root, 'orca-profile-index.json'))).toBe(false)
expect(existsSync(join(root, 'daemon', 'daemon.sock.token'))).toBe(true)
})
it('reports the newest member write in epoch seconds, or UNKNOWN', async () => {
const at = new Date('2026-09-30T12:00:00.000Z')
utimesSync(join(root, 'orca-profile-index.json'), at, at)
utimesSync(join(root, 'profiles', 'p1', 'orca-data.json'), at, at)
expect(await op('state-newest-mtime', root)).toBe(String(Math.floor(at.getTime() / 1000)))
const empty = join(dir, 'empty-root')
mkdirSync(empty)
expect(await op('state-newest-mtime', empty)).toBe('UNKNOWN')
})
})
describe('Windows owner admission', () => {
it('is CLEAR with no owners, LIVE for a running owner, and silent for a dead one', async () => {
expect(await op('owner-admission', root)).toBe('CLEAR')
writeFileSync(join(root, 'orcad.lock'), JSON.stringify({ pid: process.pid }))
expect(await op('owner-admission', root)).toBe(`LIVE orcad.lock ${process.pid}`)
writeFileSync(join(root, 'orcad.lock'), JSON.stringify({ pid: 4_194_303 }))
expect(await op('owner-admission', root)).toBe('CLEAR')
})
it('refuses a record it cannot interpret', async () => {
writeFileSync(join(root, 'orcad.lock'), 'not json')
expect(await op('owner-admission', root)).toBe('UNVERIFIABLE orcad.lock')
writeFileSync(join(root, 'orcad.lock'), JSON.stringify({ pid: -1 }))
expect(await op('owner-admission', root)).toBe('UNVERIFIABLE orcad.lock')
})
})
@@ -0,0 +1,196 @@
/**
* The host script's state ops on Windows: the pre-activation snapshot, its comparison and
* restore, the newest-write probe, and the first-activation owner admission.
*
* Same contract and tokens as the POSIX commands in `orcad-state-snapshot.ts` and
* `orcad-initial-activation-admission.ts`. The snapshot is a directory copy (`state/`) rather
* than a tar: there is no tar in Node, and spawning `tar.exe` would be a second process. It is
* built under a partial name and renamed into place, so a half-written snapshot is never
* `PRESENT`. Symlinks and junctions anywhere in captured state fail closed, as on POSIX.
*/
import { PRIMARY_RUNTIME_METADATA_FILE } from '../../shared/runtime-bootstrap'
import { ORCAD_LOCK_FILE_NAME } from '../orcad/orcad-instance-lock'
import {
ORCAD_SNAPSHOT_MEMBERS,
ORCAD_STATE_RESTORE_STAGE_DIRNAME,
ORCAD_WINDOWS_SNAPSHOT_STATE_DIRNAME
} from './orcad-state-snapshot-members'
export type OrcadWindowsHostStateOp =
| 'snapshot-capture'
| 'snapshot-probe'
| 'snapshot-restore'
| 'snapshot-clear'
| 'snapshot-compare'
| 'state-newest-mtime'
| 'owner-admission'
const OWNER_RECORD_MAX_BYTES = 64 * 1024
const text = JSON.stringify
/** Evaluated inside the host script, after `fs`, `path`, `answer` and `ops` exist. */
export const ORCAD_WINDOWS_HOST_STATE_OPS = `
const MEMBERS = ${text(ORCAD_SNAPSHOT_MEMBERS)}
const STATE_DIR = ${text(ORCAD_WINDOWS_SNAPSHOT_STATE_DIRNAME)}
const RESTORE_STAGE = ${text(ORCAD_STATE_RESTORE_STAGE_DIRNAME)}
function lstatOrNull(target) {
try { return fs.lstatSync(target) } catch (error) { if (error.code === 'ENOENT') return null; throw error }
}
// Node reports junctions as symbolic links too, so one check covers both.
function treeHasLink(target) {
const stats = fs.lstatSync(target)
if (stats.isSymbolicLink()) return true
if (!stats.isDirectory()) return false
return fs.readdirSync(target).some((name) => treeHasLink(path.join(target, name)))
}
function treesEqual(left, right) {
const a = lstatOrNull(left)
const b = lstatOrNull(right)
if (!a || !b) return !a && !b
if (a.isSymbolicLink() || b.isSymbolicLink()) throw new Error('link in state')
if (a.isDirectory() !== b.isDirectory()) return false
if (!a.isDirectory()) return a.size === b.size && fs.readFileSync(left).equals(fs.readFileSync(right))
const names = fs.readdirSync(left).sort()
const other = fs.readdirSync(right).sort()
return names.length === other.length && names.every((name, index) => name === other[index] && treesEqual(path.join(left, name), path.join(right, name)))
}
function newestMtime(target) {
const stats = lstatOrNull(target)
if (!stats || stats.isSymbolicLink()) return null
if (!stats.isDirectory()) return stats.mtimeMs
return fs.readdirSync(target).reduce((newest, name) => {
const value = newestMtime(path.join(target, name))
return value === null || (newest !== null && newest >= value) ? newest : value
}, null)
}
function renameWithRetry(from, to) {
for (const delay of [0, 50, 100, 150, 200, 250]) {
if (delay) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay)
try { fs.renameSync(from, to); return } catch (error) {
if (!['EPERM', 'EACCES', 'EBUSY'].includes(error.code) || delay === 250) throw error
}
}
}
const removeTree = (target) => fs.rmSync(target, { recursive: true, force: true, maxRetries: 5 })
Object.assign(ops, {
'snapshot-capture'(root, snapshotDir) {
let present
try {
present = MEMBERS.filter((member) => lstatOrNull(path.join(root, member)))
if (present.some((member) => treeHasLink(path.join(root, member)))) return answer('FAILED')
} catch { return answer('FAILED') }
if (present.length === 0) return answer('EMPTY')
const partial = path.join(snapshotDir, STATE_DIR + '.partial-' + process.pid)
try {
removeTree(partial)
fs.mkdirSync(partial, { recursive: true })
for (const member of present) {
fs.cpSync(path.join(root, member), path.join(partial, member), { recursive: true, errorOnExist: true })
}
removeTree(path.join(snapshotDir, STATE_DIR))
renameWithRetry(partial, path.join(snapshotDir, STATE_DIR))
} catch {
try { removeTree(partial) } catch {}
return answer('FAILED')
}
answer('CAPTURED')
},
'snapshot-probe'(snapshotDir) {
let stats
try { stats = lstatOrNull(path.join(snapshotDir, STATE_DIR)) } catch { return answer('UNKNOWN') }
answer(stats && stats.isDirectory() ? 'PRESENT' : 'ABSENT')
},
// Copy into a stage first, so an unreadable snapshot fails before live state is touched.
'snapshot-restore'(root, snapshotDir) {
const state = path.join(snapshotDir, STATE_DIR)
const stats = lstatOrNull(state)
if (!stats || !stats.isDirectory()) return answer('MISSING')
const stage = path.join(root, RESTORE_STAGE)
try {
fs.mkdirSync(root, { recursive: true })
removeTree(stage)
fs.cpSync(state, stage, { recursive: true })
if (!MEMBERS.some((member) => lstatOrNull(path.join(stage, member)))) {
removeTree(stage)
return answer('FAILED')
}
} catch {
try { removeTree(stage) } catch {}
return answer('FAILED')
}
try {
for (const member of MEMBERS) removeTree(path.join(root, member))
for (const member of MEMBERS) {
if (lstatOrNull(path.join(stage, member))) renameWithRetry(path.join(stage, member), path.join(root, member))
}
removeTree(stage)
} catch { return answer('FAILED') }
answer('RESTORED')
},
'snapshot-clear'(root) {
try {
fs.mkdirSync(root, { recursive: true })
for (const member of MEMBERS) removeTree(path.join(root, member))
} catch { return answer('FAILED') }
answer('RESTORED')
},
'snapshot-compare'(root, snapshotDir) {
try {
const rootStats = lstatOrNull(root)
const state = path.join(snapshotDir, STATE_DIR)
const stateStats = lstatOrNull(state)
if (!rootStats || !rootStats.isDirectory() || !stateStats || !stateStats.isDirectory()) return answer('UNKNOWN')
if (treeHasLink(state)) return answer('UNKNOWN')
for (const member of MEMBERS) {
const live = path.join(root, member)
if (lstatOrNull(live) && treeHasLink(live)) return answer('UNKNOWN')
if (!treesEqual(live, path.join(state, member))) return answer('CHANGED')
}
} catch { return answer('UNKNOWN') }
answer('UNCHANGED')
},
'state-newest-mtime'(root) {
let newest = null
try {
for (const member of MEMBERS) {
const value = newestMtime(path.join(root, member))
if (value !== null && (newest === null || value > newest)) newest = value
}
} catch { return answer('UNKNOWN') }
answer(newest === null ? 'UNKNOWN' : String(Math.floor(newest / 1000)))
},
// A live, unreadable or unexpected owner record defers the first activation.
'owner-admission'(userDataDir) {
const owners = ${text([ORCAD_LOCK_FILE_NAME, PRIMARY_RUNTIME_METADATA_FILE])}
for (const name of owners) {
const file = path.join(userDataDir, name)
let pid
try {
const stats = lstatOrNull(file)
if (!stats) continue
if (!stats.isFile() || stats.size > ${OWNER_RECORD_MAX_BYTES}) return answer('UNVERIFIABLE ' + name)
pid = JSON.parse(fs.readFileSync(file, 'utf8')).pid
} catch { return answer('UNVERIFIABLE ' + name) }
if (!Number.isSafeInteger(pid) || pid <= 0) return answer('UNVERIFIABLE ' + name)
try { process.kill(pid, 0); return answer('LIVE ' + name + ' ' + pid) } catch (error) {
if (error.code === 'EPERM') return answer('LIVE ' + name + ' ' + pid)
if (error.code !== 'ESRCH') return answer('UNVERIFIABLE ' + name)
}
}
answer('CLEAR')
}
})
`
@@ -31,6 +31,7 @@ import {
type WindowsSessionCommandAudit
} from './ssh-session-command-audit'
import {
isWindowsOrcadCellId,
readWindowsHostCellDescriptor,
windowsHostCell,
windowsHostSshTarget
@@ -54,6 +55,9 @@ describe.runIf(RUN)('SSH relay on a Windows OpenSSH host', () => {
'lands the cell the descriptor names',
async () => {
const descriptor = readWindowsHostCellDescriptor(process.env.ORCA_SSH_WINDOWS_HOST_CELL ?? '')
if (isWindowsOrcadCellId(descriptor.cell)) {
throw new Error(`${descriptor.cell} runs in orcad-windows-host-lane.test.ts`)
}
const cell = windowsHostCell(descriptor.cell, descriptor.target)
const observer = localHostObserver(descriptor.forbiddenToolLog)
const sshTarget = windowsHostSshTarget(descriptor, cell, randomUUID())
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import {
isWindowsOrcadCellId,
parseWindowsHostCellDescriptor,
WINDOWS_FORBIDDEN_TOOLS,
WINDOWS_HOST_CELL_IDS,
@@ -44,6 +45,9 @@ describe('Windows SSH-host cells', () => {
it('reads the descriptor PowerShell writes, BOM included', () => {
const parsed = parseWindowsHostCellDescriptor(`\uFEFF${JSON.stringify(DESCRIPTOR)}`)
expect(parsed).toEqual(DESCRIPTOR)
if (isWindowsOrcadCellId(parsed.cell)) {
throw new Error(`expected a relay cell, got ${parsed.cell}`)
}
const target = windowsHostSshTarget(parsed, windowsHostCell(parsed.cell, parsed.target), 'r1')
expect(target).toMatchObject({
id: 'windows-host-pinned-powershell-r1',
@@ -60,6 +64,9 @@ describe('Windows SSH-host cells', () => {
const parse = (patch: Record<string, unknown>): unknown =>
parseWindowsHostCellDescriptor(JSON.stringify({ ...DESCRIPTOR, ...patch }))
expect(() => parse({ cell: 'pinned-bash' })).toThrow('Unknown Windows host cell')
expect(
parseWindowsHostCellDescriptor(JSON.stringify({ ...DESCRIPTOR, cell: 'orcad-cmd' })).cell
).toBe('orcad-cmd')
expect(() => parse({ target: 'linux-x64-glibc' })).toThrow('win32-x64 or win32-arm64')
expect(() => parse({ port: '22' })).toThrow('port is invalid')
expect(() => parse({ port: 70_000 })).toThrow('port is invalid')
+17 -3
View File
@@ -50,9 +50,21 @@ export function windowsHostCell(
}
}
/** Managed orcad on the pinned node.exe, per DefaultShell: deploy, readiness, stop request, exit. */
export const WINDOWS_ORCAD_CELL_IDS = ['orcad-cmd', 'orcad-powershell'] as const
export type WindowsOrcadCellId = (typeof WINDOWS_ORCAD_CELL_IDS)[number]
export function isWindowsOrcadCellId(id: string): id is WindowsOrcadCellId {
return WINDOWS_ORCAD_CELL_IDS.some((candidate) => candidate === id)
}
export function windowsOrcadCellShell(id: WindowsOrcadCellId): WindowsSshDefaultShell {
return id === 'orcad-cmd' ? 'cmd' : 'powershell'
}
/** Written by config/ci/windows-ssh-provider/invoke-pinned-relay-cells.ps1, one per run. */
export type WindowsHostCellDescriptor = {
cell: WindowsHostCellId
cell: WindowsHostCellId | WindowsOrcadCellId
target: WindowsServerTarget
host: string
port: number
@@ -78,7 +90,9 @@ export function parseWindowsHostCellDescriptor(text: string): WindowsHostCellDes
throw new Error('Windows host cell descriptor must be a JSON object')
}
const record = Object.fromEntries(Object.entries(parsed))
const cell = WINDOWS_HOST_CELL_IDS.find((id) => id === record.cell)
const cell =
WINDOWS_HOST_CELL_IDS.find((id) => id === record.cell) ??
WINDOWS_ORCAD_CELL_IDS.find((id) => id === record.cell)
if (!cell) {
throw new Error(`Unknown Windows host cell: ${String(record.cell)}`)
}
@@ -109,7 +123,7 @@ export function readWindowsHostCellDescriptor(path: string): WindowsHostCellDesc
export function windowsHostSshTarget(
descriptor: WindowsHostCellDescriptor,
cell: WindowsHostCell,
cell: Pick<WindowsHostCell, 'id' | 'remoteRuntime'>,
runId: string
): SshTarget {
return {