mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 00:02:38 +00:00
* fix(ssh): relay version GC deletes only on an exited verdict and keeps the previous build The relay records .relay-pid in its version dir once it owns its socket. GC calls a relay version dir exited only when that PID is provably dead and every relay-*.sock refuses a connection; a dir without a PID file keeps the test -S rule. The most recently completed other relay build is pinned like orcad's rollback target. Design D5 GC liveness. * feat(ssh): collect the shared runtimes/ Node store and give it its own owner runtimes/ gets its own owner in the install model, so no version-dir GC (new or old clients, whose listings are prefix-scoped) can list or delete it. A store pass removes node-<sha> only when no retained dir references it, it is neither a current pin nor the newest other verified runtime, and a ps or /proc check ran and found no process using it. Legacy relay-*/orcad-* dirs are read for references and reported as diagnostics only (design D10 two-step). Wired behind orcad GC's nodeRuntimePins. * fix(ssh): runtime store process check holds runtimes reached through a symlinked home /proc exe resolves symlinks and argv keeps whatever spelling launched the runtime, so filtering on the exact $root path missed in-use runtimes on hosts like /home -> /var/home. Filter on the store segment instead; the parser already attributes holds root-agnostically. * test(ssh): wait for the holder process to spawn instead of a fixed delay --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
107 lines
3.7 KiB
TypeScript
107 lines
3.7 KiB
TypeScript
/**
|
|
* The version dir a model ran before the current one, pinned against GC like orcad's
|
|
* rollback target (design D5 "plus the relay's previous version as a pin").
|
|
*
|
|
* The relay keeps no activation record, so "previous" is the most recently completed install
|
|
* of the same model other than the current one, by `.install-complete` mtime.
|
|
*/
|
|
import type { SshConnection } from './ssh-connection'
|
|
import { shellEscape } from './ssh-connection-utils'
|
|
import { execCommand } from './ssh-relay-deploy-helpers'
|
|
import { isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command'
|
|
import {
|
|
remoteInstallGcPermits,
|
|
remoteInstallVersionDirRegex,
|
|
type RemoteInstallModel
|
|
} from './remote-install-model'
|
|
import { isWindowsRemoteHost, type RemoteHostPlatform } from './ssh-remote-platform'
|
|
import { powerShellCommand, powerShellLiteral } from './ssh-remote-powershell'
|
|
|
|
export const REMOTE_INSTALL_ORDER_OK = 'ORCA_INSTALL_ORDER_OK'
|
|
|
|
export function listCompletedInstallsNewestFirstCommand(
|
|
host: RemoteHostPlatform,
|
|
baseDir: string,
|
|
model: RemoteInstallModel
|
|
): string {
|
|
const prefix = `${model.dirPrefix}-`
|
|
if (isWindowsRemoteHost(host)) {
|
|
return powerShellCommand(
|
|
[
|
|
"$ErrorActionPreference = 'Stop'",
|
|
`$base = ${powerShellLiteral(baseDir)}`,
|
|
'if (Test-Path -LiteralPath $base -PathType Container) {',
|
|
`Get-ChildItem -LiteralPath $base -Directory -Filter '${prefix}*' | ForEach-Object { ` +
|
|
`$marker = Join-Path $_.FullName ${powerShellLiteral(model.installCompleteFilename)}; ` +
|
|
'if (Test-Path -LiteralPath $marker -PathType Leaf) { Get-Item -LiteralPath $marker } ' +
|
|
'} | Sort-Object LastWriteTimeUtc -Descending | ForEach-Object { $_.Directory.Name }',
|
|
'}',
|
|
`'${REMOTE_INSTALL_ORDER_OK}'`
|
|
].join('\n')
|
|
)
|
|
}
|
|
return [
|
|
`base=${shellEscape(baseDir)}`,
|
|
`set -- "$base"/${prefix}*/${shellEscape(model.installCompleteFilename)}`,
|
|
`[ -e "$1" ] || { echo ${REMOTE_INSTALL_ORDER_OK}; exit 0; }`,
|
|
'ls -1t -- "$@" || exit 1',
|
|
`echo ${REMOTE_INSTALL_ORDER_OK}`
|
|
].join('\n')
|
|
}
|
|
|
|
/** Dir names newest first, or null when the host could not answer. */
|
|
export function parseCompletedInstallsNewestFirst(
|
|
output: string,
|
|
model: RemoteInstallModel
|
|
): string[] | null {
|
|
const lines = output
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
if (!lines.includes(REMOTE_INSTALL_ORDER_OK)) {
|
|
return null
|
|
}
|
|
const versionDirRegex = remoteInstallVersionDirRegex(model)
|
|
const names: string[] = []
|
|
for (const line of lines) {
|
|
// POSIX prints `<base>/<dir>/.install-complete`; PowerShell prints `<dir>`.
|
|
const segments = line.split(/[\\/]/)
|
|
const name = segments.length > 1 ? segments.at(-2) : segments[0]
|
|
if (name && versionDirRegex.test(name) && remoteInstallGcPermits(model, name)) {
|
|
names.push(name)
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
export type PreviousInstallResult = { state: 'ok'; dirName: string | null } | { state: 'unknown' }
|
|
|
|
export async function findPreviousRemoteInstall(
|
|
conn: SshConnection,
|
|
host: RemoteHostPlatform,
|
|
baseDir: string,
|
|
model: RemoteInstallModel,
|
|
currentDirName: string
|
|
): Promise<PreviousInstallResult> {
|
|
let output: string
|
|
try {
|
|
output = await execCommand(
|
|
conn,
|
|
listCompletedInstallsNewestFirstCommand(host, baseDir, model),
|
|
{
|
|
wrapCommand: host.commandDialect !== 'powershell'
|
|
}
|
|
)
|
|
} catch (err) {
|
|
if (isUnconfirmedSshCommandTermination(err)) {
|
|
throw err
|
|
}
|
|
return { state: 'unknown' }
|
|
}
|
|
const ordered = parseCompletedInstallsNewestFirst(output, model)
|
|
if (!ordered) {
|
|
return { state: 'unknown' }
|
|
}
|
|
return { state: 'ok', dirName: ordered.find((name) => name !== currentDirName) ?? null }
|
|
}
|