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,201 @@
#!/usr/bin/env node
import { request } from 'node:http'
import { realpathSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
const RUN_ROOT = process.env.DSH_PLATFORM_RUN ?? '/run/dsh-platform'
function fetchResponse(host, port, path, options = {}) {
return new Promise((resolve, reject) => {
const outgoing = request({ hostname: host, port, path, ...options }, response => {
const chunks = []
response.on('data', chunk => chunks.push(chunk))
response.once('end', () => {
resolve({
body: Buffer.concat(chunks),
headers: response.headers,
status: response.statusCode,
})
})
})
outgoing.setTimeout(2_000, () => outgoing.destroy(new Error('DSH Web request timed out')))
outgoing.once('error', reject)
outgoing.end(options.body)
})
}
async function fetch(host, port, path, options = {}) {
const response = await fetchResponse(host, port, path, options)
if (response.status !== 200) throw new Error(`${path} returned HTTP ${String(response.status)}`)
return response.body
}
function delay(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
function lifecycleReadiness() {
const body = Buffer.from('{}')
return new Promise((resolve, reject) => {
const outgoing = request({
socketPath: `${RUN_ROOT}/dsh-lifecycle.sock`,
path: '/v1/runtime/readiness',
method: 'POST',
headers: {
'content-length': body.byteLength,
'content-type': 'application/json',
},
}, response => {
const chunks = []
response.on('data', chunk => chunks.push(chunk))
response.once('end', () => {
if (response.statusCode !== 200) {
reject(new Error(`DSH lifecycle readiness returned HTTP ${String(response.statusCode)}`))
return
}
try {
const value = JSON.parse(Buffer.concat(chunks).toString('utf8'))
resolve({
generation: typeof value?.generation === 'string' ? value.generation : null,
ready: value?.ready === true,
readyUrl: typeof value?.readyUrl === 'string' ? value.readyUrl : null,
})
} catch (error) {
reject(error)
}
})
})
outgoing.setTimeout(2_000, () => outgoing.destroy(new Error('DSH lifecycle readiness timed out')))
outgoing.once('error', reject)
outgoing.end(body)
})
}
async function waitForManagedReady(timeoutMs = 20_000) {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
const readiness = await lifecycleReadiness()
if (readiness.ready) return readiness
await delay(100)
}
throw new Error('DSH managed Web boot did not complete')
}
async function authenticationHeaders(host, port, readyUrl) {
if (readyUrl === null) return {}
const url = new URL(readyUrl)
if (url.protocol !== 'http:' || url.hostname !== host || Number(url.port) !== port
|| url.pathname !== '/' || url.username !== '' || url.password !== '' || url.hash !== ''
|| url.searchParams.getAll('token').length !== 1) {
throw new Error('DSH Web readiness URL is invalid')
}
const exchange = await fetchResponse(host, port, `${url.pathname}${url.search}`, {
headers: { 'cache-control': 'no-store' },
})
const cookie = exchange.headers['set-cookie']?.[0]?.split(';', 1)[0]
// dsh 0.2.x redirects to './' instead of '/'
if (exchange.status !== 303 || !['/', './'].includes(exchange.headers.location) || typeof cookie !== 'string' || cookie === '') {
throw new Error('DSH Web authentication exchange failed')
}
return { cookie }
}
async function verifyBootManifest(host, port, headers) {
const html = (await fetch(host, port, '/', { headers })).toString('utf8')
const match = /(?:window\.__DSH_BOOT__|globalThis\["__DSH_BOOT__"\])\s*=\s*(\{.*?\})<\/script>/s.exec(html)
if (match === null) throw new Error('DSH boot manifest is unavailable')
const manifest = JSON.parse(match[1])
if (!Array.isArray(manifest.entries)) throw new Error('DSH boot manifest entries are invalid')
if (manifest.batches !== undefined && !Array.isArray(manifest.batches)) {
throw new Error('DSH boot manifest batches are invalid')
}
const resources = [
...manifest.entries.map(entry => entry?.url),
...(manifest.batches ?? []).map(batch => batch?.url),
]
await Promise.all([...new Set(resources)].map(async resource => {
// dsh 0.2.x emits plugin URLs relative to the root ('plugins/…'), 0.1.x absolute ('/plugins/…')
const url = typeof resource === 'string' ? `/${resource.replace(/^\/+/, '')}` : resource
if (typeof resource !== 'string' || !url.startsWith('/plugins/')) {
throw new Error('DSH boot manifest contains an invalid Plugin URL')
}
await fetch(host, port, url, { headers })
}))
return JSON.stringify({
batches: manifest.batches ?? [],
entries: manifest.entries,
rev: manifest.rev ?? null,
})
}
async function verifyPluginInventory(host, port, headers) {
const body = JSON.stringify({
type: 'client-request',
rpcId: 'dsh-platform-readiness',
method: 'pluginInventory/list',
payload: { args: {} },
})
const response = JSON.parse((await fetch(host, port, '/api/pluginInventory/list', {
method: 'POST',
headers: {
...headers,
'content-length': Buffer.byteLength(body),
'content-type': 'application/json',
},
body,
})).toString('utf8'))
if (response?.rpcId !== 'dsh-platform-readiness' || response?.result?.ok !== true) {
const message = response?.result?.error?.message
throw new Error(`DSH Plugin inventory is unavailable${typeof message === 'string' ? `: ${message}` : ''}`)
}
const entries = response.result.value?.entries
if (!Array.isArray(entries)) throw new Error('DSH Plugin inventory entries are invalid')
const unhealthy = entries.filter(entry => entry?.enabled === true && entry?.fiberPhase !== 'active')
if (unhealthy.length > 0) {
const detail = unhealthy.map(entry => `${String(entry?.moduleName ?? entry?.entryId ?? 'unknown')} (${String(entry?.fiberPhase)})`).join(', ')
throw new Error(`DSH Plugins are not active: ${detail}`)
}
return JSON.stringify(entries.map(entry => ({
enabled: entry?.enabled === true,
entryId: entry?.entryId ?? null,
fiberPhase: entry?.fiberPhase ?? null,
moduleName: entry?.moduleName ?? null,
})).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))))
}
function sameLifecycle(left, right) {
return left?.generation === right?.generation && left?.readyUrl === right?.readyUrl
}
export async function verifyDshWebReady({
host = '127.0.0.1',
port = 3079,
managedReady = waitForManagedReady,
} = {}) {
const firstReadiness = await managedReady()
const headers = await authenticationHeaders(host, port, firstReadiness?.readyUrl ?? null)
const firstManifest = await verifyBootManifest(host, port, headers)
const firstInventory = await verifyPluginInventory(host, port, headers)
const middleReadiness = await managedReady()
if (!sameLifecycle(firstReadiness, middleReadiness)) {
throw new Error('DSH lifecycle changed during Web readiness verification')
}
const secondManifest = await verifyBootManifest(host, port, headers)
const secondInventory = await verifyPluginInventory(host, port, headers)
const finalReadiness = await managedReady()
if (!sameLifecycle(firstReadiness, finalReadiness)) {
throw new Error('DSH lifecycle changed during Web readiness verification')
}
if (firstManifest !== secondManifest) {
throw new Error('DSH boot manifest changed during Web readiness verification')
}
if (firstInventory !== secondInventory) {
throw new Error('DSH Plugin inventory changed during Web readiness verification')
}
}
if (process.argv[1] !== undefined
&& realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
await verifyDshWebReady()
}