Holesail Service Daemon + Bin Client
This commit is contained in:
@@ -63,6 +63,7 @@ import { runOpensslCli } from './lib/openssl-cli.js'
|
||||
import { runSshKeygenCli } from './lib/ssh-keygen-cli.js'
|
||||
import { runTarCli } from './lib/tar-cli.js'
|
||||
import { runSystemctlCli } from './lib/systemctl-cli.js'
|
||||
import { runHolesailCli } from './lib/holesail-cli.js'
|
||||
import { createBareOsDiskOsBridge } from './lib/bare-os-disk-os-bridge.js'
|
||||
import { buildBareOsHyperbeeGuestHint } from './lib/bare-os-hyperbee-guest-hint.js'
|
||||
import { invokeBareOsPearUpdaterDelegate } from './lib/bare-os-pear-updater-bridge.js'
|
||||
@@ -193,6 +194,7 @@ import {
|
||||
bareOsListThemeNames
|
||||
} from './lib/bare-os-theme-presets.js'
|
||||
import './lib/bare-cron.js'
|
||||
import { maybeStartBareHolesailKernelFromBooter } from './lib/bare-holesail.js'
|
||||
import { runSshdCli, getBareOpensshProcJsonText } from './lib/bare-openssh.js'
|
||||
import { createHash } from 'bare-crypto'
|
||||
import { buildBareOsHostProcJsonText } from './lib/bare-os-host-proc-snapshot.js'
|
||||
@@ -733,6 +735,27 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
'BARE_OS_PROC_RLIMITS_JSON',
|
||||
'BARE_OS_SUBPROCESS_BRIDGE_UID_GID_MAP_JSON',
|
||||
'BARE_OS_HDMS_VAULT_ROTATE_COUNT',
|
||||
'BARE_OS_HOLESAIL_INITD',
|
||||
'BARE_OS_HOLESAIL_MANAGED',
|
||||
'BARE_OS_HOLESAIL_SERVER',
|
||||
'BARE_OS_HOLESAIL_CLIENT',
|
||||
'BARE_OS_HOLESAIL_KEY',
|
||||
'BARE_OS_HOLESAIL_SECURE',
|
||||
'BARE_OS_HOLESAIL_PORT',
|
||||
'BARE_OS_HOLESAIL_HOST',
|
||||
'BARE_OS_HOLESAIL_UDP',
|
||||
'BARE_OS_HOLESAIL_LOG',
|
||||
'BARE_OS_HOLESAIL_DEBUG',
|
||||
'BARE_OS_HOLESAIL_STATE',
|
||||
'BARE_OS_HOLESAIL_KERNEL',
|
||||
'BARE_OS_HOLESAIL_KERNEL_SERVER',
|
||||
'BARE_OS_HOLESAIL_KERNEL_CLIENT',
|
||||
'BARE_OS_HOLESAIL_KERNEL_KEY',
|
||||
'BARE_OS_HOLESAIL_KERNEL_SECURE',
|
||||
'BARE_OS_HOLESAIL_KERNEL_PORT',
|
||||
'BARE_OS_HOLESAIL_KERNEL_HOST',
|
||||
'BARE_OS_HOLESAIL_KERNEL_UDP',
|
||||
'BARE_OS_HOLESAIL_KERNEL_LOG',
|
||||
'BARE_OS_SUBPROCESS_BRIDGE_JOBS_JSON',
|
||||
'BARE_OS_SUBPROCESS_BRIDGE_META_JSON',
|
||||
'BARE_OS_HOST_BARE_OS_PROC',
|
||||
@@ -777,6 +800,18 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
shellEnv.COLORTERM = String(hostEnv.COLORTERM)
|
||||
}
|
||||
}
|
||||
if (
|
||||
shellEnv.BARE_OS_HOLESAIL_INITD === undefined ||
|
||||
shellEnv.BARE_OS_HOLESAIL_INITD === ''
|
||||
) {
|
||||
shellEnv.BARE_OS_HOLESAIL_INITD = '1'
|
||||
}
|
||||
if (
|
||||
shellEnv.BARE_OS_HOLESAIL_MANAGED === undefined ||
|
||||
shellEnv.BARE_OS_HOLESAIL_MANAGED === ''
|
||||
) {
|
||||
shellEnv.BARE_OS_HOLESAIL_MANAGED = '1'
|
||||
}
|
||||
let bootProfileResolved = ''
|
||||
const bpfEarly = shellEnv.BARE_OS_BOOT_PROFILE
|
||||
if (bpfEarly != null && String(bpfEarly).trim()) {
|
||||
@@ -8585,6 +8620,10 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
async bareOsRunSystemctlCli(argv) {
|
||||
return runSystemctlCli(ctx, argv)
|
||||
},
|
||||
/** Persisted Holesail tunnels (/bin/holesail); requires managed bare-holesail for live start. */
|
||||
async bareOsRunHolesailCli(argv) {
|
||||
return runHolesailCli(ctx, argv)
|
||||
},
|
||||
bareOsEmitPearStageHint(payload = {}) {
|
||||
if (typeof globalThis.process?.emit !== 'function') return false
|
||||
try {
|
||||
@@ -9588,6 +9627,8 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
|
||||
emitBooterBootPhase('repl')
|
||||
|
||||
await maybeStartBareHolesailKernelFromBooter(ctx)
|
||||
|
||||
await startBareInitd(ctx)
|
||||
{
|
||||
const active = listBareServices()
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Shared env parsing for bare-holesail / managed tunnels.
|
||||
* @param {unknown} v
|
||||
*/
|
||||
export function bareHolesailEnvTruthy(v) {
|
||||
const s = String(v ?? '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
return s === '1' || s === 'true' || s === 'yes'
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Resolve the upstream `holesail` class for bare-holesail / managed tunnels.
|
||||
*
|
||||
* This follows the same pattern as `bare-openssh` / `bare-ssh2`: keep `holesail`
|
||||
* as a booter-owned dependency and import it directly so Pear stages the package.
|
||||
* Upstream is AGPL-3.0.
|
||||
*/
|
||||
|
||||
import Holesail from 'holesail'
|
||||
|
||||
/** ESM import so Pear stages `holesail` and its traced package deps. */
|
||||
let holesailCache = null
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @returns {Promise<new (opts?: object) => { ready: () => Promise<void>, close: () => Promise<void>, pause?: () => Promise<void>, resume?: () => Promise<void>, info?: unknown }>}
|
||||
*/
|
||||
export async function loadHolesailConstructor(ctx) {
|
||||
void ctx
|
||||
if (!holesailCache) {
|
||||
const Ho = Holesail && Holesail.default ? Holesail.default : Holesail
|
||||
if (typeof Ho !== 'function') {
|
||||
throw new Error('holesail: expected default export to be a constructor')
|
||||
}
|
||||
holesailCache = Ho
|
||||
}
|
||||
|
||||
return holesailCache
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
import { dirname } from 'node:path'
|
||||
|
||||
/**
|
||||
* Multi-tunnel Holesail manager: persisted state on the VFS, one Holesail instance per entry.
|
||||
* Used when managed mode is on (stock default with BARE_OS_HOLESAIL_INITD/MANAGED).
|
||||
*
|
||||
* State path: BARE_OS_HOLESAIL_STATE or default ~/.holesail/state.json (VFS expands ~/ under $HOME).
|
||||
* Upstream holesail is AGPL-3.0.
|
||||
*/
|
||||
import { bareHolesailEnvTruthy } from './bare-holesail-env.js'
|
||||
import { loadHolesailConstructor } from './bare-holesail-loader.js'
|
||||
import { appendVarLog, BARE_OS_VAR_LOG_DIR } from './bare-os-var-log.js'
|
||||
|
||||
export const BARE_HOLESAIL_STATE_DIR = '~/.holesail'
|
||||
export const BARE_HOLESAIL_STATE_DEFAULT = `${BARE_HOLESAIL_STATE_DIR}/state.json`
|
||||
|
||||
const HOLESAIL_LOG = `${BARE_OS_VAR_LOG_DIR}/holesail.log`
|
||||
|
||||
/** @type {Map<string, { hs: unknown, entry: Record<string, unknown> }>} */
|
||||
const managedInstances = new Map()
|
||||
|
||||
/** @type {(() => void)[]} */
|
||||
let managedHookUnsubs = []
|
||||
|
||||
let managedServiceRunning = false
|
||||
|
||||
/**
|
||||
* @param {Record<string, string | undefined>} env
|
||||
*/
|
||||
export function bareHolesailManagedStatePath(env) {
|
||||
const raw = String(env.BARE_OS_HOLESAIL_STATE || '').trim()
|
||||
return raw || BARE_HOLESAIL_STATE_DEFAULT
|
||||
}
|
||||
|
||||
function logLine(ctx, line) {
|
||||
void appendVarLog(ctx, HOLESAIL_LOG, 'holesail', line)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} dir
|
||||
*/
|
||||
async function ensureDir(ctx, dir) {
|
||||
const vfs = ctx.vfs
|
||||
if (!vfs || typeof vfs.mkdir !== 'function') return
|
||||
try {
|
||||
await vfs.mkdir(dir, { recursive: true })
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} hs
|
||||
*/
|
||||
async function safeCloseHolesail(hs) {
|
||||
if (!hs || typeof hs.close !== 'function') return
|
||||
try {
|
||||
await hs.close()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
export function bareHolesailManagedValidateId(id) {
|
||||
const s = String(id || '').trim()
|
||||
if (!s || s.length > 64) return { ok: false, err: 'invalid id (1–64 chars)' }
|
||||
if (!/^[a-zA-Z0-9._-]+$/.test(s))
|
||||
return { ok: false, err: 'invalid id (use [a-zA-Z0-9._-])' }
|
||||
return { ok: true, id: s }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} raw
|
||||
* @returns {{ ok: true, entry: Record<string, unknown> } | { ok: false, err: string }}
|
||||
*/
|
||||
export function bareHolesailManagedNormalizeEntry(raw) {
|
||||
if (!raw || typeof raw !== 'object')
|
||||
return { ok: false, err: 'connection entry must be an object' }
|
||||
const server = bareHolesailEnvTruthy(
|
||||
/** @type {Record<string, unknown>} */ (raw).server
|
||||
)
|
||||
const client = bareHolesailEnvTruthy(
|
||||
/** @type {Record<string, unknown>} */ (raw).client
|
||||
)
|
||||
if (server === client) {
|
||||
return { ok: false, err: 'set exactly one of server or client' }
|
||||
}
|
||||
/** @type {Record<string, unknown>} */
|
||||
const entry = { server, client }
|
||||
const key = String(raw.key ?? '').trim()
|
||||
if (client && !key) return { ok: false, err: 'client requires key' }
|
||||
|
||||
if (key) entry.key = key
|
||||
|
||||
if (raw.secure !== undefined && raw.secure !== null && String(raw.secure) !== '')
|
||||
entry.secure = bareHolesailEnvTruthy(raw.secure)
|
||||
|
||||
if (raw.port != null && raw.port !== '') {
|
||||
const n = Number.parseInt(String(raw.port), 10)
|
||||
if (!Number.isFinite(n)) return { ok: false, err: 'invalid port' }
|
||||
entry.port = n
|
||||
}
|
||||
|
||||
const host = String(raw.host ?? '').trim()
|
||||
if (host) entry.host = host
|
||||
|
||||
if (raw.udp !== undefined && raw.udp !== null && String(raw.udp) !== '')
|
||||
entry.udp = bareHolesailEnvTruthy(raw.udp)
|
||||
|
||||
if (raw.log !== undefined && raw.log !== null && raw.log !== '') {
|
||||
if (typeof raw.log === 'number') entry.log = raw.log
|
||||
else if (/^\d+$/.test(String(raw.log)))
|
||||
entry.log = Number.parseInt(String(raw.log), 10)
|
||||
else entry.log = bareHolesailEnvTruthy(raw.log)
|
||||
}
|
||||
|
||||
const enabled =
|
||||
raw.enabled === undefined || raw.enabled === null
|
||||
? true
|
||||
: bareHolesailEnvTruthy(raw.enabled)
|
||||
entry.enabled = enabled
|
||||
|
||||
return { ok: true, entry }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} parsed
|
||||
*/
|
||||
function coerceState(parsed) {
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return { version: 1, connections: {} }
|
||||
}
|
||||
const o = /** @type {Record<string, unknown>} */ (parsed)
|
||||
const conns = o.connections
|
||||
const connections =
|
||||
conns && typeof conns === 'object' && !Array.isArray(conns)
|
||||
? /** @type {Record<string, unknown>} */ (conns)
|
||||
: {}
|
||||
return { version: 1, connections }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, string | undefined>} env
|
||||
*/
|
||||
export async function bareHolesailManagedReadState(ctx, env) {
|
||||
const path = bareHolesailManagedStatePath(env)
|
||||
const vfs = ctx.vfs
|
||||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||||
return { path, state: { version: 1, connections: {} } }
|
||||
}
|
||||
try {
|
||||
const buf = await vfs.readFile(path)
|
||||
const text = ctx.b4a ? ctx.b4a.toString(buf, 'utf8') : Buffer.from(buf).toString('utf8')
|
||||
const parsed = JSON.parse(text)
|
||||
return { path, state: coerceState(parsed) }
|
||||
} catch {
|
||||
return { path, state: { version: 1, connections: {} } }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, string | undefined>} env
|
||||
* @param {{ version: number, connections: Record<string, unknown> }} state
|
||||
*/
|
||||
export async function bareHolesailManagedWriteState(ctx, env, state) {
|
||||
const path = bareHolesailManagedStatePath(env)
|
||||
const vfs = ctx.vfs
|
||||
if (!vfs || typeof vfs.writeFile !== 'function') {
|
||||
throw new Error('holesail: vfs.writeFile unavailable')
|
||||
}
|
||||
await ensureDir(ctx, dirname(path))
|
||||
const body = JSON.stringify(state, null, 2) + '\n'
|
||||
await vfs.writeFile(path, ctx.b4a.from(body))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, string | undefined>} env
|
||||
*/
|
||||
export function bareHolesailManagedDebugEnabled(ctx, env) {
|
||||
return bareHolesailEnvTruthy(env.BARE_OS_HOLESAIL_DEBUG)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} id
|
||||
* @param {Record<string, unknown>} entry
|
||||
*/
|
||||
async function startManagedInstance(ctx, env, id, entry) {
|
||||
if (managedInstances.has(id)) return
|
||||
const norm = bareHolesailManagedNormalizeEntry(entry)
|
||||
if (!norm.ok) throw new Error(norm.err)
|
||||
if (!norm.entry.enabled) return
|
||||
|
||||
const Holesail = await loadHolesailConstructor(ctx)
|
||||
const hs = new Holesail(norm.entry)
|
||||
await hs.ready()
|
||||
managedInstances.set(id, { hs, entry: norm.entry })
|
||||
logLine(ctx, `managed: started ${id}`)
|
||||
if (bareHolesailManagedDebugEnabled(ctx, env)) {
|
||||
try {
|
||||
const info = hs.info
|
||||
const url = info && typeof info === 'object' ? info.url : ''
|
||||
ctx.console?.log?.(`[bare-holesail] managed ${id} url=${String(url).slice(0, 120)}`)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} id
|
||||
*/
|
||||
async function stopManagedInstance(ctx, id) {
|
||||
const rec = managedInstances.get(id)
|
||||
if (!rec) return
|
||||
managedInstances.delete(id)
|
||||
await safeCloseHolesail(rec.hs)
|
||||
logLine(ctx, `managed: stopped ${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, string | undefined>} env
|
||||
*/
|
||||
export async function bareHolesailManagedStartService(ctx, env) {
|
||||
if (managedServiceRunning) return
|
||||
|
||||
const { state } = await bareHolesailManagedReadState(ctx, env)
|
||||
managedServiceRunning = true
|
||||
|
||||
for (const [id, raw] of Object.entries(state.connections)) {
|
||||
const idOk = bareHolesailManagedValidateId(id)
|
||||
if (!idOk.ok) {
|
||||
logLine(ctx, `managed: skip invalid id ${id}: ${idOk.err}`)
|
||||
continue
|
||||
}
|
||||
const norm = bareHolesailManagedNormalizeEntry(raw)
|
||||
if (!norm.ok) {
|
||||
logLine(ctx, `managed: skip ${id}: ${norm.err}`)
|
||||
continue
|
||||
}
|
||||
if (!norm.entry.enabled) continue
|
||||
try {
|
||||
await startManagedInstance(ctx, env, idOk.id, norm.entry)
|
||||
} catch (e) {
|
||||
const msg = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||||
logLine(ctx, `managed: start ${id} failed: ${msg}`)
|
||||
try {
|
||||
ctx.console?.error?.(`[bare-holesail] managed ${id}: ${msg}`)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
managedHookUnsubs.push(
|
||||
ctx.bareOsRegisterSuspendHook(async () => {
|
||||
for (const { hs } of managedInstances.values()) {
|
||||
if (hs && typeof hs.pause === 'function') {
|
||||
try {
|
||||
await hs.pause()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
managedHookUnsubs.push(
|
||||
ctx.bareOsRegisterResumeHook(async () => {
|
||||
for (const { hs } of managedInstances.values()) {
|
||||
if (hs && typeof hs.resume === 'function') {
|
||||
try {
|
||||
await hs.resume()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
logLine(ctx, 'managed: service ready')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
export async function bareHolesailManagedStopService(ctx) {
|
||||
for (const u of managedHookUnsubs.splice(0)) {
|
||||
try {
|
||||
u()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
const ids = [...managedInstances.keys()]
|
||||
for (const id of ids) {
|
||||
await stopManagedInstance(ctx, id)
|
||||
}
|
||||
managedServiceRunning = false
|
||||
logLine(ctx, 'managed: service stopped')
|
||||
}
|
||||
|
||||
export function bareHolesailManagedServiceIsRunning() {
|
||||
return managedServiceRunning
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
export function bareHolesailManagedRuntimeRunning(id) {
|
||||
return managedInstances.has(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
export function bareHolesailManagedRuntimeUrl(id) {
|
||||
const rec = managedInstances.get(id)
|
||||
if (!rec || !rec.hs) return ''
|
||||
try {
|
||||
const info = rec.hs.info
|
||||
if (info && typeof info === 'object' && 'url' in info)
|
||||
return String(/** @type {{ url?: string }} */ (info).url || '')
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, string | undefined>} env
|
||||
* @param {string} id
|
||||
*/
|
||||
export async function bareHolesailManagedStartOne(ctx, env, id) {
|
||||
if (!bareHolesailManagedServiceIsRunning()) {
|
||||
throw new Error('holesail: managed daemon is not running (enable bare-holesail + MANAGED)')
|
||||
}
|
||||
const idOk = bareHolesailManagedValidateId(id)
|
||||
if (!idOk.ok) throw new Error(idOk.err)
|
||||
const { state } = await bareHolesailManagedReadState(ctx, env)
|
||||
const raw = state.connections[idOk.id]
|
||||
if (!raw) throw new Error(`holesail: unknown connection ${idOk.id}`)
|
||||
const norm = bareHolesailManagedNormalizeEntry(raw)
|
||||
if (!norm.ok) throw new Error(norm.err)
|
||||
if (!norm.entry.enabled) throw new Error(`holesail: ${idOk.id} is disabled`)
|
||||
await startManagedInstance(ctx, env, idOk.id, norm.entry)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} id
|
||||
*/
|
||||
export async function bareHolesailManagedStopOne(ctx, id) {
|
||||
const idOk = bareHolesailManagedValidateId(id)
|
||||
if (!idOk.ok) throw new Error(idOk.err)
|
||||
await stopManagedInstance(ctx, idOk.id)
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* bare-holesail — Holesail TCP/UDP P2P proxy via the official holesail package API only.
|
||||
* Initd unit `bare-holesail` (stock-on managed mode; disable via env/initd) and optional early booter instance (kernel-path).
|
||||
*
|
||||
* Upstream is AGPL-3.0; the booter imports `holesail` directly (same Pear-staged pattern as
|
||||
* bare-openssh / bare-ssh2) instead of depending on ctx.bare / drive-bundle resolution.
|
||||
*/
|
||||
import { appendVarLog, BARE_OS_VAR_LOG_DIR } from './bare-os-var-log.js'
|
||||
import { bareHolesailEnvTruthy } from './bare-holesail-env.js'
|
||||
import {
|
||||
bareHolesailManagedServiceIsRunning,
|
||||
bareHolesailManagedStartService,
|
||||
bareHolesailManagedStopService
|
||||
} from './bare-holesail-managed.js'
|
||||
import { loadHolesailConstructor } from './bare-holesail-loader.js'
|
||||
import {
|
||||
registerBareInitdDisposer,
|
||||
registerBareService,
|
||||
registerKernelShutdownHook
|
||||
} from './bare-initd.js'
|
||||
|
||||
export { bareHolesailEnvTruthy }
|
||||
|
||||
export const BARE_HOLESAIL_LOG = `${BARE_OS_VAR_LOG_DIR}/holesail.log`
|
||||
|
||||
/** @type {unknown} */
|
||||
let kernelHolesail = null
|
||||
/** @type {unknown} */
|
||||
let initdHolesail = null
|
||||
/** @type {(() => void)[]} */
|
||||
let initdHookUnsubs = []
|
||||
/** @type {(() => void)[]} */
|
||||
let kernelHookUnsubs = []
|
||||
|
||||
/**
|
||||
* @param {Record<string, string | undefined>} env
|
||||
* @param {string} key
|
||||
*/
|
||||
function envStr(env, key) {
|
||||
const v = env[key]
|
||||
if (v === undefined || v === null) return ''
|
||||
return String(v).trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, string | undefined>} env
|
||||
* @param {'initd' | 'kernel'} profile
|
||||
* @returns {{ enabled: boolean, err?: string, opts?: Record<string, unknown> }}
|
||||
*/
|
||||
export function bareHolesailParseConfig(env, profile) {
|
||||
const isKernel = profile === 'kernel'
|
||||
const enabled = isKernel
|
||||
? bareHolesailEnvTruthy(env.BARE_OS_HOLESAIL_KERNEL)
|
||||
: bareHolesailEnvTruthy(env.BARE_OS_HOLESAIL_INITD)
|
||||
if (!enabled) return { enabled: false }
|
||||
|
||||
const P = isKernel ? 'BARE_OS_HOLESAIL_KERNEL_' : 'BARE_OS_HOLESAIL_'
|
||||
const server = bareHolesailEnvTruthy(env[`${P}SERVER`])
|
||||
const client = bareHolesailEnvTruthy(env[`${P}CLIENT`])
|
||||
if (server === client) {
|
||||
return {
|
||||
enabled: true,
|
||||
err: 'holesail: set exactly one of SERVER or CLIENT for this profile'
|
||||
}
|
||||
}
|
||||
const key = envStr(env, `${P}KEY`)
|
||||
if (client && !key) {
|
||||
return { enabled: true, err: 'holesail: CLIENT requires KEY' }
|
||||
}
|
||||
|
||||
/** @type {Record<string, unknown>} */
|
||||
const opts = {
|
||||
server,
|
||||
client,
|
||||
key: key || undefined
|
||||
}
|
||||
|
||||
const sec = envStr(env, `${P}SECURE`)
|
||||
if (sec !== '') opts.secure = bareHolesailEnvTruthy(sec)
|
||||
|
||||
const portRaw = envStr(env, `${P}PORT`)
|
||||
if (portRaw !== '') {
|
||||
const n = Number.parseInt(portRaw, 10)
|
||||
if (!Number.isFinite(n)) {
|
||||
return { enabled: true, err: 'holesail: invalid PORT' }
|
||||
}
|
||||
opts.port = n
|
||||
}
|
||||
|
||||
const host = envStr(env, `${P}HOST`)
|
||||
if (host !== '') opts.host = host
|
||||
|
||||
const udp = envStr(env, `${P}UDP`)
|
||||
if (udp !== '') opts.udp = bareHolesailEnvTruthy(udp)
|
||||
|
||||
const logRaw = envStr(env, `${P}LOG`)
|
||||
if (logRaw !== '') {
|
||||
if (/^\d+$/.test(logRaw)) opts.log = Number.parseInt(logRaw, 10)
|
||||
else opts.log = bareHolesailEnvTruthy(logRaw)
|
||||
}
|
||||
|
||||
return { enabled: true, opts }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} line
|
||||
*/
|
||||
function logLine(ctx, line) {
|
||||
void appendVarLog(ctx, BARE_HOLESAIL_LOG, 'holesail', line)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, string | undefined>} env
|
||||
*/
|
||||
function debugEnabled(ctx, env) {
|
||||
return bareHolesailEnvTruthy(env.BARE_OS_HOLESAIL_DEBUG)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} hs
|
||||
*/
|
||||
async function safeCloseHolesail(hs) {
|
||||
if (!hs || typeof hs.close !== 'function') return
|
||||
try {
|
||||
await hs.close()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional early booter instance: runs after ctx is wired (repl phase), before bare-initd.
|
||||
* Env: BARE_OS_HOLESAIL_KERNEL=1 and BARE_OS_HOLESAIL_KERNEL_SERVER|CLIENT, etc.
|
||||
*
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
export async function maybeStartBareHolesailKernelFromBooter(ctx) {
|
||||
const env =
|
||||
ctx.env && typeof ctx.env === 'object'
|
||||
? /** @type {Record<string, string | undefined>} */ (ctx.env)
|
||||
: /** @type {Record<string, string | undefined>} */ ({})
|
||||
|
||||
const parsed = bareHolesailParseConfig(env, 'kernel')
|
||||
if (!parsed.enabled) return
|
||||
if (parsed.err) {
|
||||
logLine(ctx, `kernel: ${parsed.err}`)
|
||||
try {
|
||||
ctx.console?.error?.(`[bare-holesail] ${parsed.err}`)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!parsed.opts) return
|
||||
|
||||
try {
|
||||
const Holesail = await loadHolesailConstructor(ctx)
|
||||
const hs = new Holesail(parsed.opts)
|
||||
await hs.ready()
|
||||
kernelHolesail = hs
|
||||
logLine(ctx, 'kernel: ready (early booter instance)')
|
||||
if (debugEnabled(ctx, env)) {
|
||||
try {
|
||||
const info = hs.info
|
||||
const url = info && typeof info === 'object' ? info.url : ''
|
||||
ctx.console?.log?.(`[bare-holesail] kernel url=${String(url).slice(0, 120)}`)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
kernelHookUnsubs.push(
|
||||
ctx.bareOsRegisterSuspendHook(async () => {
|
||||
if (kernelHolesail && typeof kernelHolesail.pause === 'function') {
|
||||
try {
|
||||
await kernelHolesail.pause()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
kernelHookUnsubs.push(
|
||||
ctx.bareOsRegisterResumeHook(async () => {
|
||||
if (kernelHolesail && typeof kernelHolesail.resume === 'function') {
|
||||
try {
|
||||
await kernelHolesail.resume()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
registerKernelShutdownHook(async () => {
|
||||
for (const u of kernelHookUnsubs.splice(0)) {
|
||||
try {
|
||||
u()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
const h = kernelHolesail
|
||||
kernelHolesail = null
|
||||
await safeCloseHolesail(h)
|
||||
logLine(ctx, 'kernel: closed (session shutdown)')
|
||||
})
|
||||
} catch (e) {
|
||||
const msg = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||||
logLine(ctx, `kernel: start error: ${msg}`)
|
||||
try {
|
||||
ctx.console?.error?.(`[bare-holesail] kernel: ${msg}`)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function initdStartBareHolesail(ctx) {
|
||||
const env =
|
||||
ctx.env && typeof ctx.env === 'object'
|
||||
? /** @type {Record<string, string | undefined>} */ (ctx.env)
|
||||
: /** @type {Record<string, string | undefined>} */ ({})
|
||||
|
||||
const initdOn = bareHolesailEnvTruthy(env.BARE_OS_HOLESAIL_INITD)
|
||||
const managedOn = bareHolesailEnvTruthy(env.BARE_OS_HOLESAIL_MANAGED)
|
||||
if (initdOn && managedOn) {
|
||||
try {
|
||||
await bareHolesailManagedStartService(ctx, env)
|
||||
} catch (e) {
|
||||
const msg = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||||
logLine(ctx, `initd managed: start error: ${msg}`)
|
||||
throw e
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const parsed = bareHolesailParseConfig(env, 'initd')
|
||||
if (!parsed.enabled) return
|
||||
if (parsed.err) {
|
||||
logLine(ctx, `initd: ${parsed.err}`)
|
||||
throw new Error(parsed.err)
|
||||
}
|
||||
if (!parsed.opts) return
|
||||
|
||||
try {
|
||||
const Holesail = await loadHolesailConstructor(ctx)
|
||||
const hs = new Holesail(parsed.opts)
|
||||
await hs.ready()
|
||||
initdHolesail = hs
|
||||
logLine(ctx, 'initd: ready')
|
||||
if (debugEnabled(ctx, env)) {
|
||||
try {
|
||||
const info = hs.info
|
||||
const url = info && typeof info === 'object' ? info.url : ''
|
||||
ctx.console?.log?.(`[bare-holesail] initd url=${String(url).slice(0, 120)}`)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
initdHookUnsubs.push(
|
||||
ctx.bareOsRegisterSuspendHook(async () => {
|
||||
if (initdHolesail && typeof initdHolesail.pause === 'function') {
|
||||
try {
|
||||
await initdHolesail.pause()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
initdHookUnsubs.push(
|
||||
ctx.bareOsRegisterResumeHook(async () => {
|
||||
if (initdHolesail && typeof initdHolesail.resume === 'function') {
|
||||
try {
|
||||
await initdHolesail.resume()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
const msg = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||||
logLine(ctx, `initd: start error: ${msg}`)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function initdStopBareHolesail(ctx) {
|
||||
if (bareHolesailManagedServiceIsRunning()) {
|
||||
await bareHolesailManagedStopService(ctx)
|
||||
logLine(ctx, 'initd: stopped (managed)')
|
||||
return
|
||||
}
|
||||
for (const u of initdHookUnsubs.splice(0)) {
|
||||
try {
|
||||
u()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
const h = initdHolesail
|
||||
initdHolesail = null
|
||||
await safeCloseHolesail(h)
|
||||
logLine(ctx, 'initd: stopped')
|
||||
}
|
||||
|
||||
registerBareInitdDisposer(() => {
|
||||
if (bareHolesailManagedServiceIsRunning()) {
|
||||
void bareHolesailManagedStopService({})
|
||||
return
|
||||
}
|
||||
for (const u of initdHookUnsubs.splice(0)) {
|
||||
try {
|
||||
u()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
const h = initdHolesail
|
||||
initdHolesail = null
|
||||
void safeCloseHolesail(h)
|
||||
})
|
||||
|
||||
registerBareService({
|
||||
name: 'bare-holesail',
|
||||
description:
|
||||
'Holesail P2P TCP/UDP proxy (official holesail API); on by default (managed multi-tunnel for /bin/holesail); disable with BARE_OS_HOLESAIL_INITD=0 or systemctl disable',
|
||||
logPath: BARE_HOLESAIL_LOG,
|
||||
start: initdStartBareHolesail,
|
||||
stop: initdStopBareHolesail
|
||||
})
|
||||
@@ -43,7 +43,8 @@ export const BARE_INITD_TIMERS_DIR = '~/.config/bare-os/timers'
|
||||
/** Built-in ordering when no drop-in file exists (kernel-logger before cron). */
|
||||
export const BARE_INITD_DEFAULT_AFTER = Object.freeze({
|
||||
'bare-cron': ['kernel-logger'],
|
||||
'bare-openssh': ['kernel-logger', 'bare-cron']
|
||||
'bare-openssh': ['kernel-logger', 'bare-cron'],
|
||||
'bare-holesail': ['kernel-logger']
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -1187,6 +1187,14 @@ export default {
|
||||
"optional": true,
|
||||
"nativeHint": true,
|
||||
"skipReason": "Optional Holepunch bare-*; may be native, platform-specific, or Bare-only"
|
||||
},
|
||||
{
|
||||
"ctxKey": "holesail",
|
||||
"package": "holesail",
|
||||
"export": "default",
|
||||
"bundle": true,
|
||||
"optional": false,
|
||||
"nativeHint": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1182,6 +1182,14 @@
|
||||
"optional": true,
|
||||
"nativeHint": true,
|
||||
"skipReason": "Optional Holepunch bare-*; may be native, platform-specific, or Bare-only"
|
||||
},
|
||||
{
|
||||
"ctxKey": "holesail",
|
||||
"package": "holesail",
|
||||
"export": "default",
|
||||
"bundle": true,
|
||||
"optional": false,
|
||||
"nativeHint": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,12 +3,29 @@
|
||||
* plus optional merge from trusted IIFE bundles on the system drive.
|
||||
*/
|
||||
|
||||
import b4a from 'b4a'
|
||||
import { readFileSync } from '#host-fs'
|
||||
import { dirname, join } from '#host-path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import bareModuleManifestEmbedded from './bare-module-manifest.data.mjs'
|
||||
import { bareOsHostBooterWarn } from './bare-os-host-booter-log.js'
|
||||
|
||||
/**
|
||||
* Used only by {@link tryLoadBareCtxKeyFromDriveBundlePath}: some Bare booters lack
|
||||
* `TextDecoder`. Dynamic `import('b4a')` fails on Pear (no referrer); static import is fine.
|
||||
* @param {Uint8Array} buf
|
||||
*/
|
||||
function decodeUtf8BytesBareFallback(buf) {
|
||||
if (typeof TextDecoder !== 'undefined') {
|
||||
try {
|
||||
return new TextDecoder().decode(buf)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
return b4a.toString(buf, 'utf8')
|
||||
}
|
||||
|
||||
/**
|
||||
* `fileURLToPath` only accepts `file:` URLs. Pear uses `pear://dev/...` for `import.meta.url`,
|
||||
* so we must not call it at load time or when running under `pear run`.
|
||||
@@ -26,35 +43,160 @@ function bareOsBooterPackageJsonPathForCreateRequire() {
|
||||
|
||||
/**
|
||||
* Drive IIFEs call `require("events")`, `require("bare-stream")`, …; `new Function` has no
|
||||
* lexical `require`, so they look at **`globalThis.require`**. On **`file:`** dev trees, install
|
||||
* Node's `createRequire` for the booter package for the duration of bundle eval. Under Pear
|
||||
* (`pear://`), skip — runtime `require` or `tryBareFetchImportWhenDriveMissing` covers fetch.
|
||||
* lexical `require`, so they look at **`globalThis.require`**. Bundled code also calls
|
||||
* **`__require.addon()`** (native bindings); that only works when `globalThis.require` is from
|
||||
* **`bare-module`'s `createRequire`** (exposes **`require.addon`**). A bare function or Node-only
|
||||
* `require` breaks eval with **`__require.addon is not a function`**.
|
||||
*
|
||||
* Order: if existing `require` already has **`addon`**, use it; else install **`createRequire`**
|
||||
* from the booter **`file:`** package.json when available; else **`createRequire(import.meta.url)`**
|
||||
* (covers **`pear://`** on Bare). If **`addon`** is still missing (e.g. Node dev), attach a stub
|
||||
* for the duration of eval only.
|
||||
*
|
||||
* @param {() => void | Promise<void>} fn
|
||||
*/
|
||||
async function withDriveBundleGlobalRequire(fn) {
|
||||
if (typeof globalThis.require === 'function') return fn()
|
||||
async function tryBareModuleCreateRequire(parentURL) {
|
||||
try {
|
||||
const bm = await import('bare-module')
|
||||
const createRequire = bm.createRequire || bm.default?.createRequire
|
||||
if (typeof createRequire !== 'function') return null
|
||||
return createRequire(parentURL)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pear `import('pkg')` can fail when the current referrer is a `pear://` booter URL.
|
||||
* Try loading via bare-module's createRequire from the booter package root first,
|
||||
* then fall back to `createRequire(import.meta.url)`.
|
||||
*
|
||||
* @param {string} specifier
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
export async function tryRequireFromBooter(specifier) {
|
||||
if (typeof specifier !== 'string' || !specifier.trim()) return undefined
|
||||
|
||||
const liveRequire = globalThis.require
|
||||
if (typeof liveRequire === 'function') {
|
||||
try {
|
||||
return liveRequire(specifier)
|
||||
} catch {
|
||||
/* continue */
|
||||
}
|
||||
}
|
||||
|
||||
const pkgJson = bareOsBooterPackageJsonPathForCreateRequire()
|
||||
let installed = false
|
||||
if (pkgJson) {
|
||||
try {
|
||||
const urlMod = await import('url')
|
||||
const bm = await import('bare-module')
|
||||
const createRequire = bm.createRequire || bm.default?.createRequire
|
||||
if (typeof createRequire === 'function') {
|
||||
const href = urlMod.pathToFileURL(pkgJson).href
|
||||
globalThis.require = createRequire(href)
|
||||
installed = true
|
||||
}
|
||||
const href = urlMod.pathToFileURL(pkgJson).href
|
||||
const req = await tryBareModuleCreateRequire(href)
|
||||
if (typeof req === 'function') return req(specifier)
|
||||
} catch {
|
||||
/* bare-module createRequire unavailable (e.g. some pear:// trees) */
|
||||
/* continue */
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const req = await tryBareModuleCreateRequire(import.meta.url)
|
||||
if (typeof req === 'function') return req(specifier)
|
||||
} catch {
|
||||
/* continue */
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** @returns {import('bare-module').Require['addon']} */
|
||||
function driveBundleRequireAddonStub() {
|
||||
const stub = function bareOsDriveBundleAddon() {
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
get() {
|
||||
return function bareOsDriveBundleAddonExport() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
stub.resolve = function bareOsDriveBundleAddonResolve() {
|
||||
return ''
|
||||
}
|
||||
stub.host = ''
|
||||
return stub
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a temporary `require` wrapper for drive-bundle eval so we control both
|
||||
* module resolution and the `require.addon` surface seen by esbuild IIFEs.
|
||||
*
|
||||
* @param {Function} delegate
|
||||
* @param {Function} addon
|
||||
*/
|
||||
function createDriveBundleRequireWrapper(delegate, addon) {
|
||||
const wrapped = function bareOsDriveBundleRequire(...args) {
|
||||
return Reflect.apply(delegate, this, args)
|
||||
}
|
||||
for (const key of ['resolve', 'cache', 'extensions', 'main']) {
|
||||
try {
|
||||
const v = delegate[key]
|
||||
if (v !== undefined) wrapped[key] = v
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
wrapped.addon = addon
|
||||
return wrapped
|
||||
}
|
||||
|
||||
async function withDriveBundleGlobalRequire(fn) {
|
||||
const origRequire = globalThis.require
|
||||
/** @type {Function | null} */
|
||||
let delegate =
|
||||
typeof origRequire === 'function' ? /** @type {Function} */ (origRequire) : null
|
||||
|
||||
if (!delegate) {
|
||||
const pkgJson = bareOsBooterPackageJsonPathForCreateRequire()
|
||||
if (pkgJson) {
|
||||
try {
|
||||
const urlMod = await import('url')
|
||||
const href = urlMod.pathToFileURL(pkgJson).href
|
||||
const req = await tryBareModuleCreateRequire(href)
|
||||
if (typeof req === 'function') delegate = req
|
||||
} catch {
|
||||
/* continue */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!delegate) {
|
||||
const req = await tryBareModuleCreateRequire(import.meta.url)
|
||||
if (typeof req === 'function') delegate = req
|
||||
}
|
||||
|
||||
if (typeof delegate === 'function') {
|
||||
/** @type {Function | null} */
|
||||
let addon =
|
||||
typeof delegate.addon === 'function' ? /** @type {Function} */ (delegate.addon) : null
|
||||
|
||||
if (!addon) {
|
||||
const addonReq = await tryBareModuleCreateRequire(import.meta.url)
|
||||
if (addonReq && typeof addonReq.addon === 'function') addon = addonReq.addon
|
||||
}
|
||||
|
||||
if (!addon) addon = driveBundleRequireAddonStub()
|
||||
|
||||
globalThis.require = createDriveBundleRequireWrapper(delegate, addon)
|
||||
}
|
||||
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
if (installed) {
|
||||
delete globalThis.require
|
||||
}
|
||||
if (origRequire === undefined) delete globalThis.require
|
||||
else globalThis.require = origRequire
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,12 +296,34 @@ function bareHostRuntime() {
|
||||
export async function buildBareCtxObjectFromHost(shellEnv, target) {
|
||||
if (!bareOsBareModulesEnabled(shellEnv)) return
|
||||
if (!bareOsBareHostImportsEnabled(shellEnv)) return
|
||||
const pearBooter =
|
||||
typeof import.meta !== 'undefined' &&
|
||||
String(import.meta.url || '').startsWith('pear:')
|
||||
const { entries } = loadBareModuleManifest()
|
||||
const skipHostKeys = new Set(
|
||||
String(shellEnv?.BARE_OS_BARE_HOST_SKIP_CTX_KEYS || '')
|
||||
.split(/[\s,]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
const onlyHostKeysRaw = String(
|
||||
shellEnv?.BARE_OS_BARE_HOST_ONLY_CTX_KEYS || ''
|
||||
)
|
||||
.split(/[\s,]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
const onlyHostKeys =
|
||||
onlyHostKeysRaw.length > 0 ? new Set(onlyHostKeysRaw) : null
|
||||
const onBare = bareHostRuntime()
|
||||
const tasks = entries.map(async (ent) => {
|
||||
if (!onBare && ent.nativeHint === true) return null
|
||||
const key = ent.ctxKey
|
||||
if (!key || target[key] !== undefined) return null
|
||||
if (skipHostKeys.has(key)) return null
|
||||
if (onlyHostKeys && !onlyHostKeys.has(key)) return null
|
||||
// Pear (`pear:` import.meta.url): bare-module cannot resolve `import("holesail")` from
|
||||
// this booter URL (MODULE_NOT_FOUND / no referrer). Drive IIFEs supply bundle:true keys.
|
||||
if (pearBooter && ent.bundle === true) return null
|
||||
try {
|
||||
const mod = await import(/* webpackIgnore: true */ ent.package)
|
||||
const val = ent.sideEffectImport
|
||||
@@ -253,6 +417,74 @@ function unwrapDriveBundleExport(val) {
|
||||
return val
|
||||
}
|
||||
|
||||
/**
|
||||
* Eval a single `/lib/bare/bundles/*.js` IIFE (same as {@link maybeMergeBareFromDrive}) and
|
||||
* return one `ctxKey` from `globalThis.__bare_os_stdlib__`. Does not mutate `ctx.bare`.
|
||||
* Used when Pear skipped populating a key (merge short-circuit or eval failure).
|
||||
*
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
||||
* @param {string} logicalPath
|
||||
* @param {string} ctxKey
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
export async function tryLoadBareCtxKeyFromDriveBundlePath(
|
||||
ctx,
|
||||
vfs,
|
||||
logicalPath,
|
||||
ctxKey
|
||||
) {
|
||||
void ctx
|
||||
if (!vfs || typeof vfs.readFile !== 'function') return undefined
|
||||
if (
|
||||
typeof logicalPath !== 'string' ||
|
||||
!logicalPath.startsWith('/lib/bare/') ||
|
||||
typeof ctxKey !== 'string' ||
|
||||
!ctxKey
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
let buf
|
||||
try {
|
||||
buf = await vfs.readFile(logicalPath)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
if (!buf || !buf.byteLength) return undefined
|
||||
const text = decodeUtf8BytesBareFallback(buf)
|
||||
if (text.length > 12 * 1024 * 1024) return undefined
|
||||
|
||||
const g = globalThis
|
||||
const sym = BARE_OS_STDLIB_GLOBAL
|
||||
/** @type {string | null} */
|
||||
let errMsg = null
|
||||
|
||||
await withDriveBundleGlobalRequire(async () => {
|
||||
g[sym] = g[sym] && typeof g[sym] === 'object' ? g[sym] : {}
|
||||
try {
|
||||
const run = new Function(
|
||||
`"use strict"; var require = globalThis.require;\n${text}\n//# sourceURL=bare-drive-bundle:${logicalPath}`
|
||||
)
|
||||
run()
|
||||
} catch (err) {
|
||||
errMsg = (err && /** @type {{ message?: string }} */ (err).message) || String(err)
|
||||
}
|
||||
})
|
||||
|
||||
if (errMsg) {
|
||||
bareOsHostBooterWarn(
|
||||
'drive_bundle_eval_failed',
|
||||
`tryLoadBareCtxKeyFromDriveBundlePath ${logicalPath}`,
|
||||
errMsg
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
const snap = g[sym]
|
||||
if (!snap || typeof snap !== 'object') return undefined
|
||||
if (!Object.prototype.hasOwnProperty.call(snap, ctxKey)) return undefined
|
||||
return unwrapDriveBundleExport(snap[ctxKey])
|
||||
}
|
||||
|
||||
export async function maybeMergeBareFromDrive(shellEnv, vfs, target) {
|
||||
if (!bareOsBareModulesEnabled(shellEnv)) return
|
||||
if (!bareOsBareDriveBundlesEnabled(shellEnv)) return
|
||||
@@ -354,7 +586,7 @@ export async function maybeMergeBareFromDrive(shellEnv, vfs, target) {
|
||||
|
||||
try {
|
||||
const run = new Function(
|
||||
`"use strict"; ${source}\n//# sourceURL=bare-drive-bundle:${path}`
|
||||
`"use strict"; var require = globalThis.require;\n${source}\n//# sourceURL=bare-drive-bundle:${path}`
|
||||
)
|
||||
run()
|
||||
} catch (err) {
|
||||
|
||||
@@ -232,6 +232,7 @@ export interface BareOsKernelContext {
|
||||
bareOsRunSshKeygenCli?(argv: string[]): Promise<unknown>
|
||||
bareOsRunTarCli?(argv: string[]): Promise<unknown>
|
||||
bareOsRunSystemctlCli?(argv: string[]): Promise<unknown>
|
||||
bareOsRunHolesailCli?(argv: string[]): Promise<unknown>
|
||||
bareOsRegisterBareDiagnosticsTap?(
|
||||
fn: (ev: Record<string, unknown>) => void
|
||||
): () => void
|
||||
|
||||
@@ -26,6 +26,7 @@ const README_TEXT = `Bare OS session logs (mirrored on your personal drive under
|
||||
kernel-console.log — console.log / console.error from the kernel session
|
||||
cron.log — bare-cron job errors
|
||||
openssh.log — bare-openssh / sshd listen and auth errors
|
||||
holesail.log — bare-holesail / early kernel-path Holesail (AGPL-3.0 upstream)
|
||||
initd.log — bare-initd service start failures
|
||||
audit.log — optional execLine audit when BARE_OS_AUDIT=1
|
||||
`
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* Drive-resident /bin/holesail → ctx.bareOsRunHolesailCli (booter).
|
||||
* Manages persisted tunnels under BARE_OS_HOLESAIL_STATE (default ~/.holesail/state.json).
|
||||
* Live start/stop uses the bare-holesail initd unit (managed mode is the stock default).
|
||||
*/
|
||||
import {
|
||||
bareHolesailManagedNormalizeEntry,
|
||||
bareHolesailManagedReadState,
|
||||
bareHolesailManagedRuntimeRunning,
|
||||
bareHolesailManagedRuntimeUrl,
|
||||
bareHolesailManagedServiceIsRunning,
|
||||
bareHolesailManagedStartOne,
|
||||
bareHolesailManagedStatePath,
|
||||
bareHolesailManagedStopOne,
|
||||
bareHolesailManagedValidateId,
|
||||
bareHolesailManagedWriteState
|
||||
} from './bare-holesail-managed.js'
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
function ctxEnv(ctx) {
|
||||
return ctx.env && typeof ctx.env === 'object'
|
||||
? /** @type {Record<string, string | undefined>} */ (ctx.env)
|
||||
: /** @type {Record<string, string | undefined>} */ ({})
|
||||
}
|
||||
|
||||
function printHelp(ctx, prog) {
|
||||
ctx.console.log(
|
||||
`Usage: ${prog} help|path|list
|
||||
${prog} add ID --server|--client [--key KEY] [--port N] [--host H] [--udp] [--secure] [--no-secure] [--log N|true|false]
|
||||
${prog} remove ID
|
||||
${prog} start ID | stop ID | restart ID
|
||||
${prog} enable ID | disable ID
|
||||
|
||||
State file: BARE_OS_HOLESAIL_STATE or ${bareHolesailManagedStatePath({})}
|
||||
Stock boot enables bare-holesail in managed mode (BARE_OS_HOLESAIL_INITD/MANAGED default on).
|
||||
Disable with BARE_OS_HOLESAIL_INITD=0 or systemctl disable bare-holesail.
|
||||
|
||||
Upstream holesail is AGPL-3.0; see handbook/04-the-booter-runtime.md.`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} flagArgs tokens after ID for "add"
|
||||
* @returns {{ ok: true, id: string, entry: Record<string, unknown> } | { ok: false, err: string }}
|
||||
*/
|
||||
/**
|
||||
* Accept `hs://…` without `--key` (otherwise parsed as an unknown flag).
|
||||
* @param {string} a
|
||||
*/
|
||||
function holesailCliTokenLooksLikeHolesailConnectionString(a) {
|
||||
return typeof a === 'string' && /^hs:\/\//i.test(a)
|
||||
}
|
||||
|
||||
export function holesailCliParseAdd(flagArgs) {
|
||||
if (flagArgs.length < 1) {
|
||||
return { ok: false, err: 'holesail add: missing ID' }
|
||||
}
|
||||
const idOk = bareHolesailManagedValidateId(flagArgs[0])
|
||||
if (!idOk.ok) return { ok: false, err: `holesail add: ${idOk.err}` }
|
||||
/** @type {Record<string, unknown>} */
|
||||
const raw = {}
|
||||
for (let i = 1; i < flagArgs.length; i++) {
|
||||
const a = flagArgs[i]
|
||||
if (a === '--server') raw.server = true
|
||||
else if (a === '--client') raw.client = true
|
||||
else if (holesailCliTokenLooksLikeHolesailConnectionString(a)) {
|
||||
if (raw.key) {
|
||||
return { ok: false, err: 'holesail add: duplicate key' }
|
||||
}
|
||||
raw.key = a
|
||||
} else if (a === '--key' && flagArgs[i + 1]) raw.key = flagArgs[++i]
|
||||
else if (a === '--port' && flagArgs[i + 1]) raw.port = flagArgs[++i]
|
||||
else if (a === '--host' && flagArgs[i + 1]) raw.host = flagArgs[++i]
|
||||
else if (a === '--udp') raw.udp = true
|
||||
else if (a === '--secure') raw.secure = true
|
||||
else if (a === '--no-secure') raw.secure = false
|
||||
else if (a === '--log' && flagArgs[i + 1]) {
|
||||
const v = flagArgs[++i]
|
||||
if (v === 'true' || v === 'false') raw.log = v
|
||||
else raw.log = v
|
||||
} else {
|
||||
return { ok: false, err: `holesail add: unknown or incomplete flag: ${a}` }
|
||||
}
|
||||
}
|
||||
const norm = bareHolesailManagedNormalizeEntry(raw)
|
||||
if (!norm.ok) return { ok: false, err: `holesail add: ${norm.err}` }
|
||||
return { ok: true, id: idOk.id, entry: norm.entry }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string[]} argv argv[0] is holesail
|
||||
*/
|
||||
export async function runHolesailCli(ctx, argv) {
|
||||
const env = ctxEnv(ctx)
|
||||
const args = argv.slice(1)
|
||||
const prog = 'holesail'
|
||||
|
||||
if (
|
||||
args.length === 0 ||
|
||||
args[0] === 'help' ||
|
||||
args[0] === '--help' ||
|
||||
args[0] === '-h'
|
||||
) {
|
||||
printHelp(ctx, prog)
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
|
||||
const sub = args[0]
|
||||
const rest = args.slice(1)
|
||||
|
||||
if (sub === 'path') {
|
||||
ctx.console.log(bareHolesailManagedStatePath(env))
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
|
||||
const vfs = ctx.vfs
|
||||
if (!vfs || typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function') {
|
||||
ctx.console.error('holesail: ctx.vfs read/write unavailable')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
if (!ctx.b4a || typeof ctx.b4a.from !== 'function') {
|
||||
ctx.console.error('holesail: ctx.b4a.from unavailable')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (sub === 'list') {
|
||||
const { path, state } = await bareHolesailManagedReadState(ctx, env)
|
||||
const daemon = bareHolesailManagedServiceIsRunning()
|
||||
ctx.console.log(`state: ${path}`)
|
||||
ctx.console.log(`daemon: ${daemon ? 'managed bare-holesail running' : 'no managed daemon (live ops limited)'}`)
|
||||
const ids = Object.keys(state.connections).sort()
|
||||
if (ids.length === 0) {
|
||||
ctx.console.log('(no connections)')
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
for (const id of ids) {
|
||||
const raw = state.connections[id]
|
||||
const norm = bareHolesailManagedNormalizeEntry(raw)
|
||||
const en = norm.ok ? String(!!norm.entry.enabled) : '?'
|
||||
const live = bareHolesailManagedRuntimeRunning(id)
|
||||
const url = live ? bareHolesailManagedRuntimeUrl(id) : ''
|
||||
const mode =
|
||||
norm.ok && norm.entry.server ? 'server' : norm.ok && norm.entry.client ? 'client' : '?'
|
||||
const err = norm.ok ? '' : ` INVALID: ${norm.err}`
|
||||
ctx.console.log(
|
||||
`${id}\t${mode}\tenabled=${en}\tlive=${live}${url ? `\turl=${url.slice(0, 80)}` : ''}${err}`
|
||||
)
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (sub === 'add') {
|
||||
const parsed = holesailCliParseAdd(rest)
|
||||
if (!parsed.ok) {
|
||||
ctx.console.error(parsed.err)
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
const { path, state } = await bareHolesailManagedReadState(ctx, env)
|
||||
if (state.connections[parsed.id]) {
|
||||
ctx.console.error(`holesail add: ${parsed.id} already exists (use remove first)`)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
state.connections[parsed.id] = parsed.entry
|
||||
await bareHolesailManagedWriteState(ctx, env, state)
|
||||
ctx.console.log(`wrote ${path}`)
|
||||
if (bareHolesailManagedServiceIsRunning()) {
|
||||
try {
|
||||
await bareHolesailManagedStartOne(ctx, env, parsed.id)
|
||||
ctx.console.log(`started ${parsed.id}`)
|
||||
} catch (e) {
|
||||
const msg = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||||
ctx.console.error(`holesail: persisted but start failed: ${msg}`)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (sub === 'remove') {
|
||||
const idOk = bareHolesailManagedValidateId(rest[0] || '')
|
||||
if (!idOk.ok) {
|
||||
ctx.console.error(`holesail remove: ${idOk.err}`)
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
const { path, state } = await bareHolesailManagedReadState(ctx, env)
|
||||
if (!state.connections[idOk.id]) {
|
||||
ctx.console.error(`holesail remove: unknown ${idOk.id}`)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
if (bareHolesailManagedRuntimeRunning(idOk.id)) {
|
||||
await bareHolesailManagedStopOne(ctx, idOk.id)
|
||||
}
|
||||
delete state.connections[idOk.id]
|
||||
await bareHolesailManagedWriteState(ctx, env, state)
|
||||
ctx.console.log(`removed ${idOk.id} (${path})`)
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (sub === 'start' || sub === 'stop' || sub === 'restart') {
|
||||
const idOk = bareHolesailManagedValidateId(rest[0] || '')
|
||||
if (!idOk.ok) {
|
||||
ctx.console.error(`holesail ${sub}: ${idOk.err}`)
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
if (sub === 'restart' || sub === 'stop') {
|
||||
if (bareHolesailManagedRuntimeRunning(idOk.id)) {
|
||||
await bareHolesailManagedStopOne(ctx, idOk.id)
|
||||
}
|
||||
}
|
||||
if (sub === 'stop') {
|
||||
ctx.console.log(`stopped ${idOk.id} (still in state; will restart on boot if enabled)`)
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
await bareHolesailManagedStartOne(ctx, env, idOk.id)
|
||||
ctx.console.log(`started ${idOk.id}`)
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (sub === 'enable' || sub === 'disable') {
|
||||
const idOk = bareHolesailManagedValidateId(rest[0] || '')
|
||||
if (!idOk.ok) {
|
||||
ctx.console.error(`holesail ${sub}: ${idOk.err}`)
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
const { path, state } = await bareHolesailManagedReadState(ctx, env)
|
||||
const raw = state.connections[idOk.id]
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
ctx.console.error(`holesail ${sub}: unknown ${idOk.id}`)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
const o = /** @type {Record<string, unknown>} */ (raw)
|
||||
o.enabled = sub === 'enable'
|
||||
await bareHolesailManagedWriteState(ctx, env, state)
|
||||
ctx.console.log(`${sub}d ${idOk.id} (${path})`)
|
||||
if (sub === 'disable' && bareHolesailManagedRuntimeRunning(idOk.id)) {
|
||||
await bareHolesailManagedStopOne(ctx, idOk.id)
|
||||
}
|
||||
if (sub === 'enable' && bareHolesailManagedServiceIsRunning()) {
|
||||
try {
|
||||
await bareHolesailManagedStartOne(ctx, env, idOk.id)
|
||||
} catch (e) {
|
||||
const msg = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||||
ctx.console.error(`holesail: enable saved but start failed: ${msg}`)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
|
||||
ctx.console.error(`holesail: unknown command ${sub} (try ${prog} help)`)
|
||||
ctx.exitCode = 2
|
||||
} catch (e) {
|
||||
const msg = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||||
ctx.console.error(`holesail: ${msg}`)
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"start": "node ../../scripts/ensure-pear-node-modules.mjs packages/bare-os-booter && bare index.js",
|
||||
"dev": "node ../../scripts/ensure-pear-node-modules.mjs packages/bare-os-booter && bare index.js",
|
||||
"pear:dev": "node ../../scripts/ensure-pear-node-modules.mjs packages/bare-os-booter && pear run --dev .",
|
||||
"test": "../../node_modules/.bin/brittle-bare test.identity.js && node --require ./scripts/bare-node-test-shim.cjs ../../node_modules/.bin/brittle-node test.js test.bare-openssh-pty.js test.hrpc-allowlist.js test.socket-scm-rights.js",
|
||||
"test": "../../node_modules/.bin/brittle-bare test.identity.js && node --require ./scripts/bare-node-test-shim.cjs ../../node_modules/.bin/brittle-node test.js test.bare-holesail.js test.bare-holesail-managed.js test.bare-openssh-pty.js test.hrpc-allowlist.js test.socket-scm-rights.js",
|
||||
"test:bare": "../../node_modules/.bin/brittle-bare test.identity.js test.bare-smoke.js"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -44,6 +44,7 @@
|
||||
"corestore": "^7.2.1",
|
||||
"hypercore-id-encoding": "^1.3.0",
|
||||
"hyperdrive": "^13.3.2",
|
||||
"holesail": "^2.4.1",
|
||||
"hyperbee": "^2.11.1",
|
||||
"hypercore-crypto": "^3.4.0",
|
||||
"hyperswarm": "^4.17.0",
|
||||
@@ -74,6 +75,8 @@
|
||||
"test.js",
|
||||
"test.identity.js",
|
||||
"test.bare-smoke.js",
|
||||
"test.bare-holesail.js",
|
||||
"test.bare-holesail-managed.js",
|
||||
".test-data"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
/**
|
||||
* Preload for `brittle-node` / Node-based booter tests:
|
||||
* - `bare-type/binding.js` uses Bare-only `require.addon()`.
|
||||
* - `bare-dns/binding.js` uses Bare-only `require.addon()` (transitive via holesail / DHT).
|
||||
* - `bare-inspect` pulls the same; brittle → bare-assert → bare-inspect.
|
||||
*
|
||||
* Usage: node --require ./scripts/bare-node-test-shim.cjs …
|
||||
*/
|
||||
'use strict'
|
||||
|
||||
/** `bare-path` and some Holepunch modules expect a global `Bare` (Pear/Bare runtime). */
|
||||
if (typeof globalThis.Bare === 'undefined') {
|
||||
globalThis.Bare = {
|
||||
platform: process.platform,
|
||||
on(event, fn) {
|
||||
if (event === 'exit' && typeof fn === 'function' && process.on) {
|
||||
process.on('exit', fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const path = require('path')
|
||||
const Module = require('module')
|
||||
const { types } = require('node:util')
|
||||
@@ -170,6 +183,25 @@ module.exports = function inspect(value, opts = {}) {
|
||||
module.exports.styles = {}
|
||||
`
|
||||
|
||||
/** Minimal stub so `require('bare-dns')` loads under Node (no native addon). */
|
||||
const bareDnsBindingStub = {
|
||||
initResolver() {
|
||||
return 0
|
||||
},
|
||||
destroyResolver() {},
|
||||
resolveTxt(_handle, _hostname, cb) {
|
||||
if (typeof cb === 'function') {
|
||||
process.nextTick(() => cb(new Error('bare-dns: stub binding in Node tests')))
|
||||
}
|
||||
},
|
||||
lookup(_hostname, _family, _all, _req, callback) {
|
||||
if (typeof callback === 'function') {
|
||||
process.nextTick(() => callback(new Error('bare-dns: stub binding in Node tests'), null))
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const origLoad = Module._load
|
||||
Module._load = function (request, parent, isMain) {
|
||||
if (
|
||||
@@ -181,13 +213,65 @@ Module._load = function (request, parent, isMain) {
|
||||
) {
|
||||
return bareTypeBinding
|
||||
}
|
||||
if (
|
||||
request === './binding' &&
|
||||
parent &&
|
||||
typeof parent.filename === 'string' &&
|
||||
parent.filename.includes(`${path.sep}bare-dns${path.sep}`) &&
|
||||
parent.filename.endsWith(`${path.sep}index.js`)
|
||||
) {
|
||||
return bareDnsBindingStub
|
||||
}
|
||||
return origLoad.apply(this, arguments)
|
||||
}
|
||||
|
||||
const ADDON_STUB_PREFIX =
|
||||
'if (typeof require.addon !== "function") {\n' +
|
||||
' require.addon = function bareOsNodeTestAddon() {\n' +
|
||||
' return new Proxy({}, {\n' +
|
||||
' get() {\n' +
|
||||
' return function () { return {} }\n' +
|
||||
' }\n' +
|
||||
' })\n' +
|
||||
' }\n' +
|
||||
'}\n'
|
||||
|
||||
function isBareOsBindingFile(filename) {
|
||||
if (typeof filename !== 'string') return false
|
||||
const n = filename.replace(/\\/g, '/')
|
||||
return n.endsWith('/bare-os/binding.js')
|
||||
}
|
||||
|
||||
function isBareAddonBindingFile(filename, src) {
|
||||
if (typeof filename !== 'string') return false
|
||||
if (isBareOsBindingFile(filename)) return false
|
||||
if (!filename.includes(`${path.sep}node_modules${path.sep}`)) return false
|
||||
if (!filename.endsWith(`${path.sep}binding.js`)) return false
|
||||
const s = String(src || '')
|
||||
.replace(/\/\/[^\n]*/g, '')
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.trim()
|
||||
return /^\s*module\.exports\s*=\s*require\.addon\s*\(\s*\)\s*;?\s*$/.test(s)
|
||||
}
|
||||
|
||||
const bareOsBindingNodeShimPath = path.join(
|
||||
__dirname,
|
||||
'bare-os-binding-node-shim.cjs'
|
||||
)
|
||||
|
||||
const origCompile = Module.prototype._compile
|
||||
Module.prototype._compile = function (content, filename) {
|
||||
if (typeof filename === 'string' && isBareInspectIndex(filename)) {
|
||||
return origCompile.call(this, BARE_INSPECT_NODE_SOURCE, filename)
|
||||
}
|
||||
if (isBareOsBindingFile(filename)) {
|
||||
const src = `module.exports = require(${JSON.stringify(
|
||||
bareOsBindingNodeShimPath
|
||||
)})\n`
|
||||
return origCompile.call(this, src, filename)
|
||||
}
|
||||
if (isBareAddonBindingFile(filename, content)) {
|
||||
return origCompile.call(this, ADDON_STUB_PREFIX + content, filename)
|
||||
}
|
||||
return origCompile.call(this, content, filename)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Node test shim: bare-os `binding.js` uses `require.addon()` (Bare-only).
|
||||
* Provides a minimal process/os-shaped object for brittle-node tests.
|
||||
*/
|
||||
'use strict'
|
||||
|
||||
const os = require('node:os')
|
||||
const p = process
|
||||
|
||||
function networkInterfacesFlat() {
|
||||
const ni = os.networkInterfaces()
|
||||
const out = []
|
||||
for (const name of Object.keys(ni)) {
|
||||
const addrs = ni[name]
|
||||
if (!addrs) continue
|
||||
for (const a of addrs) out.push({ name, ...a })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
signals: os.constants.signals,
|
||||
errnos: os.constants.errno || {},
|
||||
priority: os.constants.priority || {},
|
||||
platform: p.platform,
|
||||
arch: os.arch(),
|
||||
type: os.type(),
|
||||
version: os.version(),
|
||||
release: os.release(),
|
||||
machine: os.machine(),
|
||||
execPath: p.execPath,
|
||||
pid: () => p.pid,
|
||||
ppid: () => p.ppid,
|
||||
cwd: () => p.cwd(),
|
||||
chdir: (d) => p.chdir(d),
|
||||
tmpdir: () => os.tmpdir(),
|
||||
homedir: () => os.homedir(),
|
||||
hostname: () => os.hostname(),
|
||||
userInfo: (opts) => os.userInfo(opts),
|
||||
networkInterfaces: () => networkInterfacesFlat(),
|
||||
kill: (pid, sig) => p.kill(pid, sig),
|
||||
isLittleEndian: os.endianness() === 'LE',
|
||||
availableParallelism: () =>
|
||||
typeof os.availableParallelism === 'function'
|
||||
? os.availableParallelism()
|
||||
: os.cpus().length,
|
||||
cpuUsage: () => p.cpuUsage(),
|
||||
threadCpuUsage: () => ({ user: 0, system: 0 }),
|
||||
resourceUsage: () => p.resourceUsage(),
|
||||
memoryUsage: () => p.memoryUsage(),
|
||||
freemem: () => os.freemem(),
|
||||
totalmem: () => os.totalmem(),
|
||||
availableMemory: () => os.freemem(),
|
||||
constrainedMemory: () => os.totalmem(),
|
||||
uptime: () => os.uptime(),
|
||||
loadavg: () => os.loadavg(),
|
||||
cpus: () => os.cpus(),
|
||||
getProcessTitle: () => p.title,
|
||||
setProcessTitle: (t) => {
|
||||
p.title = t
|
||||
},
|
||||
getPriority: (pid) => p.getPriority(pid),
|
||||
setPriority: (pid, pri) => p.setPriority(pid, pri),
|
||||
getEnvKeys: () => Object.keys(p.env),
|
||||
getEnv: (k) => {
|
||||
const v = p.env[String(k)]
|
||||
return v === undefined ? '' : String(v)
|
||||
},
|
||||
hasEnv: (k) => Object.prototype.hasOwnProperty.call(p.env, String(k)),
|
||||
setEnv: (k, v) => {
|
||||
p.env[String(k)] = String(v)
|
||||
},
|
||||
unsetEnv: (k) => {
|
||||
delete p.env[String(k)]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import test from 'brittle'
|
||||
import {
|
||||
bareHolesailManagedNormalizeEntry,
|
||||
bareHolesailManagedValidateId
|
||||
} from './lib/bare-holesail-managed.js'
|
||||
import { holesailCliParseAdd } from './lib/holesail-cli.js'
|
||||
|
||||
test('bareHolesailManagedValidateId', (t) => {
|
||||
t.ok(bareHolesailManagedValidateId('a').ok)
|
||||
t.ok(bareHolesailManagedValidateId('t1._-x').ok)
|
||||
t.absent(bareHolesailManagedValidateId('').ok)
|
||||
t.absent(bareHolesailManagedValidateId('bad id').ok)
|
||||
})
|
||||
|
||||
test('bareHolesailManagedNormalizeEntry server', (t) => {
|
||||
const r = bareHolesailManagedNormalizeEntry({ server: true, port: '8080' })
|
||||
t.ok(r.ok)
|
||||
t.is(r.entry.server, true)
|
||||
t.is(r.entry.client, false)
|
||||
t.is(r.entry.port, 8080)
|
||||
})
|
||||
|
||||
test('bareHolesailManagedNormalizeEntry client needs key', (t) => {
|
||||
const r = bareHolesailManagedNormalizeEntry({ client: true })
|
||||
t.absent(r.ok)
|
||||
})
|
||||
|
||||
test('holesailCliParseAdd', (t) => {
|
||||
const r = holesailCliParseAdd(['t', '--server', '--port', '9000'])
|
||||
t.ok(r.ok)
|
||||
t.is(r.id, 't')
|
||||
t.is(r.entry.server, true)
|
||||
t.is(r.entry.port, 9000)
|
||||
})
|
||||
|
||||
test('holesailCliParseAdd implicit hs key', (t) => {
|
||||
const r = holesailCliParseAdd([
|
||||
't',
|
||||
'--client',
|
||||
'hs://s000477b2d983364922df1296a65aeaa2d2a',
|
||||
'--port',
|
||||
'8888'
|
||||
])
|
||||
t.ok(r.ok)
|
||||
t.is(r.entry.client, true)
|
||||
t.is(r.entry.key, 'hs://s000477b2d983364922df1296a65aeaa2d2a')
|
||||
t.is(r.entry.port, 8888)
|
||||
})
|
||||
@@ -0,0 +1,116 @@
|
||||
import test from 'brittle'
|
||||
import {
|
||||
bareHolesailEnvTruthy,
|
||||
bareHolesailParseConfig
|
||||
} from './lib/bare-holesail.js'
|
||||
import { loadHolesailConstructor } from './lib/bare-holesail-loader.js'
|
||||
import { tryLoadBareCtxKeyFromDriveBundlePath } from './lib/bare-os-ctx-bare.js'
|
||||
|
||||
test('bareHolesailEnvTruthy', (t) => {
|
||||
t.ok(bareHolesailEnvTruthy('1'))
|
||||
t.ok(bareHolesailEnvTruthy('true'))
|
||||
t.ok(bareHolesailEnvTruthy(' YES '))
|
||||
t.absent(bareHolesailEnvTruthy('0'))
|
||||
t.absent(bareHolesailEnvTruthy(''))
|
||||
})
|
||||
|
||||
test('bareHolesailParseConfig initd disabled', (t) => {
|
||||
const r = bareHolesailParseConfig({}, 'initd')
|
||||
t.absent(r.enabled)
|
||||
})
|
||||
|
||||
test('bareHolesailParseConfig initd server secure', (t) => {
|
||||
const r = bareHolesailParseConfig(
|
||||
{
|
||||
BARE_OS_HOLESAIL_INITD: '1',
|
||||
BARE_OS_HOLESAIL_SERVER: '1',
|
||||
BARE_OS_HOLESAIL_SECURE: '1',
|
||||
BARE_OS_HOLESAIL_PORT: '18080'
|
||||
},
|
||||
'initd'
|
||||
)
|
||||
t.ok(r.enabled)
|
||||
t.absent(r.err)
|
||||
t.ok(r.opts)
|
||||
t.is(r.opts.server, true)
|
||||
t.is(r.opts.client, false)
|
||||
t.is(r.opts.secure, true)
|
||||
t.is(r.opts.port, 18080)
|
||||
})
|
||||
|
||||
test('bareHolesailParseConfig initd client needs key', (t) => {
|
||||
const r = bareHolesailParseConfig(
|
||||
{
|
||||
BARE_OS_HOLESAIL_INITD: '1',
|
||||
BARE_OS_HOLESAIL_CLIENT: '1'
|
||||
},
|
||||
'initd'
|
||||
)
|
||||
t.ok(r.enabled)
|
||||
t.ok(r.err)
|
||||
})
|
||||
|
||||
test('bareHolesailParseConfig kernel profile uses KERNEL_ prefix', (t) => {
|
||||
const r = bareHolesailParseConfig(
|
||||
{
|
||||
BARE_OS_HOLESAIL_KERNEL: '1',
|
||||
BARE_OS_HOLESAIL_KERNEL_SERVER: '1'
|
||||
},
|
||||
'kernel'
|
||||
)
|
||||
t.ok(r.enabled)
|
||||
t.absent(r.err)
|
||||
t.ok(r.opts?.server)
|
||||
})
|
||||
|
||||
test('bareHolesailParseConfig rejects both server and client', (t) => {
|
||||
const r = bareHolesailParseConfig(
|
||||
{
|
||||
BARE_OS_HOLESAIL_INITD: '1',
|
||||
BARE_OS_HOLESAIL_SERVER: '1',
|
||||
BARE_OS_HOLESAIL_CLIENT: '1'
|
||||
},
|
||||
'initd'
|
||||
)
|
||||
t.ok(r.err)
|
||||
})
|
||||
|
||||
test('loadHolesailConstructor uses booter-owned holesail import', async (t) => {
|
||||
const ctx = { bare: Object.freeze({}) }
|
||||
const Ho = await loadHolesailConstructor(ctx)
|
||||
t.is(typeof Ho, 'function')
|
||||
t.is(Object.prototype.hasOwnProperty.call(ctx.bare, 'holesail'), false)
|
||||
})
|
||||
|
||||
test('drive bundle eval wraps non-addon require with addon-capable facade', async (t) => {
|
||||
const prevRequire = globalThis.require
|
||||
const fakeRequire = Object.preventExtensions(function fakeRequire() {
|
||||
throw new Error('unexpected require() call in test bundle')
|
||||
})
|
||||
|
||||
globalThis.require = fakeRequire
|
||||
|
||||
const bundleSource =
|
||||
'var __require = ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { throw Error(\'Dynamic require of "\' + x + \'" is not supported\'); });\n' +
|
||||
'var __bare_os_bundle_exports__ = { default: typeof __require.addon === "function" };\n' +
|
||||
';(function(){var g=globalThis;var s="__bare_os_stdlib__";g[s]=g[s]||{};var e=typeof __bare_os_bundle_exports__!=="undefined"?__bare_os_bundle_exports__:void 0;var v=e!=null&&typeof e==="object"&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e;g[s]["driveBundleProbe"]=v;})();\n'
|
||||
|
||||
try {
|
||||
const loaded = await tryLoadBareCtxKeyFromDriveBundlePath(
|
||||
{},
|
||||
{
|
||||
async readFile(path) {
|
||||
return path === '/lib/bare/bundles/driveBundleProbe.js'
|
||||
? Buffer.from(bundleSource)
|
||||
: null
|
||||
}
|
||||
},
|
||||
'/lib/bare/bundles/driveBundleProbe.js',
|
||||
'driveBundleProbe'
|
||||
)
|
||||
t.is(loaded, true)
|
||||
} finally {
|
||||
if (prevRequire === undefined) delete globalThis.require
|
||||
else globalThis.require = prevRequire
|
||||
}
|
||||
})
|
||||
@@ -3376,7 +3376,14 @@ test('bare-module-manifest.data.mjs matches bare-module-manifest.json', async (t
|
||||
|
||||
test('buildBareCtxObjectFromHost loads core keys on Node', async (t) => {
|
||||
const target = {}
|
||||
await buildBareCtxObjectFromHost({}, target)
|
||||
await buildBareCtxObjectFromHost(
|
||||
{
|
||||
// Narrow host import set: parallel import of the full manifest is slow and
|
||||
// some Bare-oriented packages disturb brittle's hrtime-based timers.
|
||||
BARE_OS_BARE_HOST_ONLY_CTX_KEYS: 'b4a,compactEncoding,protomux'
|
||||
},
|
||||
target
|
||||
)
|
||||
t.ok(target.b4a)
|
||||
t.ok(target.compactEncoding)
|
||||
t.ok(target.protomux)
|
||||
|
||||
Reference in New Issue
Block a user