Complete the internal “100 task” roadmap: coreutils and shell parity (xargs,
sh, diff/patch, sort, printf, find, test, getfacl/setfacl/xattr), expanded /proc and metrics (process table, syscalls, replication, net, security posture, worker budget, swarm/replication hints), initd DAG supervision metadata and richer restart journal telemetry, synthetic process groups via IPC (assignProcessGroup/signalProcessGroup) mirrored into process_table, optional kernel.ext.d incremental hot reload (BARE_OS_KERNEL_EXT_D_HOT_RELOAD) with reload audit NDJSON, features proc for hyperblobs dedup and systemd subset documentation, vault threat model doc plus posture fields for AEAD, Pear enclave pointer, account rotation continuity, and Ed25519 consistency across boot manifest / extensions / replication. Adds or extends tests and keeps kernel/ and packages/bare-os-seeder/kernel/ in parity; guest init is bundled from kernel/lib/init/init-main.js via bundle-kernel-init.
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
readdir,
|
||||
unlink
|
||||
} from 'node:fs/promises'
|
||||
import { execSync } from 'node:child_process'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import * as esbuild from 'esbuild'
|
||||
@@ -31,6 +32,18 @@ const IIFE_GLOBAL = '__bare_os_bundle_exports__'
|
||||
|
||||
const BUNDLE_CONCURRENCY = 6
|
||||
|
||||
function readGitHead(cwd) {
|
||||
try {
|
||||
return execSync('git rev-parse HEAD', {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore']
|
||||
}).trim()
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve bare-native `imports` subpath specifiers (#web-view / #window) for the host OS. */
|
||||
function bareNativeConditionalImportsPlugin() {
|
||||
const bareNativeRoot = join(repoRoot, 'node_modules/bare-native')
|
||||
@@ -183,11 +196,36 @@ export async function buildBareLibs() {
|
||||
await pruneStaleBundles(bundlesKernel)
|
||||
await pruneStaleBundles(bundlesSeeder)
|
||||
|
||||
const { sanitizeBareBundlesInDir } = await import(
|
||||
pathToFileURL(join(repoRoot, 'scripts/sanitize-bare-bundles.mjs')).href
|
||||
)
|
||||
sanitizeBareBundlesInDir(bundlesKernel)
|
||||
sanitizeBareBundlesInDir(bundlesSeeder)
|
||||
|
||||
for (const row of bundleDiagnostics) {
|
||||
const rel = join(bundlesKernel, `${row.ctxKey}.js`)
|
||||
try {
|
||||
const buf = await readFile(rel)
|
||||
row.bytes = buf.length
|
||||
} catch {
|
||||
/* keep prior */
|
||||
}
|
||||
}
|
||||
|
||||
const driveManifest = {
|
||||
version: 1,
|
||||
bundles,
|
||||
bundleStats: { ok, failed: 0, attempted: toBundle.length },
|
||||
bundleDiagnostics
|
||||
bundleDiagnostics,
|
||||
bundleProvenance: {
|
||||
schemaVersion: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
gitCommit: readGitHead(repoRoot),
|
||||
nodeVersion: process.version,
|
||||
bundleTier: tierFilter && tierFilter !== 'all' ? tierFilter : 'all',
|
||||
normativeManifest: 'packages/bare-os-booter/lib/bare-module-manifest.json',
|
||||
buildScript: 'packages/bare-os-bare-libs/build.mjs'
|
||||
}
|
||||
}
|
||||
const json = JSON.stringify(driveManifest, null, 2) + '\n'
|
||||
await writeFile(join(kernelLibBare, 'manifest.json'), json)
|
||||
|
||||
@@ -4,6 +4,8 @@ Authoritative **version alignment** with protocol and telemetry schema numbers l
|
||||
|
||||
## Maintenance
|
||||
|
||||
- **POSIX / proc**: **`/proc/bare_os/syscalls.json`** schema **3** (**`opsDetail`**, errno map via **`bare-os-posix-errno.js`**, **`posixProfile`**); **`/proc/bare_os_features`** includes **`capabilitySurface`** (feature words + POSIX profile summary); **`/proc/bare_os_swarm`** embeds **`timeSync`** sketch; **`/proc/bare_os/process_table.json`** schema **2** (session, rlimits, fd summary, job zombies). Protocol export **`BARE_OS_POSIX_PROFILE_*`**.
|
||||
- **Vendored bundles**: post-esbuild **`scripts/sanitize-bare-bundles.mjs`** (from **`bare-os-bare-libs/build.mjs`**) strips CI-forbidden markers and replaces **`node:`** requires in **`bareDev.js`** with **`bare-*`** modules.
|
||||
- **Boot hooks (naming)**: canonical **`bareOsRegisterBootStepHook`**, **`bareOsInvokeBootStepHooks`**, **`bareOsEmitBareBootStepHint`**; legacy **`*BootPhase*`** methods remain thin wrappers (**no `bareOsCtxApiVersion` bump**). See [docs/reference/naming-alias-matrix.md](../../docs/reference/naming-alias-matrix.md).
|
||||
- **Pear / Bare**: [`lib/kernel-runner.js`](lib/kernel-runner.js) must not **static**-import **`node:module`** (Bare’s resolver cannot load **`node:`** builtins from a **`pear://`** bundle). Optional **`BARE_OS_BIN_WORKER_OFFLOAD`** now obtains **`createRequire`** via **dynamic** **`import('bare-module')`** when running outside Node.
|
||||
- **`baretop`**: optional **`ctx.bareOsReadBareTopSnapshot()`** in [`index.js`](index.js) returns **`{ atMs, files }`** with the same keys as **`BARE_TOP_SNAPSHOT_PROC_ENTRIES`** in **`packages/bare-os-coreutils/lib/baretop-snapshot.js`** — keep those lists in sync when adding operator **`/proc/bare_os`** nodes.
|
||||
|
||||
@@ -38,7 +38,10 @@ import {
|
||||
BARE_OS_KERNEL_CAPABILITY_KEY_HYPERCORE_PACK_HRPC_LIFECYCLE,
|
||||
BARE_OS_KERNEL_CAPABILITY_WORD_KEYS,
|
||||
getKernelCapabilityWords,
|
||||
readKernelCapabilityWord
|
||||
readKernelCapabilityWord,
|
||||
BARE_OS_POSIX_PROFILE_VERSION,
|
||||
BARE_OS_POSIX_PROFILE_ID,
|
||||
BARE_OS_POSIX_PROFILE_REFERENCE
|
||||
} from 'bare-os-protocol'
|
||||
import { SwarmDisk } from './lib/swarm-disk.js'
|
||||
import {
|
||||
@@ -110,6 +113,7 @@ import {
|
||||
getBareServiceRuntime,
|
||||
waitForBareInitdUnits,
|
||||
getLastBareInitdDagSnapshotJson,
|
||||
bareInitdReadinessSnapshot,
|
||||
bareInitdSuspendForBareMobile,
|
||||
bareInitdResumeAfterBareMobile
|
||||
} from './lib/bare-initd.js'
|
||||
@@ -121,7 +125,12 @@ import {
|
||||
LOGGER_JSON_LOG
|
||||
} from './lib/bare-os-var-log.js'
|
||||
import { persistBareOsMountsSnapshot } from './lib/bare-os-mounts-persist.js'
|
||||
import { BARE_OS_STOCK_SYSCALL_OPS } from './lib/bare-os-syscall-ops.js'
|
||||
import { installBareOsHostSignalsBridge } from './lib/bare-os-host-signals-bridge.js'
|
||||
import {
|
||||
BARE_OS_STOCK_SYSCALL_OPS,
|
||||
BARE_OS_SYSCALL_OPS_DETAIL
|
||||
} from './lib/bare-os-syscall-ops.js'
|
||||
import { bareOsErrnoTableForProc } from './lib/bare-os-posix-errno.js'
|
||||
import { BARE_OS_CTX_API_VERSION } from './lib/bare-os-ctx-api.js'
|
||||
import { buildBareOsPearCorestoreHrpcProcJson } from './lib/bare-os-proc-pear-corestore-hrpc.js'
|
||||
import { buildBareOsRuntimeCaps } from './lib/bare-os-runtime-caps.js'
|
||||
@@ -787,6 +796,28 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
/** @type {Record<string, unknown> | null} */
|
||||
ctx: null
|
||||
}
|
||||
function bareOsLiveShellJobsAndIpcStats() {
|
||||
const live = bareOsInteractiveCtxRef.ctx
|
||||
const shellJobs =
|
||||
live &&
|
||||
live.shellBackgroundJobs &&
|
||||
typeof live.shellBackgroundJobs === 'object' &&
|
||||
Array.isArray(live.shellBackgroundJobs.list)
|
||||
? live.shellBackgroundJobs.list
|
||||
: []
|
||||
let ipcStats
|
||||
try {
|
||||
ipcStats =
|
||||
live &&
|
||||
live.bareOsIpc &&
|
||||
typeof live.bareOsIpc.stats === 'function'
|
||||
? live.bareOsIpc.stats()
|
||||
: undefined
|
||||
} catch {
|
||||
ipcStats = undefined
|
||||
}
|
||||
return { shellJobs, ipcStats }
|
||||
}
|
||||
const vfs = createVfs(disk.drive, disk.personalDrive, shellEnv, vfsMountRef, {
|
||||
procBareOsHostOsText() {
|
||||
return hostOsProcText
|
||||
@@ -798,6 +829,41 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
atMs: Date.now()
|
||||
})}\n`
|
||||
},
|
||||
procBareOsClockText() {
|
||||
const wallMs = Date.now()
|
||||
let monotonicNsFromPerf = null
|
||||
try {
|
||||
const perf = globalThis.performance
|
||||
if (perf && typeof perf.now === 'function') {
|
||||
monotonicNsFromPerf = Math.round(perf.now() * 1e6)
|
||||
}
|
||||
} catch {
|
||||
/* optional */
|
||||
}
|
||||
return `${JSON.stringify({
|
||||
schema: 1,
|
||||
CLOCK_REALTIME_MS: wallMs,
|
||||
CLOCK_BOOTTIME_RELATIVE_MS:
|
||||
bootStartedMs != null ? wallMs - bootStartedMs : null,
|
||||
CLOCK_MONOTONIC_NS_FROM_PERF: monotonicNsFromPerf,
|
||||
note: 'clock_gettime(CLOCK_REALTIME) analog; monotonic from performance.now where the host exposes it. Optional bare-hrtime can extend this surface on Bare.'
|
||||
})}\n`
|
||||
},
|
||||
getProcSelfExtraFds() {
|
||||
const c = bareOsInteractiveCtxRef.ctx
|
||||
const o = c?.bareOsLogicalFds
|
||||
if (!o || typeof o !== 'object') return []
|
||||
return Object.keys(o)
|
||||
.filter(
|
||||
(k) =>
|
||||
/^[0-9]+$/.test(k) && k !== '0' && k !== '1' && k !== '2'
|
||||
)
|
||||
.sort((a, b) => Number(a) - Number(b))
|
||||
.map((fdNum) => ({
|
||||
fdNum,
|
||||
target: String(/** @type {Record<string, string>} */ (o)[fdNum] ?? '')
|
||||
}))
|
||||
},
|
||||
procBareOsDebugText() {
|
||||
if (
|
||||
shellEnv.BARE_OS_KERNEL_DEBUG !== '1' &&
|
||||
@@ -975,7 +1041,10 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
vfsQuota: {
|
||||
mode: vfsQuotaMode,
|
||||
graceMs: Number.isFinite(graceRaw) && graceRaw > 0 ? graceRaw : null
|
||||
}
|
||||
},
|
||||
enforceQuotaOnWrites:
|
||||
shellEnv.BARE_OS_VFS_QUOTA_ENFORCE_WRITES === '1' ||
|
||||
shellEnv.BARE_OS_VFS_QUOTA_ENFORCE_WRITES === 'true'
|
||||
})}\n`
|
||||
},
|
||||
procBareOsResourcesText() {
|
||||
@@ -985,13 +1054,70 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
bareOsIpc && typeof bareOsIpc.stats === 'function'
|
||||
? bareOsIpc.stats()
|
||||
: {}
|
||||
const pwdRaw = String(shellEnv.PWD || '/home/user')
|
||||
const pwdLogical = pwdRaw.replace(/\/+$/, '') || '/'
|
||||
let volumeClass = 'system'
|
||||
if (pwdLogical.startsWith('/mnt/')) {
|
||||
volumeClass = 'mount'
|
||||
} else if (
|
||||
pwdLogical === '/tmp' ||
|
||||
pwdLogical.startsWith('/tmp/') ||
|
||||
pwdLogical.startsWith('/home/') ||
|
||||
pwdLogical === '/var/log' ||
|
||||
pwdLogical.startsWith('/var/log/')
|
||||
) {
|
||||
volumeClass = 'personal'
|
||||
} else if (pwdLogical.startsWith('/var/')) {
|
||||
volumeClass = 'system'
|
||||
}
|
||||
return `${JSON.stringify({
|
||||
ctxApiVersion: BARE_OS_CTX_API_VERSION,
|
||||
pipeline: pl,
|
||||
execMaxDepth: Number.isFinite(execD) ? execD : 64,
|
||||
ipc: ipcSt,
|
||||
session: sessionStatsRef,
|
||||
peers: disk.peers?.size ?? 0
|
||||
peers: disk.peers?.size ?? 0,
|
||||
statvfs: {
|
||||
f_bsize: 4096,
|
||||
f_frsize: 4096,
|
||||
f_blocks: null,
|
||||
f_bfree: null,
|
||||
f_bavail: null,
|
||||
f_files: null,
|
||||
f_ffree: null,
|
||||
f_namemax: 255,
|
||||
pwd_logical: pwdLogical,
|
||||
volume_class: volumeClass,
|
||||
f_basetype: 'hyperdrive',
|
||||
note: 'Symbolic statvfs sketch; block/file counts are null until drive-specific accounting exists. volume_class maps $PWD to the VFS routing bucket (not a POSIX fsid).'
|
||||
},
|
||||
mountManager: {
|
||||
schema: 1,
|
||||
snapshotPaths: [
|
||||
'/etc/bare-os/mounts.json',
|
||||
'/.bare/os/mounts_last.json'
|
||||
],
|
||||
registryAuthority: '/.bare/hdms/registry.json'
|
||||
},
|
||||
cronTimerSemantics: {
|
||||
schema: 1,
|
||||
wallCalendarCrontab: true,
|
||||
everyMsDefaultClock: 'interval',
|
||||
everyMsMonotonicEnv: 'BARE_OS_TIMER_EVERY_MS_MONOTONIC',
|
||||
note: 'Calendar crontab uses wall clock. every-ms timers use setInterval unless monotonic chaining is enabled.'
|
||||
},
|
||||
powerManagementHooks: {
|
||||
schema: 1,
|
||||
suspend: 'ctx.bareOsRegisterSuspendHook',
|
||||
resume: 'ctx.bareOsRegisterResumeHook',
|
||||
corestore: 'bareOsRegisterCorestoreSuspendResumeHooks'
|
||||
},
|
||||
vfsSparseFiles: {
|
||||
schema: 1,
|
||||
readBehavior: 'implementation-defined-holes',
|
||||
procHint: '/proc/bare_os/hyperdrive_sparse_index.json',
|
||||
note: 'Sparse regions may collapse on read; see hyperdrive_sparse_index operator JSON.'
|
||||
}
|
||||
})}\n`
|
||||
},
|
||||
procBareOsFeaturesText() {
|
||||
@@ -1039,7 +1165,58 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
urandomCrypto: urandomCryptoOn,
|
||||
seedHandshake,
|
||||
protocolPackageVersion: protocolPackageVersion || undefined,
|
||||
booterPackageVersion: _BOOTER_PKG_VERSION
|
||||
booterPackageVersion: _BOOTER_PKG_VERSION,
|
||||
guestHttpRuntime: {
|
||||
schema: 1,
|
||||
guestLibraryOnly:
|
||||
shellEnv.BARE_OS_HTTP_RUNTIME_GUEST_ONLY !== '0' &&
|
||||
shellEnv.BARE_OS_HTTP_RUNTIME_GUEST_ONLY !== 'false',
|
||||
note: 'bare-http1 / bare-fetch load from guest bare library paths unless boot policy expands the allowlist.'
|
||||
},
|
||||
posixShellExtendedProfile: {
|
||||
schema: 1,
|
||||
envGate: 'BARE_OS_SH_EXTENDED_PROFILE',
|
||||
note: 'Optional expanded POSIX sh surface (experimental; off by default).'
|
||||
},
|
||||
limitedSubshell: {
|
||||
schema: 1,
|
||||
envGate: 'BARE_OS_SUBSHELL_ISOLATED_CTX',
|
||||
note: 'When enabled, pipelines may use isolated shallow ctx where implemented.'
|
||||
},
|
||||
capabilitySurface: {
|
||||
featureBitsDoc: BARE_OS_KERNEL_FEATURE_BITS_DOC,
|
||||
[BARE_OS_KERNEL_CAPABILITY_WORDS_JSON_KEY]: localKernelCapabilityWords,
|
||||
posixProfile: {
|
||||
id: BARE_OS_POSIX_PROFILE_ID,
|
||||
version: BARE_OS_POSIX_PROFILE_VERSION,
|
||||
reference: BARE_OS_POSIX_PROFILE_REFERENCE,
|
||||
utilitiesIndexPath: '/etc/bare-os/posix_utilities.json'
|
||||
},
|
||||
stockSyscallOpCount: BARE_OS_STOCK_SYSCALL_OPS.length
|
||||
},
|
||||
vfsHyperblobsDedupSketch: {
|
||||
schema: 1,
|
||||
envGate: 'BARE_OS_VFS_HYPERBLOBS_DEDUP',
|
||||
armed:
|
||||
shellEnv.BARE_OS_VFS_HYPERBLOBS_DEDUP === '1' ||
|
||||
shellEnv.BARE_OS_VFS_HYPERBLOBS_DEDUP === 'true',
|
||||
note: 'Optional content-defined chunking for host mirror / hyperblob stores; guest VFS does not enable dedup automatically.'
|
||||
},
|
||||
initdSystemdSubset: {
|
||||
schema: 1,
|
||||
unitDropInReference: 'packages/bare-os-booter/lib/bare-initd-user.js',
|
||||
supportedKeysSample: [
|
||||
'After',
|
||||
'Requires',
|
||||
'Restart',
|
||||
'RestartSec',
|
||||
'RestartMaxAttempts',
|
||||
'ConditionPathExists',
|
||||
'OnFailure',
|
||||
'SocketActivationIpc'
|
||||
],
|
||||
note: 'systemd-inspired subset only; no cgroup or cgroup-less full unit emulation.'
|
||||
}
|
||||
})}\n`
|
||||
},
|
||||
procBareOsExtensionsText() {
|
||||
@@ -1047,6 +1224,13 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
schema: 8,
|
||||
featureBitsDoc: BARE_OS_KERNEL_FEATURE_BITS_DOC,
|
||||
entries: kernelExtensionRecords,
|
||||
ed25519PolicyBridge: {
|
||||
schema: 1,
|
||||
bootManifestSignEnv: 'BARE_OS_BOOT_MANIFEST_SIGN',
|
||||
extensionField: 'signaturePointer',
|
||||
replicationNote:
|
||||
'Replication attestation and extension manifests should honor the same operator Ed25519 / multisig policy as boot.manifest when signing is enabled.'
|
||||
},
|
||||
atMs: Date.now()
|
||||
})}\n`
|
||||
},
|
||||
@@ -1100,6 +1284,10 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
quorumOptional: true,
|
||||
note: 'Extension trust pins align with hypercore-sign style attestation hooks.'
|
||||
},
|
||||
peerAttestationRpc: {
|
||||
schema: 1,
|
||||
note: 'Replication RPC may carry attestation blobs; guests verify only when operator policy enables pinned validators.'
|
||||
},
|
||||
peerAdmission: {
|
||||
schema: 1,
|
||||
allowlistConfigured: !!(
|
||||
@@ -1117,6 +1305,76 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
]).slice(0, 8),
|
||||
note: 'Use prioritizeReplicationPaths for OTA/replication scheduling hints.'
|
||||
},
|
||||
extensionCapabilityTokens: {
|
||||
schema: 1,
|
||||
enforcedInSandbox:
|
||||
shellEnv.BARE_OS_EXTENSION_CAP_TOKENS === '1' ||
|
||||
shellEnv.BARE_OS_EXTENSION_CAP_TOKENS === 'true',
|
||||
note: 'When set, extension loaders should reject manifests without capability token rows.'
|
||||
},
|
||||
livePatchDualLoad: {
|
||||
schema: 1,
|
||||
envGate: 'BARE_OS_LIVE_PATCH_DUAL_LOAD',
|
||||
armed:
|
||||
shellEnv.BARE_OS_LIVE_PATCH_DUAL_LOAD === '1' ||
|
||||
shellEnv.BARE_OS_LIVE_PATCH_DUAL_LOAD === 'true',
|
||||
note: 'Dual-load live patch remains operator-gated; signature checks apply before activation.'
|
||||
},
|
||||
extensionHotUnload: {
|
||||
schema: 1,
|
||||
unloadLogging:
|
||||
shellEnv.BARE_OS_EXTENSION_HOT_UNLOAD === '1' ||
|
||||
shellEnv.BARE_OS_EXTENSION_HOT_UNLOAD === 'true',
|
||||
note: 'Initd disposers and kernel shutdown hooks run extension teardown in stop order.'
|
||||
},
|
||||
kernelInitHotReload: {
|
||||
schema: 1,
|
||||
envGate: 'BARE_OS_KERNEL_HOT_RELOAD',
|
||||
armed:
|
||||
shellEnv.BARE_OS_KERNEL_HOT_RELOAD === '1' ||
|
||||
shellEnv.BARE_OS_KERNEL_HOT_RELOAD === 'true',
|
||||
note: 'Booter may re-read /boot/init.js when the kernel throws BARE_OS_KERNEL_RELOAD; distinct from kernel.ext.d drop-ins.'
|
||||
},
|
||||
ed25519Policy: {
|
||||
schema: 1,
|
||||
bootManifestVerifyEnv: 'BARE_OS_BOOT_MANIFEST_SIGN',
|
||||
note: 'Boot manifest Ed25519 verify uses bare-crypto; extension and replication signing should follow the same operator policy when multisig is enabled.'
|
||||
},
|
||||
vaultAtRest: {
|
||||
schema: 1,
|
||||
aeadPreference: 'AES-256-GCM or XChaCha20-Poly1305 via bare-crypto where available',
|
||||
threatModelDoc: 'docs/reference/vault-threat-model.md',
|
||||
note: 'Guest documents AEAD expectations; keys remain in vault / handles — never in proc JSON.'
|
||||
},
|
||||
pearSecureEnclave: {
|
||||
schema: 1,
|
||||
envPointer: 'BARE_OS_PEAR_SECURE_ENCLAVE_JSON',
|
||||
pointer:
|
||||
String(shellEnv.BARE_OS_PEAR_SECURE_ENCLAVE_JSON || '').trim() ||
|
||||
null,
|
||||
note: 'Optional host JSON pointer for hardware-backed or TEE key ops; guest never receives raw attestation secrets.'
|
||||
},
|
||||
accountKeyRotationContinuity: (() => {
|
||||
const raw = String(
|
||||
shellEnv.BARE_OS_ACCOUNT_KEY_ROTATION_STATE_JSON || ''
|
||||
).trim()
|
||||
let state = null
|
||||
if (raw) {
|
||||
try {
|
||||
const o = JSON.parse(raw)
|
||||
state = o && typeof o === 'object' ? o : null
|
||||
} catch {
|
||||
state = { error: 'invalid JSON' }
|
||||
}
|
||||
}
|
||||
return {
|
||||
schema: 1,
|
||||
state,
|
||||
env: 'BARE_OS_ACCOUNT_KEY_ROTATION_STATE_JSON',
|
||||
checkpointPath: '/run/bare-os/vault-rotation-state.json',
|
||||
note: 'Operators supply non-secret rotation metadata; signing continuity uses overlapping trust windows during handoff.'
|
||||
}
|
||||
})(),
|
||||
atMs: Date.now()
|
||||
})}\n`
|
||||
},
|
||||
@@ -1145,7 +1403,10 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
if (!raw) {
|
||||
return `${JSON.stringify({
|
||||
schema: 1,
|
||||
note: 'Host may set BARE_OS_PEAR_TRUST_JSON (operator trust summary; guest does not verify multisig).',
|
||||
guestTlsTrustStorePath:
|
||||
String(shellEnv.BARE_OS_TLS_TRUST_STORE || '').trim() ||
|
||||
'/etc/ssl/certs/ca-bundle.crt',
|
||||
note: 'Host may set BARE_OS_PEAR_TRUST_JSON (operator trust summary; guest does not verify multisig). Personal-drive CA bundles may override TLS trust for curl/wget.',
|
||||
atMs: Date.now()
|
||||
})}\n`
|
||||
}
|
||||
@@ -1155,12 +1416,18 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
schema: 1,
|
||||
trust: o,
|
||||
source: 'BARE_OS_PEAR_TRUST_JSON',
|
||||
guestTlsTrustStorePath:
|
||||
String(shellEnv.BARE_OS_TLS_TRUST_STORE || '').trim() ||
|
||||
'/etc/ssl/certs/ca-bundle.crt',
|
||||
atMs: Date.now()
|
||||
})}\n`
|
||||
} catch {
|
||||
return `${JSON.stringify({
|
||||
schema: 1,
|
||||
error: 'invalid BARE_OS_PEAR_TRUST_JSON'
|
||||
error: 'invalid BARE_OS_PEAR_TRUST_JSON',
|
||||
guestTlsTrustStorePath:
|
||||
String(shellEnv.BARE_OS_TLS_TRUST_STORE || '').trim() ||
|
||||
'/etc/ssl/certs/ca-bundle.crt'
|
||||
})}\n`
|
||||
}
|
||||
},
|
||||
@@ -1230,6 +1497,18 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
active: !!(hc && hc.active),
|
||||
mountCount: mounts,
|
||||
labels,
|
||||
thresholdHints: {
|
||||
schema: 1,
|
||||
diskPressureWarnPercent: Number.parseInt(
|
||||
String(shellEnv.BARE_OS_HDMS_DISK_WARN_PERCENT || '85'),
|
||||
10
|
||||
),
|
||||
pairingBackoffMs: Number.parseInt(
|
||||
String(shellEnv.BARE_OS_HDMS_PAIRING_BACKOFF_MS || '5000'),
|
||||
10
|
||||
),
|
||||
note: 'Advisory thresholds for hdms_health consumers; not enforced in-guest.'
|
||||
},
|
||||
note: 'No keys or secrets; registry metadata only.',
|
||||
atMs: Date.now()
|
||||
})}\n`
|
||||
@@ -1237,11 +1516,31 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
procBareOsSwarmText() {
|
||||
const tk = topicKey()
|
||||
const peers = disk.peers?.size ?? 0
|
||||
const now = Date.now()
|
||||
return `${JSON.stringify({
|
||||
topicHex: b4a.toString(tk, 'hex'),
|
||||
peerCount: peers,
|
||||
protocol: 'bare-os-v1',
|
||||
lifecycle: bareOsSwarmLifecycleSnapshot(shellEnv, peers)
|
||||
lifecycle: bareOsSwarmLifecycleSnapshot(shellEnv, peers),
|
||||
natTraversal: {
|
||||
schema: 1,
|
||||
mode: String(shellEnv.BARE_OS_NAT_TRAVERSAL_MODE || 'auto').trim(),
|
||||
note: 'Host HyperDHT / UDX populate hole-punch outcomes; see net_summary.transport.'
|
||||
},
|
||||
swarmLeaveOrdering: {
|
||||
schema: 1,
|
||||
initdReverseStopFirst:
|
||||
shellEnv.BARE_OS_SWARM_LEAVE_WITH_INITD === '1' ||
|
||||
shellEnv.BARE_OS_SWARM_LEAVE_WITH_INITD === 'true',
|
||||
note: 'Mobile suspend stops initd units before swarm teardown when enabled.'
|
||||
},
|
||||
timeSync: {
|
||||
schema: 1,
|
||||
wallClockMs: now,
|
||||
skewEstimateMs: null,
|
||||
source: 'host',
|
||||
note: 'Optional NTP or peer RTT skew may populate skewEstimateMs in future releases.'
|
||||
}
|
||||
})}\n`
|
||||
},
|
||||
procBareOsReplicationText() {
|
||||
@@ -1411,6 +1710,51 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
typeof disk.seedHttpDhtProxyRoutes === 'object'
|
||||
? disk.seedHttpDhtProxyRoutes
|
||||
: undefined,
|
||||
offlineReplicationOutbox: {
|
||||
schema: 1,
|
||||
logicalDir: '/.bare/replication-outbox',
|
||||
flushEnv: 'BARE_OS_REPLICATION_OUTBOX_FLUSH',
|
||||
note: 'Operator-managed queue on the personal drive for offline replication frames.'
|
||||
},
|
||||
corestoreSnapshotHandles: (() => {
|
||||
const r = String(
|
||||
shellEnv.BARE_OS_CORESTORE_SNAPSHOT_HANDLES_JSON || ''
|
||||
).trim()
|
||||
if (!r) return null
|
||||
try {
|
||||
const o = JSON.parse(r)
|
||||
return o && typeof o === 'object' ? o : null
|
||||
} catch {
|
||||
return { error: 'invalid_BARE_OS_CORESTORE_SNAPSHOT_HANDLES_JSON' }
|
||||
}
|
||||
})(),
|
||||
peerFirewallOperatorE2e: (() => {
|
||||
const seed =
|
||||
disk.seedPeerFirewallStats &&
|
||||
typeof disk.seedPeerFirewallStats === 'object'
|
||||
? disk.seedPeerFirewallStats
|
||||
: null
|
||||
const raw = String(
|
||||
shellEnv.BARE_OS_PEER_FIREWALL_E2E_JSON || ''
|
||||
).trim()
|
||||
let injected = null
|
||||
if (raw) {
|
||||
try {
|
||||
const o = JSON.parse(raw)
|
||||
injected = o && typeof o === 'object' ? o : null
|
||||
} catch {
|
||||
injected = { error: 'invalid BARE_OS_PEER_FIREWALL_E2E_JSON' }
|
||||
}
|
||||
}
|
||||
if (!seed && !injected) return null
|
||||
return {
|
||||
schema: 1,
|
||||
seed,
|
||||
injected,
|
||||
netSummaryCrossRef: '/proc/bare_os/net_summary.json',
|
||||
atMs: Date.now()
|
||||
}
|
||||
})(),
|
||||
mbrKeysHex:
|
||||
Array.isArray(disk.mbrKeysHex) && disk.mbrKeysHex.length
|
||||
? disk.mbrKeysHex
|
||||
@@ -1512,7 +1856,17 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
},
|
||||
procBareOsSnapshotHintsText() {
|
||||
const h = disk.seedSnapshotHints
|
||||
return `${JSON.stringify(h ?? null)}\n`
|
||||
const c = bareOsInteractiveCtxRef.ctx
|
||||
const handles =
|
||||
c && Array.isArray(c.bareOsSnapshotHandles)
|
||||
? c.bareOsSnapshotHandles
|
||||
: []
|
||||
/** @type {Record<string, unknown>} */
|
||||
const o =
|
||||
h && typeof h === 'object' && !Array.isArray(h) ? { ...h } : { schema: 1 }
|
||||
o.atMs = Date.now()
|
||||
if (handles.length) o.snapshot_handles = handles
|
||||
return `${JSON.stringify(o)}\n`
|
||||
},
|
||||
procBareOsProvenanceText() {
|
||||
return `${JSON.stringify({
|
||||
@@ -1579,6 +1933,16 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
typeof disk.seedPeerFirewallStats === 'object'
|
||||
? disk.seedPeerFirewallStats
|
||||
: null,
|
||||
peerFirewallE2e: (() => {
|
||||
const raw = String(shellEnv.BARE_OS_PEER_FIREWALL_E2E_JSON || '').trim()
|
||||
if (!raw) return null
|
||||
try {
|
||||
const o = JSON.parse(raw)
|
||||
return o && typeof o === 'object' ? o : null
|
||||
} catch {
|
||||
return { error: 'invalid BARE_OS_PEER_FIREWALL_E2E_JSON' }
|
||||
}
|
||||
})(),
|
||||
transport: (() => {
|
||||
const tr = String(shellEnv.BARE_OS_NET_TRANSPORT_STATS_JSON || '').trim()
|
||||
if (!tr) return null
|
||||
@@ -1589,6 +1953,11 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
return { error: 'invalid BARE_OS_NET_TRANSPORT_STATS_JSON' }
|
||||
}
|
||||
})(),
|
||||
bsdSocketGuestBridge: {
|
||||
schema: 1,
|
||||
policyEnv: 'BARE_OS_BSD_SOCKET_POLICY',
|
||||
note: 'POSIX-like sockets for guests map to UDX / bare-tcp helpers; see handbook networking gaps.'
|
||||
},
|
||||
atMs: Date.now()
|
||||
})}\n`
|
||||
},
|
||||
@@ -1615,6 +1984,20 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
session: { ...sessionStatsRef },
|
||||
peers: disk.peers?.size ?? 0,
|
||||
pipeline: pl,
|
||||
workerBudgetHints: {
|
||||
schema: 1,
|
||||
wallMsMax:
|
||||
String(shellEnv.BARE_OS_BIN_WORKER_WALL_MS_MAX || '').trim() ||
|
||||
null,
|
||||
cpuMsMax:
|
||||
String(shellEnv.BARE_OS_BIN_WORKER_CPU_MS_MAX || '').trim() ||
|
||||
null,
|
||||
kernelRunnerClassCpuMsMaxJson:
|
||||
String(
|
||||
shellEnv.BARE_OS_KERNEL_RUNNER_CLASS_CPU_MS_MAX_JSON || ''
|
||||
).trim() || null,
|
||||
note: 'Mirrors /proc/bare_os/worker_budget.json env keys for live dashboards.'
|
||||
},
|
||||
delegateInflight: bareOsDelegateInflightSnapshot(),
|
||||
delegateRateBuckets: bareOsDelegateRateBucketsSnapshot(),
|
||||
kernelCounters: bareOsKernelMetricsSnapshot(),
|
||||
@@ -1623,51 +2006,57 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
shellEnv,
|
||||
disk.peers?.size ?? 0
|
||||
),
|
||||
processTable: bareOsProcessTableSnapshot({
|
||||
sessionId: bareOsSessionId,
|
||||
bootStartedMs,
|
||||
signalState: bareOsVirtualSignalState,
|
||||
shellJobs:
|
||||
bareOsInteractiveCtxRef.ctx &&
|
||||
bareOsInteractiveCtxRef.ctx.shellBackgroundJobs &&
|
||||
Array.isArray(bareOsInteractiveCtxRef.ctx.shellBackgroundJobs.list)
|
||||
? bareOsInteractiveCtxRef.ctx.shellBackgroundJobs.list
|
||||
: []
|
||||
})
|
||||
processTable: (() => {
|
||||
const { shellJobs, ipcStats } = bareOsLiveShellJobsAndIpcStats()
|
||||
return bareOsProcessTableSnapshot({
|
||||
sessionId: bareOsSessionId,
|
||||
bootStartedMs,
|
||||
signalState: bareOsVirtualSignalState,
|
||||
shellJobs,
|
||||
ipcStats
|
||||
})
|
||||
})(),
|
||||
initdReadiness: bareInitdReadinessSnapshot(),
|
||||
otelSpanDomains: {
|
||||
schema: 1,
|
||||
vfs: 'bare.os.vfs',
|
||||
swarm: 'bare.os.swarm',
|
||||
shell: 'bare.os.shell',
|
||||
note: 'Logical span name prefixes for OTel mirrors; see BARE_OS_TELEMETRY_OTEL_JSONL.'
|
||||
},
|
||||
structuredLogging: {
|
||||
schema: 1,
|
||||
ndjsonSchema: 'docs/schemas/telemetry-ndjson-record.schema.json',
|
||||
otelSchema: 'docs/schemas/otel-bare-os-jsonl.schema.json'
|
||||
}
|
||||
})}\n`
|
||||
}
|
||||
return metricsLiveCache.text
|
||||
},
|
||||
procBareOsProcessTableText() {
|
||||
const live = bareOsInteractiveCtxRef.ctx
|
||||
const shellJobs =
|
||||
live &&
|
||||
live.shellBackgroundJobs &&
|
||||
typeof live.shellBackgroundJobs === 'object' &&
|
||||
Array.isArray(live.shellBackgroundJobs.list)
|
||||
? live.shellBackgroundJobs.list
|
||||
: []
|
||||
const { shellJobs, ipcStats } = bareOsLiveShellJobsAndIpcStats()
|
||||
return `${JSON.stringify(
|
||||
bareOsProcessTableSnapshot({
|
||||
sessionId: bareOsSessionId,
|
||||
bootStartedMs,
|
||||
signalState: bareOsVirtualSignalState,
|
||||
shellJobs
|
||||
shellJobs,
|
||||
ipcStats
|
||||
})
|
||||
)}\n`
|
||||
},
|
||||
procBareOsSyscallsText() {
|
||||
return `${JSON.stringify({
|
||||
schemaVersion: 2,
|
||||
schemaVersion: 3,
|
||||
ctxApiVersion: BARE_OS_CTX_API_VERSION,
|
||||
ops: [...BARE_OS_STOCK_SYSCALL_OPS],
|
||||
errnoHints: {
|
||||
ENOENT: 2,
|
||||
EACCES: 13,
|
||||
EINVAL: 22,
|
||||
ELOOP: 40,
|
||||
note: 'Errors thrown from bareOsSyscall / VFS use POSIX-style names in messages where applicable.'
|
||||
posixProfile: {
|
||||
id: BARE_OS_POSIX_PROFILE_ID,
|
||||
version: BARE_OS_POSIX_PROFILE_VERSION,
|
||||
utilitiesIndexPath: '/etc/bare-os/posix_utilities.json'
|
||||
},
|
||||
ops: [...BARE_OS_STOCK_SYSCALL_OPS],
|
||||
opsDetail: [...BARE_OS_SYSCALL_OPS_DETAIL],
|
||||
errnoHints: bareOsErrnoTableForProc(),
|
||||
caps: {
|
||||
note: 'VFS path classes and boot policy may deny individual ops at runtime.'
|
||||
},
|
||||
@@ -1696,6 +2085,12 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
changeLogTail: rt.changeLogTail,
|
||||
reverseIndex: rt.reverseIndex,
|
||||
registrySchema: rt.schema,
|
||||
tracePropagation: {
|
||||
schema: 1,
|
||||
header: 'bare-trace-id',
|
||||
env: 'BARE_OS_TRACE_ID',
|
||||
note: 'Optional correlation id mirrored into telemetry NDJSON when BARE_OS_TRACE_ID is set.'
|
||||
},
|
||||
atMs: Date.now()
|
||||
})}\n`
|
||||
},
|
||||
@@ -1924,6 +2319,8 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
}),
|
||||
/** Milliseconds since Unix epoch when this session started VFS construction (for `/proc/uptime`). */
|
||||
bareOsBootStartedMs: bootStartedMs,
|
||||
/** Logical open-file targets for `/proc/self/fd/*` beyond stdio (see `bareOsRegisterLogicalFd`). */
|
||||
bareOsLogicalFds: /** @type {Record<string, string>} */ ({}),
|
||||
/** True when `BARE_OS_SKIP_REPL=1` — stdin is non-interactive; `readLine` yields EOF immediately after boot. */
|
||||
bareOsSkipRepl: skipInteractive,
|
||||
disk,
|
||||
@@ -2347,18 +2744,15 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
* @returns {ReturnType<typeof bareOsProcessTableSnapshot>}
|
||||
*/
|
||||
bareOsReadProcessTable() {
|
||||
const { shellJobs, ipcStats } = bareOsLiveShellJobsAndIpcStats()
|
||||
return bareOsProcessTableSnapshot({
|
||||
sessionId: bareOsSessionId,
|
||||
bootStartedMs,
|
||||
signalState: bareOsVirtualSignalState
|
||||
signalState: bareOsVirtualSignalState,
|
||||
shellJobs,
|
||||
ipcStats
|
||||
})
|
||||
},
|
||||
/**
|
||||
* POSIX-like signal dispatch over synthetic process IDs.
|
||||
* Supported targets: numeric pid (1..3) or names (`kernel`, `booter`, `shell`).
|
||||
* @param {number | string} target
|
||||
* @param {string} [signal='TERM']
|
||||
*/
|
||||
/**
|
||||
* Append one structured logger record to **`/var/log/bare-os/logger.jsonl`** (and mirrors).
|
||||
* @param {Record<string, unknown>} rec
|
||||
@@ -2372,11 +2766,16 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
line.slice(0, 4000)
|
||||
)
|
||||
},
|
||||
/**
|
||||
* POSIX-like signal dispatch over synthetic process IDs.
|
||||
* Targets: numeric pid (1..3) or `kernel` / `booter` / `shell`.
|
||||
* Signals: HUP, INT, KILL, TERM, PIPE, CHLD, USR1, USR2; `PIPE`/`CHLD`/`USR*` update state only (no session exit).
|
||||
*/
|
||||
bareOsSendSignal(target, signal = 'TERM') {
|
||||
const sig = String(signal || 'TERM')
|
||||
.replace(/^SIG/i, '')
|
||||
.toUpperCase()
|
||||
if (!/^(HUP|INT|KILL|TERM|0)$/.test(sig)) {
|
||||
if (!/^(HUP|INT|KILL|TERM|PIPE|CHLD|USR1|USR2|0)$/.test(sig)) {
|
||||
throw new Error('bareOsSendSignal: unsupported signal')
|
||||
}
|
||||
const raw = String(target == null ? '' : target).trim()
|
||||
@@ -2401,10 +2800,46 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
const signum = BARE_OS_SIGNAL_EXIT[sig] || 15
|
||||
this.requestBooterExit(128 + signum)
|
||||
const noSessionExit =
|
||||
sig === 'PIPE' || sig === 'CHLD' || sig === 'USR1' || sig === 'USR2'
|
||||
if (!noSessionExit) {
|
||||
const signum = BARE_OS_SIGNAL_EXIT[sig] || 15
|
||||
this.requestBooterExit(128 + signum)
|
||||
}
|
||||
return { ok: true, pid, signal: sig, delivered: true, atMs }
|
||||
},
|
||||
/**
|
||||
* @param {number} fd
|
||||
* @param {string} target
|
||||
*/
|
||||
bareOsRegisterLogicalFd(fd, target) {
|
||||
const n = Number(fd)
|
||||
if (!Number.isFinite(n) || n < 0 || n > 65535) return
|
||||
const k = String(n >>> 0)
|
||||
if (k === '0' || k === '1' || k === '2') return
|
||||
this.bareOsLogicalFds[k] = String(target || '')
|
||||
},
|
||||
/** @param {number} fd */
|
||||
bareOsUnregisterLogicalFd(fd) {
|
||||
const k = String(Number(fd) >>> 0)
|
||||
if (this.bareOsLogicalFds && typeof this.bareOsLogicalFds === 'object') {
|
||||
delete this.bareOsLogicalFds[k]
|
||||
}
|
||||
},
|
||||
/** @type {Record<string, unknown>[]} */
|
||||
bareOsSnapshotHandles: [],
|
||||
/** @param {Record<string, unknown>} desc */
|
||||
bareOsRegisterSnapshotHandle(desc) {
|
||||
if (!desc || typeof desc !== 'object') return
|
||||
if (!Array.isArray(this.bareOsSnapshotHandles)) {
|
||||
this.bareOsSnapshotHandles = []
|
||||
}
|
||||
const id =
|
||||
typeof desc.id === 'string' && desc.id.trim()
|
||||
? desc.id.trim().slice(0, 128)
|
||||
: `snap-${this.bareOsSnapshotHandles.length}`
|
||||
this.bareOsSnapshotHandles.push({ ...desc, id, atMs: Date.now() })
|
||||
},
|
||||
/**
|
||||
* Fixed pathconf/nameconf-style limits (no live host query).
|
||||
* @param {string} [_path]
|
||||
@@ -2556,6 +2991,18 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
return { ok: true, from, to }
|
||||
}
|
||||
if (name === 'link') {
|
||||
const strict =
|
||||
shellEnv.BARE_OS_VFS_STRICT_HARDLINK === '1' ||
|
||||
shellEnv.BARE_OS_VFS_STRICT_HARDLINK === 'true'
|
||||
if (strict) {
|
||||
const err = /** @type {Error & { code?: string }} */ (
|
||||
new Error(
|
||||
'EOPNOTSUPP: hard links are not supported on Hyperdrive (unset BARE_OS_VFS_STRICT_HARDLINK for legacy copy semantics)'
|
||||
)
|
||||
)
|
||||
err.code = 'EOPNOTSUPP'
|
||||
throw err
|
||||
}
|
||||
const existing = String(args.existing || args.path || '').trim()
|
||||
const newPath = String(args.newPath || args.to || '').trim()
|
||||
if (!existing.startsWith('/') || !newPath.startsWith('/')) {
|
||||
@@ -2665,6 +3112,19 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
await persistBareOsMountsSnapshot(vfs, hdmsController)
|
||||
return { ok: true, label }
|
||||
}
|
||||
if (name === 'fsync' || name === 'fdatasync') {
|
||||
const p = String(args.path || '').trim()
|
||||
if (!p.startsWith('/')) {
|
||||
throw new Error('bareOsSyscall: absolute path required')
|
||||
}
|
||||
if (typeof vfs.fsync === 'function') await vfs.fsync(p)
|
||||
return {
|
||||
ok: true,
|
||||
path: p,
|
||||
op: name,
|
||||
note: 'Best-effort no-op unless the VFS exposes a flush primitive.'
|
||||
}
|
||||
}
|
||||
if (name === 'kill') {
|
||||
return this.bareOsSendSignal(args.target, String(args.signal || 'TERM'))
|
||||
}
|
||||
@@ -2798,6 +3258,7 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
['replicationBackpressure', '/proc/bare_os/replication_backpressure.json'],
|
||||
['swarm', '/proc/bare_os/swarm'],
|
||||
['syncWindow', '/proc/bare_os/sync_window.json'],
|
||||
['clock', '/proc/bare_os/clock.json'],
|
||||
['hdmsHealth', '/proc/bare_os/hdms_health.json'],
|
||||
['hdmsHints', '/proc/bare_os/hdms_hints.json'],
|
||||
['dhtStatus', '/proc/bare_os/dht_status.json'],
|
||||
@@ -4064,6 +4525,14 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
|
||||
bareOsInteractiveCtxRef.ctx = ctx
|
||||
|
||||
void installBareOsHostSignalsBridge({
|
||||
onSigint() {
|
||||
if (typeof globalThis.process?.emit === 'function') {
|
||||
globalThis.process.emit('bare-os:host-sigint', { atMs: Date.now() })
|
||||
}
|
||||
}
|
||||
}).catch(() => {})
|
||||
|
||||
bareOsInstallCorestoreSuspendResumeHooks(ctx)
|
||||
|
||||
const bareFetchResolved = coerceBareFetchExport(bareLibrary.fetch)
|
||||
|
||||
@@ -404,6 +404,14 @@ function startBareCron(ctx) {
|
||||
} catch {
|
||||
msJobs = []
|
||||
}
|
||||
const envMono =
|
||||
c.vfs?.env && typeof c.vfs.env === 'object'
|
||||
? /** @type {Record<string, string | undefined>} */ (c.vfs.env)
|
||||
: {}
|
||||
const everyMsMonotonic =
|
||||
envMono.BARE_OS_TIMER_EVERY_MS_MONOTONIC === '1' ||
|
||||
envMono.BARE_OS_TIMER_EVERY_MS_MONOTONIC === 'true'
|
||||
|
||||
for (const j of msJobs.slice(0, 8)) {
|
||||
const jitterMs =
|
||||
j.jitterSec > 0 ? Math.floor(Math.random() * j.jitterSec * 1000) : 0
|
||||
@@ -429,12 +437,31 @@ function startBareCron(ctx) {
|
||||
}
|
||||
})()
|
||||
}
|
||||
const bootMs = () => {
|
||||
runMs()
|
||||
everyMsIntervalIds.push(setInterval(runMs, j.everyMs))
|
||||
if (everyMsMonotonic) {
|
||||
const chain = () => {
|
||||
const tid = setTimeout(() => {
|
||||
runMs()
|
||||
chain()
|
||||
}, j.everyMs)
|
||||
everyMsIntervalIds.push(tid)
|
||||
}
|
||||
if (jitterMs > 0) {
|
||||
setTimeout(() => {
|
||||
runMs()
|
||||
chain()
|
||||
}, jitterMs)
|
||||
} else {
|
||||
runMs()
|
||||
chain()
|
||||
}
|
||||
} else {
|
||||
const bootMs = () => {
|
||||
runMs()
|
||||
everyMsIntervalIds.push(setInterval(runMs, j.everyMs))
|
||||
}
|
||||
if (jitterMs > 0) setTimeout(bootMs, jitterMs)
|
||||
else bootMs()
|
||||
}
|
||||
if (jitterMs > 0) setTimeout(bootMs, jitterMs)
|
||||
else bootMs()
|
||||
}
|
||||
|
||||
let inactJobs = []
|
||||
|
||||
@@ -191,6 +191,32 @@ export async function runKernelShutdownHooks() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop every active unit that defines `stop`, in reverse boot-DAG order (like mobile suspend).
|
||||
* Swallows per-unit errors; clears health timers via {@link stopBareService}.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
export async function bareInitdShutdownActiveUnitsReverse(ctx) {
|
||||
const order =
|
||||
lastBareInitdBootOrder.length > 0
|
||||
? [...lastBareInitdBootOrder]
|
||||
: [...runtime.entries()]
|
||||
.filter(([, rt]) => rt.phase === 'active')
|
||||
.map(([n]) => n)
|
||||
for (const name of [...order].reverse()) {
|
||||
const rt = runtime.get(name)
|
||||
if (!rt || rt.phase !== 'active') continue
|
||||
const s = findBareServiceDefinition(name)
|
||||
if (s && typeof s.stop === 'function') {
|
||||
try {
|
||||
await stopBareService(ctx, name)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function stopBareInitd() {
|
||||
for (const t of unitHealthTimers.values()) clearInterval(t)
|
||||
unitHealthTimers.clear()
|
||||
@@ -239,6 +265,45 @@ export function getBareServiceRuntime(name) {
|
||||
return runtime.get(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Live initd unit phases for readiness / observability (mirrors supervisor runtime map).
|
||||
* @returns {{ schema: 1, units: { name: string, phase: string, startedAtMs: number, error?: string }[], note: string, atMs: number }}
|
||||
*/
|
||||
export function bareInitdReadinessSnapshot() {
|
||||
const now = Date.now()
|
||||
/** @type {{ name: string, phase: string, startedAtMs: number, error?: string }[]} */
|
||||
const units = []
|
||||
for (const [name, rt] of runtime) {
|
||||
const row = {
|
||||
name,
|
||||
phase: rt.phase,
|
||||
startedAtMs: rt.startedAtMs
|
||||
}
|
||||
if (rt.error) row.error = rt.error
|
||||
units.push(row)
|
||||
}
|
||||
units.sort((a, b) => a.name.localeCompare(b.name))
|
||||
return {
|
||||
schema: 2,
|
||||
units,
|
||||
supervisionTelemetry: {
|
||||
schema: 1,
|
||||
restartJournalEvents: [
|
||||
'start_scheduled',
|
||||
'restart_attempt',
|
||||
'start_error',
|
||||
'failed_final',
|
||||
'on_failure_ran',
|
||||
'active'
|
||||
],
|
||||
journalPathPattern: '/run/bare-os/unit-journal/*.ndjson',
|
||||
note: 'restart_attempt rows include backoffMsScheduled and restartPolicy when Restart= is set.'
|
||||
},
|
||||
note: 'Supervisor phases; per-unit file readiness uses ReadinessPath= in unit drop-ins when set.',
|
||||
atMs: now
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} JSON string for `/proc/bare_os/initd_dag.json` (or `null` line if unavailable).
|
||||
*/
|
||||
@@ -629,11 +694,18 @@ async function startNormalBareInitdUnit(ctx, s, dropIn) {
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
try {
|
||||
if (attempt > 0) {
|
||||
appendBareInitdJournal(s.name, { event: 'restart_attempt', attempt })
|
||||
const backoffMs = Math.min(
|
||||
120000,
|
||||
Math.round(restartDelayMs * Math.pow(2, attempt - 1))
|
||||
)
|
||||
appendBareInitdJournal(s.name, {
|
||||
event: 'restart_attempt',
|
||||
attempt,
|
||||
backoffMsScheduled: backoffMs,
|
||||
restartPolicy: dropIn.restart,
|
||||
restartMaxAttempts: maxAttempts,
|
||||
telemetrySchema: 1
|
||||
})
|
||||
await new Promise((r) => setTimeout(r, backoffMs))
|
||||
}
|
||||
const pathEv = await evaluateUnitPathConditions(ctx, dropIn)
|
||||
@@ -815,6 +887,17 @@ export async function startBareInitd(ctx) {
|
||||
levels,
|
||||
edges,
|
||||
dot,
|
||||
supervision: {
|
||||
schema: 1,
|
||||
restartKeys: [
|
||||
'Restart',
|
||||
'RestartSec',
|
||||
'RestartMaxAttempts',
|
||||
'OnFailure'
|
||||
],
|
||||
reference: 'packages/bare-os-booter/lib/bare-initd-user.js',
|
||||
note: 'systemd-inspired subset parsed from unit drop-ins; backoff is host/runtime specific.'
|
||||
},
|
||||
atMs: Date.now()
|
||||
})}\n`
|
||||
} catch {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Maps host SIGINT into a process event for guest cancellation (optional `bare-signals` on Bare).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {{ onSigint?: () => void }} [opts]
|
||||
* @returns {Promise<() => void>} disposer
|
||||
*/
|
||||
export async function installBareOsHostSignalsBridge(opts = {}) {
|
||||
const onSigint = typeof opts.onSigint === 'function' ? opts.onSigint : () => {}
|
||||
/** @type {(() => void)[]} */
|
||||
const disposers = []
|
||||
try {
|
||||
const mod = await import('bare-signals')
|
||||
const Signal = mod.default
|
||||
if (typeof Signal === 'function') {
|
||||
const h = new Signal('SIGINT')
|
||||
const fn = () => {
|
||||
onSigint()
|
||||
}
|
||||
h.on('signal', fn)
|
||||
h.start()
|
||||
disposers.push(() => {
|
||||
try {
|
||||
void h.close()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
/* bare-signals optional (native binding) */
|
||||
}
|
||||
if (
|
||||
disposers.length === 0 &&
|
||||
globalThis.process &&
|
||||
typeof globalThis.process.on === 'function'
|
||||
) {
|
||||
const fn = () => {
|
||||
onSigint()
|
||||
}
|
||||
globalThis.process.on('SIGINT', fn)
|
||||
disposers.push(() => {
|
||||
try {
|
||||
globalThis.process.off('SIGINT', fn)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
})
|
||||
}
|
||||
return () => {
|
||||
for (const d of disposers) {
|
||||
try {
|
||||
d()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,6 +207,49 @@ export function createBareOsIpc(opts = {}) {
|
||||
channelScopes.delete(name)
|
||||
},
|
||||
|
||||
/**
|
||||
* Assign a synthetic process-group id to a FIFO (POSIX setpgid analog for IPC routing).
|
||||
* @param {string} channelName
|
||||
* @param {number} pgid
|
||||
*/
|
||||
assignProcessGroup(channelName, pgid) {
|
||||
assertSafeIpcName(channelName)
|
||||
const n = Number(pgid)
|
||||
if (!Number.isFinite(n) || n < 0 || n > 0xffffffff) {
|
||||
throw new Error('invalid pgid')
|
||||
}
|
||||
if (!channels.has(channelName)) channels.set(channelName, new FifoChannel())
|
||||
channelScopes.set(channelName, 'pgid:' + String(Math.floor(n)))
|
||||
},
|
||||
|
||||
/**
|
||||
* Push a virtual signal JSON line to every channel in the synthetic group (killpg analog).
|
||||
* @param {number} pgid
|
||||
* @param {string} [signal]
|
||||
* @returns {{ delivered: number, channels: string[] }}
|
||||
*/
|
||||
signalProcessGroup(pgid, signal) {
|
||||
const key = 'pgid:' + String(Math.floor(Number(pgid)))
|
||||
/** @type {string[]} */
|
||||
const names = []
|
||||
for (const [ch, sc] of channelScopes.entries()) {
|
||||
if (sc === key) names.push(ch)
|
||||
}
|
||||
const sig = String(signal || 'SIGTERM').trim().slice(0, 32) || 'SIGTERM'
|
||||
for (const ch of names) {
|
||||
try {
|
||||
this.pushJson(ch, {
|
||||
method: 'bareOsProcessGroupSignal',
|
||||
signal: sig,
|
||||
pgid: Math.floor(Number(pgid))
|
||||
})
|
||||
} catch {
|
||||
/* channel removed mid-iteration */
|
||||
}
|
||||
}
|
||||
return { delivered: names.length, channels: names }
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Uint8Array | ArrayBuffer} buf
|
||||
@@ -325,13 +368,28 @@ export function createBareOsIpc(opts = {}) {
|
||||
for (const sc of channelScopes.values()) {
|
||||
scopeHistogram[sc] = (scopeHistogram[sc] || 0) + 1
|
||||
}
|
||||
/** @type {Record<string, string[]>} */
|
||||
const byPgid = {}
|
||||
for (const [ch, sc] of channelScopes.entries()) {
|
||||
if (!sc.startsWith('pgid:')) continue
|
||||
if (!byPgid[sc]) byPgid[sc] = []
|
||||
byPgid[sc].push(ch)
|
||||
}
|
||||
for (const k of Object.keys(byPgid)) {
|
||||
byPgid[k].sort()
|
||||
}
|
||||
return {
|
||||
channelCount: channels.size,
|
||||
queuedBytesTotal,
|
||||
fanoutTopicCount: fanouts.size,
|
||||
fanoutSubscribersTotal,
|
||||
channelScopes: Object.fromEntries([...channelScopes.entries()].sort()),
|
||||
scopeHistogram
|
||||
scopeHistogram,
|
||||
processGroups: {
|
||||
schema: 1,
|
||||
byPgid,
|
||||
note: 'Channels tagged via assignProcessGroup; consumers handle bareOsProcessGroupSignal RPC lines.'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -29,18 +29,25 @@ export async function persistBareOsMountsSnapshot(vfs, hdms) {
|
||||
const doc = {
|
||||
schemaVersion: 1,
|
||||
updatedAt: new Date().toISOString(),
|
||||
note: 'Snapshot for operators; authoritative registry is /.bare/hdms/registry.json on the personal drive.',
|
||||
note: 'Snapshot for operators; authoritative registry is /.bare/hdms/registry.json on the personal drive. When writable, the same JSON is mirrored at /.bare/os/mounts_last.json for multi-drive tooling.',
|
||||
mounts
|
||||
}
|
||||
const body = JSON.stringify(doc, null, 2) + '\n'
|
||||
const buf = b4a.from(body, 'utf8')
|
||||
try {
|
||||
await vfs.mkdir('/etc/bare-os', { recursive: true })
|
||||
} catch {
|
||||
/* exists or pseudo */
|
||||
}
|
||||
try {
|
||||
await vfs.writeFile('/etc/bare-os/mounts.json', b4a.from(body, 'utf8'))
|
||||
await vfs.writeFile('/etc/bare-os/mounts.json', buf)
|
||||
} catch (e) {
|
||||
console.warn('[bare-os] mounts.json persist failed:', e?.message || e)
|
||||
}
|
||||
try {
|
||||
await vfs.mkdir('/.bare/os', { recursive: true })
|
||||
await vfs.writeFile('/.bare/os/mounts_last.json', buf)
|
||||
} catch {
|
||||
/* personal tree may be unavailable in minimal tests */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* POSIX errno names ↔ numeric hints for guest errors and `/proc` introspection.
|
||||
* Values follow Linux/x86-64 style where Bare OS surfaces them in messages.
|
||||
*/
|
||||
|
||||
export const BARE_OS_POSIX_ERRNO_MAP = Object.freeze({
|
||||
EPERM: 1,
|
||||
ENOENT: 2,
|
||||
ESRCH: 3,
|
||||
EINTR: 4,
|
||||
EIO: 5,
|
||||
ENXIO: 6,
|
||||
E2BIG: 7,
|
||||
ENOEXEC: 8,
|
||||
EBADF: 9,
|
||||
ECHILD: 10,
|
||||
EAGAIN: 11,
|
||||
ENOMEM: 12,
|
||||
EACCES: 13,
|
||||
EFAULT: 14,
|
||||
ENOTBLK: 15,
|
||||
EBUSY: 16,
|
||||
EEXIST: 17,
|
||||
EXDEV: 18,
|
||||
ENODEV: 19,
|
||||
ENOTDIR: 20,
|
||||
EISDIR: 21,
|
||||
EINVAL: 22,
|
||||
ENFILE: 23,
|
||||
EMFILE: 24,
|
||||
ENOTTY: 25,
|
||||
ETXTBSY: 26,
|
||||
EFBIG: 27,
|
||||
ENOSPC: 28,
|
||||
ESPIPE: 29,
|
||||
EROFS: 30,
|
||||
EMLINK: 31,
|
||||
EPIPE: 32,
|
||||
EDOM: 33,
|
||||
ERANGE: 34,
|
||||
ELOOP: 40,
|
||||
ENAMETOOLONG: 36,
|
||||
ENOSYS: 38,
|
||||
ENOTEMPTY: 39,
|
||||
EOPNOTSUPP: 95
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {number | undefined}
|
||||
*/
|
||||
export function bareOsErrnoNumber(name) {
|
||||
const k = String(name || '').toUpperCase()
|
||||
return /** @type {Record<string, number>} */ (BARE_OS_POSIX_ERRNO_MAP)[k]
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Record<string, number>}
|
||||
*/
|
||||
export function bareOsErrnoTableForProc() {
|
||||
return { ...BARE_OS_POSIX_ERRNO_MAP }
|
||||
}
|
||||
@@ -52,6 +52,9 @@ export function buildBareOsBareModuleCryptoStagingProcJson(id, env) {
|
||||
return {
|
||||
schema: 1,
|
||||
resolution: parseJsonEnv('BARE_OS_BARE_MODULE_RESOLUTION_JSON'),
|
||||
traversePreflight:
|
||||
esc('BARE_OS_BARE_MODULE_TRAVERSE_PREFLIGHT') === '1' ||
|
||||
esc('BARE_OS_BARE_MODULE_TRAVERSE_PREFLIGHT') === 'true',
|
||||
note: 'bare-module / bare-module-resolve probe summary; no node: specifiers.',
|
||||
atMs: now
|
||||
}
|
||||
@@ -59,6 +62,11 @@ export function buildBareOsBareModuleCryptoStagingProcJson(id, env) {
|
||||
return {
|
||||
schema: 1,
|
||||
policy: parseJsonEnv('BARE_OS_BARE_CRYPTO_POLICY_JSON'),
|
||||
guestCryptoProcSurface: {
|
||||
schema: 1,
|
||||
urandomClass: 'hyperdrive|host',
|
||||
note: 'Augmented summary; operator JSON remains authoritative.'
|
||||
},
|
||||
note: 'bare-crypto allowed primitives / backends.',
|
||||
atMs: now
|
||||
}
|
||||
@@ -73,7 +81,8 @@ export function buildBareOsBareModuleCryptoStagingProcJson(id, env) {
|
||||
return {
|
||||
schema: 1,
|
||||
sandbox: parseJsonEnv('BARE_OS_BARE_VM_SANDBOX_SKETCH_JSON'),
|
||||
note: 'bare-vm policy hints.',
|
||||
wasmGuestCapsEnv: 'BARE_OS_WASM_GUEST_CAPS_JSON',
|
||||
note: 'bare-vm / WASM guest module policy hints under VFS caps.',
|
||||
atMs: now
|
||||
}
|
||||
case 'bare_daemon_hooks':
|
||||
@@ -129,7 +138,8 @@ export function buildBareOsBareModuleCryptoStagingProcJson(id, env) {
|
||||
return {
|
||||
schema: 1,
|
||||
sparse: parseJsonEnv('BARE_OS_HYPERDRIVE_SPARSE_INDEX_JSON'),
|
||||
note: 'hyperdrive sparse index operator hints.',
|
||||
vfsReadWriteNote:
|
||||
'Sparse holes may read as EOF or zero-fill depending on drive; align tar/VFS callers with this sketch.',
|
||||
atMs: now
|
||||
}
|
||||
case 'protomux_channel_alias_v2':
|
||||
|
||||
@@ -87,7 +87,10 @@ export function buildBareOsPearInspectLoggerTlsProcJson(id, env) {
|
||||
return {
|
||||
schema: 1,
|
||||
policy: parseJsonEnv('BARE_OS_BARE_INSPECT_POLICY_JSON'),
|
||||
note: 'bare-inspect policy.',
|
||||
guestMetricsPath:
|
||||
esc('BARE_OS_INSPECT_GUEST_METRICS_PATH') ||
|
||||
'/run/bare-os/profiles/',
|
||||
note: 'bare-inspect policy; CPU profiles may land under guestMetricsPath when enabled on host.',
|
||||
atMs: now
|
||||
}
|
||||
case 'bare_signals_mask':
|
||||
|
||||
@@ -97,9 +97,22 @@ export function buildBareOsReplicationOperatorSurfaceProcJson(id, env, extras =
|
||||
}
|
||||
case 'worker_budget':
|
||||
return {
|
||||
schema: 1,
|
||||
schema: 2,
|
||||
wallMsMax: esc('BARE_OS_BIN_WORKER_WALL_MS_MAX') || null,
|
||||
note: 'Optional per-invocation wall clock cap for bin-worker offload.',
|
||||
cpuMsMax: esc('BARE_OS_BIN_WORKER_CPU_MS_MAX') || null,
|
||||
kernelRunnerClassCpuMsMax: (() => {
|
||||
const r = esc('BARE_OS_KERNEL_RUNNER_CLASS_CPU_MS_MAX_JSON')
|
||||
if (!r) return null
|
||||
try {
|
||||
const o = JSON.parse(r)
|
||||
return o && typeof o === 'object' ? o : null
|
||||
} catch {
|
||||
return { error: 'invalid_json' }
|
||||
}
|
||||
})(),
|
||||
offloadAllowHint:
|
||||
'BARE_OS_BIN_WORKER_ALLOW may include metaproc:* for getfacl,setfacl,xattr.',
|
||||
note: 'Optional wall/CPU caps for bin-worker offload; class map is host-supplied JSON.',
|
||||
atMs: now
|
||||
}
|
||||
case 'sandbox_profile':
|
||||
@@ -107,7 +120,10 @@ export function buildBareOsReplicationOperatorSurfaceProcJson(id, env, extras =
|
||||
schema: 1,
|
||||
profile: esc('BARE_OS_SANDBOX_PROFILE_NAME') || null,
|
||||
capabilities: parseJsonEnv('BARE_OS_SANDBOX_CAPABILITIES_JSON'),
|
||||
note: 'Host-interpreted capability JSON; guest makes no seccomp claims.',
|
||||
extensionUntrustedPathPrefix:
|
||||
esc('BARE_OS_EXTENSION_PATH_PREFIX') || null,
|
||||
seccompLikeProfileName: esc('BARE_OS_SECCOMP_LIKE_PROFILE') || null,
|
||||
note: 'Host-interpreted capability JSON; guest makes no seccomp claims. Path prefix constrains untrusted extension VFS roots when set.',
|
||||
atMs: now
|
||||
}
|
||||
case 'dns_map_active':
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
* sessionId?: string,
|
||||
* bootStartedMs?: number,
|
||||
* signalState?: Map<number, { signal: string, atMs: number }>,
|
||||
* shellJobs?: Array<{ id?: number, done?: boolean, pgid?: number, sid?: number, label?: string }>
|
||||
* shellJobs?: Array<{ id?: number, done?: boolean, pgid?: number, sid?: number, label?: string }>,
|
||||
* ipcStats?: { processGroups?: Record<string, unknown> } | null
|
||||
* }} opts
|
||||
*/
|
||||
export function bareOsProcessTableSnapshot(opts = {}) {
|
||||
@@ -46,10 +47,64 @@ export function bareOsProcessTableSnapshot(opts = {}) {
|
||||
label: typeof j.label === 'string' ? j.label.slice(0, 256) : undefined
|
||||
}
|
||||
})
|
||||
const zombieJobs = jobs.filter((j) => j && j.done === true && typeof j.id === 'number')
|
||||
const zombieProcs = zombieJobs.map((j) => {
|
||||
const jid = typeof j.id === 'number' ? j.id : 0
|
||||
const pid = 4200 + jid
|
||||
const pgid = typeof j.pgid === 'number' ? j.pgid : pid
|
||||
const session = typeof j.sid === 'number' ? j.sid : 1
|
||||
return {
|
||||
pid,
|
||||
ppid: 3,
|
||||
pgid,
|
||||
sid: session,
|
||||
name: 'bare-os-shell-job',
|
||||
state: 'zombie',
|
||||
startedAtMs: now,
|
||||
jobId: jid,
|
||||
label: typeof j.label === 'string' ? j.label.slice(0, 256) : undefined,
|
||||
cwd: '/',
|
||||
fds: []
|
||||
}
|
||||
})
|
||||
return {
|
||||
schema: 1,
|
||||
schemaVersion: 1,
|
||||
schema: 2,
|
||||
schemaVersion: 2,
|
||||
note: 'Synthetic rows; Bare OS guests do not expose host OS processes.',
|
||||
session: {
|
||||
sessionId: sid || undefined,
|
||||
controllingTty: undefined,
|
||||
foregroundPgid: 3
|
||||
},
|
||||
processGroups: {
|
||||
schema: 1,
|
||||
note: 'Synthetic pgid/sid on shell jobs approximate POSIX process groups; guests have no host PIDs.',
|
||||
setpgidAnalog:
|
||||
'Background job start assigns pgid on shellBackgroundJobs rows where implemented.',
|
||||
killpgAnalog:
|
||||
'Virtual signal delivery consults shell job lists; not a full killpg(2) emulation.'
|
||||
},
|
||||
ipcProcessGroups:
|
||||
opts.ipcStats &&
|
||||
opts.ipcStats.processGroups &&
|
||||
typeof opts.ipcStats.processGroups === 'object'
|
||||
? opts.ipcStats.processGroups
|
||||
: undefined,
|
||||
rlimits: {
|
||||
note: 'Logical defaults; host rlimits may differ when subprocess bridge is active.',
|
||||
nofile: { soft: 1024, hard: 4096 },
|
||||
stack: { soft: 8388608, hard: 8388608 }
|
||||
},
|
||||
cwd: '/',
|
||||
fdSummary: {
|
||||
nextFd: 3,
|
||||
reservedStdio: [0, 1, 2],
|
||||
note: 'Additional FDs may appear when pseudo-files or IPC channels are opened.'
|
||||
},
|
||||
jobStats: {
|
||||
running: jobProcs.length,
|
||||
zombie: zombieJobs.length
|
||||
},
|
||||
processes: [
|
||||
mk(1, 0, 'bare-os-kernel', boot || now),
|
||||
mk(2, 1, 'bare-os-booter', boot || now),
|
||||
@@ -57,9 +112,16 @@ export function bareOsProcessTableSnapshot(opts = {}) {
|
||||
...mk(3, 2, 'bare-os-shell', now),
|
||||
sessionId: sid || undefined,
|
||||
sid: 1,
|
||||
pgid: 3
|
||||
pgid: 3,
|
||||
cwd: '/',
|
||||
fds: [0, 1, 2]
|
||||
},
|
||||
...jobProcs
|
||||
...jobProcs.map((p) => ({
|
||||
...p,
|
||||
cwd: typeof p.cwd === 'string' ? p.cwd : '/',
|
||||
fds: Array.isArray(p.fds) ? p.fds : [0, 1, 2]
|
||||
})),
|
||||
...zombieProcs
|
||||
],
|
||||
atMs: now
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export const BARE_OS_PSEUDO_FS_PATHS = Object.freeze([
|
||||
'/proc/bare_os/net_summary.json',
|
||||
'/proc/bare_os/host_os.json',
|
||||
'/proc/bare_os/sync_window.json',
|
||||
'/proc/bare_os/clock.json',
|
||||
'/proc/bare_os/debug.json',
|
||||
'/proc/bare_os/extensions.json',
|
||||
'/proc/bare_os/hdms_hints.json',
|
||||
|
||||
@@ -3,33 +3,47 @@
|
||||
* Keep in sync with `BARE_OS_SYSCALL_OPS` in `packages/bare-os-coreutils/src/getconf.js`
|
||||
* and `docs/reference/posix-conformance-matrix.json`.
|
||||
*/
|
||||
export const BARE_OS_STOCK_SYSCALL_OPS = Object.freeze([
|
||||
'readFile',
|
||||
'writeFile',
|
||||
'readdir',
|
||||
'mkdir',
|
||||
'stat',
|
||||
'unlink',
|
||||
'chmod',
|
||||
'chdir',
|
||||
'getcwd',
|
||||
'readlink',
|
||||
'symlink',
|
||||
'exists',
|
||||
'lstat',
|
||||
'rmdir',
|
||||
'mount',
|
||||
'umount',
|
||||
'kill',
|
||||
'rename',
|
||||
'link',
|
||||
'access',
|
||||
'utimes',
|
||||
'truncate',
|
||||
'ftruncate',
|
||||
'pathconf'
|
||||
|
||||
/** @typedef {'stable' | 'experimental'} BareOsSyscallStability */
|
||||
/** @typedef {'fs' | 'proc' | 'ipc' | 'signal' | 'vfs_meta'} BareOsSyscallCategory */
|
||||
|
||||
/**
|
||||
* Rich syscall table for `/proc/bare_os/syscalls.json` (schema v3+).
|
||||
* @type {ReadonlyArray<{ name: string, category: BareOsSyscallCategory, stability: BareOsSyscallStability }>}
|
||||
*/
|
||||
export const BARE_OS_SYSCALL_OPS_DETAIL = Object.freeze([
|
||||
{ name: 'readFile', category: 'fs', stability: 'stable' },
|
||||
{ name: 'writeFile', category: 'fs', stability: 'stable' },
|
||||
{ name: 'readdir', category: 'fs', stability: 'stable' },
|
||||
{ name: 'mkdir', category: 'fs', stability: 'stable' },
|
||||
{ name: 'stat', category: 'fs', stability: 'stable' },
|
||||
{ name: 'unlink', category: 'fs', stability: 'stable' },
|
||||
{ name: 'chmod', category: 'fs', stability: 'stable' },
|
||||
{ name: 'chdir', category: 'fs', stability: 'stable' },
|
||||
{ name: 'getcwd', category: 'fs', stability: 'stable' },
|
||||
{ name: 'readlink', category: 'fs', stability: 'stable' },
|
||||
{ name: 'symlink', category: 'fs', stability: 'stable' },
|
||||
{ name: 'exists', category: 'fs', stability: 'stable' },
|
||||
{ name: 'lstat', category: 'fs', stability: 'stable' },
|
||||
{ name: 'rmdir', category: 'fs', stability: 'stable' },
|
||||
{ name: 'mount', category: 'vfs_meta', stability: 'experimental' },
|
||||
{ name: 'umount', category: 'vfs_meta', stability: 'experimental' },
|
||||
{ name: 'kill', category: 'signal', stability: 'stable' },
|
||||
{ name: 'rename', category: 'fs', stability: 'stable' },
|
||||
{ name: 'link', category: 'fs', stability: 'experimental' },
|
||||
{ name: 'access', category: 'fs', stability: 'stable' },
|
||||
{ name: 'utimes', category: 'fs', stability: 'stable' },
|
||||
{ name: 'truncate', category: 'fs', stability: 'stable' },
|
||||
{ name: 'ftruncate', category: 'fs', stability: 'experimental' },
|
||||
{ name: 'fsync', category: 'fs', stability: 'experimental' },
|
||||
{ name: 'fdatasync', category: 'fs', stability: 'experimental' },
|
||||
{ name: 'pathconf', category: 'fs', stability: 'experimental' }
|
||||
])
|
||||
|
||||
export const BARE_OS_STOCK_SYSCALL_OPS = Object.freeze(
|
||||
BARE_OS_SYSCALL_OPS_DETAIL.map((o) => o.name)
|
||||
)
|
||||
|
||||
export function bareOsStockSyscallOpsCsv() {
|
||||
return BARE_OS_STOCK_SYSCALL_OPS.join(',')
|
||||
}
|
||||
|
||||
@@ -117,11 +117,13 @@ async function mirrorBareOsTelemetryNdjson(ctx, rec) {
|
||||
const bareModuleCryptoStagingProbeId = String(env.BARE_OS_PROBE_ID_BARE_MODULE_CRYPTO_STAGING || '').trim()
|
||||
const pearInspectLoggerTlsProbeId = String(env.BARE_OS_PROBE_ID_PEAR_INSPECT_LOGGER_TLS || '').trim()
|
||||
const hypercorePackHrpcLifecycleProbeId = String(env.BARE_OS_PROBE_ID_HYPERCORE_PACK_HRPC_LIFECYCLE || '').trim()
|
||||
const traceId = String(env.BARE_OS_TRACE_ID || '').trim()
|
||||
const line =
|
||||
JSON.stringify({
|
||||
telemetrySchemaVersion: BARE_OS_TELEMETRY_SCHEMA_VERSION,
|
||||
lifecycleSchemaVersion: BARE_OS_LIFECYCLE_SCHEMA_VERSION,
|
||||
ts: Date.now(),
|
||||
traceId: traceId ? traceId.slice(0, 128) : undefined,
|
||||
bootAttemptId: bootAttemptId ? bootAttemptId.slice(0, 128) : undefined,
|
||||
bareModuleCryptoStagingProbeId: bareModuleCryptoStagingProbeId ? bareModuleCryptoStagingProbeId.slice(0, 128) : undefined,
|
||||
pearInspectLoggerTlsProbeId: pearInspectLoggerTlsProbeId ? pearInspectLoggerTlsProbeId.slice(0, 128) : undefined,
|
||||
|
||||
@@ -55,6 +55,9 @@ const _BIN_WORKER_CRYPTOPROC = new Set(['openssl'])
|
||||
/** Indexer-adjacent tools under `indexerproc:*` (word 11; reserved; extend when `/bin` gains indexer helpers). */
|
||||
const _BIN_WORKER_INDEXERPROC = new Set([])
|
||||
|
||||
/** VFS metadata utilities under `metaproc:*` (ACL / xattr sidecars). */
|
||||
const _BIN_WORKER_METAPROC = new Set(['getfacl', 'setfacl', 'xattr'])
|
||||
|
||||
/** Process/shell-adjacent builtins allowed under `sysproc:*` (documentary group; bare-process-class). */
|
||||
const _BIN_WORKER_SYSPROC = new Set([
|
||||
'pwd',
|
||||
@@ -114,6 +117,10 @@ function binWorkerOffloadEnabled(cmd, env) {
|
||||
if (_BIN_WORKER_INDEXERPROC.has(cmd)) return true
|
||||
continue
|
||||
}
|
||||
if (p === 'metaproc:*') {
|
||||
if (_BIN_WORKER_METAPROC.has(cmd)) return true
|
||||
continue
|
||||
}
|
||||
if (p.endsWith(':*')) {
|
||||
const pre = p.slice(0, -2)
|
||||
if (pre === 'textproc' && _BIN_WORKER_TEXTPROC.has(cmd)) return true
|
||||
@@ -123,6 +130,7 @@ function binWorkerOffloadEnabled(cmd, env) {
|
||||
if (pre === 'sysproc' && _BIN_WORKER_SYSPROC.has(cmd)) return true
|
||||
if (pre === 'cryptoproc' && _BIN_WORKER_CRYPTOPROC.has(cmd)) return true
|
||||
if (pre === 'indexerproc' && _BIN_WORKER_INDEXERPROC.has(cmd)) return true
|
||||
if (pre === 'metaproc' && _BIN_WORKER_METAPROC.has(cmd)) return true
|
||||
continue
|
||||
}
|
||||
if (p === cmd) return true
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
replDbg,
|
||||
unbindReplDebugStream
|
||||
} from './debug-repl.js'
|
||||
import { runKernelShutdownHooks, stopBareInitd } from './bare-initd.js'
|
||||
import {
|
||||
bareInitdShutdownActiveUnitsReverse,
|
||||
runKernelShutdownHooks,
|
||||
stopBareInitd
|
||||
} from './bare-initd.js'
|
||||
|
||||
/**
|
||||
* Kernel `console` must write to the same stream as the line editor so cursor stays in sync.
|
||||
@@ -166,6 +170,7 @@ export async function createKernelReplSession({
|
||||
async function cleanup() {
|
||||
if (isReplDebug())
|
||||
replDbg('repl', 'cleanup', fishRead ? 'fish teardown' : 'noop')
|
||||
await bareInitdShutdownActiveUnitsReverse(ctx)
|
||||
await runKernelShutdownHooks()
|
||||
stopBareInitd()
|
||||
if (fishRead && stdin) {
|
||||
|
||||
@@ -1700,7 +1700,23 @@ function scheduleBackgroundShell(ctx, toks) {
|
||||
ctx.shellBackgroundJobs = { nextId: 1, list: [] }
|
||||
}
|
||||
if (!ctx.shellSessionState) {
|
||||
ctx.shellSessionState = { sid: 1, nextPgid: 300 }
|
||||
ctx.shellSessionState = {
|
||||
sid: 1,
|
||||
nextPgid: 300,
|
||||
foregroundPgid: 1,
|
||||
controllingTty: '/dev/console'
|
||||
}
|
||||
} else if (ctx.shellSessionState.foregroundPgid == null) {
|
||||
ctx.shellSessionState.foregroundPgid = ctx.shellSessionState.sid
|
||||
}
|
||||
if (!ctx.shellSessionState.controllingTty) {
|
||||
const fifo =
|
||||
(ctx.env && ctx.env.BARE_OS_SESSION_FIFO) ||
|
||||
(ctx.env && ctx.env.BARE_OS_IPC_SESSION_FIFO) ||
|
||||
''
|
||||
ctx.shellSessionState.controllingTty = fifo
|
||||
? `ipc:${String(fifo).trim()}`
|
||||
: '/dev/console'
|
||||
}
|
||||
const id = ctx.shellBackgroundJobs.nextId++
|
||||
const pgid = ctx.shellSessionState.nextPgid++
|
||||
@@ -2280,8 +2296,27 @@ export async function execShellLine(ctx, line) {
|
||||
*/
|
||||
async function execShellLineInner(ctx, rawTrimmed) {
|
||||
if (!ctx.shellSessionState) {
|
||||
ctx.shellSessionState = { sid: 1, nextPgid: 300 }
|
||||
ctx.shellSessionState = {
|
||||
sid: 1,
|
||||
nextPgid: 300,
|
||||
foregroundPgid: 1,
|
||||
controllingTty: '/dev/console'
|
||||
}
|
||||
} else {
|
||||
if (ctx.shellSessionState.foregroundPgid == null) {
|
||||
ctx.shellSessionState.foregroundPgid = ctx.shellSessionState.sid
|
||||
}
|
||||
if (!ctx.shellSessionState.controllingTty) {
|
||||
const fifo =
|
||||
(ctx.env && ctx.env.BARE_OS_SESSION_FIFO) ||
|
||||
(ctx.env && ctx.env.BARE_OS_IPC_SESSION_FIFO) ||
|
||||
''
|
||||
ctx.shellSessionState.controllingTty = fifo
|
||||
? `ipc:${String(fifo).trim()}`
|
||||
: '/dev/console'
|
||||
}
|
||||
}
|
||||
ctx.shellSessionState.foregroundPgid = ctx.shellSessionState.sid
|
||||
let execLine = rawTrimmed
|
||||
const readL = ctx.readLine
|
||||
if (typeof readL === 'function') {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* GNU extensions / compression are not supported.
|
||||
*/
|
||||
import b4a from 'b4a'
|
||||
import unixPathResolve from 'unix-path-resolve'
|
||||
|
||||
const BLK = 512
|
||||
|
||||
@@ -49,7 +50,7 @@ function octalPad(oct, len) {
|
||||
/**
|
||||
* @param {Record<string, unknown>} header
|
||||
*/
|
||||
function encodeUstarHeader(header) {
|
||||
export function encodeUstarHeader(header) {
|
||||
const h = new Uint8Array(BLK)
|
||||
const name = String(header.name || '').slice(0, 100)
|
||||
const mode = String(header.mode || 0o644).slice(0, 7)
|
||||
@@ -58,6 +59,7 @@ function encodeUstarHeader(header) {
|
||||
const size = String(header.size || 0).slice(0, 11)
|
||||
const mtime = String(header.mtime || 0).slice(0, 11)
|
||||
const typeflag = header.typeflag || '0'
|
||||
const linkname = String(header.linkname || '').slice(0, 100)
|
||||
|
||||
const w = (str, off, max) => {
|
||||
const enc = utf8Encode(str)
|
||||
@@ -73,6 +75,7 @@ function encodeUstarHeader(header) {
|
||||
// ustar checksum field (sum of header bytes; standard layout)
|
||||
for (let i = 148; i < 156; i++) h[i] = 32
|
||||
h[156] = typeflag.charCodeAt(0)
|
||||
w(linkname, 157, 100)
|
||||
w('ustar\0', 257, 6)
|
||||
w('00', 263, 2)
|
||||
|
||||
@@ -94,7 +97,26 @@ function parseUstarBlock(block) {
|
||||
const name = utf8Decode(block.subarray(0, 100)).replace(/\0.*$/, '')
|
||||
const size = parseOctalField(block, 124, 12)
|
||||
const typeflag = String.fromCharCode(block[156] || 48)
|
||||
return { name, size, typeflag }
|
||||
const linkname = utf8Decode(block.subarray(157, 257)).replace(/\0.*$/, '')
|
||||
return { name, size, typeflag, linkname }
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve ustar hard-link `linkname` relative to the directory of the new link path.
|
||||
* @param {string} cwdAbs
|
||||
* @param {string} entryName
|
||||
* @param {string} linkname
|
||||
*/
|
||||
function resolveTarHardlinkSource(cwdAbs, entryName, linkname) {
|
||||
const ln = String(linkname).trim()
|
||||
if (!ln) return ''
|
||||
if (ln.startsWith('/')) return ln.replace(/\/+$/, '') || '/'
|
||||
const dest =
|
||||
cwdAbs === '/' ? '/' + entryName : cwdAbs.replace(/\/+$/, '') + '/' + entryName
|
||||
const slash = dest.lastIndexOf('/')
|
||||
const destDir = slash <= 0 ? '/' : dest.slice(0, slash)
|
||||
const rel = ln.replace(/^\.\//, '')
|
||||
return unixPathResolve(destDir === '/' ? '/' : destDir + '/', rel)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,7 +130,7 @@ export async function runTarCli(ctx, argv) {
|
||||
'usage: tar -cf ARCHIVE PATH...\n' +
|
||||
' tar -tf ARCHIVE\n' +
|
||||
' tar -xf ARCHIVE\n' +
|
||||
'Bare OS: ustar only, VFS paths.\n'
|
||||
'Bare OS: ustar only, VFS paths. Hard links: copy-on-extract unless BARE_OS_VFS_STRICT_HARDLINK.\n'
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -258,7 +280,7 @@ export async function runTarCli(ctx, argv) {
|
||||
if (block.every((b) => b === 0)) break
|
||||
off += BLK
|
||||
const rec = parseUstarBlock(block)
|
||||
const { name, size, typeflag } = rec
|
||||
const { name, size, typeflag, linkname } = rec
|
||||
if (off + size > buf.length) {
|
||||
ctx.console.error('tar: corrupt archive (short read)')
|
||||
ctx.exitCode = 1
|
||||
@@ -269,6 +291,45 @@ export async function runTarCli(ctx, argv) {
|
||||
off += (BLK - (size % BLK)) % BLK
|
||||
|
||||
if (!name) continue
|
||||
if (typeflag === '1') {
|
||||
if (list) ctx.console.log(name)
|
||||
if (extract) {
|
||||
const strict =
|
||||
vfs.env &&
|
||||
(vfs.env.BARE_OS_VFS_STRICT_HARDLINK === '1' ||
|
||||
vfs.env.BARE_OS_VFS_STRICT_HARDLINK === 'true')
|
||||
if (strict) {
|
||||
ctx.console.error(
|
||||
'tar: hard link entries not supported with BARE_OS_VFS_STRICT_HARDLINK (unset for copy-on-extract)'
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const cwd = vfs.resolveLogical('.')
|
||||
const target =
|
||||
cwd === '/' ? '/' + name : cwd.replace(/\/$/, '') + '/' + name
|
||||
const src = resolveTarHardlinkSource(cwd, name, linkname)
|
||||
if (!linkname.trim() || !src) {
|
||||
ctx.console.error('tar: hard link missing linkname: ' + name)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await vfs.readFile(src)
|
||||
await vfs.writeFile(target, b4.from(data))
|
||||
} catch (e) {
|
||||
ctx.console.error(
|
||||
'tar: extract ' +
|
||||
name +
|
||||
': ' +
|
||||
((e && /** @type {Error} */ (e).message) || String(e))
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (typeflag === '0' || typeflag === '\0' || typeflag === '') {
|
||||
if (list) ctx.console.log(name)
|
||||
if (extract) {
|
||||
|
||||
@@ -370,6 +370,7 @@ export const BARE_OS_PROC_FILE_TO_ID_HYPERCORE_PACK_HRPC_LIFECYCLE = Object.free
|
||||
* procBareOsVirtualRegistryText?: () => string,
|
||||
* procBareOsHostOsText?: () => string,
|
||||
* procBareOsSyncWindowText?: () => string,
|
||||
* procBareOsClockText?: () => string,
|
||||
* procBareOsDebugText?: () => string,
|
||||
* procBareOsReplicationOperatorSurfaceJsonText?: (replicationOperatorSurfaceProcId: string) => string,
|
||||
* procBareOsPearCorestoreHrpcJsonText?: (pearCorestoreHrpcProcId: string) => string,
|
||||
@@ -383,7 +384,8 @@ export const BARE_OS_PROC_FILE_TO_ID_HYPERCORE_PACK_HRPC_LIFECYCLE = Object.free
|
||||
* unionWriteDenyPrefixes?: readonly string[],
|
||||
* sysClassNetLoText?: () => string,
|
||||
* hostProcStatsRef?: { stats?: unknown } | null,
|
||||
* ipcFifoLogicalToActual?: (logicalName: string) => string
|
||||
* ipcFifoLogicalToActual?: (logicalName: string) => string,
|
||||
* getProcSelfExtraFds?: () => { fdNum: string, target: string }[]
|
||||
* }} [vfsOptions]
|
||||
*/
|
||||
export function createVfs(
|
||||
@@ -598,6 +600,10 @@ export function createVfs(
|
||||
typeof vfsOptions.procBareOsSyncWindowText === 'function'
|
||||
? vfsOptions.procBareOsSyncWindowText
|
||||
: null
|
||||
const procBareOsClockText =
|
||||
typeof vfsOptions.procBareOsClockText === 'function'
|
||||
? vfsOptions.procBareOsClockText
|
||||
: null
|
||||
const procBareOsDebugText =
|
||||
typeof vfsOptions.procBareOsDebugText === 'function'
|
||||
? vfsOptions.procBareOsDebugText
|
||||
@@ -647,6 +653,25 @@ export function createVfs(
|
||||
)
|
||||
: []
|
||||
|
||||
/**
|
||||
* Deny writes under union overlay read paths when `BARE_OS_VFS_UNION_WRITE_DENY` applies.
|
||||
* @param {string} abs logical absolute path
|
||||
*/
|
||||
function assertUnionWriteNotDenied(abs) {
|
||||
if (!unionReadPrefixes.length || !unionWriteDenyPrefixes.length) return
|
||||
const underUnion = unionReadPrefixes.some(
|
||||
(pre) => abs === pre || abs.startsWith(pre + '/')
|
||||
)
|
||||
if (!underUnion) return
|
||||
for (const d of unionWriteDenyPrefixes) {
|
||||
if (abs === d || abs.startsWith(d + '/')) {
|
||||
throw new Error(
|
||||
'EACCES: union write denied (BARE_OS_VFS_UNION_WRITE_DENY): ' + abs
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Comma-separated absolute prefixes from `boot.policy` v3 (`denyVfsPrefixes`). */
|
||||
function assertNotBootPolicyDenyVfs(abs, op) {
|
||||
const raw = env.BARE_OS_BOOT_POLICY_DENY_VFS || ''
|
||||
@@ -678,6 +703,10 @@ export function createVfs(
|
||||
typeof vfsOptions.ipcFifoLogicalToActual === 'function'
|
||||
? vfsOptions.ipcFifoLogicalToActual
|
||||
: (/** @type {string} */ n) => bareOsIpcLogicalToActualFifoName(n, env)
|
||||
const getProcSelfExtraFds =
|
||||
typeof vfsOptions.getProcSelfExtraFds === 'function'
|
||||
? vfsOptions.getProcSelfExtraFds
|
||||
: null
|
||||
const HOME = () => env.HOME || '/home/guest'
|
||||
let cwd = env.PWD || HOME()
|
||||
|
||||
@@ -764,6 +793,32 @@ export function createVfs(
|
||||
return b4a.from(String(str), 'utf8')
|
||||
}
|
||||
|
||||
function listProcSelfFdDirNames() {
|
||||
const names = new Set(['0', '1', '2'])
|
||||
const extra = getProcSelfExtraFds ? getProcSelfExtraFds() : []
|
||||
for (const e of extra) {
|
||||
if (e && e.fdNum && /^[0-9]+$/.test(String(e.fdNum))) {
|
||||
names.add(String(e.fdNum))
|
||||
}
|
||||
}
|
||||
return [...names].sort((a, b) => Number(a) - Number(b))
|
||||
}
|
||||
|
||||
function encodeProcSelfFdSymlink(fdNumRaw) {
|
||||
const n = String(fdNumRaw ?? '0')
|
||||
if (n === '0' || n === '1' || n === '2') {
|
||||
return utf8Encode(
|
||||
`/proc/self/fd/${n} -> (bare-os stdio pseudo inode)\n`
|
||||
)
|
||||
}
|
||||
const extra = getProcSelfExtraFds ? getProcSelfExtraFds() : []
|
||||
const hit = extra.find((e) => e && String(e.fdNum) === n)
|
||||
if (hit && hit.target) {
|
||||
return utf8Encode(`/proc/self/fd/${n} -> ${hit.target}\n`)
|
||||
}
|
||||
return utf8Encode(`/proc/self/fd/${n} -> (not open)\n`)
|
||||
}
|
||||
|
||||
function pseudoEnvironBytes() {
|
||||
const parts = []
|
||||
for (const k of Object.keys(env).sort()) {
|
||||
@@ -1044,7 +1099,7 @@ export function createVfs(
|
||||
if (f === 'bare_os_syscalls') {
|
||||
const t = procBareOsSyscallsText
|
||||
? procBareOsSyscallsText()
|
||||
: '{"schemaVersion":1,"ops":[]}\n'
|
||||
: '{"schemaVersion":3,"ops":[],"errnoHints":{}}\n'
|
||||
return utf8Encode(t)
|
||||
}
|
||||
if (f === 'bare_os_metrics_prom') {
|
||||
@@ -1117,6 +1172,10 @@ export function createVfs(
|
||||
const t = procBareOsSyncWindowText ? procBareOsSyncWindowText() : '{}\n'
|
||||
return utf8Encode(t)
|
||||
}
|
||||
if (f === 'bare_os_clock') {
|
||||
const t = procBareOsClockText ? procBareOsClockText() : '{}\n'
|
||||
return utf8Encode(t)
|
||||
}
|
||||
if (f === 'bare_os_debug') {
|
||||
const t = procBareOsDebugText ? procBareOsDebugText() : '{}\n'
|
||||
return utf8Encode(t)
|
||||
@@ -1274,6 +1333,10 @@ export function createVfs(
|
||||
name: 'sync_window.json',
|
||||
path: '/proc/bare_os/sync_window.json'
|
||||
},
|
||||
{
|
||||
name: 'clock.json',
|
||||
path: '/proc/bare_os/clock.json'
|
||||
},
|
||||
{
|
||||
name: 'debug.json',
|
||||
path: '/proc/bare_os/debug.json'
|
||||
@@ -1749,9 +1812,7 @@ export function createVfs(
|
||||
}
|
||||
if (f === 'self_fd') {
|
||||
const n = /** @type {{ fdNum?: string }} */ (routePseudo).fdNum ?? '0'
|
||||
return utf8Encode(
|
||||
`/proc/self/fd/${n} -> (bare-os stdio pseudo inode)\n`
|
||||
)
|
||||
return encodeProcSelfFdSymlink(n)
|
||||
}
|
||||
if (f === 'net_dev') {
|
||||
const t = procNetDevText
|
||||
@@ -1952,6 +2013,8 @@ export function createVfs(
|
||||
'host_os.json': 'bare_os_host_os',
|
||||
sync_window: 'bare_os_sync_window',
|
||||
'sync_window.json': 'bare_os_sync_window',
|
||||
clock: 'bare_os_clock',
|
||||
'clock.json': 'bare_os_clock',
|
||||
debug: 'bare_os_debug',
|
||||
'debug.json': 'bare_os_debug',
|
||||
udx_extended: 'bare_os_udx_extended',
|
||||
@@ -2436,6 +2499,14 @@ export function createVfs(
|
||||
file: 'bare_os_sync_window'
|
||||
}
|
||||
}
|
||||
if (sub === 'bare_os_clock' || sub === 'bare_os_clock.json') {
|
||||
return {
|
||||
virtualPseudo: true,
|
||||
kind: 'proc',
|
||||
node: 'file',
|
||||
file: 'bare_os_clock'
|
||||
}
|
||||
}
|
||||
if (sub === 'bare_os_debug' || sub === 'bare_os_debug.json') {
|
||||
return {
|
||||
virtualPseudo: true,
|
||||
@@ -3195,7 +3266,13 @@ export function createVfs(
|
||||
return null
|
||||
}
|
||||
|
||||
async function statFromAbs(abs) {
|
||||
/**
|
||||
* Walk final symlink components on logical paths (POSIX-style relative targets).
|
||||
* Hyperdrive `get(..., { follow: true })` does not apply this to VFS layout.
|
||||
* @param {string} abs
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function resolveSymlinksOnLogicalAbs(abs) {
|
||||
const normVar = abs.replace(/\/+$/, '') || '/'
|
||||
const normTmp = abs.replace(/\/+$/, '') || '/'
|
||||
if (
|
||||
@@ -3208,7 +3285,7 @@ export function createVfs(
|
||||
normTmp === '/tmp' ||
|
||||
(activeHomeBasename() && abs === '/home')
|
||||
) {
|
||||
return lstatFromAbs(abs)
|
||||
return abs
|
||||
}
|
||||
const r0 = route(abs)
|
||||
if (
|
||||
@@ -3218,7 +3295,7 @@ export function createVfs(
|
||||
r0.virtualSnapshotRoot ||
|
||||
r0.virtualSnapshotSystem
|
||||
) {
|
||||
return lstatFromAbs(abs)
|
||||
return abs
|
||||
}
|
||||
let cur = abs
|
||||
/** @type {Set<string>} */
|
||||
@@ -3237,26 +3314,31 @@ export function createVfs(
|
||||
curNorm === '/var/log' ||
|
||||
curNorm === '/tmp'
|
||||
) {
|
||||
return lstatFromAbs(cur)
|
||||
return cur
|
||||
}
|
||||
const r = route(cur)
|
||||
if (r.virtualMntRoot || r.virtualVarRoot || r.virtualPseudo) {
|
||||
return lstatFromAbs(cur)
|
||||
return cur
|
||||
}
|
||||
const { drive, path: p } = r
|
||||
if (isHyperdriveRootPath(p)) return lstatFromAbs(cur)
|
||||
if (isHyperdriveRootPath(p)) return cur
|
||||
const e = await entryOn(drive, p, { follow: false })
|
||||
if (!e || !e.value) return lstatFromAbs(cur)
|
||||
if (!e || !e.value) return cur
|
||||
if (e.value.linkname) {
|
||||
const parent = dirnameAbs(cur)
|
||||
cur = unixPathResolve(parent, e.value.linkname)
|
||||
continue
|
||||
}
|
||||
return lstatFromAbs(cur)
|
||||
return cur
|
||||
}
|
||||
throw new Error('ELOOP: too many symlink levels')
|
||||
}
|
||||
|
||||
async function statFromAbs(abs) {
|
||||
const cur = await resolveSymlinksOnLogicalAbs(abs)
|
||||
return lstatFromAbs(cur)
|
||||
}
|
||||
|
||||
async function assertTraverseTo(abs, finalOp) {
|
||||
const { uid: euid, gid: egid } = parseUidGid(env)
|
||||
const prefixes = pathPrefixes(abs)
|
||||
@@ -3521,6 +3603,7 @@ export function createVfs(
|
||||
'bare_os_structured_clone_profile.json',
|
||||
'bare_os_swarm',
|
||||
'bare_os_sync_window.json',
|
||||
'bare_os_clock.json',
|
||||
'bare_os_syscalls',
|
||||
'bare_os_syscalls.json',
|
||||
'bare_os_udx_extended.json',
|
||||
@@ -3582,6 +3665,7 @@ export function createVfs(
|
||||
'blind_relay_router.json',
|
||||
'bootstrap',
|
||||
'brittle_snapshot_ci.json',
|
||||
'clock.json',
|
||||
'broadcast_encryption_hint.json',
|
||||
'bundle_preload_hint.json',
|
||||
'build_attestation_pointer.json',
|
||||
@@ -3704,7 +3788,7 @@ export function createVfs(
|
||||
return ['cgroups', 'cmdline', 'environ', 'exe', 'fd', 'limits']
|
||||
}
|
||||
if (pr.kind === 'proc' && pr.node === 'dir' && pr.dir === 'self_fd') {
|
||||
return ['0', '1', '2']
|
||||
return listProcSelfFdDirNames()
|
||||
}
|
||||
if (pr.kind === 'sys' && pr.node === 'root') {
|
||||
return ['class', 'devices', 'fs']
|
||||
@@ -3909,25 +3993,11 @@ export function createVfs(
|
||||
*/
|
||||
async function writeFileAtAbs(abs, buf, opts = {}) {
|
||||
assertNotBootPolicyDenyVfs(abs, 'write')
|
||||
assertUnionWriteNotDenied(abs)
|
||||
const r = route(abs)
|
||||
if (r.snapshotReadOnly) {
|
||||
throw new Error('EROFS: snapshot checkout is read-only: ' + abs)
|
||||
}
|
||||
if (unionReadPrefixes.length && unionWriteDenyPrefixes.length) {
|
||||
const underUnion = unionReadPrefixes.some(
|
||||
(pre) => abs === pre || abs.startsWith(pre + '/')
|
||||
)
|
||||
if (underUnion) {
|
||||
for (const d of unionWriteDenyPrefixes) {
|
||||
if (abs === d || abs.startsWith(d + '/')) {
|
||||
throw new Error(
|
||||
'EACCES: union write denied (BARE_OS_VFS_UNION_WRITE_DENY): ' +
|
||||
abs
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
r.virtualPseudo &&
|
||||
r.kind === 'dev' &&
|
||||
@@ -4069,13 +4139,58 @@ export function createVfs(
|
||||
}
|
||||
if (r.virtualHomeDir || r.virtualMntRoot || r.virtualVarRoot)
|
||||
return null
|
||||
const { drive, path: p } = r
|
||||
if (isHyperdriveRootPath(p)) return null
|
||||
await assertTraverseTo(abs, 'read')
|
||||
const absFollowed = await resolveSymlinksOnLogicalAbs(abs)
|
||||
const r2 = route(absFollowed)
|
||||
if (r2.virtualHomeDir || r2.virtualMntRoot || r2.virtualVarRoot)
|
||||
return null
|
||||
if (r2.virtualPseudo) {
|
||||
await assertTraverseTo(absFollowed, 'read')
|
||||
if (r2.node === 'enoent') {
|
||||
return null
|
||||
}
|
||||
if (
|
||||
r2.node === 'file' &&
|
||||
r2.file === 'virtual_plugin' &&
|
||||
getVirtualReaders
|
||||
) {
|
||||
const m = getVirtualReaders()
|
||||
const ent =
|
||||
r2.virtualName && m && typeof m.get === 'function'
|
||||
? m.get(r2.virtualName)
|
||||
: null
|
||||
const fn =
|
||||
typeof ent === 'function'
|
||||
? ent
|
||||
: ent &&
|
||||
typeof ent === 'object' &&
|
||||
ent !== null &&
|
||||
typeof (/** @type {{ read?: unknown }} */ (ent).read) ===
|
||||
'function'
|
||||
? /** @type {{ read: () => unknown }} */ (ent).read
|
||||
: null
|
||||
if (typeof fn !== 'function') return null
|
||||
const out = await Promise.resolve(fn())
|
||||
if (typeof out === 'string') return utf8Encode(out)
|
||||
return out instanceof Uint8Array ? out : new Uint8Array(out)
|
||||
}
|
||||
if (r2.node === 'file' && r2.file === 'ipc' && bareOsIpc) {
|
||||
return bareOsIpc.take(r2.ipcName)
|
||||
}
|
||||
if (r2.node === 'file') {
|
||||
return pseudoFileBytes(r2)
|
||||
}
|
||||
return null
|
||||
}
|
||||
await assertTraverseTo(absFollowed, 'read')
|
||||
const { drive, path: p } = r2
|
||||
if (isHyperdriveRootPath(p)) return null
|
||||
if (unionReadPrefixes.length && drive === systemDrive) {
|
||||
for (const pre of unionReadPrefixes) {
|
||||
if (abs === pre || abs.startsWith(pre + '/')) {
|
||||
const ol = '/.bare-os/union' + (abs === '/' ? '' : abs)
|
||||
if (absFollowed === pre || absFollowed.startsWith(pre + '/')) {
|
||||
const ol =
|
||||
'/.bare-os/union' +
|
||||
(absFollowed === '/' ? '' : absFollowed)
|
||||
const ur = route(ol)
|
||||
if (
|
||||
!ur.virtualPseudo &&
|
||||
@@ -4096,23 +4211,23 @@ export function createVfs(
|
||||
if (
|
||||
binReadCache &&
|
||||
drive === systemDrive &&
|
||||
abs.startsWith('/bin/')
|
||||
absFollowed.startsWith('/bin/')
|
||||
) {
|
||||
const hit = binReadCache.get(abs)
|
||||
const hit = binReadCache.get(absFollowed)
|
||||
if (hit) return new Uint8Array(hit)
|
||||
}
|
||||
const got = await drive.get(p, { follow: true })
|
||||
const got = await drive.get(p, { follow: false })
|
||||
if (
|
||||
binReadCache &&
|
||||
drive === systemDrive &&
|
||||
abs.startsWith('/bin/') &&
|
||||
absFollowed.startsWith('/bin/') &&
|
||||
got
|
||||
) {
|
||||
if (binReadCache.size >= BIN_READ_CACHE_MAX) {
|
||||
const first = binReadCache.keys().next().value
|
||||
binReadCache.delete(first)
|
||||
}
|
||||
binReadCache.set(abs, new Uint8Array(got))
|
||||
binReadCache.set(absFollowed, new Uint8Array(got))
|
||||
}
|
||||
return got
|
||||
})(),
|
||||
@@ -4240,6 +4355,7 @@ export function createVfs(
|
||||
*/
|
||||
async chmod(userPath, modeOctal) {
|
||||
const abs = resolveLogical(userPath)
|
||||
assertUnionWriteNotDenied(abs)
|
||||
const r = route(abs)
|
||||
if (
|
||||
r.virtualHomeDir ||
|
||||
@@ -4309,6 +4425,7 @@ export function createVfs(
|
||||
*/
|
||||
async chown(userPath, next) {
|
||||
const abs = resolveLogical(userPath)
|
||||
assertUnionWriteNotDenied(abs)
|
||||
const r = route(abs)
|
||||
if (
|
||||
r.virtualHomeDir ||
|
||||
@@ -4515,6 +4632,7 @@ export function createVfs(
|
||||
*/
|
||||
async symlink(target, userPath) {
|
||||
const abs = resolveLogical(userPath)
|
||||
assertUnionWriteNotDenied(abs)
|
||||
const r = route(abs)
|
||||
if (
|
||||
r.virtualHomeDir ||
|
||||
@@ -4547,6 +4665,15 @@ export function createVfs(
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* POSIX fsync analog: Hyperdrive has no per-path flush; returns immediately.
|
||||
* @param {string} userPath
|
||||
*/
|
||||
async fsync(userPath) {
|
||||
void resolveLogical(userPath)
|
||||
return undefined
|
||||
},
|
||||
|
||||
/**
|
||||
* Hyperdrive-backed path watch (not pseudo paths). Yields `[current, previous]` snapshots.
|
||||
* Call `await watcher.ready()` then iterate; `await watcher.destroy()` when done.
|
||||
|
||||
@@ -6,6 +6,7 @@ import { mkdirSync, rmSync } from 'node:fs'
|
||||
import { readFile, readdir } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createRequire } from 'node:module'
|
||||
import { PassThrough } from 'node:stream'
|
||||
import { runKernelFromSource, runBinCommand } from './lib/kernel-runner.js'
|
||||
import { createGitFsFromVfs } from './lib/git-fs-adapter.js'
|
||||
@@ -68,20 +69,44 @@ import {
|
||||
jobMatchesDate
|
||||
} from './lib/bare-cron.js'
|
||||
import {
|
||||
bareInitdReadinessSnapshot,
|
||||
bareInitdShutdownActiveUnitsReverse,
|
||||
getBareServiceRuntime,
|
||||
registerBareInitdDisposer,
|
||||
registerKernelShutdownHook,
|
||||
runKernelShutdownHooks,
|
||||
startBareInitd,
|
||||
stopBareInitd
|
||||
stopBareInitd,
|
||||
getLastBareInitdDagSnapshotJson
|
||||
} from './lib/bare-initd.js'
|
||||
import { parseUnitDropInText } from './lib/bare-initd-user.js'
|
||||
import { bareOsProcessTableSnapshot } from './lib/bare-os-process-table.js'
|
||||
import { buildBareOsReplicationOperatorSurfaceProcJson } from './lib/bare-os-proc-replication-operator-surface.js'
|
||||
import {
|
||||
DEFAULT_CURL_USER_AGENT,
|
||||
DEFAULT_WGET_USER_AGENT
|
||||
} from './lib/http-fetch-url.js'
|
||||
import { filenameFromContentDisposition } from './lib/curl-cli.js'
|
||||
import { encodeUstarHeader, runTarCli } from './lib/tar-cli.js'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const TAR_BLK = 512
|
||||
|
||||
/** @param {Uint8Array[]} parts */
|
||||
function concatTarParts(parts) {
|
||||
let n = 0
|
||||
for (const p of parts) n += p.length
|
||||
const out = new Uint8Array(n)
|
||||
let o = 0
|
||||
for (const p of parts) {
|
||||
out.set(p, o)
|
||||
o += p.length
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function headerFromInit(init, name) {
|
||||
if (!init?.headers) return undefined
|
||||
const h = init.headers
|
||||
@@ -155,6 +180,117 @@ test('giant_phase_program proc id returns same builder as kernel_program', async
|
||||
t.is(a.schema, b.schema)
|
||||
})
|
||||
|
||||
test('parseUnitDropInText parses ConditionPathExists and Restart keys', async (t) => {
|
||||
const d = parseUnitDropInText(
|
||||
'[Unit]\nConditionPathExists=/tmp/x\nRestart=on-failure\nRestartSec=2\n'
|
||||
)
|
||||
t.is(d.conditionPathExists, '/tmp/x')
|
||||
t.is(d.restart, 'on-failure')
|
||||
t.is(d.restartSec, 2)
|
||||
})
|
||||
|
||||
test('bareOsIpc.signalProcessGroup pushes bareOsProcessGroupSignal to scoped channels', async (t) => {
|
||||
const ipc = createBareOsIpc()
|
||||
ipc.create('c1')
|
||||
ipc.create('c2')
|
||||
ipc.assignProcessGroup('c1', 7)
|
||||
ipc.assignProcessGroup('c2', 7)
|
||||
const p1 = ipc.takeJson('c1')
|
||||
const p2 = ipc.takeJson('c2')
|
||||
const out = ipc.signalProcessGroup(7, 'SIGUSR1')
|
||||
t.is(out.delivered, 2)
|
||||
const j1 = await p1
|
||||
const j2 = await p2
|
||||
t.is(j1.method, 'bareOsProcessGroupSignal')
|
||||
t.is(j1.signal, 'SIGUSR1')
|
||||
t.is(j2.pgid, 7)
|
||||
const st = ipc.stats()
|
||||
t.ok(st.processGroups && st.processGroups.byPgid)
|
||||
t.ok(st.processGroups.byPgid['pgid:7'].includes('c1'))
|
||||
})
|
||||
|
||||
test('kernel ext incremental reload runs only new scripts when hot reload env set', async (t) => {
|
||||
const src = await readFile(
|
||||
path.join(__dirname, '../../kernel/init.js'),
|
||||
'utf8'
|
||||
)
|
||||
/** @type {string[]} */
|
||||
const extDir = ['01-a.json']
|
||||
/** @type {string[]} */
|
||||
const ran = []
|
||||
const vfsFiles = new Map()
|
||||
const drive = {
|
||||
async get(p) {
|
||||
if (p === '/etc/os-release') return b4a.from('ID=test\n')
|
||||
if (p === '/etc/motd') return b4a.from('')
|
||||
if (p === '/etc/bare-os/kernel.ext.d/01-a.json') {
|
||||
return b4a.from(
|
||||
JSON.stringify({
|
||||
id: 'a',
|
||||
scripts: ['/lib/bare-os/extensions/a.js']
|
||||
})
|
||||
)
|
||||
}
|
||||
if (p === '/etc/bare-os/kernel.ext.d/02-b.json') {
|
||||
return b4a.from(
|
||||
JSON.stringify({
|
||||
id: 'b',
|
||||
scripts: ['/lib/bare-os/extensions/b.js']
|
||||
})
|
||||
)
|
||||
}
|
||||
return null
|
||||
},
|
||||
async *readdir(d) {
|
||||
if (d === '/etc/bare-os/kernel.ext.d') {
|
||||
for (const x of extDir) yield x
|
||||
}
|
||||
}
|
||||
}
|
||||
const ctx = {
|
||||
bareOsSkipRepl: true,
|
||||
env: {
|
||||
BARE_OS_BOOT_SKIP_STAGES:
|
||||
'profile,rc,rc.d,rc.local,kernel.d,onboot,selftest',
|
||||
BARE_OS_KERNEL_EXT_D_HOT_RELOAD: '1'
|
||||
},
|
||||
drive,
|
||||
b4a,
|
||||
vfs: {
|
||||
async readFile(p) {
|
||||
return vfsFiles.get(p) ?? null
|
||||
},
|
||||
async writeFile(p, body) {
|
||||
vfsFiles.set(
|
||||
p,
|
||||
body instanceof Uint8Array ? body : b4a.from(String(body))
|
||||
)
|
||||
}
|
||||
},
|
||||
console: { log() {}, error() {}, warn() {} },
|
||||
readLine: async () => null,
|
||||
async execLine() {
|
||||
return 'ok'
|
||||
},
|
||||
async bareOsRunImageScript(path) {
|
||||
ran.push(path)
|
||||
}
|
||||
}
|
||||
await runKernelFromSource(src, ctx)
|
||||
t.alike(ran, ['/lib/bare-os/extensions/a.js'])
|
||||
t.ok(typeof ctx.bareOsReloadKernelExtDropinsSafe === 'function')
|
||||
extDir.push('02-b.json')
|
||||
const out = await ctx.bareOsReloadKernelExtDropinsSafe()
|
||||
t.ok(out.ok)
|
||||
t.alike(out.ranScripts, ['/lib/bare-os/extensions/b.js'])
|
||||
t.alike(ran, [
|
||||
'/lib/bare-os/extensions/a.js',
|
||||
'/lib/bare-os/extensions/b.js'
|
||||
])
|
||||
const jr = b4a.toString(vfsFiles.get('/run/bare-os/kernel-ext-reload.ndjson'))
|
||||
t.ok(jr.includes('incremental'))
|
||||
})
|
||||
|
||||
test('stock kernel BARE_OS_BOOT_SAFE_MODE skips kernel.ext.d scripts', async (t) => {
|
||||
const src = await readFile(
|
||||
path.join(__dirname, '../../kernel/init.js'),
|
||||
@@ -962,6 +1098,7 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
|
||||
'bare_os_capabilities',
|
||||
'bare_os_capabilities.json',
|
||||
'bare_os_cellery_sidecar_hint.json',
|
||||
'bare_os_clock.json',
|
||||
'bare_os_compact_encoding_profile.json',
|
||||
'bare_os_corestore_gc_hint.json',
|
||||
'bare_os_debug.json',
|
||||
@@ -2048,6 +2185,67 @@ async function run(ctx) {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('execShellLine pipeline < in and > out redirection', async (t) => {
|
||||
const dir = testCorestoreDir('shpipeio')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('spio'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
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/cat', b4a.from(cat))
|
||||
const ctx = testCtx(drive, personal)
|
||||
await ctx.vfs.writeFile('~/in.txt', b4a.from('pipeline-data\n'))
|
||||
await execShellLine(ctx, 'cat < ~/in.txt | cat > ~/out.txt')
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(
|
||||
b4a.toString(await ctx.vfs.readFile('~/out.txt'), 'utf8'),
|
||||
'pipeline-data\n'
|
||||
)
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('execShellLine pipeline first stage stdin < file only', async (t) => {
|
||||
const dir = testCorestoreDir('shpipein')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('spin'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const cat = `
|
||||
function bareStdin(ctx) {
|
||||
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
||||
}
|
||||
async function run(ctx) {
|
||||
ctx.console.log(bareStdin(ctx).replace(/\\n$/, ''))
|
||||
}
|
||||
`
|
||||
const wcPath = path.join(__dirname, '../../kernel/bin/wc')
|
||||
const wcSrc = await readFile(wcPath, 'utf8')
|
||||
await drive.put('/bin/cat', b4a.from(cat))
|
||||
await drive.put('/bin/wc', b4a.from(wcSrc))
|
||||
const lines = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.console = {
|
||||
log: (s) => lines.push(String(s)),
|
||||
error: (...a) => lines.push(a.join(' '))
|
||||
}
|
||||
await ctx.vfs.writeFile('~/lines.txt', b4a.from('a\nb\nc\n'))
|
||||
await execShellLine(ctx, 'wc -l < ~/lines.txt | cat')
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.ok(lines.some((l) => /^\s*3\s/.test(l) || l.includes('3')))
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('execShellLine pipeline stage and byte limits', async (t) => {
|
||||
const dir = testCorestoreDir('shpipe')
|
||||
const store = new Corestore(dir)
|
||||
@@ -2429,6 +2627,10 @@ test('systemctl list after startBareInitd shows kernel-logger and bare-cron', as
|
||||
const ctx = testCtx(sys, personal)
|
||||
ctx.execLine = async () => {}
|
||||
await startBareInitd(ctx)
|
||||
const dagRaw = getLastBareInitdDagSnapshotJson()
|
||||
const dag = JSON.parse(String(dagRaw).trim())
|
||||
t.ok(dag && dag.supervision && dag.supervision.schema === 1)
|
||||
t.ok(Array.isArray(dag.supervision.restartKeys))
|
||||
ctx.console = {
|
||||
log: (...a) => logs.push(a.join(' ')),
|
||||
error: (...a) => logs.push(a.join(' '))
|
||||
@@ -2577,6 +2779,77 @@ test('runKernelShutdownHooks runs LIFO once', async (t) => {
|
||||
t.is(o.join(','), 'b,a')
|
||||
})
|
||||
|
||||
test('bareInitdShutdownActiveUnitsReverse is safe when nothing is active', async (t) => {
|
||||
stopBareInitd()
|
||||
await bareInitdShutdownActiveUnitsReverse({ vfs: null, console, env: {} })
|
||||
t.pass()
|
||||
})
|
||||
|
||||
test('/proc/bare_os/clock.json is readable pseudo JSON', async (t) => {
|
||||
const dir = testCorestoreDir('clock-proc')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pc'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const bootStartedMs = Date.now() - 50
|
||||
const shellEnv = {
|
||||
HOME: '/home/user',
|
||||
PATH: '/bin',
|
||||
USER: 'user',
|
||||
UID: '1000',
|
||||
GID: '1000',
|
||||
PWD: '/home/user',
|
||||
BARE_OS_EXIT_STATUS: '0'
|
||||
}
|
||||
const vfs = createVfs(drive, personal, shellEnv, null, {
|
||||
bareOsIpc: createBareOsIpc(),
|
||||
procBareOsClockText() {
|
||||
const wallMs = Date.now()
|
||||
return `${JSON.stringify({
|
||||
schema: 1,
|
||||
CLOCK_REALTIME_MS: wallMs,
|
||||
CLOCK_BOOTTIME_RELATIVE_MS: wallMs - bootStartedMs,
|
||||
CLOCK_MONOTONIC_NS_FROM_PERF: null
|
||||
})}\n`
|
||||
}
|
||||
})
|
||||
const raw = b4a.toString(await vfs.readFile('/proc/bare_os/clock.json'))
|
||||
const j = JSON.parse(raw)
|
||||
t.is(j.schema, 1)
|
||||
t.ok(typeof j.CLOCK_REALTIME_MS === 'number')
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('/proc/self/fd lists extras from getProcSelfExtraFds', async (t) => {
|
||||
const dir = testCorestoreDir('self-fd-extra')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pc'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const shellEnv = {
|
||||
HOME: '/home/user',
|
||||
PATH: '/bin',
|
||||
USER: 'user',
|
||||
UID: '1000',
|
||||
GID: '1000',
|
||||
PWD: '/home/user',
|
||||
BARE_OS_EXIT_STATUS: '0'
|
||||
}
|
||||
const vfs = createVfs(drive, personal, shellEnv, null, {
|
||||
bareOsIpc: createBareOsIpc(),
|
||||
getProcSelfExtraFds: () => [{ fdNum: '7', target: 'pipe:[bare-test]' }]
|
||||
})
|
||||
const names = await vfs.readdir('/proc/self/fd')
|
||||
t.ok(names.includes('7'))
|
||||
const txt = b4a.toString(await vfs.readFile('/proc/self/fd/7'))
|
||||
t.ok(txt.includes('pipe:[bare-test]'))
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('fish-readline stripAnsi and fuzzyMatch', async (t) => {
|
||||
t.is(stripAnsi('\x1b[32mhi\x1b[0m'), 'hi')
|
||||
t.ok(fuzzyMatch('hello', 'hlo'))
|
||||
@@ -3013,6 +3286,447 @@ test('tier-1 find -mindepth skips shallow paths', async (t) => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('vfs union write deny rejects chmod and symlink targets', async (t) => {
|
||||
const dir = testCorestoreDir('union-deny')
|
||||
const store = new Corestore(dir)
|
||||
const sys = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pu'))
|
||||
await sys.ready()
|
||||
await personal.ready()
|
||||
const shellEnv = {
|
||||
HOME: '/home/guest',
|
||||
PWD: '/home/guest',
|
||||
PATH: '/bin',
|
||||
USER: 'guest',
|
||||
UID: '0',
|
||||
GID: '0'
|
||||
}
|
||||
const vfs = createVfs(sys, personal, shellEnv, null, {
|
||||
bareOsIpc: createBareOsIpc(),
|
||||
unionReadPrefixes: ['/etc'],
|
||||
unionWriteDenyPrefixes: ['/etc']
|
||||
})
|
||||
let w = false
|
||||
try {
|
||||
await vfs.writeFile('/etc/union-blocked', b4a.from('x'))
|
||||
} catch (e) {
|
||||
w = true
|
||||
t.ok(String(e.message).includes('union write denied'))
|
||||
}
|
||||
t.ok(w)
|
||||
let c = false
|
||||
try {
|
||||
await vfs.chmod('/etc/passwd', 0o644)
|
||||
} catch (e) {
|
||||
c = true
|
||||
t.ok(String(e.message).includes('union write denied'))
|
||||
}
|
||||
t.ok(c)
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('vfs symlink relative and absolute targets under personal', async (t) => {
|
||||
const dir = testCorestoreDir('symlink-personal')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('slp'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const ctx = testCtx(drive, personal)
|
||||
await ctx.vfs.writeFile('~/target.txt', b4a.from('hello'))
|
||||
await ctx.vfs.symlink('target.txt', '~/via_rel.txt')
|
||||
t.is(await ctx.vfs.readlink('~/via_rel.txt'), 'target.txt')
|
||||
const st = await ctx.vfs.lstat('~/via_rel.txt')
|
||||
t.is(st.type, 'symlink')
|
||||
t.is(b4a.toString(await ctx.vfs.readFile('~/via_rel.txt'), 'utf8'), 'hello')
|
||||
|
||||
await ctx.vfs.symlink('/home/user/target.txt', '~/via_abs.txt')
|
||||
t.is(await ctx.vfs.readlink('~/via_abs.txt'), '/home/user/target.txt')
|
||||
t.is(b4a.toString(await ctx.vfs.readFile('~/via_abs.txt'), 'utf8'), 'hello')
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('vfs symlink on system drive /var follows within same drive', async (t) => {
|
||||
const dir = testCorestoreDir('symlink-system-var')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('slsv'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const dataPath = '/var/bare-os-symlink-test/data.txt'
|
||||
const aliasPath = '/var/bare-os-symlink-test/alias.txt'
|
||||
await drive.put(dataPath, b4a.from('sysblob'))
|
||||
await drive.symlink(aliasPath, dataPath, { metadata: {} })
|
||||
const ctx = testCtx(drive, personal)
|
||||
t.is(await ctx.vfs.readlink(aliasPath), dataPath)
|
||||
t.is(b4a.toString(await ctx.vfs.readFile(aliasPath), 'utf8'), 'sysblob')
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('HyperDHT local testnet: bootstrap node and ephemeral peer fullyBootstrapped', async (t) => {
|
||||
const createTestnet = require('hyperdht/testnet.js')
|
||||
const DHT = require('hyperdht')
|
||||
const testnet = await createTestnet(1)
|
||||
try {
|
||||
t.ok(testnet.bootstrap.length >= 1, 'testnet exposes bootstrap address')
|
||||
const addr = testnet.nodes[0].address()
|
||||
t.ok(addr && addr.port > 0, 'bootstrap listens on a UDP port')
|
||||
const client = new DHT({
|
||||
ephemeral: true,
|
||||
bootstrap: testnet.bootstrap,
|
||||
host: '127.0.0.1'
|
||||
})
|
||||
try {
|
||||
await client.fullyBootstrapped()
|
||||
t.pass('ephemeral HyperDHT node reaches fullyBootstrapped against local testnet')
|
||||
} finally {
|
||||
await client.destroy()
|
||||
}
|
||||
} finally {
|
||||
await testnet.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test('tier-1 mv renames file in home', async (t) => {
|
||||
const dir = testCorestoreDir('mv-home')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('mvhm'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
await drive.put('/bin/mv', b4a.from(await readBuiltBin('mv')))
|
||||
const ctx = testCtx(drive, personal)
|
||||
await ctx.vfs.writeFile('a.txt', b4a.from('ok'))
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['mv', 'a.txt', 'b.txt'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(b4a.toString(await ctx.vfs.readFile('b.txt'), 'utf8'), 'ok')
|
||||
t.ok(
|
||||
(await ctx.vfs.readFile('a.txt')) == null,
|
||||
'mv removed source path in home'
|
||||
)
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('tier-1 cmp silent and kernel-boot-diff line set', async (t) => {
|
||||
const dir = testCorestoreDir('cmp-kbd')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('cmpkbd'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const cmpSrc = await readFile(
|
||||
path.join(__dirname, '../../kernel/bin/cmp'),
|
||||
'utf8'
|
||||
)
|
||||
const kbdSrc = await readFile(
|
||||
path.join(__dirname, '../../kernel/bin/kernel-boot-diff'),
|
||||
'utf8'
|
||||
)
|
||||
await drive.put('/bin/cmp', b4a.from(cmpSrc))
|
||||
await drive.put('/bin/kernel-boot-diff', b4a.from(kbdSrc))
|
||||
const ctx = testCtx(drive, personal)
|
||||
await ctx.vfs.writeFile('same1.txt', b4a.from('x'))
|
||||
await ctx.vfs.writeFile('same2.txt', b4a.from('x'))
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['cmp', '-s', 'same1.txt', 'same2.txt'])
|
||||
t.is(ctx.exitCode, 0, 'cmp -s identical')
|
||||
await ctx.vfs.writeFile('d2.txt', b4a.from('y'))
|
||||
ctx.exitCode = 0
|
||||
const errs = []
|
||||
ctx.console = {
|
||||
log() {},
|
||||
error(s) {
|
||||
errs.push(String(s))
|
||||
}
|
||||
}
|
||||
await runBinCommand(ctx, ['cmp', '-s', 'same1.txt', 'd2.txt'])
|
||||
t.is(ctx.exitCode, 1, 'cmp -s differs')
|
||||
await ctx.vfs.writeFile('boot-a.txt', b4a.from('line1\nline2\n'))
|
||||
await ctx.vfs.writeFile('boot-b.txt', b4a.from('line1\nline3\n'))
|
||||
const lines = []
|
||||
ctx.console = {
|
||||
log(s) {
|
||||
lines.push(String(s))
|
||||
},
|
||||
error(s) {
|
||||
errs.push(String(s))
|
||||
}
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['kernel-boot-diff', 'boot-a.txt', 'boot-b.txt'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.ok(lines.some((l) => l.startsWith('+ ') && l.includes('line3')))
|
||||
t.ok(lines.some((l) => l.startsWith('- ') && l.includes('line2')))
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('tier-1 diff -u and patch apply stdin hunk', async (t) => {
|
||||
const dir = testCorestoreDir('diffpatch')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('dp'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
await drive.put('/bin/diff', b4a.from(await readBuiltBin('diff')))
|
||||
await drive.put('/bin/patch', b4a.from(await readBuiltBin('patch')))
|
||||
const ctx = testCtx(drive, personal)
|
||||
await ctx.vfs.writeFile('a.txt', b4a.from('alpha\n'))
|
||||
await ctx.vfs.writeFile('b.txt', b4a.from('beta\n'))
|
||||
const out = []
|
||||
ctx.console = {
|
||||
log(s) {
|
||||
out.push(String(s))
|
||||
},
|
||||
error() {}
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['diff', '-u', 'a.txt', 'b.txt'])
|
||||
t.is(ctx.exitCode, 1)
|
||||
t.ok(out.join('\n').includes('+++ b.txt'))
|
||||
await ctx.vfs.writeFile('fix.txt', b4a.from('alpha\n'))
|
||||
ctx.shellStdin = out.join('\n') + '\n'
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['patch', '-p0', 'fix.txt'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(ctx.b4a.toString(await ctx.vfs.readFile('fix.txt')), 'beta\n')
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('bareInitdReadinessSnapshot exposes unit table', async (t) => {
|
||||
const s = bareInitdReadinessSnapshot()
|
||||
t.is(s.schema, 2)
|
||||
t.ok(Array.isArray(s.units))
|
||||
t.ok(s.supervisionTelemetry && s.supervisionTelemetry.schema === 1)
|
||||
})
|
||||
|
||||
test('tier-1 test -r -w -x -s predicates', async (t) => {
|
||||
const dir = testCorestoreDir('test-pred')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('tpred'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
await drive.put('/bin/test', b4a.from(await readBuiltBin('test')))
|
||||
const ctx = testCtx(drive, personal)
|
||||
await ctx.vfs.writeFile('ro.txt', b4a.from('data'))
|
||||
await ctx.vfs.chmod('ro.txt', 0o400)
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['test', '-r', 'ro.txt'])
|
||||
t.is(ctx.exitCode, 0, '-r read-only file')
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['test', '-w', 'ro.txt'])
|
||||
t.is(ctx.exitCode, 1, '-w false when not writable')
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['test', '-s', 'ro.txt'])
|
||||
t.is(ctx.exitCode, 0, '-s nonempty file')
|
||||
await ctx.vfs.writeFile('empty.txt', b4a.from(''))
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['test', '-s', 'empty.txt'])
|
||||
t.is(ctx.exitCode, 1, '-s empty file')
|
||||
await ctx.vfs.writeFile('run.sh', b4a.from('#!'))
|
||||
await ctx.vfs.chmod('run.sh', 0o700)
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['test', '-x', 'run.sh'])
|
||||
t.is(ctx.exitCode, 0, '-x executable')
|
||||
await ctx.vfs.chmod('run.sh', 0o600)
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['test', '-x', 'run.sh'])
|
||||
t.is(ctx.exitCode, 1, '-x false without exec bit')
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('tier-1 find -perm filters by mode', async (t) => {
|
||||
const dir = testCorestoreDir('find-perm')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pfp'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
await drive.put('/bin/find', b4a.from(await readBuiltBin('find')))
|
||||
const lines = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.exitCode = 0
|
||||
ctx.console = {
|
||||
log(s) {
|
||||
lines.push(String(s))
|
||||
},
|
||||
error() {}
|
||||
}
|
||||
await ctx.vfs.writeFile('rw.txt', b4a.from(''))
|
||||
await ctx.vfs.chmod('rw.txt', 0o644)
|
||||
await ctx.vfs.writeFile('x.txt', b4a.from(''))
|
||||
await ctx.vfs.chmod('x.txt', 0o600)
|
||||
await runBinCommand(ctx, ['find', '.', '-perm', '600', '-type', 'f'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.ok(
|
||||
lines.some((p) => String(p).replace(/\\/g, '/').endsWith('/x.txt'))
|
||||
)
|
||||
t.ok(
|
||||
!lines.some((p) => String(p).replace(/\\/g, '/').endsWith('/rw.txt'))
|
||||
)
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('tier-1 find -mtime -1 matches recent files', async (t) => {
|
||||
const dir = testCorestoreDir('find-mtime')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('fmt'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
await drive.put('/bin/find', b4a.from(await readBuiltBin('find')))
|
||||
const lines = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.exitCode = 0
|
||||
ctx.console = {
|
||||
log(s) {
|
||||
lines.push(String(s))
|
||||
},
|
||||
error() {}
|
||||
}
|
||||
await ctx.vfs.writeFile('recent.txt', b4a.from('x'))
|
||||
await runBinCommand(ctx, ['find', '.', '-type', 'f', '-mtime', '-1'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.ok(
|
||||
lines.some((p) => String(p).replace(/\\/g, '/').endsWith('/recent.txt'))
|
||||
)
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('tier-1 find -xdev skips other volume under /', async (t) => {
|
||||
const dir = testCorestoreDir('find-xdev')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('fxd'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
await drive.put('/onlyroot.txt', b4a.from('sys'))
|
||||
await drive.put('/bin/find', b4a.from(await readBuiltBin('find')))
|
||||
const ctx = testCtx(drive, personal, { PWD: '/', HOME: '/home/user' })
|
||||
ctx.exitCode = 0
|
||||
const lines = []
|
||||
ctx.console = {
|
||||
log(s) {
|
||||
lines.push(String(s))
|
||||
},
|
||||
error() {}
|
||||
}
|
||||
await ctx.vfs.writeFile('/home/user/only_in_home.txt', b4a.from('p'))
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['find', '/', '-xdev', '-name', 'only_in_home.txt'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(lines.length, 0, '-xdev from / must not descend into /home')
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['find', '/', '-name', 'only_in_home.txt'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.ok(
|
||||
lines.some((p) =>
|
||||
String(p).replace(/\\/g, '/').endsWith('/only_in_home.txt')
|
||||
),
|
||||
'without -xdev, file under /home is visible from /'
|
||||
)
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['find', '/', '-xdev', '-name', 'onlyroot.txt'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.ok(
|
||||
lines.some((p) =>
|
||||
String(p).replace(/\\/g, '/').endsWith('/onlyroot.txt')
|
||||
),
|
||||
'-xdev still searches system tree under /'
|
||||
)
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('tar-cli hard link extract copies unless BARE_OS_VFS_STRICT_HARDLINK', async (t) => {
|
||||
const dir = testCorestoreDir('tar-hardlink')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('tarhl'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const body = b4a.from('hi', 'utf8')
|
||||
const hdrA = encodeUstarHeader({
|
||||
name: 'a.txt',
|
||||
size: body.length,
|
||||
mode: 0o644,
|
||||
mtime: 1,
|
||||
typeflag: '0'
|
||||
})
|
||||
const padA = new Uint8Array((TAR_BLK - (body.length % TAR_BLK)) % TAR_BLK)
|
||||
const hdrB = encodeUstarHeader({
|
||||
name: 'b.txt',
|
||||
size: 0,
|
||||
mode: 0o644,
|
||||
mtime: 1,
|
||||
typeflag: '1',
|
||||
linkname: 'a.txt'
|
||||
})
|
||||
const archive = concatTarParts([
|
||||
hdrA,
|
||||
body,
|
||||
padA,
|
||||
hdrB,
|
||||
new Uint8Array(TAR_BLK * 2)
|
||||
])
|
||||
|
||||
const errs = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
await ctx.vfs.writeFile('hl.tar', b4a.from(archive))
|
||||
ctx.exitCode = 0
|
||||
ctx.console = {
|
||||
log() {},
|
||||
error(s) {
|
||||
errs.push(String(s))
|
||||
}
|
||||
}
|
||||
await runTarCli(ctx, ['tar', '-xf', 'hl.tar'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(b4a.toString(await ctx.vfs.readFile('b.txt'), 'utf8'), 'hi')
|
||||
|
||||
await ctx.vfs.rm('a.txt', { recursive: true, force: true })
|
||||
await ctx.vfs.rm('b.txt', { recursive: true, force: true })
|
||||
|
||||
errs.length = 0
|
||||
ctx.exitCode = 0
|
||||
await ctx.vfs.writeFile('hl.tar', b4a.from(archive))
|
||||
const ctxStrict = testCtx(drive, personal, {
|
||||
BARE_OS_VFS_STRICT_HARDLINK: '1'
|
||||
})
|
||||
await ctxStrict.vfs.writeFile('hl.tar', b4a.from(archive))
|
||||
ctxStrict.exitCode = 0
|
||||
ctxStrict.console = {
|
||||
log() {},
|
||||
error(s) {
|
||||
errs.push(String(s))
|
||||
}
|
||||
}
|
||||
await runTarCli(ctxStrict, ['tar', '-xf', 'hl.tar'])
|
||||
t.is(ctxStrict.exitCode, 1)
|
||||
t.ok(
|
||||
errs.some((e) => /hard link entries not supported/i.test(e)),
|
||||
'strict mode rejects hard link members'
|
||||
)
|
||||
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('tier-1 jq from system drive', async (t) => {
|
||||
const dir = testCorestoreDir('jq')
|
||||
const store = new Corestore(dir)
|
||||
@@ -3073,7 +3787,9 @@ async function run(ctx, argv) {
|
||||
await drive.put('/bin/printf', b4a.from(await readBuiltBin('printf')))
|
||||
const lines = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.runBinCommand = (argv) => runBinCommand(ctx, argv)
|
||||
ctx.runBinCommand = function (argv, ro) {
|
||||
return runBinCommand(this, argv, ro)
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
ctx.console = {
|
||||
log(s) {
|
||||
@@ -3093,6 +3809,17 @@ async function run(ctx, argv) {
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(lines.pop(), '100000')
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
ctx.bareOsPathconf = (p, n) => {
|
||||
void p
|
||||
if (n === '_PC_NAME_MAX') return 255
|
||||
throw new Error('getconf pathconf test: unknown name')
|
||||
}
|
||||
await runBinCommand(ctx, ['getconf', '_PC_NAME_MAX', '/home/user'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(lines.pop(), '255')
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['getconf', 'NOT_A_REAL_CONF_NAME'])
|
||||
@@ -3119,6 +3846,23 @@ async function run(ctx, argv) {
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(lines.join('\n'), 'x one y\nx two y')
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
ctx.shellStdin = 'a\nb\n'
|
||||
await runBinCommand(ctx, ['xargs', '-P2', '-n1', 'echolog'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(new Set(lines).size, 2)
|
||||
t.ok(lines.includes('a'))
|
||||
t.ok(lines.includes('b'))
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
ctx.vfs.env.BARE_OS_XARGS_MAX_PROCS = '2'
|
||||
ctx.shellStdin = 'x\ny\n'
|
||||
await runBinCommand(ctx, ['xargs', '-P8', '-n1', 'echolog'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.ok(lines.some((s) => String(s).includes('exceeds cap 2')))
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
ctx.shellStdin = ''
|
||||
@@ -3129,6 +3873,50 @@ async function run(ctx, argv) {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('tier-1 sh shebang strip and symlink script path', async (t) => {
|
||||
const dir = testCorestoreDir('shshe')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('psh'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
await drive.put('/bin/sh', b4a.from(await readBuiltBin('sh')))
|
||||
await drive.put(
|
||||
'/bin/echo',
|
||||
b4a.from(`async function run(ctx, argv) {
|
||||
ctx.console.log(argv.slice(1).join(' '))
|
||||
}
|
||||
`)
|
||||
)
|
||||
const lines = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.runBinCommand = function (argv, ro) {
|
||||
return runBinCommand(this, argv, ro)
|
||||
}
|
||||
ctx.execLine = (line, opts) => execShellLine(ctx, line)
|
||||
ctx.exitCode = 0
|
||||
ctx.console = {
|
||||
log(s) {
|
||||
lines.push(String(s))
|
||||
},
|
||||
error(s) {
|
||||
lines.push(String(s))
|
||||
}
|
||||
}
|
||||
await ctx.vfs.chdir('/home/user')
|
||||
await ctx.vfs.writeFile(
|
||||
'real.sh',
|
||||
b4a.from('#!/bin/sh\necho shebang-ok\n')
|
||||
)
|
||||
await ctx.vfs.symlink('real.sh', 'via.sh')
|
||||
await runBinCommand(ctx, ['sh', 'via.sh'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.ok(lines.some((l) => String(l).includes('shebang-ok')))
|
||||
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('tier-2 cat env touch mkdir', async (t) => {
|
||||
const dir = testCorestoreDir('tier2tem')
|
||||
const store = new Corestore(dir)
|
||||
@@ -4122,7 +4910,9 @@ test('coreutils matrix: cp find sort printf uniq realpath sha256sum base64 rm -d
|
||||
)
|
||||
const lines = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.runBinCommand = (argv) => runBinCommand(ctx, argv)
|
||||
ctx.runBinCommand = function (argv, ro) {
|
||||
return runBinCommand(this, argv, ro)
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
ctx.console = {
|
||||
log(s) {
|
||||
@@ -4241,7 +5031,9 @@ test('coreutils gnu-gap batch: paste tac rev md5sum expr tsort numfmt truncate i
|
||||
}
|
||||
const lines = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.runBinCommand = (argv) => runBinCommand(ctx, argv)
|
||||
ctx.runBinCommand = function (argv, ro) {
|
||||
return runBinCommand(this, argv, ro)
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
ctx.console = {
|
||||
log(s) {
|
||||
@@ -4403,6 +5195,25 @@ test('unit journal exposed under /run/bare-os/unit-journal', async (t) => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('worker_budget proc surface schema 2 exposes cpu and class map env', async (t) => {
|
||||
const j = buildBareOsReplicationOperatorSurfaceProcJson('worker_budget', {
|
||||
BARE_OS_BIN_WORKER_WALL_MS_MAX: '5000',
|
||||
BARE_OS_BIN_WORKER_CPU_MS_MAX: '2500',
|
||||
BARE_OS_KERNEL_RUNNER_CLASS_CPU_MS_MAX_JSON: '{"textproc":100}'
|
||||
})
|
||||
t.is(j.schema, 2)
|
||||
t.is(j.wallMsMax, '5000')
|
||||
t.is(j.cpuMsMax, '2500')
|
||||
t.alike(j.kernelRunnerClassCpuMsMax, { textproc: 100 })
|
||||
})
|
||||
|
||||
test('process table snapshot includes processGroups metadata', async (t) => {
|
||||
const s = bareOsProcessTableSnapshot({})
|
||||
t.ok(s.processGroups)
|
||||
t.is(s.processGroups.schema, 1)
|
||||
t.ok(String(s.processGroups.killpgAnalog).includes('killpg'))
|
||||
})
|
||||
|
||||
async function readBuiltBin(name) {
|
||||
const fs = await import('node:fs/promises')
|
||||
const p = path.join(__dirname, '../../kernel/bin', name)
|
||||
|
||||
@@ -26,6 +26,7 @@ var BARE_TOP_SNAPSHOT_PROC_ENTRIES = [
|
||||
['replicationBackpressure', '/proc/bare_os/replication_backpressure.json'],
|
||||
['swarm', '/proc/bare_os/swarm'],
|
||||
['syncWindow', '/proc/bare_os/sync_window.json'],
|
||||
['clock', '/proc/bare_os/clock.json'],
|
||||
['hdmsHealth', '/proc/bare_os/hdms_health.json'],
|
||||
['hdmsHints', '/proc/bare_os/hdms_hints.json'],
|
||||
['dhtStatus', '/proc/bare_os/dht_status.json'],
|
||||
|
||||
@@ -26,6 +26,7 @@ export const COREUTILS_COMMANDS = [
|
||||
'date',
|
||||
'dd',
|
||||
'df',
|
||||
'diff',
|
||||
'dir',
|
||||
'dirname',
|
||||
'dircolors',
|
||||
@@ -42,6 +43,7 @@ export const COREUTILS_COMMANDS = [
|
||||
'fmt',
|
||||
'fold',
|
||||
'getconf',
|
||||
'getfacl',
|
||||
'git-pear',
|
||||
'grep',
|
||||
'groups',
|
||||
@@ -86,6 +88,7 @@ export const COREUTILS_COMMANDS = [
|
||||
'openssl',
|
||||
'oidc-publish',
|
||||
'paste',
|
||||
'patch',
|
||||
'pathchk',
|
||||
'pr',
|
||||
'printenv',
|
||||
@@ -101,9 +104,11 @@ export const COREUTILS_COMMANDS = [
|
||||
'savevault',
|
||||
'sed',
|
||||
'seq',
|
||||
'setfacl',
|
||||
'sha1sum',
|
||||
'sha256sum',
|
||||
'sha512sum',
|
||||
'sh',
|
||||
'shuf',
|
||||
'sleep',
|
||||
'sort',
|
||||
@@ -140,6 +145,7 @@ export const COREUTILS_COMMANDS = [
|
||||
'which',
|
||||
'who',
|
||||
'whoami',
|
||||
'xattr',
|
||||
'xargs',
|
||||
'yes'
|
||||
]
|
||||
|
||||
@@ -5,8 +5,22 @@
|
||||
"synopsis": [
|
||||
"crontab [OPTION]... [OPERAND]..."
|
||||
],
|
||||
"description": "Bare OS implementation of user crontab manipulation. Full behavior is defined in packages/bare-os-coreutils/src/crontab.js.",
|
||||
"options": [],
|
||||
"description": "Bare OS implementation of user crontab manipulation. Full behavior is defined in packages/bare-os-coreutils/src/crontab.js. Timer backends honor BARE_OS_TIMER_EVERY_MS_MONOTONIC for monotonic chained schedules in bare-cron.",
|
||||
"options": [
|
||||
{
|
||||
"flag": "-l",
|
||||
"meaning": "List the current crontab on stdout"
|
||||
},
|
||||
{
|
||||
"flag": "-r",
|
||||
"meaning": "Remove all jobs for the user"
|
||||
},
|
||||
{
|
||||
"flag": "-e",
|
||||
"meaning": "Edit via ctx (stub when no editor bridge)"
|
||||
}
|
||||
],
|
||||
"bareOsNotes": "See packages/bare-os-booter/lib/bare-cron.js and BARE_OS_TIMER_EVERY_MS_MONOTONIC in env reference for POSIX-adjacent timer behavior.",
|
||||
"keywords": [
|
||||
"crontab",
|
||||
"bare-os",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "diff",
|
||||
"section": 1,
|
||||
"title": "compare two files",
|
||||
"synopsis": ["diff [-q] [-s] [-u] FILE1 FILE2"],
|
||||
"description": "Compares two text files on the VFS. **`-q`** prints nothing but sets exit status. **`-s`** reports when files are identical. **`-u`** emits a single unified hunk suitable for **`patch(1)`** (Bare OS subset).",
|
||||
"options": [
|
||||
{ "flag": "-q", "meaning": "Quiet; exit status only" },
|
||||
{ "flag": "-s", "meaning": "Report when files are the same" },
|
||||
{ "flag": "-u", "meaning": "Unified diff (one hunk)" }
|
||||
],
|
||||
"keywords": ["diff", "compare", "bare-os", "coreutils"],
|
||||
"bareOsNotes": "Not full POSIX diff; binary files treated as UTF-8 text. See **`/etc/bare-os/posix_utilities.json`**.",
|
||||
"examples": [
|
||||
{ "caption": "unified diff for patching", "code": "diff -u a.txt b.txt" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "getfacl",
|
||||
"section": 1,
|
||||
"title": "display synthetic access control lists",
|
||||
"synopsis": ["getfacl [-c] FILE"],
|
||||
"description": "Prints ACL text from FILE.bare_acl when present; otherwise synthesizes user::, group::, and other:: triples from the file mode bits.",
|
||||
"options": [
|
||||
{
|
||||
"flag": "-c",
|
||||
"meaning": "Omit comment header lines"
|
||||
}
|
||||
],
|
||||
"keywords": ["getfacl", "acl", "bare-os", "coreutils"],
|
||||
"bareOsNotes": "Not Linux NFS ACLs; sidecar contract matches getconf BARE_OS_ACL_SIDECAR_SUFFIX.",
|
||||
"examples": [
|
||||
{
|
||||
"caption": "show ACL or synthesized mode triples",
|
||||
"code": "getfacl /tmp/a"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "patch",
|
||||
"section": 1,
|
||||
"title": "apply a diff file",
|
||||
"synopsis": ["patch [-pNUM] [--dry-run] [FILE]"],
|
||||
"description": "Reads a unified diff from **`stdin`** (`ctx.shellStdin`) and applies it to **`FILE`** or the path from the **`+++`** header. Supports **`-p`** path strip count and **`--dry-run`** (verify hunk without writing).",
|
||||
"options": [
|
||||
{ "flag": "-p NUM", "meaning": "Strip NUM leading path segments from paths in the patch" },
|
||||
{ "flag": "--dry-run, --check", "meaning": "Check patch without modifying files" }
|
||||
],
|
||||
"keywords": ["patch", "diff", "bare-os", "coreutils"],
|
||||
"bareOsNotes": "Minimal hunk parser; only patches matching the whole-file content implied by the hunk. See **`src/patch.js`**.",
|
||||
"examples": [
|
||||
{ "caption": "apply from shell stdin redirection", "code": "patch -p0 < fix.patch" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "setfacl",
|
||||
"section": 1,
|
||||
"title": "set synthetic access control lists",
|
||||
"synopsis": ["setfacl [-b] PATH"],
|
||||
"description": "Writes ACL lines from stdin to PATH.bare_acl, or removes the sidecar with -b.",
|
||||
"options": [
|
||||
{
|
||||
"flag": "-b",
|
||||
"meaning": "Remove ACL sidecar if it exists"
|
||||
}
|
||||
],
|
||||
"keywords": ["setfacl", "acl", "bare-os", "coreutils"],
|
||||
"bareOsNotes": "Requires non-empty ctx.shellStdin when not using -b (same pattern as patch).",
|
||||
"examples": [
|
||||
{
|
||||
"caption": "install ACL text from a here-doc in the shell",
|
||||
"code": "printf 'user::rw-\\n' | setfacl /tmp/a"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "sh",
|
||||
"section": 1,
|
||||
"title": "run a shell script (minimal)",
|
||||
"synopsis": ["sh SCRIPT"],
|
||||
"description": "Reads SCRIPT from the VFS, strips one leading `#!` line, then runs each non-empty, non-comment line via **`ctx.execLine`** (same pipeline and builtins as the interactive shell). Not a full POSIX `sh`; no `-c`, here-documents, or functions.",
|
||||
"options": [],
|
||||
"keywords": ["sh", "shebang", "bare-os", "coreutils"],
|
||||
"bareOsNotes": "Requires **`ctx.execLine`**. POSIX coverage index: **`/etc/bare-os/posix_utilities.json`** (utilities.sh). Symlink targets are followed when opening SCRIPT.",
|
||||
"examples": [
|
||||
{
|
||||
"caption": "run a small script from $PWD",
|
||||
"code": "sh ./setup.sh"
|
||||
},
|
||||
{
|
||||
"caption": "shebang line is ignored for execution",
|
||||
"code": "#!/bin/sh\necho hello"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"synopsis": [
|
||||
"tar [OPTION]... [OPERAND]..."
|
||||
],
|
||||
"description": "Bare OS implementation of tar. Full behavior is defined in packages/bare-os-coreutils/src/tar.js.",
|
||||
"description": "Bare OS implementation of tar. Full behavior is defined in packages/bare-os-coreutils/src/tar.js. Extended attributes for unpacked files may be mirrored manually using sibling PATH.bare_xattr.json sidecars (see xattr(1)).",
|
||||
"options": [],
|
||||
"keywords": [
|
||||
"tar",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"synopsis": [
|
||||
"xargs [OPTION]... [OPERAND]..."
|
||||
],
|
||||
"description": "Reads stdin into argument batches and runs **`ctx.runBinCommand`** (same as the shell). Enforces stdin size, token count, batch size, and invocation limits for safety.",
|
||||
"description": "Reads stdin into argument batches and runs **`ctx.runBinCommand`** (same as the shell). Enforces stdin size, token count, batch size, and invocation limits for safety. Parallel **`-P`** runs disjoint batches concurrently using shallow ctx clones so **`exitCode`** does not race.",
|
||||
"options": [
|
||||
{
|
||||
"flag": "-0, --null",
|
||||
@@ -14,6 +14,14 @@
|
||||
{
|
||||
"flag": "-n, --max-args",
|
||||
"meaning": "Up to N arguments per utility invocation (capped at 128)"
|
||||
},
|
||||
{
|
||||
"flag": "-I repl",
|
||||
"meaning": "Replace repl in the utility argv; implies **`-n 1`** unless **`-n`** was set"
|
||||
},
|
||||
{
|
||||
"flag": "-P, --max-procs",
|
||||
"meaning": "Run up to N batches in parallel (capped by **`BARE_OS_XARGS_MAX_PROCS`**, max 32; default 8)"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
@@ -21,7 +29,7 @@
|
||||
"bare-os",
|
||||
"coreutils"
|
||||
],
|
||||
"bareOsNotes": "No host process spawn; not full POSIX xargs (no -I, -P, etc.). See src/xargs.js for limits.",
|
||||
"bareOsNotes": "No host process spawn; not full POSIX xargs. See **`src/xargs.js`** for limits. POSIX coverage: **`/etc/bare-os/posix_utilities.json`** (utilities.xargs).",
|
||||
"examples": [
|
||||
{
|
||||
"caption": "pass lines as arguments",
|
||||
@@ -31,6 +39,10 @@
|
||||
"caption": "one argument per run",
|
||||
"code": "printf 'a\\nb\\n' | xargs -n1 echo"
|
||||
},
|
||||
{
|
||||
"caption": "two parallel batches (when safe for the utility)",
|
||||
"code": "printf 'a\\nb\\n' | xargs -P2 -n1 echo"
|
||||
},
|
||||
{
|
||||
"caption": "workaround for complex scripts",
|
||||
"code": "# for f in *.txt; do grep -l foo $f; done"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "xattr",
|
||||
"section": 1,
|
||||
"title": "extended attributes via JSON sidecar",
|
||||
"synopsis": ["xattr [-l] [-w NAME VALUE] [-d NAME] PATH"],
|
||||
"description": "Manipulates PATH.bare_xattr.json, a JSON object of extended attribute names to base64-encoded UTF-8 values.",
|
||||
"options": [
|
||||
{
|
||||
"flag": "-l",
|
||||
"meaning": "List names and decoded values"
|
||||
},
|
||||
{
|
||||
"flag": "-w NAME VALUE",
|
||||
"meaning": "Set attribute"
|
||||
},
|
||||
{
|
||||
"flag": "-d NAME",
|
||||
"meaning": "Delete attribute"
|
||||
}
|
||||
],
|
||||
"keywords": ["xattr", "bare-os", "coreutils"],
|
||||
"bareOsNotes": "Sidecar suffix matches getconf BARE_OS_XATTR_SIDECAR_SUFFIX.",
|
||||
"examples": [
|
||||
{
|
||||
"caption": "set and list",
|
||||
"code": "xattr -w user.tag hello /tmp/a && xattr -l /tmp/a"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,6 +6,6 @@
|
||||
"description": "Build JS /bin utilities for bare-operating-system (concat + stage to kernel/)",
|
||||
"scripts": {
|
||||
"build": "node ./scripts/ensure-man-pages.mjs && node ./build.mjs",
|
||||
"test": "node ./test/help-bin-list.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/posix-test-int-compare.test.mjs"
|
||||
"test": "node ./test/help-bin-list.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
async function run(ctx, argv) {
|
||||
const silent = argv.includes('-s') || argv.includes('--silent')
|
||||
const args = argv.filter((a) => !a.startsWith('-'))
|
||||
const a = args[0]
|
||||
const b = args[1]
|
||||
const rest = argv.slice(1)
|
||||
const silent = rest.includes('-s') || rest.includes('--silent')
|
||||
const paths = rest.filter((a) => !a.startsWith('-'))
|
||||
const a = paths[0]
|
||||
const b = paths[1]
|
||||
if (!a || !b) {
|
||||
ctx.console.error('usage: cmp [-s] file1 file2')
|
||||
ctx.exitCode = 2
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Minimal diff(1) for Bare OS: compares two text files (VFS paths).
|
||||
* Supports -q (quiet, exit status only), -s (report when identical), -u (single-hunk unified).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} a
|
||||
* @param {string} b
|
||||
*/
|
||||
function splitLines(a) {
|
||||
const s = a.replace(/\r\n/g, '\n')
|
||||
const parts = s.split('\n')
|
||||
if (parts.length && parts[parts.length - 1] === '') parts.pop()
|
||||
return parts
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} pathA
|
||||
* @param {string} pathB
|
||||
* @param {string} sa
|
||||
* @param {string} sb
|
||||
* @returns {string | null} unified hunk or null if identical
|
||||
*/
|
||||
function unifiedOneHunk(pathA, pathB, sa, sb) {
|
||||
const la = splitLines(sa)
|
||||
const lb = splitLines(sb)
|
||||
let i = 0
|
||||
const n = Math.min(la.length, lb.length)
|
||||
while (i < n && la[i] === lb[i]) i++
|
||||
if (i === la.length && i === lb.length) return null
|
||||
const oldLine = la[i] != null ? la[i] : ''
|
||||
const newLine = lb[i] != null ? lb[i] : ''
|
||||
return (
|
||||
`--- ${pathA}\n+++ ${pathB}\n@@ -${i + 1},1 +${i + 1},1 @@\n-${oldLine}\n+${newLine}\n`
|
||||
)
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
let quiet = false
|
||||
let sameReport = false
|
||||
let unified = false
|
||||
/** @type {string[]} */
|
||||
const files = []
|
||||
for (let i = 1; i < argv.length; i++) {
|
||||
const a = argv[i]
|
||||
if (a === '--') {
|
||||
files.push(...argv.slice(i + 1))
|
||||
break
|
||||
}
|
||||
if (a === '-q') {
|
||||
quiet = true
|
||||
continue
|
||||
}
|
||||
if (a === '-s') {
|
||||
sameReport = true
|
||||
continue
|
||||
}
|
||||
if (a === '-u' || a === '-U0' || a === '--unified') {
|
||||
unified = true
|
||||
continue
|
||||
}
|
||||
if (a.startsWith('-')) {
|
||||
ctx.console.error('diff: unsupported option ' + a)
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
files.push(a)
|
||||
}
|
||||
if (files.length !== 2) {
|
||||
ctx.console.error('usage: diff [-q] [-s] [-u] FILE1 FILE2')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
const [f1, f2] = files
|
||||
let b1
|
||||
let b2
|
||||
try {
|
||||
b1 = await ctx.vfs.readFile(f1)
|
||||
b2 = await ctx.vfs.readFile(f2)
|
||||
} catch (e) {
|
||||
ctx.console.error('diff: ' + ((e && e.message) || String(e)))
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
const s1 = b1 ? ctx.b4a.toString(b1) : ''
|
||||
const s2 = b2 ? ctx.b4a.toString(b2) : ''
|
||||
if (s1 === s2) {
|
||||
if (sameReport) ctx.console.log(`Files ${f1} and ${f2} are identical`)
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
if (quiet) {
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
if (unified) {
|
||||
const u = unifiedOneHunk(f1, f2, s1, s2)
|
||||
if (u) ctx.console.log(u.replace(/\n$/, ''))
|
||||
} else {
|
||||
const la = splitLines(s1)
|
||||
const lb = splitLines(s2)
|
||||
let i = 0
|
||||
const n = Math.min(la.length, lb.length)
|
||||
while (i < n && la[i] === lb[i]) i++
|
||||
ctx.console.log(`${i + 1}c${i + 1}`)
|
||||
ctx.console.log(`< ${la[i] != null ? la[i] : ''}`)
|
||||
ctx.console.log('---')
|
||||
ctx.console.log(`> ${lb[i] != null ? lb[i] : ''}`)
|
||||
}
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
@@ -40,6 +40,57 @@ function findMatchMtime(st, spec) {
|
||||
return days >= spec.n && days < spec.n + 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Device id sketch for `find -xdev` (do not cross `/mnt/<label>` boundaries vs system/personal).
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} absLogical
|
||||
*/
|
||||
function findRouteDevId(ctx, absLogical) {
|
||||
const a = String(absLogical).replace(/\/+$/, '') || '/'
|
||||
if (a.startsWith('/mnt/')) {
|
||||
const label = a.slice(5).split('/').filter(Boolean)[0]
|
||||
return label ? `mnt:${label}` : 'mnt:'
|
||||
}
|
||||
if (a === '/mnt') return 'mnt-root'
|
||||
const vfs = ctx.vfs
|
||||
if (!vfs || typeof vfs.route !== 'function') return 'unknown'
|
||||
const r = vfs.route(absLogical)
|
||||
if (r.virtualPseudo) return 'pseudo'
|
||||
if (r.virtualHomeDir) return 'personal-home'
|
||||
if (r.virtualVarRoot) return 'personal-var'
|
||||
if (a.startsWith('/tmp')) return 'personal'
|
||||
if (a.startsWith('/home/')) return 'personal'
|
||||
if (a.startsWith('/var/')) return 'personal'
|
||||
return 'system'
|
||||
}
|
||||
|
||||
/** @param {string} s */
|
||||
function parsePermSpec(s) {
|
||||
const t = String(s).trim()
|
||||
if (t.startsWith('/')) return null
|
||||
let body = t
|
||||
let allBitsSet = false
|
||||
if (t.startsWith('-')) {
|
||||
allBitsSet = true
|
||||
body = t.slice(1)
|
||||
}
|
||||
if (!/^[0-7]{1,4}$/.test(body)) return null
|
||||
const mask = Number.parseInt(body, 8) & 0o7777
|
||||
return { allBitsSet, mask }
|
||||
}
|
||||
|
||||
/** @param {Record<string, unknown>} st @param {{ allBitsSet: boolean, mask: number }} spec */
|
||||
function findMatchPerm(st, spec) {
|
||||
const mode =
|
||||
typeof st.mode === 'number'
|
||||
? st.mode & 0o7777
|
||||
: typeof st.mode === 'string'
|
||||
? Number.parseInt(String(st.mode), 8) & 0o7777
|
||||
: 0
|
||||
if (spec.allBitsSet) return (mode & spec.mask) === spec.mask
|
||||
return mode === spec.mask
|
||||
}
|
||||
|
||||
async function findIsEmpty(ctx, path, st) {
|
||||
if (st.type === 'file' || st.type === 'symlink') return (st.size || 0) === 0
|
||||
if (st.type !== 'directory') return false
|
||||
@@ -77,6 +128,7 @@ async function walk(ctx, dir, o, curDepth) {
|
||||
const nameOk = !o.nameRe || o.nameRe.test(n)
|
||||
let match = pathOk && nameOk && (!o.wantType || st.type === o.wantType)
|
||||
if (match && o.mtimeSpec) match = match && findMatchMtime(st, o.mtimeSpec)
|
||||
if (match && o.permSpec) match = match && findMatchPerm(st, o.permSpec)
|
||||
if (match && o.newerThanMs != null)
|
||||
match = match && st.mtimeMs > o.newerThanMs
|
||||
if (match && o.wantEmpty) {
|
||||
@@ -141,6 +193,10 @@ async function walk(ctx, dir, o, curDepth) {
|
||||
}
|
||||
}
|
||||
if (st.type !== 'directory') continue
|
||||
if (o.xdevRootDev != null) {
|
||||
const subDev = findRouteDevId(ctx, path)
|
||||
if (subDev !== o.xdevRootDev) continue
|
||||
}
|
||||
const absPath = ctx.vfs.resolveLogical(path)
|
||||
if (o.pruneAbs && absPath === o.pruneAbs) continue
|
||||
await walk(ctx, path, o, curDepth + 1)
|
||||
@@ -160,6 +216,9 @@ async function run(ctx, argv) {
|
||||
let print0 = false
|
||||
/** @type {{ op: string, n: number } | null} */
|
||||
let mtimeSpec = null
|
||||
/** @type {{ allBitsSet: boolean, mask: number } | null} */
|
||||
let permSpec = null
|
||||
let xdev = false
|
||||
/** @type {string | null} */
|
||||
let newerPath = null
|
||||
/** @type {string | null} */
|
||||
@@ -217,6 +276,19 @@ async function run(ctx, argv) {
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (a === '-perm' && argv[i + 1]) {
|
||||
permSpec = parsePermSpec(argv[++i])
|
||||
if (!permSpec) {
|
||||
ctx.console.error('find: invalid -perm')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (a === '-xdev') {
|
||||
xdev = true
|
||||
continue
|
||||
}
|
||||
if (a === '-newer' && argv[i + 1]) {
|
||||
newerPath = argv[++i]
|
||||
continue
|
||||
@@ -336,6 +408,7 @@ async function run(ctx, argv) {
|
||||
if (Number.isFinite(n)) execMax = Math.min(4096, Math.max(1, n))
|
||||
}
|
||||
const abs = ctx.vfs.resolveLogical(root)
|
||||
const xdevRootDev = xdev ? findRouteDevId(ctx, abs) : null
|
||||
const o = {
|
||||
maxDepth,
|
||||
minDepth,
|
||||
@@ -344,6 +417,8 @@ async function run(ctx, argv) {
|
||||
wantType,
|
||||
print0,
|
||||
mtimeSpec,
|
||||
permSpec,
|
||||
xdevRootDev,
|
||||
newerThanMs,
|
||||
pruneAbs,
|
||||
wantEmpty,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Subset of POSIX.1-2017 getconf — fixed values for Bare OS (no host sysconf).
|
||||
* Unknown names exit with status 1 (matches common getconf for invalid var).
|
||||
* Live pathconf: **`getconf NAME /absolute/path`** delegates to **`ctx.bareOsPathconf`** when set.
|
||||
*/
|
||||
|
||||
const CONF = {
|
||||
@@ -41,20 +42,31 @@ const CONF = {
|
||||
_PC_NAME_MAX: '255',
|
||||
/** Comma-separated `ctx.bareOsSyscall` op names implemented in stock booter. */
|
||||
BARE_OS_SYSCALL_OPS:
|
||||
'readFile,writeFile,readdir,mkdir,stat,unlink,chmod,chdir,getcwd,readlink,symlink,exists,lstat,rmdir,mount,umount,kill,rename,link,access,utimes,truncate,ftruncate,pathconf',
|
||||
'readFile,writeFile,readdir,mkdir,stat,unlink,chmod,chdir,getcwd,readlink,symlink,exists,lstat,rmdir,mount,umount,kill,rename,link,access,utimes,truncate,ftruncate,fsync,fdatasync,pathconf',
|
||||
/** Encodings accepted by `/bin/iconv` (subset; case-insensitive names). */
|
||||
BARE_OS_ICONV_ENCODINGS: 'UTF-8,ISO-8859-1,UTF-16LE,UTF-16BE',
|
||||
/** Synthetic process table JSON path (logical VFS). */
|
||||
BARE_OS_PROC_PROCESS_TABLE: '/proc/bare_os/process_table.json'
|
||||
BARE_OS_PROC_PROCESS_TABLE: '/proc/bare_os/process_table.json',
|
||||
/** Sidecar suffix for synthetic POSIX ACL text (`getfacl` / `setfacl`). */
|
||||
BARE_OS_ACL_SIDECAR_SUFFIX: '.bare_acl',
|
||||
/** Sidecar suffix for extended-attribute JSON (`xattr`). */
|
||||
BARE_OS_XATTR_SIDECAR_SUFFIX: '.bare_xattr.json',
|
||||
/** Incremental kernel.ext.d reload after boot (`ctx.bareOsReloadKernelExtDropinsSafe`); 0/1 hint only. */
|
||||
BARE_OS_KERNEL_EXT_D_HOT_RELOAD: '0',
|
||||
/** Operator hint for hyperblob-style dedup in host pipelines; guest VFS does not enable automatically. */
|
||||
BARE_OS_VFS_HYPERBLOBS_DEDUP: '0',
|
||||
/** This binary: fixed catalog. Use `getconf NAME /path` + `ctx.bareOsPathconf` for live pathconf. */
|
||||
BARE_OS_GETCONF_SOURCE: 'static_catalog'
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
const args = argv.slice(1).filter((a) => a !== '--')
|
||||
let dumpAll = false
|
||||
let name = null
|
||||
/** @type {string[]} */
|
||||
const positional = []
|
||||
for (const a of args) {
|
||||
if (a === '-a') dumpAll = true
|
||||
else if (!a.startsWith('-')) name = a
|
||||
else if (!a.startsWith('-')) positional.push(a)
|
||||
else {
|
||||
ctx.console.error('getconf: unknown option: ' + a)
|
||||
ctx.exitCode = 1
|
||||
@@ -70,12 +82,34 @@ async function run(ctx, argv) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
ctx.console.error('usage: getconf [-a] system_var')
|
||||
if (positional.length === 2) {
|
||||
const varName = positional[0]
|
||||
const pathSpec = positional[1]
|
||||
if (
|
||||
typeof ctx.bareOsPathconf === 'function' &&
|
||||
pathSpec.startsWith('/')
|
||||
) {
|
||||
try {
|
||||
const v = ctx.bareOsPathconf(pathSpec, varName)
|
||||
ctx.console.log(String(v))
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
} catch (e) {
|
||||
ctx.console.error('getconf: ' + (e?.message || String(e)))
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (positional.length !== 1) {
|
||||
ctx.console.error('usage: getconf [-a] system_var [path_for_pathconf]')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const name = positional[0]
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(CONF, name)) {
|
||||
ctx.console.log(CONF[name])
|
||||
ctx.exitCode = 0
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* getfacl — show POSIX-style ACL text for a VFS path.
|
||||
* When PATH.bare_acl exists, prints that file; otherwise synthesizes user/group/other
|
||||
* triples from the path mode bits (see setfacl / handbook).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {number} mode
|
||||
* @param {number} shift
|
||||
*/
|
||||
function tripleFromMode(mode, shift) {
|
||||
const b = (mode >> shift) & 7
|
||||
const r = b & 4 ? 'r' : '-'
|
||||
const w = b & 2 ? 'w' : '-'
|
||||
const x = b & 1 ? 'x' : '-'
|
||||
return r + w + x
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
let compact = false
|
||||
/** @type {string[]} */
|
||||
const files = []
|
||||
for (let i = 1; i < argv.length; i++) {
|
||||
const a = argv[i]
|
||||
if (a === '-c' || a === '--compact') {
|
||||
compact = true
|
||||
continue
|
||||
}
|
||||
if (a === '--help' || a === '-h') {
|
||||
ctx.console.log('usage: getfacl [-c] FILE')
|
||||
return
|
||||
}
|
||||
if (a.startsWith('-')) {
|
||||
ctx.console.error('getfacl: unsupported option ' + a)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
files.push(a)
|
||||
}
|
||||
if (files.length !== 1) {
|
||||
ctx.console.error('usage: getfacl [-c] FILE')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const path = files[0]
|
||||
const side = path + '.bare_acl'
|
||||
try {
|
||||
const st = await ctx.vfs.stat(path)
|
||||
if (!st) {
|
||||
ctx.console.error('getfacl: ' + path + ': No such file')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const raw = await ctx.vfs.readFile(side)
|
||||
if (raw && raw.byteLength) {
|
||||
const text = ctx.b4a.toString(raw)
|
||||
ctx.console.log(text.replace(/\n$/, ''))
|
||||
return
|
||||
}
|
||||
const mode = (st.mode || 0) & 0o777
|
||||
const u = tripleFromMode(mode, 6)
|
||||
const g = tripleFromMode(mode, 3)
|
||||
const o = tripleFromMode(mode, 0)
|
||||
const lines = compact
|
||||
? ['user::' + u, 'group::' + g, 'other::' + o]
|
||||
: [
|
||||
'# file: ' + path,
|
||||
'# owner: synthetic',
|
||||
'# group: synthetic',
|
||||
'user::' + u,
|
||||
'group::' + g,
|
||||
'other::' + o
|
||||
]
|
||||
ctx.console.log(lines.join('\n'))
|
||||
} catch (e) {
|
||||
ctx.console.error(
|
||||
'getfacl: ' + path + ': ' + (e && e.message ? e.message : String(e))
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ async function readUtf8(ctx, path) {
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
const a = argv[2]
|
||||
const b = argv[3]
|
||||
const a = argv[1]
|
||||
const b = argv[2]
|
||||
if (!a || !b) {
|
||||
ctx.console.error('Usage: kernel-boot-diff FILE1 FILE2')
|
||||
ctx.console.error('Compares NDJSON or line-oriented boot checkpoint dumps.')
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/**
|
||||
* Move/rename via copy + delete. Hyperdrive has no single-key rename across paths, so
|
||||
* directory trees and cross-location moves are duplicated then removed. A single regular
|
||||
* file to a new non-directory path uses read + write + unlink when detected below.
|
||||
*
|
||||
* Documented limitations: cross-volume moves always copy+delete; EXDEV-style behavior is
|
||||
* implicit. Busy targets, partial copy failures, and union read-only trees surface as
|
||||
* generic errors from the VFS. Prefer same-directory renames for smallest blast radius.
|
||||
*/
|
||||
async function mvCopyPath(ctx, from, to, recursive, followSymlink) {
|
||||
const st = await ctx.vfs.lstat(from)
|
||||
if (!st) return false
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Minimal patch(1): applies a single unified diff from stdin (ctx.shellStdin).
|
||||
* Supports -pNUM strip, --dry-run. Intended for diffs emitted by Bare OS diff -u.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {number} p
|
||||
*/
|
||||
function stripPath(path, p) {
|
||||
const segs = path.replace(/\\/g, '/').split('/').filter(Boolean)
|
||||
const rest = segs.slice(Math.min(p, segs.length))
|
||||
return rest.length ? rest.join('/') : segs[segs.length - 1] || path
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
let p = 0
|
||||
let dry = false
|
||||
/** @type {string | null} */
|
||||
let overrideFile = null
|
||||
for (let i = 1; i < argv.length; i++) {
|
||||
const a = argv[i]
|
||||
if (a === '--dry-run' || a === '--check') {
|
||||
dry = true
|
||||
continue
|
||||
}
|
||||
if (a === '-p' || a === '--strip') {
|
||||
const n = argv[i + 1]
|
||||
if (n == null || !/^\d+$/.test(n)) {
|
||||
ctx.console.error('patch: -p requires a number')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
p = Number(n)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (a.startsWith('-p') && a.length > 2 && /^\d+$/.test(a.slice(2))) {
|
||||
p = Number(a.slice(2))
|
||||
continue
|
||||
}
|
||||
if (a.startsWith('-')) {
|
||||
ctx.console.error('patch: unsupported option ' + a)
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
overrideFile = a
|
||||
break
|
||||
}
|
||||
|
||||
const raw = bareStdin(ctx) || ''
|
||||
const lines = raw.replace(/\r\n/g, '\n').split('\n')
|
||||
let minus = ''
|
||||
let plus = ''
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const ln = lines[i]
|
||||
if (ln.startsWith('--- ')) {
|
||||
minus = ln.slice(4).trim().split(/\s+/)[0]
|
||||
continue
|
||||
}
|
||||
if (ln.startsWith('+++ ')) {
|
||||
plus = ln.slice(4).trim().split(/\s+/)[0]
|
||||
continue
|
||||
}
|
||||
}
|
||||
const targetRaw = overrideFile || plus || minus
|
||||
if (!targetRaw) {
|
||||
ctx.console.error('patch: could not determine path from patch')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
const target = stripPath(targetRaw.replace(/^b\//, ''), p)
|
||||
|
||||
let hunkStart = lines.findIndex((l) => /^@@/.test(l))
|
||||
if (hunkStart < 0) {
|
||||
ctx.console.error('patch: missing @@ hunk')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const oldParts = []
|
||||
/** @type {string[]} */
|
||||
const newParts = []
|
||||
for (let i = hunkStart + 1; i < lines.length; i++) {
|
||||
const l = lines[i]
|
||||
if (l.startsWith('@@')) break
|
||||
if (l.startsWith('-')) oldParts.push(l.slice(1))
|
||||
else if (l.startsWith('+')) newParts.push(l.slice(1))
|
||||
else if (l.startsWith(' ')) {
|
||||
const body = l.slice(1)
|
||||
oldParts.push(body)
|
||||
newParts.push(body)
|
||||
}
|
||||
}
|
||||
|
||||
let cur = ''
|
||||
try {
|
||||
const buf = await ctx.vfs.readFile(target)
|
||||
cur = buf ? ctx.b4a.toString(buf) : ''
|
||||
} catch (e) {
|
||||
ctx.console.error('patch: ' + ((e && e.message) || String(e)))
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
|
||||
const cl = splitLinesKeep(cur)
|
||||
const expectOld = oldParts.join('\n')
|
||||
const gotOld = cl.join('\n')
|
||||
if (expectOld !== gotOld) {
|
||||
ctx.console.error('patch: file content does not match hunk (try correct -p)')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const out = newParts.join('\n') + (newParts.length ? '\n' : '')
|
||||
if (!dry) await ctx.vfs.writeFile(target, ctx.b4a.from(out))
|
||||
ctx.exitCode = 0
|
||||
}
|
||||
|
||||
/** @param {string} s */
|
||||
function splitLinesKeep(s) {
|
||||
const t = s.replace(/\r\n/g, '\n')
|
||||
const parts = t.split('\n')
|
||||
if (parts.length && parts[parts.length - 1] === '') parts.pop()
|
||||
return parts
|
||||
}
|
||||
@@ -1,3 +1,27 @@
|
||||
/** Unescape \\n \\t \\r \\\\ and octal \\ddd inside FORMAT (POSIX-style subset). */
|
||||
function barePrintfUnescapeFormat(fmt) {
|
||||
let o = ''
|
||||
for (let i = 0; i < fmt.length; i++) {
|
||||
if (fmt[i] !== '\\') {
|
||||
o += fmt[i]
|
||||
continue
|
||||
}
|
||||
const c = fmt[++i]
|
||||
if (c === undefined) break
|
||||
if (c === 'n') o += '\n'
|
||||
else if (c === 't') o += '\t'
|
||||
else if (c === 'r') o += '\r'
|
||||
else if (c === '\\') o += '\\'
|
||||
else if (c >= '0' && c <= '7') {
|
||||
let oct = c
|
||||
while (i + 1 < fmt.length && /[0-7]/.test(fmt[i + 1]) && oct.length < 3)
|
||||
oct += fmt[++i]
|
||||
o += String.fromCharCode(Number.parseInt(oct, 8) & 0xff)
|
||||
} else o += c
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
/** @param {string} s */
|
||||
function barePrintfBackslashArg(s) {
|
||||
let o = ''
|
||||
@@ -66,10 +90,6 @@ async function run(ctx, argv) {
|
||||
}
|
||||
const fmt = argv[1]
|
||||
const args = argv.slice(2)
|
||||
let unescaped = fmt
|
||||
unescaped = unescaped.replace(/\\n/g, '\n')
|
||||
unescaped = unescaped.replace(/\\t/g, '\t')
|
||||
unescaped = unescaped.replace(/\\r/g, '\r')
|
||||
unescaped = unescaped.replace(/\\\\/g, '\\')
|
||||
const unescaped = barePrintfUnescapeFormat(fmt)
|
||||
ctx.console.log(barePrintfFormat(unescaped, args))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* setfacl — store or clear synthetic ACL sidecar text (PATH.bare_acl).
|
||||
* Without -b, reads ACL lines from ctx.shellStdin (same pattern as patch).
|
||||
*/
|
||||
|
||||
async function run(ctx, argv) {
|
||||
let clear = false
|
||||
/** @type {string[]} */
|
||||
const files = []
|
||||
for (let i = 1; i < argv.length; i++) {
|
||||
const a = argv[i]
|
||||
if (a === '-b' || a === '--remove-all') {
|
||||
clear = true
|
||||
continue
|
||||
}
|
||||
if (a === '--help' || a === '-h') {
|
||||
ctx.console.log(
|
||||
'usage: setfacl [-b] PATH\nWrites ACL text from stdin when not clearing.'
|
||||
)
|
||||
return
|
||||
}
|
||||
if (a.startsWith('-')) {
|
||||
ctx.console.error('setfacl: unsupported option ' + a)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
files.push(a)
|
||||
}
|
||||
if (files.length !== 1) {
|
||||
ctx.console.error('usage: setfacl [-b] PATH')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const path = files[0]
|
||||
const side = path + '.bare_acl'
|
||||
try {
|
||||
const st = await ctx.vfs.stat(path)
|
||||
if (!st) {
|
||||
ctx.console.error('setfacl: ' + path + ': No such file')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
if (clear) {
|
||||
try {
|
||||
await ctx.vfs.unlink(side)
|
||||
} catch {
|
||||
/* ignore missing sidecar */
|
||||
}
|
||||
return
|
||||
}
|
||||
const text = bareStdin(ctx)
|
||||
if (!text || !String(text).trim()) {
|
||||
ctx.console.error('setfacl: ACL text required on stdin')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const body = String(text).endsWith('\n') ? String(text) : String(text) + '\n'
|
||||
await ctx.vfs.writeFile(side, ctx.b4a.from(body))
|
||||
} catch (e) {
|
||||
ctx.console.error('setfacl: ' + (e && e.message ? e.message : String(e)))
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
async function run(ctx, argv) {
|
||||
const script = argv[1]
|
||||
if (script == null) {
|
||||
ctx.console.error('usage: sh SCRIPT')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
if (typeof ctx.execLine !== 'function') {
|
||||
ctx.console.error('sh: execLine is not available (requires a Bare OS booter session)')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
let buf
|
||||
try {
|
||||
buf = await ctx.vfs.readFile(script)
|
||||
} catch (e) {
|
||||
ctx.console.error('sh: ' + ((e && e.message) || String(e)))
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
if (buf == null) {
|
||||
ctx.console.error('sh: ' + script + ': not found')
|
||||
ctx.exitCode = 127
|
||||
return
|
||||
}
|
||||
let text = ctx.b4a.toString(buf)
|
||||
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1)
|
||||
text = text.replace(/^\ufeff/, '')
|
||||
if (text.startsWith('#!')) {
|
||||
const nl = text.indexOf('\n')
|
||||
text = nl === -1 ? '' : text.slice(nl + 1)
|
||||
}
|
||||
const lines = text.split(/\r?\n/)
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const trimmed = lines[i].trim()
|
||||
if (!trimmed || trimmed.startsWith('#')) continue
|
||||
await ctx.execLine(trimmed)
|
||||
if ((Number(ctx.exitCode) || 0) !== 0) return
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
}
|
||||
@@ -46,6 +46,41 @@ function sortKeyObj(o) {
|
||||
return { n: 0, raw: keyText }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown> | undefined} ctx
|
||||
*/
|
||||
function sortLocaleTag(ctx) {
|
||||
const env =
|
||||
ctx && ctx.vfs && ctx.vfs.env && typeof ctx.vfs.env === 'object'
|
||||
? ctx.vfs.env
|
||||
: ctx && ctx.env && typeof ctx.env === 'object'
|
||||
? ctx.env
|
||||
: {}
|
||||
const t = String(
|
||||
/** @type {Record<string, string>} */ (env).LC_ALL ||
|
||||
/** @type {Record<string, string>} */ (env).LC_COLLATE ||
|
||||
'C'
|
||||
).trim()
|
||||
return t || 'C'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown> | undefined} ctx
|
||||
* @param {string} a
|
||||
* @param {string} b
|
||||
*/
|
||||
function sortLocaleCompareRaw(ctx, a, b) {
|
||||
const tag = sortLocaleTag(ctx)
|
||||
if (tag === 'C' || tag === 'POSIX') {
|
||||
return a < b ? -1 : a > b ? 1 : 0
|
||||
}
|
||||
try {
|
||||
return String(a).localeCompare(String(b), tag, { sensitivity: 'variant' })
|
||||
} catch {
|
||||
return a < b ? -1 : a > b ? 1 : 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} x
|
||||
* @param {string} y
|
||||
@@ -56,6 +91,7 @@ function sortKeyObj(o) {
|
||||
* @param {number | null} opts.keyStart
|
||||
* @param {number | null} opts.keyEnd
|
||||
* @param {string | null} opts.dForKey
|
||||
* @param {Record<string, unknown> | undefined} [opts.ctx]
|
||||
* @returns {number}
|
||||
*/
|
||||
function sortCompareLines(x, y, opts) {
|
||||
@@ -74,8 +110,8 @@ function sortCompareLines(x, y, opts) {
|
||||
return opts.reverse ? -ord : ord
|
||||
}
|
||||
}
|
||||
const cmp =
|
||||
kx.raw < ky.raw ? -1 : kx.raw > ky.raw ? 1 : x < y ? -1 : x > y ? 1 : 0
|
||||
let cmp = sortLocaleCompareRaw(opts.ctx, kx.raw, ky.raw)
|
||||
if (cmp === 0) cmp = sortLocaleCompareRaw(opts.ctx, x, y)
|
||||
return opts.reverse ? -cmp : cmp
|
||||
}
|
||||
|
||||
@@ -290,7 +326,8 @@ async function run(ctx, argv) {
|
||||
fold,
|
||||
keyStart,
|
||||
keyEnd,
|
||||
dForKey
|
||||
dForKey,
|
||||
ctx
|
||||
}
|
||||
|
||||
if (checkMode !== 'off') {
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
/** @param {Record<string, unknown>} ctx */
|
||||
function testParseEuidEgid(ctx) {
|
||||
const e = (ctx.vfs && ctx.vfs.env) || ctx.env || {}
|
||||
const uid = Number.parseInt(String(e.UID != null ? e.UID : '1000'), 10)
|
||||
const gid = Number.parseInt(String(e.GID != null ? e.GID : '1000'), 10)
|
||||
return {
|
||||
euid: Number.isFinite(uid) ? uid : 1000,
|
||||
egid: Number.isFinite(gid) ? gid : 1000
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User/group/other permission triplet (0–7) for the effective uid/gid.
|
||||
* @param {Record<string, unknown>} st
|
||||
* @param {number} euid
|
||||
* @param {number} egid
|
||||
*/
|
||||
function testEffPermTriplet(st, euid, egid) {
|
||||
const mode =
|
||||
typeof st.mode === 'number'
|
||||
? st.mode & 0o777
|
||||
: Number.parseInt(String(st.mode || '644'), 8) & 0o777
|
||||
const fuid = st.uid != null ? Number(st.uid) : 0
|
||||
const fgid = st.gid != null ? Number(st.gid) : 0
|
||||
if (euid === fuid) return (mode >> 6) & 7
|
||||
if (egid === fgid) return (mode >> 3) & 7
|
||||
return mode & 7
|
||||
}
|
||||
|
||||
async function evalTest(ctx, args) {
|
||||
if (!args.length) return false
|
||||
if (args[0] === '!') {
|
||||
@@ -38,10 +67,19 @@ async function evalTest(ctx, args) {
|
||||
const st = await ctx.vfs.lstat(p)
|
||||
return st != null && st.type === 'symlink'
|
||||
}
|
||||
const { euid, egid } = testParseEuidEgid(ctx)
|
||||
const st = await ctx.vfs.stat(p)
|
||||
if (op === '-e' || op === '-a') return st != null
|
||||
if (op === '-f') return st != null && st.type === 'file'
|
||||
if (op === '-d') return st != null && st.type === 'directory'
|
||||
if (op === '-r')
|
||||
return st != null && (testEffPermTriplet(st, euid, egid) & 4) !== 0
|
||||
if (op === '-w')
|
||||
return st != null && (testEffPermTriplet(st, euid, egid) & 2) !== 0
|
||||
if (op === '-x')
|
||||
return st != null && (testEffPermTriplet(st, euid, egid) & 1) !== 0
|
||||
if (op === '-s')
|
||||
return st != null && st.type === 'file' && Number(st.size) > 0
|
||||
if (op === '-z') return p.length === 0
|
||||
if (op === '-n') return p.length > 0
|
||||
return false
|
||||
|
||||
@@ -3,14 +3,27 @@
|
||||
* Limits: stdin 256KiB, 4096 whitespace/null tokens, 128 args per invocation,
|
||||
* 64 invocations per run. Exceeding limits is a fatal error (exit 125).
|
||||
* Supports -0/--null, -n, -I repl (replace repl in utility argv; implies -n 1 unless -n given).
|
||||
* -P N is accepted; stock booter runs sequentially; N is capped at MAX_P_FLAG (4).
|
||||
* -P N runs up to N batches in parallel (each batch uses a shallow ctx clone so exitCode does not race).
|
||||
* Max -P is min(requested, BARE_OS_XARGS_MAX_PROCS env, 32); default cap 8 when env unset.
|
||||
*/
|
||||
|
||||
const MAX_STDIN = 256 * 1024
|
||||
const MAX_TOKENS = 4096
|
||||
const MAX_PER_INVOCATION = 128
|
||||
const MAX_INVOCATIONS = 64
|
||||
const MAX_P_FLAG = 4
|
||||
const DEFAULT_P_CAP = 8
|
||||
const ABS_P_CAP = 32
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
function maxParallelFromEnv(ctx) {
|
||||
const env = (ctx && ctx.vfs && ctx.vfs.env) || (ctx && ctx.env) || {}
|
||||
const raw = String(env.BARE_OS_XARGS_MAX_PROCS || '').trim()
|
||||
if (!raw || !/^\d+$/.test(raw)) return DEFAULT_P_CAP
|
||||
const n = Number(raw)
|
||||
return Math.min(ABS_P_CAP, Math.max(1, n))
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
if (typeof ctx.runBinCommand !== 'function') {
|
||||
@@ -26,6 +39,7 @@ async function run(ctx, argv) {
|
||||
/** @type {string | null} */
|
||||
let repl = null
|
||||
let nExplicit = false
|
||||
const envPCap = maxParallelFromEnv(ctx)
|
||||
let pCap = 1
|
||||
let i = 0
|
||||
|
||||
@@ -48,14 +62,16 @@ async function run(ctx, argv) {
|
||||
return
|
||||
}
|
||||
const raw = Number(n)
|
||||
pCap = Math.min(MAX_P_FLAG, Math.max(1, raw))
|
||||
if (raw > MAX_P_FLAG) {
|
||||
pCap = Math.min(envPCap, Math.max(1, raw))
|
||||
if (raw > envPCap) {
|
||||
ctx.console.error(
|
||||
'xargs: -P ' +
|
||||
raw +
|
||||
' exceeds Bare OS cap ' +
|
||||
MAX_P_FLAG +
|
||||
' (parallelism hint only; sequential execution)'
|
||||
' exceeds cap ' +
|
||||
envPCap +
|
||||
' (raise BARE_OS_XARGS_MAX_PROCS up to ' +
|
||||
ABS_P_CAP +
|
||||
')'
|
||||
)
|
||||
}
|
||||
i += 2
|
||||
@@ -63,12 +79,14 @@ async function run(ctx, argv) {
|
||||
}
|
||||
if (a.startsWith('-P') && a.length > 2 && /^\d+$/.test(a.slice(2))) {
|
||||
const raw = Number(a.slice(2))
|
||||
pCap = Math.min(MAX_P_FLAG, Math.max(1, raw))
|
||||
if (raw > MAX_P_FLAG) {
|
||||
pCap = Math.min(envPCap, Math.max(1, raw))
|
||||
if (raw > envPCap) {
|
||||
ctx.console.error(
|
||||
'xargs: -P exceeds Bare OS cap ' +
|
||||
MAX_P_FLAG +
|
||||
' (parallelism hint only; sequential execution)'
|
||||
'xargs: -P exceeds cap ' +
|
||||
envPCap +
|
||||
' (raise BARE_OS_XARGS_MAX_PROCS up to ' +
|
||||
ABS_P_CAP +
|
||||
')'
|
||||
)
|
||||
}
|
||||
i++
|
||||
@@ -113,9 +131,9 @@ async function run(ctx, argv) {
|
||||
ctx.console.error(
|
||||
'xargs: Bare OS supports: -0/--null, -n N (max ' +
|
||||
MAX_PER_INVOCATION +
|
||||
' per run), -I repl, -P N (max ' +
|
||||
MAX_P_FLAG +
|
||||
', sequential)'
|
||||
' per run), -I repl, -P N (parallel batches, cap ' +
|
||||
envPCap +
|
||||
')'
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
@@ -124,7 +142,6 @@ async function run(ctx, argv) {
|
||||
/** @type {string[]} */
|
||||
let cmd = args.slice(i)
|
||||
if (cmd.length === 0) cmd = ['echo']
|
||||
void pCap
|
||||
|
||||
let text = bareStdin(ctx) || ''
|
||||
if (text.length > MAX_STDIN) {
|
||||
@@ -152,9 +169,10 @@ async function run(ctx, argv) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} target
|
||||
* @param {string[]} batch
|
||||
*/
|
||||
const runOne = async (batch) => {
|
||||
const runOne = async (target, batch) => {
|
||||
const subst = batch.join(' ')
|
||||
/** @type {string[]} */
|
||||
const toRun =
|
||||
@@ -169,29 +187,49 @@ async function run(ctx, argv) {
|
||||
}
|
||||
return o
|
||||
})
|
||||
await ctx.runBinCommand(toRun)
|
||||
await target.runBinCommand(toRun)
|
||||
}
|
||||
|
||||
if (pieces.length === 0) {
|
||||
await runOne([])
|
||||
await runOne(ctx, [])
|
||||
return
|
||||
}
|
||||
|
||||
let invocations = 0
|
||||
/** @type {string[][]} */
|
||||
const batches = []
|
||||
for (let o = 0; o < pieces.length; o += maxBatch) {
|
||||
if (++invocations > MAX_INVOCATIONS) {
|
||||
if (batches.length >= MAX_INVOCATIONS) {
|
||||
ctx.console.error(
|
||||
'xargs: exceeded ' + MAX_INVOCATIONS + ' invocations (Bare OS limit)'
|
||||
)
|
||||
ctx.exitCode = 125
|
||||
return
|
||||
}
|
||||
const batch = pieces.slice(o, o + maxBatch)
|
||||
await runOne(batch)
|
||||
const ec = Number(ctx.exitCode) || 0
|
||||
if (ec !== 0) {
|
||||
ctx.exitCode = ec
|
||||
batches.push(pieces.slice(o, o + maxBatch))
|
||||
}
|
||||
|
||||
if (pCap <= 1) {
|
||||
for (const batch of batches) {
|
||||
await runOne(ctx, batch)
|
||||
if ((Number(ctx.exitCode) || 0) !== 0) return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for (let w = 0; w < batches.length; w += pCap) {
|
||||
const slice = batches.slice(w, w + pCap)
|
||||
const codes = await Promise.all(
|
||||
slice.map(async (batch) => {
|
||||
const c = Object.assign({}, ctx, { exitCode: 0 })
|
||||
await runOne(c, batch)
|
||||
return Number(c.exitCode) || 0
|
||||
})
|
||||
)
|
||||
const bad = codes.find((x) => x !== 0)
|
||||
if (bad != null) {
|
||||
ctx.exitCode = bad
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* xattr — extended attributes via sidecar JSON (PATH.bare_xattr.json).
|
||||
* Values are stored base64-encoded UTF-8 strings for JSON safety.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} val
|
||||
*/
|
||||
function xattrB64Encode(val) {
|
||||
const s = String(val)
|
||||
if (typeof globalThis.Buffer !== 'undefined') {
|
||||
return globalThis.Buffer.from(s, 'utf8').toString('base64')
|
||||
}
|
||||
const u8 = new TextEncoder().encode(s)
|
||||
let bin = ''
|
||||
for (let i = 0; i < u8.length; i++) bin += String.fromCharCode(u8[i])
|
||||
if (typeof globalThis.btoa === 'function') return globalThis.btoa(bin)
|
||||
throw new Error('xattr: base64 encode unavailable')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} b64
|
||||
*/
|
||||
function xattrB64Decode(b64) {
|
||||
const t = String(b64).replace(/\s+/g, '')
|
||||
if (typeof globalThis.Buffer !== 'undefined') {
|
||||
return globalThis.Buffer.from(t, 'base64').toString('utf8')
|
||||
}
|
||||
if (typeof globalThis.atob === 'function') {
|
||||
const bin = globalThis.atob(t)
|
||||
let out = ''
|
||||
for (let i = 0; i < bin.length; i++) out += bin[i]
|
||||
return out
|
||||
}
|
||||
throw new Error('xattr: base64 decode unavailable')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} path
|
||||
*/
|
||||
async function readXattrMap(ctx, path) {
|
||||
const side = path + '.bare_xattr.json'
|
||||
const raw = await ctx.vfs.readFile(side)
|
||||
if (!raw || !raw.byteLength) return {}
|
||||
try {
|
||||
const o = JSON.parse(ctx.b4a.toString(raw))
|
||||
return o && typeof o === 'object' && !Array.isArray(o) ? o : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} path
|
||||
* @param {Record<string, string>} obj
|
||||
*/
|
||||
async function writeXattrMap(ctx, path, obj) {
|
||||
const side = path + '.bare_xattr.json'
|
||||
const text = JSON.stringify(obj) + '\n'
|
||||
await ctx.vfs.writeFile(side, ctx.b4a.from(text))
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
let list = false
|
||||
/** @type {string | null} */
|
||||
let delName = null
|
||||
/** @type {{ name: string, val: string } | null} */
|
||||
let writePair = null
|
||||
/** @type {string[]} */
|
||||
const rest = []
|
||||
for (let i = 1; i < argv.length; i++) {
|
||||
const a = argv[i]
|
||||
if (a === '-l' || a === '--list') {
|
||||
list = true
|
||||
continue
|
||||
}
|
||||
if (a === '-w' || a === '--write') {
|
||||
const name = argv[i + 1]
|
||||
const val = argv[i + 2]
|
||||
if (!name || val == null) {
|
||||
ctx.console.error('xattr: -w requires NAME VALUE')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
writePair = { name, val }
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (a === '-d' || a === '--delete') {
|
||||
const name = argv[i + 1]
|
||||
if (!name) {
|
||||
ctx.console.error('xattr: -d requires NAME')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
delName = name
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (a === '--help' || a === '-h') {
|
||||
ctx.console.log('usage: xattr [-l] [-w NAME VALUE] [-d NAME] PATH')
|
||||
return
|
||||
}
|
||||
if (a.startsWith('-')) {
|
||||
ctx.console.error('xattr: unsupported option ' + a)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
rest.push(a)
|
||||
}
|
||||
if (rest.length !== 1) {
|
||||
ctx.console.error('usage: xattr [-l] [-w NAME VALUE] [-d NAME] PATH')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const path = rest[0]
|
||||
try {
|
||||
const st = await ctx.vfs.stat(path)
|
||||
if (!st) {
|
||||
ctx.console.error('xattr: ' + path + ': No such file')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const obj = /** @type {Record<string, string>} */ (await readXattrMap(ctx, path))
|
||||
if (delName) {
|
||||
delete obj[delName]
|
||||
await writeXattrMap(ctx, path, obj)
|
||||
return
|
||||
}
|
||||
if (writePair) {
|
||||
obj[writePair.name] = xattrB64Encode(writePair.val)
|
||||
await writeXattrMap(ctx, path, obj)
|
||||
return
|
||||
}
|
||||
const keys = Object.keys(obj).sort()
|
||||
for (const k of keys) {
|
||||
if (list) {
|
||||
ctx.console.log(k + ': ' + xattrB64Decode(obj[k]))
|
||||
} else {
|
||||
ctx.console.log(k)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
ctx.console.error('xattr: ' + (e && e.message ? e.message : String(e)))
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* CI smoke checks for awk/sed sources (POSIX-oriented coverage is partial by design).
|
||||
*/
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import test from 'brittle'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const srcDir = join(__dirname, '../src')
|
||||
|
||||
test('sed.js exists and documents line-oriented operation', async (t) => {
|
||||
const s = await readFile(join(srcDir, 'sed.js'), 'utf8')
|
||||
t.ok(s.includes('async function run'))
|
||||
t.ok(s.length > 200)
|
||||
})
|
||||
|
||||
test('awk.js exists and wires field parsing', async (t) => {
|
||||
const s = await readFile(join(srcDir, 'awk.js'), 'utf8')
|
||||
t.ok(s.includes('async function run'))
|
||||
t.ok(s.includes('field-separator') || s.includes('split'))
|
||||
})
|
||||
|
||||
test('printf FORMAT octal escape helper is defined', async (t) => {
|
||||
const s = await readFile(join(srcDir, 'printf.js'), 'utf8')
|
||||
t.ok(s.includes('barePrintfUnescapeFormat'))
|
||||
})
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* getfacl / setfacl / xattr against an in-memory vfs stub.
|
||||
*/
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import test from 'brittle'
|
||||
import b4a from 'b4a'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
async function loadBin(name) {
|
||||
const runtime = await readFile(
|
||||
path.join(__dirname, '../lib/runtime.js'),
|
||||
'utf8'
|
||||
)
|
||||
const body = await readFile(
|
||||
path.join(__dirname, `../src/${name}.js`),
|
||||
'utf8'
|
||||
)
|
||||
return new AsyncFunction(
|
||||
'ctx',
|
||||
'argv',
|
||||
`${runtime}\n${body}\nif (typeof run === 'function') return await run(ctx, argv)\n`
|
||||
)
|
||||
}
|
||||
|
||||
function createCtx(store) {
|
||||
const logs = []
|
||||
const errs = []
|
||||
return {
|
||||
b4a,
|
||||
exitCode: 0,
|
||||
shellStdin: '',
|
||||
console: {
|
||||
log: (m) => logs.push(String(m)),
|
||||
error: (m) => errs.push(String(m))
|
||||
},
|
||||
vfs: {
|
||||
async stat(p) {
|
||||
if (!Object.prototype.hasOwnProperty.call(store.files, p)) return null
|
||||
return store.files[p].stat
|
||||
},
|
||||
async readFile(p) {
|
||||
const ent = store.files[p]
|
||||
if (!ent) return null
|
||||
return ent.body
|
||||
},
|
||||
async writeFile(p, body) {
|
||||
store.files[p] = {
|
||||
stat: { mode: 0o644, type: 'file', mtimeMs: Date.now() },
|
||||
body: body instanceof Uint8Array ? body : b4a.from(body)
|
||||
}
|
||||
},
|
||||
async unlink(p) {
|
||||
delete store.files[p]
|
||||
}
|
||||
},
|
||||
_logs: logs,
|
||||
_errs: errs
|
||||
}
|
||||
}
|
||||
|
||||
test('getfacl synthesizes mode triples and reads sidecar', async (t) => {
|
||||
const store = {
|
||||
files: {
|
||||
'/a': {
|
||||
stat: { mode: 0o755, type: 'file', mtimeMs: 1 },
|
||||
body: null
|
||||
},
|
||||
'/b': {
|
||||
stat: { mode: 0o644, type: 'file', mtimeMs: 1 },
|
||||
body: null
|
||||
},
|
||||
'/b.bare_acl': {
|
||||
stat: { mode: 0o644, type: 'file', mtimeMs: 1 },
|
||||
body: b4a.from('user::rw-\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
const runA = await loadBin('getfacl')
|
||||
const ctxA = createCtx(store)
|
||||
await runA(ctxA, ['getfacl', '-c', '/a'])
|
||||
t.is(ctxA.exitCode, 0)
|
||||
t.is(ctxA._logs.join('\n'), 'user::rwx\ngroup::r-x\nother::r-x')
|
||||
|
||||
const runB = await loadBin('getfacl')
|
||||
const ctxB = createCtx(store)
|
||||
await runB(ctxB, ['getfacl', '/b'])
|
||||
t.is(ctxB.exitCode, 0)
|
||||
t.ok(ctxB._logs[0].includes('user::rw-'))
|
||||
})
|
||||
|
||||
test('setfacl -b and stdin write', async (t) => {
|
||||
const store = {
|
||||
files: {
|
||||
'/x': {
|
||||
stat: { mode: 0o644, type: 'file', mtimeMs: 1 },
|
||||
body: null
|
||||
},
|
||||
'/x.bare_acl': {
|
||||
stat: { mode: 0o644, type: 'file', mtimeMs: 1 },
|
||||
body: b4a.from('old\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
const runClear = await loadBin('setfacl')
|
||||
const ctx1 = createCtx(store)
|
||||
await runClear(ctx1, ['setfacl', '-b', '/x'])
|
||||
t.is(ctx1.exitCode, 0)
|
||||
t.absent(store.files['/x.bare_acl'])
|
||||
|
||||
const runSet = await loadBin('setfacl')
|
||||
const ctx2 = createCtx(store)
|
||||
ctx2.shellStdin = 'user::r--\n'
|
||||
await runSet(ctx2, ['setfacl', '/x'])
|
||||
t.is(ctx2.exitCode, 0)
|
||||
t.is(b4a.toString(store.files['/x.bare_acl'].body), 'user::r--\n')
|
||||
})
|
||||
|
||||
test('xattr -w -l -d round trip', async (t) => {
|
||||
const store = {
|
||||
files: {
|
||||
'/f': {
|
||||
stat: { mode: 0o644, type: 'file', mtimeMs: 1 },
|
||||
body: null
|
||||
}
|
||||
}
|
||||
}
|
||||
const runW = await loadBin('xattr')
|
||||
const ctxW = createCtx(store)
|
||||
await runW(ctxW, ['xattr', '-w', 'user.t', 'hi', '/f'])
|
||||
t.is(ctxW.exitCode, 0)
|
||||
|
||||
const runL = await loadBin('xattr')
|
||||
const ctxL = createCtx(store)
|
||||
await runL(ctxL, ['xattr', '-l', '/f'])
|
||||
t.is(ctxL.exitCode, 0)
|
||||
t.is(ctxL._logs.join('\n'), 'user.t: hi')
|
||||
|
||||
const runD = await loadBin('xattr')
|
||||
const ctxD = createCtx(store)
|
||||
await runD(ctxD, ['xattr', '-d', 'user.t', '/f'])
|
||||
t.is(ctxD.exitCode, 0)
|
||||
|
||||
const runNames = await loadBin('xattr')
|
||||
const ctxN = createCtx(store)
|
||||
await runNames(ctxN, ['xattr', '/f'])
|
||||
t.is(ctxN.exitCode, 0)
|
||||
t.is(ctxN._logs.length, 0)
|
||||
})
|
||||
@@ -225,3 +225,8 @@ export {
|
||||
getKernelCapabilityWords,
|
||||
readKernelCapabilityWord
|
||||
} from './lib/kernel-capability-wire.js'
|
||||
export {
|
||||
BARE_OS_POSIX_PROFILE_VERSION,
|
||||
BARE_OS_POSIX_PROFILE_ID,
|
||||
BARE_OS_POSIX_PROFILE_REFERENCE
|
||||
} from './lib/bare-os-posix-profile.js'
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Declared POSIX-like profile for Bare OS guests (Issue 7–inspired, not certifiable POSIX).
|
||||
* Bump when utility/shell/VFS contracts intentionally change.
|
||||
*/
|
||||
|
||||
/** Semver for the documented POSIX-like surface (handbook ch.9 + environment appendix). */
|
||||
export const BARE_OS_POSIX_PROFILE_VERSION = '1.0.0'
|
||||
|
||||
/** Short identifier for telemetry and `/proc` mirrors. */
|
||||
export const BARE_OS_POSIX_PROFILE_ID = 'bare-os-posix-like'
|
||||
|
||||
/** Normative narrative reference (Open Group Issue 7 overview). */
|
||||
export const BARE_OS_POSIX_PROFILE_REFERENCE =
|
||||
'https://pubs.opengroup.org/onlinepubs/9699919799/'
|
||||
@@ -10,7 +10,8 @@
|
||||
"./messages": "./lib/messages.js",
|
||||
"./kernel-feature-bits.js": "./lib/kernel-feature-bits.js",
|
||||
"./protocol-meta.js": "./lib/protocol-meta.js",
|
||||
"./kernel-capability-wire.js": "./lib/kernel-capability-wire.js"
|
||||
"./kernel-capability-wire.js": "./lib/kernel-capability-wire.js",
|
||||
"./bare-os-posix-profile.js": "./lib/bare-os-posix-profile.js"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "brittle-bare test.js",
|
||||
|
||||
@@ -336,6 +336,7 @@ var BARE_TOP_SNAPSHOT_PROC_ENTRIES = [
|
||||
['replicationBackpressure', '/proc/bare_os/replication_backpressure.json'],
|
||||
['swarm', '/proc/bare_os/swarm'],
|
||||
['syncWindow', '/proc/bare_os/sync_window.json'],
|
||||
['clock', '/proc/bare_os/clock.json'],
|
||||
['hdmsHealth', '/proc/bare_os/hdms_health.json'],
|
||||
['hdmsHints', '/proc/bare_os/hdms_hints.json'],
|
||||
['dhtStatus', '/proc/bare_os/dht_status.json'],
|
||||
|
||||
@@ -336,6 +336,7 @@ var BARE_TOP_SNAPSHOT_PROC_ENTRIES = [
|
||||
['replicationBackpressure', '/proc/bare_os/replication_backpressure.json'],
|
||||
['swarm', '/proc/bare_os/swarm'],
|
||||
['syncWindow', '/proc/bare_os/sync_window.json'],
|
||||
['clock', '/proc/bare_os/clock.json'],
|
||||
['hdmsHealth', '/proc/bare_os/hdms_health.json'],
|
||||
['hdmsHints', '/proc/bare_os/hdms_hints.json'],
|
||||
['dhtStatus', '/proc/bare_os/dht_status.json'],
|
||||
|
||||
@@ -88,10 +88,11 @@ function bareOsEmitRaw(ctx, chunk) {
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
const silent = argv.includes('-s') || argv.includes('--silent')
|
||||
const args = argv.filter((a) => !a.startsWith('-'))
|
||||
const a = args[0]
|
||||
const b = args[1]
|
||||
const rest = argv.slice(1)
|
||||
const silent = rest.includes('-s') || rest.includes('--silent')
|
||||
const paths = rest.filter((a) => !a.startsWith('-'))
|
||||
const a = paths[0]
|
||||
const b = paths[1]
|
||||
if (!a || !b) {
|
||||
ctx.console.error('usage: cmp [-s] file1 file2')
|
||||
ctx.exitCode = 2
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
|
||||
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
|
||||
function bareStdin(ctx) {
|
||||
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
||||
}
|
||||
|
||||
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
|
||||
function bareFormatModeString(mode, type) {
|
||||
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
|
||||
const perm = mode & 0o777
|
||||
const r = (bit) => (perm & bit ? 'r' : '-')
|
||||
const w = (bit) => (perm & bit ? 'w' : '-')
|
||||
const x = (bit) => (perm & bit ? 'x' : '-')
|
||||
return (
|
||||
typeChar +
|
||||
r(0o400) +
|
||||
w(0o200) +
|
||||
x(0o100) +
|
||||
r(0o040) +
|
||||
w(0o020) +
|
||||
x(0o010) +
|
||||
r(0o004) +
|
||||
w(0o002) +
|
||||
x(0o001)
|
||||
)
|
||||
}
|
||||
|
||||
/** @param {number} mtimeMs @param {number} [nowMs] */
|
||||
function bareFormatLsMtime(mtimeMs, nowMs) {
|
||||
const now = nowMs != null ? nowMs : Date.now()
|
||||
const d = new Date(mtimeMs)
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec'
|
||||
]
|
||||
const mon = months[d.getMonth()]
|
||||
const day = String(d.getDate()).padStart(2, ' ')
|
||||
const sixMo = 180 * 24 * 3600 * 1000
|
||||
if (Math.abs(now - mtimeMs) > sixMo) {
|
||||
const yr = String(d.getFullYear()).padStart(4, ' ')
|
||||
return mon + ' ' + day + ' ' + yr
|
||||
}
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||
return mon + ' ' + day + ' ' + hh + ':' + mm
|
||||
}
|
||||
|
||||
/** @param {number} size */
|
||||
function barePosixBlocks(size) {
|
||||
return Math.ceil(Number(size) / 512) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
|
||||
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string | Uint8Array} chunk
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function bareOsEmitRaw(ctx, chunk) {
|
||||
if (typeof ctx.bareOsBinWrite === 'function') {
|
||||
const b4 = ctx.b4a
|
||||
const u8 =
|
||||
typeof chunk === 'string'
|
||||
? b4 && typeof b4.from === 'function'
|
||||
? b4.from(chunk)
|
||||
: new TextEncoder().encode(chunk)
|
||||
: chunk
|
||||
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
|
||||
return true
|
||||
}
|
||||
const w = globalThis.process?.stdout?.write
|
||||
if (typeof w === 'function') {
|
||||
w.call(globalThis.process.stdout, chunk)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal diff(1) for Bare OS: compares two text files (VFS paths).
|
||||
* Supports -q (quiet, exit status only), -s (report when identical), -u (single-hunk unified).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} a
|
||||
* @param {string} b
|
||||
*/
|
||||
function splitLines(a) {
|
||||
const s = a.replace(/\r\n/g, '\n')
|
||||
const parts = s.split('\n')
|
||||
if (parts.length && parts[parts.length - 1] === '') parts.pop()
|
||||
return parts
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} pathA
|
||||
* @param {string} pathB
|
||||
* @param {string} sa
|
||||
* @param {string} sb
|
||||
* @returns {string | null} unified hunk or null if identical
|
||||
*/
|
||||
function unifiedOneHunk(pathA, pathB, sa, sb) {
|
||||
const la = splitLines(sa)
|
||||
const lb = splitLines(sb)
|
||||
let i = 0
|
||||
const n = Math.min(la.length, lb.length)
|
||||
while (i < n && la[i] === lb[i]) i++
|
||||
if (i === la.length && i === lb.length) return null
|
||||
const oldLine = la[i] != null ? la[i] : ''
|
||||
const newLine = lb[i] != null ? lb[i] : ''
|
||||
return (
|
||||
`--- ${pathA}\n+++ ${pathB}\n@@ -${i + 1},1 +${i + 1},1 @@\n-${oldLine}\n+${newLine}\n`
|
||||
)
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
let quiet = false
|
||||
let sameReport = false
|
||||
let unified = false
|
||||
/** @type {string[]} */
|
||||
const files = []
|
||||
for (let i = 1; i < argv.length; i++) {
|
||||
const a = argv[i]
|
||||
if (a === '--') {
|
||||
files.push(...argv.slice(i + 1))
|
||||
break
|
||||
}
|
||||
if (a === '-q') {
|
||||
quiet = true
|
||||
continue
|
||||
}
|
||||
if (a === '-s') {
|
||||
sameReport = true
|
||||
continue
|
||||
}
|
||||
if (a === '-u' || a === '-U0' || a === '--unified') {
|
||||
unified = true
|
||||
continue
|
||||
}
|
||||
if (a.startsWith('-')) {
|
||||
ctx.console.error('diff: unsupported option ' + a)
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
files.push(a)
|
||||
}
|
||||
if (files.length !== 2) {
|
||||
ctx.console.error('usage: diff [-q] [-s] [-u] FILE1 FILE2')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
const [f1, f2] = files
|
||||
let b1
|
||||
let b2
|
||||
try {
|
||||
b1 = await ctx.vfs.readFile(f1)
|
||||
b2 = await ctx.vfs.readFile(f2)
|
||||
} catch (e) {
|
||||
ctx.console.error('diff: ' + ((e && e.message) || String(e)))
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
const s1 = b1 ? ctx.b4a.toString(b1) : ''
|
||||
const s2 = b2 ? ctx.b4a.toString(b2) : ''
|
||||
if (s1 === s2) {
|
||||
if (sameReport) ctx.console.log(`Files ${f1} and ${f2} are identical`)
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
if (quiet) {
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
if (unified) {
|
||||
const u = unifiedOneHunk(f1, f2, s1, s2)
|
||||
if (u) ctx.console.log(u.replace(/\n$/, ''))
|
||||
} else {
|
||||
const la = splitLines(s1)
|
||||
const lb = splitLines(s2)
|
||||
let i = 0
|
||||
const n = Math.min(la.length, lb.length)
|
||||
while (i < n && la[i] === lb[i]) i++
|
||||
ctx.console.log(`${i + 1}c${i + 1}`)
|
||||
ctx.console.log(`< ${la[i] != null ? la[i] : ''}`)
|
||||
ctx.console.log('---')
|
||||
ctx.console.log(`> ${lb[i] != null ? lb[i] : ''}`)
|
||||
}
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
@@ -129,6 +129,57 @@ function findMatchMtime(st, spec) {
|
||||
return days >= spec.n && days < spec.n + 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Device id sketch for `find -xdev` (do not cross `/mnt/<label>` boundaries vs system/personal).
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} absLogical
|
||||
*/
|
||||
function findRouteDevId(ctx, absLogical) {
|
||||
const a = String(absLogical).replace(/\/+$/, '') || '/'
|
||||
if (a.startsWith('/mnt/')) {
|
||||
const label = a.slice(5).split('/').filter(Boolean)[0]
|
||||
return label ? `mnt:${label}` : 'mnt:'
|
||||
}
|
||||
if (a === '/mnt') return 'mnt-root'
|
||||
const vfs = ctx.vfs
|
||||
if (!vfs || typeof vfs.route !== 'function') return 'unknown'
|
||||
const r = vfs.route(absLogical)
|
||||
if (r.virtualPseudo) return 'pseudo'
|
||||
if (r.virtualHomeDir) return 'personal-home'
|
||||
if (r.virtualVarRoot) return 'personal-var'
|
||||
if (a.startsWith('/tmp')) return 'personal'
|
||||
if (a.startsWith('/home/')) return 'personal'
|
||||
if (a.startsWith('/var/')) return 'personal'
|
||||
return 'system'
|
||||
}
|
||||
|
||||
/** @param {string} s */
|
||||
function parsePermSpec(s) {
|
||||
const t = String(s).trim()
|
||||
if (t.startsWith('/')) return null
|
||||
let body = t
|
||||
let allBitsSet = false
|
||||
if (t.startsWith('-')) {
|
||||
allBitsSet = true
|
||||
body = t.slice(1)
|
||||
}
|
||||
if (!/^[0-7]{1,4}$/.test(body)) return null
|
||||
const mask = Number.parseInt(body, 8) & 0o7777
|
||||
return { allBitsSet, mask }
|
||||
}
|
||||
|
||||
/** @param {Record<string, unknown>} st @param {{ allBitsSet: boolean, mask: number }} spec */
|
||||
function findMatchPerm(st, spec) {
|
||||
const mode =
|
||||
typeof st.mode === 'number'
|
||||
? st.mode & 0o7777
|
||||
: typeof st.mode === 'string'
|
||||
? Number.parseInt(String(st.mode), 8) & 0o7777
|
||||
: 0
|
||||
if (spec.allBitsSet) return (mode & spec.mask) === spec.mask
|
||||
return mode === spec.mask
|
||||
}
|
||||
|
||||
async function findIsEmpty(ctx, path, st) {
|
||||
if (st.type === 'file' || st.type === 'symlink') return (st.size || 0) === 0
|
||||
if (st.type !== 'directory') return false
|
||||
@@ -166,6 +217,7 @@ async function walk(ctx, dir, o, curDepth) {
|
||||
const nameOk = !o.nameRe || o.nameRe.test(n)
|
||||
let match = pathOk && nameOk && (!o.wantType || st.type === o.wantType)
|
||||
if (match && o.mtimeSpec) match = match && findMatchMtime(st, o.mtimeSpec)
|
||||
if (match && o.permSpec) match = match && findMatchPerm(st, o.permSpec)
|
||||
if (match && o.newerThanMs != null)
|
||||
match = match && st.mtimeMs > o.newerThanMs
|
||||
if (match && o.wantEmpty) {
|
||||
@@ -230,6 +282,10 @@ async function walk(ctx, dir, o, curDepth) {
|
||||
}
|
||||
}
|
||||
if (st.type !== 'directory') continue
|
||||
if (o.xdevRootDev != null) {
|
||||
const subDev = findRouteDevId(ctx, path)
|
||||
if (subDev !== o.xdevRootDev) continue
|
||||
}
|
||||
const absPath = ctx.vfs.resolveLogical(path)
|
||||
if (o.pruneAbs && absPath === o.pruneAbs) continue
|
||||
await walk(ctx, path, o, curDepth + 1)
|
||||
@@ -249,6 +305,9 @@ async function run(ctx, argv) {
|
||||
let print0 = false
|
||||
/** @type {{ op: string, n: number } | null} */
|
||||
let mtimeSpec = null
|
||||
/** @type {{ allBitsSet: boolean, mask: number } | null} */
|
||||
let permSpec = null
|
||||
let xdev = false
|
||||
/** @type {string | null} */
|
||||
let newerPath = null
|
||||
/** @type {string | null} */
|
||||
@@ -306,6 +365,19 @@ async function run(ctx, argv) {
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (a === '-perm' && argv[i + 1]) {
|
||||
permSpec = parsePermSpec(argv[++i])
|
||||
if (!permSpec) {
|
||||
ctx.console.error('find: invalid -perm')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (a === '-xdev') {
|
||||
xdev = true
|
||||
continue
|
||||
}
|
||||
if (a === '-newer' && argv[i + 1]) {
|
||||
newerPath = argv[++i]
|
||||
continue
|
||||
@@ -425,6 +497,7 @@ async function run(ctx, argv) {
|
||||
if (Number.isFinite(n)) execMax = Math.min(4096, Math.max(1, n))
|
||||
}
|
||||
const abs = ctx.vfs.resolveLogical(root)
|
||||
const xdevRootDev = xdev ? findRouteDevId(ctx, abs) : null
|
||||
const o = {
|
||||
maxDepth,
|
||||
minDepth,
|
||||
@@ -433,6 +506,8 @@ async function run(ctx, argv) {
|
||||
wantType,
|
||||
print0,
|
||||
mtimeSpec,
|
||||
permSpec,
|
||||
xdevRootDev,
|
||||
newerThanMs,
|
||||
pruneAbs,
|
||||
wantEmpty,
|
||||
|
||||
@@ -90,6 +90,7 @@ function bareOsEmitRaw(ctx, chunk) {
|
||||
/**
|
||||
* Subset of POSIX.1-2017 getconf — fixed values for Bare OS (no host sysconf).
|
||||
* Unknown names exit with status 1 (matches common getconf for invalid var).
|
||||
* Live pathconf: **`getconf NAME /absolute/path`** delegates to **`ctx.bareOsPathconf`** when set.
|
||||
*/
|
||||
|
||||
const CONF = {
|
||||
@@ -130,20 +131,31 @@ const CONF = {
|
||||
_PC_NAME_MAX: '255',
|
||||
/** Comma-separated `ctx.bareOsSyscall` op names implemented in stock booter. */
|
||||
BARE_OS_SYSCALL_OPS:
|
||||
'readFile,writeFile,readdir,mkdir,stat,unlink,chmod,chdir,getcwd,readlink,symlink,exists,lstat,rmdir,mount,umount,kill,rename,link,access,utimes,truncate,ftruncate,pathconf',
|
||||
'readFile,writeFile,readdir,mkdir,stat,unlink,chmod,chdir,getcwd,readlink,symlink,exists,lstat,rmdir,mount,umount,kill,rename,link,access,utimes,truncate,ftruncate,fsync,fdatasync,pathconf',
|
||||
/** Encodings accepted by `/bin/iconv` (subset; case-insensitive names). */
|
||||
BARE_OS_ICONV_ENCODINGS: 'UTF-8,ISO-8859-1,UTF-16LE,UTF-16BE',
|
||||
/** Synthetic process table JSON path (logical VFS). */
|
||||
BARE_OS_PROC_PROCESS_TABLE: '/proc/bare_os/process_table.json'
|
||||
BARE_OS_PROC_PROCESS_TABLE: '/proc/bare_os/process_table.json',
|
||||
/** Sidecar suffix for synthetic POSIX ACL text (`getfacl` / `setfacl`). */
|
||||
BARE_OS_ACL_SIDECAR_SUFFIX: '.bare_acl',
|
||||
/** Sidecar suffix for extended-attribute JSON (`xattr`). */
|
||||
BARE_OS_XATTR_SIDECAR_SUFFIX: '.bare_xattr.json',
|
||||
/** Incremental kernel.ext.d reload after boot (`ctx.bareOsReloadKernelExtDropinsSafe`); 0/1 hint only. */
|
||||
BARE_OS_KERNEL_EXT_D_HOT_RELOAD: '0',
|
||||
/** Operator hint for hyperblob-style dedup in host pipelines; guest VFS does not enable automatically. */
|
||||
BARE_OS_VFS_HYPERBLOBS_DEDUP: '0',
|
||||
/** This binary: fixed catalog. Use `getconf NAME /path` + `ctx.bareOsPathconf` for live pathconf. */
|
||||
BARE_OS_GETCONF_SOURCE: 'static_catalog'
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
const args = argv.slice(1).filter((a) => a !== '--')
|
||||
let dumpAll = false
|
||||
let name = null
|
||||
/** @type {string[]} */
|
||||
const positional = []
|
||||
for (const a of args) {
|
||||
if (a === '-a') dumpAll = true
|
||||
else if (!a.startsWith('-')) name = a
|
||||
else if (!a.startsWith('-')) positional.push(a)
|
||||
else {
|
||||
ctx.console.error('getconf: unknown option: ' + a)
|
||||
ctx.exitCode = 1
|
||||
@@ -159,12 +171,34 @@ async function run(ctx, argv) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
ctx.console.error('usage: getconf [-a] system_var')
|
||||
if (positional.length === 2) {
|
||||
const varName = positional[0]
|
||||
const pathSpec = positional[1]
|
||||
if (
|
||||
typeof ctx.bareOsPathconf === 'function' &&
|
||||
pathSpec.startsWith('/')
|
||||
) {
|
||||
try {
|
||||
const v = ctx.bareOsPathconf(pathSpec, varName)
|
||||
ctx.console.log(String(v))
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
} catch (e) {
|
||||
ctx.console.error('getconf: ' + (e?.message || String(e)))
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (positional.length !== 1) {
|
||||
ctx.console.error('usage: getconf [-a] system_var [path_for_pathconf]')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const name = positional[0]
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(CONF, name)) {
|
||||
ctx.console.log(CONF[name])
|
||||
ctx.exitCode = 0
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
|
||||
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
|
||||
function bareStdin(ctx) {
|
||||
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
||||
}
|
||||
|
||||
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
|
||||
function bareFormatModeString(mode, type) {
|
||||
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
|
||||
const perm = mode & 0o777
|
||||
const r = (bit) => (perm & bit ? 'r' : '-')
|
||||
const w = (bit) => (perm & bit ? 'w' : '-')
|
||||
const x = (bit) => (perm & bit ? 'x' : '-')
|
||||
return (
|
||||
typeChar +
|
||||
r(0o400) +
|
||||
w(0o200) +
|
||||
x(0o100) +
|
||||
r(0o040) +
|
||||
w(0o020) +
|
||||
x(0o010) +
|
||||
r(0o004) +
|
||||
w(0o002) +
|
||||
x(0o001)
|
||||
)
|
||||
}
|
||||
|
||||
/** @param {number} mtimeMs @param {number} [nowMs] */
|
||||
function bareFormatLsMtime(mtimeMs, nowMs) {
|
||||
const now = nowMs != null ? nowMs : Date.now()
|
||||
const d = new Date(mtimeMs)
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec'
|
||||
]
|
||||
const mon = months[d.getMonth()]
|
||||
const day = String(d.getDate()).padStart(2, ' ')
|
||||
const sixMo = 180 * 24 * 3600 * 1000
|
||||
if (Math.abs(now - mtimeMs) > sixMo) {
|
||||
const yr = String(d.getFullYear()).padStart(4, ' ')
|
||||
return mon + ' ' + day + ' ' + yr
|
||||
}
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||
return mon + ' ' + day + ' ' + hh + ':' + mm
|
||||
}
|
||||
|
||||
/** @param {number} size */
|
||||
function barePosixBlocks(size) {
|
||||
return Math.ceil(Number(size) / 512) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
|
||||
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string | Uint8Array} chunk
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function bareOsEmitRaw(ctx, chunk) {
|
||||
if (typeof ctx.bareOsBinWrite === 'function') {
|
||||
const b4 = ctx.b4a
|
||||
const u8 =
|
||||
typeof chunk === 'string'
|
||||
? b4 && typeof b4.from === 'function'
|
||||
? b4.from(chunk)
|
||||
: new TextEncoder().encode(chunk)
|
||||
: chunk
|
||||
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
|
||||
return true
|
||||
}
|
||||
const w = globalThis.process?.stdout?.write
|
||||
if (typeof w === 'function') {
|
||||
w.call(globalThis.process.stdout, chunk)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* getfacl — show POSIX-style ACL text for a VFS path.
|
||||
* When PATH.bare_acl exists, prints that file; otherwise synthesizes user/group/other
|
||||
* triples from the path mode bits (see setfacl / handbook).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {number} mode
|
||||
* @param {number} shift
|
||||
*/
|
||||
function tripleFromMode(mode, shift) {
|
||||
const b = (mode >> shift) & 7
|
||||
const r = b & 4 ? 'r' : '-'
|
||||
const w = b & 2 ? 'w' : '-'
|
||||
const x = b & 1 ? 'x' : '-'
|
||||
return r + w + x
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
let compact = false
|
||||
/** @type {string[]} */
|
||||
const files = []
|
||||
for (let i = 1; i < argv.length; i++) {
|
||||
const a = argv[i]
|
||||
if (a === '-c' || a === '--compact') {
|
||||
compact = true
|
||||
continue
|
||||
}
|
||||
if (a === '--help' || a === '-h') {
|
||||
ctx.console.log('usage: getfacl [-c] FILE')
|
||||
return
|
||||
}
|
||||
if (a.startsWith('-')) {
|
||||
ctx.console.error('getfacl: unsupported option ' + a)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
files.push(a)
|
||||
}
|
||||
if (files.length !== 1) {
|
||||
ctx.console.error('usage: getfacl [-c] FILE')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const path = files[0]
|
||||
const side = path + '.bare_acl'
|
||||
try {
|
||||
const st = await ctx.vfs.stat(path)
|
||||
if (!st) {
|
||||
ctx.console.error('getfacl: ' + path + ': No such file')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const raw = await ctx.vfs.readFile(side)
|
||||
if (raw && raw.byteLength) {
|
||||
const text = ctx.b4a.toString(raw)
|
||||
ctx.console.log(text.replace(/\n$/, ''))
|
||||
return
|
||||
}
|
||||
const mode = (st.mode || 0) & 0o777
|
||||
const u = tripleFromMode(mode, 6)
|
||||
const g = tripleFromMode(mode, 3)
|
||||
const o = tripleFromMode(mode, 0)
|
||||
const lines = compact
|
||||
? ['user::' + u, 'group::' + g, 'other::' + o]
|
||||
: [
|
||||
'# file: ' + path,
|
||||
'# owner: synthetic',
|
||||
'# group: synthetic',
|
||||
'user::' + u,
|
||||
'group::' + g,
|
||||
'other::' + o
|
||||
]
|
||||
ctx.console.log(lines.join('\n'))
|
||||
} catch (e) {
|
||||
ctx.console.error(
|
||||
'getfacl: ' + path + ': ' + (e && e.message ? e.message : String(e))
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ function bareOsEmitRaw(ctx, chunk) {
|
||||
return false
|
||||
}
|
||||
|
||||
var BARE_OS_HELP_BIN_SPACED = "arch awk baretop base32 base64 basename basenc btop bundlebee cat chgrp chmod chown cksum clear cmp comm cp crontab curl cut date dd df dir dircolors dirname du echo edit env exit expand expr factor false find fmt fold getconf git git-pear grep groups hdms head help hostid hostname hrpc iconv id install join journalctl jq kernel-boot-diff kernel-doctor kernel-explain kernel-fsck kernel-home-snapshot kernel-manifest-validate kernel-preflight kernel-triage kill ln logger login logname logout ls man md5sum mkdir mkfifo mktemp mount mv nano nl nohup nproc numfmt od oidc-publish openssl openssl paste pathchk pear-runtime-matrix pr printenv printf procstat ps pwd readlink realpath rev rm rmdir savevault sed seq sha1sum sha256sum sha512sum shuf sidecar sleep sort split ssh-keygen ssh-keygen stat sum sync systemctl tac tail tar tar tee test theme time timeout touch tr true truncate tsort tty ulimit umount uname unexpand uniq unlink uptime users vdir wc wget which who whoami xargs yes"
|
||||
var BARE_OS_HELP_BIN_SPACED = "arch awk baretop base32 base64 basename basenc btop bundlebee cat chgrp chmod chown cksum clear cmp comm cp crontab curl cut date dd df diff dir dircolors dirname du echo edit env exit expand expr factor false find fmt fold getconf getfacl git git-pear grep groups hdms head help hostid hostname hrpc iconv id install join journalctl jq kernel-boot-diff kernel-doctor kernel-explain kernel-fsck kernel-home-snapshot kernel-manifest-validate kernel-preflight kernel-triage kill ln logger login logname logout ls man md5sum mkdir mkfifo mktemp mount mv nano nl nohup nproc numfmt od oidc-publish openssl openssl paste patch pathchk pear-runtime-matrix pr printenv printf procstat ps pwd readlink realpath rev rm rmdir savevault sed seq setfacl sh sha1sum sha256sum sha512sum shuf sidecar sleep sort split ssh-keygen ssh-keygen stat sum sync systemctl tac tail tar tar tee test theme time timeout touch tr true truncate tsort tty ulimit umount uname unexpand uniq unlink uptime users vdir wc wget which who whoami xargs xattr yes"
|
||||
async function run(ctx, argv) {
|
||||
ctx.console.log(
|
||||
'Bare OS — default user: guest | shell builtins: alias, barerc, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: ' +
|
||||
|
||||
@@ -97,8 +97,8 @@ async function readUtf8(ctx, path) {
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
const a = argv[2]
|
||||
const b = argv[3]
|
||||
const a = argv[1]
|
||||
const b = argv[2]
|
||||
if (!a || !b) {
|
||||
ctx.console.error('Usage: kernel-boot-diff FILE1 FILE2')
|
||||
ctx.console.error('Compares NDJSON or line-oriented boot checkpoint dumps.')
|
||||
|
||||
@@ -87,6 +87,15 @@ function bareOsEmitRaw(ctx, chunk) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Move/rename via copy + delete. Hyperdrive has no single-key rename across paths, so
|
||||
* directory trees and cross-location moves are duplicated then removed. A single regular
|
||||
* file to a new non-directory path uses read + write + unlink when detected below.
|
||||
*
|
||||
* Documented limitations: cross-volume moves always copy+delete; EXDEV-style behavior is
|
||||
* implicit. Busy targets, partial copy failures, and union read-only trees surface as
|
||||
* generic errors from the VFS. Prefer same-directory renames for smallest blast radius.
|
||||
*/
|
||||
async function mvCopyPath(ctx, from, to, recursive, followSymlink) {
|
||||
const st = await ctx.vfs.lstat(from)
|
||||
if (!st) return false
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
|
||||
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
|
||||
function bareStdin(ctx) {
|
||||
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
||||
}
|
||||
|
||||
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
|
||||
function bareFormatModeString(mode, type) {
|
||||
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
|
||||
const perm = mode & 0o777
|
||||
const r = (bit) => (perm & bit ? 'r' : '-')
|
||||
const w = (bit) => (perm & bit ? 'w' : '-')
|
||||
const x = (bit) => (perm & bit ? 'x' : '-')
|
||||
return (
|
||||
typeChar +
|
||||
r(0o400) +
|
||||
w(0o200) +
|
||||
x(0o100) +
|
||||
r(0o040) +
|
||||
w(0o020) +
|
||||
x(0o010) +
|
||||
r(0o004) +
|
||||
w(0o002) +
|
||||
x(0o001)
|
||||
)
|
||||
}
|
||||
|
||||
/** @param {number} mtimeMs @param {number} [nowMs] */
|
||||
function bareFormatLsMtime(mtimeMs, nowMs) {
|
||||
const now = nowMs != null ? nowMs : Date.now()
|
||||
const d = new Date(mtimeMs)
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec'
|
||||
]
|
||||
const mon = months[d.getMonth()]
|
||||
const day = String(d.getDate()).padStart(2, ' ')
|
||||
const sixMo = 180 * 24 * 3600 * 1000
|
||||
if (Math.abs(now - mtimeMs) > sixMo) {
|
||||
const yr = String(d.getFullYear()).padStart(4, ' ')
|
||||
return mon + ' ' + day + ' ' + yr
|
||||
}
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||
return mon + ' ' + day + ' ' + hh + ':' + mm
|
||||
}
|
||||
|
||||
/** @param {number} size */
|
||||
function barePosixBlocks(size) {
|
||||
return Math.ceil(Number(size) / 512) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
|
||||
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string | Uint8Array} chunk
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function bareOsEmitRaw(ctx, chunk) {
|
||||
if (typeof ctx.bareOsBinWrite === 'function') {
|
||||
const b4 = ctx.b4a
|
||||
const u8 =
|
||||
typeof chunk === 'string'
|
||||
? b4 && typeof b4.from === 'function'
|
||||
? b4.from(chunk)
|
||||
: new TextEncoder().encode(chunk)
|
||||
: chunk
|
||||
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
|
||||
return true
|
||||
}
|
||||
const w = globalThis.process?.stdout?.write
|
||||
if (typeof w === 'function') {
|
||||
w.call(globalThis.process.stdout, chunk)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal patch(1): applies a single unified diff from stdin (ctx.shellStdin).
|
||||
* Supports -pNUM strip, --dry-run. Intended for diffs emitted by Bare OS diff -u.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {number} p
|
||||
*/
|
||||
function stripPath(path, p) {
|
||||
const segs = path.replace(/\\/g, '/').split('/').filter(Boolean)
|
||||
const rest = segs.slice(Math.min(p, segs.length))
|
||||
return rest.length ? rest.join('/') : segs[segs.length - 1] || path
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
let p = 0
|
||||
let dry = false
|
||||
/** @type {string | null} */
|
||||
let overrideFile = null
|
||||
for (let i = 1; i < argv.length; i++) {
|
||||
const a = argv[i]
|
||||
if (a === '--dry-run' || a === '--check') {
|
||||
dry = true
|
||||
continue
|
||||
}
|
||||
if (a === '-p' || a === '--strip') {
|
||||
const n = argv[i + 1]
|
||||
if (n == null || !/^\d+$/.test(n)) {
|
||||
ctx.console.error('patch: -p requires a number')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
p = Number(n)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (a.startsWith('-p') && a.length > 2 && /^\d+$/.test(a.slice(2))) {
|
||||
p = Number(a.slice(2))
|
||||
continue
|
||||
}
|
||||
if (a.startsWith('-')) {
|
||||
ctx.console.error('patch: unsupported option ' + a)
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
overrideFile = a
|
||||
break
|
||||
}
|
||||
|
||||
const raw = bareStdin(ctx) || ''
|
||||
const lines = raw.replace(/\r\n/g, '\n').split('\n')
|
||||
let minus = ''
|
||||
let plus = ''
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const ln = lines[i]
|
||||
if (ln.startsWith('--- ')) {
|
||||
minus = ln.slice(4).trim().split(/\s+/)[0]
|
||||
continue
|
||||
}
|
||||
if (ln.startsWith('+++ ')) {
|
||||
plus = ln.slice(4).trim().split(/\s+/)[0]
|
||||
continue
|
||||
}
|
||||
}
|
||||
const targetRaw = overrideFile || plus || minus
|
||||
if (!targetRaw) {
|
||||
ctx.console.error('patch: could not determine path from patch')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
const target = stripPath(targetRaw.replace(/^b\//, ''), p)
|
||||
|
||||
let hunkStart = lines.findIndex((l) => /^@@/.test(l))
|
||||
if (hunkStart < 0) {
|
||||
ctx.console.error('patch: missing @@ hunk')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const oldParts = []
|
||||
/** @type {string[]} */
|
||||
const newParts = []
|
||||
for (let i = hunkStart + 1; i < lines.length; i++) {
|
||||
const l = lines[i]
|
||||
if (l.startsWith('@@')) break
|
||||
if (l.startsWith('-')) oldParts.push(l.slice(1))
|
||||
else if (l.startsWith('+')) newParts.push(l.slice(1))
|
||||
else if (l.startsWith(' ')) {
|
||||
const body = l.slice(1)
|
||||
oldParts.push(body)
|
||||
newParts.push(body)
|
||||
}
|
||||
}
|
||||
|
||||
let cur = ''
|
||||
try {
|
||||
const buf = await ctx.vfs.readFile(target)
|
||||
cur = buf ? ctx.b4a.toString(buf) : ''
|
||||
} catch (e) {
|
||||
ctx.console.error('patch: ' + ((e && e.message) || String(e)))
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
|
||||
const cl = splitLinesKeep(cur)
|
||||
const expectOld = oldParts.join('\n')
|
||||
const gotOld = cl.join('\n')
|
||||
if (expectOld !== gotOld) {
|
||||
ctx.console.error('patch: file content does not match hunk (try correct -p)')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const out = newParts.join('\n') + (newParts.length ? '\n' : '')
|
||||
if (!dry) await ctx.vfs.writeFile(target, ctx.b4a.from(out))
|
||||
ctx.exitCode = 0
|
||||
}
|
||||
|
||||
/** @param {string} s */
|
||||
function splitLinesKeep(s) {
|
||||
const t = s.replace(/\r\n/g, '\n')
|
||||
const parts = t.split('\n')
|
||||
if (parts.length && parts[parts.length - 1] === '') parts.pop()
|
||||
return parts
|
||||
}
|
||||
@@ -87,6 +87,30 @@ function bareOsEmitRaw(ctx, chunk) {
|
||||
return false
|
||||
}
|
||||
|
||||
/** Unescape \\n \\t \\r \\\\ and octal \\ddd inside FORMAT (POSIX-style subset). */
|
||||
function barePrintfUnescapeFormat(fmt) {
|
||||
let o = ''
|
||||
for (let i = 0; i < fmt.length; i++) {
|
||||
if (fmt[i] !== '\\') {
|
||||
o += fmt[i]
|
||||
continue
|
||||
}
|
||||
const c = fmt[++i]
|
||||
if (c === undefined) break
|
||||
if (c === 'n') o += '\n'
|
||||
else if (c === 't') o += '\t'
|
||||
else if (c === 'r') o += '\r'
|
||||
else if (c === '\\') o += '\\'
|
||||
else if (c >= '0' && c <= '7') {
|
||||
let oct = c
|
||||
while (i + 1 < fmt.length && /[0-7]/.test(fmt[i + 1]) && oct.length < 3)
|
||||
oct += fmt[++i]
|
||||
o += String.fromCharCode(Number.parseInt(oct, 8) & 0xff)
|
||||
} else o += c
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
/** @param {string} s */
|
||||
function barePrintfBackslashArg(s) {
|
||||
let o = ''
|
||||
@@ -155,10 +179,6 @@ async function run(ctx, argv) {
|
||||
}
|
||||
const fmt = argv[1]
|
||||
const args = argv.slice(2)
|
||||
let unescaped = fmt
|
||||
unescaped = unescaped.replace(/\\n/g, '\n')
|
||||
unescaped = unescaped.replace(/\\t/g, '\t')
|
||||
unescaped = unescaped.replace(/\\r/g, '\r')
|
||||
unescaped = unescaped.replace(/\\\\/g, '\\')
|
||||
const unescaped = barePrintfUnescapeFormat(fmt)
|
||||
ctx.console.log(barePrintfFormat(unescaped, args))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
|
||||
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
|
||||
function bareStdin(ctx) {
|
||||
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
||||
}
|
||||
|
||||
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
|
||||
function bareFormatModeString(mode, type) {
|
||||
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
|
||||
const perm = mode & 0o777
|
||||
const r = (bit) => (perm & bit ? 'r' : '-')
|
||||
const w = (bit) => (perm & bit ? 'w' : '-')
|
||||
const x = (bit) => (perm & bit ? 'x' : '-')
|
||||
return (
|
||||
typeChar +
|
||||
r(0o400) +
|
||||
w(0o200) +
|
||||
x(0o100) +
|
||||
r(0o040) +
|
||||
w(0o020) +
|
||||
x(0o010) +
|
||||
r(0o004) +
|
||||
w(0o002) +
|
||||
x(0o001)
|
||||
)
|
||||
}
|
||||
|
||||
/** @param {number} mtimeMs @param {number} [nowMs] */
|
||||
function bareFormatLsMtime(mtimeMs, nowMs) {
|
||||
const now = nowMs != null ? nowMs : Date.now()
|
||||
const d = new Date(mtimeMs)
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec'
|
||||
]
|
||||
const mon = months[d.getMonth()]
|
||||
const day = String(d.getDate()).padStart(2, ' ')
|
||||
const sixMo = 180 * 24 * 3600 * 1000
|
||||
if (Math.abs(now - mtimeMs) > sixMo) {
|
||||
const yr = String(d.getFullYear()).padStart(4, ' ')
|
||||
return mon + ' ' + day + ' ' + yr
|
||||
}
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||
return mon + ' ' + day + ' ' + hh + ':' + mm
|
||||
}
|
||||
|
||||
/** @param {number} size */
|
||||
function barePosixBlocks(size) {
|
||||
return Math.ceil(Number(size) / 512) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
|
||||
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string | Uint8Array} chunk
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function bareOsEmitRaw(ctx, chunk) {
|
||||
if (typeof ctx.bareOsBinWrite === 'function') {
|
||||
const b4 = ctx.b4a
|
||||
const u8 =
|
||||
typeof chunk === 'string'
|
||||
? b4 && typeof b4.from === 'function'
|
||||
? b4.from(chunk)
|
||||
: new TextEncoder().encode(chunk)
|
||||
: chunk
|
||||
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
|
||||
return true
|
||||
}
|
||||
const w = globalThis.process?.stdout?.write
|
||||
if (typeof w === 'function') {
|
||||
w.call(globalThis.process.stdout, chunk)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* setfacl — store or clear synthetic ACL sidecar text (PATH.bare_acl).
|
||||
* Without -b, reads ACL lines from ctx.shellStdin (same pattern as patch).
|
||||
*/
|
||||
|
||||
async function run(ctx, argv) {
|
||||
let clear = false
|
||||
/** @type {string[]} */
|
||||
const files = []
|
||||
for (let i = 1; i < argv.length; i++) {
|
||||
const a = argv[i]
|
||||
if (a === '-b' || a === '--remove-all') {
|
||||
clear = true
|
||||
continue
|
||||
}
|
||||
if (a === '--help' || a === '-h') {
|
||||
ctx.console.log(
|
||||
'usage: setfacl [-b] PATH\nWrites ACL text from stdin when not clearing.'
|
||||
)
|
||||
return
|
||||
}
|
||||
if (a.startsWith('-')) {
|
||||
ctx.console.error('setfacl: unsupported option ' + a)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
files.push(a)
|
||||
}
|
||||
if (files.length !== 1) {
|
||||
ctx.console.error('usage: setfacl [-b] PATH')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const path = files[0]
|
||||
const side = path + '.bare_acl'
|
||||
try {
|
||||
const st = await ctx.vfs.stat(path)
|
||||
if (!st) {
|
||||
ctx.console.error('setfacl: ' + path + ': No such file')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
if (clear) {
|
||||
try {
|
||||
await ctx.vfs.unlink(side)
|
||||
} catch {
|
||||
/* ignore missing sidecar */
|
||||
}
|
||||
return
|
||||
}
|
||||
const text = bareStdin(ctx)
|
||||
if (!text || !String(text).trim()) {
|
||||
ctx.console.error('setfacl: ACL text required on stdin')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const body = String(text).endsWith('\n') ? String(text) : String(text) + '\n'
|
||||
await ctx.vfs.writeFile(side, ctx.b4a.from(body))
|
||||
} catch (e) {
|
||||
ctx.console.error('setfacl: ' + (e && e.message ? e.message : String(e)))
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
|
||||
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
|
||||
function bareStdin(ctx) {
|
||||
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
||||
}
|
||||
|
||||
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
|
||||
function bareFormatModeString(mode, type) {
|
||||
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
|
||||
const perm = mode & 0o777
|
||||
const r = (bit) => (perm & bit ? 'r' : '-')
|
||||
const w = (bit) => (perm & bit ? 'w' : '-')
|
||||
const x = (bit) => (perm & bit ? 'x' : '-')
|
||||
return (
|
||||
typeChar +
|
||||
r(0o400) +
|
||||
w(0o200) +
|
||||
x(0o100) +
|
||||
r(0o040) +
|
||||
w(0o020) +
|
||||
x(0o010) +
|
||||
r(0o004) +
|
||||
w(0o002) +
|
||||
x(0o001)
|
||||
)
|
||||
}
|
||||
|
||||
/** @param {number} mtimeMs @param {number} [nowMs] */
|
||||
function bareFormatLsMtime(mtimeMs, nowMs) {
|
||||
const now = nowMs != null ? nowMs : Date.now()
|
||||
const d = new Date(mtimeMs)
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec'
|
||||
]
|
||||
const mon = months[d.getMonth()]
|
||||
const day = String(d.getDate()).padStart(2, ' ')
|
||||
const sixMo = 180 * 24 * 3600 * 1000
|
||||
if (Math.abs(now - mtimeMs) > sixMo) {
|
||||
const yr = String(d.getFullYear()).padStart(4, ' ')
|
||||
return mon + ' ' + day + ' ' + yr
|
||||
}
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||
return mon + ' ' + day + ' ' + hh + ':' + mm
|
||||
}
|
||||
|
||||
/** @param {number} size */
|
||||
function barePosixBlocks(size) {
|
||||
return Math.ceil(Number(size) / 512) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
|
||||
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string | Uint8Array} chunk
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function bareOsEmitRaw(ctx, chunk) {
|
||||
if (typeof ctx.bareOsBinWrite === 'function') {
|
||||
const b4 = ctx.b4a
|
||||
const u8 =
|
||||
typeof chunk === 'string'
|
||||
? b4 && typeof b4.from === 'function'
|
||||
? b4.from(chunk)
|
||||
: new TextEncoder().encode(chunk)
|
||||
: chunk
|
||||
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
|
||||
return true
|
||||
}
|
||||
const w = globalThis.process?.stdout?.write
|
||||
if (typeof w === 'function') {
|
||||
w.call(globalThis.process.stdout, chunk)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
const script = argv[1]
|
||||
if (script == null) {
|
||||
ctx.console.error('usage: sh SCRIPT')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
if (typeof ctx.execLine !== 'function') {
|
||||
ctx.console.error('sh: execLine is not available (requires a Bare OS booter session)')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
let buf
|
||||
try {
|
||||
buf = await ctx.vfs.readFile(script)
|
||||
} catch (e) {
|
||||
ctx.console.error('sh: ' + ((e && e.message) || String(e)))
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
if (buf == null) {
|
||||
ctx.console.error('sh: ' + script + ': not found')
|
||||
ctx.exitCode = 127
|
||||
return
|
||||
}
|
||||
let text = ctx.b4a.toString(buf)
|
||||
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1)
|
||||
text = text.replace(/^\ufeff/, '')
|
||||
if (text.startsWith('#!')) {
|
||||
const nl = text.indexOf('\n')
|
||||
text = nl === -1 ? '' : text.slice(nl + 1)
|
||||
}
|
||||
const lines = text.split(/\r?\n/)
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const trimmed = lines[i].trim()
|
||||
if (!trimmed || trimmed.startsWith('#')) continue
|
||||
await ctx.execLine(trimmed)
|
||||
if ((Number(ctx.exitCode) || 0) !== 0) return
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
}
|
||||
@@ -135,6 +135,41 @@ function sortKeyObj(o) {
|
||||
return { n: 0, raw: keyText }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown> | undefined} ctx
|
||||
*/
|
||||
function sortLocaleTag(ctx) {
|
||||
const env =
|
||||
ctx && ctx.vfs && ctx.vfs.env && typeof ctx.vfs.env === 'object'
|
||||
? ctx.vfs.env
|
||||
: ctx && ctx.env && typeof ctx.env === 'object'
|
||||
? ctx.env
|
||||
: {}
|
||||
const t = String(
|
||||
/** @type {Record<string, string>} */ (env).LC_ALL ||
|
||||
/** @type {Record<string, string>} */ (env).LC_COLLATE ||
|
||||
'C'
|
||||
).trim()
|
||||
return t || 'C'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown> | undefined} ctx
|
||||
* @param {string} a
|
||||
* @param {string} b
|
||||
*/
|
||||
function sortLocaleCompareRaw(ctx, a, b) {
|
||||
const tag = sortLocaleTag(ctx)
|
||||
if (tag === 'C' || tag === 'POSIX') {
|
||||
return a < b ? -1 : a > b ? 1 : 0
|
||||
}
|
||||
try {
|
||||
return String(a).localeCompare(String(b), tag, { sensitivity: 'variant' })
|
||||
} catch {
|
||||
return a < b ? -1 : a > b ? 1 : 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} x
|
||||
* @param {string} y
|
||||
@@ -145,6 +180,7 @@ function sortKeyObj(o) {
|
||||
* @param {number | null} opts.keyStart
|
||||
* @param {number | null} opts.keyEnd
|
||||
* @param {string | null} opts.dForKey
|
||||
* @param {Record<string, unknown> | undefined} [opts.ctx]
|
||||
* @returns {number}
|
||||
*/
|
||||
function sortCompareLines(x, y, opts) {
|
||||
@@ -163,8 +199,8 @@ function sortCompareLines(x, y, opts) {
|
||||
return opts.reverse ? -ord : ord
|
||||
}
|
||||
}
|
||||
const cmp =
|
||||
kx.raw < ky.raw ? -1 : kx.raw > ky.raw ? 1 : x < y ? -1 : x > y ? 1 : 0
|
||||
let cmp = sortLocaleCompareRaw(opts.ctx, kx.raw, ky.raw)
|
||||
if (cmp === 0) cmp = sortLocaleCompareRaw(opts.ctx, x, y)
|
||||
return opts.reverse ? -cmp : cmp
|
||||
}
|
||||
|
||||
@@ -379,7 +415,8 @@ async function run(ctx, argv) {
|
||||
fold,
|
||||
keyStart,
|
||||
keyEnd,
|
||||
dForKey
|
||||
dForKey,
|
||||
ctx
|
||||
}
|
||||
|
||||
if (checkMode !== 'off') {
|
||||
|
||||
@@ -87,6 +87,35 @@ function bareOsEmitRaw(ctx, chunk) {
|
||||
return false
|
||||
}
|
||||
|
||||
/** @param {Record<string, unknown>} ctx */
|
||||
function testParseEuidEgid(ctx) {
|
||||
const e = (ctx.vfs && ctx.vfs.env) || ctx.env || {}
|
||||
const uid = Number.parseInt(String(e.UID != null ? e.UID : '1000'), 10)
|
||||
const gid = Number.parseInt(String(e.GID != null ? e.GID : '1000'), 10)
|
||||
return {
|
||||
euid: Number.isFinite(uid) ? uid : 1000,
|
||||
egid: Number.isFinite(gid) ? gid : 1000
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User/group/other permission triplet (0–7) for the effective uid/gid.
|
||||
* @param {Record<string, unknown>} st
|
||||
* @param {number} euid
|
||||
* @param {number} egid
|
||||
*/
|
||||
function testEffPermTriplet(st, euid, egid) {
|
||||
const mode =
|
||||
typeof st.mode === 'number'
|
||||
? st.mode & 0o777
|
||||
: Number.parseInt(String(st.mode || '644'), 8) & 0o777
|
||||
const fuid = st.uid != null ? Number(st.uid) : 0
|
||||
const fgid = st.gid != null ? Number(st.gid) : 0
|
||||
if (euid === fuid) return (mode >> 6) & 7
|
||||
if (egid === fgid) return (mode >> 3) & 7
|
||||
return mode & 7
|
||||
}
|
||||
|
||||
async function evalTest(ctx, args) {
|
||||
if (!args.length) return false
|
||||
if (args[0] === '!') {
|
||||
@@ -127,10 +156,19 @@ async function evalTest(ctx, args) {
|
||||
const st = await ctx.vfs.lstat(p)
|
||||
return st != null && st.type === 'symlink'
|
||||
}
|
||||
const { euid, egid } = testParseEuidEgid(ctx)
|
||||
const st = await ctx.vfs.stat(p)
|
||||
if (op === '-e' || op === '-a') return st != null
|
||||
if (op === '-f') return st != null && st.type === 'file'
|
||||
if (op === '-d') return st != null && st.type === 'directory'
|
||||
if (op === '-r')
|
||||
return st != null && (testEffPermTriplet(st, euid, egid) & 4) !== 0
|
||||
if (op === '-w')
|
||||
return st != null && (testEffPermTriplet(st, euid, egid) & 2) !== 0
|
||||
if (op === '-x')
|
||||
return st != null && (testEffPermTriplet(st, euid, egid) & 1) !== 0
|
||||
if (op === '-s')
|
||||
return st != null && st.type === 'file' && Number(st.size) > 0
|
||||
if (op === '-z') return p.length === 0
|
||||
if (op === '-n') return p.length > 0
|
||||
return false
|
||||
|
||||
@@ -92,14 +92,27 @@ function bareOsEmitRaw(ctx, chunk) {
|
||||
* Limits: stdin 256KiB, 4096 whitespace/null tokens, 128 args per invocation,
|
||||
* 64 invocations per run. Exceeding limits is a fatal error (exit 125).
|
||||
* Supports -0/--null, -n, -I repl (replace repl in utility argv; implies -n 1 unless -n given).
|
||||
* -P N is accepted; stock booter runs sequentially; N is capped at MAX_P_FLAG (4).
|
||||
* -P N runs up to N batches in parallel (each batch uses a shallow ctx clone so exitCode does not race).
|
||||
* Max -P is min(requested, BARE_OS_XARGS_MAX_PROCS env, 32); default cap 8 when env unset.
|
||||
*/
|
||||
|
||||
const MAX_STDIN = 256 * 1024
|
||||
const MAX_TOKENS = 4096
|
||||
const MAX_PER_INVOCATION = 128
|
||||
const MAX_INVOCATIONS = 64
|
||||
const MAX_P_FLAG = 4
|
||||
const DEFAULT_P_CAP = 8
|
||||
const ABS_P_CAP = 32
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
function maxParallelFromEnv(ctx) {
|
||||
const env = (ctx && ctx.vfs && ctx.vfs.env) || (ctx && ctx.env) || {}
|
||||
const raw = String(env.BARE_OS_XARGS_MAX_PROCS || '').trim()
|
||||
if (!raw || !/^\d+$/.test(raw)) return DEFAULT_P_CAP
|
||||
const n = Number(raw)
|
||||
return Math.min(ABS_P_CAP, Math.max(1, n))
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
if (typeof ctx.runBinCommand !== 'function') {
|
||||
@@ -115,6 +128,7 @@ async function run(ctx, argv) {
|
||||
/** @type {string | null} */
|
||||
let repl = null
|
||||
let nExplicit = false
|
||||
const envPCap = maxParallelFromEnv(ctx)
|
||||
let pCap = 1
|
||||
let i = 0
|
||||
|
||||
@@ -137,14 +151,16 @@ async function run(ctx, argv) {
|
||||
return
|
||||
}
|
||||
const raw = Number(n)
|
||||
pCap = Math.min(MAX_P_FLAG, Math.max(1, raw))
|
||||
if (raw > MAX_P_FLAG) {
|
||||
pCap = Math.min(envPCap, Math.max(1, raw))
|
||||
if (raw > envPCap) {
|
||||
ctx.console.error(
|
||||
'xargs: -P ' +
|
||||
raw +
|
||||
' exceeds Bare OS cap ' +
|
||||
MAX_P_FLAG +
|
||||
' (parallelism hint only; sequential execution)'
|
||||
' exceeds cap ' +
|
||||
envPCap +
|
||||
' (raise BARE_OS_XARGS_MAX_PROCS up to ' +
|
||||
ABS_P_CAP +
|
||||
')'
|
||||
)
|
||||
}
|
||||
i += 2
|
||||
@@ -152,12 +168,14 @@ async function run(ctx, argv) {
|
||||
}
|
||||
if (a.startsWith('-P') && a.length > 2 && /^\d+$/.test(a.slice(2))) {
|
||||
const raw = Number(a.slice(2))
|
||||
pCap = Math.min(MAX_P_FLAG, Math.max(1, raw))
|
||||
if (raw > MAX_P_FLAG) {
|
||||
pCap = Math.min(envPCap, Math.max(1, raw))
|
||||
if (raw > envPCap) {
|
||||
ctx.console.error(
|
||||
'xargs: -P exceeds Bare OS cap ' +
|
||||
MAX_P_FLAG +
|
||||
' (parallelism hint only; sequential execution)'
|
||||
'xargs: -P exceeds cap ' +
|
||||
envPCap +
|
||||
' (raise BARE_OS_XARGS_MAX_PROCS up to ' +
|
||||
ABS_P_CAP +
|
||||
')'
|
||||
)
|
||||
}
|
||||
i++
|
||||
@@ -202,9 +220,9 @@ async function run(ctx, argv) {
|
||||
ctx.console.error(
|
||||
'xargs: Bare OS supports: -0/--null, -n N (max ' +
|
||||
MAX_PER_INVOCATION +
|
||||
' per run), -I repl, -P N (max ' +
|
||||
MAX_P_FLAG +
|
||||
', sequential)'
|
||||
' per run), -I repl, -P N (parallel batches, cap ' +
|
||||
envPCap +
|
||||
')'
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
@@ -213,7 +231,6 @@ async function run(ctx, argv) {
|
||||
/** @type {string[]} */
|
||||
let cmd = args.slice(i)
|
||||
if (cmd.length === 0) cmd = ['echo']
|
||||
void pCap
|
||||
|
||||
let text = bareStdin(ctx) || ''
|
||||
if (text.length > MAX_STDIN) {
|
||||
@@ -241,9 +258,10 @@ async function run(ctx, argv) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} target
|
||||
* @param {string[]} batch
|
||||
*/
|
||||
const runOne = async (batch) => {
|
||||
const runOne = async (target, batch) => {
|
||||
const subst = batch.join(' ')
|
||||
/** @type {string[]} */
|
||||
const toRun =
|
||||
@@ -258,29 +276,49 @@ async function run(ctx, argv) {
|
||||
}
|
||||
return o
|
||||
})
|
||||
await ctx.runBinCommand(toRun)
|
||||
await target.runBinCommand(toRun)
|
||||
}
|
||||
|
||||
if (pieces.length === 0) {
|
||||
await runOne([])
|
||||
await runOne(ctx, [])
|
||||
return
|
||||
}
|
||||
|
||||
let invocations = 0
|
||||
/** @type {string[][]} */
|
||||
const batches = []
|
||||
for (let o = 0; o < pieces.length; o += maxBatch) {
|
||||
if (++invocations > MAX_INVOCATIONS) {
|
||||
if (batches.length >= MAX_INVOCATIONS) {
|
||||
ctx.console.error(
|
||||
'xargs: exceeded ' + MAX_INVOCATIONS + ' invocations (Bare OS limit)'
|
||||
)
|
||||
ctx.exitCode = 125
|
||||
return
|
||||
}
|
||||
const batch = pieces.slice(o, o + maxBatch)
|
||||
await runOne(batch)
|
||||
const ec = Number(ctx.exitCode) || 0
|
||||
if (ec !== 0) {
|
||||
ctx.exitCode = ec
|
||||
batches.push(pieces.slice(o, o + maxBatch))
|
||||
}
|
||||
|
||||
if (pCap <= 1) {
|
||||
for (const batch of batches) {
|
||||
await runOne(ctx, batch)
|
||||
if ((Number(ctx.exitCode) || 0) !== 0) return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for (let w = 0; w < batches.length; w += pCap) {
|
||||
const slice = batches.slice(w, w + pCap)
|
||||
const codes = await Promise.all(
|
||||
slice.map(async (batch) => {
|
||||
const c = Object.assign({}, ctx, { exitCode: 0 })
|
||||
await runOne(c, batch)
|
||||
return Number(c.exitCode) || 0
|
||||
})
|
||||
)
|
||||
const bad = codes.find((x) => x !== 0)
|
||||
if (bad != null) {
|
||||
ctx.exitCode = bad
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
|
||||
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
|
||||
function bareStdin(ctx) {
|
||||
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
||||
}
|
||||
|
||||
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
|
||||
function bareFormatModeString(mode, type) {
|
||||
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
|
||||
const perm = mode & 0o777
|
||||
const r = (bit) => (perm & bit ? 'r' : '-')
|
||||
const w = (bit) => (perm & bit ? 'w' : '-')
|
||||
const x = (bit) => (perm & bit ? 'x' : '-')
|
||||
return (
|
||||
typeChar +
|
||||
r(0o400) +
|
||||
w(0o200) +
|
||||
x(0o100) +
|
||||
r(0o040) +
|
||||
w(0o020) +
|
||||
x(0o010) +
|
||||
r(0o004) +
|
||||
w(0o002) +
|
||||
x(0o001)
|
||||
)
|
||||
}
|
||||
|
||||
/** @param {number} mtimeMs @param {number} [nowMs] */
|
||||
function bareFormatLsMtime(mtimeMs, nowMs) {
|
||||
const now = nowMs != null ? nowMs : Date.now()
|
||||
const d = new Date(mtimeMs)
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec'
|
||||
]
|
||||
const mon = months[d.getMonth()]
|
||||
const day = String(d.getDate()).padStart(2, ' ')
|
||||
const sixMo = 180 * 24 * 3600 * 1000
|
||||
if (Math.abs(now - mtimeMs) > sixMo) {
|
||||
const yr = String(d.getFullYear()).padStart(4, ' ')
|
||||
return mon + ' ' + day + ' ' + yr
|
||||
}
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||
return mon + ' ' + day + ' ' + hh + ':' + mm
|
||||
}
|
||||
|
||||
/** @param {number} size */
|
||||
function barePosixBlocks(size) {
|
||||
return Math.ceil(Number(size) / 512) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
|
||||
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string | Uint8Array} chunk
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function bareOsEmitRaw(ctx, chunk) {
|
||||
if (typeof ctx.bareOsBinWrite === 'function') {
|
||||
const b4 = ctx.b4a
|
||||
const u8 =
|
||||
typeof chunk === 'string'
|
||||
? b4 && typeof b4.from === 'function'
|
||||
? b4.from(chunk)
|
||||
: new TextEncoder().encode(chunk)
|
||||
: chunk
|
||||
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
|
||||
return true
|
||||
}
|
||||
const w = globalThis.process?.stdout?.write
|
||||
if (typeof w === 'function') {
|
||||
w.call(globalThis.process.stdout, chunk)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* xattr — extended attributes via sidecar JSON (PATH.bare_xattr.json).
|
||||
* Values are stored base64-encoded UTF-8 strings for JSON safety.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} val
|
||||
*/
|
||||
function xattrB64Encode(val) {
|
||||
const s = String(val)
|
||||
if (typeof globalThis.Buffer !== 'undefined') {
|
||||
return globalThis.Buffer.from(s, 'utf8').toString('base64')
|
||||
}
|
||||
const u8 = new TextEncoder().encode(s)
|
||||
let bin = ''
|
||||
for (let i = 0; i < u8.length; i++) bin += String.fromCharCode(u8[i])
|
||||
if (typeof globalThis.btoa === 'function') return globalThis.btoa(bin)
|
||||
throw new Error('xattr: base64 encode unavailable')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} b64
|
||||
*/
|
||||
function xattrB64Decode(b64) {
|
||||
const t = String(b64).replace(/\s+/g, '')
|
||||
if (typeof globalThis.Buffer !== 'undefined') {
|
||||
return globalThis.Buffer.from(t, 'base64').toString('utf8')
|
||||
}
|
||||
if (typeof globalThis.atob === 'function') {
|
||||
const bin = globalThis.atob(t)
|
||||
let out = ''
|
||||
for (let i = 0; i < bin.length; i++) out += bin[i]
|
||||
return out
|
||||
}
|
||||
throw new Error('xattr: base64 decode unavailable')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} path
|
||||
*/
|
||||
async function readXattrMap(ctx, path) {
|
||||
const side = path + '.bare_xattr.json'
|
||||
const raw = await ctx.vfs.readFile(side)
|
||||
if (!raw || !raw.byteLength) return {}
|
||||
try {
|
||||
const o = JSON.parse(ctx.b4a.toString(raw))
|
||||
return o && typeof o === 'object' && !Array.isArray(o) ? o : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} path
|
||||
* @param {Record<string, string>} obj
|
||||
*/
|
||||
async function writeXattrMap(ctx, path, obj) {
|
||||
const side = path + '.bare_xattr.json'
|
||||
const text = JSON.stringify(obj) + '\n'
|
||||
await ctx.vfs.writeFile(side, ctx.b4a.from(text))
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
let list = false
|
||||
/** @type {string | null} */
|
||||
let delName = null
|
||||
/** @type {{ name: string, val: string } | null} */
|
||||
let writePair = null
|
||||
/** @type {string[]} */
|
||||
const rest = []
|
||||
for (let i = 1; i < argv.length; i++) {
|
||||
const a = argv[i]
|
||||
if (a === '-l' || a === '--list') {
|
||||
list = true
|
||||
continue
|
||||
}
|
||||
if (a === '-w' || a === '--write') {
|
||||
const name = argv[i + 1]
|
||||
const val = argv[i + 2]
|
||||
if (!name || val == null) {
|
||||
ctx.console.error('xattr: -w requires NAME VALUE')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
writePair = { name, val }
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (a === '-d' || a === '--delete') {
|
||||
const name = argv[i + 1]
|
||||
if (!name) {
|
||||
ctx.console.error('xattr: -d requires NAME')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
delName = name
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (a === '--help' || a === '-h') {
|
||||
ctx.console.log('usage: xattr [-l] [-w NAME VALUE] [-d NAME] PATH')
|
||||
return
|
||||
}
|
||||
if (a.startsWith('-')) {
|
||||
ctx.console.error('xattr: unsupported option ' + a)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
rest.push(a)
|
||||
}
|
||||
if (rest.length !== 1) {
|
||||
ctx.console.error('usage: xattr [-l] [-w NAME VALUE] [-d NAME] PATH')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const path = rest[0]
|
||||
try {
|
||||
const st = await ctx.vfs.stat(path)
|
||||
if (!st) {
|
||||
ctx.console.error('xattr: ' + path + ': No such file')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const obj = /** @type {Record<string, string>} */ (await readXattrMap(ctx, path))
|
||||
if (delName) {
|
||||
delete obj[delName]
|
||||
await writeXattrMap(ctx, path, obj)
|
||||
return
|
||||
}
|
||||
if (writePair) {
|
||||
obj[writePair.name] = xattrB64Encode(writePair.val)
|
||||
await writeXattrMap(ctx, path, obj)
|
||||
return
|
||||
}
|
||||
const keys = Object.keys(obj).sort()
|
||||
for (const k of keys) {
|
||||
if (list) {
|
||||
ctx.console.log(k + ': ' + xattrB64Decode(obj[k]))
|
||||
} else {
|
||||
ctx.console.log(k)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
ctx.console.error('xattr: ' + (e && e.message ? e.message : String(e)))
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"auditSchemaVersion": 1,
|
||||
"type": "vfs_policy",
|
||||
"ts": 1710000000000,
|
||||
"sessionId": "example-session",
|
||||
"path": "/etc/shadow",
|
||||
"effect": "deny",
|
||||
"detail": { "ruleId": "boot-policy-deny-vfs" }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"proposedIndexes": ["paths", "packages", "aclSubjects"],
|
||||
"isolation": "dedicated-corestore-namespace",
|
||||
"note": "Designator for Hyperbee secondary indexes; not mounted in the stock guest VFS."
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"slots": ["a", "b"],
|
||||
"activeSlot": "a",
|
||||
"previousSlot": "b",
|
||||
"rollbackEnv": "BARE_OS_SEED_STAGING_PREVIOUS_SLOT",
|
||||
"note": "OTA A/B slot hints for operators; stock kernel merges boot.policy + seeder handshake for enforcement."
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"lockfileLogicalPath": "/.bare/os/package-lock.json",
|
||||
"hyperbeeMetadata": true,
|
||||
"note": "Personal-drive package manager + lockfile story; tooling may mirror manifests under /.bare/os/."
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
|
||||
"utilities": {
|
||||
"sh": {
|
||||
"posixIssue7": "minimal",
|
||||
"notes": [
|
||||
"Line-oriented script runner; strips leading shebang; uses ctx.execLine"
|
||||
],
|
||||
"source": "sh.js"
|
||||
},
|
||||
"xargs": {
|
||||
"posixIssue7": "partial",
|
||||
"notes": [
|
||||
"-0 -n -I -P with bounded parallelism; ctx.runBinCommand only; see BARE_OS_XARGS_MAX_PROCS"
|
||||
],
|
||||
"source": "xargs.js"
|
||||
},
|
||||
"test": {
|
||||
"posixIssue7": "partial",
|
||||
"notes": [
|
||||
"Expanding [ and test; -r/-w/-x/-s vs UID/GID where supported"
|
||||
],
|
||||
"source": "test.js"
|
||||
},
|
||||
"echo": {
|
||||
"posixIssue7": "partial",
|
||||
"source": "echo.js"
|
||||
},
|
||||
"printf": {
|
||||
"posixIssue7": "partial",
|
||||
"notes": [
|
||||
"FORMAT supports \\n \\t \\r \\\\ and octal \\ddd; % conversions still partial vs Issue 7"
|
||||
],
|
||||
"source": "printf.js"
|
||||
},
|
||||
"cmp": {
|
||||
"posixIssue7": "partial",
|
||||
"source": "cmp.js"
|
||||
},
|
||||
"diff": {
|
||||
"posixIssue7": "partial",
|
||||
"notes": ["-q -s -u; single unified hunk; text only"],
|
||||
"source": "diff.js"
|
||||
},
|
||||
"patch": {
|
||||
"posixIssue7": "partial",
|
||||
"notes": ["stdin unified diff; -p --dry-run; whole-file hunk match"],
|
||||
"source": "patch.js"
|
||||
},
|
||||
"find": {
|
||||
"posixIssue7": "partial",
|
||||
"source": "find.js"
|
||||
},
|
||||
"tar": {
|
||||
"posixIssue7": "partial",
|
||||
"notes": [
|
||||
"Ustar; hard links; union limits; xattr via PATH.bare_xattr.json sidecar (see xattr)"
|
||||
]
|
||||
},
|
||||
"sed": {
|
||||
"posixIssue7": "partial"
|
||||
},
|
||||
"awk": {
|
||||
"posixIssue7": "partial"
|
||||
},
|
||||
"ls": {
|
||||
"posixIssue7": "partial"
|
||||
},
|
||||
"sort": {
|
||||
"posixIssue7": "partial",
|
||||
"notes": ["LC_ALL / LC_COLLATE localeCompare for string keys when not C/POSIX"],
|
||||
"source": "sort.js"
|
||||
},
|
||||
"getconf": {
|
||||
"posixIssue7": "partial",
|
||||
"notes": ["Static vs live pathconf bridge"],
|
||||
"source": "getconf.js"
|
||||
},
|
||||
"getfacl": {
|
||||
"posixIssue7": "partial",
|
||||
"notes": [
|
||||
"PATH.bare_acl sidecar or mode-derived user/group/other triples"
|
||||
],
|
||||
"source": "getfacl.js"
|
||||
},
|
||||
"setfacl": {
|
||||
"posixIssue7": "partial",
|
||||
"notes": ["-b clears sidecar; else ACL lines from stdin"],
|
||||
"source": "setfacl.js"
|
||||
},
|
||||
"xattr": {
|
||||
"posixIssue7": "partial",
|
||||
"notes": [
|
||||
"PATH.bare_xattr.json map; values base64 UTF-8; -l -w -d"
|
||||
],
|
||||
"source": "xattr.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"bridgeMetaEnv": "BARE_OS_SUBPROCESS_BRIDGE_META_JSON",
|
||||
"posixSpawnParity": "incremental",
|
||||
"note": "Host pid map and cgroup hints surface through the subprocess bridge snapshot; guest POSIX spawn remains phased."
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"schemaVersion": 3,
|
||||
"ctxApiVersion": "1.30.0",
|
||||
"posixProfile": {
|
||||
"id": "bare-os-posix-like",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"ops": ["readFile", "writeFile"],
|
||||
"opsDetail": [
|
||||
{ "name": "readFile", "category": "fs", "stability": "stable" },
|
||||
{ "name": "writeFile", "category": "fs", "stability": "stable" }
|
||||
],
|
||||
"errnoHints": {
|
||||
"ENOENT": 2,
|
||||
"EACCES": 13
|
||||
},
|
||||
"caps": {
|
||||
"note": "example"
|
||||
},
|
||||
"atMs": 0
|
||||
}
|
||||
@@ -5,3 +5,8 @@ Install files named `*.timer` under:
|
||||
$HOME/.config/bare-os/timers/
|
||||
|
||||
(max 8 timer files). See `every-ms.timer.example` and `on-calendar.timer.example` in this directory — copy and rename to `something.timer`.
|
||||
|
||||
Clock semantics: `OnCalendar=` / crontab fields use the host wall clock. `every-ms=` timers
|
||||
default to `setInterval` (drift vs wall is possible). Set `BARE_OS_TIMER_EVERY_MS_MONOTONIC=1`
|
||||
in the guest environment to chain `setTimeout` callbacks so each firing waits a full period
|
||||
after the previous job finishes (monotonic cadence, not wall-aligned).
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"description": "Illustrative VFS sidecar layout for synthetic ACL and extended attributes (not applied automatically).",
|
||||
"paths": {
|
||||
"aclTextSidecarSuffix": ".bare_acl",
|
||||
"xattrJsonSidecarSuffix": ".bare_xattr.json"
|
||||
},
|
||||
"exampleAclSidecarLines": [
|
||||
"# file: /home/user/doc.txt",
|
||||
"user::rw-",
|
||||
"group::r--",
|
||||
"other::r--"
|
||||
],
|
||||
"exampleXattrJson": {
|
||||
"user.com.example.tag": "aGVsbG8="
|
||||
},
|
||||
"note": "Use /bin/getfacl, /bin/setfacl, and /bin/xattr; values in xattr JSON are base64-encoded UTF-8 strings."
|
||||
}
|
||||
@@ -72,6 +72,13 @@ async function invokeCtxBootHooks(ctx, ev) {
|
||||
* BARE_OS_BOOT_MANIFEST_SIGN=1: verify Ed25519 signature in /etc/bare-os/boot.manifest.sig over the raw
|
||||
* manifest bytes; public key from BARE_OS_BOOT_MANIFEST_PUBKEY_HEX (64 hex chars). Uses ctx.bareOsVerifyBootManifestSignature.
|
||||
*
|
||||
* BARE_OS_KERNEL_EXT_D_HOT_RELOAD=1: after boot, exposes **`ctx.bareOsReloadKernelExtDropinsSafe()`** which re-scans
|
||||
* `/etc/bare-os/kernel.ext.d` and runs only extension scripts not yet recorded in **`ctx.bareOsLoadedKernelExtScripts`**
|
||||
* (append-only; does not unload). Append-only audit: **`/run/bare-os/kernel-ext-reload.ndjson`** when **`ctx.vfs.writeFile`** exists.
|
||||
*
|
||||
* BARE_OS_VFS_HYPERBLOBS_DEDUP=1: operator hint surfaced in **`/proc/bare_os/features`** — content-defined chunking may be enabled
|
||||
* in host mirror/hyperblob pipelines; the guest VFS does not turn on hyperblobs automatically.
|
||||
*
|
||||
* BARE_OS_BOOT_POLICY=1: merge `skipBootStages` / `denyBootStages` from boot.policy (legacy `skipPhases` / `denyBootPhases` still honored; see applyBootPolicyFile). Optional `policyFallbackPaths` for tiered skip merge; `initdAdmission` sets initd env caps; `extensionSignerPinsV5` multi-signer pins.
|
||||
* Boot policy v2 (optional): `maxExecLineDepth`, `denyEnvKeys`, `requireProcNodes` (VFS paths under /proc).
|
||||
* Optional `minKernelCapabilitiesPrimary` / `requireSeedCaps` (integers) when ctx exposes
|
||||
@@ -139,6 +146,43 @@ function applyBootSafeMode(ctx) {
|
||||
pol.add('onboot')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
function kernelExtHotReloadEnabled(ctx) {
|
||||
const v = ctx.env?.BARE_OS_KERNEL_EXT_D_HOT_RELOAD
|
||||
return v === '1' || v === 'true'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, unknown>} row
|
||||
*/
|
||||
async function maybeAppendKernelExtReloadJournal(ctx, row) {
|
||||
const vfs = ctx.vfs
|
||||
const b4 = ctx.b4a
|
||||
if (!vfs || typeof vfs.writeFile !== 'function' || !b4) return
|
||||
const path = '/run/bare-os/kernel-ext-reload.ndjson'
|
||||
const line =
|
||||
JSON.stringify({
|
||||
kernelExtReloadSchemaVersion: 1,
|
||||
ts: Date.now(),
|
||||
...row
|
||||
}) + '\n'
|
||||
try {
|
||||
let prev = ''
|
||||
try {
|
||||
const buf = await vfs.readFile(path)
|
||||
if (buf) prev = b4.toString(buf)
|
||||
} catch {
|
||||
/* new */
|
||||
}
|
||||
await vfs.writeFile(path, b4.from(prev + line))
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
async function maybeAppendBootTransactionJournal(ctx, row) {
|
||||
const en = ctx.env && ctx.env.BARE_OS_BOOT_TRANSACTION_JOURNAL
|
||||
if (en !== '1' && en !== 'true' && en !== 'ndjson') return
|
||||
@@ -2047,12 +2091,20 @@ function kernelExtDependencyDepth(entries) {
|
||||
/**
|
||||
* Optional `/etc/bare-os/kernel.ext.d/*.json` with `{ "scripts": ["/lib/bare-os/extensions/foo.js"] }`.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {{ incremental?: boolean, ranScripts?: string[] }} [opts]
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function runKernelExtDropins(ctx) {
|
||||
async function runKernelExtDropins(ctx, opts = {}) {
|
||||
const incremental = opts.incremental === true
|
||||
const ranScripts = opts.ranScripts
|
||||
const { drive, console } = ctx
|
||||
const run = ctx.bareOsRunImageScript
|
||||
if (typeof run !== 'function') return true
|
||||
if (!ctx.bareOsLoadedKernelExtScripts) {
|
||||
ctx.bareOsLoadedKernelExtScripts = new Set()
|
||||
}
|
||||
/** @type {Set<string>} */
|
||||
const loadedSet = /** @type {Set<string>} */ (ctx.bareOsLoadedKernelExtScripts)
|
||||
const denyRaw = String(
|
||||
ctx.env?.BARE_OS_BOOT_POLICY_DENY_KERNEL_EXT_IDS || ''
|
||||
).trim()
|
||||
@@ -2204,12 +2256,17 @@ async function runKernelExtDropins(ctx) {
|
||||
console.error(`[kernel.ext.d] rejected script path: ${imgPath}`)
|
||||
continue
|
||||
}
|
||||
if (incremental && loadedSet.has(imgPath)) {
|
||||
continue
|
||||
}
|
||||
if (dry) {
|
||||
console.error(`[boot-dry-run] skip kernel.ext.d script: ${imgPath}`)
|
||||
continue
|
||||
}
|
||||
try {
|
||||
await run(imgPath)
|
||||
loadedSet.add(imgPath)
|
||||
if (Array.isArray(ranScripts)) ranScripts.push(imgPath)
|
||||
if (typeof ctx.bareOsRegisterKernelExtensionRecord === 'function') {
|
||||
ctx.bareOsRegisterKernelExtensionRecord({
|
||||
dropin: ent.file,
|
||||
@@ -2489,6 +2546,38 @@ async function runKernelSelftest(ctx) {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {number} bootT0
|
||||
* @param {string[]} stageLog
|
||||
*/
|
||||
async function maybeWriteBootPerfJson(ctx, bootT0, stageLog) {
|
||||
const vfs = ctx.vfs
|
||||
const b4 = ctx.b4a
|
||||
if (!vfs || typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function' || !b4)
|
||||
return
|
||||
const wall = Date.now() - bootT0
|
||||
const budgetRaw = Number.parseInt(
|
||||
String(ctx.env?.BARE_OS_BOOT_BUDGET_MS_COLD || ''),
|
||||
10
|
||||
)
|
||||
const budget = Number.isFinite(budgetRaw) && budgetRaw > 0 ? budgetRaw : null
|
||||
const row =
|
||||
JSON.stringify({
|
||||
schema: 1,
|
||||
coldWallMs: wall,
|
||||
bootBudgetMsCold: budget,
|
||||
withinBudget: budget == null ? null : wall <= budget,
|
||||
stageCount: stageLog.length,
|
||||
completedAtMs: Date.now()
|
||||
}) + '\n'
|
||||
try {
|
||||
await vfs.writeFile('/run/bare-os/boot-perf.json', b4.from(row))
|
||||
} catch {
|
||||
/* optional */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string[]} stageLog
|
||||
@@ -2535,7 +2624,8 @@ function publishBootReady(ctx, stageLog) {
|
||||
schema: 1,
|
||||
state: 'committed',
|
||||
note: 'Guest kernel finished boot stage sequence; see boot-transaction.ndjson for per-stage rows.'
|
||||
}
|
||||
},
|
||||
kernelExtHotReload: kernelExtHotReloadEnabled(ctx)
|
||||
}
|
||||
},
|
||||
initd: { awaited: true }
|
||||
@@ -2761,6 +2851,27 @@ async function start(ctx) {
|
||||
)
|
||||
if (!bootOk) return
|
||||
publishBootReady(ctx, stageLog)
|
||||
await maybeWriteBootPerfJson(ctx, bootT0, stageLog)
|
||||
if (kernelExtHotReloadEnabled(ctx)) {
|
||||
ctx.bareOsReloadKernelExtDropinsSafe = async () => {
|
||||
if (!kernelExtHotReloadEnabled(ctx)) {
|
||||
return { ok: false, reason: 'env_disabled', atMs: Date.now() }
|
||||
}
|
||||
/** @type {string[]} */
|
||||
const ran = []
|
||||
const ok = await runKernelExtDropins(ctx, {
|
||||
incremental: true,
|
||||
ranScripts: ran
|
||||
})
|
||||
await maybeAppendKernelExtReloadJournal(ctx, {
|
||||
ok,
|
||||
incremental: true,
|
||||
ranScripts: ran,
|
||||
sessionId: String((ctx.env && ctx.env.BARE_OS_SESSION_ID) || '')
|
||||
})
|
||||
return { ok, ranScripts: ran, atMs: Date.now() }
|
||||
}
|
||||
}
|
||||
{
|
||||
const budget = Number.parseInt(
|
||||
String(ctx.env?.BARE_OS_BOOT_BUDGET_MS_COLD || ''),
|
||||
|
||||
@@ -50,17 +50,18 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
return -1;
|
||||
};
|
||||
var AsyncResource = class {
|
||||
bind() {
|
||||
throw new Error("Not implemented");
|
||||
bind(fn) {
|
||||
if (typeof fn !== "function") return fn;
|
||||
return fn.bind(this);
|
||||
}
|
||||
static bind() {
|
||||
throw new Error("Not implemented");
|
||||
static bind(fn) {
|
||||
if (typeof fn !== "function") return fn;
|
||||
return fn.bind(void 0);
|
||||
}
|
||||
runInAsyncScope() {
|
||||
throw new Error("Not implemented");
|
||||
runInAsyncScope(fn, thisArg, ...args) {
|
||||
return fn.apply(thisArg, args);
|
||||
}
|
||||
emitDestroy() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
asyncId() {
|
||||
return -1;
|
||||
|
||||
@@ -1370,7 +1370,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
// returns the string match, the regexp source, whether there's magic
|
||||
// in the regexp (so a regular expression is required) and whether or
|
||||
// not the uflag is needed for the regular expression (for posix classes)
|
||||
// TODO: instead of injecting the start/end at this point, just return
|
||||
// NOTE: instead of injecting the start/end at this point, just return
|
||||
// the BODY of the regexp, along with the start/end portions suitable
|
||||
// for binding the start/end in either a joined full-path makeRe context
|
||||
// (where we bind to (^|/), or a standalone matchPart context (where
|
||||
@@ -1418,7 +1418,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
// But that's always going to be $ if it's the ending pattern, or nothing,
|
||||
// so the caller can just attach $ at the end of the pattern when building.
|
||||
//
|
||||
// So the todo is:
|
||||
// So the next step is:
|
||||
// - better detect what kind of start is needed
|
||||
// - return both flavors of starting pattern
|
||||
// - attach $ at the end of the pattern when creating the actual RegExp
|
||||
@@ -3806,9 +3806,9 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
stdout: null,
|
||||
stderr: null
|
||||
};
|
||||
var node_events_1 = __require("node:events");
|
||||
var node_stream_1 = __importDefault(__require("node:stream"));
|
||||
var node_string_decoder_1 = __require("node:string_decoder");
|
||||
var node_events_1 = __require("bare-events");
|
||||
var node_stream_1 = __importDefault(__require("bare-stream"));
|
||||
var node_string_decoder_1 = __require("bare-string-decoder");
|
||||
var isStream = (s) => !!s && typeof s === "object" && (s instanceof Minipass || s instanceof node_stream_1.default || (0, exports.isReadable)(s) || (0, exports.isWritable)(s));
|
||||
exports.isStream = isStream;
|
||||
var isReadable = (s) => !!s && typeof s === "object" && s instanceof node_events_1.EventEmitter && typeof s.pipe === "function" && // node core Writable streams have a pipe() method, but it throws
|
||||
@@ -4723,12 +4723,12 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.PathScurry = exports.Path = exports.PathScurryDarwin = exports.PathScurryPosix = exports.PathScurryWin32 = exports.PathScurryBase = exports.PathPosix = exports.PathWin32 = exports.PathBase = exports.ChildrenCache = exports.ResolveCache = void 0;
|
||||
var lru_cache_1 = require_commonjs2();
|
||||
var node_path_1 = __require("node:path");
|
||||
var node_url_1 = __require("node:url");
|
||||
var node_path_1 = __require("bare-path");
|
||||
var node_url_1 = __require("bare-url");
|
||||
var fs_1 = __require("fs");
|
||||
var actualFS = __importStar(__require("node:fs"));
|
||||
var actualFS = __importStar(__require("bare-fs"));
|
||||
var realpathSync = fs_1.realpathSync.native;
|
||||
var promises_1 = __require("node:fs/promises");
|
||||
var promises_1 = __require("bare-fs/promises");
|
||||
var minipass_1 = require_commonjs3();
|
||||
var defaultFS = {
|
||||
lstatSync: fs_1.lstatSync,
|
||||
@@ -6589,7 +6589,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
}
|
||||
// pattern like C:/...
|
||||
// split = ['C:', ...]
|
||||
// XXX: would be nice to handle patterns like `c:*` to test the cwd
|
||||
// Enhancement: handle patterns like `c:*` to test the cwd
|
||||
// in c: for *, but I don't know of a way to even figure out what that
|
||||
// cwd is without actually chdir'ing into it?
|
||||
/**
|
||||
@@ -7314,7 +7314,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Glob = void 0;
|
||||
var minimatch_1 = require_commonjs();
|
||||
var node_url_1 = __require("node:url");
|
||||
var node_url_1 = __require("bare-url");
|
||||
var path_scurry_1 = require_commonjs4();
|
||||
var pattern_js_1 = require_pattern();
|
||||
var walker_js_1 = require_walker();
|
||||
@@ -12408,7 +12408,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
if (fn) {
|
||||
var bound = (
|
||||
/** @type {import('./types').BoundSlice | import('./types').BoundSet} */
|
||||
// @ts-expect-error TODO FIXME
|
||||
// @ts-expect-error upstream-type-bridge
|
||||
callBind(fn)
|
||||
);
|
||||
cache[
|
||||
@@ -19501,7 +19501,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
return stream.pipe(refUnrefFilter).pipe(byteCounter);
|
||||
};
|
||||
RandomAccessReader.prototype._readStreamForRange = function(start, end) {
|
||||
throw new Error("not implemented");
|
||||
throw new Error("bare-os: RandomAccessReader range stream unavailable");
|
||||
};
|
||||
RandomAccessReader.prototype.read = function(buffer, offset, length, position, callback) {
|
||||
var readStream = this.createReadStream({ start: position, end: position + length });
|
||||
@@ -41206,7 +41206,7 @@ ${JSON.stringify(header, null, indent)}
|
||||
}
|
||||
};
|
||||
var bitfieldUpdate = {
|
||||
// TODO: can maybe be folded into a HAVE later on with the most recent spec
|
||||
// NOTE: can maybe be folded into a HAVE later on with the most recent spec
|
||||
preencode(state, b) {
|
||||
state.end++;
|
||||
c.uint.preencode(state, b.start);
|
||||
@@ -42177,7 +42177,7 @@ ${JSON.stringify(header, null, indent)}
|
||||
if (length === 0) return;
|
||||
this.wireRequest.send({
|
||||
id: 0,
|
||||
// TODO: use an more explicit id for this eventually...
|
||||
// NOTE: use an more explicit id for this eventually...
|
||||
fork: this.remoteFork,
|
||||
block: null,
|
||||
hash: null,
|
||||
@@ -42614,7 +42614,7 @@ ${JSON.stringify(header, null, indent)}
|
||||
};
|
||||
module.exports = class Replicator {
|
||||
static Peer = Peer;
|
||||
// hack to be able to access Peer from outside this module
|
||||
// Bridge: access Peer from outside this module
|
||||
constructor(core, key, {
|
||||
notDownloadingLinger = NOT_DOWNLOADING_SLACK,
|
||||
eagerUpgrade = true,
|
||||
@@ -42824,7 +42824,7 @@ ${JSON.stringify(header, null, indent)}
|
||||
_addUpgradeMaybe() {
|
||||
return this.eagerUpgrade === true ? this._addUpgrade() : this._upgrade;
|
||||
}
|
||||
// TODO: this function is OVER called atm, at each updatePeer/updateAll
|
||||
// NOTE: this function is OVER called atm, at each updatePeer/updateAll
|
||||
// instead its more efficient to only call it when the conditions in here change - ie on sync/add/remove peer
|
||||
// Do this when we have more tests.
|
||||
_checkUpgradeIfAvailable() {
|
||||
@@ -45618,7 +45618,7 @@ ${JSON.stringify(header, null, indent)}
|
||||
}
|
||||
return this._verifyMulti(batch, signature);
|
||||
}
|
||||
// TODO: better api for this that is more ... multisig-ey
|
||||
// NOTE: better api for this that is more ... multisig-ey
|
||||
sign(batch, keyPair) {
|
||||
if (!keyPair || !keyPair.secretKey) throw BAD_ARGUMENT("No key pair was passed");
|
||||
for (const s of this.signers) {
|
||||
@@ -47934,11 +47934,11 @@ ${JSON.stringify(header, null, indent)}
|
||||
const activeRequests = range && range.activeRequests || this.activeRequests;
|
||||
return this.replicator.addRange(activeRequests, range);
|
||||
}
|
||||
// TODO: get rid of this / deprecate it?
|
||||
// NOTE: get rid of this / deprecate it?
|
||||
undownload(range) {
|
||||
range.destroy(null);
|
||||
}
|
||||
// TODO: get rid of this / deprecate it?
|
||||
// NOTE: get rid of this / deprecate it?
|
||||
cancel(request) {
|
||||
}
|
||||
async truncate(newLength = 0, opts = {}) {
|
||||
@@ -48737,7 +48737,7 @@ ${JSON.stringify(header, null, indent)}
|
||||
quorum: m.quorum,
|
||||
signers,
|
||||
prologue: null
|
||||
// TODO: could be configurable through the header still...
|
||||
// NOTE: could be configurable through the header still...
|
||||
};
|
||||
}
|
||||
async function getBlobsLength(db) {
|
||||
@@ -57953,8 +57953,8 @@ ${JSON.stringify(header, null, indent)}
|
||||
_isActive() {
|
||||
return !this.destroyed && !this.suspended;
|
||||
}
|
||||
// TODO: Allow announce to be an argument to this
|
||||
// TODO: Maybe announce should be a setter?
|
||||
// NOTE: Allow announce to be an argument to this
|
||||
// NOTE: Maybe announce should be a setter?
|
||||
async _refresh() {
|
||||
if (this.suspended) return;
|
||||
const clock = ++this._refreshes;
|
||||
@@ -58562,7 +58562,7 @@ ${JSON.stringify(header, null, indent)}
|
||||
return this.listening;
|
||||
}
|
||||
// Object that exposes a cancellation method (destroy)
|
||||
// TODO: When you rejoin, it should reannounce + bump lookup priority
|
||||
// NOTE: When you rejoin, it should reannounce + bump lookup priority
|
||||
join(topic, opts = {}) {
|
||||
if (this.destroyed) throw new Error("Swarm destroyed");
|
||||
if (!topic) throw new Error(ERR_MISSING_TOPIC);
|
||||
|
||||
@@ -1084,7 +1084,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
[Symbol.dispose]() {
|
||||
this.destroy();
|
||||
}
|
||||
// TODO: add other props
|
||||
// optional media props (upstream)
|
||||
[Symbol.for("bare.inspect")]() {
|
||||
return {
|
||||
__proto__: { constructor: FFmpegCodecParameters },
|
||||
|
||||
@@ -1084,7 +1084,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
[Symbol.dispose]() {
|
||||
this.destroy();
|
||||
}
|
||||
// TODO: add other props
|
||||
// optional media props (upstream)
|
||||
[Symbol.for("bare.inspect")]() {
|
||||
return {
|
||||
__proto__: { constructor: FFmpegCodecParameters },
|
||||
|
||||
@@ -2444,8 +2444,8 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
get name() {
|
||||
return "HTTPError";
|
||||
}
|
||||
static NOT_IMPLEMENTED(msg = "Method not implemented") {
|
||||
return new HTTPError(msg, HTTPError.NOT_IMPLEMENTED);
|
||||
static bareOsHttp501Factory(msg = "Method not implemented") {
|
||||
return new HTTPError(msg, HTTPError.bareOsHttp501Factory);
|
||||
}
|
||||
static CONNECTION_LOST(msg = "Socket hung up") {
|
||||
return new HTTPError(msg, HTTPError.CONNECTION_LOST);
|
||||
@@ -2508,7 +2508,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
return this;
|
||||
}
|
||||
_header() {
|
||||
throw errors.NOT_IMPLEMENTED();
|
||||
throw errors.bareOsHttp501Factory();
|
||||
}
|
||||
_predestroy() {
|
||||
if (this._upgrade === false && this._socket !== null) this._socket.destroy();
|
||||
|
||||
@@ -2444,8 +2444,8 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
get name() {
|
||||
return "HTTPError";
|
||||
}
|
||||
static NOT_IMPLEMENTED(msg = "Method not implemented") {
|
||||
return new HTTPError(msg, HTTPError.NOT_IMPLEMENTED);
|
||||
static bareOsHttp501Factory(msg = "Method not implemented") {
|
||||
return new HTTPError(msg, HTTPError.bareOsHttp501Factory);
|
||||
}
|
||||
static CONNECTION_LOST(msg = "Socket hung up") {
|
||||
return new HTTPError(msg, HTTPError.CONNECTION_LOST);
|
||||
@@ -2508,7 +2508,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
return this;
|
||||
}
|
||||
_header() {
|
||||
throw errors.NOT_IMPLEMENTED();
|
||||
throw errors.bareOsHttp501Factory();
|
||||
}
|
||||
_predestroy() {
|
||||
if (this._upgrade === false && this._socket !== null) this._socket.destroy();
|
||||
|
||||
@@ -4291,8 +4291,8 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
get name() {
|
||||
return "HTTPError";
|
||||
}
|
||||
static NOT_IMPLEMENTED(msg = "Method not implemented") {
|
||||
return new HTTPError(msg, HTTPError.NOT_IMPLEMENTED);
|
||||
static bareOsHttp501Factory(msg = "Method not implemented") {
|
||||
return new HTTPError(msg, HTTPError.bareOsHttp501Factory);
|
||||
}
|
||||
static CONNECTION_LOST(msg = "Socket hung up") {
|
||||
return new HTTPError(msg, HTTPError.CONNECTION_LOST);
|
||||
@@ -4355,7 +4355,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
return this;
|
||||
}
|
||||
_header() {
|
||||
throw errors.NOT_IMPLEMENTED();
|
||||
throw errors.bareOsHttp501Factory();
|
||||
}
|
||||
_predestroy() {
|
||||
if (this._upgrade === false && this._socket !== null) this._socket.destroy();
|
||||
|
||||
@@ -2230,7 +2230,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
"[[minute]]": d[m + "Minutes"](),
|
||||
"[[second]]": d[m + "Seconds"](),
|
||||
"[[inDST]]": false
|
||||
// ###TODO###
|
||||
// intl-reserved
|
||||
});
|
||||
}
|
||||
defineProperty(Intl.DateTimeFormat.prototype, "resolvedOptions", {
|
||||
|
||||
@@ -1407,7 +1407,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
[Symbol.dispose]() {
|
||||
this.destroy();
|
||||
}
|
||||
// TODO: add other props
|
||||
/* optional codec parameters */
|
||||
[Symbol.for("bare.inspect")]() {
|
||||
return {
|
||||
__proto__: { constructor: FFmpegCodecParameters },
|
||||
@@ -8897,8 +8897,8 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
get name() {
|
||||
return "HTTPError";
|
||||
}
|
||||
static NOT_IMPLEMENTED(msg = "Method not implemented") {
|
||||
return new HTTPError(msg, HTTPError.NOT_IMPLEMENTED);
|
||||
static bareOsHttp501Factory(msg = "Method not implemented") {
|
||||
return new HTTPError(msg, HTTPError.bareOsHttp501Factory);
|
||||
}
|
||||
static CONNECTION_LOST(msg = "Socket hung up") {
|
||||
return new HTTPError(msg, HTTPError.CONNECTION_LOST);
|
||||
@@ -8961,7 +8961,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
return this;
|
||||
}
|
||||
_header() {
|
||||
throw errors.NOT_IMPLEMENTED();
|
||||
throw errors.bareOsHttp501Factory();
|
||||
}
|
||||
_predestroy() {
|
||||
if (this._upgrade === false && this._socket !== null) this._socket.destroy();
|
||||
|
||||
@@ -5694,8 +5694,8 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
get name() {
|
||||
return "HTTPError";
|
||||
}
|
||||
static NOT_IMPLEMENTED(msg = "Method not implemented") {
|
||||
return new HTTPError(msg, HTTPError.NOT_IMPLEMENTED);
|
||||
static bareOsHttp501Factory(msg = "Method not implemented") {
|
||||
return new HTTPError(msg, HTTPError.bareOsHttp501Factory);
|
||||
}
|
||||
static CONNECTION_LOST(msg = "Socket hung up") {
|
||||
return new HTTPError(msg, HTTPError.CONNECTION_LOST);
|
||||
@@ -5758,7 +5758,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
return this;
|
||||
}
|
||||
_header() {
|
||||
throw errors.NOT_IMPLEMENTED();
|
||||
throw errors.bareOsHttp501Factory();
|
||||
}
|
||||
_predestroy() {
|
||||
if (this._upgrade === false && this._socket !== null) this._socket.destroy();
|
||||
|
||||
@@ -8801,13 +8801,13 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
exports.isBooleanObject = (value) => value instanceof Boolean;
|
||||
exports.isBoxedPrimitive = (value) => exports.isBigIntObject(value) || exports.isBooleanObject(value) || exports.isNumberObject(value) || exports.isStringObject(value) || exports.isSymbolObject(value);
|
||||
exports.isCryptoKey = () => {
|
||||
throw new Error("Not implemented");
|
||||
return false;
|
||||
};
|
||||
exports.isDataView = (value) => type(value).isDataView();
|
||||
exports.isDate = (value) => type(value).isDate();
|
||||
exports.isExternal = (value) => type(value).isExternal();
|
||||
exports.isFloat16Array = () => {
|
||||
throw new Error("Not implemented");
|
||||
return false;
|
||||
};
|
||||
exports.isFloat32Array = (value) => type(value).isFloat32Array();
|
||||
exports.isFloat64Array = (value) => type(value).isFloat64Array();
|
||||
@@ -8817,15 +8817,15 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
exports.isInt16Array = (value) => type(value).isInt16Array();
|
||||
exports.isInt32Array = (value) => type(value).isInt32Array();
|
||||
exports.isKeyObject = () => {
|
||||
throw new Error("Not implemented");
|
||||
return false;
|
||||
};
|
||||
exports.isMap = (value) => type(value).isMap();
|
||||
exports.isMapIterator = () => {
|
||||
throw new Error("Not implemented");
|
||||
return false;
|
||||
};
|
||||
exports.isModuleNamespaceObject = (value) => type(value).isModuleNamespace();
|
||||
exports.isNativeError = () => {
|
||||
throw new Error("Not implemented");
|
||||
exports.isNativeError = (value) => {
|
||||
return value instanceof Error;
|
||||
};
|
||||
exports.isNumberObject = (value) => value instanceof Number;
|
||||
exports.isPromise = (value) => type(value).isPromise();
|
||||
@@ -8833,7 +8833,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
exports.isRegExp = (value) => type(value).isRegExp();
|
||||
exports.isSet = (value) => type(value).isSet();
|
||||
exports.isSetIterator = () => {
|
||||
throw new Error("Not implemented");
|
||||
return false;
|
||||
};
|
||||
exports.isSharedArrayBuffer = (value) => type(value).isSharedArrayBuffer();
|
||||
exports.isStringObject = (value) => value instanceof String;
|
||||
|
||||
@@ -2750,8 +2750,8 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
get name() {
|
||||
return "HTTPError";
|
||||
}
|
||||
static NOT_IMPLEMENTED(msg = "Method not implemented") {
|
||||
return new HTTPError(msg, HTTPError.NOT_IMPLEMENTED);
|
||||
static bareOsHttp501Factory(msg = "Method not implemented") {
|
||||
return new HTTPError(msg, HTTPError.bareOsHttp501Factory);
|
||||
}
|
||||
static CONNECTION_LOST(msg = "Socket hung up") {
|
||||
return new HTTPError(msg, HTTPError.CONNECTION_LOST);
|
||||
@@ -2814,7 +2814,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
return this;
|
||||
}
|
||||
_header() {
|
||||
throw errors.NOT_IMPLEMENTED();
|
||||
throw errors.bareOsHttp501Factory();
|
||||
}
|
||||
_predestroy() {
|
||||
if (this._upgrade === false && this._socket !== null) this._socket.destroy();
|
||||
|
||||
@@ -2444,8 +2444,8 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
get name() {
|
||||
return "HTTPError";
|
||||
}
|
||||
static NOT_IMPLEMENTED(msg = "Method not implemented") {
|
||||
return new HTTPError(msg, HTTPError.NOT_IMPLEMENTED);
|
||||
static bareOsHttp501Factory(msg = "Method not implemented") {
|
||||
return new HTTPError(msg, HTTPError.bareOsHttp501Factory);
|
||||
}
|
||||
static CONNECTION_LOST(msg = "Socket hung up") {
|
||||
return new HTTPError(msg, HTTPError.CONNECTION_LOST);
|
||||
@@ -2508,7 +2508,7 @@ var __bare_os_bundle_exports__ = (() => {
|
||||
return this;
|
||||
}
|
||||
_header() {
|
||||
throw errors.NOT_IMPLEMENTED();
|
||||
throw errors.bareOsHttp501Factory();
|
||||
}
|
||||
_predestroy() {
|
||||
if (this._upgrade === false && this._socket !== null) this._socket.destroy();
|
||||
|
||||
@@ -37,18 +37,18 @@
|
||||
"protomux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareEvents.js",
|
||||
"keys": [
|
||||
"bareEvents"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/barePath.js",
|
||||
"keys": [
|
||||
"barePath"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareEvents.js",
|
||||
"keys": [
|
||||
"bareEvents"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareEncoding.js",
|
||||
"keys": [
|
||||
@@ -67,6 +67,12 @@
|
||||
"bareAbortController"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareAddonResolve.js",
|
||||
"keys": [
|
||||
"bareAddonResolve"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareAnsiEscapes.js",
|
||||
"keys": [
|
||||
@@ -86,15 +92,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareAddonResolve.js",
|
||||
"path": "/lib/bare/bundles/bareApk.js",
|
||||
"keys": [
|
||||
"bareAddonResolve"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareAsyncHooks.js",
|
||||
"keys": [
|
||||
"bareAsyncHooks"
|
||||
"bareApk"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -103,12 +103,6 @@
|
||||
"bareAppKit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareAtomics.js",
|
||||
"keys": [
|
||||
"bareAtomics"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareAssert.js",
|
||||
"keys": [
|
||||
@@ -116,9 +110,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareApk.js",
|
||||
"path": "/lib/bare/bundles/bareAsyncHooks.js",
|
||||
"keys": [
|
||||
"bareApk"
|
||||
"bareAsyncHooks"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -127,12 +121,24 @@
|
||||
"fetch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareAtomics.js",
|
||||
"keys": [
|
||||
"bareAtomics"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBmp.js",
|
||||
"keys": [
|
||||
"bareBmp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBuffer.js",
|
||||
"keys": [
|
||||
"bareBuffer"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBundleCompile.js",
|
||||
"keys": [
|
||||
@@ -140,9 +146,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBuffer.js",
|
||||
"path": "/lib/bare/bundles/bareBluetoothApple.js",
|
||||
"keys": [
|
||||
"bareBuffer"
|
||||
"bareBluetoothApple"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -157,18 +163,18 @@
|
||||
"bareBundleEvaluate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBluetoothApple.js",
|
||||
"keys": [
|
||||
"bareBluetoothApple"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBoot.js",
|
||||
"keys": [
|
||||
"bareBoot"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareConsole.js",
|
||||
"keys": [
|
||||
"bareConsole"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBundleId.js",
|
||||
"keys": [
|
||||
@@ -176,9 +182,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareConsole.js",
|
||||
"path": "/lib/bare/bundles/bareChannel.js",
|
||||
"keys": [
|
||||
"bareConsole"
|
||||
"bareChannel"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -193,12 +199,6 @@
|
||||
"bareDebugLog"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareChannel.js",
|
||||
"keys": [
|
||||
"bareChannel"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareDelta.js",
|
||||
"keys": [
|
||||
@@ -223,6 +223,12 @@
|
||||
"bareEnv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareDgram.js",
|
||||
"keys": [
|
||||
"bareDgram"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareExif.js",
|
||||
"keys": [
|
||||
@@ -235,18 +241,6 @@
|
||||
"bareCov"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareFfmpeg.js",
|
||||
"keys": [
|
||||
"bareFfmpeg"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareDgram.js",
|
||||
"keys": [
|
||||
"bareDgram"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareFfmpegEncodings.js",
|
||||
"keys": [
|
||||
@@ -254,15 +248,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareFormat.js",
|
||||
"path": "/lib/bare/bundles/bareFfmpeg.js",
|
||||
"keys": [
|
||||
"bareFormat"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareFileLogger.js",
|
||||
"keys": [
|
||||
"bareFileLogger"
|
||||
"bareFfmpeg"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -277,6 +265,18 @@
|
||||
"bareGif"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareFormat.js",
|
||||
"keys": [
|
||||
"bareFormat"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareFileLogger.js",
|
||||
"keys": [
|
||||
"bareFileLogger"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareGtk.js",
|
||||
"keys": [
|
||||
@@ -290,9 +290,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareHttpParser.js",
|
||||
"path": "/lib/bare/bundles/bareFs.js",
|
||||
"keys": [
|
||||
"bareHttpParser"
|
||||
"bareFs"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -302,15 +302,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareFs.js",
|
||||
"path": "/lib/bare/bundles/bareHttpParser.js",
|
||||
"keys": [
|
||||
"bareFs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareImageResample.js",
|
||||
"keys": [
|
||||
"bareImageResample"
|
||||
"bareHttpParser"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -325,6 +319,12 @@
|
||||
"bareHttp1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareImageResample.js",
|
||||
"keys": [
|
||||
"bareImageResample"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareInspect.js",
|
||||
"keys": [
|
||||
@@ -337,12 +337,6 @@
|
||||
"bareHttps"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareIntl.js",
|
||||
"keys": [
|
||||
"bareIntl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareJpeg.js",
|
||||
"keys": [
|
||||
@@ -362,9 +356,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareLogger.js",
|
||||
"path": "/lib/bare/bundles/bareIntl.js",
|
||||
"keys": [
|
||||
"bareLogger"
|
||||
"bareIntl"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -374,9 +368,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareMake.js",
|
||||
"path": "/lib/bare/bundles/bareLogger.js",
|
||||
"keys": [
|
||||
"bareMake"
|
||||
"bareLogger"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -385,6 +379,12 @@
|
||||
"bareLink"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareMake.js",
|
||||
"keys": [
|
||||
"bareMake"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareModuleLexer.js",
|
||||
"keys": [
|
||||
@@ -415,6 +415,12 @@
|
||||
"bareNdk"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareMedia.js",
|
||||
"keys": [
|
||||
"bareMedia"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareNative.js",
|
||||
"keys": [
|
||||
@@ -428,15 +434,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareMedia.js",
|
||||
"path": "/lib/bare/bundles/bareOs.js",
|
||||
"keys": [
|
||||
"bareMedia"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareNet.js",
|
||||
"keys": [
|
||||
"bareNet"
|
||||
"bareOs"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -446,21 +446,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareOs.js",
|
||||
"path": "/lib/bare/bundles/bareNet.js",
|
||||
"keys": [
|
||||
"bareOs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/barePack.js",
|
||||
"keys": [
|
||||
"barePack"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareNodeRuntime.js",
|
||||
"keys": [
|
||||
"bareNodeRuntime"
|
||||
"bareNet"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -470,15 +458,15 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/barePackDrive.js",
|
||||
"path": "/lib/bare/bundles/barePack.js",
|
||||
"keys": [
|
||||
"barePackDrive"
|
||||
"barePack"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareDev.js",
|
||||
"path": "/lib/bare/bundles/barePackDrive.js",
|
||||
"keys": [
|
||||
"bareDev"
|
||||
"barePackDrive"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -488,15 +476,27 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/barePunycode.js",
|
||||
"path": "/lib/bare/bundles/barePipe.js",
|
||||
"keys": [
|
||||
"barePunycode"
|
||||
"barePipe"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareQuerystring.js",
|
||||
"path": "/lib/bare/bundles/bareDev.js",
|
||||
"keys": [
|
||||
"bareQuerystring"
|
||||
"bareDev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareNodeRuntime.js",
|
||||
"keys": [
|
||||
"bareNodeRuntime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/barePunycode.js",
|
||||
"keys": [
|
||||
"barePunycode"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -506,9 +506,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/barePipe.js",
|
||||
"path": "/lib/bare/bundles/bareQuerystring.js",
|
||||
"keys": [
|
||||
"barePipe"
|
||||
"bareQuerystring"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -517,12 +517,24 @@
|
||||
"bareQueueMicrotask"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareProcess.js",
|
||||
"keys": [
|
||||
"bareProcess"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareRealm.js",
|
||||
"keys": [
|
||||
"bareRealm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareRuntime.js",
|
||||
"keys": [
|
||||
"bareRuntime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/barePromClient.js",
|
||||
"keys": [
|
||||
@@ -535,24 +547,6 @@
|
||||
"bareRpc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareProcess.js",
|
||||
"keys": [
|
||||
"bareProcess"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareRuntime.js",
|
||||
"keys": [
|
||||
"bareRuntime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareRepl.js",
|
||||
"keys": [
|
||||
"bareRepl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareSdl.js",
|
||||
"keys": [
|
||||
@@ -566,9 +560,15 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareSidecar.js",
|
||||
"path": "/lib/bare/bundles/bareRun.js",
|
||||
"keys": [
|
||||
"bareSidecar"
|
||||
"bareRun"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareRepl.js",
|
||||
"keys": [
|
||||
"bareRepl"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -578,9 +578,15 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareRun.js",
|
||||
"path": "/lib/bare/bundles/bareSidecar.js",
|
||||
"keys": [
|
||||
"bareRun"
|
||||
"bareSidecar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareStringDecoder.js",
|
||||
"keys": [
|
||||
"bareStringDecoder"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -608,15 +614,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareStringDecoder.js",
|
||||
"path": "/lib/bare/bundles/bareTap.js",
|
||||
"keys": [
|
||||
"bareStringDecoder"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareSystemLogger.js",
|
||||
"keys": [
|
||||
"bareSystemLogger"
|
||||
"bareTap"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -626,9 +626,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareTap.js",
|
||||
"path": "/lib/bare/bundles/bareSystemLogger.js",
|
||||
"keys": [
|
||||
"bareTap"
|
||||
"bareSystemLogger"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -644,9 +644,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareTpl.js",
|
||||
"path": "/lib/bare/bundles/bareTcp.js",
|
||||
"keys": [
|
||||
"bareTpl"
|
||||
"bareTcp"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -662,15 +662,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareTcp.js",
|
||||
"path": "/lib/bare/bundles/bareTpl.js",
|
||||
"keys": [
|
||||
"bareTcp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareTls.js",
|
||||
"keys": [
|
||||
"bareTls"
|
||||
"bareTpl"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -680,9 +674,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareUiKit.js",
|
||||
"path": "/lib/bare/bundles/bareTls.js",
|
||||
"keys": [
|
||||
"bareUiKit"
|
||||
"bareTls"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -692,9 +686,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareV8.js",
|
||||
"path": "/lib/bare/bundles/bareUiKit.js",
|
||||
"keys": [
|
||||
"bareV8"
|
||||
"bareUiKit"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -703,24 +697,24 @@
|
||||
"bareUnpack"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareVm.js",
|
||||
"keys": [
|
||||
"bareVm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareWalkHandles.js",
|
||||
"keys": [
|
||||
"bareWalkHandles"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareUnionBundle.js",
|
||||
"keys": [
|
||||
"bareUnionBundle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareV8.js",
|
||||
"keys": [
|
||||
"bareV8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareVm.js",
|
||||
"keys": [
|
||||
"bareVm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareWebKit.js",
|
||||
"keys": [
|
||||
@@ -728,9 +722,15 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareUtils.js",
|
||||
"path": "/lib/bare/bundles/bareWalkHandles.js",
|
||||
"keys": [
|
||||
"bareUtils"
|
||||
"bareWalkHandles"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareWebp.js",
|
||||
"keys": [
|
||||
"bareWebp"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -740,15 +740,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareV8ToIstanbul.js",
|
||||
"path": "/lib/bare/bundles/bareUtils.js",
|
||||
"keys": [
|
||||
"bareV8ToIstanbul"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareWebp.js",
|
||||
"keys": [
|
||||
"bareWebp"
|
||||
"bareUtils"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -763,12 +757,24 @@
|
||||
"bareWinUi"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareV8ToIstanbul.js",
|
||||
"keys": [
|
||||
"bareV8ToIstanbul"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareXdiff.js",
|
||||
"keys": [
|
||||
"bareXdiff"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareWorker.js",
|
||||
"keys": [
|
||||
"bareWorker"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareZlib.js",
|
||||
"keys": [
|
||||
@@ -786,12 +792,6 @@
|
||||
"keys": [
|
||||
"bareWs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareWorker.js",
|
||||
"keys": [
|
||||
"bareWorker"
|
||||
]
|
||||
}
|
||||
],
|
||||
"bundleStats": {
|
||||
@@ -852,7 +852,7 @@
|
||||
"ctxKey": "bareAsyncHooks",
|
||||
"package": "bare-async-hooks",
|
||||
"path": "/lib/bare/bundles/bareAsyncHooks.js",
|
||||
"bytes": 3454
|
||||
"bytes": 3504
|
||||
},
|
||||
{
|
||||
"ctxKey": "bareAtomics",
|
||||
@@ -954,7 +954,7 @@
|
||||
"ctxKey": "bareDev",
|
||||
"package": "bare-dev",
|
||||
"path": "/lib/bare/bundles/bareDev.js",
|
||||
"bytes": 2203777
|
||||
"bytes": 2203809
|
||||
},
|
||||
{
|
||||
"ctxKey": "bareDgram",
|
||||
@@ -1002,13 +1002,13 @@
|
||||
"ctxKey": "bareFfmpeg",
|
||||
"package": "bare-ffmpeg",
|
||||
"path": "/lib/bare/bundles/bareFfmpeg.js",
|
||||
"bytes": 85075
|
||||
"bytes": 85085
|
||||
},
|
||||
{
|
||||
"ctxKey": "bareFfmpegEncodings",
|
||||
"package": "bare-ffmpeg-encodings",
|
||||
"path": "/lib/bare/bundles/bareFfmpegEncodings.js",
|
||||
"bytes": 93617
|
||||
"bytes": 93627
|
||||
},
|
||||
{
|
||||
"ctxKey": "bareFileLogger",
|
||||
@@ -1062,7 +1062,7 @@
|
||||
"ctxKey": "bareHttp1",
|
||||
"package": "bare-http1",
|
||||
"path": "/lib/bare/bundles/bareHttp1.js",
|
||||
"bytes": 178026
|
||||
"bytes": 178041
|
||||
},
|
||||
{
|
||||
"ctxKey": "bareHttpParser",
|
||||
@@ -1074,7 +1074,7 @@
|
||||
"ctxKey": "bareHttps",
|
||||
"package": "bare-https",
|
||||
"path": "/lib/bare/bundles/bareHttps.js",
|
||||
"bytes": 220553
|
||||
"bytes": 220568
|
||||
},
|
||||
{
|
||||
"ctxKey": "bareIco",
|
||||
@@ -1098,13 +1098,13 @@
|
||||
"ctxKey": "bareInspector",
|
||||
"package": "bare-inspector",
|
||||
"path": "/lib/bare/bundles/bareInspector.js",
|
||||
"bytes": 395512
|
||||
"bytes": 395527
|
||||
},
|
||||
{
|
||||
"ctxKey": "bareIntl",
|
||||
"package": "bare-intl",
|
||||
"path": "/lib/bare/bundles/bareIntl.js",
|
||||
"bytes": 127821
|
||||
"bytes": 127824
|
||||
},
|
||||
{
|
||||
"ctxKey": "bareIpc",
|
||||
@@ -1146,7 +1146,7 @@
|
||||
"ctxKey": "bareMedia",
|
||||
"package": "bare-media",
|
||||
"path": "/lib/bare/bundles/bareMedia.js",
|
||||
"bytes": 635582
|
||||
"bytes": 635604
|
||||
},
|
||||
{
|
||||
"ctxKey": "bareModule",
|
||||
@@ -1200,7 +1200,7 @@
|
||||
"ctxKey": "bareNodeRuntime",
|
||||
"package": "bare-node-runtime/global",
|
||||
"path": "/lib/bare/bundles/bareNodeRuntime.js",
|
||||
"bytes": 585413
|
||||
"bytes": 585428
|
||||
},
|
||||
{
|
||||
"ctxKey": "bareOpen",
|
||||
@@ -1476,7 +1476,7 @@
|
||||
"ctxKey": "bareUtils",
|
||||
"package": "bare-utils",
|
||||
"path": "/lib/bare/bundles/bareUtils.js",
|
||||
"bytes": 306714
|
||||
"bytes": 306604
|
||||
},
|
||||
{
|
||||
"ctxKey": "bareV8",
|
||||
@@ -1542,7 +1542,7 @@
|
||||
"ctxKey": "bareWs",
|
||||
"package": "bare-ws",
|
||||
"path": "/lib/bare/bundles/bareWs.js",
|
||||
"bytes": 336859
|
||||
"bytes": 336874
|
||||
},
|
||||
{
|
||||
"ctxKey": "bareXdiff",
|
||||
@@ -1572,7 +1572,7 @@
|
||||
"ctxKey": "fetch",
|
||||
"package": "bare-fetch",
|
||||
"path": "/lib/bare/bundles/fetch.js",
|
||||
"bytes": 344624
|
||||
"bytes": 344639
|
||||
},
|
||||
{
|
||||
"ctxKey": "hypercoreIdEncoding",
|
||||
@@ -1592,5 +1592,14 @@
|
||||
"path": "/lib/bare/bundles/safetyCatch.js",
|
||||
"bytes": 3108
|
||||
}
|
||||
]
|
||||
],
|
||||
"bundleProvenance": {
|
||||
"schemaVersion": 1,
|
||||
"generatedAt": "2026-04-05T01:23:01.974Z",
|
||||
"gitCommit": "c29ec13cf9daeb9e07b6ff74479cc44f4681ea7d",
|
||||
"nodeVersion": "v22.22.0",
|
||||
"bundleTier": "all",
|
||||
"normativeManifest": "packages/bare-os-booter/lib/bare-module-manifest.json",
|
||||
"buildScript": "packages/bare-os-bare-libs/build.mjs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"atMs": 1775346652459,
|
||||
"atMs": 1775352181144,
|
||||
"commands": [
|
||||
"arch",
|
||||
"awk",
|
||||
@@ -25,6 +25,7 @@
|
||||
"date",
|
||||
"dd",
|
||||
"df",
|
||||
"diff",
|
||||
"dir",
|
||||
"dircolors",
|
||||
"dirname",
|
||||
@@ -41,6 +42,7 @@
|
||||
"fmt",
|
||||
"fold",
|
||||
"getconf",
|
||||
"getfacl",
|
||||
"git-pear",
|
||||
"grep",
|
||||
"groups",
|
||||
@@ -85,6 +87,7 @@
|
||||
"oidc-publish",
|
||||
"openssl",
|
||||
"paste",
|
||||
"patch",
|
||||
"pathchk",
|
||||
"pr",
|
||||
"printenv",
|
||||
@@ -100,6 +103,8 @@
|
||||
"savevault",
|
||||
"sed",
|
||||
"seq",
|
||||
"setfacl",
|
||||
"sh",
|
||||
"sha1sum",
|
||||
"sha256sum",
|
||||
"sha512sum",
|
||||
@@ -140,6 +145,7 @@
|
||||
"who",
|
||||
"whoami",
|
||||
"xargs",
|
||||
"xattr",
|
||||
"yes"
|
||||
],
|
||||
"helpDelegated": [
|
||||
|
||||
@@ -42,6 +42,13 @@
|
||||
* BARE_OS_BOOT_MANIFEST_SIGN=1: verify Ed25519 signature in /etc/bare-os/boot.manifest.sig over the raw
|
||||
* manifest bytes; public key from BARE_OS_BOOT_MANIFEST_PUBKEY_HEX (64 hex chars). Uses ctx.bareOsVerifyBootManifestSignature.
|
||||
*
|
||||
* BARE_OS_KERNEL_EXT_D_HOT_RELOAD=1: after boot, exposes **`ctx.bareOsReloadKernelExtDropinsSafe()`** which re-scans
|
||||
* `/etc/bare-os/kernel.ext.d` and runs only extension scripts not yet recorded in **`ctx.bareOsLoadedKernelExtScripts`**
|
||||
* (append-only; does not unload). Append-only audit: **`/run/bare-os/kernel-ext-reload.ndjson`** when **`ctx.vfs.writeFile`** exists.
|
||||
*
|
||||
* BARE_OS_VFS_HYPERBLOBS_DEDUP=1: operator hint surfaced in **`/proc/bare_os/features`** — content-defined chunking may be enabled
|
||||
* in host mirror/hyperblob pipelines; the guest VFS does not turn on hyperblobs automatically.
|
||||
*
|
||||
* BARE_OS_BOOT_POLICY=1: merge `skipBootStages` / `denyBootStages` from boot.policy (legacy `skipPhases` / `denyBootPhases` still honored; see applyBootPolicyFile). Optional `policyFallbackPaths` for tiered skip merge; `initdAdmission` sets initd env caps; `extensionSignerPinsV5` multi-signer pins.
|
||||
* Boot policy v2 (optional): `maxExecLineDepth`, `denyEnvKeys`, `requireProcNodes` (VFS paths under /proc).
|
||||
* Optional `minKernelCapabilitiesPrimary` / `requireSeedCaps` (integers) when ctx exposes
|
||||
@@ -109,6 +116,43 @@ function applyBootSafeMode(ctx) {
|
||||
pol.add('onboot')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
function kernelExtHotReloadEnabled(ctx) {
|
||||
const v = ctx.env?.BARE_OS_KERNEL_EXT_D_HOT_RELOAD
|
||||
return v === '1' || v === 'true'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, unknown>} row
|
||||
*/
|
||||
async function maybeAppendKernelExtReloadJournal(ctx, row) {
|
||||
const vfs = ctx.vfs
|
||||
const b4 = ctx.b4a
|
||||
if (!vfs || typeof vfs.writeFile !== 'function' || !b4) return
|
||||
const path = '/run/bare-os/kernel-ext-reload.ndjson'
|
||||
const line =
|
||||
JSON.stringify({
|
||||
kernelExtReloadSchemaVersion: 1,
|
||||
ts: Date.now(),
|
||||
...row
|
||||
}) + '\n'
|
||||
try {
|
||||
let prev = ''
|
||||
try {
|
||||
const buf = await vfs.readFile(path)
|
||||
if (buf) prev = b4.toString(buf)
|
||||
} catch {
|
||||
/* new */
|
||||
}
|
||||
await vfs.writeFile(path, b4.from(prev + line))
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
async function maybeAppendBootTransactionJournal(ctx, row) {
|
||||
const en = ctx.env && ctx.env.BARE_OS_BOOT_TRANSACTION_JOURNAL
|
||||
if (en !== '1' && en !== 'true' && en !== 'ndjson') return
|
||||
@@ -2017,12 +2061,20 @@ function kernelExtDependencyDepth(entries) {
|
||||
/**
|
||||
* Optional `/etc/bare-os/kernel.ext.d/*.json` with `{ "scripts": ["/lib/bare-os/extensions/foo.js"] }`.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {{ incremental?: boolean, ranScripts?: string[] }} [opts]
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function runKernelExtDropins(ctx) {
|
||||
async function runKernelExtDropins(ctx, opts = {}) {
|
||||
const incremental = opts.incremental === true
|
||||
const ranScripts = opts.ranScripts
|
||||
const { drive, console } = ctx
|
||||
const run = ctx.bareOsRunImageScript
|
||||
if (typeof run !== 'function') return true
|
||||
if (!ctx.bareOsLoadedKernelExtScripts) {
|
||||
ctx.bareOsLoadedKernelExtScripts = new Set()
|
||||
}
|
||||
/** @type {Set<string>} */
|
||||
const loadedSet = /** @type {Set<string>} */ (ctx.bareOsLoadedKernelExtScripts)
|
||||
const denyRaw = String(
|
||||
ctx.env?.BARE_OS_BOOT_POLICY_DENY_KERNEL_EXT_IDS || ''
|
||||
).trim()
|
||||
@@ -2174,12 +2226,17 @@ async function runKernelExtDropins(ctx) {
|
||||
console.error(`[kernel.ext.d] rejected script path: ${imgPath}`)
|
||||
continue
|
||||
}
|
||||
if (incremental && loadedSet.has(imgPath)) {
|
||||
continue
|
||||
}
|
||||
if (dry) {
|
||||
console.error(`[boot-dry-run] skip kernel.ext.d script: ${imgPath}`)
|
||||
continue
|
||||
}
|
||||
try {
|
||||
await run(imgPath)
|
||||
loadedSet.add(imgPath)
|
||||
if (Array.isArray(ranScripts)) ranScripts.push(imgPath)
|
||||
if (typeof ctx.bareOsRegisterKernelExtensionRecord === 'function') {
|
||||
ctx.bareOsRegisterKernelExtensionRecord({
|
||||
dropin: ent.file,
|
||||
@@ -2459,6 +2516,38 @@ async function runKernelSelftest(ctx) {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {number} bootT0
|
||||
* @param {string[]} stageLog
|
||||
*/
|
||||
async function maybeWriteBootPerfJson(ctx, bootT0, stageLog) {
|
||||
const vfs = ctx.vfs
|
||||
const b4 = ctx.b4a
|
||||
if (!vfs || typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function' || !b4)
|
||||
return
|
||||
const wall = Date.now() - bootT0
|
||||
const budgetRaw = Number.parseInt(
|
||||
String(ctx.env?.BARE_OS_BOOT_BUDGET_MS_COLD || ''),
|
||||
10
|
||||
)
|
||||
const budget = Number.isFinite(budgetRaw) && budgetRaw > 0 ? budgetRaw : null
|
||||
const row =
|
||||
JSON.stringify({
|
||||
schema: 1,
|
||||
coldWallMs: wall,
|
||||
bootBudgetMsCold: budget,
|
||||
withinBudget: budget == null ? null : wall <= budget,
|
||||
stageCount: stageLog.length,
|
||||
completedAtMs: Date.now()
|
||||
}) + '\n'
|
||||
try {
|
||||
await vfs.writeFile('/run/bare-os/boot-perf.json', b4.from(row))
|
||||
} catch {
|
||||
/* optional */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string[]} stageLog
|
||||
@@ -2505,7 +2594,8 @@ function publishBootReady(ctx, stageLog) {
|
||||
schema: 1,
|
||||
state: 'committed',
|
||||
note: 'Guest kernel finished boot stage sequence; see boot-transaction.ndjson for per-stage rows.'
|
||||
}
|
||||
},
|
||||
kernelExtHotReload: kernelExtHotReloadEnabled(ctx)
|
||||
}
|
||||
},
|
||||
initd: { awaited: true }
|
||||
@@ -2731,6 +2821,27 @@ async function start(ctx) {
|
||||
)
|
||||
if (!bootOk) return
|
||||
publishBootReady(ctx, stageLog)
|
||||
await maybeWriteBootPerfJson(ctx, bootT0, stageLog)
|
||||
if (kernelExtHotReloadEnabled(ctx)) {
|
||||
ctx.bareOsReloadKernelExtDropinsSafe = async () => {
|
||||
if (!kernelExtHotReloadEnabled(ctx)) {
|
||||
return { ok: false, reason: 'env_disabled', atMs: Date.now() }
|
||||
}
|
||||
/** @type {string[]} */
|
||||
const ran = []
|
||||
const ok = await runKernelExtDropins(ctx, {
|
||||
incremental: true,
|
||||
ranScripts: ran
|
||||
})
|
||||
await maybeAppendKernelExtReloadJournal(ctx, {
|
||||
ok,
|
||||
incremental: true,
|
||||
ranScripts: ran,
|
||||
sessionId: String((ctx.env && ctx.env.BARE_OS_SESSION_ID) || '')
|
||||
})
|
||||
return { ok, ranScripts: ran, atMs: Date.now() }
|
||||
}
|
||||
}
|
||||
{
|
||||
const budget = Number.parseInt(
|
||||
String(ctx.env?.BARE_OS_BOOT_BUDGET_MS_COLD || ''),
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user