Updates
This commit is contained in:
@@ -23,8 +23,10 @@ The following are set on `ctx` before the kernel starts (unless noted as overwri
|
|||||||
|
|
||||||
| Field | Role |
|
| Field | Role |
|
||||||
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **`bareOsCtxApiVersion`** | String semver for the documented **`ctx`** contract (e.g. **`1.3.0`**). Bump in [`bare-os-ctx-api.js`](../packages/bare-os-booter/lib/bare-os-ctx-api.js) when you make breaking changes to stable fields. |
|
| **`bareOsCtxApiVersion`** | String semver for the documented **`ctx`** contract (e.g. **`1.4.0`**). Bump in [`bare-os-ctx-api.js`](../packages/bare-os-booter/lib/bare-os-ctx-api.js) when you make breaking changes to stable fields. |
|
||||||
| **`bareOsRuntimeCaps`** | **Frozen** snapshot from [`bare-os-runtime-caps.js`](../packages/bare-os-booter/lib/bare-os-runtime-caps.js): **`ctxApiVersion`**, resolved simulated **pipeline** limits (from **`env`** / defaults), documented pseudo **`/`** paths (**`pseudoFsPaths`**), and **`features`** flags (**`simulatedPipelines`**, **`bareInitd`**, **`vfsTwoDrive`**, **`identityVault`**, **`httpDelegate`**, **`gitDelegate`**, **`systemctlDelegate`**). Reflects session env at **`executeKernel`** start (including host **`BARE_OS_PIPELINE_*`** when set). |
|
| **`bareOsRuntimeCaps`** | **Frozen** snapshot from [`bare-os-runtime-caps.js`](../packages/bare-os-booter/lib/bare-os-runtime-caps.js): **`ctxApiVersion`**, simulated **pipeline** limits, **`pseudoFsPaths`**, and **`features`** (including **`jobControl`**, **`shellHereString`**, **`bootReadyPseudoFs`**, **`vfsChown`**, **`auditLog`**, **`initdRequiresWants`**, **`seederRpcExtended`**, …). |
|
||||||
|
| **`bareOsPublishBootReady(patch)`** | Kernel-only: merge **`patch`** into the session boot-ready state exposed as **`/run/bare-os/boot.json`** and **`/run/bare-os/ready`**. |
|
||||||
|
| **`bareOsSessionStats`** | Mutable **`{ execLineCount, pipelineBytesTotal }`** mirrored in **`/proc/bare_os_session_stats`**. |
|
||||||
| **`bareOsBootStartedMs`** | Epoch milliseconds when the booter started building the session (used for synthetic **`/proc/uptime`**). |
|
| **`bareOsBootStartedMs`** | Epoch milliseconds when the booter started building the session (used for synthetic **`/proc/uptime`**). |
|
||||||
| **`bareOsSkipRepl`** | **`true`** when **`BARE_OS_SKIP_REPL=1`** (non-interactive stdin); kernels may shorten banners. |
|
| **`bareOsSkipRepl`** | **`true`** when **`BARE_OS_SKIP_REPL=1`** (non-interactive stdin); kernels may shorten banners. |
|
||||||
| **`disk`** | Disk bundle used during boot (includes drives and helpers); advanced use |
|
| **`disk`** | Disk bundle used during boot (includes drives and helpers); advanced use |
|
||||||
@@ -45,7 +47,7 @@ The following are set on `ctx` before the kernel starts (unless noted as overwri
|
|||||||
| **`runBinCommand(argv)`** | Runs a command with the **same** resolution rules as the interactive shell (used by **`time`**, **`xargs`**, and similar) |
|
| **`runBinCommand(argv)`** | Runs a command with the **same** resolution rules as the interactive shell (used by **`time`**, **`xargs`**, and similar) |
|
||||||
| **`registerKernelShutdownHook(fn)`** | Register an async or sync function to run when the REPL session ends, **before** **`stopBareInitd`** and initd **disposers**. Pair with **`registerBareInitdDisposer(fn)`** in [`bare-initd.js`](../packages/bare-os-booter/lib/bare-initd.js) when you need teardown **after** shutdown hooks but still inside **`stopBareInitd`** (intervals, sync cleanup). |
|
| **`registerKernelShutdownHook(fn)`** | Register an async or sync function to run when the REPL session ends, **before** **`stopBareInitd`** and initd **disposers**. Pair with **`registerBareInitdDisposer(fn)`** in [`bare-initd.js`](../packages/bare-os-booter/lib/bare-initd.js) when you need teardown **after** shutdown hooks but still inside **`stopBareInitd`** (intervals, sync cleanup). |
|
||||||
|
|
||||||
Kernel boot composition lives on the **system image** (`/boot/init.js`, `/etc/bare-os/rc`, `/etc/bare-os/rc.d/`, optional **`/etc/bare-os/rc.local`**, optional **`/etc/bare-os/kernel.d/`** (same digit-prefix rules as **`rc.d`**), optional **`/etc/bare-os/profile`** / **`rc.profile.*`**, **`/etc/bare-os/onboot`**), not on `ctx`—extend the image or hooks like **`registerKernelShutdownHook`** rather than adding boot fields to the context object. The booter seeds **`ctx.env`** from the host for **`BARE_OS_PIPELINE_MAX_STAGES`**, **`BARE_OS_PIPELINE_MAX_BYTES`**, **`BARE_OS_PIPELINE_MAX_LINES`**, **`BARE_OS_BOOT_PROFILE`**, **`BARE_OS_ONBOOT`**, **`BARE_OS_BOOT_STRICT`**, and **`BARE_OS_RC_D_SKIP`** when those variables are set, always sets **`BARE_OS_BOOT_PROFILE_RESOLVED`** for **`rc.profile.*`** and **`/run/bare-os/boot_profile`**, and assigns **`BARE_OS_SESSION_ID`** (UUID) for **`/run/bare-os/session`** and **`/proc/self/environ`**. When **`ctx.httpFetch`** handles **`curl`**, check optional **`init.bareOsCurlTls`** (`insecure`, **`caPem`**) for **`--cacert`** / **`-k`** semantics.
|
Kernel boot composition lives on the **system image** (`/boot/init.js`, `/etc/bare-os/rc`, `/etc/bare-os/rc.d/`, optional **`/etc/bare-os/rc.local`**, optional **`/etc/bare-os/kernel.d/`** (same digit-prefix rules as **`rc.d`**), optional **`/etc/bare-os/profile`** / **`rc.profile.*`**, **`/etc/bare-os/onboot`**), not on `ctx`—extend the image or hooks like **`registerKernelShutdownHook`** rather than adding boot fields to the context object. The booter seeds **`ctx.env`** from the host for **`BARE_OS_PIPELINE_*`**, **`BARE_OS_BOOT_PROFILE`**, **`BARE_OS_ONBOOT`**, **`BARE_OS_BOOT_STRICT`**, **`BARE_OS_RC_D_SKIP`**, **`BARE_OS_BOOT_MINIMAL`**, **`BARE_OS_BOOT_SKIP`**, **`BARE_OS_BOOT_TRACE`**, **`BARE_OS_KERNEL_SELFTEST`**, **`BARE_OS_AUDIT`**, and **`BARE_OS_IMAGE_DIGEST`** when set; always sets **`BARE_OS_BOOT_PROFILE_RESOLVED`** and **`BARE_OS_SESSION_ID`**. When **`ctx.httpFetch`** handles **`curl`**, check optional **`init.bareOsCurlTls`** (`insecure`, **`caPem`**) for **`--cacert`** / **`-k`** semantics.
|
||||||
|
|
||||||
After [`createKernelReplSession`](../packages/bare-os-booter/lib/repl-session.js) returns:
|
After [`createKernelReplSession`](../packages/bare-os-booter/lib/repl-session.js) returns:
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,17 @@ Staged from **`kernel/init.js`**. Responsibilities (typical):
|
|||||||
|
|
||||||
The **prompt** (`[user@host:path] > `) is applied by the booter’s readline layer, not by `init.js`.
|
The **prompt** (`[user@host:path] > `) is applied by the booter’s readline layer, not by `init.js`.
|
||||||
|
|
||||||
|
### Boot profiles, recovery, and readiness
|
||||||
|
|
||||||
|
| Host / kernel env | Purpose |
|
||||||
|
| ----------------- | ------- |
|
||||||
|
| **`BARE_OS_BOOT_PROFILE`** / **`/etc/bare-os/profile`** | First line names **`/etc/bare-os/rc.profile.<name>`** (before **`rc`**). Suggested names: **`dev`** (extra PATH, aliases), **`ci`** (**`BARE_OS_SKIP_REPL`** + **`BARE_OS_ONBOOT`** smoke lines), **`kiosk`** (minimal **`rc.d`**, fixed **`onboot`**). |
|
||||||
|
| **`BARE_OS_BOOT_MINIMAL`** | Skip **`rc`**, **`rc.d`**, **`rc.local`**, **`kernel.d`**, **`onboot`**, and profile **`rc.profile.*`** — recovery shell with **`os-release`** / **`motd`** only. |
|
||||||
|
| **`BARE_OS_BOOT_SKIP`** | Comma list of phases to skip: **`profile`**, **`rc`**, **`rc.d`**, **`rc.local`**, **`kernel.d`**, **`onboot`**. |
|
||||||
|
| **`BARE_OS_BOOT_TRACE=ndjson`** | One JSON object per boot phase on stderr (**`type`**, **`phase`**, **`ms`**, **`sessionId`**, **`ts`**). |
|
||||||
|
| **`BARE_OS_KERNEL_SELFTEST`** | After boot snippets, run a short trusted **`execLine`** checklist. |
|
||||||
|
| **`ctx.bareOsPublishBootReady(...)`** | Kernel calls this when boot is complete; populates **`/run/bare-os/ready`** (**`1`** / **`0`**) and **`/run/bare-os/boot.json`**. |
|
||||||
|
|
||||||
### Custom `init.js` contract
|
### Custom `init.js` contract
|
||||||
|
|
||||||
Replacing **`/boot/init.js`** on the system image is supported: the booter loads it with **`AsyncFunction`** and expects a top-level **`async function start(ctx)`**. Stable **`ctx`** fields for kernels are documented in [Chapter 4](04-the-booter-runtime.md) and the [context object developer guide](../developer-guide/02-the-context-object.md), including **`ctx.bareOsRuntimeCaps`** for pipeline limits, pseudo path lists, and **`features`** (e.g. **`httpDelegate`**, **`gitDelegate`**, **`systemctlDelegate`**). Use **`ctx.registerKernelShutdownHook`** for teardown that must run before **`stopBareInitd`**. Boot snippets under **`/etc/bare-os/rc`**, **`/etc/bare-os/rc.d/`** (only **`rc.d`** files whose names start with a digit), **`rc.local`**, **`kernel.d/`** (same digit-prefix rules), **`rc.profile.*`**, and **`onboot`** are **trusted** (full **`execLine`** power); keep them minimal.
|
Replacing **`/boot/init.js`** on the system image is supported: the booter loads it with **`AsyncFunction`** and expects a top-level **`async function start(ctx)`**. Stable **`ctx`** fields for kernels are documented in [Chapter 4](04-the-booter-runtime.md) and the [context object developer guide](../developer-guide/02-the-context-object.md), including **`ctx.bareOsRuntimeCaps`** for pipeline limits, pseudo path lists, and **`features`** (e.g. **`httpDelegate`**, **`gitDelegate`**, **`systemctlDelegate`**). Use **`ctx.registerKernelShutdownHook`** for teardown that must run before **`stopBareInitd`**. Boot snippets under **`/etc/bare-os/rc`**, **`/etc/bare-os/rc.d/`** (only **`rc.d`** files whose names start with a digit), **`rc.local`**, **`kernel.d/`** (same digit-prefix rules), **`rc.profile.*`**, and **`onboot`** are **trusted** (full **`execLine`** power); keep them minimal.
|
||||||
@@ -83,7 +94,8 @@ Hyperdrive entries carry optional **`metadata.bareOs`** (mode, uid, gid, names,
|
|||||||
| `sed`, `awk` | Large JavaScript engines in **`lib/*-engine.js`** — not byte-identical to GNU/POSIX everywhere; see ch. 9 |
|
| `sed`, `awk` | Large JavaScript engines in **`lib/*-engine.js`** — not byte-identical to GNU/POSIX everywhere; see ch. 9 |
|
||||||
| `tee`, `find`, `du`, `cksum` | Pipe tee, limited **`find`** (**`-maxdepth`**, **`-mindepth`**, **`-name`**, **`-type`**), **`du -k`**, POSIX CRC **`cksum`** |
|
| `tee`, `find`, `du`, `cksum` | Pipe tee, limited **`find`** (**`-maxdepth`**, **`-mindepth`**, **`-name`**, **`-type`**), **`du -k`**, POSIX CRC **`cksum`** |
|
||||||
| `time`, `logname` | Wall-clock **`time`** via **`ctx.runBinCommand`**; identity string |
|
| `time`, `logname` | Wall-clock **`time`** via **`ctx.runBinCommand`**; identity string |
|
||||||
| `chown`, `chgrp`, `mkfifo` | Stubs with explicit “not supported” messages |
|
| `chown`, `chgrp` | Update ownership in **`metadata.bareOs`** on the personal drive (same writable scope as **`chmod`**); supports **`:group`** and numeric ids; root vs owner rules as in VFS |
|
||||||
|
| `mkfifo` | Stub with explicit “not supported” message |
|
||||||
| `getconf`, `xargs` | Documented Bare subsets (fixed **`getconf`** table; bounded **`xargs`** via **`ctx.runBinCommand`**) |
|
| `getconf`, `xargs` | Documented Bare subsets (fixed **`getconf`** table; bounded **`xargs`** via **`ctx.runBinCommand`**) |
|
||||||
| `pathchk` | Path sanity |
|
| `pathchk` | Path sanity |
|
||||||
| `pwd` | Logical cwd |
|
| `pwd` | Logical cwd |
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ Files in this directory are **read from disk by the seeder** (or copied into `pa
|
|||||||
- **`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)).
|
- **`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`, …).
|
- **`etc/os-release`** — Static OS metadata (`NAME`, `VERSION`, …).
|
||||||
- **`etc/motd`** — Optional message printed after **`os-release`** (distributors can customize).
|
- **`etc/motd`** — Optional message printed after **`os-release`** (distributors can customize).
|
||||||
- **`etc/bare-os/banner`** or **`/etc/issue`** — If present on the system drive, the default kernel prints one of these instead of the built-in session hint (unless **`BARE_OS_SKIP_REPL`** shortens the banner). Set **`BARE_OS_BOOT_TRACE=1`** or **`true`** for **`[boot] phase: Nms`** lines on stderr, or **`json`** for **`{"phase":"…","ms":n}`** per phase.
|
- **`etc/bare-os/banner`** or **`/etc/issue`** — If present on the system drive, the default kernel prints one of these instead of the built-in session hint (unless **`BARE_OS_SKIP_REPL`** shortens the banner). Set **`BARE_OS_BOOT_TRACE=1`** or **`true`** for **`[boot] phase: Nms`** lines on stderr, **`json`** for **`{"phase":"…","ms":n}`** per phase, or **`ndjson`** for machine-readable lines with **`sessionId`**. Recovery: **`BARE_OS_BOOT_MINIMAL`**, granular **`BARE_OS_BOOT_SKIP`**, optional **`BARE_OS_KERNEL_SELFTEST`**, readiness via **`ctx.bareOsPublishBootReady`** → **`/run/bare-os/ready`** and **`/run/bare-os/boot.json`** (see [handbook ch.6](../handbook/06-kernel-and-binaries.md)).
|
||||||
- **`etc/bare-os/rc`** — Optional boot snippet: one **`execLine`** per non-comment line (trusted).
|
- **`etc/bare-os/rc`** — Optional boot snippet: one **`execLine`** per non-comment line (trusted).
|
||||||
- **`etc/bare-os/rc.d/`** — Optional extra snippets (basename must start with a digit), same line rules, run after **`rc`** in filename order. Human-oriented notes live in **`.README`** (a dotfile so legacy **`init.js`** never executes it).
|
- **`etc/bare-os/rc.d/`** — Optional extra snippets (basename must start with a digit), same line rules, run after **`rc`** in filename order. Human-oriented notes live in **`.README`** (a dotfile so legacy **`init.js`** never executes it).
|
||||||
|
|
||||||
|
|||||||
+84
-5
@@ -60,9 +60,88 @@ function barePosixBlocks(size) {
|
|||||||
return Math.ceil(Number(size) / 512) || 0
|
return Math.ceil(Number(size) / 512) || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
async function run(ctx, argv) {
|
/**
|
||||||
ctx.console.error(
|
* chgrp — change file group (Hyperdrive metadata on writable paths).
|
||||||
'chgrp: changing group is not supported on Bare OS (single-user Hyperdrive metadata).'
|
*/
|
||||||
)
|
|
||||||
ctx.exitCode = 1
|
/**
|
||||||
|
* @param {string} spec
|
||||||
|
* @param {Record<string, string>} env
|
||||||
|
* @returns {number | null}
|
||||||
|
*/
|
||||||
|
function parseGroupSpec(spec, env) {
|
||||||
|
const s = String(spec).trim()
|
||||||
|
if (!s) return null
|
||||||
|
if (/^\d+$/.test(s)) {
|
||||||
|
const n = Number.parseInt(s, 10)
|
||||||
|
return Number.isFinite(n) ? n : null
|
||||||
|
}
|
||||||
|
const g = (env.GROUP || env.USER || 'guest').trim()
|
||||||
|
if (s === 'root') return 0
|
||||||
|
if (s === 'guest' || s === 'nobody') return 65534
|
||||||
|
if (g && s === g) {
|
||||||
|
const gid = Number.parseInt(String(env.GID ?? '65534'), 10)
|
||||||
|
return Number.isFinite(gid) ? gid : 65534
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(ctx, argv) {
|
||||||
|
let i = 1
|
||||||
|
const files = []
|
||||||
|
for (; i < argv.length; i++) {
|
||||||
|
const a = argv[i]
|
||||||
|
if (a === '-h' || a === '--help') {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: chgrp [-h] GROUP FILE...\n' +
|
||||||
|
'GROUP may be a numeric gid or root/guest/nobody/current GROUP name.'
|
||||||
|
)
|
||||||
|
ctx.exitCode = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!a.startsWith('-')) break
|
||||||
|
ctx.console.error('chgrp: unsupported option: ' + a)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const groupSpec = argv[i++]
|
||||||
|
if (!groupSpec || i >= argv.length) {
|
||||||
|
ctx.console.error('usage: chgrp GROUP FILE...')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (; i < argv.length; i++) files.push(argv[i])
|
||||||
|
const gid = parseGroupSpec(groupSpec, ctx.vfs.env)
|
||||||
|
if (gid == null) {
|
||||||
|
ctx.console.error('chgrp: invalid group: ' + groupSpec)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const gname = /^\d+$/.test(groupSpec.trim())
|
||||||
|
? undefined
|
||||||
|
: groupSpec.trim() || undefined
|
||||||
|
for (const f of files) {
|
||||||
|
try {
|
||||||
|
const st = await ctx.vfs.stat(f)
|
||||||
|
if (!st) {
|
||||||
|
ctx.console.error('chgrp: ' + f + ': No such file')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (typeof ctx.vfs.chown !== 'function') {
|
||||||
|
ctx.console.error('chgrp: not supported by this VFS')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await ctx.vfs.chown(f, {
|
||||||
|
uid: st.uid,
|
||||||
|
gid,
|
||||||
|
uname: st.user,
|
||||||
|
gname
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
ctx.console.error('chgrp: ' + f + ': ' + (e.message || e))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+152
-5
@@ -60,9 +60,156 @@ function barePosixBlocks(size) {
|
|||||||
return Math.ceil(Number(size) / 512) || 0
|
return Math.ceil(Number(size) / 512) || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
async function run(ctx, argv) {
|
/**
|
||||||
ctx.console.error(
|
* chown — change file owner and group (Hyperdrive metadata on writable paths).
|
||||||
'chown: changing file ownership is not supported on Bare OS (single-user Hyperdrive metadata).'
|
*/
|
||||||
)
|
|
||||||
ctx.exitCode = 1
|
/**
|
||||||
|
* @param {string} spec
|
||||||
|
* @param {Record<string, string>} env
|
||||||
|
* @returns {{ uid: number | null, gid: number | null }}
|
||||||
|
*/
|
||||||
|
function parseIdSpec(spec, env) {
|
||||||
|
const s = String(spec).trim()
|
||||||
|
if (!s) return { uid: null, gid: null }
|
||||||
|
if (/^\d+$/.test(s)) {
|
||||||
|
const n = Number.parseInt(s, 10)
|
||||||
|
return { uid: Number.isFinite(n) ? n : null, gid: null }
|
||||||
|
}
|
||||||
|
const u = (env.USER || env.LOGNAME || '').trim()
|
||||||
|
if (s === 'root') return { uid: 0, gid: null }
|
||||||
|
if (s === 'guest' || s === 'nobody') return { uid: 65534, gid: null }
|
||||||
|
if (u && s === u) {
|
||||||
|
const uid = Number.parseInt(String(env.UID ?? '65534'), 10)
|
||||||
|
return { uid: Number.isFinite(uid) ? uid : 65534, gid: null }
|
||||||
|
}
|
||||||
|
return { uid: null, gid: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} spec
|
||||||
|
* @param {Record<string, string>} env
|
||||||
|
* @returns {number | null}
|
||||||
|
*/
|
||||||
|
function parseGroupSpec(spec, env) {
|
||||||
|
const s = String(spec).trim()
|
||||||
|
if (!s) return null
|
||||||
|
if (/^\d+$/.test(s)) {
|
||||||
|
const n = Number.parseInt(s, 10)
|
||||||
|
return Number.isFinite(n) ? n : null
|
||||||
|
}
|
||||||
|
const g = (env.GROUP || env.USER || 'guest').trim()
|
||||||
|
if (s === 'root') return 0
|
||||||
|
if (s === 'guest' || s === 'nobody') return 65534
|
||||||
|
if (g && s === g) {
|
||||||
|
const gid = Number.parseInt(String(env.GID ?? '65534'), 10)
|
||||||
|
return Number.isFinite(gid) ? gid : 65534
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(ctx, argv) {
|
||||||
|
let i = 1
|
||||||
|
const files = []
|
||||||
|
for (; i < argv.length; i++) {
|
||||||
|
const a = argv[i]
|
||||||
|
if (a === '-h' || a === '--help') {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: chown [-h] OWNER[:GROUP] FILE...\n' +
|
||||||
|
'OWNER/GROUP may be numeric ids or root/guest/nobody/current USER/GROUP.'
|
||||||
|
)
|
||||||
|
ctx.exitCode = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!a.startsWith('-')) break
|
||||||
|
ctx.console.error('chown: unsupported option: ' + a)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const ownSpec = argv[i++]
|
||||||
|
if (!ownSpec || i >= argv.length) {
|
||||||
|
ctx.console.error('usage: chown OWNER[:GROUP] FILE...')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (; i < argv.length; i++) files.push(argv[i])
|
||||||
|
const env = ctx.vfs.env
|
||||||
|
const colon = ownSpec.indexOf(':')
|
||||||
|
let uid
|
||||||
|
let gid
|
||||||
|
let uname
|
||||||
|
let gname
|
||||||
|
if (colon === 0 && ownSpec.length > 1) {
|
||||||
|
const gPart = ownSpec.slice(1)
|
||||||
|
gid = parseGroupSpec(gPart, env)
|
||||||
|
gname = /^\d+$/.test(gPart.trim()) ? undefined : gPart.trim() || undefined
|
||||||
|
if (gid == null) {
|
||||||
|
ctx.console.error('chown: invalid group: ' + gPart)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const f of files) {
|
||||||
|
try {
|
||||||
|
if (typeof ctx.vfs.chown !== 'function') {
|
||||||
|
ctx.console.error('chown: not supported by this VFS')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const st = await ctx.vfs.stat(f)
|
||||||
|
if (!st) {
|
||||||
|
ctx.console.error('chown: ' + f + ': No such file')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await ctx.vfs.chown(f, {
|
||||||
|
uid: st.uid,
|
||||||
|
gid,
|
||||||
|
uname: st.user,
|
||||||
|
gname
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
ctx.console.error('chown: ' + f + ': ' + (e.message || e))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (colon >= 0) {
|
||||||
|
const uPart = ownSpec.slice(0, colon)
|
||||||
|
const gPart = ownSpec.slice(colon + 1)
|
||||||
|
const pu = parseIdSpec(uPart, env)
|
||||||
|
uid = pu.uid
|
||||||
|
uname = /^\d+$/.test(uPart.trim()) ? undefined : uPart.trim() || undefined
|
||||||
|
if (gPart === '') {
|
||||||
|
gid = Number.parseInt(String(env.GID ?? '65534'), 10)
|
||||||
|
gname = env.GROUP || env.USER
|
||||||
|
} else {
|
||||||
|
gid = parseGroupSpec(gPart, env)
|
||||||
|
gname = /^\d+$/.test(gPart.trim()) ? undefined : gPart.trim() || undefined
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const pu = parseIdSpec(ownSpec, env)
|
||||||
|
uid = pu.uid
|
||||||
|
uname = /^\d+$/.test(ownSpec.trim()) ? undefined : ownSpec.trim() || undefined
|
||||||
|
gid = Number.parseInt(String(env.GID ?? '65534'), 10)
|
||||||
|
gname = env.GROUP || env.USER
|
||||||
|
}
|
||||||
|
if (uid == null || gid == null) {
|
||||||
|
ctx.console.error('chown: invalid owner or group: ' + ownSpec)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const f of files) {
|
||||||
|
try {
|
||||||
|
if (typeof ctx.vfs.chown !== 'function') {
|
||||||
|
ctx.console.error('chown: not supported by this VFS')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await ctx.vfs.chown(f, { uid, gid, uname, gname })
|
||||||
|
} catch (e) {
|
||||||
|
ctx.console.error('chown: ' + f + ': ' + (e.message || e))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-4
@@ -64,7 +64,7 @@ function barePosixBlocks(size) {
|
|||||||
* curDepth is distance from the search root directory (0 at the initial path).
|
* curDepth is distance from the search root directory (0 at the initial path).
|
||||||
* Printed paths use gnu-like depth curDepth + 1 (immediate children of the root are depth 1).
|
* Printed paths use gnu-like depth curDepth + 1 (immediate children of the root are depth 1).
|
||||||
*/
|
*/
|
||||||
async function walk(ctx, dir, nameRe, wantType, maxDepth, minDepth, curDepth) {
|
async function walk(ctx, dir, nameRe, pathRe, wantType, maxDepth, minDepth, curDepth) {
|
||||||
if (maxDepth >= 0 && curDepth > maxDepth) return
|
if (maxDepth >= 0 && curDepth > maxDepth) return
|
||||||
let names
|
let names
|
||||||
try {
|
try {
|
||||||
@@ -83,13 +83,24 @@ async function walk(ctx, dir, nameRe, wantType, maxDepth, minDepth, curDepth) {
|
|||||||
}
|
}
|
||||||
if (!st) continue
|
if (!st) continue
|
||||||
const gnuDepth = curDepth + 1
|
const gnuDepth = curDepth + 1
|
||||||
if (!nameRe || nameRe.test(n)) {
|
const pathOk = !pathRe || pathRe.test(path)
|
||||||
|
const nameOk = !nameRe || nameRe.test(n)
|
||||||
|
if (pathOk && nameOk) {
|
||||||
if (!wantType || st.type === wantType) {
|
if (!wantType || st.type === wantType) {
|
||||||
if (gnuDepth >= minDepth) ctx.console.log(path)
|
if (gnuDepth >= minDepth) ctx.console.log(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (st.type === 'directory')
|
if (st.type === 'directory')
|
||||||
await walk(ctx, path, nameRe, wantType, maxDepth, minDepth, curDepth + 1)
|
await walk(
|
||||||
|
ctx,
|
||||||
|
path,
|
||||||
|
nameRe,
|
||||||
|
pathRe,
|
||||||
|
wantType,
|
||||||
|
maxDepth,
|
||||||
|
minDepth,
|
||||||
|
curDepth + 1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +110,8 @@ async function run(ctx, argv) {
|
|||||||
let minDepth = 1
|
let minDepth = 1
|
||||||
/** @type {string | null} */
|
/** @type {string | null} */
|
||||||
let nameGlob = null
|
let nameGlob = null
|
||||||
|
/** @type {string | null} */
|
||||||
|
let pathGlob = null
|
||||||
/** @type {'file' | 'directory' | 'symlink' | null} */
|
/** @type {'file' | 'directory' | 'symlink' | null} */
|
||||||
let wantType = null
|
let wantType = null
|
||||||
const rest = []
|
const rest = []
|
||||||
@@ -113,6 +126,10 @@ async function run(ctx, argv) {
|
|||||||
if (!Number.isFinite(minDepth) || minDepth < 1) minDepth = 1
|
if (!Number.isFinite(minDepth) || minDepth < 1) minDepth = 1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (a === '-path' && argv[i + 1]) {
|
||||||
|
pathGlob = argv[++i]
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (a === '-name' && argv[i + 1]) {
|
if (a === '-name' && argv[i + 1]) {
|
||||||
nameGlob = argv[++i]
|
nameGlob = argv[++i]
|
||||||
continue
|
continue
|
||||||
@@ -144,6 +161,16 @@ async function run(ctx, argv) {
|
|||||||
.replace(/\?/g, '.')
|
.replace(/\?/g, '.')
|
||||||
nameRe = new RegExp('^' + esc + '$')
|
nameRe = new RegExp('^' + esc + '$')
|
||||||
}
|
}
|
||||||
|
let pathRe = null
|
||||||
|
if (pathGlob) {
|
||||||
|
const esc = pathGlob
|
||||||
|
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
||||||
|
.replace(/\*\*/g, '\0GLOBSTAR\0')
|
||||||
|
.replace(/\*/g, '[^/]*')
|
||||||
|
.replace(/\?/g, '[^/]')
|
||||||
|
.replace(/\0GLOBSTAR\0/g, '.*')
|
||||||
|
pathRe = new RegExp('^' + esc + '$')
|
||||||
|
}
|
||||||
const abs = ctx.vfs.resolveLogical(root)
|
const abs = ctx.vfs.resolveLogical(root)
|
||||||
await walk(ctx, abs, nameRe, wantType, maxDepth, minDepth, 0)
|
await walk(ctx, abs, nameRe, pathRe, wantType, maxDepth, minDepth, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
+177
-25
@@ -15,6 +15,13 @@
|
|||||||
* in file order (trusted). Then readLine yields EOF.
|
* in file order (trusted). Then readLine yields EOF.
|
||||||
*
|
*
|
||||||
* BARE_OS_BOOT_TRACE=json logs one JSON object per phase on stderr: {"phase":"…","ms":n}.
|
* BARE_OS_BOOT_TRACE=json logs one JSON object per phase on stderr: {"phase":"…","ms":n}.
|
||||||
|
* BARE_OS_BOOT_TRACE=ndjson logs {"type":"boot","phase","ms","sessionId","ts"} per line.
|
||||||
|
*
|
||||||
|
* BARE_OS_BOOT_MINIMAL=1 or true: skip profile rc, rc, rc.d, rc.local, kernel.d (recovery shell).
|
||||||
|
* BARE_OS_BOOT_SKIP: comma-separated phases to skip among:
|
||||||
|
* profile, rc, rc.d, rc.local, kernel.d, onboot (os-release and motd always run).
|
||||||
|
*
|
||||||
|
* BARE_OS_KERNEL_SELFTEST=1 or true: after boot snippets, run a short trusted self-check via execLine.
|
||||||
*
|
*
|
||||||
* BARE_OS_BOOT_STRICT=1 or true: first execLine throw in trusted boot snippets calls
|
* BARE_OS_BOOT_STRICT=1 or true: first execLine throw in trusted boot snippets calls
|
||||||
* requestBooterExit(1) and stops further boot phases.
|
* requestBooterExit(1) and stops further boot phases.
|
||||||
@@ -30,7 +37,12 @@
|
|||||||
*/
|
*/
|
||||||
function wantBootTrace(ctx) {
|
function wantBootTrace(ctx) {
|
||||||
const v = ctx.env && ctx.env.BARE_OS_BOOT_TRACE
|
const v = ctx.env && ctx.env.BARE_OS_BOOT_TRACE
|
||||||
return v === '1' || v === 'true' || v === 'json'
|
return (
|
||||||
|
v === '1' ||
|
||||||
|
v === 'true' ||
|
||||||
|
v === 'json' ||
|
||||||
|
v === 'ndjson'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +52,56 @@ function isBootTraceJson(ctx) {
|
|||||||
return ctx.env && ctx.env.BARE_OS_BOOT_TRACE === 'json'
|
return ctx.env && ctx.env.BARE_OS_BOOT_TRACE === 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
*/
|
||||||
|
function isBootTraceNdjson(ctx) {
|
||||||
|
return ctx.env && ctx.env.BARE_OS_BOOT_TRACE === 'ndjson'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
*/
|
||||||
|
function bootMinimal(ctx) {
|
||||||
|
const v = ctx.env && ctx.env.BARE_OS_BOOT_MINIMAL
|
||||||
|
return v === '1' || v === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
* @returns {Set<string>}
|
||||||
|
*/
|
||||||
|
function parseBootSkip(ctx) {
|
||||||
|
const raw = ctx.env && ctx.env.BARE_OS_BOOT_SKIP
|
||||||
|
const out = new Set()
|
||||||
|
if (raw == null || !String(raw).trim()) return out
|
||||||
|
for (const p of String(raw)
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim().toLowerCase())
|
||||||
|
.filter(Boolean)) {
|
||||||
|
out.add(p)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
* @param {string} phase
|
||||||
|
*/
|
||||||
|
function shouldSkipBootPhase(ctx, phase) {
|
||||||
|
if (bootMinimal(ctx)) {
|
||||||
|
return (
|
||||||
|
phase === 'profile' ||
|
||||||
|
phase === 'rc' ||
|
||||||
|
phase === 'rc.d' ||
|
||||||
|
phase === 'rc.local' ||
|
||||||
|
phase === 'kernel.d' ||
|
||||||
|
phase === 'onboot'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return parseBootSkip(ctx).has(phase.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Record<string, unknown>} ctx
|
* @param {Record<string, unknown>} ctx
|
||||||
*/
|
*/
|
||||||
@@ -80,13 +142,26 @@ function shouldSkipRcDName(name, patterns) {
|
|||||||
* @param {Record<string, unknown>} ctx
|
* @param {Record<string, unknown>} ctx
|
||||||
* @param {string} label
|
* @param {string} label
|
||||||
* @param {() => void | Promise<void>} fn
|
* @param {() => void | Promise<void>} fn
|
||||||
|
* @param {string[]} phaseLog
|
||||||
*/
|
*/
|
||||||
async function bootTimed(ctx, label, fn) {
|
async function bootTimed(ctx, label, fn, phaseLog) {
|
||||||
const t0 = Date.now()
|
const t0 = Date.now()
|
||||||
await fn()
|
await fn()
|
||||||
|
const ms = Date.now() - t0
|
||||||
|
phaseLog.push(label)
|
||||||
if (wantBootTrace(ctx)) {
|
if (wantBootTrace(ctx)) {
|
||||||
const ms = Date.now() - t0
|
if (isBootTraceNdjson(ctx)) {
|
||||||
if (isBootTraceJson(ctx)) {
|
const sid =
|
||||||
|
(ctx.env && ctx.env.BARE_OS_SESSION_ID) || ''
|
||||||
|
const rec = {
|
||||||
|
type: 'boot',
|
||||||
|
phase: label,
|
||||||
|
ms,
|
||||||
|
sessionId: sid,
|
||||||
|
ts: Date.now()
|
||||||
|
}
|
||||||
|
ctx.console.error(JSON.stringify(rec))
|
||||||
|
} else if (isBootTraceJson(ctx)) {
|
||||||
ctx.console.error(JSON.stringify({ phase: label, ms }))
|
ctx.console.error(JSON.stringify({ phase: label, ms }))
|
||||||
} else {
|
} else {
|
||||||
ctx.console.error(`[boot] ${label}: ${ms}ms`)
|
ctx.console.error(`[boot] ${label}: ${ms}ms`)
|
||||||
@@ -348,7 +423,7 @@ async function printSessionBanner(ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const defaultBanner =
|
const defaultBanner =
|
||||||
'Bare operating system — guest session (login [--new] <passphrase> to unlock) | shell: cd, export, if/fi, && || ;, |, exit | services: systemctl list-units, journalctl -u UNIT | try: help, ls /bin, crontab -l'
|
'Bare operating system — guest session (login [--new] <passphrase> to unlock) | shell: cd, export, if/fi, && || ;, |, &, jobs/fg, <<<, exit | services: systemctl list-units, journalctl -u UNIT | try: help, ls /bin, crontab -l'
|
||||||
if (ctx.bareOsSkipRepl) {
|
if (ctx.bareOsSkipRepl) {
|
||||||
console.log(
|
console.log(
|
||||||
'Bare operating system — non-interactive session (BARE_OS_SKIP_REPL).'
|
'Bare operating system — non-interactive session (BARE_OS_SKIP_REPL).'
|
||||||
@@ -358,38 +433,115 @@ async function printSessionBanner(ctx) {
|
|||||||
console.log(defaultBanner)
|
console.log(defaultBanner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async function runKernelSelftest(ctx) {
|
||||||
|
const v = ctx.env && ctx.env.BARE_OS_KERNEL_SELFTEST
|
||||||
|
if (v !== '1' && v !== 'true') return true
|
||||||
|
const { execLine, console } = ctx
|
||||||
|
const strict = bootStrict(ctx)
|
||||||
|
const lines = [
|
||||||
|
':',
|
||||||
|
'true',
|
||||||
|
'false; echo selftest_false_ok',
|
||||||
|
'systemctl list-units 2>/dev/null || true'
|
||||||
|
]
|
||||||
|
for (const line of lines) {
|
||||||
|
try {
|
||||||
|
await execLine(line)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('selftest: ' + ((e && e.message) || String(e)))
|
||||||
|
if (strict) {
|
||||||
|
if (typeof ctx.requestBooterExit === 'function') ctx.requestBooterExit(1)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
* @param {string[]} phaseLog
|
||||||
|
*/
|
||||||
|
function publishBootReady(ctx, phaseLog) {
|
||||||
|
if (typeof ctx.bareOsPublishBootReady !== 'function') return
|
||||||
|
const sid = (ctx.env && ctx.env.BARE_OS_SESSION_ID) || ''
|
||||||
|
ctx.bareOsPublishBootReady({
|
||||||
|
ready: true,
|
||||||
|
phases: [...phaseLog],
|
||||||
|
sessionId: sid,
|
||||||
|
completedAtMs: Date.now(),
|
||||||
|
minimal: bootMinimal(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function start(ctx) {
|
async function start(ctx) {
|
||||||
const { readLine, execLine, console } = ctx
|
const { readLine, execLine, console } = ctx
|
||||||
await bootTimed(ctx, 'os-release', () => printOsRelease(ctx))
|
/** @type {string[]} */
|
||||||
await bootTimed(ctx, 'motd', () => printMotd(ctx))
|
const phaseLog = []
|
||||||
|
await bootTimed(ctx, 'os-release', () => printOsRelease(ctx), phaseLog)
|
||||||
|
await bootTimed(ctx, 'motd', () => printMotd(ctx), phaseLog)
|
||||||
const profileName = await resolveBootProfileName(ctx)
|
const profileName = await resolveBootProfileName(ctx)
|
||||||
/** @type {boolean} */
|
/** @type {boolean} */
|
||||||
let bootOk = true
|
let bootOk = true
|
||||||
await bootTimed(ctx, 'rc.profile', async () => {
|
if (!shouldSkipBootPhase(ctx, 'profile')) {
|
||||||
bootOk = await runProfileRc(ctx, profileName)
|
await bootTimed(ctx, 'rc.profile', async () => {
|
||||||
})
|
bootOk = await runProfileRc(ctx, profileName)
|
||||||
|
}, phaseLog)
|
||||||
|
} else {
|
||||||
|
phaseLog.push('rc.profile(skipped)')
|
||||||
|
}
|
||||||
if (!bootOk) return
|
if (!bootOk) return
|
||||||
await bootTimed(ctx, 'rc', async () => {
|
if (!shouldSkipBootPhase(ctx, 'rc')) {
|
||||||
bootOk = await runRcFileAt(ctx, '/etc/bare-os/rc', 'rc')
|
await bootTimed(ctx, 'rc', async () => {
|
||||||
})
|
bootOk = await runRcFileAt(ctx, '/etc/bare-os/rc', 'rc')
|
||||||
|
}, phaseLog)
|
||||||
|
} else {
|
||||||
|
phaseLog.push('rc(skipped)')
|
||||||
|
}
|
||||||
if (!bootOk) return
|
if (!bootOk) return
|
||||||
await bootTimed(ctx, 'rc.d', async () => {
|
if (!shouldSkipBootPhase(ctx, 'rc.d')) {
|
||||||
bootOk = await runBareOsRcDir(ctx)
|
await bootTimed(ctx, 'rc.d', async () => {
|
||||||
})
|
bootOk = await runBareOsRcDir(ctx)
|
||||||
|
}, phaseLog)
|
||||||
|
} else {
|
||||||
|
phaseLog.push('rc.d(skipped)')
|
||||||
|
}
|
||||||
if (!bootOk) return
|
if (!bootOk) return
|
||||||
await bootTimed(ctx, 'rc.local', async () => {
|
if (!shouldSkipBootPhase(ctx, 'rc.local')) {
|
||||||
bootOk = await runRcFileAt(ctx, '/etc/bare-os/rc.local', 'rc.local')
|
await bootTimed(ctx, 'rc.local', async () => {
|
||||||
})
|
bootOk = await runRcFileAt(ctx, '/etc/bare-os/rc.local', 'rc.local')
|
||||||
|
}, phaseLog)
|
||||||
|
} else {
|
||||||
|
phaseLog.push('rc.local(skipped)')
|
||||||
|
}
|
||||||
if (!bootOk) return
|
if (!bootOk) return
|
||||||
await bootTimed(ctx, 'kernel.d', async () => {
|
if (!shouldSkipBootPhase(ctx, 'kernel.d')) {
|
||||||
bootOk = await runBareOsKernelDir(ctx)
|
await bootTimed(ctx, 'kernel.d', async () => {
|
||||||
})
|
bootOk = await runBareOsKernelDir(ctx)
|
||||||
|
}, phaseLog)
|
||||||
|
} else {
|
||||||
|
phaseLog.push('kernel.d(skipped)')
|
||||||
|
}
|
||||||
if (!bootOk) return
|
if (!bootOk) return
|
||||||
await printSessionBanner(ctx)
|
await printSessionBanner(ctx)
|
||||||
await bootTimed(ctx, 'onboot', async () => {
|
phaseLog.push('banner')
|
||||||
bootOk = await runOnboot(ctx)
|
if (!shouldSkipBootPhase(ctx, 'onboot')) {
|
||||||
})
|
await bootTimed(ctx, 'onboot', async () => {
|
||||||
|
bootOk = await runOnboot(ctx)
|
||||||
|
}, phaseLog)
|
||||||
|
} else {
|
||||||
|
phaseLog.push('onboot(skipped)')
|
||||||
|
}
|
||||||
if (!bootOk) return
|
if (!bootOk) return
|
||||||
|
await bootTimed(ctx, 'selftest', async () => {
|
||||||
|
bootOk = await runKernelSelftest(ctx)
|
||||||
|
}, phaseLog)
|
||||||
|
if (!bootOk) return
|
||||||
|
publishBootReady(ctx, phaseLog)
|
||||||
while (true) {
|
while (true) {
|
||||||
const line = await readLine('')
|
const line = await readLine('')
|
||||||
if (line == null) break
|
if (line == null) break
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -44,7 +44,8 @@ HTTP in Pear/Bare uses **WHATWG Fetch** (Node `fetch` or **`bare-fetch`** on `ba
|
|||||||
| `-c` / `--continue` | supported | `Range: bytes=<existing>-` when output file exists; 206 appends, 200 replaces full file |
|
| `-c` / `--continue` | supported | `Range: bytes=<existing>-` when output file exists; 206 appends, 200 replaces full file |
|
||||||
| `-V` / `-h` | supported | |
|
| `-V` / `-h` | supported | |
|
||||||
| Recursive / spider / FTP / WARC | out of scope | |
|
| Recursive / spider / FTP / WARC | out of scope | |
|
||||||
| `--no-clobber`, `-N` timestamping | out of scope | |
|
| `--no-clobber` / `-nc` | supported | Skip download when the target file already exists and has size > 0 (not combined with `--continue`) |
|
||||||
|
| `-N` timestamping | out of scope | |
|
||||||
|
|
||||||
## Fetch / bare-fetch vs curl (`-I` + `-L`)
|
## Fetch / bare-fetch vs curl (`-I` + `-L`)
|
||||||
|
|
||||||
@@ -54,6 +55,8 @@ We **do not** depend on bare-fetch for that case: [`fetchHeadWithLocationFollow`
|
|||||||
|
|
||||||
## Upstream (bare-fetch)
|
## Upstream (bare-fetch)
|
||||||
|
|
||||||
|
**Tracking:** Bare OS implements **`curl -I -L`** with manual redirects so the first hop can stay **HEAD** and a **3xx** response can be followed with **GET** (see **`fetchHeadWithLocationFollow`**). Consider an upstream **`bare-fetch`** option for “curl-like” redirect method switching so other callers get the same behavior without a custom loop.
|
||||||
|
|
||||||
**Suggested issue title:** Redirect following preserves original method (HEAD on every hop); optional curl-like mode?
|
**Suggested issue title:** Redirect following preserves original method (HEAD on every hop); optional curl-like mode?
|
||||||
|
|
||||||
**Body (paste into GitHub):**
|
**Body (paste into GitHub):**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { randomUUID } from 'node:crypto'
|
import { randomUUID, randomBytes } from 'node:crypto'
|
||||||
import Hyperswarm from 'hyperswarm'
|
import Hyperswarm from 'hyperswarm'
|
||||||
import Protomux from 'protomux'
|
import Protomux from 'protomux'
|
||||||
import b4a from 'b4a'
|
import b4a from 'b4a'
|
||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
listBareServices,
|
listBareServices,
|
||||||
getBareServiceRuntime
|
getBareServiceRuntime
|
||||||
} from './lib/bare-initd.js'
|
} from './lib/bare-initd.js'
|
||||||
|
import { appendVarLog, AUDIT_LOG } from './lib/bare-os-var-log.js'
|
||||||
import { BARE_OS_CTX_API_VERSION } from './lib/bare-os-ctx-api.js'
|
import { BARE_OS_CTX_API_VERSION } from './lib/bare-os-ctx-api.js'
|
||||||
import { buildBareOsRuntimeCaps } from './lib/bare-os-runtime-caps.js'
|
import { buildBareOsRuntimeCaps } from './lib/bare-os-runtime-caps.js'
|
||||||
import './lib/bare-cron.js'
|
import './lib/bare-cron.js'
|
||||||
@@ -239,7 +240,13 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
'BARE_OS_BOOT_PROFILE',
|
'BARE_OS_BOOT_PROFILE',
|
||||||
'BARE_OS_ONBOOT',
|
'BARE_OS_ONBOOT',
|
||||||
'BARE_OS_BOOT_STRICT',
|
'BARE_OS_BOOT_STRICT',
|
||||||
'BARE_OS_RC_D_SKIP'
|
'BARE_OS_RC_D_SKIP',
|
||||||
|
'BARE_OS_BOOT_MINIMAL',
|
||||||
|
'BARE_OS_BOOT_SKIP',
|
||||||
|
'BARE_OS_BOOT_TRACE',
|
||||||
|
'BARE_OS_KERNEL_SELFTEST',
|
||||||
|
'BARE_OS_AUDIT',
|
||||||
|
'BARE_OS_IMAGE_DIGEST'
|
||||||
]) {
|
]) {
|
||||||
const v = hostEnv[k]
|
const v = hostEnv[k]
|
||||||
if (v != null && v !== '') shellEnv[k] = v
|
if (v != null && v !== '') shellEnv[k] = v
|
||||||
@@ -263,6 +270,16 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
shellEnv.BARE_OS_BOOT_PROFILE_RESOLVED = bootProfileResolved
|
shellEnv.BARE_OS_BOOT_PROFILE_RESOLVED = bootProfileResolved
|
||||||
const bareOsSessionId = randomUUID()
|
const bareOsSessionId = randomUUID()
|
||||||
shellEnv.BARE_OS_SESSION_ID = bareOsSessionId
|
shellEnv.BARE_OS_SESSION_ID = bareOsSessionId
|
||||||
|
/** @type {{ ready: boolean, phases: string[], sessionId: string, completedAtMs: number | null, minimal: boolean }} */
|
||||||
|
const bootReadyStateRef = {
|
||||||
|
ready: false,
|
||||||
|
phases: [],
|
||||||
|
sessionId: bareOsSessionId,
|
||||||
|
completedAtMs: null,
|
||||||
|
minimal: false
|
||||||
|
}
|
||||||
|
/** @type {{ execLineCount: number, pipelineBytesTotal: number }} */
|
||||||
|
const sessionStatsRef = { execLineCount: 0, pipelineBytesTotal: 0 }
|
||||||
/** @type {{ getMounts: () => Map<string, { drive: import('hyperdrive').default, writable: boolean }> }} */
|
/** @type {{ getMounts: () => Map<string, { drive: import('hyperdrive').default, writable: boolean }> }} */
|
||||||
const vfsMountRef = { getMounts: () => new Map() }
|
const vfsMountRef = { getMounts: () => new Map() }
|
||||||
const bootStartedMs = Date.now()
|
const bootStartedMs = Date.now()
|
||||||
@@ -291,6 +308,22 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
lines.push(`${s.name}\t${phase}\t${started}\t${desc}`)
|
lines.push(`${s.name}\t${phase}\t${started}\t${desc}`)
|
||||||
}
|
}
|
||||||
return lines.join('\n') + '\n'
|
return lines.join('\n') + '\n'
|
||||||
|
},
|
||||||
|
sessionStatsText() {
|
||||||
|
return `${JSON.stringify(sessionStatsRef)}\n`
|
||||||
|
},
|
||||||
|
bootReadyMarkerText() {
|
||||||
|
return bootReadyStateRef.ready ? '1\n' : '0\n'
|
||||||
|
},
|
||||||
|
bootReadyJsonText() {
|
||||||
|
return `${JSON.stringify(bootReadyStateRef)}\n`
|
||||||
|
},
|
||||||
|
buildIdText() {
|
||||||
|
const d = shellEnv.BARE_OS_IMAGE_DIGEST
|
||||||
|
return (d != null && String(d).trim() ? String(d).trim() : 'unknown') + '\n'
|
||||||
|
},
|
||||||
|
secureRandomBytes(n) {
|
||||||
|
return new Uint8Array(randomBytes(Math.min(65536, Math.max(1, n | 0))))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -402,7 +435,16 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
*/
|
*/
|
||||||
registerKernelShutdownHook(fn) {
|
registerKernelShutdownHook(fn) {
|
||||||
registerKernelShutdownHook(fn)
|
registerKernelShutdownHook(fn)
|
||||||
}
|
},
|
||||||
|
/**
|
||||||
|
* Kernel publishes boot completion for pseudo `/run/bare-os/boot.json` and `ready`.
|
||||||
|
* @param {Partial<{ ready: boolean, phases: string[], sessionId: string, completedAtMs: number, minimal: boolean }>} patch
|
||||||
|
*/
|
||||||
|
bareOsPublishBootReady(patch) {
|
||||||
|
if (patch && typeof patch === 'object') Object.assign(bootReadyStateRef, patch)
|
||||||
|
},
|
||||||
|
/** Mutable ref mirrored in `/proc/bare_os_session_stats` (execLineCount, pipelineBytesTotal). */
|
||||||
|
bareOsSessionStats: sessionStatsRef
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.replStdin = sessionStdin
|
ctx.replStdin = sessionStdin
|
||||||
@@ -430,6 +472,12 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
const sessionReadLine = session.readLine
|
const sessionReadLine = session.readLine
|
||||||
ctx.execLine = async (line) => {
|
ctx.execLine = async (line) => {
|
||||||
const t = line.trim()
|
const t = line.trim()
|
||||||
|
sessionStatsRef.execLineCount++
|
||||||
|
const auditOn =
|
||||||
|
shellEnv.BARE_OS_AUDIT === '1' || shellEnv.BARE_OS_AUDIT === 'true'
|
||||||
|
if (auditOn && t) {
|
||||||
|
void appendVarLog(ctx, AUDIT_LOG, 'exec', t.slice(0, 500))
|
||||||
|
}
|
||||||
const exitMatch = t.match(/^exit(?:\s+(-?\d+))?$/)
|
const exitMatch = t.match(/^exit(?:\s+(-?\d+))?$/)
|
||||||
if (exitMatch) {
|
if (exitMatch) {
|
||||||
const ec =
|
const ec =
|
||||||
|
|||||||
@@ -5,10 +5,13 @@
|
|||||||
|
|
||||||
import b4a from 'b4a'
|
import b4a from 'b4a'
|
||||||
|
|
||||||
/** One unit name per line; blank lines and `#…` comments ignored. */
|
/** One unit per line; blank lines and `#…` comments ignored. */
|
||||||
export const BARE_INITD_DISABLED_FILE = '~/.config/bare-os/initd/disabled.txt'
|
export const BARE_INITD_DISABLED_FILE = '~/.config/bare-os/initd/disabled.txt'
|
||||||
|
|
||||||
/** Optional `unit.unit` files: `[Unit]` with `After=other-unit …`. */
|
/**
|
||||||
|
* Optional `name.unit` files under this directory. Supports `[Unit]` keys:
|
||||||
|
* After=, Requires=, Wants=, TimeoutStartSec=, TimeoutStopSec=, Restart=, RestartSec=, ExecStartPost=.
|
||||||
|
*/
|
||||||
export const BARE_INITD_UNITS_DIR = '~/.config/bare-os/units'
|
export const BARE_INITD_UNITS_DIR = '~/.config/bare-os/units'
|
||||||
|
|
||||||
/** Built-in ordering when no drop-in file exists (kernel-logger before cron). */
|
/** Built-in ordering when no drop-in file exists (kernel-logger before cron). */
|
||||||
@@ -16,6 +19,88 @@ export const BARE_INITD_DEFAULT_AFTER = Object.freeze({
|
|||||||
'bare-cron': ['kernel-logger']
|
'bare-cron': ['kernel-logger']
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{
|
||||||
|
* after: string[],
|
||||||
|
* requires: string[],
|
||||||
|
* wants: string[],
|
||||||
|
* timeoutStartSec: number | null,
|
||||||
|
* timeoutStopSec: number | null,
|
||||||
|
* restart: string | null,
|
||||||
|
* restartSec: number | null,
|
||||||
|
* execStartPost: string | null
|
||||||
|
* }} BareInitdUnitDropIn
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @returns {BareInitdUnitDropIn} */
|
||||||
|
export function emptyUnitDropIn() {
|
||||||
|
return {
|
||||||
|
after: [],
|
||||||
|
requires: [],
|
||||||
|
wants: [],
|
||||||
|
timeoutStartSec: null,
|
||||||
|
timeoutStopSec: null,
|
||||||
|
restart: null,
|
||||||
|
restartSec: null,
|
||||||
|
execStartPost: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} val
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
function parseList(val) {
|
||||||
|
return val
|
||||||
|
.split(/[\s,]+/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a systemd-inspired `.unit` snippet (only `[Unit]` keys are honored).
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {BareInitdUnitDropIn}
|
||||||
|
*/
|
||||||
|
export function parseUnitDropInText(text) {
|
||||||
|
const out = emptyUnitDropIn()
|
||||||
|
let section = ''
|
||||||
|
for (const raw of text.split(/\r?\n/)) {
|
||||||
|
const line = raw.trim()
|
||||||
|
if (!line || line.startsWith('#')) continue
|
||||||
|
if (/^\[[^\]]+\]$/.test(line)) {
|
||||||
|
section = line.slice(1, -1).toLowerCase()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (section !== 'unit') continue
|
||||||
|
const eq = line.indexOf('=')
|
||||||
|
if (eq < 1) continue
|
||||||
|
const key = line.slice(0, eq).trim().toLowerCase()
|
||||||
|
const val = line.slice(eq + 1).trim()
|
||||||
|
if (key === 'after') out.after.push(...parseList(val))
|
||||||
|
else if (key === 'requires') out.requires.push(...parseList(val))
|
||||||
|
else if (key === 'wants') out.wants.push(...parseList(val))
|
||||||
|
else if (key === 'timeoutstartsec') {
|
||||||
|
const n = Number.parseInt(val, 10)
|
||||||
|
if (Number.isFinite(n) && n > 0) out.timeoutStartSec = n
|
||||||
|
} else if (key === 'timeoutstopsec') {
|
||||||
|
const n = Number.parseInt(val, 10)
|
||||||
|
if (Number.isFinite(n) && n > 0) out.timeoutStopSec = n
|
||||||
|
} else if (key === 'restart') {
|
||||||
|
out.restart = val.toLowerCase()
|
||||||
|
} else if (key === 'restartsec') {
|
||||||
|
const n = Number.parseInt(val, 10)
|
||||||
|
if (Number.isFinite(n) && n >= 0) out.restartSec = n
|
||||||
|
} else if (key === 'execstartpost') {
|
||||||
|
out.execStartPost = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.after = [...new Set(out.after)]
|
||||||
|
out.requires = [...new Set(out.requires)]
|
||||||
|
out.wants = [...new Set(out.wants)]
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
||||||
* @returns {Promise<Set<string>>}
|
* @returns {Promise<Set<string>>}
|
||||||
@@ -60,16 +145,20 @@ export async function writeInitdDisabledSet(vfs, disabled) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} text
|
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
||||||
* @returns {string[]}
|
* @param {string} unitName
|
||||||
|
* @returns {Promise<BareInitdUnitDropIn>}
|
||||||
*/
|
*/
|
||||||
function parseAfterLine(text) {
|
export async function readUnitDropIn(vfs, unitName) {
|
||||||
const m = text.match(/^\s*After\s*=\s*(.+)$/im)
|
if (!/^[a-zA-Z0-9._-]+$/.test(unitName)) return emptyUnitDropIn()
|
||||||
if (!m) return []
|
const path = `${BARE_INITD_UNITS_DIR}/${unitName}.unit`
|
||||||
return m[1]
|
try {
|
||||||
.split(/[\s,]+/)
|
const buf = await vfs.readFile(path)
|
||||||
.map((s) => s.trim())
|
if (!buf) return emptyUnitDropIn()
|
||||||
.filter(Boolean)
|
return parseUnitDropInText(b4a.toString(buf, 'utf8'))
|
||||||
|
} catch {
|
||||||
|
return emptyUnitDropIn()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,16 +167,26 @@ function parseAfterLine(text) {
|
|||||||
* @returns {Promise<string[]>}
|
* @returns {Promise<string[]>}
|
||||||
*/
|
*/
|
||||||
export async function readUnitAfterFromDropIn(vfs, unitName) {
|
export async function readUnitAfterFromDropIn(vfs, unitName) {
|
||||||
if (!/^[a-zA-Z0-9._-]+$/.test(unitName)) return []
|
const d = await readUnitDropIn(vfs, unitName)
|
||||||
const path = `${BARE_INITD_UNITS_DIR}/${unitName}.unit`
|
return d.after
|
||||||
try {
|
}
|
||||||
const buf = await vfs.readFile(path)
|
|
||||||
if (!buf) return []
|
/**
|
||||||
const text = b4a.toString(buf, 'utf8')
|
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
||||||
return parseAfterLine(text)
|
* @param {{ name: string }[]} services
|
||||||
} catch {
|
* @param {Readonly<Record<string, string[]>>} defaultAfter
|
||||||
return []
|
* @returns {Promise<Map<string, BareInitdUnitDropIn>>}
|
||||||
|
*/
|
||||||
|
export async function loadInitdUnitDropIns(vfs, services, defaultAfter) {
|
||||||
|
/** @type {Map<string, BareInitdUnitDropIn>} */
|
||||||
|
const map = new Map()
|
||||||
|
for (const s of services) {
|
||||||
|
const fromFile = await readUnitDropIn(vfs, s.name)
|
||||||
|
const def = defaultAfter[s.name] || []
|
||||||
|
fromFile.after = [...new Set([...def, ...fromFile.after])]
|
||||||
|
map.set(s.name, fromFile)
|
||||||
}
|
}
|
||||||
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,26 +196,50 @@ export async function readUnitAfterFromDropIn(vfs, unitName) {
|
|||||||
* @returns {Promise<Map<string, string[]>>}
|
* @returns {Promise<Map<string, string[]>>}
|
||||||
*/
|
*/
|
||||||
export async function loadInitdAfterMap(vfs, services, defaultAfter) {
|
export async function loadInitdAfterMap(vfs, services, defaultAfter) {
|
||||||
|
const full = await loadInitdUnitDropIns(vfs, services, defaultAfter)
|
||||||
const map = new Map()
|
const map = new Map()
|
||||||
for (const s of services) {
|
for (const [k, v] of full) map.set(k, v.after)
|
||||||
const fromFile = await readUnitAfterFromDropIn(vfs, s.name)
|
|
||||||
const def = defaultAfter[s.name] || []
|
|
||||||
const merged = [...new Set([...def, ...fromFile])]
|
|
||||||
map.set(s.name, merged)
|
|
||||||
}
|
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T extends { name: string }
|
||||||
|
* @param {T[]} services
|
||||||
|
* @param {Set<string>} disabled
|
||||||
|
* @param {Map<string, string[]>} requiresMap
|
||||||
|
*/
|
||||||
|
function filterSatisfiedRequires(services, disabled, requiresMap) {
|
||||||
|
const names = new Set(services.map((s) => s.name))
|
||||||
|
return services.filter((s) => {
|
||||||
|
for (const r of requiresMap.get(s.name) || []) {
|
||||||
|
if (!names.has(r)) return false
|
||||||
|
if (disabled.has(r)) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Topological sort; cycles fall back to registration order for the remainder.
|
* Topological sort; cycles fall back to registration order for the remainder.
|
||||||
* @template T extends { name: string }
|
* @template T extends { name: string }
|
||||||
* @param {T[]} services
|
* @param {T[]} services
|
||||||
* @param {Set<string>} disabled
|
* @param {Set<string>} disabled
|
||||||
* @param {Map<string, string[]>} afterMap
|
* @param {Map<string, string[]>} afterMap
|
||||||
|
* @param {Map<string, string[]>} [requiresMap]
|
||||||
|
* @param {Map<string, string[]>} [wantsMap]
|
||||||
* @returns {T[]}
|
* @returns {T[]}
|
||||||
*/
|
*/
|
||||||
export function sortServicesForBoot(services, disabled, afterMap) {
|
export function sortServicesForBoot(
|
||||||
const active = services.filter((s) => !disabled.has(s.name))
|
services,
|
||||||
|
disabled,
|
||||||
|
afterMap,
|
||||||
|
requiresMap,
|
||||||
|
wantsMap
|
||||||
|
) {
|
||||||
|
const reqM = requiresMap || new Map()
|
||||||
|
const wanM = wantsMap || new Map()
|
||||||
|
let active = services.filter((s) => !disabled.has(s.name))
|
||||||
|
active = filterSatisfiedRequires(active, disabled, reqM)
|
||||||
const nameSet = new Set(active.map((s) => s.name))
|
const nameSet = new Set(active.map((s) => s.name))
|
||||||
/** @type {Map<string, Set<string>>} */
|
/** @type {Map<string, Set<string>>} */
|
||||||
const prereq = new Map()
|
const prereq = new Map()
|
||||||
@@ -125,6 +248,12 @@ export function sortServicesForBoot(services, disabled, afterMap) {
|
|||||||
for (const a of afterMap.get(s.name) || []) {
|
for (const a of afterMap.get(s.name) || []) {
|
||||||
if (nameSet.has(a)) inc.add(a)
|
if (nameSet.has(a)) inc.add(a)
|
||||||
}
|
}
|
||||||
|
for (const r of reqM.get(s.name) || []) {
|
||||||
|
if (nameSet.has(r)) inc.add(r)
|
||||||
|
}
|
||||||
|
for (const w of wanM.get(s.name) || []) {
|
||||||
|
if (nameSet.has(w)) inc.add(w)
|
||||||
|
}
|
||||||
prereq.set(s.name, inc)
|
prereq.set(s.name, inc)
|
||||||
}
|
}
|
||||||
const remaining = new Set(active.map((s) => s.name))
|
const remaining = new Set(active.map((s) => s.name))
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import {
|
|||||||
} from './bare-os-var-log.js'
|
} from './bare-os-var-log.js'
|
||||||
import {
|
import {
|
||||||
BARE_INITD_DEFAULT_AFTER,
|
BARE_INITD_DEFAULT_AFTER,
|
||||||
loadInitdAfterMap,
|
emptyUnitDropIn,
|
||||||
|
loadInitdUnitDropIns,
|
||||||
readInitdDisabledSet,
|
readInitdDisabledSet,
|
||||||
|
readUnitDropIn,
|
||||||
sortServicesForBoot
|
sortServicesForBoot
|
||||||
} from './bare-initd-user.js'
|
} from './bare-initd-user.js'
|
||||||
|
|
||||||
@@ -135,10 +137,30 @@ export async function startBareService(ctx, name) {
|
|||||||
if (rt?.phase === 'active') {
|
if (rt?.phase === 'active') {
|
||||||
return { noop: true, message: `${name} is already active` }
|
return { noop: true, message: `${name} is already active` }
|
||||||
}
|
}
|
||||||
|
const vfs = ctx.vfs
|
||||||
|
const di =
|
||||||
|
vfs && typeof vfs.readFile === 'function'
|
||||||
|
? await readUnitDropIn(vfs, name)
|
||||||
|
: emptyUnitDropIn()
|
||||||
|
const startSec = di.timeoutStartSec
|
||||||
const t0 = Date.now()
|
const t0 = Date.now()
|
||||||
try {
|
try {
|
||||||
await s.start(ctx)
|
await withTimeoutSec(s.start(ctx), startSec, `start ${name}`)
|
||||||
runtime.set(name, { phase: 'active', startedAtMs: t0 })
|
runtime.set(name, { phase: 'active', startedAtMs: t0 })
|
||||||
|
const post = di.execStartPost
|
||||||
|
if (post && typeof ctx.execLine === 'function' && post.trim()) {
|
||||||
|
try {
|
||||||
|
await ctx.execLine(post.trim())
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e?.message || String(e)
|
||||||
|
try {
|
||||||
|
ctx.console?.error?.(`[bare-initd] ${name} ExecStartPost: ${msg}`)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
void appendVarLog(ctx, INITD_LOG, name, 'ExecStartPost: ' + msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e?.message || String(e)
|
const msg = e?.message || String(e)
|
||||||
@@ -154,16 +176,43 @@ export async function startBareService(ctx, name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Record<string, unknown>} ctx
|
* @param {Promise<void>} promise
|
||||||
* @param {string} name
|
* @param {number | null | undefined} sec
|
||||||
|
* @param {string} label
|
||||||
*/
|
*/
|
||||||
|
async function withTimeoutSec(promise, sec, label) {
|
||||||
|
if (sec == null || !Number.isFinite(sec) || sec <= 0) return promise
|
||||||
|
const ms = Math.round(sec * 1000)
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const t = setTimeout(() => {
|
||||||
|
reject(new Error(`${label} timed out after ${sec}s`))
|
||||||
|
}, ms)
|
||||||
|
Promise.resolve(promise).then(
|
||||||
|
() => {
|
||||||
|
clearTimeout(t)
|
||||||
|
resolve(undefined)
|
||||||
|
},
|
||||||
|
(e) => {
|
||||||
|
clearTimeout(t)
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function stopBareService(ctx, name) {
|
export async function stopBareService(ctx, name) {
|
||||||
const s = findBareServiceDefinition(name)
|
const s = findBareServiceDefinition(name)
|
||||||
if (!s) throw new Error(`Unknown unit: ${name}`)
|
if (!s) throw new Error(`Unknown unit: ${name}`)
|
||||||
if (typeof s.stop !== 'function') {
|
if (typeof s.stop !== 'function') {
|
||||||
throw new Error(`Unit ${name} does not support stop (no stop handler)`)
|
throw new Error(`Unit ${name} does not support stop (no stop handler)`)
|
||||||
}
|
}
|
||||||
await s.stop(ctx)
|
const vfs = ctx.vfs
|
||||||
|
let stopSec = null
|
||||||
|
if (vfs && typeof vfs.readFile === 'function') {
|
||||||
|
const di = await readUnitDropIn(vfs, name)
|
||||||
|
stopSec = di.timeoutStopSec
|
||||||
|
}
|
||||||
|
await withTimeoutSec(s.stop(ctx), stopSec, `stop ${name}`)
|
||||||
const prev = runtime.get(name)
|
const prev = runtime.get(name)
|
||||||
runtime.set(name, {
|
runtime.set(name, {
|
||||||
phase: 'inactive',
|
phase: 'inactive',
|
||||||
@@ -183,10 +232,16 @@ export async function restartBareService(ctx, name) {
|
|||||||
`Unit ${name} does not support restart (no stop handler; session teardown only)`
|
`Unit ${name} does not support restart (no stop handler; session teardown only)`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
await s.stop(ctx)
|
await stopBareService(ctx, name)
|
||||||
|
const vfs = ctx.vfs
|
||||||
|
const di =
|
||||||
|
vfs && typeof vfs.readFile === 'function'
|
||||||
|
? await readUnitDropIn(vfs, name)
|
||||||
|
: null
|
||||||
|
const startSec = di?.timeoutStartSec
|
||||||
const t0 = Date.now()
|
const t0 = Date.now()
|
||||||
try {
|
try {
|
||||||
await s.start(ctx)
|
await withTimeoutSec(s.start(ctx), startSec, `start ${name}`)
|
||||||
runtime.set(name, { phase: 'active', startedAtMs: t0 })
|
runtime.set(name, { phase: 'active', startedAtMs: t0 })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e?.message || String(e)
|
const msg = e?.message || String(e)
|
||||||
@@ -211,27 +266,84 @@ export async function startBareInitd(ctx) {
|
|||||||
let ordered = registry
|
let ordered = registry
|
||||||
if (vfs && typeof vfs.readFile === 'function') {
|
if (vfs && typeof vfs.readFile === 'function') {
|
||||||
const disabled = await readInitdDisabledSet(vfs)
|
const disabled = await readInitdDisabledSet(vfs)
|
||||||
const afterMap = await loadInitdAfterMap(
|
const dropIns = await loadInitdUnitDropIns(
|
||||||
vfs,
|
vfs,
|
||||||
registry,
|
registry,
|
||||||
BARE_INITD_DEFAULT_AFTER
|
BARE_INITD_DEFAULT_AFTER
|
||||||
)
|
)
|
||||||
ordered = sortServicesForBoot(registry, disabled, afterMap)
|
/** @type {Map<string, string[]>} */
|
||||||
|
const requiresMap = new Map()
|
||||||
|
/** @type {Map<string, string[]>} */
|
||||||
|
const wantsMap = new Map()
|
||||||
|
/** @type {Map<string, string[]>} */
|
||||||
|
const afterMap = new Map()
|
||||||
|
for (const [name, di] of dropIns) {
|
||||||
|
afterMap.set(name, di.after)
|
||||||
|
requiresMap.set(name, di.requires)
|
||||||
|
wantsMap.set(name, di.wants)
|
||||||
|
}
|
||||||
|
ordered = sortServicesForBoot(
|
||||||
|
registry,
|
||||||
|
disabled,
|
||||||
|
afterMap,
|
||||||
|
requiresMap,
|
||||||
|
wantsMap
|
||||||
|
)
|
||||||
}
|
}
|
||||||
for (const s of ordered) {
|
for (const s of ordered) {
|
||||||
const t0 = Date.now()
|
const t0 = Date.now()
|
||||||
try {
|
/** @type {import('./bare-initd-user.js').BareInitdUnitDropIn | null} */
|
||||||
await s.start(ctx)
|
let dropIn = null
|
||||||
runtime.set(s.name, { phase: 'active', startedAtMs: t0 })
|
if (vfs && typeof vfs.readFile === 'function') {
|
||||||
} catch (e) {
|
dropIn = await readUnitDropIn(vfs, s.name)
|
||||||
const msg = e?.message || String(e)
|
}
|
||||||
runtime.set(s.name, { phase: 'failed', startedAtMs: t0, error: msg })
|
const startSec = dropIn?.timeoutStartSec
|
||||||
|
const maxAttempts =
|
||||||
|
dropIn?.restart === 'on-failure' || dropIn?.restart === 'always'
|
||||||
|
? 3
|
||||||
|
: 1
|
||||||
|
const restartDelayMs =
|
||||||
|
dropIn?.restartSec != null && dropIn.restartSec >= 0
|
||||||
|
? Math.round(dropIn.restartSec * 1000)
|
||||||
|
: 1000
|
||||||
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||||
try {
|
try {
|
||||||
ctx.console?.error?.(`[bare-initd] ${s.name}: ${msg}`)
|
if (attempt > 0) {
|
||||||
} catch {
|
await new Promise((r) => setTimeout(r, restartDelayMs))
|
||||||
/* ignore */
|
}
|
||||||
|
await withTimeoutSec(s.start(ctx), startSec, `start ${s.name}`)
|
||||||
|
runtime.set(s.name, { phase: 'active', startedAtMs: t0 })
|
||||||
|
const post = dropIn?.execStartPost
|
||||||
|
if (
|
||||||
|
post &&
|
||||||
|
typeof ctx.execLine === 'function' &&
|
||||||
|
post.trim()
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await ctx.execLine(post.trim())
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e?.message || String(e)
|
||||||
|
try {
|
||||||
|
ctx.console?.error?.(`[bare-initd] ${s.name} ExecStartPost: ${msg}`)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
void appendVarLog(ctx, INITD_LOG, s.name, 'ExecStartPost: ' + msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e?.message || String(e)
|
||||||
|
if (attempt === maxAttempts - 1) {
|
||||||
|
runtime.set(s.name, { phase: 'failed', startedAtMs: t0, error: msg })
|
||||||
|
try {
|
||||||
|
ctx.console?.error?.(`[bare-initd] ${s.name}: ${msg}`)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
void appendVarLog(ctx, INITD_LOG, s.name, msg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
void appendVarLog(ctx, INITD_LOG, s.name, msg)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
* Semantic version of the booter `ctx` contract for custom kernels.
|
* Semantic version of the booter `ctx` contract for custom kernels.
|
||||||
* Bump when adding/removing/renaming documented `ctx` fields or changing behavior.
|
* Bump when adding/removing/renaming documented `ctx` fields or changing behavior.
|
||||||
*/
|
*/
|
||||||
export const BARE_OS_CTX_API_VERSION = '1.3.0'
|
export const BARE_OS_CTX_API_VERSION = '1.4.0'
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const BARE_OS_PSEUDO_FS_PATHS = Object.freeze([
|
|||||||
'/dev/zero',
|
'/dev/zero',
|
||||||
'/dev/urandom',
|
'/dev/urandom',
|
||||||
'/proc',
|
'/proc',
|
||||||
|
'/proc/bare_os_session_stats',
|
||||||
'/proc/bare_os_version',
|
'/proc/bare_os_version',
|
||||||
'/proc/cpuinfo',
|
'/proc/cpuinfo',
|
||||||
'/proc/loadavg',
|
'/proc/loadavg',
|
||||||
@@ -34,12 +35,15 @@ export const BARE_OS_PSEUDO_FS_PATHS = Object.freeze([
|
|||||||
'/proc/version',
|
'/proc/version',
|
||||||
'/run',
|
'/run',
|
||||||
'/run/bare-os',
|
'/run/bare-os',
|
||||||
|
'/run/bare-os/boot.json',
|
||||||
'/run/bare-os/boot_profile',
|
'/run/bare-os/boot_profile',
|
||||||
|
'/run/bare-os/ready',
|
||||||
'/run/bare-os/session',
|
'/run/bare-os/session',
|
||||||
'/run/bare-os/units',
|
'/run/bare-os/units',
|
||||||
'/sys',
|
'/sys',
|
||||||
'/sys/fs',
|
'/sys/fs',
|
||||||
'/sys/fs/bare_os',
|
'/sys/fs/bare_os',
|
||||||
|
'/sys/fs/bare_os/build_id',
|
||||||
'/sys/fs/bare_os/version'
|
'/sys/fs/bare_os/version'
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -48,6 +52,11 @@ export const BARE_OS_PSEUDO_FS_PATHS = Object.freeze([
|
|||||||
*/
|
*/
|
||||||
export function buildBareOsRuntimeCaps(shellEnv) {
|
export function buildBareOsRuntimeCaps(shellEnv) {
|
||||||
const pl = getBareOsPipelineLimits(shellEnv)
|
const pl = getBareOsPipelineLimits(shellEnv)
|
||||||
|
const auditOn =
|
||||||
|
shellEnv.BARE_OS_AUDIT === '1' || shellEnv.BARE_OS_AUDIT === 'true'
|
||||||
|
const minimalBoot =
|
||||||
|
shellEnv.BARE_OS_BOOT_MINIMAL === '1' ||
|
||||||
|
shellEnv.BARE_OS_BOOT_MINIMAL === 'true'
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
ctxApiVersion: BARE_OS_CTX_API_VERSION,
|
ctxApiVersion: BARE_OS_CTX_API_VERSION,
|
||||||
pipeline: Object.freeze({
|
pipeline: Object.freeze({
|
||||||
@@ -73,7 +82,19 @@ export function buildBareOsRuntimeCaps(shellEnv) {
|
|||||||
identityVault: true,
|
identityVault: true,
|
||||||
httpDelegate: true,
|
httpDelegate: true,
|
||||||
gitDelegate: true,
|
gitDelegate: true,
|
||||||
systemctlDelegate: true
|
systemctlDelegate: true,
|
||||||
|
jobControl: true,
|
||||||
|
shellHereString: true,
|
||||||
|
bootReadyPseudoFs: true,
|
||||||
|
sessionStatsProc: true,
|
||||||
|
initdRequiresWants: true,
|
||||||
|
initdTimeoutsRestart: true,
|
||||||
|
vfsChown: true,
|
||||||
|
auditLog: auditOn,
|
||||||
|
recoveryBootCapable: true,
|
||||||
|
minimalBootActive: minimalBoot,
|
||||||
|
vfsWatch: false,
|
||||||
|
seederRpcExtended: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,15 @@ export const CRON_LOG = `${BARE_OS_VAR_LOG_DIR}/cron.log`
|
|||||||
|
|
||||||
export const INITD_LOG = `${BARE_OS_VAR_LOG_DIR}/initd.log`
|
export const INITD_LOG = `${BARE_OS_VAR_LOG_DIR}/initd.log`
|
||||||
|
|
||||||
|
export const AUDIT_LOG = `${BARE_OS_VAR_LOG_DIR}/audit.log`
|
||||||
|
|
||||||
const README_REL = `${BARE_OS_VAR_LOG_DIR}/README`
|
const README_REL = `${BARE_OS_VAR_LOG_DIR}/README`
|
||||||
|
|
||||||
const README_TEXT = `Bare OS session logs (mirrored on your personal drive under /.bare-os/var/log/<HOME-basename>/).
|
const README_TEXT = `Bare OS session logs (mirrored on your personal drive under /.bare-os/var/log/<HOME-basename>/).
|
||||||
kernel-console.log — console.log / console.error from the kernel session
|
kernel-console.log — console.log / console.error from the kernel session
|
||||||
cron.log — bare-cron job errors
|
cron.log — bare-cron job errors
|
||||||
initd.log — bare-initd service start failures
|
initd.log — bare-initd service start failures
|
||||||
|
audit.log — optional execLine audit when BARE_OS_AUDIT=1
|
||||||
`
|
`
|
||||||
|
|
||||||
/** Max size before trimming older log bytes (best-effort). */
|
/** Max size before trimming older log bytes (best-effort). */
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ const SHELL_BUILTINS = new Set([
|
|||||||
'type',
|
'type',
|
||||||
'login',
|
'login',
|
||||||
'logout',
|
'logout',
|
||||||
'exit'
|
'exit',
|
||||||
|
'jobs',
|
||||||
|
'fg',
|
||||||
|
'bg'
|
||||||
])
|
])
|
||||||
|
|
||||||
function isShellBuiltin(cmd) {
|
function isShellBuiltin(cmd) {
|
||||||
@@ -332,6 +335,11 @@ export function tokenize(line) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (c === '<') {
|
if (c === '<') {
|
||||||
|
if (line[i + 1] === '<' && line[i + 2] === '<') {
|
||||||
|
tokens.push({ type: 'op', value: '<<<' })
|
||||||
|
i += 3
|
||||||
|
continue
|
||||||
|
}
|
||||||
tokens.push({ type: 'op', value: '<' })
|
tokens.push({ type: 'op', value: '<' })
|
||||||
i++
|
i++
|
||||||
continue
|
continue
|
||||||
@@ -444,7 +452,8 @@ export function expandWord(s, env) {
|
|||||||
* redirAppend: boolean,
|
* redirAppend: boolean,
|
||||||
* redirErr: string | null,
|
* redirErr: string | null,
|
||||||
* redirErrAppend: boolean,
|
* redirErrAppend: boolean,
|
||||||
* mergeStderrToStdout: boolean
|
* mergeStderrToStdout: boolean,
|
||||||
|
* redirHereDoc: string | null
|
||||||
* }} SimpleCmd
|
* }} SimpleCmd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -476,6 +485,8 @@ function parseSimpleCommand(seg) {
|
|||||||
let redirErr = null
|
let redirErr = null
|
||||||
let redirErrAppend = false
|
let redirErrAppend = false
|
||||||
let mergeStderrToStdout = false
|
let mergeStderrToStdout = false
|
||||||
|
/** @type {string | null} */
|
||||||
|
let redirHereDoc = null
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
const argvWords = []
|
const argvWords = []
|
||||||
let seenCommand = false
|
let seenCommand = false
|
||||||
@@ -515,6 +526,14 @@ function parseSimpleCommand(seg) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (t.value === '<<<') {
|
||||||
|
const n = seg[w + 1]
|
||||||
|
if (n && n.type === 'word') {
|
||||||
|
redirHereDoc = n.value
|
||||||
|
w += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
w++
|
w++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -574,6 +593,11 @@ function parseSimpleCommand(seg) {
|
|||||||
argvWords.splice(i, 2)
|
argvWords.splice(i, 2)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (argvWords[i] === '<<<') {
|
||||||
|
redirHereDoc = argvWords[i + 1] ?? ''
|
||||||
|
argvWords.splice(i, 2)
|
||||||
|
continue
|
||||||
|
}
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,7 +609,8 @@ function parseSimpleCommand(seg) {
|
|||||||
redirAppend,
|
redirAppend,
|
||||||
redirErr,
|
redirErr,
|
||||||
redirErrAppend,
|
redirErrAppend,
|
||||||
mergeStderrToStdout
|
mergeStderrToStdout,
|
||||||
|
redirHereDoc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,7 +706,9 @@ async function execParsedPipeline(ctx, pipeline) {
|
|||||||
}
|
}
|
||||||
const name = argv[0]
|
const name = argv[0]
|
||||||
|
|
||||||
if (cmd.redirIn) {
|
if (cmd.redirHereDoc) {
|
||||||
|
stdinText = expandWord(cmd.redirHereDoc, env)
|
||||||
|
} else if (cmd.redirIn) {
|
||||||
const p = expandWord(cmd.redirIn, env)
|
const p = expandWord(cmd.redirIn, env)
|
||||||
const buf = await vfs.readFile(p)
|
const buf = await vfs.readFile(p)
|
||||||
stdinText = buf ? ctx.b4a.toString(buf) : ''
|
stdinText = buf ? ctx.b4a.toString(buf) : ''
|
||||||
@@ -710,11 +737,21 @@ async function execParsedPipeline(ctx, pipeline) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const pushOut = (...args) => {
|
const pushOut = (...args) => {
|
||||||
outChunks.push(args.map(String).join(' ') + '\n')
|
const line = args.map(String).join(' ') + '\n'
|
||||||
|
outChunks.push(line)
|
||||||
|
const st = ctx.bareOsSessionStats
|
||||||
|
if (st && typeof st.pipelineBytesTotal === 'number') {
|
||||||
|
st.pipelineBytesTotal += line.length
|
||||||
|
}
|
||||||
checkCaptureSize(outChunks)
|
checkCaptureSize(outChunks)
|
||||||
}
|
}
|
||||||
const pushErr = (...args) => {
|
const pushErr = (...args) => {
|
||||||
errChunks.push(args.map(String).join(' ') + '\n')
|
const line = args.map(String).join(' ') + '\n'
|
||||||
|
errChunks.push(line)
|
||||||
|
const st = ctx.bareOsSessionStats
|
||||||
|
if (st && typeof st.pipelineBytesTotal === 'number') {
|
||||||
|
st.pipelineBytesTotal += line.length
|
||||||
|
}
|
||||||
checkCaptureSize(errChunks)
|
checkCaptureSize(errChunks)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -920,6 +957,45 @@ async function execParsedPipeline(ctx, pipeline) {
|
|||||||
origErr.call(ctx.console, 'logout: not supported in this environment')
|
origErr.call(ctx.console, 'logout: not supported in this environment')
|
||||||
ctx.exitCode = 1
|
ctx.exitCode = 1
|
||||||
}
|
}
|
||||||
|
} else if (name === 'jobs') {
|
||||||
|
const list = ctx.shellBackgroundJobs?.list || []
|
||||||
|
if (!list.length) {
|
||||||
|
origLog.call(ctx.console, '')
|
||||||
|
} else {
|
||||||
|
for (const j of list) {
|
||||||
|
const st = j.done ? 'Done' : 'Running'
|
||||||
|
origLog.call(ctx.console, `[${j.id}]+ ${st} ${j.label}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (name === 'fg') {
|
||||||
|
const list = ctx.shellBackgroundJobs?.list || []
|
||||||
|
let candidates = list.filter((j) => !j.done)
|
||||||
|
const arg = argv[1]
|
||||||
|
if (arg) {
|
||||||
|
const raw = arg.startsWith('%') ? arg.slice(1) : arg
|
||||||
|
const id = Number.parseInt(raw, 10)
|
||||||
|
if (Number.isFinite(id)) {
|
||||||
|
candidates = list.filter((j) => j.id === id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const j = candidates.length ? candidates[candidates.length - 1] : null
|
||||||
|
if (!j) {
|
||||||
|
origErr.call(ctx.console, 'fg: no such job')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await j.promise
|
||||||
|
} catch {
|
||||||
|
/* background errors already logged */
|
||||||
|
}
|
||||||
|
ctx.exitCode = 0
|
||||||
|
}
|
||||||
|
} else if (name === 'bg') {
|
||||||
|
origErr.call(
|
||||||
|
ctx.console,
|
||||||
|
'bg: job resume is not supported (no stopped jobs)'
|
||||||
|
)
|
||||||
|
ctx.exitCode = 1
|
||||||
} else if (name === 'exit') {
|
} else if (name === 'exit') {
|
||||||
code = 'exit'
|
code = 'exit'
|
||||||
let ec = 0
|
let ec = 0
|
||||||
@@ -1024,6 +1100,70 @@ function splitTopLevelStatements(tokens) {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split on `&` at depth 0 (outside `if`/`fi`). Each segment except the last runs in the background.
|
||||||
|
* @param {Token[]} tokens
|
||||||
|
* @returns {Token[][]}
|
||||||
|
*/
|
||||||
|
function splitTopLevelByAmpersand(tokens) {
|
||||||
|
/** @type {Token[][]} */
|
||||||
|
const out = []
|
||||||
|
/** @type {Token[]} */
|
||||||
|
let cur = []
|
||||||
|
let depth = 0
|
||||||
|
for (const t of tokens) {
|
||||||
|
if (t.type === 'word') {
|
||||||
|
if (t.value === 'if') depth++
|
||||||
|
else if (t.value === 'fi') depth = Math.max(0, depth - 1)
|
||||||
|
}
|
||||||
|
if (t.type === 'op' && t.value === '&' && depth === 0) {
|
||||||
|
out.push(cur)
|
||||||
|
cur = []
|
||||||
|
} else {
|
||||||
|
cur.push(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push(cur)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
* @param {Token[]} toks
|
||||||
|
*/
|
||||||
|
function scheduleBackgroundShell(ctx, toks) {
|
||||||
|
if (!ctx.shellBackgroundJobs) {
|
||||||
|
ctx.shellBackgroundJobs = { nextId: 1, list: [] }
|
||||||
|
}
|
||||||
|
const id = ctx.shellBackgroundJobs.nextId++
|
||||||
|
const label = toks
|
||||||
|
.filter((t) => t.type === 'word')
|
||||||
|
.map((t) => t.value)
|
||||||
|
.slice(0, 6)
|
||||||
|
.join(' ')
|
||||||
|
const childCtx = Object.assign({}, ctx)
|
||||||
|
const promise = (async () => {
|
||||||
|
const statements = splitTopLevelStatements(toks)
|
||||||
|
for (const stmt of statements) {
|
||||||
|
if (!stmt.length) continue
|
||||||
|
const r =
|
||||||
|
stmt[0]?.type === 'word' && stmt[0].value === 'if'
|
||||||
|
? await execIfConstruct(childCtx, stmt)
|
||||||
|
: await execAndOrList(childCtx, stmt)
|
||||||
|
if (r === 'exit') return r
|
||||||
|
}
|
||||||
|
syncBareOsExitStatusEnv(childCtx)
|
||||||
|
return 'ok'
|
||||||
|
})()
|
||||||
|
/** @type {{ id: number, label: string, promise: Promise<string>, done: boolean }} */
|
||||||
|
const entry = { id, label, promise, done: false }
|
||||||
|
ctx.shellBackgroundJobs.list.push(entry)
|
||||||
|
promise.finally(() => {
|
||||||
|
entry.done = true
|
||||||
|
})
|
||||||
|
ctx.console.error(`[${id}] ${label || '(job)'} &`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Token[]} tokens
|
* @param {Token[]} tokens
|
||||||
* @param {number} start index after `if`
|
* @param {number} start index after `if`
|
||||||
@@ -1208,25 +1348,26 @@ export async function execShellLine(ctx, line) {
|
|||||||
const tokens = tokenize(raw)
|
const tokens = tokenize(raw)
|
||||||
if (!tokens.length) return 'ok'
|
if (!tokens.length) return 'ok'
|
||||||
|
|
||||||
if (tokens.some((t) => t.type === 'op' && t.value === '&')) {
|
|
||||||
ctx.console.error(
|
|
||||||
'shell: unsupported operator & (job control is not available)'
|
|
||||||
)
|
|
||||||
ctx.exitCode = 2
|
|
||||||
syncBareOsExitStatusEnv(ctx)
|
|
||||||
return 'ok'
|
|
||||||
}
|
|
||||||
|
|
||||||
const statements = splitTopLevelStatements(tokens)
|
const statements = splitTopLevelStatements(tokens)
|
||||||
for (const stmt of statements) {
|
for (const stmt of statements) {
|
||||||
if (!stmt.length) continue
|
if (!stmt.length) continue
|
||||||
const r =
|
const ampParts = splitTopLevelByAmpersand(stmt)
|
||||||
stmt[0]?.type === 'word' && stmt[0].value === 'if'
|
for (let ai = 0; ai < ampParts.length; ai++) {
|
||||||
? await execIfConstruct(ctx, stmt)
|
const part = ampParts[ai]
|
||||||
: await execAndOrList(ctx, stmt)
|
if (!part.length) continue
|
||||||
if (r === 'exit') {
|
const isBg = ai < ampParts.length - 1
|
||||||
syncBareOsExitStatusEnv(ctx)
|
if (isBg) {
|
||||||
return 'exit'
|
scheduleBackgroundShell(ctx, part)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const r =
|
||||||
|
part[0]?.type === 'word' && part[0].value === 'if'
|
||||||
|
? await execIfConstruct(ctx, part)
|
||||||
|
: await execAndOrList(ctx, part)
|
||||||
|
if (r === 'exit') {
|
||||||
|
syncBareOsExitStatusEnv(ctx)
|
||||||
|
return 'exit'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
syncBareOsExitStatusEnv(ctx)
|
syncBareOsExitStatusEnv(ctx)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import unixPathResolve from 'unix-path-resolve'
|
|||||||
import b4a from 'b4a'
|
import b4a from 'b4a'
|
||||||
import {
|
import {
|
||||||
extractBareOs,
|
extractBareOs,
|
||||||
|
identityNames,
|
||||||
isPersonalRoute,
|
isPersonalRoute,
|
||||||
isVirtualMountPoint,
|
isVirtualMountPoint,
|
||||||
mergeBareOsOnWrite,
|
mergeBareOsOnWrite,
|
||||||
@@ -33,7 +34,12 @@ const DIR_MARKER = '.bareos_empty'
|
|||||||
* bootStartedMs?: number,
|
* bootStartedMs?: number,
|
||||||
* initdRunText?: () => string,
|
* initdRunText?: () => string,
|
||||||
* bootProfileText?: () => string,
|
* bootProfileText?: () => string,
|
||||||
* sessionText?: () => string
|
* sessionText?: () => string,
|
||||||
|
* sessionStatsText?: () => string,
|
||||||
|
* bootReadyMarkerText?: () => string,
|
||||||
|
* bootReadyJsonText?: () => string,
|
||||||
|
* buildIdText?: () => string,
|
||||||
|
* secureRandomBytes?: (n: number) => Uint8Array
|
||||||
* }} [vfsOptions]
|
* }} [vfsOptions]
|
||||||
*/
|
*/
|
||||||
export function createVfs(
|
export function createVfs(
|
||||||
@@ -60,6 +66,26 @@ export function createVfs(
|
|||||||
typeof vfsOptions.sessionText === 'function'
|
typeof vfsOptions.sessionText === 'function'
|
||||||
? vfsOptions.sessionText
|
? vfsOptions.sessionText
|
||||||
: null
|
: null
|
||||||
|
const sessionStatsText =
|
||||||
|
typeof vfsOptions.sessionStatsText === 'function'
|
||||||
|
? vfsOptions.sessionStatsText
|
||||||
|
: null
|
||||||
|
const bootReadyMarkerText =
|
||||||
|
typeof vfsOptions.bootReadyMarkerText === 'function'
|
||||||
|
? vfsOptions.bootReadyMarkerText
|
||||||
|
: null
|
||||||
|
const bootReadyJsonText =
|
||||||
|
typeof vfsOptions.bootReadyJsonText === 'function'
|
||||||
|
? vfsOptions.bootReadyJsonText
|
||||||
|
: null
|
||||||
|
const buildIdText =
|
||||||
|
typeof vfsOptions.buildIdText === 'function'
|
||||||
|
? vfsOptions.buildIdText
|
||||||
|
: null
|
||||||
|
const secureRandomBytes =
|
||||||
|
typeof vfsOptions.secureRandomBytes === 'function'
|
||||||
|
? vfsOptions.secureRandomBytes
|
||||||
|
: null
|
||||||
const HOME = () => env.HOME || '/home/guest'
|
const HOME = () => env.HOME || '/home/guest'
|
||||||
let cwd = env.PWD || HOME()
|
let cwd = env.PWD || HOME()
|
||||||
|
|
||||||
@@ -190,9 +216,19 @@ export function createVfs(
|
|||||||
return '/bin/sh (bare-os stub; not a real inode)\n'
|
return '/bin/sh (bare-os stub; not a real inode)\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Bounded pseudo-random buffer; **not** suitable for cryptography. */
|
/**
|
||||||
|
* When the booter supplies `secureRandomBytes` (e.g. node:crypto), reads are suitable
|
||||||
|
* for cryptographic use; otherwise falls back to Math.random (not crypto-grade).
|
||||||
|
*/
|
||||||
function pseudoUrandomBytes() {
|
function pseudoUrandomBytes() {
|
||||||
const n = 4096
|
const n = 4096
|
||||||
|
if (secureRandomBytes) {
|
||||||
|
try {
|
||||||
|
return secureRandomBytes(n)
|
||||||
|
} catch {
|
||||||
|
/* fall through */
|
||||||
|
}
|
||||||
|
}
|
||||||
const u = new Uint8Array(n)
|
const u = new Uint8Array(n)
|
||||||
for (let i = 0; i < n; i++) u[i] = Math.floor(Math.random() * 256)
|
for (let i = 0; i < n; i++) u[i] = Math.floor(Math.random() * 256)
|
||||||
return u
|
return u
|
||||||
@@ -204,6 +240,10 @@ export function createVfs(
|
|||||||
if (f === 'version' && (k === 'proc' || k === 'sys')) {
|
if (f === 'version' && (k === 'proc' || k === 'sys')) {
|
||||||
return utf8Encode(pseudoVersionText())
|
return utf8Encode(pseudoVersionText())
|
||||||
}
|
}
|
||||||
|
if (k === 'sys' && f === 'build_id') {
|
||||||
|
const t = buildIdText ? buildIdText() : 'unknown\n'
|
||||||
|
return utf8Encode(t)
|
||||||
|
}
|
||||||
if (k === 'proc') {
|
if (k === 'proc') {
|
||||||
if (f === 'cmdline') return utf8Encode(pseudoCmdlineText())
|
if (f === 'cmdline') return utf8Encode(pseudoCmdlineText())
|
||||||
if (f === 'environ') return pseudoEnvironBytes()
|
if (f === 'environ') return pseudoEnvironBytes()
|
||||||
@@ -213,6 +253,12 @@ export function createVfs(
|
|||||||
if (f === 'loadavg') return utf8Encode(pseudoLoadavgText())
|
if (f === 'loadavg') return utf8Encode(pseudoLoadavgText())
|
||||||
if (f === 'exe') return utf8Encode(pseudoSelfExeText())
|
if (f === 'exe') return utf8Encode(pseudoSelfExeText())
|
||||||
if (f === 'mounts') return utf8Encode(pseudoMountsText())
|
if (f === 'mounts') return utf8Encode(pseudoMountsText())
|
||||||
|
if (f === 'bare_os_session_stats') {
|
||||||
|
const t = sessionStatsText
|
||||||
|
? sessionStatsText()
|
||||||
|
: '{}\n'
|
||||||
|
return utf8Encode(t)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (k === 'run' && f === 'units') {
|
if (k === 'run' && f === 'units') {
|
||||||
const t = initdRunText
|
const t = initdRunText
|
||||||
@@ -228,6 +274,14 @@ export function createVfs(
|
|||||||
const t = sessionText ? sessionText() : '\n'
|
const t = sessionText ? sessionText() : '\n'
|
||||||
return utf8Encode(t)
|
return utf8Encode(t)
|
||||||
}
|
}
|
||||||
|
if (k === 'run' && f === 'ready') {
|
||||||
|
const t = bootReadyMarkerText ? bootReadyMarkerText() : '0\n'
|
||||||
|
return utf8Encode(t)
|
||||||
|
}
|
||||||
|
if (k === 'run' && f === 'boot_json') {
|
||||||
|
const t = bootReadyJsonText ? bootReadyJsonText() : '{}\n'
|
||||||
|
return utf8Encode(t)
|
||||||
|
}
|
||||||
if (k === 'dev' && f === 'null') return utf8Encode('')
|
if (k === 'dev' && f === 'null') return utf8Encode('')
|
||||||
if (k === 'dev' && f === 'zero') return new Uint8Array(65536)
|
if (k === 'dev' && f === 'zero') return new Uint8Array(65536)
|
||||||
if (k === 'dev' && f === 'urandom') return pseudoUrandomBytes()
|
if (k === 'dev' && f === 'urandom') return pseudoUrandomBytes()
|
||||||
@@ -275,6 +329,14 @@ export function createVfs(
|
|||||||
if (sub === 'mounts') {
|
if (sub === 'mounts') {
|
||||||
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'mounts' }
|
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'mounts' }
|
||||||
}
|
}
|
||||||
|
if (sub === 'bare_os_session_stats') {
|
||||||
|
return {
|
||||||
|
virtualPseudo: true,
|
||||||
|
kind: 'proc',
|
||||||
|
node: 'file',
|
||||||
|
file: 'bare_os_session_stats'
|
||||||
|
}
|
||||||
|
}
|
||||||
return { virtualPseudo: true, kind: 'proc', node: 'enoent' }
|
return { virtualPseudo: true, kind: 'proc', node: 'enoent' }
|
||||||
}
|
}
|
||||||
if (n === '/run' || n.startsWith('/run/')) {
|
if (n === '/run' || n.startsWith('/run/')) {
|
||||||
@@ -303,6 +365,17 @@ export function createVfs(
|
|||||||
file: 'session'
|
file: 'session'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (n === '/run/bare-os/ready') {
|
||||||
|
return { virtualPseudo: true, kind: 'run', node: 'file', file: 'ready' }
|
||||||
|
}
|
||||||
|
if (n === '/run/bare-os/boot.json') {
|
||||||
|
return {
|
||||||
|
virtualPseudo: true,
|
||||||
|
kind: 'run',
|
||||||
|
node: 'file',
|
||||||
|
file: 'boot_json'
|
||||||
|
}
|
||||||
|
}
|
||||||
return { virtualPseudo: true, kind: 'run', node: 'enoent' }
|
return { virtualPseudo: true, kind: 'run', node: 'enoent' }
|
||||||
}
|
}
|
||||||
if (n === '/dev' || n.startsWith('/dev/')) {
|
if (n === '/dev' || n.startsWith('/dev/')) {
|
||||||
@@ -333,6 +406,9 @@ export function createVfs(
|
|||||||
if (n === '/sys/fs/bare_os/version') {
|
if (n === '/sys/fs/bare_os/version') {
|
||||||
return { virtualPseudo: true, kind: 'sys', node: 'file', file: 'version' }
|
return { virtualPseudo: true, kind: 'sys', node: 'file', file: 'version' }
|
||||||
}
|
}
|
||||||
|
if (n === '/sys/fs/bare_os/build_id') {
|
||||||
|
return { virtualPseudo: true, kind: 'sys', node: 'file', file: 'build_id' }
|
||||||
|
}
|
||||||
return { virtualPseudo: true, kind: 'sys', node: 'enoent' }
|
return { virtualPseudo: true, kind: 'sys', node: 'enoent' }
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -823,6 +899,7 @@ export function createVfs(
|
|||||||
}
|
}
|
||||||
if (pr.kind === 'proc' && pr.node === 'root') {
|
if (pr.kind === 'proc' && pr.node === 'root') {
|
||||||
return [
|
return [
|
||||||
|
'bare_os_session_stats',
|
||||||
'bare_os_version',
|
'bare_os_version',
|
||||||
'cpuinfo',
|
'cpuinfo',
|
||||||
'loadavg',
|
'loadavg',
|
||||||
@@ -843,13 +920,13 @@ export function createVfs(
|
|||||||
return ['bare_os']
|
return ['bare_os']
|
||||||
}
|
}
|
||||||
if (pr.kind === 'sys' && pr.node === 'dir' && pr.dir === 'bare_os') {
|
if (pr.kind === 'sys' && pr.node === 'dir' && pr.dir === 'bare_os') {
|
||||||
return ['version']
|
return ['build_id', 'version']
|
||||||
}
|
}
|
||||||
if (pr.kind === 'run' && pr.node === 'root') {
|
if (pr.kind === 'run' && pr.node === 'root') {
|
||||||
return ['bare-os']
|
return ['bare-os']
|
||||||
}
|
}
|
||||||
if (pr.kind === 'run' && pr.node === 'dir' && pr.dir === 'bare_os') {
|
if (pr.kind === 'run' && pr.node === 'dir' && pr.dir === 'bare_os') {
|
||||||
return ['boot_profile', 'session', 'units']
|
return ['boot.json', 'boot_profile', 'ready', 'session', 'units']
|
||||||
}
|
}
|
||||||
if (pr.kind === 'dev' && pr.node === 'root') {
|
if (pr.kind === 'dev' && pr.node === 'root') {
|
||||||
return ['null', 'urandom', 'zero']
|
return ['null', 'urandom', 'zero']
|
||||||
@@ -1201,6 +1278,93 @@ export function createVfs(
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update stored ownership on the personal drive (same writable scope as chmod). Root (euid 0)
|
||||||
|
* may set any uid/gid; non-root may only change the group of files they own.
|
||||||
|
* @param {string} userPath
|
||||||
|
* @param {{ uid: number, gid: number, uname?: string, gname?: string }} next
|
||||||
|
*/
|
||||||
|
async chown(userPath, next) {
|
||||||
|
const abs = resolveLogical(userPath)
|
||||||
|
const r = route(abs)
|
||||||
|
if (
|
||||||
|
r.virtualHomeDir ||
|
||||||
|
r.virtualMntRoot ||
|
||||||
|
r.virtualVarRoot ||
|
||||||
|
r.virtualPseudo
|
||||||
|
) {
|
||||||
|
throw new Error('chown: ' + userPath + ': Operation not supported')
|
||||||
|
}
|
||||||
|
if (r.mntReadOnly === true) {
|
||||||
|
throw new Error('Read-only mount: ' + userPath)
|
||||||
|
}
|
||||||
|
const { drive, path: p } = r
|
||||||
|
if (drive !== personalDrive) {
|
||||||
|
throw new Error('chown: read-only system path: ' + userPath)
|
||||||
|
}
|
||||||
|
if (isHyperdriveRootPath(p)) {
|
||||||
|
throw new Error('chown: invalid path')
|
||||||
|
}
|
||||||
|
const st = await lstatFromAbs(abs)
|
||||||
|
if (!st) throw new Error('chown: ' + userPath + ': No such file')
|
||||||
|
const { uid: euid } = parseUidGid(env)
|
||||||
|
const nu = Number(next.uid)
|
||||||
|
const ng = Number(next.gid)
|
||||||
|
if (!Number.isFinite(nu) || !Number.isFinite(ng)) {
|
||||||
|
throw new Error('chown: invalid uid/gid')
|
||||||
|
}
|
||||||
|
if (euid !== 0) {
|
||||||
|
if (euid !== st.uid) {
|
||||||
|
throw new Error('chown: ' + userPath + ': Operation not permitted')
|
||||||
|
}
|
||||||
|
if (nu !== st.uid) {
|
||||||
|
throw new Error('chown: ' + userPath + ': Operation not permitted')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { user: defU, group: defG } = identityNames(env)
|
||||||
|
const uname =
|
||||||
|
typeof next.uname === 'string' && next.uname.trim()
|
||||||
|
? next.uname.trim()
|
||||||
|
: defU
|
||||||
|
const gname =
|
||||||
|
typeof next.gname === 'string' && next.gname.trim()
|
||||||
|
? next.gname.trim()
|
||||||
|
: defG
|
||||||
|
const e = await entryOn(drive, p, { follow: false })
|
||||||
|
if (!e?.value) {
|
||||||
|
throw new Error('chown: cannot change inferred directory: ' + userPath)
|
||||||
|
}
|
||||||
|
const v = e.value
|
||||||
|
const prevBare = extractBareOs(v)
|
||||||
|
const now = Date.now()
|
||||||
|
const bo = prevBare
|
||||||
|
? {
|
||||||
|
...prevBare,
|
||||||
|
uid: nu,
|
||||||
|
gid: ng,
|
||||||
|
uname,
|
||||||
|
gname,
|
||||||
|
mtimeMs: prevBare.mtimeMs,
|
||||||
|
ctimeMs: now
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
mode: S_IFREG | 0o644,
|
||||||
|
uid: nu,
|
||||||
|
gid: ng,
|
||||||
|
uname,
|
||||||
|
gname,
|
||||||
|
mtimeMs: st.mtimeMs,
|
||||||
|
ctimeMs: now
|
||||||
|
}
|
||||||
|
const executable = !!(v.blob && (bo.mode & 0o111))
|
||||||
|
await drive.putEntry(p, {
|
||||||
|
executable,
|
||||||
|
linkname: v.linkname ?? null,
|
||||||
|
blob: v.blob ?? null,
|
||||||
|
metadata: mergeEntryMetadata(v.metadata, bo)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Like stat but do not follow symlinks at the final path (for isomorphic-git lstat).
|
* Like stat but do not follow symlinks at the final path (for isomorphic-git lstat).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ export async function runWgetCli(ctx, argv) {
|
|||||||
userAgent: /** @type {string | null} */ (null),
|
userAgent: /** @type {string | null} */ (null),
|
||||||
timeoutMs: 0,
|
timeoutMs: 0,
|
||||||
continuePartial: false,
|
continuePartial: false,
|
||||||
|
noClobber: false,
|
||||||
extraHeaders: /** @type {string[]} */ ([]),
|
extraHeaders: /** @type {string[]} */ ([]),
|
||||||
postData: /** @type {string | null} */ (null),
|
postData: /** @type {string | null} */ (null),
|
||||||
postFile: /** @type {string | null} */ (null)
|
postFile: /** @type {string | null} */ (null)
|
||||||
@@ -207,6 +208,12 @@ export async function runWgetCli(ctx, argv) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (a === '-nc' || a === '--no-clobber') {
|
||||||
|
st.noClobber = true
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (a === '-O' || a === '--output-document') {
|
if (a === '-O' || a === '--output-document') {
|
||||||
if (i + 1 >= args.length) {
|
if (i + 1 >= args.length) {
|
||||||
ctx.console.error('wget: option requires an argument: ' + a)
|
ctx.console.error('wget: option requires an argument: ' + a)
|
||||||
@@ -345,6 +352,7 @@ export async function runWgetCli(ctx, argv) {
|
|||||||
userAgent,
|
userAgent,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
continuePartial,
|
continuePartial,
|
||||||
|
noClobber,
|
||||||
extraHeaders,
|
extraHeaders,
|
||||||
postData,
|
postData,
|
||||||
postFile
|
postFile
|
||||||
@@ -380,6 +388,12 @@ export async function runWgetCli(ctx, argv) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (noClobber && continuePartial) {
|
||||||
|
ctx.console.error('wget: --no-clobber cannot be used with --continue')
|
||||||
|
ctx.exitCode = 2
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (outputDocument && directoryPrefix) {
|
if (outputDocument && directoryPrefix) {
|
||||||
ctx.console.error('wget: cannot combine -O and -P')
|
ctx.console.error('wget: cannot combine -O and -P')
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
@@ -509,6 +523,24 @@ export async function runWgetCli(ctx, argv) {
|
|||||||
buf = combined
|
buf = combined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
noClobber &&
|
||||||
|
targetPath != null &&
|
||||||
|
outputDocument !== '-' &&
|
||||||
|
!continuePartial
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const ex = await vfs.stat(targetPath)
|
||||||
|
if (ex && ex.type === 'file' && (ex.size ?? 0) > 0) {
|
||||||
|
if (!quiet)
|
||||||
|
ctx.console.error('wget: ' + targetPath + ': File exists (--no-clobber)')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* treat as absent */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (outputDocument === '-') {
|
if (outputDocument === '-') {
|
||||||
ctx.console.log(ctx.b4a.toString(buf))
|
ctx.console.log(ctx.b4a.toString(buf))
|
||||||
} else if (outputDocument != null) {
|
} else if (outputDocument != null) {
|
||||||
|
|||||||
@@ -691,6 +691,7 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
|
|||||||
t.ok(root.includes('run'))
|
t.ok(root.includes('run'))
|
||||||
t.ok(root.includes('dev'))
|
t.ok(root.includes('dev'))
|
||||||
t.alike(await vfs.readdir('/proc').then((a) => [...a].sort()), [
|
t.alike(await vfs.readdir('/proc').then((a) => [...a].sort()), [
|
||||||
|
'bare_os_session_stats',
|
||||||
'bare_os_version',
|
'bare_os_version',
|
||||||
'cpuinfo',
|
'cpuinfo',
|
||||||
'loadavg',
|
'loadavg',
|
||||||
@@ -741,7 +742,9 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
|
|||||||
const z = await vfs.readFile('/dev/zero')
|
const z = await vfs.readFile('/dev/zero')
|
||||||
t.ok(z && z.byteLength === 65536)
|
t.ok(z && z.byteLength === 65536)
|
||||||
t.alike(await vfs.readdir('/run/bare-os').then((a) => [...a].sort()), [
|
t.alike(await vfs.readdir('/run/bare-os').then((a) => [...a].sort()), [
|
||||||
|
'boot.json',
|
||||||
'boot_profile',
|
'boot_profile',
|
||||||
|
'ready',
|
||||||
'session',
|
'session',
|
||||||
'units'
|
'units'
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -1,6 +1,85 @@
|
|||||||
async function run(ctx, argv) {
|
/**
|
||||||
ctx.console.error(
|
* chgrp — change file group (Hyperdrive metadata on writable paths).
|
||||||
'chgrp: changing group is not supported on Bare OS (single-user Hyperdrive metadata).'
|
*/
|
||||||
)
|
|
||||||
ctx.exitCode = 1
|
/**
|
||||||
|
* @param {string} spec
|
||||||
|
* @param {Record<string, string>} env
|
||||||
|
* @returns {number | null}
|
||||||
|
*/
|
||||||
|
function parseGroupSpec(spec, env) {
|
||||||
|
const s = String(spec).trim()
|
||||||
|
if (!s) return null
|
||||||
|
if (/^\d+$/.test(s)) {
|
||||||
|
const n = Number.parseInt(s, 10)
|
||||||
|
return Number.isFinite(n) ? n : null
|
||||||
|
}
|
||||||
|
const g = (env.GROUP || env.USER || 'guest').trim()
|
||||||
|
if (s === 'root') return 0
|
||||||
|
if (s === 'guest' || s === 'nobody') return 65534
|
||||||
|
if (g && s === g) {
|
||||||
|
const gid = Number.parseInt(String(env.GID ?? '65534'), 10)
|
||||||
|
return Number.isFinite(gid) ? gid : 65534
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(ctx, argv) {
|
||||||
|
let i = 1
|
||||||
|
const files = []
|
||||||
|
for (; i < argv.length; i++) {
|
||||||
|
const a = argv[i]
|
||||||
|
if (a === '-h' || a === '--help') {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: chgrp [-h] GROUP FILE...\n' +
|
||||||
|
'GROUP may be a numeric gid or root/guest/nobody/current GROUP name.'
|
||||||
|
)
|
||||||
|
ctx.exitCode = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!a.startsWith('-')) break
|
||||||
|
ctx.console.error('chgrp: unsupported option: ' + a)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const groupSpec = argv[i++]
|
||||||
|
if (!groupSpec || i >= argv.length) {
|
||||||
|
ctx.console.error('usage: chgrp GROUP FILE...')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (; i < argv.length; i++) files.push(argv[i])
|
||||||
|
const gid = parseGroupSpec(groupSpec, ctx.vfs.env)
|
||||||
|
if (gid == null) {
|
||||||
|
ctx.console.error('chgrp: invalid group: ' + groupSpec)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const gname = /^\d+$/.test(groupSpec.trim())
|
||||||
|
? undefined
|
||||||
|
: groupSpec.trim() || undefined
|
||||||
|
for (const f of files) {
|
||||||
|
try {
|
||||||
|
const st = await ctx.vfs.stat(f)
|
||||||
|
if (!st) {
|
||||||
|
ctx.console.error('chgrp: ' + f + ': No such file')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (typeof ctx.vfs.chown !== 'function') {
|
||||||
|
ctx.console.error('chgrp: not supported by this VFS')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await ctx.vfs.chown(f, {
|
||||||
|
uid: st.uid,
|
||||||
|
gid,
|
||||||
|
uname: st.user,
|
||||||
|
gname
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
ctx.console.error('chgrp: ' + f + ': ' + (e.message || e))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,153 @@
|
|||||||
async function run(ctx, argv) {
|
/**
|
||||||
ctx.console.error(
|
* chown — change file owner and group (Hyperdrive metadata on writable paths).
|
||||||
'chown: changing file ownership is not supported on Bare OS (single-user Hyperdrive metadata).'
|
*/
|
||||||
)
|
|
||||||
ctx.exitCode = 1
|
/**
|
||||||
|
* @param {string} spec
|
||||||
|
* @param {Record<string, string>} env
|
||||||
|
* @returns {{ uid: number | null, gid: number | null }}
|
||||||
|
*/
|
||||||
|
function parseIdSpec(spec, env) {
|
||||||
|
const s = String(spec).trim()
|
||||||
|
if (!s) return { uid: null, gid: null }
|
||||||
|
if (/^\d+$/.test(s)) {
|
||||||
|
const n = Number.parseInt(s, 10)
|
||||||
|
return { uid: Number.isFinite(n) ? n : null, gid: null }
|
||||||
|
}
|
||||||
|
const u = (env.USER || env.LOGNAME || '').trim()
|
||||||
|
if (s === 'root') return { uid: 0, gid: null }
|
||||||
|
if (s === 'guest' || s === 'nobody') return { uid: 65534, gid: null }
|
||||||
|
if (u && s === u) {
|
||||||
|
const uid = Number.parseInt(String(env.UID ?? '65534'), 10)
|
||||||
|
return { uid: Number.isFinite(uid) ? uid : 65534, gid: null }
|
||||||
|
}
|
||||||
|
return { uid: null, gid: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} spec
|
||||||
|
* @param {Record<string, string>} env
|
||||||
|
* @returns {number | null}
|
||||||
|
*/
|
||||||
|
function parseGroupSpec(spec, env) {
|
||||||
|
const s = String(spec).trim()
|
||||||
|
if (!s) return null
|
||||||
|
if (/^\d+$/.test(s)) {
|
||||||
|
const n = Number.parseInt(s, 10)
|
||||||
|
return Number.isFinite(n) ? n : null
|
||||||
|
}
|
||||||
|
const g = (env.GROUP || env.USER || 'guest').trim()
|
||||||
|
if (s === 'root') return 0
|
||||||
|
if (s === 'guest' || s === 'nobody') return 65534
|
||||||
|
if (g && s === g) {
|
||||||
|
const gid = Number.parseInt(String(env.GID ?? '65534'), 10)
|
||||||
|
return Number.isFinite(gid) ? gid : 65534
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(ctx, argv) {
|
||||||
|
let i = 1
|
||||||
|
const files = []
|
||||||
|
for (; i < argv.length; i++) {
|
||||||
|
const a = argv[i]
|
||||||
|
if (a === '-h' || a === '--help') {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: chown [-h] OWNER[:GROUP] FILE...\n' +
|
||||||
|
'OWNER/GROUP may be numeric ids or root/guest/nobody/current USER/GROUP.'
|
||||||
|
)
|
||||||
|
ctx.exitCode = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!a.startsWith('-')) break
|
||||||
|
ctx.console.error('chown: unsupported option: ' + a)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const ownSpec = argv[i++]
|
||||||
|
if (!ownSpec || i >= argv.length) {
|
||||||
|
ctx.console.error('usage: chown OWNER[:GROUP] FILE...')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (; i < argv.length; i++) files.push(argv[i])
|
||||||
|
const env = ctx.vfs.env
|
||||||
|
const colon = ownSpec.indexOf(':')
|
||||||
|
let uid
|
||||||
|
let gid
|
||||||
|
let uname
|
||||||
|
let gname
|
||||||
|
if (colon === 0 && ownSpec.length > 1) {
|
||||||
|
const gPart = ownSpec.slice(1)
|
||||||
|
gid = parseGroupSpec(gPart, env)
|
||||||
|
gname = /^\d+$/.test(gPart.trim()) ? undefined : gPart.trim() || undefined
|
||||||
|
if (gid == null) {
|
||||||
|
ctx.console.error('chown: invalid group: ' + gPart)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const f of files) {
|
||||||
|
try {
|
||||||
|
if (typeof ctx.vfs.chown !== 'function') {
|
||||||
|
ctx.console.error('chown: not supported by this VFS')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const st = await ctx.vfs.stat(f)
|
||||||
|
if (!st) {
|
||||||
|
ctx.console.error('chown: ' + f + ': No such file')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await ctx.vfs.chown(f, {
|
||||||
|
uid: st.uid,
|
||||||
|
gid,
|
||||||
|
uname: st.user,
|
||||||
|
gname
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
ctx.console.error('chown: ' + f + ': ' + (e.message || e))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (colon >= 0) {
|
||||||
|
const uPart = ownSpec.slice(0, colon)
|
||||||
|
const gPart = ownSpec.slice(colon + 1)
|
||||||
|
const pu = parseIdSpec(uPart, env)
|
||||||
|
uid = pu.uid
|
||||||
|
uname = /^\d+$/.test(uPart.trim()) ? undefined : uPart.trim() || undefined
|
||||||
|
if (gPart === '') {
|
||||||
|
gid = Number.parseInt(String(env.GID ?? '65534'), 10)
|
||||||
|
gname = env.GROUP || env.USER
|
||||||
|
} else {
|
||||||
|
gid = parseGroupSpec(gPart, env)
|
||||||
|
gname = /^\d+$/.test(gPart.trim()) ? undefined : gPart.trim() || undefined
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const pu = parseIdSpec(ownSpec, env)
|
||||||
|
uid = pu.uid
|
||||||
|
uname = /^\d+$/.test(ownSpec.trim()) ? undefined : ownSpec.trim() || undefined
|
||||||
|
gid = Number.parseInt(String(env.GID ?? '65534'), 10)
|
||||||
|
gname = env.GROUP || env.USER
|
||||||
|
}
|
||||||
|
if (uid == null || gid == null) {
|
||||||
|
ctx.console.error('chown: invalid owner or group: ' + ownSpec)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const f of files) {
|
||||||
|
try {
|
||||||
|
if (typeof ctx.vfs.chown !== 'function') {
|
||||||
|
ctx.console.error('chown: not supported by this VFS')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await ctx.vfs.chown(f, { uid, gid, uname, gname })
|
||||||
|
} catch (e) {
|
||||||
|
ctx.console.error('chown: ' + f + ': ' + (e.message || e))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* curDepth is distance from the search root directory (0 at the initial path).
|
* curDepth is distance from the search root directory (0 at the initial path).
|
||||||
* Printed paths use gnu-like depth curDepth + 1 (immediate children of the root are depth 1).
|
* Printed paths use gnu-like depth curDepth + 1 (immediate children of the root are depth 1).
|
||||||
*/
|
*/
|
||||||
async function walk(ctx, dir, nameRe, wantType, maxDepth, minDepth, curDepth) {
|
async function walk(ctx, dir, nameRe, pathRe, wantType, maxDepth, minDepth, curDepth) {
|
||||||
if (maxDepth >= 0 && curDepth > maxDepth) return
|
if (maxDepth >= 0 && curDepth > maxDepth) return
|
||||||
let names
|
let names
|
||||||
try {
|
try {
|
||||||
@@ -21,13 +21,24 @@ async function walk(ctx, dir, nameRe, wantType, maxDepth, minDepth, curDepth) {
|
|||||||
}
|
}
|
||||||
if (!st) continue
|
if (!st) continue
|
||||||
const gnuDepth = curDepth + 1
|
const gnuDepth = curDepth + 1
|
||||||
if (!nameRe || nameRe.test(n)) {
|
const pathOk = !pathRe || pathRe.test(path)
|
||||||
|
const nameOk = !nameRe || nameRe.test(n)
|
||||||
|
if (pathOk && nameOk) {
|
||||||
if (!wantType || st.type === wantType) {
|
if (!wantType || st.type === wantType) {
|
||||||
if (gnuDepth >= minDepth) ctx.console.log(path)
|
if (gnuDepth >= minDepth) ctx.console.log(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (st.type === 'directory')
|
if (st.type === 'directory')
|
||||||
await walk(ctx, path, nameRe, wantType, maxDepth, minDepth, curDepth + 1)
|
await walk(
|
||||||
|
ctx,
|
||||||
|
path,
|
||||||
|
nameRe,
|
||||||
|
pathRe,
|
||||||
|
wantType,
|
||||||
|
maxDepth,
|
||||||
|
minDepth,
|
||||||
|
curDepth + 1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +48,8 @@ async function run(ctx, argv) {
|
|||||||
let minDepth = 1
|
let minDepth = 1
|
||||||
/** @type {string | null} */
|
/** @type {string | null} */
|
||||||
let nameGlob = null
|
let nameGlob = null
|
||||||
|
/** @type {string | null} */
|
||||||
|
let pathGlob = null
|
||||||
/** @type {'file' | 'directory' | 'symlink' | null} */
|
/** @type {'file' | 'directory' | 'symlink' | null} */
|
||||||
let wantType = null
|
let wantType = null
|
||||||
const rest = []
|
const rest = []
|
||||||
@@ -51,6 +64,10 @@ async function run(ctx, argv) {
|
|||||||
if (!Number.isFinite(minDepth) || minDepth < 1) minDepth = 1
|
if (!Number.isFinite(minDepth) || minDepth < 1) minDepth = 1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (a === '-path' && argv[i + 1]) {
|
||||||
|
pathGlob = argv[++i]
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (a === '-name' && argv[i + 1]) {
|
if (a === '-name' && argv[i + 1]) {
|
||||||
nameGlob = argv[++i]
|
nameGlob = argv[++i]
|
||||||
continue
|
continue
|
||||||
@@ -82,6 +99,16 @@ async function run(ctx, argv) {
|
|||||||
.replace(/\?/g, '.')
|
.replace(/\?/g, '.')
|
||||||
nameRe = new RegExp('^' + esc + '$')
|
nameRe = new RegExp('^' + esc + '$')
|
||||||
}
|
}
|
||||||
|
let pathRe = null
|
||||||
|
if (pathGlob) {
|
||||||
|
const esc = pathGlob
|
||||||
|
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
||||||
|
.replace(/\*\*/g, '\0GLOBSTAR\0')
|
||||||
|
.replace(/\*/g, '[^/]*')
|
||||||
|
.replace(/\?/g, '[^/]')
|
||||||
|
.replace(/\0GLOBSTAR\0/g, '.*')
|
||||||
|
pathRe = new RegExp('^' + esc + '$')
|
||||||
|
}
|
||||||
const abs = ctx.vfs.resolveLogical(root)
|
const abs = ctx.vfs.resolveLogical(root)
|
||||||
await walk(ctx, abs, nameRe, wantType, maxDepth, minDepth, 0)
|
await walk(ctx, abs, nameRe, pathRe, wantType, maxDepth, minDepth, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,33 @@ export function setupSeedChannel(mux, localRAM, replicateDrive) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (m.module === 'bare_os' && m.method === 'health') {
|
||||||
|
chan.messages[6].send({
|
||||||
|
id: m.id,
|
||||||
|
success: true,
|
||||||
|
result: JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
protocol: PROTOCOL_NAME,
|
||||||
|
role: 'seeder'
|
||||||
|
}),
|
||||||
|
error: ''
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (m.module === 'bare_os' && m.method === 'kernel_info') {
|
||||||
|
chan.messages[6].send({
|
||||||
|
id: m.id,
|
||||||
|
success: true,
|
||||||
|
result: JSON.stringify({
|
||||||
|
protocol: PROTOCOL_NAME,
|
||||||
|
rpc: ['bare_os.version', 'bare_os.health', 'bare_os.kernel_info'],
|
||||||
|
note:
|
||||||
|
'Kernel and image semver are resolved at boot on the booter; this channel is seeder-local.'
|
||||||
|
}),
|
||||||
|
error: ''
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
chan.messages[6].send({
|
chan.messages[6].send({
|
||||||
id: m.id,
|
id: m.id,
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Files in this directory are **read from disk by the seeder** (or copied into `pa
|
|||||||
- **`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)).
|
- **`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`, …).
|
- **`etc/os-release`** — Static OS metadata (`NAME`, `VERSION`, …).
|
||||||
- **`etc/motd`** — Optional message printed after **`os-release`** (distributors can customize).
|
- **`etc/motd`** — Optional message printed after **`os-release`** (distributors can customize).
|
||||||
- **`etc/bare-os/banner`** or **`/etc/issue`** — If present on the system drive, the default kernel prints one of these instead of the built-in session hint (unless **`BARE_OS_SKIP_REPL`** shortens the banner). Set **`BARE_OS_BOOT_TRACE=1`** or **`true`** for **`[boot] phase: Nms`** lines on stderr, or **`json`** for **`{"phase":"…","ms":n}`** per phase.
|
- **`etc/bare-os/banner`** or **`/etc/issue`** — If present on the system drive, the default kernel prints one of these instead of the built-in session hint (unless **`BARE_OS_SKIP_REPL`** shortens the banner). Set **`BARE_OS_BOOT_TRACE=1`** or **`true`** for **`[boot] phase: Nms`** lines on stderr, **`json`** for **`{"phase":"…","ms":n}`** per phase, or **`ndjson`** for machine-readable lines with **`sessionId`**. Recovery: **`BARE_OS_BOOT_MINIMAL`**, granular **`BARE_OS_BOOT_SKIP`**, optional **`BARE_OS_KERNEL_SELFTEST`**, readiness via **`ctx.bareOsPublishBootReady`** → **`/run/bare-os/ready`** and **`/run/bare-os/boot.json`** (see [handbook ch.6](../handbook/06-kernel-and-binaries.md)).
|
||||||
- **`etc/bare-os/rc`** — Optional boot snippet: one **`execLine`** per non-comment line (trusted).
|
- **`etc/bare-os/rc`** — Optional boot snippet: one **`execLine`** per non-comment line (trusted).
|
||||||
- **`etc/bare-os/rc.d/`** — Optional extra snippets (basename must start with a digit), same line rules, run after **`rc`** in filename order. Human-oriented notes live in **`.README`** (a dotfile so legacy **`init.js`** never executes it).
|
- **`etc/bare-os/rc.d/`** — Optional extra snippets (basename must start with a digit), same line rules, run after **`rc`** in filename order. Human-oriented notes live in **`.README`** (a dotfile so legacy **`init.js`** never executes it).
|
||||||
|
|
||||||
|
|||||||
@@ -60,9 +60,88 @@ function barePosixBlocks(size) {
|
|||||||
return Math.ceil(Number(size) / 512) || 0
|
return Math.ceil(Number(size) / 512) || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
async function run(ctx, argv) {
|
/**
|
||||||
ctx.console.error(
|
* chgrp — change file group (Hyperdrive metadata on writable paths).
|
||||||
'chgrp: changing group is not supported on Bare OS (single-user Hyperdrive metadata).'
|
*/
|
||||||
)
|
|
||||||
ctx.exitCode = 1
|
/**
|
||||||
|
* @param {string} spec
|
||||||
|
* @param {Record<string, string>} env
|
||||||
|
* @returns {number | null}
|
||||||
|
*/
|
||||||
|
function parseGroupSpec(spec, env) {
|
||||||
|
const s = String(spec).trim()
|
||||||
|
if (!s) return null
|
||||||
|
if (/^\d+$/.test(s)) {
|
||||||
|
const n = Number.parseInt(s, 10)
|
||||||
|
return Number.isFinite(n) ? n : null
|
||||||
|
}
|
||||||
|
const g = (env.GROUP || env.USER || 'guest').trim()
|
||||||
|
if (s === 'root') return 0
|
||||||
|
if (s === 'guest' || s === 'nobody') return 65534
|
||||||
|
if (g && s === g) {
|
||||||
|
const gid = Number.parseInt(String(env.GID ?? '65534'), 10)
|
||||||
|
return Number.isFinite(gid) ? gid : 65534
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(ctx, argv) {
|
||||||
|
let i = 1
|
||||||
|
const files = []
|
||||||
|
for (; i < argv.length; i++) {
|
||||||
|
const a = argv[i]
|
||||||
|
if (a === '-h' || a === '--help') {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: chgrp [-h] GROUP FILE...\n' +
|
||||||
|
'GROUP may be a numeric gid or root/guest/nobody/current GROUP name.'
|
||||||
|
)
|
||||||
|
ctx.exitCode = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!a.startsWith('-')) break
|
||||||
|
ctx.console.error('chgrp: unsupported option: ' + a)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const groupSpec = argv[i++]
|
||||||
|
if (!groupSpec || i >= argv.length) {
|
||||||
|
ctx.console.error('usage: chgrp GROUP FILE...')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (; i < argv.length; i++) files.push(argv[i])
|
||||||
|
const gid = parseGroupSpec(groupSpec, ctx.vfs.env)
|
||||||
|
if (gid == null) {
|
||||||
|
ctx.console.error('chgrp: invalid group: ' + groupSpec)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const gname = /^\d+$/.test(groupSpec.trim())
|
||||||
|
? undefined
|
||||||
|
: groupSpec.trim() || undefined
|
||||||
|
for (const f of files) {
|
||||||
|
try {
|
||||||
|
const st = await ctx.vfs.stat(f)
|
||||||
|
if (!st) {
|
||||||
|
ctx.console.error('chgrp: ' + f + ': No such file')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (typeof ctx.vfs.chown !== 'function') {
|
||||||
|
ctx.console.error('chgrp: not supported by this VFS')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await ctx.vfs.chown(f, {
|
||||||
|
uid: st.uid,
|
||||||
|
gid,
|
||||||
|
uname: st.user,
|
||||||
|
gname
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
ctx.console.error('chgrp: ' + f + ': ' + (e.message || e))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,9 +60,156 @@ function barePosixBlocks(size) {
|
|||||||
return Math.ceil(Number(size) / 512) || 0
|
return Math.ceil(Number(size) / 512) || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
async function run(ctx, argv) {
|
/**
|
||||||
ctx.console.error(
|
* chown — change file owner and group (Hyperdrive metadata on writable paths).
|
||||||
'chown: changing file ownership is not supported on Bare OS (single-user Hyperdrive metadata).'
|
*/
|
||||||
)
|
|
||||||
ctx.exitCode = 1
|
/**
|
||||||
|
* @param {string} spec
|
||||||
|
* @param {Record<string, string>} env
|
||||||
|
* @returns {{ uid: number | null, gid: number | null }}
|
||||||
|
*/
|
||||||
|
function parseIdSpec(spec, env) {
|
||||||
|
const s = String(spec).trim()
|
||||||
|
if (!s) return { uid: null, gid: null }
|
||||||
|
if (/^\d+$/.test(s)) {
|
||||||
|
const n = Number.parseInt(s, 10)
|
||||||
|
return { uid: Number.isFinite(n) ? n : null, gid: null }
|
||||||
|
}
|
||||||
|
const u = (env.USER || env.LOGNAME || '').trim()
|
||||||
|
if (s === 'root') return { uid: 0, gid: null }
|
||||||
|
if (s === 'guest' || s === 'nobody') return { uid: 65534, gid: null }
|
||||||
|
if (u && s === u) {
|
||||||
|
const uid = Number.parseInt(String(env.UID ?? '65534'), 10)
|
||||||
|
return { uid: Number.isFinite(uid) ? uid : 65534, gid: null }
|
||||||
|
}
|
||||||
|
return { uid: null, gid: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} spec
|
||||||
|
* @param {Record<string, string>} env
|
||||||
|
* @returns {number | null}
|
||||||
|
*/
|
||||||
|
function parseGroupSpec(spec, env) {
|
||||||
|
const s = String(spec).trim()
|
||||||
|
if (!s) return null
|
||||||
|
if (/^\d+$/.test(s)) {
|
||||||
|
const n = Number.parseInt(s, 10)
|
||||||
|
return Number.isFinite(n) ? n : null
|
||||||
|
}
|
||||||
|
const g = (env.GROUP || env.USER || 'guest').trim()
|
||||||
|
if (s === 'root') return 0
|
||||||
|
if (s === 'guest' || s === 'nobody') return 65534
|
||||||
|
if (g && s === g) {
|
||||||
|
const gid = Number.parseInt(String(env.GID ?? '65534'), 10)
|
||||||
|
return Number.isFinite(gid) ? gid : 65534
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(ctx, argv) {
|
||||||
|
let i = 1
|
||||||
|
const files = []
|
||||||
|
for (; i < argv.length; i++) {
|
||||||
|
const a = argv[i]
|
||||||
|
if (a === '-h' || a === '--help') {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: chown [-h] OWNER[:GROUP] FILE...\n' +
|
||||||
|
'OWNER/GROUP may be numeric ids or root/guest/nobody/current USER/GROUP.'
|
||||||
|
)
|
||||||
|
ctx.exitCode = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!a.startsWith('-')) break
|
||||||
|
ctx.console.error('chown: unsupported option: ' + a)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const ownSpec = argv[i++]
|
||||||
|
if (!ownSpec || i >= argv.length) {
|
||||||
|
ctx.console.error('usage: chown OWNER[:GROUP] FILE...')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (; i < argv.length; i++) files.push(argv[i])
|
||||||
|
const env = ctx.vfs.env
|
||||||
|
const colon = ownSpec.indexOf(':')
|
||||||
|
let uid
|
||||||
|
let gid
|
||||||
|
let uname
|
||||||
|
let gname
|
||||||
|
if (colon === 0 && ownSpec.length > 1) {
|
||||||
|
const gPart = ownSpec.slice(1)
|
||||||
|
gid = parseGroupSpec(gPart, env)
|
||||||
|
gname = /^\d+$/.test(gPart.trim()) ? undefined : gPart.trim() || undefined
|
||||||
|
if (gid == null) {
|
||||||
|
ctx.console.error('chown: invalid group: ' + gPart)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const f of files) {
|
||||||
|
try {
|
||||||
|
if (typeof ctx.vfs.chown !== 'function') {
|
||||||
|
ctx.console.error('chown: not supported by this VFS')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const st = await ctx.vfs.stat(f)
|
||||||
|
if (!st) {
|
||||||
|
ctx.console.error('chown: ' + f + ': No such file')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await ctx.vfs.chown(f, {
|
||||||
|
uid: st.uid,
|
||||||
|
gid,
|
||||||
|
uname: st.user,
|
||||||
|
gname
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
ctx.console.error('chown: ' + f + ': ' + (e.message || e))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (colon >= 0) {
|
||||||
|
const uPart = ownSpec.slice(0, colon)
|
||||||
|
const gPart = ownSpec.slice(colon + 1)
|
||||||
|
const pu = parseIdSpec(uPart, env)
|
||||||
|
uid = pu.uid
|
||||||
|
uname = /^\d+$/.test(uPart.trim()) ? undefined : uPart.trim() || undefined
|
||||||
|
if (gPart === '') {
|
||||||
|
gid = Number.parseInt(String(env.GID ?? '65534'), 10)
|
||||||
|
gname = env.GROUP || env.USER
|
||||||
|
} else {
|
||||||
|
gid = parseGroupSpec(gPart, env)
|
||||||
|
gname = /^\d+$/.test(gPart.trim()) ? undefined : gPart.trim() || undefined
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const pu = parseIdSpec(ownSpec, env)
|
||||||
|
uid = pu.uid
|
||||||
|
uname = /^\d+$/.test(ownSpec.trim()) ? undefined : ownSpec.trim() || undefined
|
||||||
|
gid = Number.parseInt(String(env.GID ?? '65534'), 10)
|
||||||
|
gname = env.GROUP || env.USER
|
||||||
|
}
|
||||||
|
if (uid == null || gid == null) {
|
||||||
|
ctx.console.error('chown: invalid owner or group: ' + ownSpec)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const f of files) {
|
||||||
|
try {
|
||||||
|
if (typeof ctx.vfs.chown !== 'function') {
|
||||||
|
ctx.console.error('chown: not supported by this VFS')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await ctx.vfs.chown(f, { uid, gid, uname, gname })
|
||||||
|
} catch (e) {
|
||||||
|
ctx.console.error('chown: ' + f + ': ' + (e.message || e))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function barePosixBlocks(size) {
|
|||||||
* curDepth is distance from the search root directory (0 at the initial path).
|
* curDepth is distance from the search root directory (0 at the initial path).
|
||||||
* Printed paths use gnu-like depth curDepth + 1 (immediate children of the root are depth 1).
|
* Printed paths use gnu-like depth curDepth + 1 (immediate children of the root are depth 1).
|
||||||
*/
|
*/
|
||||||
async function walk(ctx, dir, nameRe, wantType, maxDepth, minDepth, curDepth) {
|
async function walk(ctx, dir, nameRe, pathRe, wantType, maxDepth, minDepth, curDepth) {
|
||||||
if (maxDepth >= 0 && curDepth > maxDepth) return
|
if (maxDepth >= 0 && curDepth > maxDepth) return
|
||||||
let names
|
let names
|
||||||
try {
|
try {
|
||||||
@@ -83,13 +83,24 @@ async function walk(ctx, dir, nameRe, wantType, maxDepth, minDepth, curDepth) {
|
|||||||
}
|
}
|
||||||
if (!st) continue
|
if (!st) continue
|
||||||
const gnuDepth = curDepth + 1
|
const gnuDepth = curDepth + 1
|
||||||
if (!nameRe || nameRe.test(n)) {
|
const pathOk = !pathRe || pathRe.test(path)
|
||||||
|
const nameOk = !nameRe || nameRe.test(n)
|
||||||
|
if (pathOk && nameOk) {
|
||||||
if (!wantType || st.type === wantType) {
|
if (!wantType || st.type === wantType) {
|
||||||
if (gnuDepth >= minDepth) ctx.console.log(path)
|
if (gnuDepth >= minDepth) ctx.console.log(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (st.type === 'directory')
|
if (st.type === 'directory')
|
||||||
await walk(ctx, path, nameRe, wantType, maxDepth, minDepth, curDepth + 1)
|
await walk(
|
||||||
|
ctx,
|
||||||
|
path,
|
||||||
|
nameRe,
|
||||||
|
pathRe,
|
||||||
|
wantType,
|
||||||
|
maxDepth,
|
||||||
|
minDepth,
|
||||||
|
curDepth + 1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +110,8 @@ async function run(ctx, argv) {
|
|||||||
let minDepth = 1
|
let minDepth = 1
|
||||||
/** @type {string | null} */
|
/** @type {string | null} */
|
||||||
let nameGlob = null
|
let nameGlob = null
|
||||||
|
/** @type {string | null} */
|
||||||
|
let pathGlob = null
|
||||||
/** @type {'file' | 'directory' | 'symlink' | null} */
|
/** @type {'file' | 'directory' | 'symlink' | null} */
|
||||||
let wantType = null
|
let wantType = null
|
||||||
const rest = []
|
const rest = []
|
||||||
@@ -113,6 +126,10 @@ async function run(ctx, argv) {
|
|||||||
if (!Number.isFinite(minDepth) || minDepth < 1) minDepth = 1
|
if (!Number.isFinite(minDepth) || minDepth < 1) minDepth = 1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (a === '-path' && argv[i + 1]) {
|
||||||
|
pathGlob = argv[++i]
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (a === '-name' && argv[i + 1]) {
|
if (a === '-name' && argv[i + 1]) {
|
||||||
nameGlob = argv[++i]
|
nameGlob = argv[++i]
|
||||||
continue
|
continue
|
||||||
@@ -144,6 +161,16 @@ async function run(ctx, argv) {
|
|||||||
.replace(/\?/g, '.')
|
.replace(/\?/g, '.')
|
||||||
nameRe = new RegExp('^' + esc + '$')
|
nameRe = new RegExp('^' + esc + '$')
|
||||||
}
|
}
|
||||||
|
let pathRe = null
|
||||||
|
if (pathGlob) {
|
||||||
|
const esc = pathGlob
|
||||||
|
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
||||||
|
.replace(/\*\*/g, '\0GLOBSTAR\0')
|
||||||
|
.replace(/\*/g, '[^/]*')
|
||||||
|
.replace(/\?/g, '[^/]')
|
||||||
|
.replace(/\0GLOBSTAR\0/g, '.*')
|
||||||
|
pathRe = new RegExp('^' + esc + '$')
|
||||||
|
}
|
||||||
const abs = ctx.vfs.resolveLogical(root)
|
const abs = ctx.vfs.resolveLogical(root)
|
||||||
await walk(ctx, abs, nameRe, wantType, maxDepth, minDepth, 0)
|
await walk(ctx, abs, nameRe, pathRe, wantType, maxDepth, minDepth, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,13 @@
|
|||||||
* in file order (trusted). Then readLine yields EOF.
|
* in file order (trusted). Then readLine yields EOF.
|
||||||
*
|
*
|
||||||
* BARE_OS_BOOT_TRACE=json logs one JSON object per phase on stderr: {"phase":"…","ms":n}.
|
* BARE_OS_BOOT_TRACE=json logs one JSON object per phase on stderr: {"phase":"…","ms":n}.
|
||||||
|
* BARE_OS_BOOT_TRACE=ndjson logs {"type":"boot","phase","ms","sessionId","ts"} per line.
|
||||||
|
*
|
||||||
|
* BARE_OS_BOOT_MINIMAL=1 or true: skip profile rc, rc, rc.d, rc.local, kernel.d (recovery shell).
|
||||||
|
* BARE_OS_BOOT_SKIP: comma-separated phases to skip among:
|
||||||
|
* profile, rc, rc.d, rc.local, kernel.d, onboot (os-release and motd always run).
|
||||||
|
*
|
||||||
|
* BARE_OS_KERNEL_SELFTEST=1 or true: after boot snippets, run a short trusted self-check via execLine.
|
||||||
*
|
*
|
||||||
* BARE_OS_BOOT_STRICT=1 or true: first execLine throw in trusted boot snippets calls
|
* BARE_OS_BOOT_STRICT=1 or true: first execLine throw in trusted boot snippets calls
|
||||||
* requestBooterExit(1) and stops further boot phases.
|
* requestBooterExit(1) and stops further boot phases.
|
||||||
@@ -30,7 +37,12 @@
|
|||||||
*/
|
*/
|
||||||
function wantBootTrace(ctx) {
|
function wantBootTrace(ctx) {
|
||||||
const v = ctx.env && ctx.env.BARE_OS_BOOT_TRACE
|
const v = ctx.env && ctx.env.BARE_OS_BOOT_TRACE
|
||||||
return v === '1' || v === 'true' || v === 'json'
|
return (
|
||||||
|
v === '1' ||
|
||||||
|
v === 'true' ||
|
||||||
|
v === 'json' ||
|
||||||
|
v === 'ndjson'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +52,56 @@ function isBootTraceJson(ctx) {
|
|||||||
return ctx.env && ctx.env.BARE_OS_BOOT_TRACE === 'json'
|
return ctx.env && ctx.env.BARE_OS_BOOT_TRACE === 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
*/
|
||||||
|
function isBootTraceNdjson(ctx) {
|
||||||
|
return ctx.env && ctx.env.BARE_OS_BOOT_TRACE === 'ndjson'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
*/
|
||||||
|
function bootMinimal(ctx) {
|
||||||
|
const v = ctx.env && ctx.env.BARE_OS_BOOT_MINIMAL
|
||||||
|
return v === '1' || v === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
* @returns {Set<string>}
|
||||||
|
*/
|
||||||
|
function parseBootSkip(ctx) {
|
||||||
|
const raw = ctx.env && ctx.env.BARE_OS_BOOT_SKIP
|
||||||
|
const out = new Set()
|
||||||
|
if (raw == null || !String(raw).trim()) return out
|
||||||
|
for (const p of String(raw)
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim().toLowerCase())
|
||||||
|
.filter(Boolean)) {
|
||||||
|
out.add(p)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
* @param {string} phase
|
||||||
|
*/
|
||||||
|
function shouldSkipBootPhase(ctx, phase) {
|
||||||
|
if (bootMinimal(ctx)) {
|
||||||
|
return (
|
||||||
|
phase === 'profile' ||
|
||||||
|
phase === 'rc' ||
|
||||||
|
phase === 'rc.d' ||
|
||||||
|
phase === 'rc.local' ||
|
||||||
|
phase === 'kernel.d' ||
|
||||||
|
phase === 'onboot'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return parseBootSkip(ctx).has(phase.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Record<string, unknown>} ctx
|
* @param {Record<string, unknown>} ctx
|
||||||
*/
|
*/
|
||||||
@@ -80,13 +142,26 @@ function shouldSkipRcDName(name, patterns) {
|
|||||||
* @param {Record<string, unknown>} ctx
|
* @param {Record<string, unknown>} ctx
|
||||||
* @param {string} label
|
* @param {string} label
|
||||||
* @param {() => void | Promise<void>} fn
|
* @param {() => void | Promise<void>} fn
|
||||||
|
* @param {string[]} phaseLog
|
||||||
*/
|
*/
|
||||||
async function bootTimed(ctx, label, fn) {
|
async function bootTimed(ctx, label, fn, phaseLog) {
|
||||||
const t0 = Date.now()
|
const t0 = Date.now()
|
||||||
await fn()
|
await fn()
|
||||||
|
const ms = Date.now() - t0
|
||||||
|
phaseLog.push(label)
|
||||||
if (wantBootTrace(ctx)) {
|
if (wantBootTrace(ctx)) {
|
||||||
const ms = Date.now() - t0
|
if (isBootTraceNdjson(ctx)) {
|
||||||
if (isBootTraceJson(ctx)) {
|
const sid =
|
||||||
|
(ctx.env && ctx.env.BARE_OS_SESSION_ID) || ''
|
||||||
|
const rec = {
|
||||||
|
type: 'boot',
|
||||||
|
phase: label,
|
||||||
|
ms,
|
||||||
|
sessionId: sid,
|
||||||
|
ts: Date.now()
|
||||||
|
}
|
||||||
|
ctx.console.error(JSON.stringify(rec))
|
||||||
|
} else if (isBootTraceJson(ctx)) {
|
||||||
ctx.console.error(JSON.stringify({ phase: label, ms }))
|
ctx.console.error(JSON.stringify({ phase: label, ms }))
|
||||||
} else {
|
} else {
|
||||||
ctx.console.error(`[boot] ${label}: ${ms}ms`)
|
ctx.console.error(`[boot] ${label}: ${ms}ms`)
|
||||||
@@ -348,7 +423,7 @@ async function printSessionBanner(ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const defaultBanner =
|
const defaultBanner =
|
||||||
'Bare operating system — guest session (login [--new] <passphrase> to unlock) | shell: cd, export, if/fi, && || ;, |, exit | services: systemctl list-units, journalctl -u UNIT | try: help, ls /bin, crontab -l'
|
'Bare operating system — guest session (login [--new] <passphrase> to unlock) | shell: cd, export, if/fi, && || ;, |, &, jobs/fg, <<<, exit | services: systemctl list-units, journalctl -u UNIT | try: help, ls /bin, crontab -l'
|
||||||
if (ctx.bareOsSkipRepl) {
|
if (ctx.bareOsSkipRepl) {
|
||||||
console.log(
|
console.log(
|
||||||
'Bare operating system — non-interactive session (BARE_OS_SKIP_REPL).'
|
'Bare operating system — non-interactive session (BARE_OS_SKIP_REPL).'
|
||||||
@@ -358,38 +433,115 @@ async function printSessionBanner(ctx) {
|
|||||||
console.log(defaultBanner)
|
console.log(defaultBanner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async function runKernelSelftest(ctx) {
|
||||||
|
const v = ctx.env && ctx.env.BARE_OS_KERNEL_SELFTEST
|
||||||
|
if (v !== '1' && v !== 'true') return true
|
||||||
|
const { execLine, console } = ctx
|
||||||
|
const strict = bootStrict(ctx)
|
||||||
|
const lines = [
|
||||||
|
':',
|
||||||
|
'true',
|
||||||
|
'false; echo selftest_false_ok',
|
||||||
|
'systemctl list-units 2>/dev/null || true'
|
||||||
|
]
|
||||||
|
for (const line of lines) {
|
||||||
|
try {
|
||||||
|
await execLine(line)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('selftest: ' + ((e && e.message) || String(e)))
|
||||||
|
if (strict) {
|
||||||
|
if (typeof ctx.requestBooterExit === 'function') ctx.requestBooterExit(1)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
* @param {string[]} phaseLog
|
||||||
|
*/
|
||||||
|
function publishBootReady(ctx, phaseLog) {
|
||||||
|
if (typeof ctx.bareOsPublishBootReady !== 'function') return
|
||||||
|
const sid = (ctx.env && ctx.env.BARE_OS_SESSION_ID) || ''
|
||||||
|
ctx.bareOsPublishBootReady({
|
||||||
|
ready: true,
|
||||||
|
phases: [...phaseLog],
|
||||||
|
sessionId: sid,
|
||||||
|
completedAtMs: Date.now(),
|
||||||
|
minimal: bootMinimal(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function start(ctx) {
|
async function start(ctx) {
|
||||||
const { readLine, execLine, console } = ctx
|
const { readLine, execLine, console } = ctx
|
||||||
await bootTimed(ctx, 'os-release', () => printOsRelease(ctx))
|
/** @type {string[]} */
|
||||||
await bootTimed(ctx, 'motd', () => printMotd(ctx))
|
const phaseLog = []
|
||||||
|
await bootTimed(ctx, 'os-release', () => printOsRelease(ctx), phaseLog)
|
||||||
|
await bootTimed(ctx, 'motd', () => printMotd(ctx), phaseLog)
|
||||||
const profileName = await resolveBootProfileName(ctx)
|
const profileName = await resolveBootProfileName(ctx)
|
||||||
/** @type {boolean} */
|
/** @type {boolean} */
|
||||||
let bootOk = true
|
let bootOk = true
|
||||||
await bootTimed(ctx, 'rc.profile', async () => {
|
if (!shouldSkipBootPhase(ctx, 'profile')) {
|
||||||
bootOk = await runProfileRc(ctx, profileName)
|
await bootTimed(ctx, 'rc.profile', async () => {
|
||||||
})
|
bootOk = await runProfileRc(ctx, profileName)
|
||||||
|
}, phaseLog)
|
||||||
|
} else {
|
||||||
|
phaseLog.push('rc.profile(skipped)')
|
||||||
|
}
|
||||||
if (!bootOk) return
|
if (!bootOk) return
|
||||||
await bootTimed(ctx, 'rc', async () => {
|
if (!shouldSkipBootPhase(ctx, 'rc')) {
|
||||||
bootOk = await runRcFileAt(ctx, '/etc/bare-os/rc', 'rc')
|
await bootTimed(ctx, 'rc', async () => {
|
||||||
})
|
bootOk = await runRcFileAt(ctx, '/etc/bare-os/rc', 'rc')
|
||||||
|
}, phaseLog)
|
||||||
|
} else {
|
||||||
|
phaseLog.push('rc(skipped)')
|
||||||
|
}
|
||||||
if (!bootOk) return
|
if (!bootOk) return
|
||||||
await bootTimed(ctx, 'rc.d', async () => {
|
if (!shouldSkipBootPhase(ctx, 'rc.d')) {
|
||||||
bootOk = await runBareOsRcDir(ctx)
|
await bootTimed(ctx, 'rc.d', async () => {
|
||||||
})
|
bootOk = await runBareOsRcDir(ctx)
|
||||||
|
}, phaseLog)
|
||||||
|
} else {
|
||||||
|
phaseLog.push('rc.d(skipped)')
|
||||||
|
}
|
||||||
if (!bootOk) return
|
if (!bootOk) return
|
||||||
await bootTimed(ctx, 'rc.local', async () => {
|
if (!shouldSkipBootPhase(ctx, 'rc.local')) {
|
||||||
bootOk = await runRcFileAt(ctx, '/etc/bare-os/rc.local', 'rc.local')
|
await bootTimed(ctx, 'rc.local', async () => {
|
||||||
})
|
bootOk = await runRcFileAt(ctx, '/etc/bare-os/rc.local', 'rc.local')
|
||||||
|
}, phaseLog)
|
||||||
|
} else {
|
||||||
|
phaseLog.push('rc.local(skipped)')
|
||||||
|
}
|
||||||
if (!bootOk) return
|
if (!bootOk) return
|
||||||
await bootTimed(ctx, 'kernel.d', async () => {
|
if (!shouldSkipBootPhase(ctx, 'kernel.d')) {
|
||||||
bootOk = await runBareOsKernelDir(ctx)
|
await bootTimed(ctx, 'kernel.d', async () => {
|
||||||
})
|
bootOk = await runBareOsKernelDir(ctx)
|
||||||
|
}, phaseLog)
|
||||||
|
} else {
|
||||||
|
phaseLog.push('kernel.d(skipped)')
|
||||||
|
}
|
||||||
if (!bootOk) return
|
if (!bootOk) return
|
||||||
await printSessionBanner(ctx)
|
await printSessionBanner(ctx)
|
||||||
await bootTimed(ctx, 'onboot', async () => {
|
phaseLog.push('banner')
|
||||||
bootOk = await runOnboot(ctx)
|
if (!shouldSkipBootPhase(ctx, 'onboot')) {
|
||||||
})
|
await bootTimed(ctx, 'onboot', async () => {
|
||||||
|
bootOk = await runOnboot(ctx)
|
||||||
|
}, phaseLog)
|
||||||
|
} else {
|
||||||
|
phaseLog.push('onboot(skipped)')
|
||||||
|
}
|
||||||
if (!bootOk) return
|
if (!bootOk) return
|
||||||
|
await bootTimed(ctx, 'selftest', async () => {
|
||||||
|
bootOk = await runKernelSelftest(ctx)
|
||||||
|
}, phaseLog)
|
||||||
|
if (!bootOk) return
|
||||||
|
publishBootReady(ctx, phaseLog)
|
||||||
while (true) {
|
while (true) {
|
||||||
const line = await readLine('')
|
const line = await readLine('')
|
||||||
if (line == null) break
|
if (line == null) break
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user