updates
This commit is contained in:
@@ -67,7 +67,7 @@ Work is sequenced for **POSIX/script ergonomics first**, then networking and lon
|
|||||||
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.
|
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`**).
|
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.
|
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.
|
4. **Init and protocol** — 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).
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ There is no arbitrary command execution, **`source`**, or control flow — it is
|
|||||||
**`bare-initd.js`**:
|
**`bare-initd.js`**:
|
||||||
|
|
||||||
- **`registerBareService({ name, start, stop?, description?, logPath? })`** — optional **`stop`** enables **`systemctl stop`** / **`restart`** for that unit; **`logPath`** is a logical VFS path for **`systemctl status`** / **`logs`**
|
- **`registerBareService({ name, start, stop?, description?, logPath? })`** — optional **`stop`** enables **`systemctl stop`** / **`restart`** for that unit; **`logPath`** is a logical VFS path for **`systemctl status`** / **`logs`**
|
||||||
- **`startBareInitd(ctx)`** — ensures **`/var/log/bare-os`** (see below), then sequential start, per-service `try/catch`, `[bare-initd] name: err` on failure; failures also append to **`/var/log/bare-os/initd.log`**; records per-unit runtime state (**`active`** / **`failed`**)
|
- **`startBareInitd(ctx)`** — ensures **`/var/log/bare-os`** (see below), then ordered start (skips units listed in **`~/.config/bare-os/initd/disabled.txt`** on the personal drive; optional **`~/.config/bare-os/units/<name>.unit`** with **`[Unit]`** **`After=`** for boot ordering; default **`bare-cron`** runs after **`kernel-logger`**), per-service `try/catch`, `[bare-initd] name: err` on failure; failures also append to **`/var/log/bare-os/initd.log`**; records per-unit runtime state (**`active`** / **`failed`**)
|
||||||
- **`listBareServices()`**, **`getBareServiceRuntime(name)`**, **`findBareServiceDefinition(name)`**, **`startBareService` / `stopBareService` / `restartBareService`** — introspection and lifecycle (used by the CLI below)
|
- **`listBareServices()`**, **`getBareServiceRuntime(name)`**, **`findBareServiceDefinition(name)`**, **`startBareService` / `stopBareService` / `restartBareService`** — introspection and lifecycle (used by the CLI below)
|
||||||
- **`registerKernelShutdownHook(fn)`** + **`runKernelShutdownHooks()`** — async-friendly teardown before disposers (REPL **`session.cleanup`** awaits hooks, then **`stopBareInitd()`**)
|
- **`registerKernelShutdownHook(fn)`** + **`runKernelShutdownHooks()`** — async-friendly teardown before disposers (REPL **`session.cleanup`** awaits hooks, then **`stopBareInitd()`**)
|
||||||
- **`registerBareInitdDisposer(fn)`** + **`stopBareInitd()`** — for intervals and synchronous teardown
|
- **`registerBareInitdDisposer(fn)`** + **`stopBareInitd()`** — for intervals and synchronous teardown
|
||||||
@@ -118,7 +118,7 @@ There is no arbitrary command execution, **`source`**, or control flow — it is
|
|||||||
|
|
||||||
Built-in **`kernel-logger`** wraps **`ctx.console.log` / `error`** to append UTF-8 lines to that path (with **`stop`** / **`restart`** support). **`startBareInitd`** also creates **`/var/log/bare-os`** and a short **`README`** there. Other services use the same tree (e.g. **`cron.log`**). Failures to write logs are swallowed so logging never kills the session.
|
Built-in **`kernel-logger`** wraps **`ctx.console.log` / `error`** to append UTF-8 lines to that path (with **`stop`** / **`restart`** support). **`startBareInitd`** also creates **`/var/log/bare-os`** and a short **`README`** there. Other services use the same tree (e.g. **`cron.log`**). Failures to write logs are swallowed so logging never kills the session.
|
||||||
|
|
||||||
**Service control:** **`/bin/systemctl`** is implemented by the booter (**`kernel-runner`** delegates to **`systemctl-cli.js`**), not by eval’d image JS. Subcommands: **`list`** / **`list-units`**, **`status`**, **`logs`**, **`start`**, **`stop`**, **`restart`**. **`journalctl -u UNIT`** (log tail only) shares the same backend. The legacy name **`bare-initctl`** is still accepted as an alias. There is **no** **`enable`** / **`disable`** (units are session-scoped, not persistent on the image). See **`man systemctl`**.
|
**Service control:** **`/bin/systemctl`** is implemented by the booter (**`kernel-runner`** delegates to **`systemctl-cli.js`**), not by eval’d image JS. Subcommands: **`list`** / **`list-units`** (shows **PRESET** enabled/disabled from **`disabled.txt`**), **`status`**, **`logs`**, **`start`**, **`stop`**, **`restart`**, **`enable`**, **`disable`**, **`is-enabled`**. **`journalctl -u UNIT`** (log tail only) shares the same backend. The legacy name **`bare-initctl`** is still accepted as an alias. **`enable`** / **`disable`** only affect the next **`startBareInitd`** (personal-drive config); runtime **`start`** / **`stop`** remain session commands. See **`man systemctl`**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ Hyperdrive entries carry optional **`metadata.bareOs`** (mode, uid, gid, names,
|
|||||||
|
|
||||||
| Command | Role |
|
| Command | Role |
|
||||||
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `systemctl`, `journalctl` | **Booter-delegated:** list/status/logs/start/stop/restart for **bare-initd** units (session-scoped; **`journalctl -u`** only); **`bare-initctl`** accepted as alias; see ch. 4 |
|
| `systemctl`, `journalctl` | **Booter-delegated:** list/status/logs/start/stop/restart/**enable**/**disable**/**is-enabled** for **bare-initd** units; preset file on personal drive; **`journalctl -u`** only; **`bare-initctl`** alias; see ch. 4 |
|
||||||
| `basename`, `dirname` | Path manipulation |
|
| `basename`, `dirname` | Path manipulation |
|
||||||
| `cat`, `head`, `tail`, `nl` | Text |
|
| `cat`, `head`, `tail`, `nl` | Text |
|
||||||
| `clear` | ANSI clear screen |
|
| `clear` | ANSI clear screen |
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Bare OS targets a **usable subset** of [POSIX.1-2017](https://pubs.opengroup.org
|
|||||||
| Expectation (full POSIX) | Bare OS reality |
|
| Expectation (full POSIX) | Bare OS reality |
|
||||||
| ---------------------------------------------- | ------------------------------------------------------------------------------- |
|
| ---------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||||
| **`sh`** grammar (`for`, subshells, …) | Line-at-a-time shell: builtins + `/bin`; lists with **`;`** (outside **`if`/`fi`**), **`&&`**, **`||`**, **`\|`** pipelines; bounded **`if`** **`then`** **[** **`else`** **]** **`fi`** (condition = last exit status like **`&&`**)—no **`for`**, groups, or full **`sh`** grammar. |
|
| **`sh`** grammar (`for`, subshells, …) | Line-at-a-time shell: builtins + `/bin`; lists with **`;`** (outside **`if`/`fi`**), **`&&`**, **`||`**, **`\|`** pipelines; bounded **`if`** **`then`** **[** **`else`** **]** **`fi`** (condition = last exit status like **`&&`**)—no **`for`**, groups, or full **`sh`** grammar. |
|
||||||
| **Processes, `fork`, pipes as OS primitives** | Pipelines are simulated by capturing **`console.log`** into the next command. |
|
| **Processes, `fork`, pipes as OS primitives** | Pipelines are simulated by capturing **`console.log`** (and merged **`console.error`** when **`2>&1`** is used) into the next command. |
|
||||||
| **`chown` / `chgrp` / real UIDs across users** | Single-session identity; metadata carries **uid/gid** for display and checks. |
|
| **`chown` / `chgrp` / real UIDs across users** | Single-session identity; metadata carries **uid/gid** for display and checks. |
|
||||||
| **FIFOs, `mknod`, real devices** | Not available; **`mkfifo`** is a documented stub. |
|
| **FIFOs, `mknod`, real devices** | Not available; **`mkfifo`** is a documented stub. |
|
||||||
| **`xargs`** full POSIX/GNU surface | Bounded implementation: **`ctx.runBinCommand`** only; **`-0`**, **`-n`** (and **`-nN`**); stdin/token/invocation caps (see **`src/xargs.js`**). |
|
| **`xargs`** full POSIX/GNU surface | Bounded implementation: **`ctx.runBinCommand`** only; **`-0`**, **`-n`** (and **`-nN`**); stdin/token/invocation caps (see **`src/xargs.js`**). |
|
||||||
@@ -37,7 +37,7 @@ Hyperdrive does not always behave like a POSIX directory tree. Empty directories
|
|||||||
|
|
||||||
### 2.3 Pseudo **`/proc`**, **`/sys`**, **`/run`**, **`/dev`**, and session **`/tmp`**
|
### 2.3 Pseudo **`/proc`**, **`/sys`**, **`/run`**, **`/dev`**, and session **`/tmp`**
|
||||||
|
|
||||||
- **`/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.
|
- **`/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. Extra Linux-shaped stubs include **`/proc/cpuinfo`**, **`/proc/loadavg`**, **`/proc/self/exe`**, and **`/dev/urandom`** (each **`readFile`** of urandom returns a **bounded** buffer; **not** suitable for cryptography).
|
||||||
- **`/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)
|
||||||
@@ -52,7 +52,7 @@ There is **no** **`chown`** / **`chgrp`** that changes stored ownership in a mul
|
|||||||
### 2.5 Copy and move
|
### 2.5 Copy and move
|
||||||
|
|
||||||
- **`cp`** — **`-R`/`-r`** recursive copy; follows the same marker convention (skips copying **`.bareos_empty`** as a separate “file” where appropriate).
|
- **`cp`** — **`-R`/`-r`** recursive copy; follows the same marker convention (skips copying **`.bareos_empty`** as a separate “file” where appropriate).
|
||||||
- **`mv`** — copy-tree + **`vfs.rm`** on the source (no single-key atomic rename API in the VFS).
|
- **`mv`** — Hyperdrive has no atomic **`rename`**; the general case is copy-tree + **`vfs.rm`** on the source. A **single regular file** to a non-directory destination uses **`readFile` → `writeFile` → `unlink`** on the source (same semantics, slightly less overhead than recursive **`rm`**).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -64,6 +64,8 @@ There is **no** **`chown`** / **`chgrp`** that changes stored ownership in a mul
|
|||||||
|
|
||||||
**Pipeline limits:** simulated pipe capture is bounded. Defaults: **`BARE_OS_PIPELINE_MAX_STAGES`** (32), **`BARE_OS_PIPELINE_MAX_BYTES`** (2 MiB), **`BARE_OS_PIPELINE_MAX_LINES`** (50 000). Exceeding a limit fails the pipeline with exit status **1** and an error on stderr.
|
**Pipeline limits:** simulated pipe capture is bounded. Defaults: **`BARE_OS_PIPELINE_MAX_STAGES`** (32), **`BARE_OS_PIPELINE_MAX_BYTES`** (2 MiB), **`BARE_OS_PIPELINE_MAX_LINES`** (50 000). Exceeding a limit fails the pipeline with exit status **1** and an error on stderr.
|
||||||
|
|
||||||
|
**Stderr redirection:** **`2>`** / **`2>>`** redirect **`console.error`** for the command (same byte/line caps as stdout when captured for a pipe). **`2>&1`** merges stderr into the stdout capture stream so both appear in the simulated pipe input for the next stage (and in a stdout redirect when it is the last command in the list).
|
||||||
|
|
||||||
**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`**:
|
||||||
|
|||||||
@@ -116,6 +116,19 @@ async function run(ctx, argv) {
|
|||||||
? dest.replace(/\/+$/, '') + '/' + base
|
? dest.replace(/\/+$/, '') + '/' + base
|
||||||
: dest
|
: dest
|
||||||
try {
|
try {
|
||||||
|
const st = await ctx.vfs.lstat(src)
|
||||||
|
const singleFileToFile =
|
||||||
|
sources.length === 1 &&
|
||||||
|
!destIsDir &&
|
||||||
|
st &&
|
||||||
|
st.type === 'file'
|
||||||
|
if (singleFileToFile) {
|
||||||
|
const buf = await ctx.vfs.readFile(src)
|
||||||
|
if (!buf) throw new Error('cannot read source')
|
||||||
|
await ctx.vfs.writeFile(target, buf)
|
||||||
|
await ctx.vfs.unlink(src)
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (!(await mvCopyPath(ctx, src, target, true))) {
|
if (!(await mvCopyPath(ctx, src, target, true))) {
|
||||||
throw new Error('cannot copy')
|
throw new Error('cannot copy')
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -23,9 +23,10 @@ HTTP in Pear/Bare uses **WHATWG Fetch** (Node `fetch` or **`bare-fetch`** on `ba
|
|||||||
| `-m` / `--max-time` | supported | Whole-request `AbortSignal` timeout (not separate connect timeout) |
|
| `-m` / `--max-time` | supported | Whole-request `AbortSignal` timeout (not separate connect timeout) |
|
||||||
| `-w` / `--write-out` | partial | `%{http_code}`, `%{url_effective}`, `%{size_download}`; `%{num_redirects}` non-zero only for `-I -L` manual hop path |
|
| `-w` / `--write-out` | partial | `%{http_code}`, `%{url_effective}`, `%{size_download}`; `%{num_redirects}` non-zero only for `-I -L` manual hop path |
|
||||||
| `-V` / `--version`, `-h` / `--help` | supported | |
|
| `-V` / `--version`, `-h` / `--help` | supported | |
|
||||||
| Cookie jar, `--cookie`, `-b` | out of scope | Needs persistent jar + spec |
|
| Cookie jar, `--cookie`, `-b`, `-c` / `--cookie-jar` | supported | JSON map `hostname → { name → value }` on **`~/.config/bare-os/curl/cookies.json`** (or path from `-c`); `-b` file or `name=value`; see `curl-cli.js` |
|
||||||
| `--connect-timeout`, `-Y`, `-y` | out of scope | Fetch exposes one abort timer |
|
| `--connect-timeout`, `-Y`, `-y` | out of scope | Fetch exposes one abort timer |
|
||||||
| TLS client certs, `--cacert`, `-k` | out of scope | Stack / trust store |
|
| `--cacert`, `-k` / `--insecure` | partial | Node **undici** `Agent` when using global `fetch` (not when `ctx.httpFetch` is overridden) |
|
||||||
|
| TLS client certs (mutual TLS) | out of scope | Stack only |
|
||||||
| HTTP/2, HTTP/3, SOCKS, FTP, SCP | out of scope | |
|
| HTTP/2, HTTP/3, SOCKS, FTP, SCP | out of scope | |
|
||||||
| `-J` content-disposition filename | supported | With **`-O`**; uses **`Content-Disposition`** basename only; rejects **`..`** and absolute paths in the server-provided name |
|
| `-J` content-disposition filename | supported | With **`-O`**; uses **`Content-Disposition`** basename only; rejects **`..`** and absolute paths in the server-provided name |
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* User initd config on the personal drive: disabled units and optional drop-in ordering.
|
||||||
|
* Paths are logical VFS paths under $HOME.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import b4a from 'b4a'
|
||||||
|
|
||||||
|
/** One unit name per line; blank lines and `#…` comments ignored. */
|
||||||
|
export const BARE_INITD_DISABLED_FILE = '~/.config/bare-os/initd/disabled.txt'
|
||||||
|
|
||||||
|
/** Optional `unit.unit` files: `[Unit]` with `After=other-unit …`. */
|
||||||
|
export const BARE_INITD_UNITS_DIR = '~/.config/bare-os/units'
|
||||||
|
|
||||||
|
/** Built-in ordering when no drop-in file exists (kernel-logger before cron). */
|
||||||
|
export const BARE_INITD_DEFAULT_AFTER = Object.freeze({
|
||||||
|
'bare-cron': ['kernel-logger']
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
||||||
|
* @returns {Promise<Set<string>>}
|
||||||
|
*/
|
||||||
|
export async function readInitdDisabledSet(vfs) {
|
||||||
|
const out = new Set()
|
||||||
|
try {
|
||||||
|
const buf = await vfs.readFile(BARE_INITD_DISABLED_FILE)
|
||||||
|
if (!buf) return out
|
||||||
|
const text = b4a.toString(buf, 'utf8')
|
||||||
|
for (const line of text.split(/\r?\n/)) {
|
||||||
|
const t = line.trim()
|
||||||
|
if (!t || t.startsWith('#')) continue
|
||||||
|
out.add(t)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* missing file */
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{
|
||||||
|
* mkdir: (p: string, opts?: { recursive?: boolean }) => Promise<unknown>,
|
||||||
|
* writeFile: (p: string, buf: Uint8Array) => Promise<unknown>,
|
||||||
|
* unlink: (p: string) => Promise<unknown>
|
||||||
|
* }} vfs
|
||||||
|
* @param {Set<string>} disabled
|
||||||
|
*/
|
||||||
|
export async function writeInitdDisabledSet(vfs, disabled) {
|
||||||
|
await vfs.mkdir('~/.config/bare-os/initd', { recursive: true })
|
||||||
|
if (disabled.size === 0) {
|
||||||
|
try {
|
||||||
|
await vfs.unlink(BARE_INITD_DISABLED_FILE)
|
||||||
|
} catch {
|
||||||
|
/* absent */
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const body = [...disabled].sort().join('\n') + '\n'
|
||||||
|
await vfs.writeFile(BARE_INITD_DISABLED_FILE, b4a.from(body, 'utf8'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
function parseAfterLine(text) {
|
||||||
|
const m = text.match(/^\s*After\s*=\s*(.+)$/im)
|
||||||
|
if (!m) return []
|
||||||
|
return m[1]
|
||||||
|
.split(/[\s,]+/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
||||||
|
* @param {string} unitName
|
||||||
|
* @returns {Promise<string[]>}
|
||||||
|
*/
|
||||||
|
export async function readUnitAfterFromDropIn(vfs, unitName) {
|
||||||
|
if (!/^[a-zA-Z0-9._-]+$/.test(unitName)) return []
|
||||||
|
const path = `${BARE_INITD_UNITS_DIR}/${unitName}.unit`
|
||||||
|
try {
|
||||||
|
const buf = await vfs.readFile(path)
|
||||||
|
if (!buf) return []
|
||||||
|
const text = b4a.toString(buf, 'utf8')
|
||||||
|
return parseAfterLine(text)
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
||||||
|
* @param {{ name: string }[]} services
|
||||||
|
* @param {Readonly<Record<string, string[]>>} defaultAfter
|
||||||
|
* @returns {Promise<Map<string, string[]>>}
|
||||||
|
*/
|
||||||
|
export async function loadInitdAfterMap(vfs, services, defaultAfter) {
|
||||||
|
const map = new Map()
|
||||||
|
for (const s of services) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Topological sort; cycles fall back to registration order for the remainder.
|
||||||
|
* @template T extends { name: string }
|
||||||
|
* @param {T[]} services
|
||||||
|
* @param {Set<string>} disabled
|
||||||
|
* @param {Map<string, string[]>} afterMap
|
||||||
|
* @returns {T[]}
|
||||||
|
*/
|
||||||
|
export function sortServicesForBoot(services, disabled, afterMap) {
|
||||||
|
const active = services.filter((s) => !disabled.has(s.name))
|
||||||
|
const nameSet = new Set(active.map((s) => s.name))
|
||||||
|
/** @type {Map<string, Set<string>>} */
|
||||||
|
const prereq = new Map()
|
||||||
|
for (const s of active) {
|
||||||
|
const inc = new Set()
|
||||||
|
for (const a of afterMap.get(s.name) || []) {
|
||||||
|
if (nameSet.has(a)) inc.add(a)
|
||||||
|
}
|
||||||
|
prereq.set(s.name, inc)
|
||||||
|
}
|
||||||
|
const remaining = new Set(active.map((s) => s.name))
|
||||||
|
/** @type {T[]} */
|
||||||
|
const ordered = []
|
||||||
|
while (remaining.size) {
|
||||||
|
const ready = [...remaining].filter((n) => {
|
||||||
|
for (const p of prereq.get(n) || []) {
|
||||||
|
if (remaining.has(p)) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if (!ready.length) {
|
||||||
|
const regOrder = active.map((s) => s.name).filter((n) => remaining.has(n))
|
||||||
|
for (const n of regOrder) {
|
||||||
|
const svc = active.find((s) => s.name === n)
|
||||||
|
if (svc) ordered.push(svc)
|
||||||
|
remaining.delete(n)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ready.sort()
|
||||||
|
for (const n of ready) {
|
||||||
|
const svc = active.find((s) => s.name === n)
|
||||||
|
if (svc) ordered.push(svc)
|
||||||
|
remaining.delete(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ordered
|
||||||
|
}
|
||||||
@@ -9,6 +9,12 @@ import {
|
|||||||
INITD_LOG,
|
INITD_LOG,
|
||||||
KERNEL_CONSOLE_LOG
|
KERNEL_CONSOLE_LOG
|
||||||
} from './bare-os-var-log.js'
|
} from './bare-os-var-log.js'
|
||||||
|
import {
|
||||||
|
BARE_INITD_DEFAULT_AFTER,
|
||||||
|
loadInitdAfterMap,
|
||||||
|
readInitdDisabledSet,
|
||||||
|
sortServicesForBoot
|
||||||
|
} from './bare-initd-user.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
@@ -201,7 +207,18 @@ export async function restartBareService(ctx, name) {
|
|||||||
export async function startBareInitd(ctx) {
|
export async function startBareInitd(ctx) {
|
||||||
runtime.clear()
|
runtime.clear()
|
||||||
await ensureBareOsVarLogTree(ctx)
|
await ensureBareOsVarLogTree(ctx)
|
||||||
for (const s of registry) {
|
const vfs = ctx.vfs
|
||||||
|
let ordered = registry
|
||||||
|
if (vfs && typeof vfs.readFile === 'function') {
|
||||||
|
const disabled = await readInitdDisabledSet(vfs)
|
||||||
|
const afterMap = await loadInitdAfterMap(
|
||||||
|
vfs,
|
||||||
|
registry,
|
||||||
|
BARE_INITD_DEFAULT_AFTER
|
||||||
|
)
|
||||||
|
ordered = sortServicesForBoot(registry, disabled, afterMap)
|
||||||
|
}
|
||||||
|
for (const s of ordered) {
|
||||||
const t0 = Date.now()
|
const t0 = Date.now()
|
||||||
try {
|
try {
|
||||||
await s.start(ctx)
|
await s.start(ctx)
|
||||||
|
|||||||
@@ -144,12 +144,152 @@ function expandWriteOut(fmt, vars) {
|
|||||||
|
|
||||||
const CURL_MAX_REDIRECTS = 50
|
const CURL_MAX_REDIRECTS = 50
|
||||||
|
|
||||||
|
/** Default persistent jar on the personal drive (JSON per CLI_PARITY). */
|
||||||
|
export const BARE_OS_CURL_COOKIE_JAR = '~/.config/bare-os/curl/cookies.json'
|
||||||
|
|
||||||
|
function curlHostFromUrl(urlStr) {
|
||||||
|
try {
|
||||||
|
return new URL(urlStr).hostname
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {Record<string, Record<string, string>>} */
|
||||||
|
function parseCookieJarJson(text) {
|
||||||
|
try {
|
||||||
|
const o = JSON.parse(text)
|
||||||
|
if (!o || typeof o !== 'object') return {}
|
||||||
|
/** @type {Record<string, Record<string, string>>} */
|
||||||
|
const out = {}
|
||||||
|
for (const [h, bag] of Object.entries(o)) {
|
||||||
|
if (!bag || typeof bag !== 'object') continue
|
||||||
|
out[String(h)] = {}
|
||||||
|
for (const [k, v] of Object.entries(bag)) {
|
||||||
|
out[String(h)][String(k)] = String(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
||||||
|
* @param {string} path
|
||||||
|
*/
|
||||||
|
async function loadCookieJarFile(vfs, path) {
|
||||||
|
const buf = await vfs.readFile(path)
|
||||||
|
if (!buf) return {}
|
||||||
|
return parseCookieJarJson(b4a.toString(buf, 'utf8'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, Record<string, string>>} jar
|
||||||
|
* @param {string} host
|
||||||
|
* @param {string} line
|
||||||
|
*/
|
||||||
|
function mergeInlineCookieLine(jar, host, line) {
|
||||||
|
if (!host) return
|
||||||
|
for (const part of line.split(';')) {
|
||||||
|
const p = part.trim()
|
||||||
|
const eq = p.indexOf('=')
|
||||||
|
if (eq < 1) continue
|
||||||
|
const name = p.slice(0, eq).trim()
|
||||||
|
const val = p.slice(eq + 1).trim()
|
||||||
|
if (!jar[host]) jar[host] = {}
|
||||||
|
jar[host][name] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, Record<string, string>>} target
|
||||||
|
* @param {Record<string, Record<string, string>>} add
|
||||||
|
*/
|
||||||
|
function mergeCookieJars(target, add) {
|
||||||
|
for (const [h, bag] of Object.entries(add)) {
|
||||||
|
if (!target[h]) target[h] = {}
|
||||||
|
Object.assign(target[h], bag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, Record<string, string>>} jar
|
||||||
|
* @param {string} host
|
||||||
|
*/
|
||||||
|
function cookieHeaderForHost(jar, host) {
|
||||||
|
const bag = jar[host]
|
||||||
|
if (!bag || !Object.keys(bag).length) return ''
|
||||||
|
return Object.entries(bag)
|
||||||
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
|
.join('; ')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, Record<string, string>>} jar
|
||||||
|
* @param {string} host
|
||||||
|
* @param {string} headerVal
|
||||||
|
*/
|
||||||
|
function applySetCookieHeader(jar, host, headerVal) {
|
||||||
|
if (!host || !headerVal) return
|
||||||
|
const first = String(headerVal).split(';')[0].trim()
|
||||||
|
const eq = first.indexOf('=')
|
||||||
|
if (eq < 1) return
|
||||||
|
const name = first.slice(0, eq).trim()
|
||||||
|
const val = first.slice(eq + 1).trim()
|
||||||
|
if (!jar[host]) jar[host] = {}
|
||||||
|
jar[host][name] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Response} res */
|
||||||
|
function gatherSetCookieValues(res) {
|
||||||
|
const h = res.headers
|
||||||
|
if (typeof h.getSetCookie === 'function') return h.getSetCookie()
|
||||||
|
const v = h.get('Set-Cookie')
|
||||||
|
return v ? [v] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node undici Agent for `--cacert` / `-k` (no effect when ctx.httpFetch is a stub).
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
||||||
|
* @param {{ insecure?: boolean, cacertPath?: string | null }} opts
|
||||||
|
* @returns {Promise<{ dispatcher?: unknown, error?: string }>}
|
||||||
|
*/
|
||||||
|
async function buildTlsFetchExtras(ctx, vfs, opts) {
|
||||||
|
const insecure = !!opts.insecure
|
||||||
|
const cacertPath = opts.cacertPath || null
|
||||||
|
if (!insecure && !cacertPath) return {}
|
||||||
|
if (typeof ctx.httpFetch === 'function') {
|
||||||
|
return {
|
||||||
|
error:
|
||||||
|
'curl: --cacert / -k are not applied when ctx.httpFetch is set (use native fetch)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const undici = await import('undici')
|
||||||
|
const Agent = undici.Agent
|
||||||
|
if (insecure) {
|
||||||
|
return { dispatcher: new Agent({ connect: { rejectUnauthorized: false } }) }
|
||||||
|
}
|
||||||
|
if (!cacertPath) return {}
|
||||||
|
const buf = await vfs.readFile(cacertPath)
|
||||||
|
if (!buf) return { error: 'curl: cannot read --cacert file' }
|
||||||
|
const ca = b4a.toString(buf, 'utf8')
|
||||||
|
return { dispatcher: new Agent({ connect: { ca } }) }
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e && e.message ? e.message : String(e)
|
||||||
|
return { error: 'curl: TLS options need undici (Node): ' + msg }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* curl -I -L: send HEAD first; after any 3xx, follow with GET (curl man: "GET is used after redirect").
|
* curl -I -L: send HEAD first; after any 3xx, follow with GET (curl man: "GET is used after redirect").
|
||||||
* Using fetch(HEAD, redirect:'follow') keeps HEAD on each hop and breaks many CDNs; manual hops match curl.
|
* Using fetch(HEAD, redirect:'follow') keeps HEAD on each hop and breaks many CDNs; manual hops match curl.
|
||||||
* @returns {{ response: Response, redirectCount: number }}
|
* @returns {{ response: Response, redirectCount: number }}
|
||||||
*/
|
*/
|
||||||
async function fetchHeadWithLocationFollow(fetchFn, startUrl, hdr, signal) {
|
async function fetchHeadWithLocationFollow(fetchFn, startUrl, hdr, signal, fetchExtra = {}) {
|
||||||
let url = startUrl
|
let url = startUrl
|
||||||
let method = 'HEAD'
|
let method = 'HEAD'
|
||||||
let redirectCount = 0
|
let redirectCount = 0
|
||||||
@@ -159,7 +299,8 @@ async function fetchHeadWithLocationFollow(fetchFn, startUrl, hdr, signal) {
|
|||||||
headers: hdr,
|
headers: hdr,
|
||||||
redirect: 'manual',
|
redirect: 'manual',
|
||||||
signal,
|
signal,
|
||||||
body: undefined
|
body: undefined,
|
||||||
|
...fetchExtra
|
||||||
})
|
})
|
||||||
if (res.status >= 300 && res.status < 400) {
|
if (res.status >= 300 && res.status < 400) {
|
||||||
const loc = res.headers.get('Location')
|
const loc = res.headers.get('Location')
|
||||||
@@ -225,6 +366,13 @@ export async function runCurlCli(ctx, argv) {
|
|||||||
let jsonBody = null
|
let jsonBody = null
|
||||||
/** @type {string | null} */
|
/** @type {string | null} */
|
||||||
let uploadPath = null
|
let uploadPath = null
|
||||||
|
let insecureTls = false
|
||||||
|
/** @type {string | null} */
|
||||||
|
let cacertPath = null
|
||||||
|
/** @type {string | null} */
|
||||||
|
let cookieJarOut = null
|
||||||
|
/** @type {string[]} */
|
||||||
|
const cookieSpecs = []
|
||||||
|
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
const urls = []
|
const urls = []
|
||||||
@@ -429,6 +577,47 @@ export async function runCurlCli(ctx, argv) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (a === '-k' || a === '--insecure') {
|
||||||
|
insecureTls = true
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (a === '--cacert') {
|
||||||
|
if (i + 1 >= args.length) {
|
||||||
|
ctx.console.error('curl: option requires an argument: --cacert')
|
||||||
|
ctx.exitCode = 2
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cacertPath = String(args[++i])
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (a === '-b' || a === '--cookie') {
|
||||||
|
if (i + 1 >= args.length) {
|
||||||
|
ctx.console.error('curl: option requires an argument: ' + a)
|
||||||
|
ctx.exitCode = 2
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cookieSpecs.push(String(args[++i]))
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (a.startsWith('-b') && a.length > 2) {
|
||||||
|
cookieSpecs.push(a.slice(2))
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (a === '-c' || a === '--cookie-jar') {
|
||||||
|
if (i + 1 >= args.length) {
|
||||||
|
ctx.console.error('curl: option requires an argument: ' + a)
|
||||||
|
ctx.exitCode = 2
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cookieJarOut = String(args[++i])
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (a === '-I' || a === '--head') {
|
if (a === '-I' || a === '--head') {
|
||||||
headOnly = true
|
headOnly = true
|
||||||
i++
|
i++
|
||||||
@@ -502,6 +691,15 @@ export async function runCurlCli(ctx, argv) {
|
|||||||
case 'J':
|
case 'J':
|
||||||
remoteHeaderName = true
|
remoteHeaderName = true
|
||||||
break
|
break
|
||||||
|
case 'k':
|
||||||
|
insecureTls = true
|
||||||
|
break
|
||||||
|
case 'b':
|
||||||
|
ctx.console.error(
|
||||||
|
'curl: bundled -b requires a separate argument (use -b cookie.txt)'
|
||||||
|
)
|
||||||
|
ctx.exitCode = 2
|
||||||
|
return
|
||||||
default:
|
default:
|
||||||
ctx.console.error('curl: invalid option -- ' + c)
|
ctx.console.error('curl: invalid option -- ' + c)
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
@@ -567,7 +765,26 @@ export async function runCurlCli(ctx, argv) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const hdr = new Headers()
|
if ((cookieSpecs.length > 0 || cookieJarOut) && !vfs) {
|
||||||
|
ctx.console.error('curl: -b / -c require ctx.vfs')
|
||||||
|
ctx.exitCode = 2
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tlsExtras = await buildTlsFetchExtras(ctx, vfs, {
|
||||||
|
insecure: insecureTls,
|
||||||
|
cacertPath
|
||||||
|
})
|
||||||
|
if (tlsExtras.error) {
|
||||||
|
if (!silent || showError) ctx.console.error(tlsExtras.error)
|
||||||
|
ctx.exitCode = 2
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const fetchTlsExtra = tlsExtras.dispatcher
|
||||||
|
? { dispatcher: tlsExtras.dispatcher }
|
||||||
|
: {}
|
||||||
|
|
||||||
|
const baseHdr = new Headers()
|
||||||
for (const line of headers) {
|
for (const line of headers) {
|
||||||
const colon = line.indexOf(':')
|
const colon = line.indexOf(':')
|
||||||
if (colon < 1) {
|
if (colon < 1) {
|
||||||
@@ -575,30 +792,44 @@ export async function runCurlCli(ctx, argv) {
|
|||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hdr.set(line.slice(0, colon).trim(), line.slice(colon + 1).trim())
|
baseHdr.set(line.slice(0, colon).trim(), line.slice(colon + 1).trim())
|
||||||
}
|
}
|
||||||
if (userColonPass) {
|
if (userColonPass) {
|
||||||
const colon = userColonPass.indexOf(':')
|
const colon = userColonPass.indexOf(':')
|
||||||
const user = colon >= 0 ? userColonPass.slice(0, colon) : userColonPass
|
const user = colon >= 0 ? userColonPass.slice(0, colon) : userColonPass
|
||||||
const pass = colon >= 0 ? userColonPass.slice(colon + 1) : ''
|
const pass = colon >= 0 ? userColonPass.slice(colon + 1) : ''
|
||||||
hdr.set('Authorization', basicAuthHeader(user, pass))
|
baseHdr.set('Authorization', basicAuthHeader(user, pass))
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const ua = userAgentSequence.length
|
const ua = userAgentSequence.length
|
||||||
? userAgentSequence[userAgentSequence.length - 1]
|
? userAgentSequence[userAgentSequence.length - 1]
|
||||||
: DEFAULT_CURL_USER_AGENT
|
: DEFAULT_CURL_USER_AGENT
|
||||||
hdr.set('User-Agent', ua)
|
baseHdr.set('User-Agent', ua)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Record<string, Record<string, string>>} */
|
||||||
|
const sessionJar = {}
|
||||||
|
if (vfs && cookieJarOut) {
|
||||||
|
mergeCookieJars(sessionJar, await loadCookieJarFile(vfs, cookieJarOut))
|
||||||
|
}
|
||||||
|
if (vfs && cookieSpecs.length) {
|
||||||
|
for (const spec of cookieSpecs) {
|
||||||
|
if (!spec.includes('=')) {
|
||||||
|
mergeCookieJars(sessionJar, await loadCookieJarFile(vfs, spec))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {string | Uint8Array | undefined} */
|
/** @type {string | Uint8Array | undefined} */
|
||||||
let body = undefined
|
let body = undefined
|
||||||
if (jsonBody) {
|
if (jsonBody) {
|
||||||
if (!hdr.has('Content-Type')) hdr.set('Content-Type', 'application/json')
|
if (!baseHdr.has('Content-Type'))
|
||||||
|
baseHdr.set('Content-Type', 'application/json')
|
||||||
body = jsonBody
|
body = jsonBody
|
||||||
} else if (dataChunks.length > 0) {
|
} else if (dataChunks.length > 0) {
|
||||||
const joined = dataChunks.join('&')
|
const joined = dataChunks.join('&')
|
||||||
if (!hdr.has('Content-Type') && !dataBinary)
|
if (!baseHdr.has('Content-Type') && !dataBinary)
|
||||||
hdr.set('Content-Type', 'application/x-www-form-urlencoded')
|
baseHdr.set('Content-Type', 'application/x-www-form-urlencoded')
|
||||||
body = joined
|
body = joined
|
||||||
} else if (uploadPath) {
|
} else if (uploadPath) {
|
||||||
const buf = await vfs.readFile(uploadPath)
|
const buf = await vfs.readFile(uploadPath)
|
||||||
@@ -609,8 +840,8 @@ export async function runCurlCli(ctx, argv) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
body = new Uint8Array(buf)
|
body = new Uint8Array(buf)
|
||||||
if (!hdr.has('Content-Type'))
|
if (!baseHdr.has('Content-Type'))
|
||||||
hdr.set('Content-Type', 'application/octet-stream')
|
baseHdr.set('Content-Type', 'application/octet-stream')
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastSize = 0
|
let lastSize = 0
|
||||||
@@ -618,6 +849,22 @@ export async function runCurlCli(ctx, argv) {
|
|||||||
|
|
||||||
for (let ui = 0; ui < urls.length; ui++) {
|
for (let ui = 0; ui < urls.length; ui++) {
|
||||||
const url = urls[ui]
|
const url = urls[ui]
|
||||||
|
const reqHdr = new Headers(baseHdr)
|
||||||
|
const host = curlHostFromUrl(url)
|
||||||
|
if (vfs && cookieSpecs.length) {
|
||||||
|
for (const spec of cookieSpecs) {
|
||||||
|
if (spec.includes('=')) mergeInlineCookieLine(sessionJar, host, spec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const jarCookie = cookieHeaderForHost(sessionJar, host)
|
||||||
|
if (jarCookie) {
|
||||||
|
if (reqHdr.has('Cookie')) {
|
||||||
|
reqHdr.set('Cookie', reqHdr.get('Cookie') + '; ' + jarCookie)
|
||||||
|
} else {
|
||||||
|
reqHdr.set('Cookie', jarCookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {string | null} */
|
/** @type {string | null} */
|
||||||
let outForUrl = null
|
let outForUrl = null
|
||||||
if (outputPath != null) {
|
if (outputPath != null) {
|
||||||
@@ -646,18 +893,20 @@ export async function runCurlCli(ctx, argv) {
|
|||||||
const { response, redirectCount } = await fetchHeadWithLocationFollow(
|
const { response, redirectCount } = await fetchHeadWithLocationFollow(
|
||||||
fetchFn,
|
fetchFn,
|
||||||
url,
|
url,
|
||||||
hdr,
|
reqHdr,
|
||||||
ac ? ac.signal : undefined
|
ac ? ac.signal : undefined,
|
||||||
|
fetchTlsExtra
|
||||||
)
|
)
|
||||||
res = response
|
res = response
|
||||||
numRedirects = redirectCount
|
numRedirects = redirectCount
|
||||||
} else {
|
} else {
|
||||||
res = await fetchFn(url, {
|
res = await fetchFn(url, {
|
||||||
method: m,
|
method: m,
|
||||||
headers: hdr,
|
headers: reqHdr,
|
||||||
body: m === 'HEAD' || m === 'GET' ? undefined : body,
|
body: m === 'HEAD' || m === 'GET' ? undefined : body,
|
||||||
redirect: location ? 'follow' : 'manual',
|
redirect: location ? 'follow' : 'manual',
|
||||||
signal: ac ? ac.signal : undefined
|
signal: ac ? ac.signal : undefined,
|
||||||
|
...fetchTlsExtra
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -671,6 +920,18 @@ export async function runCurlCli(ctx, argv) {
|
|||||||
|
|
||||||
lastUrl = res.url || url
|
lastUrl = res.url || url
|
||||||
|
|
||||||
|
const cookieHost = curlHostFromUrl(lastUrl) || host
|
||||||
|
for (const sc of gatherSetCookieValues(res)) {
|
||||||
|
applySetCookieHeader(sessionJar, cookieHost, sc)
|
||||||
|
}
|
||||||
|
if (cookieJarOut && vfs) {
|
||||||
|
await vfs.mkdir('~/.config/bare-os/curl', { recursive: true })
|
||||||
|
await vfs.writeFile(
|
||||||
|
cookieJarOut,
|
||||||
|
b4a.from(JSON.stringify(sessionJar), 'utf8')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (remoteName && remoteHeaderName && res.ok && m !== 'HEAD') {
|
if (remoteName && remoteHeaderName && res.ok && m !== 'HEAD') {
|
||||||
const raw = filenameFromContentDisposition(
|
const raw = filenameFromContentDisposition(
|
||||||
res.headers.get('Content-Disposition')
|
res.headers.get('Content-Disposition')
|
||||||
|
|||||||
@@ -305,6 +305,21 @@ export function tokenize(line) {
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (c === '2' && line[i + 1] === '>') {
|
||||||
|
if (line[i + 2] === '>') {
|
||||||
|
tokens.push({ type: 'op', value: '2>>' })
|
||||||
|
i += 3
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (line[i + 2] === '&' && line[i + 3] === '1') {
|
||||||
|
tokens.push({ type: 'op', value: '2>&1' })
|
||||||
|
i += 4
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tokens.push({ type: 'op', value: '2>' })
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (c === '>') {
|
if (c === '>') {
|
||||||
if (line[i + 1] === '>') {
|
if (line[i + 1] === '>') {
|
||||||
tokens.push({ type: 'op', value: '>>' })
|
tokens.push({ type: 'op', value: '>>' })
|
||||||
@@ -420,7 +435,16 @@ export function expandWord(s, env) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {{ argv: string[], assign: Record<string, string>, redirIn: string | null, redirOut: string | null, redirAppend: boolean }} SimpleCmd
|
* @typedef {{
|
||||||
|
* argv: string[],
|
||||||
|
* assign: Record<string, string>,
|
||||||
|
* redirIn: string | null,
|
||||||
|
* redirOut: string | null,
|
||||||
|
* redirAppend: boolean,
|
||||||
|
* redirErr: string | null,
|
||||||
|
* redirErrAppend: boolean,
|
||||||
|
* mergeStderrToStdout: boolean
|
||||||
|
* }} SimpleCmd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -448,6 +472,9 @@ function parseSimpleCommand(seg) {
|
|||||||
let redirIn = null
|
let redirIn = null
|
||||||
let redirOut = null
|
let redirOut = null
|
||||||
let redirAppend = false
|
let redirAppend = false
|
||||||
|
let redirErr = null
|
||||||
|
let redirErrAppend = false
|
||||||
|
let mergeStderrToStdout = false
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
const argvWords = []
|
const argvWords = []
|
||||||
let seenCommand = false
|
let seenCommand = false
|
||||||
@@ -456,6 +483,20 @@ function parseSimpleCommand(seg) {
|
|||||||
while (w < seg.length) {
|
while (w < seg.length) {
|
||||||
const t = seg[w]
|
const t = seg[w]
|
||||||
if (t.type === 'op') {
|
if (t.type === 'op') {
|
||||||
|
if (t.value === '2>' || t.value === '2>>') {
|
||||||
|
const n = seg[w + 1]
|
||||||
|
if (n && n.type === 'word') {
|
||||||
|
redirErrAppend = t.value === '2>>'
|
||||||
|
redirErr = n.value
|
||||||
|
w += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (t.value === '2>&1') {
|
||||||
|
mergeStderrToStdout = true
|
||||||
|
w++
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (t.value === '>' || t.value === '>>') {
|
if (t.value === '>' || t.value === '>>') {
|
||||||
const n = seg[w + 1]
|
const n = seg[w + 1]
|
||||||
if (n && n.type === 'word') {
|
if (n && n.type === 'word') {
|
||||||
@@ -493,6 +534,28 @@ function parseSimpleCommand(seg) {
|
|||||||
|
|
||||||
let i = 0
|
let i = 0
|
||||||
while (i < argvWords.length) {
|
while (i < argvWords.length) {
|
||||||
|
if (
|
||||||
|
argvWords[i] === '2' &&
|
||||||
|
argvWords[i + 1] === '>' &&
|
||||||
|
argvWords[i + 2] === '&' &&
|
||||||
|
argvWords[i + 3] === '1'
|
||||||
|
) {
|
||||||
|
mergeStderrToStdout = true
|
||||||
|
argvWords.splice(i, 4)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (argvWords[i] === '2' && argvWords[i + 1] === '>') {
|
||||||
|
redirErr = argvWords[i + 2] ?? ''
|
||||||
|
redirErrAppend = false
|
||||||
|
argvWords.splice(i, 3)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (argvWords[i] === '2' && argvWords[i + 1] === '>>') {
|
||||||
|
redirErr = argvWords[i + 2] ?? ''
|
||||||
|
redirErrAppend = true
|
||||||
|
argvWords.splice(i, 3)
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (argvWords[i] === '>') {
|
if (argvWords[i] === '>') {
|
||||||
redirOut = argvWords[i + 1] ?? ''
|
redirOut = argvWords[i + 1] ?? ''
|
||||||
redirAppend = false
|
redirAppend = false
|
||||||
@@ -513,7 +576,16 @@ function parseSimpleCommand(seg) {
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
return { argv: argvWords, assign, redirIn, redirOut, redirAppend }
|
return {
|
||||||
|
argv: argvWords,
|
||||||
|
assign,
|
||||||
|
redirIn,
|
||||||
|
redirOut,
|
||||||
|
redirAppend,
|
||||||
|
redirErr,
|
||||||
|
redirErrAppend,
|
||||||
|
mergeStderrToStdout
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -615,8 +687,35 @@ async function execParsedPipeline(ctx, pipeline) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const outChunks = []
|
const outChunks = []
|
||||||
|
const errChunks = []
|
||||||
const origLog = ctx.console.log
|
const origLog = ctx.console.log
|
||||||
const origErr = ctx.console.error
|
const origErr = ctx.console.error
|
||||||
|
const capOut = !isLast || cmd.redirOut != null
|
||||||
|
const mergeErr = cmd.mergeStderrToStdout
|
||||||
|
const capErrSeparate = cmd.redirErr != null && !mergeErr
|
||||||
|
|
||||||
|
const checkCaptureSize = (chunks) => {
|
||||||
|
const joined = chunks.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})`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pushOut = (...args) => {
|
||||||
|
outChunks.push(args.map(String).join(' ') + '\n')
|
||||||
|
checkCaptureSize(outChunks)
|
||||||
|
}
|
||||||
|
const pushErr = (...args) => {
|
||||||
|
errChunks.push(args.map(String).join(' ') + '\n')
|
||||||
|
checkCaptureSize(errChunks)
|
||||||
|
}
|
||||||
|
|
||||||
for (const [k, val] of Object.entries(cmd.assign)) {
|
for (const [k, val] of Object.entries(cmd.assign)) {
|
||||||
if (
|
if (
|
||||||
@@ -637,23 +736,16 @@ async function execParsedPipeline(ctx, pipeline) {
|
|||||||
|
|
||||||
ctx.exitCode = 0
|
ctx.exitCode = 0
|
||||||
|
|
||||||
if (!isLast || cmd.redirOut) {
|
if (capOut) {
|
||||||
ctx.console.log = (...args) => {
|
ctx.console.log = pushOut
|
||||||
outChunks.push(args.map(String).join(' ') + '\n')
|
if (mergeErr) ctx.console.error = pushOut
|
||||||
const joined = outChunks.join('')
|
else if (capErrSeparate) ctx.console.error = pushErr
|
||||||
if (joined.length > lim.maxBytes) {
|
else ctx.console.error = origErr
|
||||||
throw new Error(
|
} else {
|
||||||
`shell: pipeline output exceeds BARE_OS_PIPELINE_MAX_BYTES (${lim.maxBytes})`
|
ctx.console.log = origLog
|
||||||
)
|
if (capErrSeparate) ctx.console.error = pushErr
|
||||||
}
|
else if (mergeErr) ctx.console.error = origLog
|
||||||
const lineCount = joined.split('\n').length - 1
|
else ctx.console.error = origErr
|
||||||
if (lineCount > lim.maxLines) {
|
|
||||||
throw new Error(
|
|
||||||
`shell: pipeline output exceeds BARE_OS_PIPELINE_MAX_LINES (${lim.maxLines})`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.console.error = ctx.console.log
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let code = 'ok'
|
let code = 'ok'
|
||||||
@@ -854,7 +946,7 @@ async function execParsedPipeline(ctx, pipeline) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (!isLast || cmd.redirOut) {
|
if (capOut || capErrSeparate || mergeErr) {
|
||||||
ctx.console.log = origLog
|
ctx.console.log = origLog
|
||||||
ctx.console.error = origErr
|
ctx.console.error = origErr
|
||||||
}
|
}
|
||||||
@@ -862,6 +954,22 @@ async function execParsedPipeline(ctx, pipeline) {
|
|||||||
|
|
||||||
if (code === 'exit') return 'exit'
|
if (code === 'exit') return 'exit'
|
||||||
|
|
||||||
|
if (ctx.exitCode !== 0) {
|
||||||
|
return 'ok'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd.redirErr) {
|
||||||
|
const epath = expandWord(cmd.redirErr, env)
|
||||||
|
const edata = ctx.b4a.from(errChunks.join(''))
|
||||||
|
if (cmd.redirErrAppend) {
|
||||||
|
const prev = await vfs.readFile(epath)
|
||||||
|
const merged = prev ? ctx.b4a.concat([prev, edata]) : edata
|
||||||
|
await vfs.writeFile(epath, merged)
|
||||||
|
} else {
|
||||||
|
await vfs.writeFile(epath, edata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let pipeOut = outChunks.join('')
|
let pipeOut = outChunks.join('')
|
||||||
if (cmd.redirOut) {
|
if (cmd.redirOut) {
|
||||||
const path = expandWord(cmd.redirOut, env)
|
const path = expandWord(cmd.redirOut, env)
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import {
|
|||||||
startBareService,
|
startBareService,
|
||||||
stopBareService
|
stopBareService
|
||||||
} from './bare-initd.js'
|
} from './bare-initd.js'
|
||||||
|
import {
|
||||||
|
readInitdDisabledSet,
|
||||||
|
writeInitdDisabledSet
|
||||||
|
} from './bare-initd-user.js'
|
||||||
|
|
||||||
/** @param {Uint8Array | null} buf @param {number} maxLines */
|
/** @param {Uint8Array | null} buf @param {number} maxLines */
|
||||||
function tailUtf8Lines(buf, maxLines) {
|
function tailUtf8Lines(buf, maxLines) {
|
||||||
@@ -58,9 +62,12 @@ function printHelp(ctx, prog) {
|
|||||||
${prog} status [UNIT] [--lines N]
|
${prog} status [UNIT] [--lines N]
|
||||||
${prog} logs UNIT [--lines N]
|
${prog} logs UNIT [--lines N]
|
||||||
${prog} start|stop|restart UNIT
|
${prog} start|stop|restart UNIT
|
||||||
|
${prog} enable|disable UNIT
|
||||||
|
${prog} is-enabled UNIT
|
||||||
${prog} help
|
${prog} help
|
||||||
|
|
||||||
Session-scoped bare-initd units. No enable/disable (not persistent).
|
Persistent preset: ~/.config/bare-os/initd/disabled.txt (personal drive).
|
||||||
|
Optional ~/.config/bare-os/units/<name>.unit with [Unit] After=other-unit.
|
||||||
journalctl: journalctl -u UNIT [--lines N] (alias for logs)`
|
journalctl: journalctl -u UNIT [--lines N] (alias for logs)`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -127,8 +134,16 @@ export async function runSystemctlCli(ctx, argv) {
|
|||||||
|
|
||||||
if (sub === 'list') {
|
if (sub === 'list') {
|
||||||
const defs = listBareServices()
|
const defs = listBareServices()
|
||||||
|
let disabled = new Set()
|
||||||
|
if (ctx.vfs && typeof ctx.vfs.readFile === 'function') {
|
||||||
|
try {
|
||||||
|
disabled = await readInitdDisabledSet(ctx.vfs)
|
||||||
|
} catch {
|
||||||
|
disabled = new Set()
|
||||||
|
}
|
||||||
|
}
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'UNIT LOAD ACTIVE SUB DESCRIPTION'
|
'UNIT LOAD PRESET ACTIVE SUB DESCRIPTION'
|
||||||
)
|
)
|
||||||
for (const d of defs) {
|
for (const d of defs) {
|
||||||
const rt = getBareServiceRuntime(d.name)
|
const rt = getBareServiceRuntime(d.name)
|
||||||
@@ -151,10 +166,11 @@ export async function runSystemctlCli(ctx, argv) {
|
|||||||
? 'dead'
|
? 'dead'
|
||||||
: 'n/a'
|
: 'n/a'
|
||||||
const load = 'static'
|
const load = 'static'
|
||||||
|
const preset = disabled.has(d.name) ? 'disabled' : 'enabled'
|
||||||
const namePad = (d.name + ' '.repeat(22)).slice(0, 22)
|
const namePad = (d.name + ' '.repeat(22)).slice(0, 22)
|
||||||
const desc = d.description || '—'
|
const desc = d.description || '—'
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
`${namePad} ${load.padEnd(6)} ${active.padEnd(7)} ${subState.padEnd(12)} ${desc}`
|
`${namePad} ${load.padEnd(6)} ${preset.padEnd(8)} ${active.padEnd(7)} ${subState.padEnd(12)} ${desc}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ctx.exitCode = 0
|
ctx.exitCode = 0
|
||||||
@@ -181,9 +197,19 @@ export async function runSystemctlCli(ctx, argv) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const rt = getBareServiceRuntime(unit)
|
const rt = getBareServiceRuntime(unit)
|
||||||
|
let preset = 'enabled'
|
||||||
|
if (ctx.vfs && typeof ctx.vfs.readFile === 'function') {
|
||||||
|
try {
|
||||||
|
const dis = await readInitdDisabledSet(ctx.vfs)
|
||||||
|
if (dis.has(unit)) preset = 'disabled'
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
ctx.console.log(`● ${unit}`)
|
ctx.console.log(`● ${unit}`)
|
||||||
ctx.console.log(` Description: ${def.description || '(none)'}`)
|
ctx.console.log(` Description: ${def.description || '(none)'}`)
|
||||||
ctx.console.log(` Load: static`)
|
ctx.console.log(` Load: static`)
|
||||||
|
ctx.console.log(` Preset: ${preset}`)
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
` Active: ${rt?.phase || 'unknown'}${rt?.error ? ` (${rt.error})` : ''}`
|
` Active: ${rt?.phase || 'unknown'}${rt?.error ? ` (${rt.error})` : ''}`
|
||||||
)
|
)
|
||||||
@@ -251,6 +277,76 @@ export async function runSystemctlCli(ctx, argv) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sub === 'enable' || sub === 'disable') {
|
||||||
|
const unit = rest[0]
|
||||||
|
if (!unit) {
|
||||||
|
ctx.console.error(`${prog}: ${sub} requires a UNIT`)
|
||||||
|
ctx.exitCode = 2
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const def = findBareServiceDefinition(unit)
|
||||||
|
if (!def) {
|
||||||
|
ctx.console.error(`${prog}: unknown unit: ${unit}`)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!ctx.vfs || typeof ctx.vfs.readFile !== 'function') {
|
||||||
|
ctx.console.error(`${prog}: vfs required for enable/disable`)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const dis = await readInitdDisabledSet(ctx.vfs)
|
||||||
|
if (sub === 'disable') dis.add(unit)
|
||||||
|
else dis.delete(unit)
|
||||||
|
await writeInitdDisabledSet(ctx.vfs, dis)
|
||||||
|
ctx.console.log(
|
||||||
|
sub === 'disable'
|
||||||
|
? `Disabled ${unit} for future boots (see ~/.config/bare-os/initd/disabled.txt)`
|
||||||
|
: `Enabled ${unit} for future boots`
|
||||||
|
)
|
||||||
|
ctx.exitCode = 0
|
||||||
|
} catch (e) {
|
||||||
|
ctx.console.error(`${prog}: ${e?.message || String(e)}`)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'is-enabled') {
|
||||||
|
const unit = rest[0]
|
||||||
|
if (!unit) {
|
||||||
|
ctx.console.error(`${prog}: is-enabled requires a UNIT`)
|
||||||
|
ctx.exitCode = 2
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const def = findBareServiceDefinition(unit)
|
||||||
|
if (!def) {
|
||||||
|
ctx.console.error(`${prog}: unknown unit: ${unit}`)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!ctx.vfs || typeof ctx.vfs.readFile !== 'function') {
|
||||||
|
ctx.console.error(`${prog}: vfs required for is-enabled`)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const dis = await readInitdDisabledSet(ctx.vfs)
|
||||||
|
if (dis.has(unit)) {
|
||||||
|
ctx.console.log('disabled')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
} else {
|
||||||
|
ctx.console.log('enabled')
|
||||||
|
ctx.exitCode = 0
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ctx.console.error(`${prog}: ${e?.message || String(e)}`)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx.console.error(`${prog}: unknown command: ${sub}`)
|
ctx.console.error(`${prog}: unknown command: ${sub}`)
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ 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`, `sys`, `run`, `dev` under `/`; session `tmp` maps to `/.bare-os/tmp/<seg>/` on the personal drive.
|
* Read-only pseudo `proc`, `sys`, `run`, `dev` under `/` (including stub **`/proc/cpuinfo`**, **`/proc/loadavg`**, **`/proc/self/exe`**, and **`/dev/urandom`** — urandom is bounded and **not** crypto-grade); 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
|
||||||
@@ -161,6 +161,32 @@ export function createVfs(
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pseudoCpuinfoText() {
|
||||||
|
return [
|
||||||
|
'processor\t: 0',
|
||||||
|
'vendor_id\t: BareOS',
|
||||||
|
'model name\t: Bare OS virtual CPU (stub)',
|
||||||
|
'flags\t: bare_os',
|
||||||
|
''
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function pseudoLoadavgText() {
|
||||||
|
return '0.05 0.04 0.02 1/64 1\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
function pseudoSelfExeText() {
|
||||||
|
return '/bin/sh (bare-os stub; not a real inode)\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bounded pseudo-random buffer; **not** suitable for cryptography. */
|
||||||
|
function pseudoUrandomBytes() {
|
||||||
|
const n = 4096
|
||||||
|
const u = new Uint8Array(n)
|
||||||
|
for (let i = 0; i < n; i++) u[i] = Math.floor(Math.random() * 256)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
function pseudoFileBytes(routePseudo) {
|
function pseudoFileBytes(routePseudo) {
|
||||||
const f = routePseudo.file
|
const f = routePseudo.file
|
||||||
const k = routePseudo.kind
|
const k = routePseudo.kind
|
||||||
@@ -172,6 +198,9 @@ export function createVfs(
|
|||||||
if (f === 'environ') return pseudoEnvironBytes()
|
if (f === 'environ') return pseudoEnvironBytes()
|
||||||
if (f === 'uptime') return utf8Encode(pseudoUptimeText())
|
if (f === 'uptime') return utf8Encode(pseudoUptimeText())
|
||||||
if (f === 'meminfo') return utf8Encode(pseudoMeminfoText())
|
if (f === 'meminfo') return utf8Encode(pseudoMeminfoText())
|
||||||
|
if (f === 'cpuinfo') return utf8Encode(pseudoCpuinfoText())
|
||||||
|
if (f === 'loadavg') return utf8Encode(pseudoLoadavgText())
|
||||||
|
if (f === 'exe') return utf8Encode(pseudoSelfExeText())
|
||||||
}
|
}
|
||||||
if (k === 'run' && f === 'units') {
|
if (k === 'run' && f === 'units') {
|
||||||
const t = initdRunText
|
const t = initdRunText
|
||||||
@@ -181,6 +210,7 @@ export function createVfs(
|
|||||||
}
|
}
|
||||||
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()
|
||||||
return utf8Encode('')
|
return utf8Encode('')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +243,15 @@ export function createVfs(
|
|||||||
if (sub === 'meminfo') {
|
if (sub === 'meminfo') {
|
||||||
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'meminfo' }
|
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'meminfo' }
|
||||||
}
|
}
|
||||||
|
if (sub === 'cpuinfo') {
|
||||||
|
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'cpuinfo' }
|
||||||
|
}
|
||||||
|
if (sub === 'loadavg') {
|
||||||
|
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'loadavg' }
|
||||||
|
}
|
||||||
|
if (sub === 'self/exe') {
|
||||||
|
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'exe' }
|
||||||
|
}
|
||||||
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/')) {
|
||||||
@@ -237,6 +276,9 @@ export function createVfs(
|
|||||||
if (n === '/dev/zero') {
|
if (n === '/dev/zero') {
|
||||||
return { virtualPseudo: true, kind: 'dev', node: 'file', file: 'zero' }
|
return { virtualPseudo: true, kind: 'dev', node: 'file', file: 'zero' }
|
||||||
}
|
}
|
||||||
|
if (n === '/dev/urandom') {
|
||||||
|
return { virtualPseudo: true, kind: 'dev', node: 'file', file: 'urandom' }
|
||||||
|
}
|
||||||
return { virtualPseudo: true, kind: 'dev', node: 'enoent' }
|
return { virtualPseudo: true, kind: 'dev', node: 'enoent' }
|
||||||
}
|
}
|
||||||
if (n === '/sys' || n.startsWith('/sys/')) {
|
if (n === '/sys' || n.startsWith('/sys/')) {
|
||||||
@@ -727,10 +769,18 @@ 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', 'meminfo', 'self', 'uptime', 'version']
|
return [
|
||||||
|
'bare_os_version',
|
||||||
|
'cpuinfo',
|
||||||
|
'loadavg',
|
||||||
|
'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', 'exe']
|
||||||
}
|
}
|
||||||
if (pr.kind === 'sys' && pr.node === 'root') {
|
if (pr.kind === 'sys' && pr.node === 'root') {
|
||||||
return ['fs']
|
return ['fs']
|
||||||
@@ -748,7 +798,7 @@ export function createVfs(
|
|||||||
return ['units']
|
return ['units']
|
||||||
}
|
}
|
||||||
if (pr.kind === 'dev' && pr.node === 'root') {
|
if (pr.kind === 'dev' && pr.node === 'root') {
|
||||||
return ['null', 'zero']
|
return ['null', 'urandom', 'zero']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const r = route(abs)
|
const r = route(abs)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
jobMatchesDate
|
jobMatchesDate
|
||||||
} from './lib/bare-cron.js'
|
} from './lib/bare-cron.js'
|
||||||
import {
|
import {
|
||||||
|
getBareServiceRuntime,
|
||||||
registerBareInitdDisposer,
|
registerBareInitdDisposer,
|
||||||
registerKernelShutdownHook,
|
registerKernelShutdownHook,
|
||||||
runKernelShutdownHooks,
|
runKernelShutdownHooks,
|
||||||
@@ -462,6 +463,8 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
|
|||||||
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_version',
|
'bare_os_version',
|
||||||
|
'cpuinfo',
|
||||||
|
'loadavg',
|
||||||
'meminfo',
|
'meminfo',
|
||||||
'self',
|
'self',
|
||||||
'uptime',
|
'uptime',
|
||||||
@@ -469,7 +472,8 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
|
|||||||
])
|
])
|
||||||
t.alike(await vfs.readdir('/proc/self').then((a) => [...a].sort()), [
|
t.alike(await vfs.readdir('/proc/self').then((a) => [...a].sort()), [
|
||||||
'cmdline',
|
'cmdline',
|
||||||
'environ'
|
'environ',
|
||||||
|
'exe'
|
||||||
])
|
])
|
||||||
const ver = b4a.toString(await vfs.readFile('/proc/version'))
|
const ver = b4a.toString(await vfs.readFile('/proc/version'))
|
||||||
t.ok(ver.includes('Bare OS'))
|
t.ok(ver.includes('Bare OS'))
|
||||||
@@ -484,10 +488,19 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
|
|||||||
t.ok(mem.includes('MemTotal:'))
|
t.ok(mem.includes('MemTotal:'))
|
||||||
const up = b4a.toString(await vfs.readFile('/proc/uptime'))
|
const up = b4a.toString(await vfs.readFile('/proc/uptime'))
|
||||||
t.ok(/^\d+\.\d+\s+\d+\.\d+/.test(up.trim()))
|
t.ok(/^\d+\.\d+\s+\d+\.\d+/.test(up.trim()))
|
||||||
|
const cpu = b4a.toString(await vfs.readFile('/proc/cpuinfo'))
|
||||||
|
t.ok(cpu.includes('Bare OS virtual CPU'))
|
||||||
|
const la = b4a.toString(await vfs.readFile('/proc/loadavg'))
|
||||||
|
t.ok(/^\d+\.\d+\s+\d+\.\d+\s+\d+\.\d+\s+/.test(la))
|
||||||
|
const exe = b4a.toString(await vfs.readFile('/proc/self/exe'))
|
||||||
|
t.ok(exe.includes('bare-os stub'))
|
||||||
t.alike(await vfs.readdir('/dev').then((a) => [...a].sort()), [
|
t.alike(await vfs.readdir('/dev').then((a) => [...a].sort()), [
|
||||||
'null',
|
'null',
|
||||||
|
'urandom',
|
||||||
'zero'
|
'zero'
|
||||||
])
|
])
|
||||||
|
const ur = await vfs.readFile('/dev/urandom')
|
||||||
|
t.ok(ur && ur.byteLength === 4096)
|
||||||
await vfs.writeFile('/dev/null', b4a.from('gone'))
|
await vfs.writeFile('/dev/null', b4a.from('gone'))
|
||||||
t.is((await vfs.readFile('/dev/null'))?.byteLength ?? 0, 0)
|
t.is((await vfs.readFile('/dev/null'))?.byteLength ?? 0, 0)
|
||||||
const z = await vfs.readFile('/dev/zero')
|
const z = await vfs.readFile('/dev/zero')
|
||||||
@@ -804,6 +817,55 @@ test('loadBarerc createSkeletonIfMissing writes ~/.barerc when absent', async (t
|
|||||||
rmSync(dir, { recursive: true, force: true })
|
rmSync(dir, { recursive: true, force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('tokenize leaves echo 2 > file as stdout redirect not 2>', async (t) => {
|
||||||
|
const toks = tokenize('echo 2 > /tmp/x')
|
||||||
|
const words = toks.filter((x) => x.type === 'word').map((x) => x.value)
|
||||||
|
t.ok(words.includes('2'))
|
||||||
|
t.ok(toks.some((x) => x.type === 'op' && x.value === '>'))
|
||||||
|
t.ok(!toks.some((x) => x.type === 'op' && x.value === '2>'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('execShellLine stderr 2> and 2>&1 in pipeline', async (t) => {
|
||||||
|
const dir = testCorestoreDir('sherr')
|
||||||
|
const store = new Corestore(dir)
|
||||||
|
const drive = new Hyperdrive(store)
|
||||||
|
const personal = new Hyperdrive(store.namespace('serr'))
|
||||||
|
await drive.ready()
|
||||||
|
await personal.ready()
|
||||||
|
const emit = `
|
||||||
|
async function run(ctx) {
|
||||||
|
ctx.console.log('OUT')
|
||||||
|
ctx.console.error('ERR')
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const cat = `
|
||||||
|
function bareStdin(ctx) {
|
||||||
|
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
||||||
|
}
|
||||||
|
async function run(ctx) {
|
||||||
|
ctx.console.log(bareStdin(ctx).replace(/\\n$/, ''))
|
||||||
|
}
|
||||||
|
`
|
||||||
|
await drive.put('/bin/emit', b4a.from(emit))
|
||||||
|
await drive.put('/bin/cat', b4a.from(cat))
|
||||||
|
const ctx = testCtx(drive, personal)
|
||||||
|
await execShellLine(ctx, 'emit 2>~/e.out')
|
||||||
|
const eb = await ctx.vfs.readFile('~/e.out')
|
||||||
|
t.ok(eb && ctx.b4a.toString(eb).includes('ERR'))
|
||||||
|
t.ok(!ctx.b4a.toString(eb).includes('OUT'))
|
||||||
|
await ctx.vfs.unlink('~/e.out')
|
||||||
|
const logs = []
|
||||||
|
ctx.console = {
|
||||||
|
log: (...a) => logs.push(['out', ...a]),
|
||||||
|
error: (...a) => logs.push(['err', ...a])
|
||||||
|
}
|
||||||
|
await execShellLine(ctx, 'emit 2>&1 | cat')
|
||||||
|
const piped = logs.filter((x) => x[0] === 'out').map((x) => x.slice(1).join(' '))
|
||||||
|
t.ok(piped.some((l) => l.includes('OUT') && l.includes('ERR')))
|
||||||
|
await store.close()
|
||||||
|
rmSync(dir, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
test('execShellLine pipeline stage and byte limits', async (t) => {
|
test('execShellLine pipeline stage and byte limits', async (t) => {
|
||||||
const dir = testCorestoreDir('shpipe')
|
const dir = testCorestoreDir('shpipe')
|
||||||
const store = new Corestore(dir)
|
const store = new Corestore(dir)
|
||||||
@@ -817,6 +879,9 @@ async function run(ctx, argv) {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
const cat = `
|
const cat = `
|
||||||
|
function bareStdin(ctx) {
|
||||||
|
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
||||||
|
}
|
||||||
async function run(ctx) {
|
async function run(ctx) {
|
||||||
ctx.console.log(bareStdin(ctx).replace(/\\n$/, ''))
|
ctx.console.log(bareStdin(ctx).replace(/\\n$/, ''))
|
||||||
}
|
}
|
||||||
@@ -1145,6 +1210,38 @@ test('systemctl restart bare-cron succeeds', async (t) => {
|
|||||||
rmSync(dir, { recursive: true, force: true })
|
rmSync(dir, { recursive: true, force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('initd disabled.txt skips unit on boot; systemctl enable undoes', async (t) => {
|
||||||
|
const dir = testCorestoreDir('initdis')
|
||||||
|
const store = new Corestore(dir)
|
||||||
|
const sys = new Hyperdrive(store)
|
||||||
|
const personal = new Hyperdrive(store.namespace('pdis'))
|
||||||
|
await sys.ready()
|
||||||
|
await personal.ready()
|
||||||
|
const ctx = testCtx(sys, personal)
|
||||||
|
ctx.execLine = async () => {}
|
||||||
|
await ctx.vfs.mkdir('/home/user/.config/bare-os/initd', { recursive: true })
|
||||||
|
await ctx.vfs.writeFile(
|
||||||
|
'/home/user/.config/bare-os/initd/disabled.txt',
|
||||||
|
b4a.from('bare-cron\n')
|
||||||
|
)
|
||||||
|
await startBareInitd(ctx)
|
||||||
|
t.is(getBareServiceRuntime('bare-cron'), undefined)
|
||||||
|
t.is(getBareServiceRuntime('kernel-logger')?.phase, 'active')
|
||||||
|
stopBareInitd()
|
||||||
|
ctx.exitCode = 0
|
||||||
|
ctx.console = {
|
||||||
|
log: () => {},
|
||||||
|
error: () => {}
|
||||||
|
}
|
||||||
|
await runBinCommand(ctx, ['systemctl', 'enable', 'bare-cron'])
|
||||||
|
t.is(ctx.exitCode, 0)
|
||||||
|
await startBareInitd(ctx)
|
||||||
|
t.is(getBareServiceRuntime('bare-cron')?.phase, 'active')
|
||||||
|
stopBareInitd()
|
||||||
|
await store.close()
|
||||||
|
rmSync(dir, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
test('stopBareInitd runs registered disposers', async (t) => {
|
test('stopBareInitd runs registered disposers', async (t) => {
|
||||||
let n = 0
|
let n = 0
|
||||||
registerBareInitdDisposer(() => {
|
registerBareInitdDisposer(() => {
|
||||||
@@ -1574,6 +1671,66 @@ test('curl delegated from booter with stub fetch', async (t) => {
|
|||||||
t.is(ctx.exitCode, 0)
|
t.is(ctx.exitCode, 0)
|
||||||
t.is(lines[lines.length - 1], '1')
|
t.is(lines[lines.length - 1], '1')
|
||||||
|
|
||||||
|
lines.length = 0
|
||||||
|
ctx.exitCode = 0
|
||||||
|
/** @type {RequestInit | undefined} */
|
||||||
|
let curlCookieInit
|
||||||
|
ctx.httpFetch = async (_url, init) => {
|
||||||
|
curlCookieInit = init
|
||||||
|
return new Response('c', { status: 200 })
|
||||||
|
}
|
||||||
|
await runBinCommand(ctx, [
|
||||||
|
'curl',
|
||||||
|
'-b',
|
||||||
|
'sid=abc',
|
||||||
|
'https://stub.example/cookie-inline'
|
||||||
|
])
|
||||||
|
t.is(ctx.exitCode, 0)
|
||||||
|
t.is(headerFromInit(curlCookieInit, 'Cookie'), 'sid=abc')
|
||||||
|
|
||||||
|
lines.length = 0
|
||||||
|
ctx.exitCode = 0
|
||||||
|
await ctx.vfs.writeFile(
|
||||||
|
'/home/user/cookies.json',
|
||||||
|
b4a.from(JSON.stringify({ 'stub.example': { x: 'y' } }), 'utf8')
|
||||||
|
)
|
||||||
|
ctx.httpFetch = async (_url, init) => {
|
||||||
|
curlCookieInit = init
|
||||||
|
return new Response('c', { status: 200 })
|
||||||
|
}
|
||||||
|
await runBinCommand(ctx, [
|
||||||
|
'curl',
|
||||||
|
'-b',
|
||||||
|
'/home/user/cookies.json',
|
||||||
|
'https://stub.example/cookie-file'
|
||||||
|
])
|
||||||
|
t.is(ctx.exitCode, 0)
|
||||||
|
t.ok(String(headerFromInit(curlCookieInit, 'Cookie')).includes('x=y'))
|
||||||
|
|
||||||
|
lines.length = 0
|
||||||
|
ctx.exitCode = 0
|
||||||
|
ctx.httpFetch = async () =>
|
||||||
|
new Response('z', {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Set-Cookie': 'sess=hello; Path=/' }
|
||||||
|
})
|
||||||
|
await runBinCommand(ctx, [
|
||||||
|
'curl',
|
||||||
|
'-c',
|
||||||
|
'/home/user/jar.json',
|
||||||
|
'https://stub.example/jar'
|
||||||
|
])
|
||||||
|
t.is(ctx.exitCode, 0)
|
||||||
|
const jraw = await ctx.vfs.readFile('/home/user/jar.json')
|
||||||
|
t.ok(jraw && ctx.b4a.toString(jraw).includes('sess'))
|
||||||
|
|
||||||
|
lines.length = 0
|
||||||
|
ctx.exitCode = 0
|
||||||
|
ctx.httpFetch = async () => new Response('z', { status: 200 })
|
||||||
|
await runBinCommand(ctx, ['curl', '-k', 'https://stub.example/insecure'])
|
||||||
|
t.is(ctx.exitCode, 2)
|
||||||
|
t.ok(lines.some((l) => String(l).includes('httpFetch')))
|
||||||
|
|
||||||
await store.close()
|
await store.close()
|
||||||
rmSync(dir, { recursive: true, force: true })
|
rmSync(dir, { recursive: true, force: true })
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -54,6 +54,19 @@ async function run(ctx, argv) {
|
|||||||
? dest.replace(/\/+$/, '') + '/' + base
|
? dest.replace(/\/+$/, '') + '/' + base
|
||||||
: dest
|
: dest
|
||||||
try {
|
try {
|
||||||
|
const st = await ctx.vfs.lstat(src)
|
||||||
|
const singleFileToFile =
|
||||||
|
sources.length === 1 &&
|
||||||
|
!destIsDir &&
|
||||||
|
st &&
|
||||||
|
st.type === 'file'
|
||||||
|
if (singleFileToFile) {
|
||||||
|
const buf = await ctx.vfs.readFile(src)
|
||||||
|
if (!buf) throw new Error('cannot read source')
|
||||||
|
await ctx.vfs.writeFile(target, buf)
|
||||||
|
await ctx.vfs.unlink(src)
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (!(await mvCopyPath(ctx, src, target, true))) {
|
if (!(await mvCopyPath(ctx, src, target, true))) {
|
||||||
throw new Error('cannot copy')
|
throw new Error('cannot copy')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,19 @@ async function run(ctx, argv) {
|
|||||||
? dest.replace(/\/+$/, '') + '/' + base
|
? dest.replace(/\/+$/, '') + '/' + base
|
||||||
: dest
|
: dest
|
||||||
try {
|
try {
|
||||||
|
const st = await ctx.vfs.lstat(src)
|
||||||
|
const singleFileToFile =
|
||||||
|
sources.length === 1 &&
|
||||||
|
!destIsDir &&
|
||||||
|
st &&
|
||||||
|
st.type === 'file'
|
||||||
|
if (singleFileToFile) {
|
||||||
|
const buf = await ctx.vfs.readFile(src)
|
||||||
|
if (!buf) throw new Error('cannot read source')
|
||||||
|
await ctx.vfs.writeFile(target, buf)
|
||||||
|
await ctx.vfs.unlink(src)
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (!(await mvCopyPath(ctx, src, target, true))) {
|
if (!(await mvCopyPath(ctx, src, target, true))) {
|
||||||
throw new Error('cannot copy')
|
throw new Error('cannot copy')
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user