diff --git a/src/main/node-runtime/managed-runtime-release.test.ts b/src/main/node-runtime/managed-runtime-release.test.ts new file mode 100644 index 00000000000..300093a4cac --- /dev/null +++ b/src/main/node-runtime/managed-runtime-release.test.ts @@ -0,0 +1,13 @@ +import { mkdir, mkdtemp, readdir, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { expect, it } from 'vitest' +import { reclaimRuntimeReleases } from './managed-runtime-release' + +it('removes every release and leftover but the kept one', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-runtime-reclaim-')) + await Promise.all(['1.0', '0.9', '.download-1-left'].map((name) => mkdir(join(root, name)))) + expect(await reclaimRuntimeReleases(root, '1.0')).toEqual([]) + expect(await readdir(root)).toEqual(['1.0']) + await rm(root, { recursive: true, force: true }) +}) diff --git a/src/main/node-runtime/managed-runtime-release.ts b/src/main/node-runtime/managed-runtime-release.ts index f16ade04e42..446a7fc41e1 100644 --- a/src/main/node-runtime/managed-runtime-release.ts +++ b/src/main/node-runtime/managed-runtime-release.ts @@ -3,6 +3,7 @@ // cheap enough for every launch: a full hash is only ever taken by an install. import { lstatSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs' +import { readdir, rm } from 'node:fs/promises' import { basename, dirname, join } from 'node:path' import { z } from 'zod' import { isDefinitiveAbsence } from '../../shared/definitive-filesystem-absence' @@ -107,6 +108,20 @@ export function copyBesidePinnedMembers( } } +/** Removes everything under `root` but the release named `keep`: called only while the caller + * owns every install and launch of this runtime, so nothing else runs from or publishes into it. + * Answers the entries it could not remove. */ +export async function reclaimRuntimeReleases(root: string, keep: string | null): Promise { + const names = await readdir(root).catch((error: NodeJS.ErrnoException) => + error.code === 'ENOENT' ? [] : Promise.reject(error) + ) + const stale = names.filter((name) => name !== keep) + const results = await Promise.allSettled( + stale.map((name) => rm(join(root, name), { recursive: true, force: true })) + ) + return stale.filter((_, index) => results[index]?.status === 'rejected') +} + function readRelease(folder: string, version: string): ReleaseRead { try { const receiptPath = join(folder, RUNTIME_RELEASE_RECEIPT)