dsh 0.2.0-rc.2: in-container update (experimental channel), path-agnostic compose, upstream patches adapted to 0.1.x/0.2.x

This commit is contained in:
hermes committed 2026-10-01 19:21:19 +00:00
commit e6dcef9bf5
302 files changed
+75178

No files matched your search

@@ -0,0 +1,148 @@
import { lstat, readFile, readdir, rm } from 'node:fs/promises'
import { join } from 'node:path'
import { parseDeploymentRecord } from '../../lib/deployment-contracts.mjs'
async function jsonOptional(path) {
try { return JSON.parse(await readFile(path, 'utf8')) } catch (error) {
if (error?.code === 'ENOENT') return undefined
throw error
}
}
async function files(root, suffix = '') {
try { return (await readdir(root)).filter(name => !name.startsWith('.') && name.endsWith(suffix)).sort() } catch (error) {
if (error?.code === 'ENOENT') return []
throw error
}
}
async function removeUnmarked(root, retained, reserved = new Set()) {
const removed = []
for (const name of await files(root)) {
if (reserved.has(name) || retained.has(name)) continue
const path = join(root, name)
const details = await lstat(path)
if (!details.isDirectory() || details.isSymbolicLink()) continue
await rm(path, { recursive: true, force: true })
removed.push(name)
}
return removed
}
async function deploymentRecords(root) {
const records = new Map()
for (const name of await files(root, '.json')) {
const record = parseDeploymentRecord(await readFile(join(root, name)))
records.set(record.id, record)
}
return records
}
function recordIds(value, output) {
if (typeof value === 'string' && value.startsWith('deployment-record-')) output.add(value)
else if (Array.isArray(value)) for (const entry of value) recordIds(entry, output)
else if (value !== null && typeof value === 'object') for (const entry of Object.values(value)) recordIds(entry, output)
}
function heldRecords(channel, records, roots) {
const held = [...(channel?.holds ?? []), ...(channel?.experimentalBlocked === null || channel?.experimentalBlocked === undefined
? []
: [channel.experimentalBlocked])]
for (const hold of held) {
for (const record of records.values()) {
if (
record.dshVersion === hold.dshVersion
&& (hold.environmentVersion === null || hold.environmentVersion === undefined || record.environmentVersion === hold.environmentVersion)
) roots.add(record.id)
}
}
}
export class PlatformGarbageCollector {
constructor({ paths, deployments, snapshots }) {
this.paths = paths
this.deployments = deployments
this.snapshots = snapshots
}
async removeSnapshots(retained) {
if (this.snapshots?.remove === undefined) throw new Error('privileged snapshot cleanup is unavailable')
const removed = []
for (const name of await files(join(this.paths.snapshotsRoot, 'versions'))) {
if (retained.has(name)) continue
const details = await lstat(join(this.paths.snapshotsRoot, 'versions', name))
if (!details.isDirectory() || details.isSymbolicLink()) continue
if (await this.snapshots.remove(name)) removed.push(name)
}
return removed
}
async roots(records) {
const roots = new Set()
const slots = await this.deployments.state()
if (slots.current !== null) roots.add(slots.current)
if (slots.previous !== null) roots.add(slots.previous)
recordIds(await jsonOptional(this.deployments.activationPath), roots)
const transaction = await jsonOptional(join(this.paths.updaterStateRoot, 'transaction.json'))
recordIds(transaction, roots)
heldRecords(await jsonOptional(join(this.paths.updaterStateRoot, 'channel.json')), records, roots)
return { roots, transaction }
}
async collectDeployments() {
const records = await deploymentRecords(this.deployments.recordsRoot)
const { roots, transaction } = await this.roots(records)
const retained = {
environments: new Set(),
pristine: new Set(),
runtimes: new Set(),
systemPlugins: new Set(),
snapshots: new Set(),
}
for (const id of roots) {
const record = records.get(id)
if (record === undefined) continue
for (const [field, output] of [
['environment', retained.environments],
['pristine', retained.pristine],
['runtime', retained.runtimes],
['systemPlugins', retained.systemPlugins],
]) if (record[field].storage === 'store') output.add(record[field].id)
if (record.snapshotId !== null) retained.snapshots.add(record.snapshotId)
}
if (typeof transaction?.snapshotId === 'string') retained.snapshots.add(transaction.snapshotId)
const reserved = new Set(['current', 'previous', 'versions'])
const removed = {
environments: await removeUnmarked(this.paths.environmentsRoot, retained.environments, reserved),
pristine: await removeUnmarked(this.paths.pristineRoot, retained.pristine, reserved),
runtimes: await removeUnmarked(this.paths.runtimesRoot, retained.runtimes, reserved),
systemPlugins: await removeUnmarked(this.paths.systemPluginsRoot, retained.systemPlugins, reserved),
snapshots: await this.removeSnapshots(retained.snapshots),
records: [],
}
for (const [id] of records) {
if (!roots.has(id)) {
await rm(join(this.deployments.recordsRoot, `${id}.json`), { force: true })
removed.records.push(id)
}
}
return removed
}
async cleanCache() {
const removed = []
for (const name of await files(this.paths.downloadsRoot)) {
await rm(join(this.paths.downloadsRoot, name), { recursive: true, force: true })
removed.push(name)
}
return removed
}
async collect() {
return Object.freeze({
deployments: await this.collectDeployments(),
cache: await this.cleanCache(),
})
}
}