Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ssh-windows-hosts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 }}'
Expand Down
10 changes: 6 additions & 4 deletions config/ci/windows-ssh-provider/invoke-pinned-relay-cells.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
Expand Down
9 changes: 6 additions & 3 deletions config/scripts/ssh-windows-hosts-workflow.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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, '../..')
Expand Down Expand Up @@ -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')
})
})
19 changes: 17 additions & 2 deletions src/main/ssh/orcad-activation-lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
17 changes: 14 additions & 3 deletions src/main/ssh/orcad-incumbent-recovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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}).`)
Expand Down
12 changes: 11 additions & 1 deletion src/main/ssh/orcad-initial-activation-admission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion src/main/ssh/orcad-installed-activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions src/main/ssh/orcad-remote-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)),
Expand All @@ -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,
Expand Down
2 changes: 0 additions & 2 deletions src/main/ssh/orcad-remote-deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
9 changes: 4 additions & 5 deletions src/main/ssh/orcad-remote-host-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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'
}
Expand Down
Loading
Loading