This commit is contained in:
Raven Scott
2026-04-03 19:13:20 -04:00
parent 8002a66f6e
commit 04d903beb8
24 changed files with 564 additions and 74 deletions
+1 -1
View File
@@ -341,7 +341,7 @@ Pear-safe path resolution (same idea as Holepunch [pear-rti](https://github.com/
**`executeKernel(disk, store, swarm, initSource)`** (`store`/`swarm` unused but kept for signature symmetry / future use) **`executeKernel(disk, store, swarm, initSource)`** (`store`/`swarm` unused but kept for signature symmetry / future use)
- Builds `shellEnv` with **guest** defaults: `USER`/`LOGNAME`=`guest`, `HOME`/`PWD`=`/home/guest`, `UID`/`GID`=`65534`, `BARE_OS_IDENTITY=guest`, `BARE_OS_EXIT_STATUS`=`0`, `BARE_OS_CTX_API_VERSION`, `PATH=/bin`, `SHELL`, `HOSTNAME`, `0`. `createVfs(drive, personalDrive, shellEnv, vfsMountRef, { procSnapshot })``ctx.vfs` (same `env` object as `ctx.env`). - Builds `shellEnv` with **guest** defaults: `USER`/`LOGNAME`=`guest`, `HOME`/`PWD`=`/home/guest`, `UID`/`GID`=`65534`, `BARE_OS_IDENTITY=guest`, `BARE_OS_EXIT_STATUS`=`0`, `BARE_OS_CTX_API_VERSION`, `PATH=/bin`, `SHELL`, `HOSTNAME`, `0`. `createVfs(drive, personalDrive, shellEnv, vfsMountRef, { procSnapshot, bootStartedMs, initdRunText })``ctx.vfs` (same `env` object as `ctx.env`). Optional **`bootStartedMs`** and **`initdRunText`** feed synthetic **`/proc/uptime`** and **`/run/bare-os/units`**.
- **`applyGuestEnv(ctx)`** then **`ensureGuestHome(ctx)`** — normalizes `ctx.identity` and seeds `/.bare/` (and a guest marker) on the personal drive. - **`applyGuestEnv(ctx)`** then **`ensureGuestHome(ctx)`** — normalizes `ctx.identity` and seeds `/.bare/` (and a guest marker) on the personal drive.
- **`createReadLine()`** always resolves stdio first and returns `stdout` (may be `null`) alongside `readLine` so the kernel can write to the **same** stream as the REPL (including `bare-stdio` under Pear). - **`createReadLine()`** always resolves stdio first and returns `stdout` (may be `null`) alongside `readLine` so the kernel can write to the **same** stream as the REPL (including `bare-stdio` under Pear).
- Builds `ctx`: **`bareOsCtxApiVersion`** (from [`bare-os-ctx-api.js`](packages/bare-os-booter/lib/bare-os-ctx-api.js)), `disk`, `drive`, `personalDrive`, `vfs`, `env`, `console`, `b4a`, `topic: topicKey()`, `readLine`, **`writeScreen(str)`**, `execLine``execShellLine(ctx, line)` (returns `'ok'` or `'exit'`; updates **`BARE_OS_EXIT_STATUS`** in **`vfs.env`**; bare **`exit`** line sets status then **`requestBooterExit`**), **`runBinCommand(argv)`** → delegates to **`runBinCommand(this, argv)`** from `kernel-runner.js` (for `/bin/time` and similar), plus identity hooks for `/bin` and builtins: - Builds `ctx`: **`bareOsCtxApiVersion`** (from [`bare-os-ctx-api.js`](packages/bare-os-booter/lib/bare-os-ctx-api.js)), `disk`, `drive`, `personalDrive`, `vfs`, `env`, `console`, `b4a`, `topic: topicKey()`, `readLine`, **`writeScreen(str)`**, `execLine``execShellLine(ctx, line)` (returns `'ok'` or `'exit'`; updates **`BARE_OS_EXIT_STATUS`** in **`vfs.env`**; bare **`exit`** line sets status then **`requestBooterExit`**), **`runBinCommand(argv)`** → delegates to **`runBinCommand(this, argv)`** from `kernel-runner.js` (for `/bin/time` and similar), plus identity hooks for `/bin` and builtins:
+3 -1
View File
@@ -23,7 +23,9 @@ 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.0.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.1.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. |
| **`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. |
| **`disk`** | Disk bundle used during boot (includes drives and helpers); advanced use | | **`disk`** | Disk bundle used during boot (includes drives and helpers); advanced use |
| **`drive`** | **System** Hyperdrive (`ctx.drive` is the OS image: `/bin`, `/boot`, …) | | **`drive`** | **System** Hyperdrive (`ctx.drive` is the OS image: `/bin`, `/boot`, …) |
| **`personalDrive`** | **Personal** Hyperdrive (mutable per-user state; VFS maps **`$HOME`** to **`/.bare-os/home/<HOME-basename>/…`** and session **`/var/log`** to **`/.bare-os/var/log/<basename>/…`** so guest vs unlocked trees do not share the same keys) | | **`personalDrive`** | **Personal** Hyperdrive (mutable per-user state; VFS maps **`$HOME`** to **`/.bare-os/home/<HOME-basename>/…`** and session **`/var/log`** to **`/.bare-os/var/log/<basename>/…`** so guest vs unlocked trees do not share the same keys) |
+16 -5
View File
@@ -39,25 +39,36 @@ Cleanup path closes swarm/drives and calls **`session.cleanup()`**, which runs *
Virtual listings include **`/home`** (session-specific), **`/mnt`** when HDMS mounts exist, and injected root entries **`proc`**, **`sys`**, **`tmp`** when absent from the system image. Virtual listings include **`/home`** (session-specific), **`/mnt`** when HDMS mounts exist, and injected root entries **`proc`**, **`sys`**, **`tmp`** when absent from the system image.
### Pseudo **`/proc`** and **`/sys`** (read-only) ### Pseudo **`/proc`**, **`/sys`**, **`/run`**, **`/dev`** (mostly read-only)
These paths are **synthetic** (not stored on either Hyperdrive). They exist for inspection and scripting ergonomics, **not** Linux ABI compatibility. These paths are **synthetic** (not stored on either Hyperdrive). They exist for inspection and scripting ergonomics, **not** Linux ABI compatibility.
- **`/proc`**: **`version`**, **`bare_os_version`** (same payload as **`version`**), **`self/`** with **`environ`** (null-separated **`KEY=value`** pairs) and **`cmdline`**. **`environ`** omits keys whose names look secret-bearing (e.g. **`PASSWORD`**, **`TOKEN`**, **`VAULT`**) and only includes a small public set plus **`BARE_OS_*`**. - **`/proc`**: **`version`**, **`bare_os_version`**, **`uptime`**, **`meminfo`** (static, Linux-shaped text), **`self/`** with **`environ`** (null-separated **`KEY=value`** pairs) and **`cmdline`**. **`environ`** omits keys whose names look secret-bearing (e.g. **`PASSWORD`**, **`TOKEN`**, **`VAULT`**) and only includes a small public set plus **`BARE_OS_*`**.
- **`/sys/fs/bare_os/version`**: same text as **`/proc/version`**. - **`/sys/fs/bare_os/version`**: same text as **`/proc/version`**.
- **`/run/bare-os/units`**: tab-separated snapshot of **bare-initd** registered units (phase, start time, description).
- **`/dev/null`**, **`/dev/zero`**: minimal device semantics — **`null`** discards writes and reads empty; **`zero`** reads a fixed 64KiB zero buffer. Not infinite **`/dev/zero`** like Linux.
**Non-goals:** no real PIDs, **`/proc/meminfo`**, device nodes, or guarantees of path parity with Linux. **Non-goals:** no real PIDs, accurate **`meminfo`**, or guarantees of path parity with Linux.
**Implementation note:** pseudo-file content is UTF-8 encoded with **`b4a`**, not **`TextEncoder`**, because some Bare/Pear runtimes omit the Web Encoding globals (`TextEncoder` / `TextDecoder`). The same applies elsewhere in the booter and in-image utilities that must run on Bare. **Implementation note:** pseudo-file content is UTF-8 encoded with **`b4a`**, not **`TextEncoder`**, because some Bare/Pear runtimes omit the Web Encoding globals (`TextEncoder` / `TextDecoder`). The same applies elsewhere in the booter and in-image utilities that must run on Bare.
### **`/dev`** and **`/run`** (not implemented) ### **`/dev`** and **`/run`** (minimal subset)
Special device files (**`/dev/null`**, **`/dev/zero`**, …) would need dedicated read/write semantics across **`cat`**, redirection, and **`cp`**; **`/run`** could mirror **bare-initd** state. Both are **out of scope** until explicitly specified and tested. See **Pseudo `/proc`, `/sys`, `/run`, `/dev`** above. A full device tree and **`/run`** parity with Linux are still **out of scope**.
### Kernel / booter follow-ons (backlog) ### Kernel / booter follow-ons (backlog)
Priorities from existing gap docs: bounded **shell pipeline** limits (bytes/lines, stderr) per [Chapter 9](09-posix-utilities-shell-and-vfs.md); **TLS trust** and cookie storage for **`curl`** / **`wget`** per **`packages/bare-os-booter/CLI_PARITY.md`**; optional extra **`bare_os.*`** seeder RPC beyond **`version`** in **`bare-os-protocol`**; coreutils stubs and flag gaps per **DOCUMENTATION.md** §14a. Priorities from existing gap docs: bounded **shell pipeline** limits (bytes/lines, stderr) per [Chapter 9](09-posix-utilities-shell-and-vfs.md); **TLS trust** and cookie storage for **`curl`** / **`wget`** per **`packages/bare-os-booter/CLI_PARITY.md`**; optional extra **`bare_os.*`** seeder RPC beyond **`version`** in **`bare-os-protocol`**; coreutils stubs and flag gaps per **DOCUMENTATION.md** §14a.
### Implementation priority (milestones)
Work is sequenced for **POSIX/script ergonomics first**, then networking and long-running service features:
1. **VFS and shell** — Synthetic **`/proc`** / **`/run`** / minimal **`/dev`** paths, bounded **pipeline** capture in **`execShellLine`**, kernel **boot trace** and **banner** hooks on the system image.
2. **`/bin` and help** — **`man -w`**, optional **sliced** long pages via **`MAN_SLICE`** / **`PAGER=bare-slice`**, incremental **coreutils** flags (e.g. **`grep -w`**).
3. **Networking****`CLI_PARITY.md`** items (cookies, TLS trust, finer timeouts) once storage and trust policy exist.
4. **Init and protocol** — Persistent **`systemctl enable`**, richer **`bare_os.*`** RPC, after the above stabilize.
**Directories:** **`vfs.mkdir(path, { recursive })`** and **`vfs.rmdir(path)`** implement POSIX-like tree creation and removal using a **`.bareos_empty`** marker file for empty directories (aligned with **`git-fs-adapter`**). See [Chapter 9](09-posix-utilities-shell-and-vfs.md). **Directories:** **`vfs.mkdir(path, { recursive })`** and **`vfs.rmdir(path)`** implement POSIX-like tree creation and removal using a **`.bareos_empty`** marker file for empty directories (aligned with **`git-fs-adapter`**). See [Chapter 9](09-posix-utilities-shell-and-vfs.md).
**`ctx.runBinCommand(argv)`** — same resolution as external commands in the shell; exposed for utilities such as **`/bin/time`**. **`ctx.runBinCommand(argv)`** — same resolution as external commands in the shell; exposed for utilities such as **`/bin/time`**.
+4 -2
View File
@@ -35,9 +35,9 @@ Hyperdrive does not always behave like a POSIX directory tree. Empty directories
- **`vfs.mkdir(path, { recursive })`** — creates directories by writing **`dirname/.bareos_empty`**. **`-p` / `--parents`** is implemented by **`/bin/mkdir`**. - **`vfs.mkdir(path, { recursive })`** — creates directories by writing **`dirname/.bareos_empty`**. **`-p` / `--parents`** is implemented by **`/bin/mkdir`**.
- **`vfs.rmdir(path)`** — removes a directory only if it has **no entries other than** `.bareos_empty` (and removes the marker). - **`vfs.rmdir(path)`** — removes a directory only if it has **no entries other than** `.bareos_empty` (and removes the marker).
### 2.3 Pseudo **`/proc`**, **`/sys`**, and session **`/tmp`** ### 2.3 Pseudo **`/proc`**, **`/sys`**, **`/run`**, **`/dev`**, and session **`/tmp`**
- **`/proc`** and **`/sys/...`** — read-only synthetic files and directories (see [Chapter 4](04-the-booter-runtime.md)). **`writeFile`**, **`unlink`**, **`chmod`**, and **`symlink`** on these paths fail. - **`/proc`**, **`/sys`**, **`/run`**, **`/dev`** — read-only synthetic trees except **`/dev/null`** and **`/dev/zero`** accept writes that are discarded (see [Chapter 4](04-the-booter-runtime.md)). Other pseudo **`writeFile`** / **`unlink`** / **`chmod`** paths fail as documented there.
- **`/tmp`** — writable on the **personal** drive under **`/.bare-os/tmp/<HOME-basename>/…`**, isolated like **`$HOME`** and **`/var/log`**. - **`/tmp`** — writable on the **personal** drive under **`/.bare-os/tmp/<HOME-basename>/…`**, isolated like **`$HOME`** and **`/var/log`**.
### 2.4 `chmod` (octal and symbolic) ### 2.4 `chmod` (octal and symbolic)
@@ -62,6 +62,8 @@ There is **no** **`chown`** / **`chgrp`** that changes stored ownership in a mul
**Unsupported:** lone **`&`** (background) is rejected with an error. There is no job control. **Unsupported:** lone **`&`** (background) is rejected with an error. There is no job control.
**Pipeline limits:** simulated pipe capture is bounded. Defaults: **`BARE_OS_PIPELINE_MAX_STAGES`** (32), **`BARE_OS_PIPELINE_MAX_BYTES`** (2MiB), **`BARE_OS_PIPELINE_MAX_LINES`** (50000). Exceeding a limit fails the pipeline with exit status **1** and an error on stderr.
**Last exit status:** after each full **`execLine`** evaluation, **`vfs.env.BARE_OS_EXIT_STATUS`** is updated (decimal string). Words expand **`$?`** and **`${?}`** from that value (default **`0`** if unset), similar to POSIX **`$?`**. **Last exit status:** after each full **`execLine`** evaluation, **`vfs.env.BARE_OS_EXIT_STATUS`** is updated (decimal string). Words expand **`$?`** and **`${?}`** from that value (default **`0`** if unset), similar to POSIX **`$?`**.
Beyond **`alias`**, **`unalias`**, **`cd`**, **`export`**, **`login`**, **`logout`**, **`exit`**: Beyond **`alias`**, **`unalias`**, **`cd`**, **`export`**, **`login`**, **`logout`**, **`exit`**:
+2 -2
View File
@@ -103,8 +103,8 @@ The merged **`man.json`** adds **`schemaVersion`**, **`generatedAt`**, **`pages`
## Future work ## Future work
- **`PAGER`** / scrollable view on TTY. - Interactive **`PAGER`** (keypress paging on TTY) beyond **`PAGER=bare-slice`** section breaks.
- **`man -w`** printing the logical path **`/share/man/man.json`** (or per-page anchors). - **`man -w`** is implemented (prints **`/share/man/man.json`**); per-page anchor paths remain future work.
- HTML export for Pear / browser shells. - HTML export for Pear / browser shells.
- Section **7** overview pages and i18n. - Section **7** overview pages and i18n.
+1
View File
@@ -19,6 +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`** in the session environment to log boot phase timings on stderr.
- **`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).
+18 -7
View File
@@ -78,13 +78,14 @@ async function run(ctx, argv) {
let suppressErrors = false let suppressErrors = false
let forceFilename = false let forceFilename = false
let noFilename = false let noFilename = false
let word = false
const args = argv.slice(1) const args = argv.slice(1)
let i = 0 let i = 0
function usage() { function usage() {
ctx.console.error( ctx.console.error(
'usage: grep [-E|-F] [-i] [-v] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]' 'usage: grep [-E|-F] [-i] [-v] [-w] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]'
) )
ctx.exitCode = 2 ctx.exitCode = 2
} }
@@ -169,6 +170,9 @@ async function run(ctx, argv) {
case 'h': case 'h':
noFilename = true noFilename = true
break break
case 'w':
word = true
break
default: default:
ctx.console.error('grep: invalid option -- ' + c) ctx.console.error('grep: invalid option -- ' + c)
ctx.exitCode = 2 ctx.exitCode = 2
@@ -217,7 +221,7 @@ async function run(ctx, argv) {
let matchers let matchers
try { try {
matchers = buildMatchers(patterns, { fixed, icase }) matchers = buildMatchers(patterns, { fixed, icase, word })
} catch (e) { } catch (e) {
ctx.console.error('grep: ' + (e.message || e)) ctx.console.error('grep: ' + (e.message || e))
ctx.exitCode = 2 ctx.exitCode = 2
@@ -304,7 +308,7 @@ async function run(ctx, argv) {
/** /**
* @param {string[]} patterns * @param {string[]} patterns
* @param {{ fixed: boolean, icase: boolean }} o * @param {{ fixed: boolean, icase: boolean, word?: boolean }} o
*/ */
function buildMatchers(patterns, o) { function buildMatchers(patterns, o) {
if (patterns.length === 0) throw new Error('no pattern') if (patterns.length === 0) throw new Error('no pattern')
@@ -312,15 +316,22 @@ function buildMatchers(patterns, o) {
const pats = o.icase const pats = o.icase
? patterns.map((p) => p.toLowerCase()) ? patterns.map((p) => p.toLowerCase())
: patterns.slice() : patterns.slice()
return pats.map( return pats.map((p) => {
(p) => (line) => if (o.word) {
const esc = p.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&')
const flags = o.icase ? 'i' : ''
const re = new RegExp('(?:^|[^0-9A-Za-z_])' + esc + '(?:$|[^0-9A-Za-z_])', flags)
return (line) => re.test(line)
}
return (line) =>
o.icase ? line.toLowerCase().includes(p) : line.includes(p) o.icase ? line.toLowerCase().includes(p) : line.includes(p)
) })
} }
const flags = o.icase ? 'i' : '' const flags = o.icase ? 'i' : ''
return patterns.map((p) => { return patterns.map((p) => {
try { try {
const re = new RegExp(p, flags) const body = o.word ? '\\b(?:' + p + ')\\b' : p
const re = new RegExp(body, flags)
return (line) => re.test(line) return (line) => re.test(line)
} catch (e) { } catch (e) {
throw new Error('invalid regex: ' + (e.message || e)) throw new Error('invalid regex: ' + (e.message || e))
+38 -3
View File
@@ -274,9 +274,10 @@ async function run(ctx, argv) {
function usage() { function usage() {
ctx.console.error( ctx.console.error(
'usage: man [-k keyword] [-f name] [-l] [[section] name]\n' + 'usage: man [-k keyword] [-f name] [-l] [-w] [[section] name]\n' +
' Section 1: /bin; section 7: handbook + developer guide (man handbook, man devguide).\n' + ' Section 1: /bin; section 7: handbook + developer guide (man handbook, man devguide).\n' +
' Data: /share/man/man.json on the system drive.' ' Data: /share/man/man.json on the system drive.\n' +
' Long pages: set PAGER=bare-slice and optional MAN_SLICE=N (default 24) for section breaks.'
) )
ctx.exitCode = 2 ctx.exitCode = 2
} }
@@ -318,6 +319,10 @@ async function run(ctx, argv) {
mode = 'list' mode = 'list'
continue continue
} }
if (a === '-w' || a === '--where' || a === '--path') {
ctx.console.log('/share/man/man.json')
return
}
if (a.startsWith('-')) { if (a.startsWith('-')) {
ctx.console.error('man: unknown option: ' + a) ctx.console.error('man: unknown option: ' + a)
ctx.exitCode = 2 ctx.exitCode = 2
@@ -474,5 +479,35 @@ async function run(ctx, argv) {
ctx.exitCode = 1 ctx.exitCode = 1
return return
} }
ctx.console.log(bareManRenderPage(page, ctx, width).replace(/\n$/, '')) const rendered = bareManRenderPage(page, ctx, width).replace(/\n$/, '')
if (bareManSlicePage(rendered, env, ctx.console)) return
ctx.console.log(rendered)
}
/**
* @param {string} text
* @param {Record<string, string>} env
* @param {{ log: (s: string) => void }} cons
*/
function bareManSlicePage(text, env, cons) {
const pager = env.PAGER || ''
if (pager !== 'bare-slice' && pager !== 'bare_slice') return false
const raw = env.MAN_SLICE || '24'
const n = Number.parseInt(String(raw), 10)
const sliceLines = Number.isFinite(n) && n > 0 ? n : 24
const lines = text.split('\n')
const total = lines.length
for (let i = 0; i < total; i += sliceLines) {
const chunk = lines.slice(i, i + sliceLines).join('\n')
cons.log(chunk)
if (i + sliceLines < total) {
const hi = Math.min(i + sliceLines, total)
cons.log('')
cons.log(
`--- man: lines ${i + 1}-${hi} of ${total} (PAGER=bare-slice) ---`
)
cons.log('')
}
}
return true
} }
+48 -9
View File
@@ -3,10 +3,31 @@
* Loaded by the booter with an injected ctx object (trusted replication source). * Loaded by the booter with an injected ctx object (trusted replication source).
* *
* Boot order: /etc/os-release → /etc/motd → /etc/bare-os/rc → /etc/bare-os/rc.d/* * Boot order: /etc/os-release → /etc/motd → /etc/bare-os/rc → /etc/bare-os/rc.d/*
* (sorted by filename) → session banner → interactive loop. * (sorted; digit-prefixed snippet names) → session banner → interactive loop.
* Use ctx.registerKernelShutdownHook(fn) for teardown before initd disposers. * Use ctx.registerKernelShutdownHook(fn) for teardown before initd disposers.
*/ */
/**
* @param {Record<string, unknown>} ctx
*/
function wantBootTrace(ctx) {
const v = ctx.env && ctx.env.BARE_OS_BOOT_TRACE
return v === '1' || v === 'true'
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} label
* @param {() => void | Promise<void>} fn
*/
async function bootTimed(ctx, label, fn) {
const t0 = Date.now()
await fn()
if (wantBootTrace(ctx)) {
ctx.console.error(`[boot] ${label}: ${Date.now() - t0}ms`)
}
}
/** /**
* @param {Record<string, unknown>} ctx * @param {Record<string, unknown>} ctx
* @param {string} text * @param {string} text
@@ -112,19 +133,37 @@ async function runBareOsRcDir(ctx) {
/** /**
* @param {Record<string, unknown>} ctx * @param {Record<string, unknown>} ctx
*/ */
function printSessionBanner(ctx) { async function printSessionBanner(ctx) {
ctx.console.log( const { drive, b4a, console } = ctx
for (const p of ['/etc/bare-os/banner', '/etc/issue']) {
try {
const buf = await drive.get(p)
if (buf) {
console.log(b4a.toString(buf).trimEnd())
return
}
} catch {
/* ignore */
}
}
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, && || ;, |, exit | services: systemctl list-units, journalctl -u UNIT | try: help, ls /bin, crontab -l'
) if (ctx.bareOsSkipRepl) {
console.log(
'Bare operating system — non-interactive session (BARE_OS_SKIP_REPL).'
)
return
}
console.log(defaultBanner)
} }
async function start(ctx) { async function start(ctx) {
const { readLine, execLine, console } = ctx const { readLine, execLine, console } = ctx
await printOsRelease(ctx) await bootTimed(ctx, 'os-release', () => printOsRelease(ctx))
await printMotd(ctx) await bootTimed(ctx, 'motd', () => printMotd(ctx))
await runRcFileAt(ctx, '/etc/bare-os/rc', 'rc') await bootTimed(ctx, 'rc', () => runRcFileAt(ctx, '/etc/bare-os/rc', 'rc'))
await runBareOsRcDir(ctx) await bootTimed(ctx, 'rc.d', () => runBareOsRcDir(ctx))
printSessionBanner(ctx) await printSessionBanner(ctx)
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
+23 -1
View File
@@ -34,7 +34,9 @@ import {
import { HdmsController, runHdmsCli } from './lib/hdms-manager.js' import { HdmsController, runHdmsCli } from './lib/hdms-manager.js'
import { import {
startBareInitd, startBareInitd,
registerKernelShutdownHook registerKernelShutdownHook,
listBareServices,
getBareServiceRuntime
} from './lib/bare-initd.js' } from './lib/bare-initd.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 './lib/bare-cron.js' import './lib/bare-cron.js'
@@ -228,10 +230,26 @@ async function executeKernel(disk, store, swarm, initSource) {
} }
/** @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 vfs = createVfs(disk.drive, disk.personalDrive, shellEnv, vfsMountRef, { const vfs = createVfs(disk.drive, disk.personalDrive, shellEnv, vfsMountRef, {
procSnapshot: { procSnapshot: {
version: BARE_OS_CTX_API_VERSION, version: BARE_OS_CTX_API_VERSION,
cmdline: 'bare-os-booter' cmdline: 'bare-os-booter'
},
bootStartedMs,
initdRunText() {
const lines = [
'# bare-initd units (name<TAB>phase<TAB>startedAtMs<TAB>description)',
''
]
for (const s of listBareServices()) {
const rt = getBareServiceRuntime(s.name)
const phase = rt?.phase ?? 'inactive'
const started = rt?.startedAtMs ?? 0
const desc = (s.description || '').replace(/\t/g, ' ').replace(/\n/g, ' ')
lines.push(`${s.name}\t${phase}\t${started}\t${desc}`)
}
return lines.join('\n') + '\n'
} }
}) })
@@ -244,6 +262,10 @@ async function executeKernel(disk, store, swarm, initSource) {
const ctx = { const ctx = {
/** Documented `ctx` contract version; bump in lib/bare-os-ctx-api.js when the surface changes. */ /** Documented `ctx` contract version; bump in lib/bare-os-ctx-api.js when the surface changes. */
bareOsCtxApiVersion: BARE_OS_CTX_API_VERSION, bareOsCtxApiVersion: BARE_OS_CTX_API_VERSION,
/** Milliseconds since Unix epoch when this session started VFS construction (for `/proc/uptime`). */
bareOsBootStartedMs: bootStartedMs,
/** True when `BARE_OS_SKIP_REPL=1` — stdin is non-interactive; `readLine` yields EOF immediately after boot. */
bareOsSkipRepl: skipInteractive,
disk, disk,
drive: disk.drive, drive: disk.drive,
personalDrive: disk.personalDrive, personalDrive: disk.personalDrive,
@@ -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.0.0' export const BARE_OS_CTX_API_VERSION = '1.1.0'
+58
View File
@@ -47,6 +47,38 @@ export function syncBareOsExitStatusEnv(ctx) {
/** Max alias indirections (prevents cycles). */ /** Max alias indirections (prevents cycles). */
const MAX_ALIAS_DEPTH = 16 const MAX_ALIAS_DEPTH = 16
/** Default caps for simulated pipeline capture (`console.log` between stages). */
export const DEFAULT_PIPELINE_MAX_STAGES = 32
export const DEFAULT_PIPELINE_MAX_CAPTURE_BYTES = 2 * 1024 * 1024
export const DEFAULT_PIPELINE_MAX_CAPTURE_LINES = 50000
/**
* @param {Record<string, string> | null | undefined} env
*/
function pipelineLimitsFromEnv(env) {
const o = env && typeof env === 'object' ? env : {}
const parse = (key, def) => {
const v = o[key]
if (v == null || v === '') return def
const n = Number.parseInt(String(v), 10)
return Number.isFinite(n) && n > 0 ? n : def
}
return {
maxStages: parse(
'BARE_OS_PIPELINE_MAX_STAGES',
DEFAULT_PIPELINE_MAX_STAGES
),
maxBytes: parse(
'BARE_OS_PIPELINE_MAX_BYTES',
DEFAULT_PIPELINE_MAX_CAPTURE_BYTES
),
maxLines: parse(
'BARE_OS_PIPELINE_MAX_LINES',
DEFAULT_PIPELINE_MAX_CAPTURE_LINES
)
}
}
/** /**
* Baseline aliases; `~/.barerc` and `unalias -a` merge/reset from this table. * Baseline aliases; `~/.barerc` and `unalias -a` merge/reset from this table.
* @returns {Record<string, string>} * @returns {Record<string, string>}
@@ -546,9 +578,18 @@ function segmentHasCommand(seg) {
async function execParsedPipeline(ctx, pipeline) { async function execParsedPipeline(ctx, pipeline) {
const vfs = ctx.vfs const vfs = ctx.vfs
const env = vfs.env const env = vfs.env
const lim = pipelineLimitsFromEnv(env)
if (pipeline.length > lim.maxStages) {
ctx.console.error(
`shell: pipeline exceeds BARE_OS_PIPELINE_MAX_STAGES (${lim.maxStages})`
)
ctx.exitCode = 1
return 'ok'
}
let stdinText = typeof ctx.shellStdin === 'string' ? ctx.shellStdin : null let stdinText = typeof ctx.shellStdin === 'string' ? ctx.shellStdin : null
try {
for (let pi = 0; pi < pipeline.length; pi++) { for (let pi = 0; pi < pipeline.length; pi++) {
const cmd = pipeline[pi] const cmd = pipeline[pi]
const isLast = pi === pipeline.length - 1 const isLast = pi === pipeline.length - 1
@@ -599,6 +640,18 @@ async function execParsedPipeline(ctx, pipeline) {
if (!isLast || cmd.redirOut) { if (!isLast || cmd.redirOut) {
ctx.console.log = (...args) => { ctx.console.log = (...args) => {
outChunks.push(args.map(String).join(' ') + '\n') outChunks.push(args.map(String).join(' ') + '\n')
const joined = outChunks.join('')
if (joined.length > lim.maxBytes) {
throw new Error(
`shell: pipeline output exceeds BARE_OS_PIPELINE_MAX_BYTES (${lim.maxBytes})`
)
}
const lineCount = joined.split('\n').length - 1
if (lineCount > lim.maxLines) {
throw new Error(
`shell: pipeline output exceeds BARE_OS_PIPELINE_MAX_LINES (${lim.maxLines})`
)
}
} }
ctx.console.error = ctx.console.log ctx.console.error = ctx.console.log
} }
@@ -828,6 +881,11 @@ async function execParsedPipeline(ctx, pipeline) {
} }
return 'ok' return 'ok'
} catch (e) {
ctx.console.error((e && e.message) || String(e))
ctx.exitCode = 1
return 'ok'
}
} }
/** /**
@@ -330,7 +330,10 @@ export function isVirtualMountPoint(abs) {
n === '/proc/self' || n === '/proc/self' ||
n === '/sys' || n === '/sys' ||
n === '/sys/fs' || n === '/sys/fs' ||
n === '/sys/fs/bare_os' n === '/sys/fs/bare_os' ||
n === '/run' ||
n === '/run/bare-os' ||
n === '/dev'
) )
} }
+103 -6
View File
@@ -23,12 +23,16 @@ const DIR_MARKER = '.bareos_empty'
* Unified path view: system Hyperdrive for OS paths, personal Hyperdrive under $HOME, * Unified path view: system Hyperdrive for OS paths, personal Hyperdrive under $HOME,
* optional HDMS mounts under /mnt/<label>/, virtual /var with writable /var/log/ * optional HDMS mounts under /mnt/<label>/, virtual /var with writable /var/log/
* on the personal drive under /.bare-os/var/log/<home-seg>/ (session-isolated). * on the personal drive under /.bare-os/var/log/<home-seg>/ (session-isolated).
* Read-only pseudo `proc` and `sys` under `/`; session `tmp` maps to `/.bare-os/tmp/<seg>/` on the personal drive. * Read-only pseudo `proc`, `sys`, `run`, `dev` under `/`; session `tmp` maps to `/.bare-os/tmp/<seg>/` on the personal drive.
* @param {import('hyperdrive').default} systemDrive * @param {import('hyperdrive').default} systemDrive
* @param {import('hyperdrive').default} personalDrive * @param {import('hyperdrive').default} personalDrive
* @param {Record<string, string>} env * @param {Record<string, string>} env
* @param {{ getMounts?: () => Map<string, { drive: import('hyperdrive').default, writable: boolean }> } | null} [mntRef] * @param {{ getMounts?: () => Map<string, { drive: import('hyperdrive').default, writable: boolean }> } | null} [mntRef]
* @param {{ procSnapshot?: { version?: string, cmdline?: string } }} [vfsOptions] * @param {{
* procSnapshot?: { version?: string, cmdline?: string },
* bootStartedMs?: number,
* initdRunText?: () => string
* }} [vfsOptions]
*/ */
export function createVfs( export function createVfs(
systemDrive, systemDrive,
@@ -38,6 +42,14 @@ export function createVfs(
vfsOptions = {} vfsOptions = {}
) { ) {
const procSnapshot = vfsOptions.procSnapshot || null const procSnapshot = vfsOptions.procSnapshot || null
const bootStartedMs =
typeof vfsOptions.bootStartedMs === 'number'
? vfsOptions.bootStartedMs
: null
const initdRunText =
typeof vfsOptions.initdRunText === 'function'
? vfsOptions.initdRunText
: null
const HOME = () => env.HOME || '/home/guest' const HOME = () => env.HOME || '/home/guest'
let cwd = env.PWD || HOME() let cwd = env.PWD || HOME()
@@ -132,11 +144,43 @@ export function createVfs(
return utf8Encode(parts.join('')) return utf8Encode(parts.join(''))
} }
function pseudoUptimeText() {
const start = bootStartedMs != null ? bootStartedMs : Date.now()
const up = Math.max(0, (Date.now() - start) / 1000)
return `${up.toFixed(2)} ${up.toFixed(2)}\n`
}
function pseudoMeminfoText() {
return [
'MemTotal: 524288 kB',
'MemFree: 262144 kB',
'MemAvailable: 262144 kB',
'SwapTotal: 0 kB',
'SwapFree: 0 kB',
''
].join('\n')
}
function pseudoFileBytes(routePseudo) { function pseudoFileBytes(routePseudo) {
const f = routePseudo.file const f = routePseudo.file
if (f === 'version') return utf8Encode(pseudoVersionText()) const k = routePseudo.kind
if (f === 'cmdline') return utf8Encode(pseudoCmdlineText()) if (f === 'version' && (k === 'proc' || k === 'sys')) {
if (f === 'environ') return pseudoEnvironBytes() return utf8Encode(pseudoVersionText())
}
if (k === 'proc') {
if (f === 'cmdline') return utf8Encode(pseudoCmdlineText())
if (f === 'environ') return pseudoEnvironBytes()
if (f === 'uptime') return utf8Encode(pseudoUptimeText())
if (f === 'meminfo') return utf8Encode(pseudoMeminfoText())
}
if (k === 'run' && f === 'units') {
const t = initdRunText
? initdRunText()
: '# bare-initd: no snapshot provider\n'
return utf8Encode(t)
}
if (k === 'dev' && f === 'null') return utf8Encode('')
if (k === 'dev' && f === 'zero') return new Uint8Array(65536)
return utf8Encode('') return utf8Encode('')
} }
@@ -163,8 +207,38 @@ export function createVfs(
if (sub === 'self/cmdline') { if (sub === 'self/cmdline') {
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'cmdline' } return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'cmdline' }
} }
if (sub === 'uptime') {
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'uptime' }
}
if (sub === 'meminfo') {
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'meminfo' }
}
return { virtualPseudo: true, kind: 'proc', node: 'enoent' } return { virtualPseudo: true, kind: 'proc', node: 'enoent' }
} }
if (n === '/run' || n.startsWith('/run/')) {
if (n === '/run') {
return { virtualPseudo: true, kind: 'run', node: 'root' }
}
if (n === '/run/bare-os') {
return { virtualPseudo: true, kind: 'run', node: 'dir', dir: 'bare_os' }
}
if (n === '/run/bare-os/units') {
return { virtualPseudo: true, kind: 'run', node: 'file', file: 'units' }
}
return { virtualPseudo: true, kind: 'run', node: 'enoent' }
}
if (n === '/dev' || n.startsWith('/dev/')) {
if (n === '/dev') {
return { virtualPseudo: true, kind: 'dev', node: 'root' }
}
if (n === '/dev/null') {
return { virtualPseudo: true, kind: 'dev', node: 'file', file: 'null' }
}
if (n === '/dev/zero') {
return { virtualPseudo: true, kind: 'dev', node: 'file', file: 'zero' }
}
return { virtualPseudo: true, kind: 'dev', node: 'enoent' }
}
if (n === '/sys' || n.startsWith('/sys/')) { if (n === '/sys' || n.startsWith('/sys/')) {
if (n === '/sys') { if (n === '/sys') {
return { virtualPseudo: true, kind: 'sys', node: 'root' } return { virtualPseudo: true, kind: 'sys', node: 'root' }
@@ -653,7 +727,7 @@ export function createVfs(
throw new Error('Not a directory: ' + abs) throw new Error('Not a directory: ' + abs)
} }
if (pr.kind === 'proc' && pr.node === 'root') { if (pr.kind === 'proc' && pr.node === 'root') {
return ['bare_os_version', 'self', 'version'] return ['bare_os_version', 'meminfo', 'self', 'uptime', 'version']
} }
if (pr.kind === 'proc' && pr.node === 'dir' && pr.dir === 'self') { if (pr.kind === 'proc' && pr.node === 'dir' && pr.dir === 'self') {
return ['cmdline', 'environ'] return ['cmdline', 'environ']
@@ -667,6 +741,15 @@ export function createVfs(
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 ['version']
} }
if (pr.kind === 'run' && pr.node === 'root') {
return ['bare-os']
}
if (pr.kind === 'run' && pr.node === 'dir' && pr.dir === 'bare_os') {
return ['units']
}
if (pr.kind === 'dev' && pr.node === 'root') {
return ['null', 'zero']
}
} }
const r = route(abs) const r = route(abs)
if (r.virtualMntRoot) { if (r.virtualMntRoot) {
@@ -697,6 +780,12 @@ export function createVfs(
if (abs === '/' && !names.includes('tmp')) { if (abs === '/' && !names.includes('tmp')) {
names.push('tmp') names.push('tmp')
} }
if (abs === '/' && !names.includes('run')) {
names.push('run')
}
if (abs === '/' && !names.includes('dev')) {
names.push('dev')
}
return names.sort() return names.sort()
} }
@@ -773,6 +862,14 @@ export function createVfs(
*/ */
async function writeFileAtAbs(abs, buf, opts = {}) { async function writeFileAtAbs(abs, buf, opts = {}) {
const r = route(abs) const r = route(abs)
if (
r.virtualPseudo &&
r.kind === 'dev' &&
r.node === 'file' &&
(r.file === 'null' || r.file === 'zero')
) {
return
}
if ( if (
r.virtualHomeDir || r.virtualHomeDir ||
r.virtualMntRoot || r.virtualMntRoot ||
+70 -1
View File
@@ -450,15 +450,21 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
BARE_OS_CTX_API_VERSION: '9.9.9-test' BARE_OS_CTX_API_VERSION: '9.9.9-test'
} }
const vfs = createVfs(sys, personal, env, null, { const vfs = createVfs(sys, personal, env, null, {
procSnapshot: { version: '1.2.3-test', cmdline: 'unit-test' } procSnapshot: { version: '1.2.3-test', cmdline: 'unit-test' },
bootStartedMs: Date.now() - 4000,
initdRunText: () => 'demo-unit\tactive\t1\tdemo\n'
}) })
const root = await vfs.readdir('/') const root = await vfs.readdir('/')
t.ok(root.includes('proc')) t.ok(root.includes('proc'))
t.ok(root.includes('sys')) t.ok(root.includes('sys'))
t.ok(root.includes('tmp')) t.ok(root.includes('tmp'))
t.ok(root.includes('run'))
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_version', 'bare_os_version',
'meminfo',
'self', 'self',
'uptime',
'version' 'version'
]) ])
t.alike(await vfs.readdir('/proc/self').then((a) => [...a].sort()), [ t.alike(await vfs.readdir('/proc/self').then((a) => [...a].sort()), [
@@ -474,6 +480,20 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
t.ok(sysVer.includes('1.2.3-test')) t.ok(sysVer.includes('1.2.3-test'))
const cmd = b4a.toString(await vfs.readFile('/proc/self/cmdline')) const cmd = b4a.toString(await vfs.readFile('/proc/self/cmdline'))
t.ok(cmd.includes('unit-test')) t.ok(cmd.includes('unit-test'))
const mem = b4a.toString(await vfs.readFile('/proc/meminfo'))
t.ok(mem.includes('MemTotal:'))
const up = b4a.toString(await vfs.readFile('/proc/uptime'))
t.ok(/^\d+\.\d+\s+\d+\.\d+/.test(up.trim()))
t.alike(await vfs.readdir('/dev').then((a) => [...a].sort()), [
'null',
'zero'
])
await vfs.writeFile('/dev/null', b4a.from('gone'))
t.is((await vfs.readFile('/dev/null'))?.byteLength ?? 0, 0)
const z = await vfs.readFile('/dev/zero')
t.ok(z && z.byteLength === 65536)
const units = b4a.toString(await vfs.readFile('/run/bare-os/units'))
t.ok(units.includes('demo-unit'))
let writeErr = null let writeErr = null
try { try {
await vfs.writeFile('/proc/version', b4a.from('x')) await vfs.writeFile('/proc/version', b4a.from('x'))
@@ -784,6 +804,51 @@ test('loadBarerc createSkeletonIfMissing writes ~/.barerc when absent', async (t
rmSync(dir, { recursive: true, force: true }) rmSync(dir, { recursive: true, force: true })
}) })
test('execShellLine pipeline stage and byte limits', async (t) => {
const dir = testCorestoreDir('shpipe')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('spipe'))
await drive.ready()
await personal.ready()
const echo = `
async function run(ctx, argv) {
ctx.console.log(argv.slice(1).join(' '))
}
`
const cat = `
async function run(ctx) {
ctx.console.log(bareStdin(ctx).replace(/\\n$/, ''))
}
`
await drive.put('/bin/echo', b4a.from(echo))
await drive.put('/bin/cat', b4a.from(cat))
const spam = `
async function run(ctx) {
ctx.console.log('z'.repeat(200))
}
`
await drive.put('/bin/spam', b4a.from(spam))
const logs = []
const ctx = testCtx(drive, personal)
ctx.console = {
log: (...a) => logs.push(a.join(' ')),
error: (...a) => logs.push(a.join(' '))
}
ctx.env.BARE_OS_PIPELINE_MAX_STAGES = '2'
await execShellLine(ctx, 'echo a | echo b | echo c')
t.is(ctx.exitCode, 1)
t.ok(logs.some((l) => l.includes('BARE_OS_PIPELINE_MAX_STAGES')))
logs.length = 0
ctx.env.BARE_OS_PIPELINE_MAX_STAGES = '32'
ctx.env.BARE_OS_PIPELINE_MAX_BYTES = '80'
await execShellLine(ctx, 'spam | cat')
t.is(ctx.exitCode, 1)
t.ok(logs.some((l) => l.includes('BARE_OS_PIPELINE_MAX_BYTES')))
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('execShellLine runs cd and external', async (t) => { test('execShellLine runs cd and external', async (t) => {
const dir = testCorestoreDir('sh') const dir = testCorestoreDir('sh')
const store = new Corestore(dir) const store = new Corestore(dir)
@@ -1872,6 +1937,10 @@ test('runBinCommand man ls prints manual text', async (t) => {
t.ok(text.includes('SYNOPSIS')) t.ok(text.includes('SYNOPSIS'))
t.ok(text.includes('EXAMPLES')) t.ok(text.includes('EXAMPLES'))
t.ok(text.includes('ls')) t.ok(text.includes('ls'))
logs.length = 0
await runBinCommand(ctx, ['man', '-w'])
t.is(ctx.exitCode, 0)
t.is(logs.join('\n').trim(), '/share/man/man.json')
await store.close() await store.close()
rmSync(dir, { recursive: true, force: true }) rmSync(dir, { recursive: true, force: true })
}) })
@@ -3,7 +3,7 @@
"section": 1, "section": 1,
"title": "pattern matching utility", "title": "pattern matching utility",
"synopsis": [ "synopsis": [
"grep [-E|-F] [-i] [-v] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]" "grep [-E|-F] [-i] [-v] [-w] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]"
], ],
"description": "Searches input or files for lines matching a pattern. Uses JavaScript RegExp unless -F (fixed string). Not bit-identical to GNU grep.", "description": "Searches input or files for lines matching a pattern. Uses JavaScript RegExp unless -F (fixed string). Not bit-identical to GNU grep.",
"options": [ "options": [
@@ -23,6 +23,10 @@
"flag": "-v", "flag": "-v",
"meaning": "Invert match" "meaning": "Invert match"
}, },
{
"flag": "-w",
"meaning": "Match whole words (regex: \\b…\\b; fixed: non-alphanumeric boundaries)"
},
{ {
"flag": "-n", "flag": "-n",
"meaning": "Prefix lines with line number" "meaning": "Prefix lines with line number"
@@ -3,7 +3,7 @@
"section": 1, "section": 1,
"title": "display on-line manual pages", "title": "display on-line manual pages",
"synopsis": [ "synopsis": [
"man [-k keyword] [-f name] [-l] [[section] name]", "man [-k keyword] [-f name] [-l] [-w] [[section] name]",
"man reads /share/man/man.json on the system drive." "man reads /share/man/man.json on the system drive."
], ],
"description": "Displays manual pages from the merged JSON database. Section 1: /bin and git/shell pages. Section 7: handbook (man handbook) and developer guide (man devguide), merged at build from handbook/*.md and developer-guide/*.md.", "description": "Displays manual pages from the merged JSON database. Section 1: /bin and git/shell pages. Section 7: handbook (man handbook) and developer guide (man devguide), merged at build from handbook/*.md and developer-guide/*.md.",
@@ -19,6 +19,10 @@
{ {
"flag": "-l, --list", "flag": "-l, --list",
"meaning": "List pages grouped by category (/bin, git/shell, handbook, developer guide), then alphabetically" "meaning": "List pages grouped by category (/bin, git/shell, handbook, developer guide), then alphabetically"
},
{
"flag": "-w, --where, --path",
"meaning": "Print logical path to the manual database (/share/man/man.json)"
} }
], ],
"keywords": [ "keywords": [
@@ -33,7 +37,8 @@
], ],
"environment": [ "environment": [
"MANWIDTH — wrap width (default 72, min 40)", "MANWIDTH — wrap width (default 72, min 40)",
"NO_COLOR — disable bold headings on TTY" "NO_COLOR — disable bold headings on TTY",
"PAGER=bare-slice — insert section breaks in long pages (optional MAN_SLICE lines per chunk, default 24)"
], ],
"seeAlso": [ "seeAlso": [
{ {
+18 -7
View File
@@ -16,13 +16,14 @@ async function run(ctx, argv) {
let suppressErrors = false let suppressErrors = false
let forceFilename = false let forceFilename = false
let noFilename = false let noFilename = false
let word = false
const args = argv.slice(1) const args = argv.slice(1)
let i = 0 let i = 0
function usage() { function usage() {
ctx.console.error( ctx.console.error(
'usage: grep [-E|-F] [-i] [-v] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]' 'usage: grep [-E|-F] [-i] [-v] [-w] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]'
) )
ctx.exitCode = 2 ctx.exitCode = 2
} }
@@ -107,6 +108,9 @@ async function run(ctx, argv) {
case 'h': case 'h':
noFilename = true noFilename = true
break break
case 'w':
word = true
break
default: default:
ctx.console.error('grep: invalid option -- ' + c) ctx.console.error('grep: invalid option -- ' + c)
ctx.exitCode = 2 ctx.exitCode = 2
@@ -155,7 +159,7 @@ async function run(ctx, argv) {
let matchers let matchers
try { try {
matchers = buildMatchers(patterns, { fixed, icase }) matchers = buildMatchers(patterns, { fixed, icase, word })
} catch (e) { } catch (e) {
ctx.console.error('grep: ' + (e.message || e)) ctx.console.error('grep: ' + (e.message || e))
ctx.exitCode = 2 ctx.exitCode = 2
@@ -242,7 +246,7 @@ async function run(ctx, argv) {
/** /**
* @param {string[]} patterns * @param {string[]} patterns
* @param {{ fixed: boolean, icase: boolean }} o * @param {{ fixed: boolean, icase: boolean, word?: boolean }} o
*/ */
function buildMatchers(patterns, o) { function buildMatchers(patterns, o) {
if (patterns.length === 0) throw new Error('no pattern') if (patterns.length === 0) throw new Error('no pattern')
@@ -250,15 +254,22 @@ function buildMatchers(patterns, o) {
const pats = o.icase const pats = o.icase
? patterns.map((p) => p.toLowerCase()) ? patterns.map((p) => p.toLowerCase())
: patterns.slice() : patterns.slice()
return pats.map( return pats.map((p) => {
(p) => (line) => if (o.word) {
const esc = p.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&')
const flags = o.icase ? 'i' : ''
const re = new RegExp('(?:^|[^0-9A-Za-z_])' + esc + '(?:$|[^0-9A-Za-z_])', flags)
return (line) => re.test(line)
}
return (line) =>
o.icase ? line.toLowerCase().includes(p) : line.includes(p) o.icase ? line.toLowerCase().includes(p) : line.includes(p)
) })
} }
const flags = o.icase ? 'i' : '' const flags = o.icase ? 'i' : ''
return patterns.map((p) => { return patterns.map((p) => {
try { try {
const re = new RegExp(p, flags) const body = o.word ? '\\b(?:' + p + ')\\b' : p
const re = new RegExp(body, flags)
return (line) => re.test(line) return (line) => re.test(line)
} catch (e) { } catch (e) {
throw new Error('invalid regex: ' + (e.message || e)) throw new Error('invalid regex: ' + (e.message || e))
+38 -3
View File
@@ -5,9 +5,10 @@ async function run(ctx, argv) {
function usage() { function usage() {
ctx.console.error( ctx.console.error(
'usage: man [-k keyword] [-f name] [-l] [[section] name]\n' + 'usage: man [-k keyword] [-f name] [-l] [-w] [[section] name]\n' +
' Section 1: /bin; section 7: handbook + developer guide (man handbook, man devguide).\n' + ' Section 1: /bin; section 7: handbook + developer guide (man handbook, man devguide).\n' +
' Data: /share/man/man.json on the system drive.' ' Data: /share/man/man.json on the system drive.\n' +
' Long pages: set PAGER=bare-slice and optional MAN_SLICE=N (default 24) for section breaks.'
) )
ctx.exitCode = 2 ctx.exitCode = 2
} }
@@ -49,6 +50,10 @@ async function run(ctx, argv) {
mode = 'list' mode = 'list'
continue continue
} }
if (a === '-w' || a === '--where' || a === '--path') {
ctx.console.log('/share/man/man.json')
return
}
if (a.startsWith('-')) { if (a.startsWith('-')) {
ctx.console.error('man: unknown option: ' + a) ctx.console.error('man: unknown option: ' + a)
ctx.exitCode = 2 ctx.exitCode = 2
@@ -205,5 +210,35 @@ async function run(ctx, argv) {
ctx.exitCode = 1 ctx.exitCode = 1
return return
} }
ctx.console.log(bareManRenderPage(page, ctx, width).replace(/\n$/, '')) const rendered = bareManRenderPage(page, ctx, width).replace(/\n$/, '')
if (bareManSlicePage(rendered, env, ctx.console)) return
ctx.console.log(rendered)
}
/**
* @param {string} text
* @param {Record<string, string>} env
* @param {{ log: (s: string) => void }} cons
*/
function bareManSlicePage(text, env, cons) {
const pager = env.PAGER || ''
if (pager !== 'bare-slice' && pager !== 'bare_slice') return false
const raw = env.MAN_SLICE || '24'
const n = Number.parseInt(String(raw), 10)
const sliceLines = Number.isFinite(n) && n > 0 ? n : 24
const lines = text.split('\n')
const total = lines.length
for (let i = 0; i < total; i += sliceLines) {
const chunk = lines.slice(i, i + sliceLines).join('\n')
cons.log(chunk)
if (i + sliceLines < total) {
const hi = Math.min(i + sliceLines, total)
cons.log('')
cons.log(
`--- man: lines ${i + 1}-${hi} of ${total} (PAGER=bare-slice) ---`
)
cons.log('')
}
}
return true
} }
+18 -7
View File
@@ -78,13 +78,14 @@ async function run(ctx, argv) {
let suppressErrors = false let suppressErrors = false
let forceFilename = false let forceFilename = false
let noFilename = false let noFilename = false
let word = false
const args = argv.slice(1) const args = argv.slice(1)
let i = 0 let i = 0
function usage() { function usage() {
ctx.console.error( ctx.console.error(
'usage: grep [-E|-F] [-i] [-v] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]' 'usage: grep [-E|-F] [-i] [-v] [-w] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]'
) )
ctx.exitCode = 2 ctx.exitCode = 2
} }
@@ -169,6 +170,9 @@ async function run(ctx, argv) {
case 'h': case 'h':
noFilename = true noFilename = true
break break
case 'w':
word = true
break
default: default:
ctx.console.error('grep: invalid option -- ' + c) ctx.console.error('grep: invalid option -- ' + c)
ctx.exitCode = 2 ctx.exitCode = 2
@@ -217,7 +221,7 @@ async function run(ctx, argv) {
let matchers let matchers
try { try {
matchers = buildMatchers(patterns, { fixed, icase }) matchers = buildMatchers(patterns, { fixed, icase, word })
} catch (e) { } catch (e) {
ctx.console.error('grep: ' + (e.message || e)) ctx.console.error('grep: ' + (e.message || e))
ctx.exitCode = 2 ctx.exitCode = 2
@@ -304,7 +308,7 @@ async function run(ctx, argv) {
/** /**
* @param {string[]} patterns * @param {string[]} patterns
* @param {{ fixed: boolean, icase: boolean }} o * @param {{ fixed: boolean, icase: boolean, word?: boolean }} o
*/ */
function buildMatchers(patterns, o) { function buildMatchers(patterns, o) {
if (patterns.length === 0) throw new Error('no pattern') if (patterns.length === 0) throw new Error('no pattern')
@@ -312,15 +316,22 @@ function buildMatchers(patterns, o) {
const pats = o.icase const pats = o.icase
? patterns.map((p) => p.toLowerCase()) ? patterns.map((p) => p.toLowerCase())
: patterns.slice() : patterns.slice()
return pats.map( return pats.map((p) => {
(p) => (line) => if (o.word) {
const esc = p.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&')
const flags = o.icase ? 'i' : ''
const re = new RegExp('(?:^|[^0-9A-Za-z_])' + esc + '(?:$|[^0-9A-Za-z_])', flags)
return (line) => re.test(line)
}
return (line) =>
o.icase ? line.toLowerCase().includes(p) : line.includes(p) o.icase ? line.toLowerCase().includes(p) : line.includes(p)
) })
} }
const flags = o.icase ? 'i' : '' const flags = o.icase ? 'i' : ''
return patterns.map((p) => { return patterns.map((p) => {
try { try {
const re = new RegExp(p, flags) const body = o.word ? '\\b(?:' + p + ')\\b' : p
const re = new RegExp(body, flags)
return (line) => re.test(line) return (line) => re.test(line)
} catch (e) { } catch (e) {
throw new Error('invalid regex: ' + (e.message || e)) throw new Error('invalid regex: ' + (e.message || e))
+38 -3
View File
@@ -274,9 +274,10 @@ async function run(ctx, argv) {
function usage() { function usage() {
ctx.console.error( ctx.console.error(
'usage: man [-k keyword] [-f name] [-l] [[section] name]\n' + 'usage: man [-k keyword] [-f name] [-l] [-w] [[section] name]\n' +
' Section 1: /bin; section 7: handbook + developer guide (man handbook, man devguide).\n' + ' Section 1: /bin; section 7: handbook + developer guide (man handbook, man devguide).\n' +
' Data: /share/man/man.json on the system drive.' ' Data: /share/man/man.json on the system drive.\n' +
' Long pages: set PAGER=bare-slice and optional MAN_SLICE=N (default 24) for section breaks.'
) )
ctx.exitCode = 2 ctx.exitCode = 2
} }
@@ -318,6 +319,10 @@ async function run(ctx, argv) {
mode = 'list' mode = 'list'
continue continue
} }
if (a === '-w' || a === '--where' || a === '--path') {
ctx.console.log('/share/man/man.json')
return
}
if (a.startsWith('-')) { if (a.startsWith('-')) {
ctx.console.error('man: unknown option: ' + a) ctx.console.error('man: unknown option: ' + a)
ctx.exitCode = 2 ctx.exitCode = 2
@@ -474,5 +479,35 @@ async function run(ctx, argv) {
ctx.exitCode = 1 ctx.exitCode = 1
return return
} }
ctx.console.log(bareManRenderPage(page, ctx, width).replace(/\n$/, '')) const rendered = bareManRenderPage(page, ctx, width).replace(/\n$/, '')
if (bareManSlicePage(rendered, env, ctx.console)) return
ctx.console.log(rendered)
}
/**
* @param {string} text
* @param {Record<string, string>} env
* @param {{ log: (s: string) => void }} cons
*/
function bareManSlicePage(text, env, cons) {
const pager = env.PAGER || ''
if (pager !== 'bare-slice' && pager !== 'bare_slice') return false
const raw = env.MAN_SLICE || '24'
const n = Number.parseInt(String(raw), 10)
const sliceLines = Number.isFinite(n) && n > 0 ? n : 24
const lines = text.split('\n')
const total = lines.length
for (let i = 0; i < total; i += sliceLines) {
const chunk = lines.slice(i, i + sliceLines).join('\n')
cons.log(chunk)
if (i + sliceLines < total) {
const hi = Math.min(i + sliceLines, total)
cons.log('')
cons.log(
`--- man: lines ${i + 1}-${hi} of ${total} (PAGER=bare-slice) ---`
)
cons.log('')
}
}
return true
} }
+48 -9
View File
@@ -3,10 +3,31 @@
* Loaded by the booter with an injected ctx object (trusted replication source). * Loaded by the booter with an injected ctx object (trusted replication source).
* *
* Boot order: /etc/os-release /etc/motd /etc/bare-os/rc /etc/bare-os/rc.d/* * Boot order: /etc/os-release /etc/motd /etc/bare-os/rc /etc/bare-os/rc.d/*
* (sorted by filename) session banner interactive loop. * (sorted; digit-prefixed snippet names) session banner interactive loop.
* Use ctx.registerKernelShutdownHook(fn) for teardown before initd disposers. * Use ctx.registerKernelShutdownHook(fn) for teardown before initd disposers.
*/ */
/**
* @param {Record<string, unknown>} ctx
*/
function wantBootTrace(ctx) {
const v = ctx.env && ctx.env.BARE_OS_BOOT_TRACE
return v === '1' || v === 'true'
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} label
* @param {() => void | Promise<void>} fn
*/
async function bootTimed(ctx, label, fn) {
const t0 = Date.now()
await fn()
if (wantBootTrace(ctx)) {
ctx.console.error(`[boot] ${label}: ${Date.now() - t0}ms`)
}
}
/** /**
* @param {Record<string, unknown>} ctx * @param {Record<string, unknown>} ctx
* @param {string} text * @param {string} text
@@ -112,19 +133,37 @@ async function runBareOsRcDir(ctx) {
/** /**
* @param {Record<string, unknown>} ctx * @param {Record<string, unknown>} ctx
*/ */
function printSessionBanner(ctx) { async function printSessionBanner(ctx) {
ctx.console.log( const { drive, b4a, console } = ctx
for (const p of ['/etc/bare-os/banner', '/etc/issue']) {
try {
const buf = await drive.get(p)
if (buf) {
console.log(b4a.toString(buf).trimEnd())
return
}
} catch {
/* ignore */
}
}
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, && || ;, |, exit | services: systemctl list-units, journalctl -u UNIT | try: help, ls /bin, crontab -l'
) if (ctx.bareOsSkipRepl) {
console.log(
'Bare operating system — non-interactive session (BARE_OS_SKIP_REPL).'
)
return
}
console.log(defaultBanner)
} }
async function start(ctx) { async function start(ctx) {
const { readLine, execLine, console } = ctx const { readLine, execLine, console } = ctx
await printOsRelease(ctx) await bootTimed(ctx, 'os-release', () => printOsRelease(ctx))
await printMotd(ctx) await bootTimed(ctx, 'motd', () => printMotd(ctx))
await runRcFileAt(ctx, '/etc/bare-os/rc', 'rc') await bootTimed(ctx, 'rc', () => runRcFileAt(ctx, '/etc/bare-os/rc', 'rc'))
await runBareOsRcDir(ctx) await bootTimed(ctx, 'rc.d', () => runBareOsRcDir(ctx))
printSessionBanner(ctx) await printSessionBanner(ctx)
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