mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 16:03:49 +00:00
Scope host prune evidence per version
This commit is contained in:
@@ -268,7 +268,10 @@ describe('pruneOldDaemonHosts', () => {
|
||||
for (const v of ['9.9.9', '1.0.0', '2.0.0']) {
|
||||
mkdirSync(join(root, v), { recursive: true })
|
||||
}
|
||||
pruneOldDaemonHosts({ status: 'complete', pinnedVersions: new Set(['2.0.0']) })
|
||||
pruneOldDaemonHosts({
|
||||
status: 'complete',
|
||||
versionLiveness: new Map([['2.0.0', { status: 'live' }]])
|
||||
})
|
||||
expect(existsSync(join(root, '9.9.9'))).toBe(true)
|
||||
expect(existsSync(join(root, '2.0.0'))).toBe(true)
|
||||
expect(existsSync(join(root, '1.0.0'))).toBe(false)
|
||||
@@ -288,6 +291,10 @@ describe('pruneOldDaemonHosts', () => {
|
||||
})
|
||||
|
||||
const evidence = collectPinnedDaemonVersions(runtimeDir)
|
||||
expect(evidence).toEqual({
|
||||
status: 'complete',
|
||||
versionLiveness: new Map([['8.0.0', { status: 'live' }]])
|
||||
})
|
||||
pruneOldDaemonHosts(evidence)
|
||||
|
||||
expect(existsSync(join(root, '8.0.0'))).toBe(true)
|
||||
@@ -298,6 +305,7 @@ describe('pruneOldDaemonHosts', () => {
|
||||
const root = join(localAppDataDir, 'Orca', 'daemon-host')
|
||||
const runtimeDir = join(userDataDir, 'daemon')
|
||||
mkdirSync(join(root, '7.0.0'), { recursive: true })
|
||||
mkdirSync(join(root, '6.0.0'), { recursive: true })
|
||||
mkdirSync(runtimeDir, { recursive: true })
|
||||
writeFileSync(
|
||||
join(runtimeDir, 'daemon-v7.pid'),
|
||||
@@ -309,12 +317,15 @@ describe('pruneOldDaemonHosts', () => {
|
||||
|
||||
const evidence = collectPinnedDaemonVersions(runtimeDir)
|
||||
expect(evidence).toEqual({
|
||||
status: 'unverifiable',
|
||||
reason: 'the daemon process could not be queried'
|
||||
status: 'complete',
|
||||
versionLiveness: new Map([
|
||||
['7.0.0', { status: 'unverifiable', reason: 'the daemon process could not be queried' }]
|
||||
])
|
||||
})
|
||||
pruneOldDaemonHosts(evidence)
|
||||
|
||||
expect(existsSync(join(root, '7.0.0'))).toBe(true)
|
||||
expect(existsSync(join(root, '6.0.0'))).toBe(false)
|
||||
killSpy.mockRestore()
|
||||
})
|
||||
|
||||
@@ -323,7 +334,7 @@ describe('pruneOldDaemonHosts', () => {
|
||||
mkdirSync(join(root, '1.0.0'), { recursive: true })
|
||||
hostApp.appPath = join(installDir, 'resources', 'app')
|
||||
installHostApp()
|
||||
pruneOldDaemonHosts({ status: 'complete', pinnedVersions: new Set() })
|
||||
pruneOldDaemonHosts({ status: 'complete', versionLiveness: new Map() })
|
||||
// A Node host owns no daemon-host tree, so deleting under it would be reaching into a
|
||||
// directory layout it never created.
|
||||
expect(existsSync(join(root, '1.0.0'))).toBe(true)
|
||||
@@ -343,6 +354,12 @@ describe('collectPinnedDaemonVersions', () => {
|
||||
JSON.stringify({ pid: 2147483646, startedAtMs: null, appVersion: '6.0.0' })
|
||||
)
|
||||
const pinned = collectPinnedDaemonVersions(runtimeDir)
|
||||
expect(pinned).toEqual({ status: 'complete', pinnedVersions: new Set(['7.0.0']) })
|
||||
expect(pinned).toEqual({
|
||||
status: 'complete',
|
||||
versionLiveness: new Map([
|
||||
['7.0.0', { status: 'live' }],
|
||||
['6.0.0', { status: 'exited' }]
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,8 +11,9 @@ import {
|
||||
} from 'node:fs'
|
||||
import { dirname, join, win32 as winPath } from 'node:path'
|
||||
import { getAppEnvironment } from '../../shared/app-environment'
|
||||
import type { ProcessLivenessVerdict } from './daemon-incarnation-evidence-types'
|
||||
import { parseDaemonPidFile } from './daemon-pid-file-parse'
|
||||
import { inspectProcessLiveness } from './daemon-process-inspection'
|
||||
import { inspectProcessLiveness, mergeProcessLivenessVerdict } from './daemon-process-inspection'
|
||||
|
||||
/**
|
||||
* Relocate the terminal daemon's process image out of the app install dir into LOCAL userData so it
|
||||
@@ -294,7 +295,7 @@ export function materializeRelocatedDaemonHost(): RelocatedDaemonHost | null {
|
||||
}
|
||||
|
||||
export type PinnedDaemonVersionsEvidence =
|
||||
| { status: 'complete'; pinnedVersions: ReadonlySet<string> }
|
||||
| { status: 'complete'; versionLiveness: ReadonlyMap<string, ProcessLivenessVerdict> }
|
||||
| { status: 'unverifiable'; reason: string }
|
||||
|
||||
/**
|
||||
@@ -302,7 +303,7 @@ export type PinnedDaemonVersionsEvidence =
|
||||
* host dir must not be reclaimed while alive. On win32 start-time can't verify, so a matching pid pins conservatively.
|
||||
*/
|
||||
export function collectPinnedDaemonVersions(runtimeDir: string): PinnedDaemonVersionsEvidence {
|
||||
const pinned = new Set<string>()
|
||||
const versionLiveness = new Map<string, ProcessLivenessVerdict>()
|
||||
let entries
|
||||
try {
|
||||
entries = readdirSync(runtimeDir, { withFileTypes: true })
|
||||
@@ -333,19 +334,12 @@ export function collectPinnedDaemonVersions(runtimeDir: string): PinnedDaemonVer
|
||||
continue
|
||||
}
|
||||
const verdict = inspectProcessLiveness(parsed.pid)
|
||||
switch (verdict.status) {
|
||||
case 'live':
|
||||
pinned.add(parsed.appVersion)
|
||||
break
|
||||
case 'exited':
|
||||
break
|
||||
case 'unverifiable':
|
||||
return verdict
|
||||
default:
|
||||
verdict satisfies never
|
||||
}
|
||||
versionLiveness.set(
|
||||
parsed.appVersion,
|
||||
mergeProcessLivenessVerdict(versionLiveness.get(parsed.appVersion), verdict)
|
||||
)
|
||||
}
|
||||
return { status: 'complete', pinnedVersions: pinned }
|
||||
return { status: 'complete', versionLiveness }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -368,9 +362,20 @@ export function pruneOldDaemonHosts(evidence: PinnedDaemonVersionsEvidence): voi
|
||||
return
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory() || entry.name === version || evidence.pinnedVersions.has(entry.name)) {
|
||||
if (!entry.isDirectory() || entry.name === version) {
|
||||
continue
|
||||
}
|
||||
// A complete runtime-dir listing with no pid record for this version proves it is unowned.
|
||||
const verdict = evidence.versionLiveness.get(entry.name) ?? { status: 'exited' }
|
||||
switch (verdict.status) {
|
||||
case 'live':
|
||||
case 'unverifiable':
|
||||
continue
|
||||
case 'exited':
|
||||
break
|
||||
default:
|
||||
verdict satisfies never
|
||||
}
|
||||
try {
|
||||
rmSync(join(root, entry.name), { recursive: true, force: true })
|
||||
} catch {
|
||||
|
||||
@@ -48,6 +48,22 @@ export function inspectProcessLiveness(pid: number): ProcessLivenessVerdict {
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeProcessLivenessVerdict(
|
||||
current: ProcessLivenessVerdict | undefined,
|
||||
next: ProcessLivenessVerdict
|
||||
): ProcessLivenessVerdict {
|
||||
switch (next.status) {
|
||||
case 'live':
|
||||
return next
|
||||
case 'unverifiable':
|
||||
return current?.status === 'live' ? current : next
|
||||
case 'exited':
|
||||
return current ?? next
|
||||
default:
|
||||
return next satisfies never
|
||||
}
|
||||
}
|
||||
|
||||
export async function readLinuxStat(pid: number): Promise<LinuxStatEvidence> {
|
||||
try {
|
||||
return { status: 'present', value: await readFile(`/proc/${pid}/stat`, 'utf8') }
|
||||
|
||||
Reference in New Issue
Block a user