Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions stasis-core/bin/stasis-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { once } from 'node:events'
import { fileURLToPath } from 'node:url'
import { basename, resolve } from 'node:path'
import { existsSync, realpathSync } from 'node:fs'
import { constants as osConstants } from 'node:os'
import assert from 'node:assert/strict'
import pkg from '../package.json' with { type: 'json' }

Expand Down Expand Up @@ -103,8 +104,9 @@ if (command === '-v' || command === '--version') {
setEnv('EXODUS_STASIS_RESOURCES', resources)
const nodeArgs = ['--import', import.meta.resolve('../src/loader.js')]
const child = spawn(process.execPath, [...nodeArgs, ...argv], { stdio: 'inherit' })
const [code] = await once(child, 'close')
process.exitCode = code
const [code, signal] = await once(child, 'close')
// code is null when the child died from a signal; report 128+signo (shell convention) instead of the implicit 0
process.exitCode = code ?? 128 + (osConstants.signals[signal] ?? 0)
} else if (command === 'prune') {
if (argv.length > 1) usage('Error: prune takes at most one path argument')
const root = argv[0] ? resolve(argv[0]) : process.cwd()
Expand Down
6 changes: 4 additions & 2 deletions stasis/bin/stasis.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { once } from 'node:events'
import { fileURLToPath } from 'node:url'
import { basename, dirname, resolve } from 'node:path'
import { existsSync, realpathSync } from 'node:fs'
import { constants as osConstants } from 'node:os'
import assert from 'node:assert/strict'
import pkg from '../package.json' with { type: 'json' }

Expand Down Expand Up @@ -167,8 +168,9 @@ if (command === '-v' || command === '--version') {
const loaderEntry = values.mock ? '../src/loader-mock.js' : '@exodus/stasis-core/loader'
nodeArgs.push('--import', import.meta.resolve(loaderEntry))
const child = spawn(process.execPath, [...nodeArgs, ...argv], { stdio: 'inherit' })
const [code] = await once(child, 'close')
process.exitCode = code
const [code, signal] = await once(child, 'close')
// code is null when the child died from a signal; report 128+signo (shell convention) instead of the implicit 0
process.exitCode = code ?? 128 + (osConstants.signals[signal] ?? 0)
} else if (command === 'bundle') {
const flags = []
const valueFlags = new Set(['--mapping', '--output', '--scope', '--lockfile', '--conditions', '--mainFields', '--platforms', '-o'])
Expand Down
11 changes: 10 additions & 1 deletion tests/stasis-core-cli.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test } from 'node:test'
import { spawnSync } from 'node:child_process'
import { cpSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
Expand Down Expand Up @@ -117,6 +117,15 @@ test('run: a forked child is enforced from the bundle but does not write (via th
t.assert.deepEqual(readFileSync(join(tmp, 'stasis.lock.json')), lockBaseline, 'child must not rewrite the lockfile')
}))

test('run reports 128+signo when the child dies from a signal', withTmp((t, tmp) => {
// A signal-killed child (SIGSEGV, SIGKILL, OOM-kill) closes with code=null; the launcher
// must not turn that into an implicit exit 0. 137 = 128 + SIGKILL(9), the shell convention.
writeFileSync(join(tmp, 'package.json'), JSON.stringify({ name: 'sigkill-fixture', version: '0.0.0', private: true, type: 'module' }))
writeFileSync(join(tmp, 'entry.js'), "process.kill(process.pid, 'SIGKILL')\n")
const r = run(['run', '--lock=ignore', 'entry.js'], { cwd: tmp })
t.assert.equal(r.status, 137, `stderr: ${r.stderr}`)
}))

test('prune validates and removes against the lockfile', withTmp((t, tmp) => {
cpSync(pruneFixture, tmp, { recursive: true })
const r = run(['prune', tmp])
Expand Down