This commit is contained in:
Raven Scott
2026-04-03 19:54:49 -04:00
parent 4dfee8398b
commit eb6e8260cf
19 changed files with 302 additions and 39 deletions
+2 -2
View File
@@ -20,11 +20,11 @@ HTTP in Pear/Bare uses **WHATWG Fetch** (Node `fetch` or **`bare-fetch`** on `ba
| `-f` / `--fail` | supported | Exit 22 |
| `-s` / `-S` / `-v` | supported | |
| `-u` / `--user` | supported | Basic auth only |
| `-m` / `--max-time` | supported | Whole-request `AbortSignal` timeout (not separate connect timeout) |
| `-m` / `--max-time` | supported | Whole-request `AbortSignal` timeout; combined with `--connect-timeout` as **`min(max-time, connect-timeout)`** when both are set |
| `-w` / `--write-out` | partial | `%{http_code}`, `%{url_effective}`, `%{size_download}`; `%{num_redirects}` non-zero only for `-I -L` manual hop path |
| `-V` / `--version`, `-h` / `--help` | supported | |
| Cookie jar, `--cookie`, `-b`, `-c` / `--cookie-jar` | supported | JSON map `hostname → { name → value }` on **`~/.config/bare-os/curl/cookies.json`** (or path from `-c`); `-b` file or `name=value`; see `curl-cli.js` |
| `--connect-timeout`, `-Y`, `-y` | out of scope | Fetch exposes one abort timer |
| `--connect-timeout` | supported | Seconds (integer or decimal); sets the abort deadline when used alone, or **`min`** with **`--max-time`** when both are set. **`-Y` / `-y`** remain out of scope |
| `--cacert`, `-k` / `--insecure` | partial | Node **undici** `Agent` when using global `fetch` (not when `ctx.httpFetch` is overridden) |
| TLS client certs (mutual TLS) | out of scope | Stack only |
| HTTP/2, HTTP/3, SOCKS, FTP, SCP | out of scope | |
+19
View File
@@ -242,6 +242,22 @@ async function executeKernel(disk, store, swarm, initSource) {
if (v != null && v !== '') shellEnv[k] = v
}
}
let bootProfileResolved = ''
const bpfEarly = shellEnv.BARE_OS_BOOT_PROFILE
if (bpfEarly != null && String(bpfEarly).trim()) {
bootProfileResolved = String(bpfEarly).trim()
} else {
try {
const pbuf = await disk.drive.get('/etc/bare-os/profile')
if (pbuf) {
const line = b4a.toString(pbuf).split(/\r?\n/)[0] || ''
bootProfileResolved = line.trim()
}
} catch {
/* ignore */
}
}
shellEnv.BARE_OS_BOOT_PROFILE_RESOLVED = bootProfileResolved
/** @type {{ getMounts: () => Map<string, { drive: import('hyperdrive').default, writable: boolean }> }} */
const vfsMountRef = { getMounts: () => new Map() }
const bootStartedMs = Date.now()
@@ -251,6 +267,9 @@ async function executeKernel(disk, store, swarm, initSource) {
cmdline: 'bare-os-booter'
},
bootStartedMs,
bootProfileText() {
return bootProfileResolved ? `${bootProfileResolved}\n` : '\n'
},
initdRunText() {
const lines = [
'# bare-initd units (name<TAB>phase<TAB>startedAtMs<TAB>description)',
@@ -33,6 +33,7 @@ export const BARE_OS_PSEUDO_FS_PATHS = Object.freeze([
'/proc/version',
'/run',
'/run/bare-os',
'/run/bare-os/boot_profile',
'/run/bare-os/units',
'/sys',
'/sys/fs',
+29 -3
View File
@@ -360,6 +360,8 @@ export async function runCurlCli(ctx, argv) {
/** @type {string | null} */
let writeOut = null
let maxTimeMs = 0
/** Whole-operation ceiling (Fetch has no separate connect phase; see CLI_PARITY.md). */
let connectTimeMs = 0
/** @type {string | null} */
let userColonPass = null
/** @type {string | null} */
@@ -577,6 +579,23 @@ export async function runCurlCli(ctx, argv) {
continue
}
if (a === '--connect-timeout') {
if (i + 1 >= args.length) {
ctx.console.error('curl: option requires an argument: --connect-timeout')
ctx.exitCode = 2
return
}
const sec = Number(args[++i])
if (!Number.isFinite(sec) || sec < 0) {
ctx.console.error('curl: invalid --connect-timeout')
ctx.exitCode = 2
return
}
connectTimeMs = Math.round(sec * 1000)
i++
continue
}
if (a === '-k' || a === '--insecure') {
insecureTls = true
i++
@@ -741,6 +760,13 @@ export async function runCurlCli(ctx, argv) {
return
}
const deadlineMs =
maxTimeMs > 0 && connectTimeMs > 0
? Math.min(maxTimeMs, connectTimeMs)
: maxTimeMs > 0
? maxTimeMs
: connectTimeMs
let m = method
if (!m) {
if (headOnly) m = 'HEAD'
@@ -873,16 +899,16 @@ export async function runCurlCli(ctx, argv) {
outForUrl = defaultFetchSaveName(url)
}
const ac = maxTimeMs > 0 ? new AbortController() : null
const ac = deadlineMs > 0 ? new AbortController() : null
const t =
maxTimeMs > 0
deadlineMs > 0
? setTimeout(() => {
try {
ac.abort()
} catch {
/* ignore */
}
}, maxTimeMs)
}, deadlineMs)
: null
let res
@@ -64,6 +64,7 @@ function printHelp(ctx, prog) {
${prog} start|stop|restart UNIT
${prog} enable|disable UNIT
${prog} is-enabled UNIT
${prog} is-active UNIT
${prog} help
Persistent preset: ~/.config/bare-os/initd/disabled.txt (personal drive).
@@ -347,6 +348,30 @@ export async function runSystemctlCli(ctx, argv) {
return
}
if (sub === 'is-active') {
const unit = rest[0]
if (!unit) {
ctx.console.error(`${prog}: is-active requires a UNIT`)
ctx.exitCode = 2
return
}
const def = findBareServiceDefinition(unit)
if (!def) {
ctx.console.error(`${prog}: unknown unit: ${unit}`)
ctx.exitCode = 3
return
}
const rt = getBareServiceRuntime(unit)
if (rt?.phase === 'active') {
ctx.console.log('active')
ctx.exitCode = 0
} else {
ctx.console.log('inactive')
ctx.exitCode = 3
}
return
}
ctx.console.error(`${prog}: unknown command: ${sub}`)
ctx.exitCode = 2
}
+19 -2
View File
@@ -31,7 +31,8 @@ const DIR_MARKER = '.bareos_empty'
* @param {{
* procSnapshot?: { version?: string, cmdline?: string },
* bootStartedMs?: number,
* initdRunText?: () => string
* initdRunText?: () => string,
* bootProfileText?: () => string
* }} [vfsOptions]
*/
export function createVfs(
@@ -50,6 +51,10 @@ export function createVfs(
typeof vfsOptions.initdRunText === 'function'
? vfsOptions.initdRunText
: null
const bootProfileText =
typeof vfsOptions.bootProfileText === 'function'
? vfsOptions.bootProfileText
: null
const HOME = () => env.HOME || '/home/guest'
let cwd = env.PWD || HOME()
@@ -208,6 +213,10 @@ export function createVfs(
: '# bare-initd: no snapshot provider\n'
return utf8Encode(t)
}
if (k === 'run' && f === 'boot_profile') {
const t = bootProfileText ? bootProfileText() : '\n'
return utf8Encode(t)
}
if (k === 'dev' && f === 'null') return utf8Encode('')
if (k === 'dev' && f === 'zero') return new Uint8Array(65536)
if (k === 'dev' && f === 'urandom') return pseudoUrandomBytes()
@@ -264,6 +273,14 @@ export function createVfs(
if (n === '/run/bare-os/units') {
return { virtualPseudo: true, kind: 'run', node: 'file', file: 'units' }
}
if (n === '/run/bare-os/boot_profile') {
return {
virtualPseudo: true,
kind: 'run',
node: 'file',
file: 'boot_profile'
}
}
return { virtualPseudo: true, kind: 'run', node: 'enoent' }
}
if (n === '/dev' || n.startsWith('/dev/')) {
@@ -795,7 +812,7 @@ export function createVfs(
return ['bare-os']
}
if (pr.kind === 'run' && pr.node === 'dir' && pr.dir === 'bare_os') {
return ['units']
return ['boot_profile', 'units']
}
if (pr.kind === 'dev' && pr.node === 'root') {
return ['null', 'urandom', 'zero']
+104
View File
@@ -177,6 +177,40 @@ test('stock kernel init.js runs rc.local before onboot', async (t) => {
t.is(execLines[1], 'echo onboot-line')
})
test('stock kernel init.js runs kernel.d after rc.local before onboot', async (t) => {
const src = await readFile(
path.join(__dirname, '../../kernel/init.js'),
'utf8'
)
const execLines = []
const drive = {
async get(p) {
if (p === '/etc/bare-os/rc.local') return b4a.from('echo rc-local')
if (p === '/etc/bare-os/kernel.d/05-k.mod') return b4a.from('echo kmod')
return null
},
async *readdir(dir) {
if (dir === '/etc/bare-os/kernel.d') yield '05-k.mod'
}
}
const ctx = {
bareOsSkipRepl: true,
env: { BARE_OS_ONBOOT: 'echo onboot-line' },
drive,
b4a,
console: { log() {}, error() {} },
readLine: async () => null,
async execLine(line) {
execLines.push(String(line).trim())
return 'ok'
}
}
await runKernelFromSource(src, ctx)
t.is(execLines[0], 'echo rc-local')
t.is(execLines[1], 'echo kmod')
t.is(execLines[2], 'echo onboot-line')
})
test('stock kernel init.js BARE_OS_BOOT_TRACE=json emits phase JSON on stderr', async (t) => {
const src = await readFile(
path.join(__dirname, '../../kernel/init.js'),
@@ -219,6 +253,7 @@ test('stock kernel init.js BARE_OS_BOOT_TRACE=json emits phase JSON on stderr',
.filter(Boolean)
t.ok(phases.length >= 3)
t.ok(phases.some((o) => o.phase === 'rc.local'))
t.ok(phases.some((o) => o.phase === 'kernel.d'))
t.ok(phases.every((o) => typeof o.ms === 'number'))
})
@@ -566,6 +601,7 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
const vfs = createVfs(sys, personal, env, null, {
procSnapshot: { version: '1.2.3-test', cmdline: 'unit-test' },
bootStartedMs: Date.now() - 4000,
bootProfileText: () => 'mini\n',
initdRunText: () => 'demo-unit\tactive\t1\tdemo\n'
})
const root = await vfs.readdir('/')
@@ -616,6 +652,11 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
t.is((await vfs.readFile('/dev/null'))?.byteLength ?? 0, 0)
const z = await vfs.readFile('/dev/zero')
t.ok(z && z.byteLength === 65536)
t.alike(await vfs.readdir('/run/bare-os').then((a) => [...a].sort()), [
'boot_profile',
'units'
])
t.is(b4a.toString(await vfs.readFile('/run/bare-os/boot_profile')), 'mini\n')
const units = b4a.toString(await vfs.readFile('/run/bare-os/units'))
t.ok(units.includes('demo-unit'))
let writeErr = null
@@ -876,6 +917,7 @@ test('buildBareOsRuntimeCaps matches ctx API version and pipeline env', async (t
t.is(caps.pipeline.maxStages, 8)
t.ok(Array.isArray(caps.pseudoFsPaths))
t.ok(caps.pseudoFsPaths.includes('/proc/version'))
t.ok(caps.pseudoFsPaths.includes('/run/bare-os/boot_profile'))
t.is(caps.features.simulatedPipelines, true)
t.is(caps.features.httpDelegate, true)
t.is(caps.features.gitDelegate, true)
@@ -1317,6 +1359,34 @@ test('systemctl list-units delegates to same backend', async (t) => {
rmSync(dir, { recursive: true, force: true })
})
test('systemctl is-active matches bare-initd runtime', async (t) => {
const dir = testCorestoreDir('initctlactive')
const store = new Corestore(dir)
const sys = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('pica'))
await sys.ready()
await personal.ready()
const logs = []
const ctx = testCtx(sys, personal)
ctx.execLine = async () => {}
await startBareInitd(ctx)
ctx.console = {
log: (...a) => logs.push(a.join(' ')),
error: (...a) => logs.push(a.join(' '))
}
ctx.exitCode = 0
await runBinCommand(ctx, ['systemctl', 'is-active', 'kernel-logger'])
t.is(ctx.exitCode, 0)
t.ok(logs.join('\n').includes('active'))
logs.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['systemctl', 'is-active', 'nonexistent-unit'])
t.is(ctx.exitCode, 3)
stopBareInitd()
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('systemctl restart bare-cron succeeds', async (t) => {
const dir = testCorestoreDir('initctlrst')
const store = new Corestore(dir)
@@ -1911,6 +1981,40 @@ test('curl delegated from booter with stub fetch', async (t) => {
rmSync(dir, { recursive: true, force: true })
})
test('curl --connect-timeout aborts hanging stub fetch', async (t) => {
const dir = testCorestoreDir('curlcto')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('pccto'))
await drive.ready()
await personal.ready()
const ctx = testCtx(drive, personal)
ctx.exitCode = 0
ctx.console = { log() {}, error() {} }
ctx.httpFetch = async (_url, init) =>
new Promise((_resolve, reject) => {
const sig = init && init.signal
if (sig && sig.aborted) {
reject(new Error('The operation was aborted'))
return
}
if (sig) {
sig.addEventListener('abort', () =>
reject(new Error('The operation was aborted'))
)
}
})
await runBinCommand(ctx, [
'curl',
'--connect-timeout',
'1',
'https://stub.example/hang'
])
t.is(ctx.exitCode, 7)
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('wget delegated from booter with stub fetch', async (t) => {
const dir = testCorestoreDir('wget')
const store = new Corestore(dir)
@@ -7,9 +7,12 @@
"systemctl status [UNIT] [--lines N]",
"systemctl logs UNIT [--lines N]",
"systemctl start|stop|restart UNIT",
"systemctl enable|disable UNIT",
"systemctl is-enabled UNIT",
"systemctl is-active UNIT",
"journalctl -u UNIT [--lines N]"
],
"description": "Lists and manages session-scoped bare-initd units (kernel-logger, bare-cron, …). Implemented by the booter (kernel-runner); /bin stubs exist for PATH and man(1). Units are not persistent: there is no enable/disable. Logs live under /var/log/bare-os/ when the unit defines a logPath. The legacy name bare-initctl is still accepted by the booter as an alias.",
"description": "Lists and manages session-scoped bare-initd units (kernel-logger, bare-cron, …). Implemented by the booter (kernel-runner); /bin stubs exist for PATH and man(1). enable/disable toggle the personal-drive preset file ~/.config/bare-os/initd/disabled.txt for future boots in the same image. is-enabled reports enabled or disabled; is-active reports active vs inactive from runtime phase (exit 0 vs 3). Logs live under /var/log/bare-os/ when the unit defines a logPath. The legacy name bare-initctl is still accepted by the booter as an alias.",
"options": [
{
"flag": "--lines N",
+1 -1
View File
@@ -14,7 +14,7 @@ Files in this directory are **read from disk by the seeder** (or copied into `pa
## Contents
- **`init.js`** — Kernel entry: must define `async function start(ctx)`. Boot order: **`/etc/os-release`** → **`/etc/motd`** → optional **`/etc/bare-os/rc.profile.<profile>`** (profile from **`BARE_OS_BOOT_PROFILE`** or first line of **`/etc/bare-os/profile`**) → **`/etc/bare-os/rc`** → **`/etc/bare-os/rc.d/*`** (sorted; only names starting with a digit, plus skips dotfiles, `*~`, `README*`, `*.md`) → optional **`/etc/bare-os/rc.local`** → banner → when **`BARE_OS_SKIP_REPL`**, optional **onboot** lines from **`BARE_OS_ONBOOT`** (newline-separated) or **`/etc/bare-os/onboot`** (file order) → **`readLine` / `execLine`** loop (boot snippet errors are logged, not fatal). Custom kernels may call **`ctx.registerKernelShutdownHook(fn)`** before initd disposers; use **`ctx.bareOsRuntimeCaps`** for limits, pseudo paths, and **`features`** ([`developer-guide/02-the-context-object.md`](../developer-guide/02-the-context-object.md)).
- **`init.js`** — Kernel entry: must define `async function start(ctx)`. Boot order: **`/etc/os-release`** → **`/etc/motd`** → optional **`/etc/bare-os/rc.profile.<profile>`** (profile from **`BARE_OS_BOOT_PROFILE`** or first line of **`/etc/bare-os/profile`**; the booter mirrors the resolved name in **`ctx.env.BARE_OS_BOOT_PROFILE_RESOLVED`** and **`/run/bare-os/boot_profile`**) → **`/etc/bare-os/rc`** → **`/etc/bare-os/rc.d/*`** (sorted; only names starting with a digit, plus skips dotfiles, `*~`, `README*`, `*.md`) → optional **`/etc/bare-os/rc.local`** → **`/etc/bare-os/kernel.d/*`** (same rules as **`rc.d`**) → banner → when **`BARE_OS_SKIP_REPL`**, optional **onboot** lines from **`BARE_OS_ONBOOT`** (newline-separated) or **`/etc/bare-os/onboot`** (file order) → **`readLine` / `execLine`** loop (boot snippet errors are logged, not fatal). Custom kernels may call **`ctx.registerKernelShutdownHook(fn)`** before initd disposers; use **`ctx.bareOsRuntimeCaps`** for limits, pseudo paths, and **`features`** ([`developer-guide/02-the-context-object.md`](../developer-guide/02-the-context-object.md)).
- **`bin/`** — **Tier-1 utilities** built by [bare-os-coreutils](../packages/bare-os-coreutils/README.md). Each file is **`runtime.js`** + optional **`lib/*-engine.js`** (**`sed`**, **`awk`**) or **`lib/man-render.js`** (**`man`**) + **`async function run(ctx, argv)`** (no ESM **`import`** in **`src/`**).
- **`share/man/man.json`** — Merged manual database for **`/bin/man`** (built by **`bare-os-coreutils`**; see [handbook ch.10](../handbook/10-manpages-and-online-help.md)).
- **`etc/os-release`** — Static OS metadata (`NAME`, `VERSION`, …).
+34 -1
View File
@@ -4,7 +4,8 @@
*
* Boot order: /etc/os-release → /etc/motd → optional profile rc → /etc/bare-os/rc →
* /etc/bare-os/rc.d/* (sorted; digit-prefixed snippet names) → /etc/bare-os/rc.local →
* session banner → optional onboot lines when BARE_OS_SKIP_REPL → interactive loop.
* /etc/bare-os/kernel.d/* (same naming rules as rc.d) → session banner →
* optional onboot lines when BARE_OS_SKIP_REPL → interactive loop.
*
* Profile: first non-empty line of /etc/bare-os/profile, overridden by BARE_OS_BOOT_PROFILE.
* When set, runs /etc/bare-os/rc.profile.<name> if present (trusted execLine, before main rc).
@@ -201,6 +202,37 @@ function isBareOsRcSnippetFile(name) {
return /^[0-9]/.test(name)
}
/**
* Optional snippets under /etc/bare-os/kernel.d/ — same rules as rc.d; runs after rc.local.
* @param {Record<string, unknown>} ctx
*/
async function runBareOsKernelDir(ctx) {
const { drive, b4a, console } = ctx
try {
/** @type {string[]} */
const names = []
try {
for await (const n of drive.readdir('/etc/bare-os/kernel.d')) names.push(n)
} catch {
return
}
names.sort()
for (const name of names) {
if (!isBareOsRcSnippetFile(name)) continue
const p = `/etc/bare-os/kernel.d/${name}`
try {
const buf = await drive.get(p)
if (!buf) continue
await runRcLines(ctx, b4a.toString(buf))
} catch (e) {
console.error(`kernel.d/${name}: ` + ((e && e.message) || String(e)))
}
}
} catch (e) {
console.error((e && e.message) || String(e))
}
}
/**
* Optional snippets under /etc/bare-os/rc.d/ — executed in lexicographic order.
* @param {Record<string, unknown>} ctx
@@ -270,6 +302,7 @@ async function start(ctx) {
await bootTimed(ctx, 'rc.local', () =>
runRcFileAt(ctx, '/etc/bare-os/rc.local', 'rc.local')
)
await bootTimed(ctx, 'kernel.d', () => runBareOsKernelDir(ctx))
await printSessionBanner(ctx)
await bootTimed(ctx, 'onboot', () => runOnboot(ctx))
while (true) {
File diff suppressed because one or more lines are too long