373 lines
19 KiB
JavaScript
373 lines
19 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { chmod, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
import { createServer } from 'node:http'
|
|
import { tmpdir } from 'node:os'
|
|
import { dirname, join } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import test from 'node:test'
|
|
import {
|
|
applyPatch,
|
|
MANAGED_LIFECYCLE_MODULE,
|
|
PROFILE_PACKAGE_STORAGE_MODULE,
|
|
verifyPatch,
|
|
} from '../../environment/resources/patches/managed-lifecycle.mjs'
|
|
|
|
async function fixture({ duplicateImport = false, sigterm = true, wrapper = true } = {}) {
|
|
const root = await mkdtemp(join(tmpdir(), 'dsh-managed-lifecycle-patch-'))
|
|
await mkdir(join(root, 'lib'))
|
|
await writeFile(join(root, 'package.json'), '{"type":"module"}\n')
|
|
const dynamicImport = 'const { runProfile } = await import("./profile-boot-wrapper_42.js");'
|
|
await writeFile(join(root, 'lib/bin.js'), `#!/usr/bin/env node
|
|
const invocation = parseDshArgs(process.argv.slice(2), readVersion());
|
|
switch (invocation.mode) {
|
|
case "profile": {
|
|
${dynamicImport}
|
|
${duplicateImport ? dynamicImport : ''}
|
|
}
|
|
}
|
|
`)
|
|
await writeFile(join(root, 'lib/profile-boot-wrapper_42.js'), wrapper
|
|
? 'import { x as runProfile } from "./profile-boot-anyHash.js";\nexport { runProfile };\n'
|
|
: 'export const unrelated = true;\n')
|
|
await writeFile(join(root, 'lib/profile-boot-anyHash.js'), `async function runProfile(options) {
|
|
${sigterm ? '\tprocess.on("SIGTERM", () => {\n\t\tinterrupt(0);\n\t});' : '\tprocess.on("SIGHUP", () => interrupt(0));'}
|
|
\tprocess.on("SIGINT", () => { interrupt(130); });
|
|
\tconst ctx = await boot(NAME, rootConfig, patches, (hostCtx) => {
|
|
\t\thostCtx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, options.environment);
|
|
\t});
|
|
\treturn {
|
|
\t\tctx,
|
|
\t\tshutdown
|
|
\t};
|
|
}
|
|
export { runProfile as x };
|
|
`)
|
|
return root
|
|
}
|
|
|
|
async function listen(socketPath, handler) {
|
|
const server = createServer(handler)
|
|
await new Promise((resolve, reject) => {
|
|
server.once('error', reject)
|
|
server.listen(socketPath, resolve)
|
|
})
|
|
await chmod(socketPath, 0o600)
|
|
return server
|
|
}
|
|
|
|
function json(response, status, value) {
|
|
response.writeHead(status, { 'content-type': 'application/json' })
|
|
response.end(JSON.stringify(value))
|
|
}
|
|
|
|
test('patches a hash-independent Profile import chain exactly once', async () => {
|
|
const root = await fixture()
|
|
applyPatch(root)
|
|
verifyPatch(root)
|
|
assert.match(await readFile(join(root, 'lib/bin.js'), 'utf8'), /prepareManagedInvocation/)
|
|
assert.match(await readFile(join(root, 'lib/profile-boot-anyHash.js'), 'utf8'), /managedSigtermHandler/)
|
|
assert.match(await readFile(join(root, MANAGED_LIFECYCLE_MODULE), 'utf8'), /dshPlatformLifecycle/)
|
|
assert.match(await readFile(join(root, PROFILE_PACKAGE_STORAGE_MODULE), 'utf8'), /prepareProfilePackageStorage/)
|
|
assert.throws(() => applyPatch(root), /already applied/)
|
|
})
|
|
|
|
test('fails closed when upstream Profile anchors are missing or ambiguous', async () => {
|
|
await assert.rejects(Promise.resolve().then(async () => applyPatch(await fixture({ duplicateImport: true }))), /found 2/)
|
|
await assert.rejects(Promise.resolve().then(async () => applyPatch(await fixture({ wrapper: false }))), /found 0/)
|
|
await assert.rejects(Promise.resolve().then(async () => applyPatch(await fixture({ sigterm: false }))), /SIGTERM handler.*found 0/)
|
|
})
|
|
|
|
async function fakePnpm(root) {
|
|
const bin = join(root, 'bin')
|
|
await mkdir(bin, { recursive: true })
|
|
const executable = join(bin, 'pnpm')
|
|
await writeFile(executable, `#!/usr/bin/env node
|
|
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
if (process.env.DSH_TEST_PNPM_FAIL === '1') {
|
|
process.stderr.write('fixture pnpm failed\\n')
|
|
process.exit(1)
|
|
}
|
|
const workspace = await import('node:fs').then(({ readFileSync }) => readFileSync('pnpm-workspace.yaml', 'utf8'))
|
|
const match = /^storeDir:\\s*(.+)$/m.exec(workspace)
|
|
if (match === null) process.exit(2)
|
|
const storeRoot = JSON.parse(match[1])
|
|
mkdirSync('node_modules', { recursive: true })
|
|
writeFileSync(join('node_modules', '.modules.yaml'), JSON.stringify({ storeDir: join(storeRoot, 'v11') }))
|
|
writeFileSync(join('node_modules', 'migration-marker'), 'rebuilt\\n')
|
|
`)
|
|
await chmod(executable, 0o755)
|
|
return bin
|
|
}
|
|
|
|
test('pins compatible Profiles to their custom DSH_HOME store', async () => {
|
|
const root = await fixture()
|
|
applyPatch(root)
|
|
const storage = await import(`${pathToFileURL(join(root, PROFILE_PACKAGE_STORAGE_MODULE)).href}?storage`)
|
|
const dshHome = await mkdtemp(join(tmpdir(), 'dsh-profile-storage-home-'))
|
|
const profileDir = join(dshHome, 'profiles', 'web')
|
|
const workspacePath = join(profileDir, 'pnpm-workspace.yaml')
|
|
const modulesRoot = join(profileDir, 'node_modules')
|
|
await mkdir(modulesRoot, { recursive: true })
|
|
await writeFile(join(profileDir, 'package.json'), '{"name":"dsh-profile-web"}\n')
|
|
await writeFile(workspacePath, 'packages:\n - .\n\nnodeLinker: hoisted\n')
|
|
const previousHome = process.env.DSH_HOME
|
|
process.env.DSH_HOME = dshHome
|
|
try {
|
|
assert.deepEqual(storage.prepareProfilePackageStorage('web'), {
|
|
status: 'ready',
|
|
currentStore: null,
|
|
storeRoot: join(dshHome, '.pnpm-store'),
|
|
})
|
|
assert.match(await readFile(workspacePath, 'utf8'), new RegExp(
|
|
`storeDir: ${JSON.stringify(join(dshHome, '.pnpm-store')).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
|
|
))
|
|
|
|
await writeFile(join(modulesRoot, '.modules.yaml'), JSON.stringify({
|
|
storeDir: join(dshHome, '.pnpm-store', 'v11'),
|
|
}))
|
|
assert.equal(storage.prepareProfilePackageStorage('web').status, 'ready')
|
|
assert.equal((await readFile(workspacePath, 'utf8')).match(/^storeDir:/gm)?.length, 1)
|
|
|
|
const ambiguous = `${await readFile(workspacePath, 'utf8')}storeDir: "/another/store"\n`
|
|
await writeFile(workspacePath, ambiguous)
|
|
assert.throws(() => storage.prepareProfilePackageStorage('web'), /multiple storeDir entries/)
|
|
assert.equal(await readFile(workspacePath, 'utf8'), ambiguous)
|
|
} finally {
|
|
if (previousHome === undefined) delete process.env.DSH_HOME
|
|
else process.env.DSH_HOME = previousHome
|
|
}
|
|
})
|
|
|
|
test('migrates a legacy Profile store and recovers a failed or interrupted rebuild', async () => {
|
|
const root = await fixture()
|
|
applyPatch(root)
|
|
const storage = await import(`${pathToFileURL(join(root, PROFILE_PACKAGE_STORAGE_MODULE)).href}?migration`)
|
|
const dshHome = await mkdtemp(join(tmpdir(), 'dsh-profile-migration-home-'))
|
|
const profileDir = join(dshHome, 'profiles', 'web')
|
|
const modulesRoot = join(profileDir, 'node_modules')
|
|
const workspacePath = join(profileDir, 'pnpm-workspace.yaml')
|
|
const legacyStore = join(root, 'workspace', '.pnpm-store', 'v11')
|
|
const originalWorkspace = `packages:\n - .\n\nstoreDir: ${JSON.stringify(dirname(legacyStore))}\n`
|
|
await mkdir(modulesRoot, { recursive: true })
|
|
await mkdir(legacyStore, { recursive: true })
|
|
await writeFile(join(legacyStore, 'store-marker'), 'cached\n')
|
|
await writeFile(join(modulesRoot, '.modules.yaml'), JSON.stringify({ storeDir: legacyStore }))
|
|
await writeFile(join(modulesRoot, 'original-marker'), 'original\n')
|
|
await writeFile(join(profileDir, 'package.json'), '{"name":"dsh-profile-web","private":true}\n')
|
|
await writeFile(join(profileDir, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n')
|
|
await writeFile(workspacePath, originalWorkspace)
|
|
const bin = await fakePnpm(root)
|
|
const previous = {
|
|
DSH_HOME: process.env.DSH_HOME,
|
|
PATH: process.env.PATH,
|
|
DSH_TEST_PNPM_FAIL: process.env.DSH_TEST_PNPM_FAIL,
|
|
}
|
|
process.env.DSH_HOME = dshHome
|
|
process.env.PATH = `${bin}:${process.env.PATH}`
|
|
try {
|
|
assert.deepEqual(storage.prepareProfilePackageStorage('web'), {
|
|
status: 'migrated',
|
|
currentStore: join(dshHome, '.pnpm-store', 'v11'),
|
|
storeRoot: join(dshHome, '.pnpm-store'),
|
|
})
|
|
assert.equal(await readFile(join(dshHome, '.pnpm-store', 'v11', 'store-marker'), 'utf8'), 'cached\n')
|
|
assert.equal(await readFile(join(modulesRoot, 'migration-marker'), 'utf8'), 'rebuilt\n')
|
|
await assert.rejects(readFile(join(profileDir, '.dsh-platform-node_modules.previous')), { code: 'ENOENT' })
|
|
await assert.rejects(readFile(join(profileDir, '.dsh-platform-pnpm-workspace.previous')), { code: 'ENOENT' })
|
|
|
|
await rm(modulesRoot, { recursive: true })
|
|
await mkdir(modulesRoot)
|
|
await writeFile(join(modulesRoot, '.modules.yaml'), JSON.stringify({ storeDir: legacyStore }))
|
|
await writeFile(join(modulesRoot, 'original-marker'), 'restored\n')
|
|
await writeFile(workspacePath, originalWorkspace)
|
|
process.env.DSH_TEST_PNPM_FAIL = '1'
|
|
assert.throws(() => storage.prepareProfilePackageStorage('web'), /fixture pnpm failed/)
|
|
assert.equal(await readFile(join(modulesRoot, 'original-marker'), 'utf8'), 'restored\n')
|
|
assert.equal(await readFile(workspacePath, 'utf8'), originalWorkspace)
|
|
|
|
delete process.env.DSH_TEST_PNPM_FAIL
|
|
await writeFile(join(profileDir, '.dsh-platform-pnpm-workspace.previous'), originalWorkspace)
|
|
await writeFile(workspacePath, `packages:\n - .\n\nstoreDir: ${JSON.stringify(join(dshHome, '.pnpm-store'))}\n`)
|
|
assert.equal(storage.prepareProfilePackageStorage('web').status, 'migrated')
|
|
assert.equal(await readFile(join(modulesRoot, 'migration-marker'), 'utf8'), 'rebuilt\n')
|
|
|
|
await rm(modulesRoot, { recursive: true })
|
|
await mkdir(modulesRoot)
|
|
await writeFile(join(modulesRoot, '.modules.yaml'), JSON.stringify({ storeDir: legacyStore }))
|
|
await writeFile(join(modulesRoot, 'original-marker'), 'interrupted\n')
|
|
await writeFile(workspacePath, originalWorkspace)
|
|
await writeFile(join(profileDir, '.dsh-platform-pnpm-workspace.previous'), originalWorkspace)
|
|
await rename(modulesRoot, join(profileDir, '.dsh-platform-node_modules.previous'))
|
|
await writeFile(workspacePath, `packages:\n - .\n\nstoreDir: ${JSON.stringify(join(dshHome, '.pnpm-store'))}\n`)
|
|
assert.equal(storage.prepareProfilePackageStorage('web').status, 'migrated')
|
|
assert.equal(await readFile(join(modulesRoot, 'migration-marker'), 'utf8'), 'rebuilt\n')
|
|
|
|
await rm(modulesRoot, { recursive: true })
|
|
await mkdir(modulesRoot)
|
|
await writeFile(join(modulesRoot, '.modules.yaml'), JSON.stringify({
|
|
storeDir: join(dshHome, '.pnpm-store', 'v11'),
|
|
}))
|
|
await writeFile(join(modulesRoot, 'partial-marker'), 'partial\n')
|
|
await writeFile(workspacePath, `packages:\n - .\n\nstoreDir: ${JSON.stringify(join(dshHome, '.pnpm-store'))}\n`)
|
|
await mkdir(join(profileDir, '.dsh-platform-node_modules.previous'))
|
|
await writeFile(join(profileDir, '.dsh-platform-node_modules.previous', '.modules.yaml'), JSON.stringify({
|
|
storeDir: legacyStore,
|
|
}))
|
|
await writeFile(join(profileDir, '.dsh-platform-node_modules.previous', 'original-marker'), 'original\n')
|
|
await writeFile(join(profileDir, '.dsh-platform-pnpm-workspace.previous'), originalWorkspace)
|
|
assert.equal(storage.prepareProfilePackageStorage('web').status, 'migrated')
|
|
assert.equal(await readFile(join(modulesRoot, 'migration-marker'), 'utf8'), 'rebuilt\n')
|
|
await assert.rejects(readFile(join(modulesRoot, 'partial-marker')), { code: 'ENOENT' })
|
|
} finally {
|
|
for (const [name, value] of Object.entries(previous)) {
|
|
if (value === undefined) delete process.env[name]
|
|
else process.env[name] = value
|
|
}
|
|
}
|
|
})
|
|
|
|
test('gates only managed Web launches and routes restart through Management', async t => {
|
|
const root = await fixture()
|
|
applyPatch(root)
|
|
const runRoot = await mkdtemp(join(tmpdir(), 'dsh-managed-lifecycle-run-'))
|
|
const dshHome = await mkdtemp(join(tmpdir(), 'dsh-managed-lifecycle-home-'))
|
|
const profileDir = join(dshHome, 'profiles', 'web')
|
|
await mkdir(profileDir, { recursive: true })
|
|
await mkdir(join(root, 'node_modules', 'built-in-bundle'), { recursive: true })
|
|
await writeFile(join(root, 'node_modules', 'built-in-bundle', 'package.json'), '{"name":"built-in-bundle"}\n')
|
|
const profileManifest = join(profileDir, 'package.json')
|
|
await writeFile(join(profileDir, 'pnpm-workspace.yaml'), 'packages:\n - .\n\nnodeLinker: hoisted\n')
|
|
await writeFile(profileManifest, JSON.stringify({
|
|
dependencies: { 'missing-dependency': '1.0.0' },
|
|
dsh: { profile: { bundles: ['built-in-bundle', 'removed-plugin', '../../outside', 'missing-dependency'] } },
|
|
}, null, 2))
|
|
const calls = []
|
|
let claimStatus = 200
|
|
let disposition = 'request-restart'
|
|
let lifecycleState = 'running'
|
|
let signalDelayMs = 0
|
|
const broker = await listen(join(runRoot, 'dsh-lifecycle.sock'), async (request, response) => {
|
|
const chunks = []
|
|
for await (const chunk of request) chunks.push(chunk)
|
|
calls.push({ socket: 'broker', path: request.url, body: JSON.parse(Buffer.concat(chunks)) })
|
|
if (claimStatus !== 200) json(response, claimStatus, { error: 'launch already claimed' })
|
|
else if (request.url === '/v1/runtime/claim') json(response, 200, { sessionId: 'session-1' })
|
|
else if (request.url === '/v1/runtime/ready') json(response, 200, { ready: true })
|
|
else {
|
|
if (signalDelayMs > 0) await new Promise(resolve => setTimeout(resolve, signalDelayMs))
|
|
json(response, 200, { disposition })
|
|
}
|
|
})
|
|
const management = await listen(join(runRoot, 'management-cli.sock'), (request, response) => {
|
|
calls.push({ socket: 'management', path: request.url })
|
|
if (request.url?.endsWith('/status')) json(response, 200, { dshLifecycle: { state: lifecycleState }, recoveryMode: null })
|
|
else json(response, 202, { taskId: request.url?.endsWith('/start-dsh') ? 'start-task' : 'restart-task' })
|
|
})
|
|
t.after(() => { broker.close(); management.close() })
|
|
const previous = Object.fromEntries(['DSH_HOME', 'DSH_PLATFORM_MANAGED', 'DSH_PLATFORM_RUN', 'DSH_PLATFORM_LAUNCH_TOKEN']
|
|
.map(name => [name, process.env[name]]))
|
|
process.env.DSH_HOME = dshHome
|
|
process.env.DSH_PLATFORM_MANAGED = '1'
|
|
process.env.DSH_PLATFORM_RUN = runRoot
|
|
process.env.DSH_PLATFORM_LAUNCH_TOKEN = 'launch-token'
|
|
try {
|
|
const adapter = await import(`${pathToFileURL(join(root, MANAGED_LIFECYCLE_MODULE)).href}?authorized`)
|
|
assert.equal(await adapter.prepareManagedInvocation({ mode: 'plugin', profile: 'web' }), null)
|
|
assert.equal(calls.length, 0)
|
|
assert.match(await readFile(join(profileDir, 'pnpm-workspace.yaml'), 'utf8'), new RegExp(
|
|
`storeDir: ${JSON.stringify(join(dshHome, '.pnpm-store')).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
|
|
))
|
|
assert.equal(await adapter.prepareManagedInvocation({ mode: 'profile', profile: 'web' }), null)
|
|
assert.equal(process.env.DSH_PLATFORM_LAUNCH_TOKEN, undefined)
|
|
assert.deepEqual(
|
|
JSON.parse(await readFile(profileManifest, 'utf8')).dsh.profile.bundles,
|
|
['built-in-bundle', 'missing-dependency'],
|
|
)
|
|
await adapter.markManagedReady({
|
|
get: name => name === 'connection'
|
|
? { authenticatedUrl: base => `${base}/?token=fixture-token` }
|
|
: name === 'webServer' ? { port: 3079 } : undefined,
|
|
})
|
|
assert.equal(calls.some(value => value.socket === 'broker' && value.path === '/v1/runtime/ready'), true)
|
|
assert.deepEqual(calls.find(value => value.path === '/v1/runtime/ready')?.body, {
|
|
sessionId: 'session-1',
|
|
readyUrl: 'http://127.0.0.1:3079/?token=fixture-token',
|
|
})
|
|
let provided
|
|
adapter.provideManagedLifecycle({ provide: (name, value) => { provided = { name, value } } })
|
|
assert.equal(provided.name, 'dshPlatformLifecycle')
|
|
assert.deepEqual(await provided.value.restart(), { taskId: 'restart-task' })
|
|
const restartCallsBeforeSigterm = calls.filter(value => value.path?.endsWith('/restart-dsh')).length
|
|
let interrupts = 0
|
|
const onSigterm = adapter.managedSigtermHandler(() => { interrupts += 1 })
|
|
onSigterm()
|
|
for (let attempt = 0; attempt < 50 && calls.filter(value => value.path?.endsWith('/restart-dsh')).length === restartCallsBeforeSigterm; attempt += 1) {
|
|
await new Promise(resolve => setTimeout(resolve, 5))
|
|
}
|
|
assert.equal(calls.some(value => value.socket === 'broker' && value.path === '/v1/runtime/signal'), true)
|
|
assert.equal(calls.filter(value => value.path?.endsWith('/restart-dsh')).length, restartCallsBeforeSigterm + 1)
|
|
assert.equal(interrupts, 0)
|
|
onSigterm()
|
|
assert.equal(interrupts, 1)
|
|
|
|
const restartCallsBeforeCancellation = calls.filter(value => value.path?.endsWith('/restart-dsh')).length
|
|
signalDelayMs = 30
|
|
let cancellationInterrupts = 0
|
|
const cancelSigterm = adapter.managedSigtermHandler(() => { cancellationInterrupts += 1 })
|
|
cancelSigterm()
|
|
cancelSigterm()
|
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
assert.equal(cancellationInterrupts, 1)
|
|
assert.equal(calls.filter(value => value.path?.endsWith('/restart-dsh')).length, restartCallsBeforeCancellation)
|
|
signalDelayMs = 0
|
|
|
|
disposition = 'terminate'
|
|
let terminationInterrupts = 0
|
|
const managementCalls = calls.filter(value => value.path?.endsWith('/restart-dsh')).length
|
|
adapter.managedSigtermHandler(() => { terminationInterrupts += 1 })()
|
|
for (let attempt = 0; attempt < 50 && terminationInterrupts === 0; attempt += 1) {
|
|
await new Promise(resolve => setTimeout(resolve, 5))
|
|
}
|
|
assert.equal(terminationInterrupts, 1)
|
|
assert.equal(calls.filter(value => value.path?.endsWith('/restart-dsh')).length, managementCalls)
|
|
|
|
claimStatus = 409
|
|
disposition = 'request-restart'
|
|
const duplicate = await import(`${pathToFileURL(join(root, MANAGED_LIFECYCLE_MODULE)).href}?duplicate`)
|
|
assert.equal(await duplicate.prepareManagedInvocation({ mode: 'profile', profile: 'web' }), 0)
|
|
lifecycleState = 'stopped'
|
|
const stopped = await import(`${pathToFileURL(join(root, MANAGED_LIFECYCLE_MODULE)).href}?stopped`)
|
|
assert.equal(await stopped.prepareManagedInvocation({ mode: 'profile', profile: 'web' }), 0)
|
|
assert.equal(calls.some(value => value.path?.endsWith('/start-dsh')), true)
|
|
lifecycleState = 'failed'
|
|
const failed = await import(`${pathToFileURL(join(root, MANAGED_LIFECYCLE_MODULE)).href}?failed`)
|
|
assert.equal(await failed.prepareManagedInvocation({ mode: 'profile', profile: 'web' }), 1)
|
|
} finally {
|
|
for (const [name, value] of Object.entries(previous)) {
|
|
if (value === undefined) delete process.env[name]
|
|
else process.env[name] = value
|
|
}
|
|
}
|
|
})
|
|
|
|
test('keeps native behavior without the image marker and fails closed without the Broker', async () => {
|
|
const root = await fixture()
|
|
applyPatch(root)
|
|
const adapter = await import(`${pathToFileURL(join(root, MANAGED_LIFECYCLE_MODULE)).href}?fail-closed`)
|
|
const previous = Object.fromEntries(['DSH_PLATFORM_MANAGED', 'DSH_PLATFORM_RUN']
|
|
.map(name => [name, process.env[name]]))
|
|
try {
|
|
delete process.env.DSH_PLATFORM_MANAGED
|
|
assert.equal(await adapter.prepareManagedInvocation({ mode: 'profile', profile: 'web' }), null)
|
|
process.env.DSH_PLATFORM_MANAGED = '1'
|
|
process.env.DSH_PLATFORM_RUN = await mkdtemp(join(tmpdir(), 'dsh-no-broker-'))
|
|
assert.equal(await adapter.prepareManagedInvocation({ mode: 'profile', profile: 'web' }), 1)
|
|
} finally {
|
|
for (const [name, value] of Object.entries(previous)) {
|
|
if (value === undefined) delete process.env[name]
|
|
else process.env[name] = value
|
|
}
|
|
}
|
|
})
|