diff --git a/container-runtime/package-lock.json b/container-runtime/package-lock.json index 244ade9f4..2fddf23f2 100644 --- a/container-runtime/package-lock.json +++ b/container-runtime/package-lock.json @@ -20,6 +20,7 @@ "node-fetch": "^3.1.0", "ts-matches": "^5.5.1", "tslib": "^2.5.3", + "tslog": "^4.9.3", "typescript": "^5.1.3", "yaml": "^2.3.1" }, @@ -5627,6 +5628,17 @@ "version": "2.6.3", "license": "0BSD" }, + "node_modules/tslog": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/tslog/-/tslog-4.9.3.tgz", + "integrity": "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/fullstack-build/tslog?sponsor=1" + } + }, "node_modules/type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -9746,6 +9758,11 @@ "tslib": { "version": "2.6.3" }, + "tslog": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/tslog/-/tslog-4.9.3.tgz", + "integrity": "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw==" + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", diff --git a/container-runtime/src/Adapters/EffectCreator.ts b/container-runtime/src/Adapters/EffectCreator.ts index 0ef299151..607e18b02 100644 --- a/container-runtime/src/Adapters/EffectCreator.ts +++ b/container-runtime/src/Adapters/EffectCreator.ts @@ -1,4 +1,4 @@ -import { types as T } from "@start9labs/start-sdk" +import { types as T, utils } from "@start9labs/start-sdk" import * as net from "net" import { object, string, number, literals, some, unknown } from "ts-matches" import { Effects } from "../Models/Effects" @@ -65,7 +65,10 @@ const rpcRoundFor = ) if (testRpcError(res)) { let message = res.error.message - console.error("Error in host RPC:", { method, params }) + console.error( + "Error in host RPC:", + utils.asError({ method, params }), + ) if (string.test(res.error.data)) { message += ": " + res.error.data console.error(`Details: ${res.error.data}`) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts index 40a152bab..47325170d 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts @@ -4,19 +4,35 @@ import { Overlay, types as T } from "@start9labs/start-sdk" import { promisify } from "util" import { DockerProcedure, VolumeId } from "../../../Models/DockerProcedure" import { Volume } from "./matchVolume" +import { ExecSpawnable } from "@start9labs/start-sdk/cjs/lib/util/Overlay" export const exec = promisify(cp.exec) export const execFile = promisify(cp.execFile) export class DockerProcedureContainer { - private constructor(readonly overlay: Overlay) {} - // static async readonlyOf(data: DockerProcedure) { - // return DockerProcedureContainer.of(data, ["-o", "ro"]) - // } + private constructor(private readonly overlay: ExecSpawnable) {} + static async of( effects: T.Effects, packageId: string, data: DockerProcedure, volumes: { [id: VolumeId]: Volume }, + options: { overlay?: ExecSpawnable } = {}, + ) { + const overlay = + options?.overlay ?? + (await DockerProcedureContainer.createOverlay( + effects, + packageId, + data, + volumes, + )) + return new DockerProcedureContainer(overlay) + } + static async createOverlay( + effects: T.Effects, + packageId: string, + data: DockerProcedure, + volumes: { [id: VolumeId]: Volume }, ) { const overlay = await Overlay.of(effects, { id: data.image }) @@ -84,23 +100,18 @@ export class DockerProcedureContainer { } } } - - return new DockerProcedureContainer(overlay) + return overlay } - async exec(commands: string[], { destroy = true } = {}) { + async exec(commands: string[], {} = {}) { try { return await this.overlay.exec(commands) } finally { - if (destroy) await this.overlay.destroy() + await this.overlay.destroy?.() } } - async execFail( - commands: string[], - timeoutMs: number | null, - { destroy = true } = {}, - ) { + async execFail(commands: string[], timeoutMs: number | null, {} = {}) { try { const res = await this.overlay.exec(commands, {}, timeoutMs) if (res.exitCode !== 0) { @@ -114,7 +125,7 @@ export class DockerProcedureContainer { } return res } finally { - if (destroy) await this.overlay.destroy() + await this.overlay.destroy?.() } } diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts index 7f778c151..79f197091 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts @@ -5,6 +5,7 @@ import { T, utils } from "@start9labs/start-sdk" import { Daemon } from "@start9labs/start-sdk/cjs/lib/mainFn/Daemon" import { Effects } from "../../../Models/Effects" import { off } from "node:process" +import { CommandController } from "@start9labs/start-sdk/cjs/lib/mainFn/CommandController" const EMBASSY_HEALTH_INTERVAL = 15 * 1000 const EMBASSY_PROPERTIES_LOOP = 30 * 1000 @@ -14,9 +15,8 @@ const EMBASSY_PROPERTIES_LOOP = 30 * 1000 * Also, this has an ability to clean itself up too if need be. */ export class MainLoop { - private _mainDockerContainer?: DockerProcedureContainer - get mainDockerContainer() { - return this._mainDockerContainer + get mainOverlay() { + return this.mainEvent?.daemon?.overlay } private healthLoops?: { name: string @@ -52,27 +52,33 @@ export class MainLoop { await this.setupInterfaces(effects) await effects.setMainStatus({ status: "running" }) const jsMain = (this.system.moduleCode as any)?.jsMain - const dockerProcedureContainer = await DockerProcedureContainer.of( - effects, - this.system.manifest.id, - this.system.manifest.main, - this.system.manifest.volumes, - ) - this._mainDockerContainer = dockerProcedureContainer if (jsMain) { throw new Error("Unreachable") } - const daemon = await Daemon.of()( - this.effects, - { id: this.system.manifest.main.image }, - currentCommand, - { - overlay: dockerProcedureContainer.overlay, - sigtermTimeout: utils.inMs( - this.system.manifest.main["sigterm-timeout"], - ), - }, - ) + const daemon = new Daemon(async () => { + const overlay = await DockerProcedureContainer.createOverlay( + effects, + this.system.manifest.id, + this.system.manifest.main, + this.system.manifest.volumes, + ) + return CommandController.of()( + this.effects, + + { id: this.system.manifest.main.image }, + currentCommand, + { + overlay, + env: { + TINI_SUBREAPER: "true", + }, + sigtermTimeout: utils.inMs( + this.system.manifest.main["sigterm-timeout"], + ), + }, + ) + }) + daemon.start() return { daemon, @@ -128,10 +134,11 @@ export class MainLoop { const main = await mainEvent delete this.mainEvent delete this.healthLoops - await main?.daemon.stop().catch((e) => console.error(e)) + await main?.daemon + .stop() + .catch((e) => console.error(`Main loop error`, utils.asError(e))) this.effects.setMainStatus({ status: "stopped" }) if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval)) - delete this._mainDockerContainer } private constructHealthLoops() { @@ -144,24 +151,27 @@ export class MainLoop { const actionProcedure = value const timeChanged = Date.now() - start if (actionProcedure.type === "docker") { + const overlay = actionProcedure.inject + ? this.mainOverlay + : undefined // prettier-ignore const container = - actionProcedure.inject && this._mainDockerContainer ? - this._mainDockerContainer : await DockerProcedureContainer.of( effects, manifest.id, actionProcedure, manifest.volumes, + { + overlay, + } ) - const shouldDestroy = container !== this._mainDockerContainer const executed = await container.exec( [ actionProcedure.entrypoint, ...actionProcedure.args, JSON.stringify(timeChanged), ], - { destroy: shouldDestroy }, + {}, ) if (executed.exitCode === 0) { await effects.setHealth({ diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index 8b74a7217..131d912e1 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -194,7 +194,7 @@ export class SystemForEmbassy implements System { const moduleCode = await import(EMBASSY_JS_LOCATION) .catch((_) => require(EMBASSY_JS_LOCATION)) .catch(async (_) => { - console.error("Could not load the js") + console.error(utils.asError("Could not load the js")) console.error({ exists: await fs.stat(EMBASSY_JS_LOCATION), }) @@ -798,17 +798,18 @@ export class SystemForEmbassy implements System { const actionProcedure = this.manifest.actions?.[actionId]?.implementation if (!actionProcedure) return { message: "Action not found", value: null } if (actionProcedure.type === "docker") { - const container = - actionProcedure.inject && this.currentRunning?.mainDockerContainer - ? this.currentRunning?.mainDockerContainer - : await DockerProcedureContainer.of( - effects, - this.manifest.id, - actionProcedure, - this.manifest.volumes, - ) - const shouldDestroy = - container !== this.currentRunning?.mainDockerContainer + const overlay = actionProcedure.inject + ? this.currentRunning?.mainOverlay + : undefined + const container = await DockerProcedureContainer.of( + effects, + this.manifest.id, + actionProcedure, + this.manifest.volumes, + { + overlay, + }, + ) return JSON.parse( ( await container.execFail( @@ -818,7 +819,6 @@ export class SystemForEmbassy implements System { JSON.stringify(formData), ], timeoutMs, - { destroy: shouldDestroy }, ) ).stdout.toString(), ) @@ -987,7 +987,10 @@ async function updateConfig( }) .once() .catch((x) => { - console.error("Could not get the service interface", x) + console.error( + "Could not get the service interface", + utils.asError(x), + ) return null }) const catchFn = (fn: () => X) => { diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts index 6481a7a56..03af30c90 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts @@ -3,7 +3,7 @@ import * as oet from "./oldEmbassyTypes" import { Volume } from "../../../Models/Volume" import * as child_process from "child_process" import { promisify } from "util" -import { daemons, startSdk, T } from "@start9labs/start-sdk" +import { daemons, startSdk, T, utils } from "@start9labs/start-sdk" import "isomorphic-fetch" import { Manifest } from "./matchManifest" import { DockerProcedureContainer } from "./DockerProcedureContainer" @@ -124,19 +124,19 @@ export const polyfillEffects = ( wait(): Promise> term(): Promise } { - const dockerProcedureContainer = DockerProcedureContainer.of( + const promiseOverlay = DockerProcedureContainer.createOverlay( effects, manifest.id, manifest.main, manifest.volumes, ) - const daemon = dockerProcedureContainer.then((dockerProcedureContainer) => + const daemon = promiseOverlay.then((overlay) => daemons.runCommand()( effects, { id: manifest.main.image }, [input.command, ...(input.args || [])], { - overlay: dockerProcedureContainer.overlay, + overlay, }, ), ) @@ -224,16 +224,16 @@ export const polyfillEffects = ( return new Promise((resolve) => setTimeout(resolve, timeMs)) }, trace(whatToPrint: string): void { - console.trace(whatToPrint) + console.trace(utils.asError(whatToPrint)) }, warn(whatToPrint: string): void { - console.warn(whatToPrint) + console.warn(utils.asError(whatToPrint)) }, error(whatToPrint: string): void { - console.error(whatToPrint) + console.error(utils.asError(whatToPrint)) }, debug(whatToPrint: string): void { - console.debug(whatToPrint) + console.debug(utils.asError(whatToPrint)) }, info(whatToPrint: string): void { console.log(false) @@ -357,7 +357,7 @@ export const polyfillEffects = ( }) spawned.stderr.on("data", (data: unknown) => { - console.error(String(data)) + console.error(`polyfill.runAsync`, utils.asError(data)) }) const id = async () => { diff --git a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts index 029b504c0..1dd0a9744 100644 --- a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts +++ b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts @@ -4,7 +4,7 @@ import matches, { any, number, object, string, tuple } from "ts-matches" import { Effects } from "../../Models/Effects" import { RpcResult, matchRpcResult } from "../RpcListener" import { duration } from "../../Models/Duration" -import { T } from "@start9labs/start-sdk" +import { T, utils } from "@start9labs/start-sdk" import { Volume } from "../../Models/Volume" import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk" import { CallbackHolder } from "../../Models/CallbackHolder" @@ -57,7 +57,9 @@ export class SystemForStartOs implements System { if (this.runningMain) { this.runningMain.callbacks .callCallback(callback, args) - .catch((error) => console.error(`callback ${callback} failed`, error)) + .catch((error) => + console.error(`callback ${callback} failed`, utils.asError(error)), + ) } else { console.warn(`callback ${callback} ignored because system is not running`) } diff --git a/core/startos/src/service/effects/image.rs b/core/startos/src/service/effects/image.rs index af62047ed..4b5293506 100644 --- a/core/startos/src/service/effects/image.rs +++ b/core/startos/src/service/effects/image.rs @@ -34,7 +34,7 @@ pub fn chroot( args, }: ChrootParams, ) -> Result<(), Error> { - let mut cmd = std::process::Command::new(command); + let mut cmd: std::process::Command = std::process::Command::new(command); if let Some(env) = env { for (k, v) in std::fs::read_to_string(env)? .lines() diff --git a/sdk/lib/backup/Backups.ts b/sdk/lib/backup/Backups.ts index 6751b1910..031ac4e4c 100644 --- a/sdk/lib/backup/Backups.ts +++ b/sdk/lib/backup/Backups.ts @@ -2,6 +2,7 @@ import * as T from "../types" import * as child_process from "child_process" import { promises as fsPromises } from "fs" +import { asError } from "../util" export type BACKUP = "BACKUP" export const DEFAULT_OPTIONS: T.BackupOptions = { @@ -183,7 +184,7 @@ async function runRsync( }) spawned.stderr.on("data", (data: unknown) => { - console.error(String(data)) + console.error(`Backups.runAsync`, asError(data)) }) const id = async () => { diff --git a/sdk/lib/config/setupConfig.ts b/sdk/lib/config/setupConfig.ts index 8a1550d57..f354c81ed 100644 --- a/sdk/lib/config/setupConfig.ts +++ b/sdk/lib/config/setupConfig.ts @@ -57,7 +57,9 @@ export function setupConfig< return { setConfig: (async ({ effects, input }) => { if (!validator.test(input)) { - await console.error(String(validator.errorMessage(input))) + await console.error( + new Error(validator.errorMessage(input)?.toString()), + ) return { error: "Set config type error for config" } } await effects.clearBindings() diff --git a/sdk/lib/health/HealthCheck.ts b/sdk/lib/health/HealthCheck.ts index adb00e296..8186a3cce 100644 --- a/sdk/lib/health/HealthCheck.ts +++ b/sdk/lib/health/HealthCheck.ts @@ -8,6 +8,7 @@ import { once } from "../util/once" import { Overlay } from "../util/Overlay" import { object, unknown } from "ts-matches" import * as T from "../types" +import { asError } from "../util/asError" export type HealthCheckParams = { effects: Effects @@ -44,7 +45,7 @@ export function healthCheck(o: HealthCheckParams) { }) currentValue.lastResult = result await triggerFirstSuccess().catch((err) => { - console.error(err) + console.error(asError(err)) }) } catch (e) { await o.effects.setHealth({ diff --git a/sdk/lib/health/checkFns/checkWebUrl.ts b/sdk/lib/health/checkFns/checkWebUrl.ts index b25c792e1..042115211 100644 --- a/sdk/lib/health/checkFns/checkWebUrl.ts +++ b/sdk/lib/health/checkFns/checkWebUrl.ts @@ -1,4 +1,5 @@ import { Effects } from "../../types" +import { asError } from "../../util/asError" import { HealthCheckResult } from "./HealthCheckResult" import { timeoutPromise } from "./index" import "isomorphic-fetch" @@ -29,7 +30,7 @@ export const checkWebUrl = async ( .catch((e) => { console.warn(`Error while fetching URL: ${url}`) console.error(JSON.stringify(e)) - console.error(e.toString()) + console.error(asError(e)) return { result: "failure" as const, message: errorMessage } }) } diff --git a/sdk/lib/mainFn/CommandController.ts b/sdk/lib/mainFn/CommandController.ts index 40f787f86..8635f76e6 100644 --- a/sdk/lib/mainFn/CommandController.ts +++ b/sdk/lib/mainFn/CommandController.ts @@ -2,14 +2,20 @@ import { DEFAULT_SIGTERM_TIMEOUT } from "." import { NO_TIMEOUT, SIGKILL, SIGTERM } from "../StartSdk" import * as T from "../types" -import { MountOptions, Overlay } from "../util/Overlay" +import { asError } from "../util/asError" +import { + ExecSpawnable, + MountOptions, + NonDestroyableOverlay, + Overlay, +} from "../util/Overlay" import { splitCommand } from "../util/splitCommand" import { cpExecFile, cpExec } from "./Daemons" export class CommandController { private constructor( readonly runningAnswer: Promise, - readonly overlay: Overlay, + private readonly overlay: ExecSpawnable, readonly pid: number | undefined, readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT, ) {} @@ -25,7 +31,7 @@ export class CommandController { // Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms sigtermTimeout?: number mounts?: { path: string; options: MountOptions }[] - overlay?: Overlay + overlay?: ExecSpawnable env?: | { [variable: string]: string @@ -38,10 +44,15 @@ export class CommandController { }, ) => { const commands = splitCommand(command) - const overlay = options.overlay || (await Overlay.of(effects, imageId)) - for (let mount of options.mounts || []) { - await overlay.mount(mount.options, mount.path) - } + const overlay = + options.overlay || + (await (async () => { + const overlay = await Overlay.of(effects, imageId) + for (let mount of options.mounts || []) { + await overlay.mount(mount.options, mount.path) + } + return overlay + })()) const childProcess = await overlay.spawn(commands, { env: options.env, }) @@ -57,7 +68,7 @@ export class CommandController { "data", options.onStderr ?? ((data: any) => { - console.error(data.toString()) + console.error(asError(data)) }), ) @@ -74,7 +85,10 @@ export class CommandController { return new CommandController(answer, overlay, pid, options.sigtermTimeout) } } - async wait(timeout: number = NO_TIMEOUT) { + get nonDestroyableOverlay() { + return new NonDestroyableOverlay(this.overlay) + } + async wait({ timeout = NO_TIMEOUT } = {}) { if (timeout > 0) setTimeout(() => { this.term() @@ -87,7 +101,7 @@ export class CommandController { (_) => {}, ) } - await this.overlay.destroy().catch((_) => {}) + await this.overlay.destroy?.().catch((_) => {}) } } async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) { @@ -106,7 +120,7 @@ export class CommandController { ) } } finally { - await this.overlay.destroy() + await this.overlay.destroy?.() } } } diff --git a/sdk/lib/mainFn/Daemon.ts b/sdk/lib/mainFn/Daemon.ts index 6dceda951..20a067ff6 100644 --- a/sdk/lib/mainFn/Daemon.ts +++ b/sdk/lib/mainFn/Daemon.ts @@ -1,5 +1,6 @@ import * as T from "../types" -import { MountOptions, Overlay } from "../util/Overlay" +import { asError } from "../util/asError" +import { ExecSpawnable, MountOptions, Overlay } from "../util/Overlay" import { CommandController } from "./CommandController" const TIMEOUT_INCREMENT_MS = 1000 @@ -12,7 +13,10 @@ const MAX_TIMEOUT_MS = 30000 export class Daemon { private commandController: CommandController | null = null private shouldBeRunning = false - private constructor(private startCommand: () => Promise) {} + constructor(private startCommand: () => Promise) {} + get overlay(): undefined | ExecSpawnable { + return this.commandController?.nonDestroyableOverlay + } static of() { return async ( effects: T.Effects, @@ -41,7 +45,6 @@ export class Daemon { return new Daemon(startCommand) } } - async start() { if (this.commandController) { return @@ -57,7 +60,7 @@ export class Daemon { timeoutCounter = Math.max(MAX_TIMEOUT_MS, timeoutCounter) } }).catch((err) => { - console.error(err) + console.error(asError(err)) }) } async term(termOptions?: { @@ -72,8 +75,8 @@ export class Daemon { }) { this.shouldBeRunning = false await this.commandController - ?.term(termOptions) - .catch((e) => console.error(e)) + ?.term({ ...termOptions }) + .catch((e) => console.error(asError(e))) this.commandController = null } } diff --git a/sdk/lib/mainFn/HealthDaemon.ts b/sdk/lib/mainFn/HealthDaemon.ts index 7cb15cd42..1a036532f 100644 --- a/sdk/lib/mainFn/HealthDaemon.ts +++ b/sdk/lib/mainFn/HealthDaemon.ts @@ -4,6 +4,7 @@ import { Ready } from "./Daemons" import { Daemon } from "./Daemon" import { Effects, SetHealth } from "../types" import { DEFAULT_SIGTERM_TIMEOUT } from "." +import { asError } from "../util/asError" const oncePromise = () => { let resolve: (value: T) => void @@ -21,13 +22,13 @@ const oncePromise = () => { * */ export class HealthDaemon { - #health: HealthCheckResult = { result: "starting", message: null } - #healthWatchers: Array<() => unknown> = [] - #running = false + private _health: HealthCheckResult = { result: "starting", message: null } + private healthWatchers: Array<() => unknown> = [] + private running = false constructor( - readonly daemon: Promise, + private readonly daemon: Promise, readonly daemonIndex: number, - readonly dependencies: HealthDaemon[], + private readonly dependencies: HealthDaemon[], readonly id: string, readonly ids: string[], readonly ready: Ready, @@ -43,12 +44,12 @@ export class HealthDaemon { signal?: NodeJS.Signals | undefined timeout?: number | undefined }) { - this.#healthWatchers = [] - this.#running = false - this.#healthCheckCleanup?.() + this.healthWatchers = [] + this.running = false + this.healthCheckCleanup?.() await this.daemon.then((d) => - d.stop({ + d.term({ timeout: this.sigtermTimeout, ...termOptions, }), @@ -57,17 +58,17 @@ export class HealthDaemon { /** Want to add another notifier that the health might have changed */ addWatcher(watcher: () => unknown) { - this.#healthWatchers.push(watcher) + this.healthWatchers.push(watcher) } get health() { - return Object.freeze(this.#health) + return Object.freeze(this._health) } private async changeRunning(newStatus: boolean) { - if (this.#running === newStatus) return + if (this.running === newStatus) return - this.#running = newStatus + this.running = newStatus if (newStatus) { ;(await this.daemon).start() @@ -80,14 +81,14 @@ export class HealthDaemon { } } - #healthCheckCleanup: (() => void) | null = null + private healthCheckCleanup: (() => void) | null = null private turnOffHealthCheck() { - this.#healthCheckCleanup?.() + this.healthCheckCleanup?.() } private async setupHealthCheck() { - if (this.#healthCheckCleanup) return + if (this.healthCheckCleanup) return const trigger = (this.ready.trigger ?? defaultTrigger)(() => ({ - lastResult: this.#health.result, + lastResult: this._health.result, })) const { promise: status, resolve: setStatus } = oncePromise<{ @@ -102,7 +103,7 @@ export class HealthDaemon { const response: HealthCheckResult = await Promise.resolve( this.ready.fn(), ).catch((err) => { - console.error(err) + console.error(asError(err)) return { result: "failure", message: "message" in err ? err.message : String(err), @@ -112,15 +113,15 @@ export class HealthDaemon { } }).catch((err) => console.error(`Daemon ${this.id} failed: ${err}`)) - this.#healthCheckCleanup = () => { + this.healthCheckCleanup = () => { setStatus({ done: true }) - this.#healthCheckCleanup = null + this.healthCheckCleanup = null } } private async setHealth(health: HealthCheckResult) { - this.#health = health - this.#healthWatchers.forEach((watcher) => watcher()) + this._health = health + this.healthWatchers.forEach((watcher) => watcher()) const display = this.ready.display const result = health.result if (!display) { @@ -134,7 +135,7 @@ export class HealthDaemon { } private async updateStatus() { - const healths = this.dependencies.map((d) => d.#health) + const healths = this.dependencies.map((d) => d._health) this.changeRunning(healths.every((x) => x.result === "success")) } } diff --git a/sdk/lib/s9pk/merkleArchive/varint.ts b/sdk/lib/s9pk/merkleArchive/varint.ts index 2bf4793b1..016505307 100644 --- a/sdk/lib/s9pk/merkleArchive/varint.ts +++ b/sdk/lib/s9pk/merkleArchive/varint.ts @@ -1,3 +1,5 @@ +import { asError } from "../../util/asError" + const msb = 0x80 const dropMsb = 0x7f const maxSize = Math.floor((8 * 8 + 7) / 7) @@ -38,7 +40,7 @@ export class VarIntProcessor { if (success) { return result } else { - console.error(this.buf) + console.error(asError(this.buf)) return null } } diff --git a/sdk/lib/util/Overlay.ts b/sdk/lib/util/Overlay.ts index 29ddadfb1..526c489e0 100644 --- a/sdk/lib/util/Overlay.ts +++ b/sdk/lib/util/Overlay.ts @@ -5,7 +5,40 @@ import { promisify } from "util" import { Buffer } from "node:buffer" export const execFile = promisify(cp.execFile) const WORKDIR = (imageId: string) => `/media/startos/images/${imageId}/` -export class Overlay { + +type ExecResults = { + exitCode: number | null + exitSignal: NodeJS.Signals | null + stdout: string | Buffer + stderr: string | Buffer +} + +/** + * This is the type that is going to describe what an overlay could do. The main point of the + * overlay is to have commands that run in a chrooted environment. This is useful for running + * commands in a containerized environment. But, I wanted the destroy to sometimes be doable, for example the + * case where the overlay isn't owned by the process, the overlay shouldn't be destroyed. + */ +export interface ExecSpawnable { + get destroy(): undefined | (() => Promise) + exec( + command: string[], + options?: CommandOptions, + timeoutMs?: number | null, + ): Promise + spawn( + command: string[], + options?: CommandOptions, + ): Promise +} +/** + * Want to limit what we can do in a container, so we want to launch a container with a specific image and the mounts. + * + * Implements: + * @see {@link ExecSpawnable} + */ +export class Overlay implements ExecSpawnable { + private destroyed = false private constructor( readonly effects: T.Effects, readonly imageId: T.ImageId, @@ -39,23 +72,6 @@ export class Overlay { return new Overlay(effects, id, rootfs, guid) } - static async with( - effects: T.Effects, - image: { id: T.ImageId; sharedRun?: boolean }, - mounts: { options: MountOptions; path: string }[], - fn: (overlay: Overlay) => Promise, - ): Promise { - const overlay = await Overlay.of(effects, image) - try { - for (let mount of mounts) { - await overlay.mount(mount.options, mount.path) - } - return await fn(overlay) - } finally { - await overlay.destroy() - } - } - async mount(options: MountOptions, path: string): Promise { path = path.startsWith("/") ? `${this.rootfs}${path}` @@ -70,7 +86,7 @@ export class Overlay { await fs.mkdir(from, { recursive: true }) await fs.mkdir(path, { recursive: true }) - await await execFile("mount", ["--bind", from, path]) + await execFile("mount", ["--bind", from, path]) } else if (options.type === "assets") { const subpath = options.subpath ? options.subpath.startsWith("/") @@ -101,10 +117,14 @@ export class Overlay { return this } - async destroy() { - const imageId = this.imageId - const guid = this.guid - await this.effects.destroyOverlayedImage({ guid }) + get destroy() { + return async () => { + if (this.destroyed) return + this.destroyed = true + const imageId = this.imageId + const guid = this.guid + await this.effects.destroyOverlayedImage({ guid }) + } } async exec( @@ -218,6 +238,32 @@ export class Overlay { } } +/** + * Take an overlay but remove the ability to add the mounts and the destroy function. + * Lets other functions, like health checks, to not destroy the parents. + * + */ +export class NonDestroyableOverlay implements ExecSpawnable { + constructor(private overlay: ExecSpawnable) {} + get destroy() { + return undefined + } + + exec( + command: string[], + options?: CommandOptions, + timeoutMs?: number | null, + ): Promise { + return this.overlay.exec(command, options, timeoutMs) + } + spawn( + command: string[], + options?: CommandOptions, + ): Promise { + return this.overlay.spawn(command, options) + } +} + export type CommandOptions = { env?: { [variable: string]: string } cwd?: string diff --git a/sdk/lib/util/asError.ts b/sdk/lib/util/asError.ts new file mode 100644 index 000000000..6e98afb6a --- /dev/null +++ b/sdk/lib/util/asError.ts @@ -0,0 +1,6 @@ +export const asError = (e: unknown) => { + if (e instanceof Error) { + return new Error(e as any) + } + return new Error(`${e}`) +} diff --git a/sdk/lib/util/index.ts b/sdk/lib/util/index.ts index d4427adb0..d7606d5d0 100644 --- a/sdk/lib/util/index.ts +++ b/sdk/lib/util/index.ts @@ -7,6 +7,7 @@ import "./Overlay" import "./once" export { GetServiceInterface, getServiceInterface } from "./getServiceInterface" +export { asError } from "./asError" export { getServiceInterfaces } from "./getServiceInterfaces" export { addressHostToUrl } from "./getServiceInterface" export { hostnameInfoToAddress } from "./Hostname"