Orginize
This commit is contained in:
@@ -0,0 +1,620 @@
|
||||
/**
|
||||
* User initd config on the personal drive: disabled units and optional drop-in ordering.
|
||||
* Paths are logical VFS paths under $HOME.
|
||||
*/
|
||||
|
||||
import b4a from 'b4a'
|
||||
|
||||
/** One unit per line; blank lines and `#…` comments ignored. */
|
||||
export const BARE_INITD_DISABLED_FILE = '~/.config/bare-os/initd/disabled.txt'
|
||||
|
||||
/** Masked units cannot be started until `systemctl unmask` (unioned with disabled for boot order). */
|
||||
export const BARE_INITD_MASKED_FILE = '~/.config/bare-os/initd/masked.txt'
|
||||
|
||||
/**
|
||||
* Optional `name.unit` files under this directory. Supports `[Unit]` keys:
|
||||
* After=, Requires=, Wants=, TimeoutStartSec=, TimeoutStopSec=, Restart=, RestartSec=, ExecStartPost=, SocketActivationIpc=,
|
||||
* IdleSec= (with SocketActivationIpc: stop after idle seconds; requires unit `stop`),
|
||||
* ReadinessPath= (VFS path until exists), or `exec:<execLine>` (bounded by ReadinessTimeoutSec=),
|
||||
* ConditionPathExists= (skip unit start if path missing), AssertPathExists= (fail start if path missing),
|
||||
* ConditionPathIsDirectory= / AssertPathIsDirectory= (same semantics for directories),
|
||||
* ReadinessTimeoutSec=,
|
||||
* ExecHealthCmd=, HealthIntervalSec=, HealthFailureThreshold=, BareMaxExecDepth=,
|
||||
* Before= (reverse edge: listed units start after this one), OnFailure= (execLine after restart exhausted),
|
||||
* FailureAction=exec|none (default exec when OnFailure is set),
|
||||
* RestartMaxAttempts= (1–32; overrides default 3 for Restart=on-failure|always).
|
||||
*/
|
||||
export const BARE_INITD_UNITS_DIR = '~/.config/bare-os/units'
|
||||
|
||||
/** `systemctl --user` style overrides (merged on top of {@link BARE_INITD_UNITS_DIR}). */
|
||||
export const BARE_INITD_SYSTEMCTL_USER_UNITS_DIR = '~/.config/bare-init/units'
|
||||
|
||||
/**
|
||||
* Optional fragments: `~/.config/bare-os/units.d/<unitName>/*.conf` (sorted), merged after the main `.unit` and before {@link BARE_INITD_SYSTEMCTL_USER_UNITS_DIR}.
|
||||
*/
|
||||
export const BARE_INITD_UNITS_D_BASE = '~/.config/bare-os/units.d'
|
||||
|
||||
/**
|
||||
* Timer drop-ins: `[Timer]` with OnCalendar= (five cron fields), EveryMs=, or OnInactiveSec=
|
||||
* (seconds between end of last run and next start), optional Persistent=false (run once), ExecLine=.
|
||||
*/
|
||||
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-os-www': ['kernel-logger'],
|
||||
'bare-holesail': ['kernel-logger', 'bare-os-www'],
|
||||
'bare-os-chat': ['kernel-logger'],
|
||||
'bare-os-discord': ['kernel-logger']
|
||||
})
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* after: string[],
|
||||
* requires: string[],
|
||||
* wants: string[],
|
||||
* timeoutStartSec: number | null,
|
||||
* timeoutStopSec: number | null,
|
||||
* restart: string | null,
|
||||
* restartSec: number | null,
|
||||
* execStartPost: string | null,
|
||||
* socketActivationIpc: string | null,
|
||||
* idleSec: number | null,
|
||||
* readinessPath: string | null,
|
||||
* readinessTimeoutSec: number | null,
|
||||
* execHealthCmd: string | null,
|
||||
* healthIntervalSec: number | null,
|
||||
* healthFailureThreshold: number | null,
|
||||
* bareMaxExecDepth: number | null,
|
||||
* before: string[],
|
||||
* onFailure: string | null,
|
||||
* failureAction: 'exec' | 'none',
|
||||
* restartMaxAttempts: number | null,
|
||||
* conditionPathExists: string | null,
|
||||
* assertPathExists: string | null,
|
||||
* conditionPathIsDirectory: string | null,
|
||||
* assertPathIsDirectory: string | null
|
||||
* }} BareInitdUnitDropIn
|
||||
*/
|
||||
|
||||
/** @returns {BareInitdUnitDropIn} */
|
||||
export function emptyUnitDropIn() {
|
||||
return {
|
||||
after: [],
|
||||
requires: [],
|
||||
wants: [],
|
||||
timeoutStartSec: null,
|
||||
timeoutStopSec: null,
|
||||
restart: null,
|
||||
restartSec: null,
|
||||
execStartPost: null,
|
||||
socketActivationIpc: null,
|
||||
idleSec: null,
|
||||
readinessPath: null,
|
||||
readinessTimeoutSec: null,
|
||||
execHealthCmd: null,
|
||||
healthIntervalSec: null,
|
||||
healthFailureThreshold: null,
|
||||
bareMaxExecDepth: null,
|
||||
before: [],
|
||||
onFailure: null,
|
||||
failureAction: 'exec',
|
||||
restartMaxAttempts: null,
|
||||
conditionPathExists: null,
|
||||
assertPathExists: null,
|
||||
conditionPathIsDirectory: null,
|
||||
assertPathIsDirectory: null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} val
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function parseList(val) {
|
||||
return val
|
||||
.split(/[\s,]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a systemd-inspired `.unit` snippet (only `[Unit]` keys are honored).
|
||||
* @param {string} text
|
||||
* @returns {BareInitdUnitDropIn}
|
||||
*/
|
||||
export function parseUnitDropInText(text) {
|
||||
const out = emptyUnitDropIn()
|
||||
let section = ''
|
||||
for (const raw of text.split(/\r?\n/)) {
|
||||
const line = raw.trim()
|
||||
if (!line || line.startsWith('#')) continue
|
||||
if (/^\[[^\]]+\]$/.test(line)) {
|
||||
section = line.slice(1, -1).toLowerCase()
|
||||
continue
|
||||
}
|
||||
if (section !== 'unit') continue
|
||||
const eq = line.indexOf('=')
|
||||
if (eq < 1) continue
|
||||
const key = line.slice(0, eq).trim().toLowerCase()
|
||||
const val = line.slice(eq + 1).trim()
|
||||
if (key === 'after') out.after.push(...parseList(val))
|
||||
else if (key === 'requires') out.requires.push(...parseList(val))
|
||||
else if (key === 'wants') out.wants.push(...parseList(val))
|
||||
else if (key === 'timeoutstartsec') {
|
||||
const n = Number.parseInt(val, 10)
|
||||
if (Number.isFinite(n) && n > 0) out.timeoutStartSec = n
|
||||
} else if (key === 'timeoutstopsec') {
|
||||
const n = Number.parseInt(val, 10)
|
||||
if (Number.isFinite(n) && n > 0) out.timeoutStopSec = n
|
||||
} else if (key === 'restart') {
|
||||
out.restart = val.toLowerCase()
|
||||
} else if (key === 'restartsec') {
|
||||
const n = Number.parseInt(val, 10)
|
||||
if (Number.isFinite(n) && n >= 0) out.restartSec = n
|
||||
} else if (key === 'execstartpost') {
|
||||
out.execStartPost = val
|
||||
} else if (key === 'socketactivationipc') {
|
||||
if (/^[a-zA-Z0-9._-]+$/.test(val)) out.socketActivationIpc = val
|
||||
} else if (key === 'idlesec') {
|
||||
const n = Number.parseInt(val, 10)
|
||||
if (Number.isFinite(n) && n > 0 && n <= 86400) out.idleSec = n
|
||||
} else if (key === 'readinesspath') {
|
||||
if (val.trim()) out.readinessPath = val.trim()
|
||||
} else if (key === 'readinesstimeoutsec') {
|
||||
const n = Number.parseInt(val, 10)
|
||||
if (Number.isFinite(n) && n > 0) out.readinessTimeoutSec = n
|
||||
} else if (key === 'exechealthcmd') {
|
||||
if (val.trim()) out.execHealthCmd = val.trim()
|
||||
} else if (key === 'healthintervalsec') {
|
||||
const n = Number.parseInt(val, 10)
|
||||
if (Number.isFinite(n) && n > 0) out.healthIntervalSec = n
|
||||
} else if (key === 'healthfailurethreshold') {
|
||||
const n = Number.parseInt(val, 10)
|
||||
if (Number.isFinite(n) && n > 0) out.healthFailureThreshold = n
|
||||
} else if (key === 'baremaxexecdepth') {
|
||||
const n = Number.parseInt(val, 10)
|
||||
if (Number.isFinite(n) && n > 0) out.bareMaxExecDepth = n
|
||||
} else if (key === 'before') {
|
||||
out.before.push(...parseList(val))
|
||||
} else if (key === 'onfailure') {
|
||||
if (val.trim()) out.onFailure = val
|
||||
} else if (key === 'failureaction') {
|
||||
const v = val.toLowerCase()
|
||||
if (v === 'none') out.failureAction = 'none'
|
||||
else out.failureAction = 'exec'
|
||||
} else if (key === 'restartmaxattempts') {
|
||||
const n = Number.parseInt(val, 10)
|
||||
if (Number.isFinite(n) && n >= 1 && n <= 32) out.restartMaxAttempts = n
|
||||
} else if (key === 'conditionpathexists') {
|
||||
const t = val.trim()
|
||||
if (t.length > 0 && t.length <= 512) out.conditionPathExists = t
|
||||
} else if (key === 'assertpathexists') {
|
||||
const t = val.trim()
|
||||
if (t.length > 0 && t.length <= 512) out.assertPathExists = t
|
||||
} else if (key === 'conditionpathisdirectory') {
|
||||
const t = val.trim()
|
||||
if (t.length > 0 && t.length <= 512) out.conditionPathIsDirectory = t
|
||||
} else if (key === 'assertpathisdirectory') {
|
||||
const t = val.trim()
|
||||
if (t.length > 0 && t.length <= 512) out.assertPathIsDirectory = t
|
||||
}
|
||||
}
|
||||
out.after = [...new Set(out.after)]
|
||||
out.requires = [...new Set(out.requires)]
|
||||
out.wants = [...new Set(out.wants)]
|
||||
out.before = [...new Set(out.before)]
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BareInitdUnitDropIn} base
|
||||
* @param {BareInitdUnitDropIn} user
|
||||
* @returns {BareInitdUnitDropIn}
|
||||
*/
|
||||
export function mergeUnitDropIns(base, user) {
|
||||
const pickArr = (u, b) =>
|
||||
u.length ? [...new Set(u)] : [...new Set(b)]
|
||||
return {
|
||||
after: pickArr(user.after, base.after),
|
||||
requires: pickArr(user.requires, base.requires),
|
||||
wants: pickArr(user.wants, base.wants),
|
||||
timeoutStartSec: user.timeoutStartSec ?? base.timeoutStartSec,
|
||||
timeoutStopSec: user.timeoutStopSec ?? base.timeoutStopSec,
|
||||
restart: user.restart ?? base.restart,
|
||||
restartSec: user.restartSec ?? base.restartSec,
|
||||
execStartPost: user.execStartPost ?? base.execStartPost,
|
||||
socketActivationIpc: user.socketActivationIpc ?? base.socketActivationIpc,
|
||||
idleSec: user.idleSec ?? base.idleSec,
|
||||
readinessPath: user.readinessPath ?? base.readinessPath,
|
||||
readinessTimeoutSec:
|
||||
user.readinessTimeoutSec ?? base.readinessTimeoutSec,
|
||||
execHealthCmd: user.execHealthCmd ?? base.execHealthCmd,
|
||||
healthIntervalSec: user.healthIntervalSec ?? base.healthIntervalSec,
|
||||
healthFailureThreshold:
|
||||
user.healthFailureThreshold ?? base.healthFailureThreshold,
|
||||
bareMaxExecDepth: user.bareMaxExecDepth ?? base.bareMaxExecDepth,
|
||||
before: pickArr(user.before, base.before),
|
||||
onFailure: user.onFailure ?? base.onFailure,
|
||||
failureAction: user.failureAction ?? base.failureAction,
|
||||
restartMaxAttempts: user.restartMaxAttempts ?? base.restartMaxAttempts,
|
||||
conditionPathExists:
|
||||
user.conditionPathExists ?? base.conditionPathExists,
|
||||
assertPathExists: user.assertPathExists ?? base.assertPathExists,
|
||||
conditionPathIsDirectory:
|
||||
user.conditionPathIsDirectory ?? base.conditionPathIsDirectory,
|
||||
assertPathIsDirectory:
|
||||
user.assertPathIsDirectory ?? base.assertPathIsDirectory
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
||||
* @param {string} logicalPath
|
||||
* @returns {Promise<BareInitdUnitDropIn>}
|
||||
*/
|
||||
export async function readUnitDropInFromPath(vfs, logicalPath) {
|
||||
try {
|
||||
const buf = await vfs.readFile(logicalPath)
|
||||
if (!buf) return emptyUnitDropIn()
|
||||
return parseUnitDropInText(b4a.toString(buf, 'utf8'))
|
||||
} catch {
|
||||
return emptyUnitDropIn()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
||||
* @returns {Promise<Set<string>>}
|
||||
*/
|
||||
export async function readInitdDisabledSet(vfs) {
|
||||
const out = new Set()
|
||||
const envRaw =
|
||||
typeof globalThis.process !== 'undefined' &&
|
||||
globalThis.process.env &&
|
||||
typeof globalThis.process.env.BARE_OS_INITD_DISABLED === 'string'
|
||||
? globalThis.process.env.BARE_OS_INITD_DISABLED
|
||||
: ''
|
||||
if (envRaw.trim()) {
|
||||
for (const part of envRaw.split(/[\s,]+/)) {
|
||||
const t = part.trim()
|
||||
if (t) out.add(t)
|
||||
}
|
||||
}
|
||||
try {
|
||||
const buf = await vfs.readFile(BARE_INITD_DISABLED_FILE)
|
||||
if (!buf) return out
|
||||
const text = b4a.toString(buf, 'utf8')
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const t = line.trim()
|
||||
if (!t || t.startsWith('#')) continue
|
||||
out.add(t)
|
||||
}
|
||||
} catch {
|
||||
/* missing file */
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* mkdir: (p: string, opts?: { recursive?: boolean }) => Promise<unknown>,
|
||||
* writeFile: (p: string, buf: Uint8Array) => Promise<unknown>,
|
||||
* unlink: (p: string) => Promise<unknown>
|
||||
* }} vfs
|
||||
* @param {Set<string>} disabled
|
||||
*/
|
||||
export async function writeInitdDisabledSet(vfs, disabled) {
|
||||
await vfs.mkdir('~/.config/bare-os/initd', { recursive: true })
|
||||
if (disabled.size === 0) {
|
||||
try {
|
||||
await vfs.unlink(BARE_INITD_DISABLED_FILE)
|
||||
} catch {
|
||||
/* absent */
|
||||
}
|
||||
return
|
||||
}
|
||||
const body = [...disabled].sort().join('\n') + '\n'
|
||||
await vfs.writeFile(BARE_INITD_DISABLED_FILE, b4a.from(body, 'utf8'))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
||||
* @returns {Promise<Set<string>>}
|
||||
*/
|
||||
export async function readInitdMaskedSet(vfs) {
|
||||
const out = new Set()
|
||||
try {
|
||||
const buf = await vfs.readFile(BARE_INITD_MASKED_FILE)
|
||||
if (!buf) return out
|
||||
const text = b4a.toString(buf, 'utf8')
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const t = line.trim()
|
||||
if (!t || t.startsWith('#')) continue
|
||||
out.add(t)
|
||||
}
|
||||
} catch {
|
||||
/* missing */
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* mkdir: (p: string, opts?: { recursive?: boolean }) => Promise<unknown>,
|
||||
* writeFile: (p: string, buf: Uint8Array) => Promise<unknown>,
|
||||
* unlink: (p: string) => Promise<unknown>
|
||||
* }} vfs
|
||||
* @param {Set<string>} masked
|
||||
*/
|
||||
export async function writeInitdMaskedSet(vfs, masked) {
|
||||
await vfs.mkdir('~/.config/bare-os/initd', { recursive: true })
|
||||
if (masked.size === 0) {
|
||||
try {
|
||||
await vfs.unlink(BARE_INITD_MASKED_FILE)
|
||||
} catch {
|
||||
/* absent */
|
||||
}
|
||||
return
|
||||
}
|
||||
const body = [...masked].sort().join('\n') + '\n'
|
||||
await vfs.writeFile(BARE_INITD_MASKED_FILE, b4a.from(body, 'utf8'))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ readFile: (p: string) => Promise<Uint8Array | null>, readdir?: (p: string) => Promise<string[]> }} vfs
|
||||
* @param {string} unitName
|
||||
* @returns {Promise<BareInitdUnitDropIn>}
|
||||
*/
|
||||
export async function readUnitDropIn(vfs, unitName) {
|
||||
if (!/^[a-zA-Z0-9._-]+$/.test(unitName)) return emptyUnitDropIn()
|
||||
let merged = await readUnitDropInFromPath(
|
||||
vfs,
|
||||
`${BARE_INITD_UNITS_DIR}/${unitName}.unit`
|
||||
)
|
||||
const fragDir = `${BARE_INITD_UNITS_D_BASE}/${unitName}`
|
||||
if (typeof vfs.readdir === 'function') {
|
||||
try {
|
||||
const names = await vfs.readdir(fragDir)
|
||||
const confs = names
|
||||
.filter((n) => typeof n === 'string' && n.endsWith('.conf'))
|
||||
.sort()
|
||||
for (const n of confs) {
|
||||
const frag = await readUnitDropInFromPath(vfs, `${fragDir}/${n}`)
|
||||
merged = mergeUnitDropIns(merged, frag)
|
||||
}
|
||||
} catch {
|
||||
/* missing dir */
|
||||
}
|
||||
}
|
||||
const usr = await readUnitDropInFromPath(
|
||||
vfs,
|
||||
`${BARE_INITD_SYSTEMCTL_USER_UNITS_DIR}/${unitName}.unit`
|
||||
)
|
||||
return mergeUnitDropIns(merged, usr)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
||||
* @param {string} unitName
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
export async function readUnitAfterFromDropIn(vfs, unitName) {
|
||||
const d = await readUnitDropIn(vfs, unitName)
|
||||
return d.after
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
||||
* @param {{ name: string }[]} services
|
||||
* @param {Readonly<Record<string, string[]>>} defaultAfter
|
||||
* @returns {Promise<Map<string, BareInitdUnitDropIn>>}
|
||||
*/
|
||||
export async function loadInitdUnitDropIns(vfs, services, defaultAfter) {
|
||||
/** @type {Map<string, BareInitdUnitDropIn>} */
|
||||
const map = new Map()
|
||||
for (const s of services) {
|
||||
const fromFile = await readUnitDropIn(vfs, s.name)
|
||||
const def = defaultAfter[s.name] || []
|
||||
fromFile.after = [...new Set([...def, ...fromFile.after])]
|
||||
map.set(s.name, fromFile)
|
||||
}
|
||||
for (const [name, di] of map) {
|
||||
for (const b of di.before) {
|
||||
if (!/^[a-zA-Z0-9._-]+$/.test(b)) continue
|
||||
const target = map.get(b)
|
||||
if (target) {
|
||||
target.after = [...new Set([...target.after, name])]
|
||||
}
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ readFile: (p: string) => Promise<Uint8Array | null> }} vfs
|
||||
* @param {{ name: string }[]} services
|
||||
* @param {Readonly<Record<string, string[]>>} defaultAfter
|
||||
* @returns {Promise<Map<string, string[]>>}
|
||||
*/
|
||||
export async function loadInitdAfterMap(vfs, services, defaultAfter) {
|
||||
const full = await loadInitdUnitDropIns(vfs, services, defaultAfter)
|
||||
const map = new Map()
|
||||
for (const [k, v] of full) map.set(k, v.after)
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T extends { name: string }
|
||||
* @param {T[]} services
|
||||
* @param {Set<string>} disabled
|
||||
* @param {Map<string, string[]>} requiresMap
|
||||
*/
|
||||
function filterSatisfiedRequires(services, disabled, requiresMap) {
|
||||
const names = new Set(services.map((s) => s.name))
|
||||
return services.filter((s) => {
|
||||
for (const r of requiresMap.get(s.name) || []) {
|
||||
if (!names.has(r)) return false
|
||||
if (disabled.has(r)) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Topological sort; cycles fall back to registration order for the remainder.
|
||||
* @template T extends { name: string }
|
||||
* @param {T[]} services
|
||||
* @param {Set<string>} disabled
|
||||
* @param {Map<string, string[]>} afterMap
|
||||
* @param {Map<string, string[]>} [requiresMap]
|
||||
* @param {Map<string, string[]>} [wantsMap]
|
||||
* @returns {T[]}
|
||||
*/
|
||||
export function sortServicesForBoot(
|
||||
services,
|
||||
disabled,
|
||||
afterMap,
|
||||
requiresMap,
|
||||
wantsMap
|
||||
) {
|
||||
const reqM = requiresMap || new Map()
|
||||
const wanM = wantsMap || new Map()
|
||||
let active = services.filter((s) => !disabled.has(s.name))
|
||||
active = filterSatisfiedRequires(active, disabled, reqM)
|
||||
const nameSet = new Set(active.map((s) => s.name))
|
||||
/** @type {Map<string, Set<string>>} */
|
||||
const prereq = new Map()
|
||||
for (const s of active) {
|
||||
const inc = new Set()
|
||||
for (const a of afterMap.get(s.name) || []) {
|
||||
if (nameSet.has(a)) inc.add(a)
|
||||
}
|
||||
for (const r of reqM.get(s.name) || []) {
|
||||
if (nameSet.has(r)) inc.add(r)
|
||||
}
|
||||
for (const w of wanM.get(s.name) || []) {
|
||||
if (nameSet.has(w)) inc.add(w)
|
||||
}
|
||||
prereq.set(s.name, inc)
|
||||
}
|
||||
const remaining = new Set(active.map((s) => s.name))
|
||||
/** @type {T[]} */
|
||||
const ordered = []
|
||||
while (remaining.size) {
|
||||
const ready = [...remaining].filter((n) => {
|
||||
for (const p of prereq.get(n) || []) {
|
||||
if (remaining.has(p)) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if (!ready.length) {
|
||||
const regOrder = active.map((s) => s.name).filter((n) => remaining.has(n))
|
||||
for (const n of regOrder) {
|
||||
const svc = active.find((s) => s.name === n)
|
||||
if (svc) ordered.push(svc)
|
||||
remaining.delete(n)
|
||||
}
|
||||
break
|
||||
}
|
||||
ready.sort()
|
||||
for (const n of ready) {
|
||||
const svc = active.find((s) => s.name === n)
|
||||
if (svc) ordered.push(svc)
|
||||
remaining.delete(n)
|
||||
}
|
||||
}
|
||||
return ordered
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* kind: 'calendar',
|
||||
* onCalendar: string,
|
||||
* execLine: string,
|
||||
* jitterSec: number
|
||||
* } | {
|
||||
* kind: 'everyMs',
|
||||
* everyMs: number,
|
||||
* execLine: string,
|
||||
* jitterSec: number
|
||||
* } | {
|
||||
* kind: 'onInactiveSec',
|
||||
* inactiveSec: number,
|
||||
* execLine: string,
|
||||
* jitterSec: number,
|
||||
* persistent: boolean
|
||||
* }} BareOsTimerDropIn
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse `~/.config/bare-os/timers/*.timer` — `[Timer]` with OnCalendar=, EveryMs=, or OnInactiveSec= and ExecLine=.
|
||||
* @param {string} text
|
||||
* @returns {BareOsTimerDropIn | null}
|
||||
*/
|
||||
export function parseBareOsTimerFile(text) {
|
||||
let onCalendar = ''
|
||||
/** @type {number | null} */
|
||||
let everyMs = null
|
||||
/** @type {number | null} */
|
||||
let inactiveSec = null
|
||||
let persistent = true
|
||||
/** @type {number | null} */
|
||||
let jitterSec = null
|
||||
let execLine = ''
|
||||
let section = ''
|
||||
for (const raw of text.split(/\r?\n/)) {
|
||||
const line = raw.trim()
|
||||
if (!line || line.startsWith('#')) continue
|
||||
if (/^\[[^\]]+\]$/.test(line)) {
|
||||
section = line.slice(1, -1).toLowerCase()
|
||||
continue
|
||||
}
|
||||
if (section !== 'timer') continue
|
||||
const eq = line.indexOf('=')
|
||||
if (eq < 1) continue
|
||||
const key = line.slice(0, eq).trim().toLowerCase()
|
||||
const val = line.slice(eq + 1).trim()
|
||||
if (key === 'oncalendar') onCalendar = val
|
||||
else if (key === 'everyms') {
|
||||
const n = Number.parseInt(val, 10)
|
||||
if (Number.isFinite(n) && n >= 1000 && n <= 86400000) everyMs = n
|
||||
} else if (key === 'oninactivesec') {
|
||||
const n = Number.parseInt(val, 10)
|
||||
if (Number.isFinite(n) && n >= 1 && n <= 86400) inactiveSec = n
|
||||
} else if (key === 'persistent') {
|
||||
const v = val.toLowerCase()
|
||||
persistent = !(
|
||||
v === '0' ||
|
||||
v === 'false' ||
|
||||
v === 'no' ||
|
||||
v === 'off'
|
||||
)
|
||||
} else if (key === 'jittersec') {
|
||||
const n = Number.parseInt(val, 10)
|
||||
if (Number.isFinite(n) && n >= 0 && n <= 86400) jitterSec = n
|
||||
} else if (key === 'execline') execLine = val
|
||||
}
|
||||
const el = execLine.trim()
|
||||
if (!el) return null
|
||||
const j = jitterSec != null ? jitterSec : 0
|
||||
if (everyMs != null)
|
||||
return { kind: 'everyMs', everyMs, execLine: el, jitterSec: j }
|
||||
if (inactiveSec != null)
|
||||
return {
|
||||
kind: 'onInactiveSec',
|
||||
inactiveSec,
|
||||
execLine: el,
|
||||
jitterSec: j,
|
||||
persistent
|
||||
}
|
||||
if (!onCalendar.trim()) return null
|
||||
return {
|
||||
kind: 'calendar',
|
||||
onCalendar: onCalendar.trim(),
|
||||
execLine: el,
|
||||
jitterSec: j
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user