diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index 764f419a6..a56926a80 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -19,9 +19,14 @@ import { BackupOptions, DeepPartial, MaybePromise, + ServiceInterfaceId, + PackageId, + EnsureStorePath, + ExtractStore, + DaemonReturned, + ValidIfNoStupidEscape, } from "./types" import * as patterns from "./util/patterns" -import { Utils } from "./util/utils" import { DependencyConfig, Update } from "./dependencyConfig/DependencyConfig" import { BackupSet, Backups } from "./backup/Backups" import { smtpConfig } from "./config/configConstants" @@ -46,7 +51,13 @@ import { setupMain } from "./mainFn" import { defaultTrigger } from "./trigger/defaultTrigger" import { changeOnFirstSuccess, cooldownTrigger } from "./trigger" import setupConfig, { Read, Save } from "./config/setupConfig" -import { setupDependencyMounts } from "./dependency/setupDependencyMounts" +import { + ManifestId, + NamedPath, + Path, + VolumeName, + setupDependencyMounts, +} from "./dependency/setupDependencyMounts" import { InterfacesReceipt, SetInterfaces, @@ -55,6 +66,19 @@ import { import { successFailure } from "./trigger/successFailure" import { SetupExports } from "./inits/setupExports" import { HealthReceipt } from "./health/HealthReceipt" +import { MultiHost, Scheme, SingleHost, StaticHost } from "./interfaces/Host" +import { ServiceInterfaceBuilder } from "./interfaces/ServiceInterfaceBuilder" +import { GetSystemSmtp } from "./util/GetSystemSmtp" +import nullIfEmpty from "./util/nullIfEmpty" +import { + GetServiceInterface, + getServiceInterface, +} from "./util/getServiceInterface" +import { getServiceInterfaces } from "./util/getServiceInterfaces" +import { getStore } from "./store/getStore" +import { mountDependencies } from "./dependency/mountDependencies" +import { CommandOptions, MountOptions, Overlay } from "./util/Overlay" +import { splitCommand } from "./util/splitCommand" // prettier-ignore type AnyNeverCond = @@ -63,6 +87,17 @@ type AnyNeverCond = T extends [any, ...infer U] ? AnyNeverCond : never +export type ServiceInterfaceType = "ui" | "p2p" | "api" +export type MainEffects = Effects & { _type: "main" } +export type Signals = NodeJS.Signals +export const SIGTERM: Signals = "SIGTERM" +export const SIGKILL: Signals = "SIGTERM" +export const NO_TIMEOUT = -1 + +function removeConstType() { + return (t: T) => t as T & (E extends MainEffects ? {} : { const: never }) +} + export class StartSdk { private constructor(readonly manifest: Manifest) {} static of() { @@ -77,7 +112,78 @@ export class StartSdk { build(isReady: AnyNeverCond<[Manifest, Store], "Build not ready", true>) { return { + serviceInterface: { + getOwn: (effects: E, id: ServiceInterfaceId) => + removeConstType()( + getServiceInterface(effects, { + id, + packageId: null, + }), + ), + get: ( + effects: E, + opts: { id: ServiceInterfaceId; packageId: PackageId }, + ) => removeConstType()(getServiceInterface(effects, opts)), + getAllOwn: (effects: E) => + removeConstType()( + getServiceInterfaces(effects, { + packageId: null, + }), + ), + getAll: ( + effects: E, + opts: { packageId: PackageId }, + ) => removeConstType()(getServiceInterfaces(effects, opts)), + }, + + store: { + get: ( + effects: E, + packageId: string, + path: EnsureStorePath, + ) => + removeConstType()( + getStore(effects, path as any, { + packageId, + }), + ), + getOwn: ( + effects: E, + path: EnsureStorePath, + ) => removeConstType()(getStore(effects, path as any)), + setOwn: ( + effects: E, + path: EnsureStorePath, + value: ExtractStore, + ) => effects.store.set({ value, path: path as any }), + }, + + host: { + static: (effects: Effects, id: string) => + new StaticHost({ id, effects }), + single: (effects: Effects, id: string) => + new SingleHost({ id, effects }), + multi: (effects: Effects, id: string) => new MultiHost({ id, effects }), + }, + nullIfEmpty, + configConstants: { smtpConfig }, + createInterface: ( + effects: Effects, + options: { + name: string + id: string + description: string + hasPrimary: boolean + disabled: boolean + type: ServiceInterfaceType + username: null | string + path: string + search: Record + schemeOverride: { ssl: Scheme; noSsl: Scheme } | null + masked: boolean + }, + ) => new ServiceInterfaceBuilder({ ...options, effects }), createAction: < ConfigType extends | Record @@ -90,13 +196,44 @@ export class StartSdk { }, fn: (options: { effects: Effects - utils: Utils input: Type }) => Promise, ) => { const { input, ...rest } = metaData return createAction(rest, fn, input) }, + getSystemSmtp: (effects: E) => + removeConstType()(new GetSystemSmtp(effects)), + runCommand: async ( + effects: Effects, + imageId: Manifest["images"][number], + command: ValidIfNoStupidEscape | [string, ...string[]], + options: CommandOptions & { + mounts?: { path: string; options: MountOptions }[] + }, + ): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> => { + const commands = splitCommand(command) + const overlay = await Overlay.of(effects, imageId) + try { + for (let mount of options.mounts || []) { + await overlay.mount(mount.options, mount.path) + } + return await overlay.exec(commands) + } finally { + await overlay.destroy() + } + }, + + mountDependencies: < + In extends + | Record>> + | Record> + | Record + | Path, + >( + effects: Effects, + value: In, + ) => mountDependencies(effects, value), createDynamicAction: < ConfigType extends | Record @@ -106,11 +243,9 @@ export class StartSdk { >( metaData: (options: { effects: Effects - utils: Utils }) => MaybePromise>, fn: (options: { effects: Effects - utils: Utils input: Type }) => Promise, input: Config | Config, @@ -196,9 +331,8 @@ export class StartSdk { ) => setupInterfaces(config, fn), setupMain: ( fn: (o: { - effects: Effects + effects: MainEffects started(onTerm: () => PromiseLike): PromiseLike - utils: Utils }) => Promise>, ) => setupMain(fn), setupMigrations: < @@ -259,7 +393,6 @@ export class StartSdk { dependencyConfig: (options: { effects: Effects localConfig: LocalConfig - utils: Utils }) => Promise> update?: Update, RemoteConfig> }) { @@ -343,14 +476,8 @@ export class StartSdk { Migration: { of: (options: { version: Version - up: (opts: { - effects: Effects - utils: Utils - }) => Promise - down: (opts: { - effects: Effects - utils: Utils - }) => Promise + up: (opts: { effects: Effects }) => Promise + down: (opts: { effects: Effects }) => Promise }) => Migration.of(options), }, Value: { diff --git a/sdk/lib/actions/createAction.ts b/sdk/lib/actions/createAction.ts index d14b7ce0d..dc52a658a 100644 --- a/sdk/lib/actions/createAction.ts +++ b/sdk/lib/actions/createAction.ts @@ -1,15 +1,10 @@ import { Config, ExtractConfigType } from "../config/builder/config" import { SDKManifest } from "../manifest/ManifestTypes" import { ActionMetadata, ActionResult, Effects, ExportedAction } from "../types" -import { createUtils } from "../util" -import { Utils } from "../util/utils" export type MaybeFn = | Value - | ((options: { - effects: Effects - utils: Utils - }) => Promise | Value) + | ((options: { effects: Effects }) => Promise | Value) export class CreatedAction< Manifest extends SDKManifest, Store, @@ -27,7 +22,6 @@ export class CreatedAction< >, readonly fn: (options: { effects: Effects - utils: Utils input: Type }) => Promise, readonly input: Config, @@ -44,11 +38,7 @@ export class CreatedAction< Type extends Record = ExtractConfigType, >( metaData: MaybeFn>, - fn: (options: { - effects: Effects - utils: Utils - input: Type - }) => Promise, + fn: (options: { effects: Effects; input: Type }) => Promise, inputConfig: Config | Config, ) { return new CreatedAction( @@ -61,7 +51,6 @@ export class CreatedAction< exportedAction: ExportedAction = ({ effects, input }) => { return this.fn({ effects, - utils: createUtils(effects), input: this.validator.unsafeCast(input), }) } @@ -69,21 +58,17 @@ export class CreatedAction< run = async ({ effects, input }: { effects: Effects; input?: Type }) => { return this.fn({ effects, - utils: createUtils(effects), input: this.validator.unsafeCast(input), }) } - async metaData(options: { effects: Effects; utils: Utils }) { + async metaData(options: { effects: Effects }) { if (this.myMetaData instanceof Function) return await this.myMetaData(options) return this.myMetaData } - async ActionMetadata(options: { - effects: Effects - utils: Utils - }): Promise { + async ActionMetadata(options: { effects: Effects }): Promise { return { ...(await this.metaData(options)), input: await this.input.build(options), @@ -93,7 +78,6 @@ export class CreatedAction< async getConfig({ effects }: { effects: Effects }) { return this.input.build({ effects, - utils: createUtils(effects) as any, }) } } diff --git a/sdk/lib/actions/setupActions.ts b/sdk/lib/actions/setupActions.ts index 84a0e4345..035f8dafa 100644 --- a/sdk/lib/actions/setupActions.ts +++ b/sdk/lib/actions/setupActions.ts @@ -1,17 +1,12 @@ import { SDKManifest } from "../manifest/ManifestTypes" import { Effects, ExpectedExports } from "../types" -import { createUtils } from "../util" import { once } from "../util/once" -import { Utils } from "../util/utils" import { CreatedAction } from "./createAction" export function setupActions( ...createdActions: CreatedAction[] ) { - const myActions = async (options: { - effects: Effects - utils: Utils - }) => { + const myActions = async (options: { effects: Effects }) => { const actions: Record> = {} for (const action of createdActions) { const actionMetadata = await action.metaData(options) @@ -24,17 +19,11 @@ export function setupActions( actionsMetadata: ExpectedExports.actionsMetadata } = { actions(options: { effects: Effects }) { - const utils = createUtils(options.effects) - - return myActions({ - ...options, - utils, - }) + return myActions(options) }, async actionsMetadata({ effects }: { effects: Effects }) { - const utils = createUtils(effects) return Promise.all( - createdActions.map((x) => x.ActionMetadata({ effects, utils })), + createdActions.map((x) => x.ActionMetadata({ effects })), ) }, } diff --git a/sdk/lib/config/builder/config.ts b/sdk/lib/config/builder/config.ts index 81009abaa..c30f37890 100644 --- a/sdk/lib/config/builder/config.ts +++ b/sdk/lib/config/builder/config.ts @@ -1,5 +1,4 @@ import { ValueSpec } from "../configTypes" -import { Utils } from "../../util/utils" import { Value } from "./value" import { _ } from "../../util" import { Effects } from "../../types" @@ -7,7 +6,6 @@ import { Parser, object } from "ts-matches" export type LazyBuildOptions = { effects: Effects - utils: Utils } export type LazyBuild = ( options: LazyBuildOptions, diff --git a/sdk/lib/config/configConstants.ts b/sdk/lib/config/configConstants.ts index 13cfe32b9..aa0e024c9 100644 --- a/sdk/lib/config/configConstants.ts +++ b/sdk/lib/config/configConstants.ts @@ -1,4 +1,5 @@ import { SmtpValue } from "../types" +import { GetSystemSmtp } from "../util/GetSystemSmtp" import { email } from "../util/patterns" import { Config, ConfigSpecOf } from "./builder/config" import { Value } from "./builder/value" @@ -47,8 +48,8 @@ export const customSmtp = Config.of, never>({ * For service config. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings */ export const smtpConfig = Value.filteredUnion( - async ({ effects, utils }) => { - const smtp = await utils.getSystemSmtp().once() + async ({ effects }) => { + const smtp = await new GetSystemSmtp(effects).once() return smtp ? [] : ["system"] }, { diff --git a/sdk/lib/config/setupConfig.ts b/sdk/lib/config/setupConfig.ts index 95f9fd1ac..8519eb358 100644 --- a/sdk/lib/config/setupConfig.ts +++ b/sdk/lib/config/setupConfig.ts @@ -2,7 +2,6 @@ import { Effects, ExpectedExports } from "../types" import { SDKManifest } from "../manifest/ManifestTypes" import * as D from "./configDependencies" import { Config, ExtractConfigType } from "./builder/config" -import { Utils, createUtils } from "../util/utils" import nullIfEmpty from "../util/nullIfEmpty" import { InterfaceReceipt } from "../interfaces/interfaceReceipt" import { InterfacesReceipt as InterfacesReceipt } from "../interfaces/setupInterfaces" @@ -22,7 +21,6 @@ export type Save< > = (options: { effects: Effects input: ExtractConfigType & Record - utils: Utils dependencies: D.ConfigDependencies }) => Promise<{ dependenciesReceipt: DependenciesReceipt @@ -38,7 +36,6 @@ export type Read< | Config, never>, > = (options: { effects: Effects - utils: Utils }) => Promise & Record)> /** * We want to setup a config export with a get and set, this @@ -72,7 +69,6 @@ export function setupConfig< const { restart } = await write({ input: JSON.parse(JSON.stringify(input)), effects, - utils: createUtils(effects), dependencies: D.configDependenciesSet(), }) if (restart) { @@ -80,14 +76,10 @@ export function setupConfig< } }) as ExpectedExports.setConfig, getConfig: (async ({ effects }) => { - const myUtils = createUtils(effects) - const configValue = nullIfEmpty( - (await read({ effects, utils: myUtils })) || null, - ) + const configValue = nullIfEmpty((await read({ effects })) || null) return { spec: await spec.build({ effects, - utils: myUtils as any, }), config: configValue, } diff --git a/sdk/lib/dependencyConfig/DependencyConfig.ts b/sdk/lib/dependencyConfig/DependencyConfig.ts index 10dcb4bd8..d7ce435ad 100644 --- a/sdk/lib/dependencyConfig/DependencyConfig.ts +++ b/sdk/lib/dependencyConfig/DependencyConfig.ts @@ -3,7 +3,6 @@ import { DeepPartial, Effects, } from "../types" -import { Utils, createUtils } from "../util/utils" import { deepEqual } from "../util/deepEqual" import { deepMerge } from "../util/deepMerge" import { SDKManifest } from "../manifest/ManifestTypes" @@ -29,7 +28,6 @@ export class DependencyConfig< readonly dependencyConfig: (options: { effects: Effects localConfig: Input - utils: Utils }) => Promise>, readonly update: Update< void | DeepPartial, @@ -41,7 +39,6 @@ export class DependencyConfig< return this.dependencyConfig({ localConfig: options.localConfig as Input, effects: options.effects, - utils: createUtils(options.effects), }) } } diff --git a/sdk/lib/health/HealthCheck.ts b/sdk/lib/health/HealthCheck.ts index 652aed94e..488ecdcf5 100644 --- a/sdk/lib/health/HealthCheck.ts +++ b/sdk/lib/health/HealthCheck.ts @@ -6,52 +6,59 @@ import { Trigger } from "../trigger" import { TriggerInput } from "../trigger/TriggerInput" import { defaultTrigger } from "../trigger/defaultTrigger" import { once } from "../util/once" +import { Overlay } from "../util/Overlay" export function healthCheck(o: { effects: Effects name: string + imageId: string trigger?: Trigger - fn(): Promise | CheckResult + fn(overlay: Overlay): Promise | CheckResult onFirstSuccess?: () => unknown | Promise }) { new Promise(async () => { - let currentValue: TriggerInput = { - hadSuccess: false, - } - const getCurrentValue = () => currentValue - const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue) - const triggerFirstSuccess = once(() => - Promise.resolve( - "onFirstSuccess" in o && o.onFirstSuccess - ? o.onFirstSuccess() - : undefined, - ), - ) - for ( - let res = await trigger.next(); - !res.done; - res = await trigger.next() - ) { - try { - const { status, message } = await o.fn() - await o.effects.setHealth({ - name: o.name, - status, - message, - }) - currentValue.hadSuccess = true - currentValue.lastResult = "passing" - await triggerFirstSuccess().catch((err) => { - console.error(err) - }) - } catch (e) { - await o.effects.setHealth({ - name: o.name, - status: "failure", - message: asMessage(e), - }) - currentValue.lastResult = "failure" + const overlay = await Overlay.of(o.effects, o.imageId) + try { + let currentValue: TriggerInput = { + hadSuccess: false, } + const getCurrentValue = () => currentValue + const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue) + const triggerFirstSuccess = once(() => + Promise.resolve( + "onFirstSuccess" in o && o.onFirstSuccess + ? o.onFirstSuccess() + : undefined, + ), + ) + for ( + let res = await trigger.next(); + !res.done; + res = await trigger.next() + ) { + try { + const { status, message } = await o.fn(overlay) + await o.effects.setHealth({ + name: o.name, + status, + message, + }) + currentValue.hadSuccess = true + currentValue.lastResult = "passing" + await triggerFirstSuccess().catch((err) => { + console.error(err) + }) + } catch (e) { + await o.effects.setHealth({ + name: o.name, + status: "failure", + message: asMessage(e), + }) + currentValue.lastResult = "failure" + } + } + } finally { + await overlay.destroy() } }) return {} as HealthReceipt diff --git a/sdk/lib/health/checkFns/checkPortListening.ts b/sdk/lib/health/checkFns/checkPortListening.ts index a82b75fd4..89dcf89b3 100644 --- a/sdk/lib/health/checkFns/checkPortListening.ts +++ b/sdk/lib/health/checkFns/checkPortListening.ts @@ -1,7 +1,12 @@ import { Effects } from "../../types" -import { createUtils } from "../../util" import { stringFromStdErrOut } from "../../util/stringFromStdErrOut" import { CheckResult } from "./CheckResult" + +import { promisify } from "node:util" +import * as CP from "node:child_process" + +const cpExec = promisify(CP.exec) +const cpExecFile = promisify(CP.execFile) export function containsAddress(x: string, port: number) { const readPorts = x .split("\n") @@ -28,20 +33,15 @@ export async function checkPortListening( timeout?: number }, ): Promise { - const utils = createUtils(effects) return Promise.race([ Promise.resolve().then(async () => { const hasAddress = containsAddress( - await utils.childProcess - .exec(`cat /proc/net/tcp`, {}) - .then(stringFromStdErrOut), + await cpExec(`cat /proc/net/tcp`, {}).then(stringFromStdErrOut), port, ) || containsAddress( - await utils.childProcess - .exec("cat /proc/net/udp", {}) - .then(stringFromStdErrOut), + await cpExec("cat /proc/net/udp", {}).then(stringFromStdErrOut), port, ) if (hasAddress) { diff --git a/sdk/lib/health/checkFns/runHealthScript.ts b/sdk/lib/health/checkFns/runHealthScript.ts index 659c787f8..5d69f5e17 100644 --- a/sdk/lib/health/checkFns/runHealthScript.ts +++ b/sdk/lib/health/checkFns/runHealthScript.ts @@ -1,5 +1,5 @@ -import { CommandType, Effects } from "../../types" -import { createUtils } from "../../util" +import { Effects } from "../../types" +import { Overlay } from "../../util/Overlay" import { stringFromStdErrOut } from "../../util/stringFromStdErrOut" import { CheckResult } from "./CheckResult" import { timeoutPromise } from "./index" @@ -13,7 +13,8 @@ import { timeoutPromise } from "./index" */ export const runHealthScript = async ( effects: Effects, - runCommand: string, + runCommand: string[], + overlay: Overlay, { timeout = 30000, errorMessage = `Error while running command: ${runCommand}`, @@ -21,9 +22,8 @@ export const runHealthScript = async ( `Have ran script ${runCommand} and the result: ${res}`, } = {}, ): Promise => { - const utils = createUtils(effects) const res = await Promise.race([ - utils.childProcess.exec(runCommand, { timeout }).then(stringFromStdErrOut), + overlay.exec(runCommand), timeoutPromise(timeout), ]).catch((e) => { console.warn(errorMessage) @@ -33,6 +33,6 @@ export const runHealthScript = async ( }) return { status: "passing", - message: message(res), + message: message(res.stdout.toString()), } as CheckResult } diff --git a/sdk/lib/index.ts b/sdk/lib/index.ts index 49d385207..2b22e2c83 100644 --- a/sdk/lib/index.ts +++ b/sdk/lib/index.ts @@ -1,7 +1,6 @@ export { Daemons } from "./mainFn/Daemons" export { EmVer } from "./emverLite/mod" export { Overlay } from "./util/Overlay" -export { Utils } from "./util/utils" export { StartSdk } from "./StartSdk" export { setupManifest } from "./manifest/setupManifest" export { FileHelper } from "./util/fileHelper" diff --git a/sdk/lib/inits/migrations/Migration.ts b/sdk/lib/inits/migrations/Migration.ts index 06e8e6e39..119271aea 100644 --- a/sdk/lib/inits/migrations/Migration.ts +++ b/sdk/lib/inits/migrations/Migration.ts @@ -1,6 +1,5 @@ import { ManifestVersion, SDKManifest } from "../../manifest/ManifestTypes" import { Effects } from "../../types" -import { Utils } from "../../util/utils" export class Migration< Manifest extends SDKManifest, @@ -10,14 +9,8 @@ export class Migration< constructor( readonly options: { version: Version - up: (opts: { - effects: Effects - utils: Utils - }) => Promise - down: (opts: { - effects: Effects - utils: Utils - }) => Promise + up: (opts: { effects: Effects }) => Promise + down: (opts: { effects: Effects }) => Promise }, ) {} static of< @@ -26,23 +19,17 @@ export class Migration< Version extends ManifestVersion, >(options: { version: Version - up: (opts: { - effects: Effects - utils: Utils - }) => Promise - down: (opts: { - effects: Effects - utils: Utils - }) => Promise + up: (opts: { effects: Effects }) => Promise + down: (opts: { effects: Effects }) => Promise }) { return new Migration(options) } - async up(opts: { effects: Effects; utils: Utils }) { + async up(opts: { effects: Effects }) { this.up(opts) } - async down(opts: { effects: Effects; utils: Utils }) { + async down(opts: { effects: Effects }) { this.down(opts) } } diff --git a/sdk/lib/inits/migrations/setupMigrations.ts b/sdk/lib/inits/migrations/setupMigrations.ts index dabe3122c..288b2b9d7 100644 --- a/sdk/lib/inits/migrations/setupMigrations.ts +++ b/sdk/lib/inits/migrations/setupMigrations.ts @@ -1,7 +1,6 @@ import { EmVer } from "../../emverLite/mod" import { SDKManifest } from "../../manifest/ManifestTypes" import { ExpectedExports } from "../../types" -import { createUtils } from "../../util" import { once } from "../../util/once" import { Migration } from "./Migration" @@ -32,13 +31,12 @@ export class Migrations { effects, previousVersion, }: Parameters[0]) { - const utils = createUtils(effects) if (!!previousVersion) { const previousVersionEmVer = EmVer.parse(previousVersion) for (const [_, migration] of this.sortedMigrations() .filter((x) => x[0].greaterThan(previousVersionEmVer)) .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { - await migration.up({ effects, utils }) + await migration.up({ effects }) } } } @@ -46,14 +44,13 @@ export class Migrations { effects, nextVersion, }: Parameters[0]) { - const utils = createUtils(effects) if (!!nextVersion) { const nextVersionEmVer = EmVer.parse(nextVersion) const reversed = [...this.sortedMigrations()].reverse() for (const [_, migration] of reversed .filter((x) => x[0].greaterThan(nextVersionEmVer)) .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { - await migration.down({ effects, utils }) + await migration.down({ effects }) } } } diff --git a/sdk/lib/inits/setupExports.ts b/sdk/lib/inits/setupExports.ts index ec5e5eb1f..5f7c2b23f 100644 --- a/sdk/lib/inits/setupExports.ts +++ b/sdk/lib/inits/setupExports.ts @@ -1,10 +1,6 @@ import { Effects, ExposeServicePaths, ExposeUiPaths } from "../types" -import { Utils } from "../util/utils" -export type SetupExports = (opts: { - effects: Effects - utils: Utils -}) => +export type SetupExports = (opts: { effects: Effects }) => | { ui: { [k: string]: ExposeUiPaths } services: ExposeServicePaths diff --git a/sdk/lib/inits/setupInit.ts b/sdk/lib/inits/setupInit.ts index f5581fb8d..2df688f18 100644 --- a/sdk/lib/inits/setupInit.ts +++ b/sdk/lib/inits/setupInit.ts @@ -1,7 +1,6 @@ import { SetInterfaces } from "../interfaces/setupInterfaces" import { SDKManifest } from "../manifest/ManifestTypes" import { ExpectedExports, ExposeUiPaths, ExposeUiPathsAll } from "../types" -import { createUtils } from "../util" import { Migrations } from "./migrations/setupMigrations" import { SetupExports } from "./setupExports" import { Install } from "./setupInstall" @@ -19,18 +18,13 @@ export function setupInit( } { return { init: async (opts) => { - const utils = createUtils(opts.effects) await migrations.init(opts) await install.init(opts) await setInterfaces({ ...opts, input: null, - utils, - }) - const { services, ui } = await setupExports({ - ...opts, - utils, }) + const { services, ui } = await setupExports(opts) await opts.effects.exposeForDependents(services) await opts.effects.exposeUi( forExpose({ diff --git a/sdk/lib/inits/setupInstall.ts b/sdk/lib/inits/setupInstall.ts index e49c0b545..3990be0ca 100644 --- a/sdk/lib/inits/setupInstall.ts +++ b/sdk/lib/inits/setupInstall.ts @@ -1,10 +1,8 @@ import { SDKManifest } from "../manifest/ManifestTypes" import { Effects, ExpectedExports } from "../types" -import { Utils, createUtils } from "../util/utils" export type InstallFn = (opts: { effects: Effects - utils: Utils }) => Promise export class Install { private constructor(readonly fn: InstallFn) {} @@ -21,7 +19,6 @@ export class Install { if (!previousVersion) await this.fn({ effects, - utils: createUtils(effects), }) } } diff --git a/sdk/lib/inits/setupUninstall.ts b/sdk/lib/inits/setupUninstall.ts index b411d2fc7..812848c8f 100644 --- a/sdk/lib/inits/setupUninstall.ts +++ b/sdk/lib/inits/setupUninstall.ts @@ -1,10 +1,8 @@ import { SDKManifest } from "../manifest/ManifestTypes" import { Effects, ExpectedExports } from "../types" -import { Utils, createUtils } from "../util/utils" export type UninstallFn = (opts: { effects: Effects - utils: Utils }) => Promise export class Uninstall { private constructor(readonly fn: UninstallFn) {} @@ -21,7 +19,6 @@ export class Uninstall { if (!nextVersion) await this.fn({ effects, - utils: createUtils(effects), }) } } diff --git a/sdk/lib/interfaces/ServiceInterfaceBuilder.ts b/sdk/lib/interfaces/ServiceInterfaceBuilder.ts index c7b99b2d5..14eaee1d3 100644 --- a/sdk/lib/interfaces/ServiceInterfaceBuilder.ts +++ b/sdk/lib/interfaces/ServiceInterfaceBuilder.ts @@ -1,5 +1,5 @@ +import { ServiceInterfaceType } from "../StartSdk" import { Effects } from "../types" -import { ServiceInterfaceType } from "../util/utils" import { Scheme } from "./Host" /** diff --git a/sdk/lib/interfaces/setupInterfaces.ts b/sdk/lib/interfaces/setupInterfaces.ts index 1514cabf3..5ad8d8a7d 100644 --- a/sdk/lib/interfaces/setupInterfaces.ts +++ b/sdk/lib/interfaces/setupInterfaces.ts @@ -1,7 +1,6 @@ import { Config } from "../config/builder/config" import { SDKManifest } from "../manifest/ManifestTypes" import { AddressInfo, Effects } from "../types" -import { Utils } from "../util/utils" import { AddressReceipt } from "./AddressReceipt" export type InterfacesReceipt = Array @@ -10,11 +9,7 @@ export type SetInterfaces< Store, ConfigInput extends Record, Output extends InterfacesReceipt, -> = (opts: { - effects: Effects - input: null | ConfigInput - utils: Utils -}) => Promise +> = (opts: { effects: Effects; input: null | ConfigInput }) => Promise export type SetupInterfaces = < Manifest extends SDKManifest, Store, diff --git a/sdk/lib/mainFn/Daemons.ts b/sdk/lib/mainFn/Daemons.ts index 368c15c89..e537969fa 100644 --- a/sdk/lib/mainFn/Daemons.ts +++ b/sdk/lib/mainFn/Daemons.ts @@ -1,3 +1,4 @@ +import { NO_TIMEOUT, SIGKILL, SIGTERM, Signals } from "../StartSdk" import { HealthReceipt } from "../health/HealthReceipt" import { CheckResult } from "../health/checkFns" import { SDKManifest } from "../manifest/ManifestTypes" @@ -5,8 +6,21 @@ import { Trigger } from "../trigger" import { TriggerInput } from "../trigger/TriggerInput" import { defaultTrigger } from "../trigger/defaultTrigger" import { DaemonReturned, Effects, ValidIfNoStupidEscape } from "../types" -import { createUtils } from "../util" -import { Signals } from "../util/utils" +import { CommandOptions, MountOptions, Overlay } from "../util/Overlay" +import { splitCommand } from "../util/splitCommand" + +import { promisify } from "node:util" +import * as CP from "node:child_process" + +const cpExec = promisify(CP.exec) +const cpExecFile = promisify(CP.execFile) +async function psTree(pid: number, overlay: Overlay): Promise { + const { stdout } = await cpExec(`pstree -p ${pid}`) + const regex: RegExp = /\((\d+)\)/g + return [...stdout.toString().matchAll(regex)].map(([_all, pid]) => + parseInt(pid), + ) +} type Daemon< Manifest extends SDKManifest, Ids extends string, @@ -26,6 +40,89 @@ type Daemon< } type ErrorDuplicateId = `The id '${Id}' is already used` + +const runDaemon = + () => + async ( + effects: Effects, + imageId: Manifest["images"][number], + command: ValidIfNoStupidEscape | [string, ...string[]], + options: CommandOptions & { + mounts?: { path: string; options: MountOptions }[] + overlay?: Overlay + }, + ): Promise => { + 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 childProcess = await overlay.spawn(commands, { + env: options.env, + }) + const answer = new Promise((resolve, reject) => { + childProcess.stdout.on("data", (data: any) => { + console.log(data.toString()) + }) + childProcess.stderr.on("data", (data: any) => { + console.error(data.toString()) + }) + + childProcess.on("exit", (code: any) => { + if (code === 0) { + return resolve(null) + } + return reject(new Error(`${commands[0]} exited with code ${code}`)) + }) + }) + + const pid = childProcess.pid + return { + async wait() { + const pids = pid ? await psTree(pid, overlay) : [] + try { + return await answer + } finally { + for (const process of pids) { + cpExecFile("kill", [`-9`, String(process)]).catch((_) => {}) + } + } + }, + async term({ signal = SIGTERM, timeout = NO_TIMEOUT } = {}) { + const pids = pid ? await psTree(pid, overlay) : [] + try { + childProcess.kill(signal) + + if (timeout > NO_TIMEOUT) { + const didTimeout = await Promise.race([ + new Promise((resolve) => setTimeout(resolve, timeout)).then( + () => true, + ), + answer.then(() => false), + ]) + if (didTimeout) { + childProcess.kill(SIGKILL) + } + } else { + await answer + } + } finally { + await overlay.destroy() + } + + try { + for (const process of pids) { + await cpExecFile("kill", [`-${signal}`, String(process)]) + } + } finally { + for (const process of pids) { + cpExecFile("kill", [`-9`, String(process)]).catch((_) => {}) + } + } + }, + } + } + /** * A class for defining and controlling the service daemons ```ts @@ -104,9 +201,10 @@ export class Daemons { ) daemonsStarted[daemon.id] = requiredPromise.then(async () => { const { command, imageId } = daemon - const utils = createUtils(effects) - const child = utils.runDaemon(imageId, command, { env: daemon.env }) + const child = runDaemon()(effects, imageId, command, { + env: daemon.env, + }) let currentInput: TriggerInput = {} const getCurrentInput = () => currentInput const trigger = (daemon.ready.trigger ?? defaultTrigger)( diff --git a/sdk/lib/mainFn/index.ts b/sdk/lib/mainFn/index.ts index 58f0228b2..3da57d32f 100644 --- a/sdk/lib/mainFn/index.ts +++ b/sdk/lib/mainFn/index.ts @@ -1,12 +1,11 @@ -import { Effects, ExpectedExports } from "../types" -import { createMainUtils } from "../util" -import { Utils, createUtils } from "../util/utils" +import { ExpectedExports } from "../types" import { Daemons } from "./Daemons" import "../interfaces/ServiceInterfaceBuilder" import "../interfaces/Origin" import "./Daemons" import { SDKManifest } from "../manifest/ManifestTypes" +import { MainEffects } from "../StartSdk" /** * Used to ensure that the main function is running with the valid proofs. @@ -20,16 +19,12 @@ import { SDKManifest } from "../manifest/ManifestTypes" */ export const setupMain = ( fn: (o: { - effects: Effects + effects: MainEffects started(onTerm: () => PromiseLike): PromiseLike - utils: Utils }) => Promise>, ): ExpectedExports.main => { return async (options) => { - const result = await fn({ - ...options, - utils: createMainUtils(options.effects), - }) + const result = await fn(options) return result } } diff --git a/sdk/lib/test/configBuilder.test.ts b/sdk/lib/test/configBuilder.test.ts index fe9d123ea..ef85ee366 100644 --- a/sdk/lib/test/configBuilder.test.ts +++ b/sdk/lib/test/configBuilder.test.ts @@ -4,6 +4,8 @@ import { List } from "../config/builder/list" import { Value } from "../config/builder/value" import { Variants } from "../config/builder/variants" import { ValueSpec } from "../config/configTypes" +import { setupManifest } from "../manifest/setupManifest" +import { StartSdk } from "../StartSdk" describe("builder tests", () => { test("text", async () => { @@ -379,17 +381,61 @@ describe("values", () => { }) }) test("datetime", async () => { - const value = Value.dynamicDatetime<{ test: "a" }>(async ({ utils }) => { - ;async () => { - ;(await utils.store.getOwn("/test").once()) satisfies "a" - } + const sdk = StartSdk.of() + .withManifest( + setupManifest({ + id: "testOutput", + title: "", + version: "1.0", + releaseNotes: "", + license: "", + replaces: [], + wrapperRepo: "", + upstreamRepo: "", + supportSite: "", + marketingSite: "", + donationUrl: null, + description: { + short: "", + long: "", + }, + containers: {}, + images: [], + volumes: [], + assets: [], + alerts: { + install: null, + update: null, + uninstall: null, + restore: null, + start: null, + stop: null, + }, + dependencies: { + remoteTest: { + description: "", + requirement: { how: "", type: "opt-in" }, + version: "1.0", + }, + }, + }), + ) + .withStore<{ test: "a" }>() + .build(true) - return { - name: "Testing", - required: { default: null }, - inputmode: "date", - } - }) + const value = Value.dynamicDatetime<{ test: "a" }>( + async ({ effects }) => { + ;async () => { + ;(await sdk.store.getOwn(effects, "/test").once()) satisfies "a" + } + + return { + name: "Testing", + required: { default: null }, + inputmode: "date", + } + }, + ) const validator = value.validator validator.unsafeCast("2021-01-01") validator.unsafeCast(null) diff --git a/sdk/lib/test/host.test.ts b/sdk/lib/test/host.test.ts index eec63bb8a..82372f61b 100644 --- a/sdk/lib/test/host.test.ts +++ b/sdk/lib/test/host.test.ts @@ -1,12 +1,11 @@ import { ServiceInterfaceBuilder } from "../interfaces/ServiceInterfaceBuilder" import { Effects } from "../types" -import { createUtils } from "../util" +import { sdk } from "./output.sdk" describe("host", () => { test("Testing that the types work", () => { async function test(effects: Effects) { - const utils = createUtils(effects) - const foo = utils.host.multi("foo") + const foo = sdk.host.multi(effects, "foo") const fooOrigin = await foo.bindPort(80, { protocol: "http" as const, }) diff --git a/sdk/lib/test/store.test.ts b/sdk/lib/test/store.test.ts index 2ed8c4dfd..c41f3c85a 100644 --- a/sdk/lib/test/store.test.ts +++ b/sdk/lib/test/store.test.ts @@ -1,6 +1,5 @@ +import { MainEffects, StartSdk } from "../StartSdk" import { Effects } from "../types" -import { createMainUtils } from "../util" -import { createUtils } from "../util/utils" type Store = { config: { @@ -12,26 +11,31 @@ const todo = (): A => { throw new Error("not implemented") } const noop = () => {} + +const sdk = StartSdk.of() + .withManifest({} as Manifest) + .withStore() + .build(true) + describe("Store", () => { test("types", async () => { ;async () => { - createUtils(todo()).store.setOwn("/config", { + sdk.store.setOwn(todo(), "/config", { someValue: "a", }) - createUtils(todo()).store.setOwn( - "/config/someValue", - "b", - ) - createUtils(todo()).store.setOwn("", { + sdk.store.setOwn(todo(), "/config/someValue", "b") + sdk.store.setOwn(todo(), "", { config: { someValue: "b" }, }) - createUtils(todo()).store.setOwn( + sdk.store.setOwn( + todo(), "/config/someValue", // @ts-expect-error Type is wrong for the setting value 5, ) - createUtils(todo()).store.setOwn( + sdk.store.setOwn( + todo(), // @ts-expect-error Path is wrong "/config/someVae3lue", "someValue", @@ -52,49 +56,47 @@ describe("Store", () => { path: "/config/some2Value", value: "a", }) - ;(await createMainUtils(todo()) - .store.getOwn("/config/someValue") + ;(await sdk.store + .getOwn(todo(), "/config/someValue") .const()) satisfies string - ;(await createMainUtils(todo()) - .store.getOwn("/config") + ;(await sdk.store + .getOwn(todo(), "/config") .const()) satisfies Store["config"] - await createMainUtils(todo()) - // @ts-expect-error Path is wrong - .store.getOwn("/config/somdsfeValue") + await sdk.store // @ts-expect-error Path is wrong + .getOwn(todo(), "/config/somdsfeValue") .const() /// ----------------- ERRORS ----------------- - createUtils(todo()).store.setOwn("", { + sdk.store.setOwn(todo(), "", { // @ts-expect-error Type is wrong for the setting value config: { someValue: "notInAOrB" }, }) - createUtils(todo()).store.setOwn( + sdk.store.setOwn( + todo(), "/config/someValue", // @ts-expect-error Type is wrong for the setting value "notInAOrB", ) - ;(await createUtils(todo()) - .store.getOwn("/config/someValue") + ;(await sdk.store + .getOwn(todo(), "/config/someValue") // @ts-expect-error Const should normally not be callable .const()) satisfies string - ;(await createUtils(todo()) - .store.getOwn("/config") + ;(await sdk.store + .getOwn(todo(), "/config") // @ts-expect-error Const should normally not be callable .const()) satisfies Store["config"] - await createUtils(todo()) - // @ts-expect-error Path is wrong - .store.getOwn("/config/somdsfeValue") + await sdk.store // @ts-expect-error Path is wrong + .getOwn("/config/somdsfeValue") // @ts-expect-error Const should normally not be callable .const() /// - ;(await createUtils(todo()) - .store.getOwn("/config/someValue") + ;(await sdk.store + .getOwn(todo(), "/config/someValue") // @ts-expect-error satisfies type is wrong .const()) satisfies number - ;(await createMainUtils(todo()) - // @ts-expect-error Path is wrong - .store.getOwn("/config/") + ;(await sdk.store // @ts-expect-error Path is wrong + .getOwn(todo(), "/config/") .const()) satisfies Store["config"] ;(await todo().store.get({ path: "/config/someValue", diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index e3b9045e6..7fb59f194 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -1,11 +1,11 @@ export * as configTypes from "./config/configTypes" import { AddSslOptions } from "../../core/startos/bindings/AddSslOptions" +import { MainEffects, ServiceInterfaceType, Signals } from "./StartSdk" import { InputSpec } from "./config/configTypes" import { DependenciesReceipt } from "./config/setupConfig" import { BindOptions, Scheme } from "./interfaces/Host" import { Daemons } from "./mainFn/Daemons" import { UrlString } from "./util/getServiceInterface" -import { ServiceInterfaceType, Signals } from "./util/utils" export type ExportedAction = (options: { effects: Effects @@ -59,7 +59,7 @@ export namespace ExpectedExports { * package represents, like running a bitcoind in a bitcoind-wrapper. */ export type main = (options: { - effects: Effects + effects: MainEffects started(onTerm: () => PromiseLike): PromiseLike }) => Promise> diff --git a/sdk/lib/util/getServiceInterface.ts b/sdk/lib/util/getServiceInterface.ts index d5ad048e3..3b7af1c41 100644 --- a/sdk/lib/util/getServiceInterface.ts +++ b/sdk/lib/util/getServiceInterface.ts @@ -1,3 +1,4 @@ +import { ServiceInterfaceType } from "../StartSdk" import { AddressInfo, Effects, @@ -5,7 +6,6 @@ import { Hostname, HostnameInfo, } from "../types" -import { ServiceInterfaceType } from "./utils" export type UrlString = string export type HostId = string diff --git a/sdk/lib/util/index.ts b/sdk/lib/util/index.ts index 81bd88da6..b3cab7183 100644 --- a/sdk/lib/util/index.ts +++ b/sdk/lib/util/index.ts @@ -7,7 +7,6 @@ import "./deepEqual" import "./deepMerge" import "./Overlay" import "./once" -import * as utils from "./utils" import { SDKManifest } from "../manifest/ManifestTypes" // prettier-ignore @@ -23,11 +22,6 @@ export const isKnownError = (e: unknown): e is T.KnownError => declare const affine: unique symbol -export const createUtils = utils.createUtils -export const createMainUtils = ( - effects: T.Effects, -) => createUtils(effects) - type NeverPossible = { [affine]: string } export type NoAny = NeverPossible extends A ? keyof NeverPossible extends keyof A diff --git a/sdk/lib/util/utils.ts b/sdk/lib/util/utils.ts deleted file mode 100644 index 9d424888e..000000000 --- a/sdk/lib/util/utils.ts +++ /dev/null @@ -1,310 +0,0 @@ -import nullIfEmpty from "./nullIfEmpty" -import { - CheckResult, - checkPortListening, - checkWebUrl, -} from "../health/checkFns" -import { - DaemonReturned, - Effects, - EnsureStorePath, - ExtractStore, - ServiceInterfaceId, - PackageId, - ValidIfNoStupidEscape, -} from "../types" -import { GetSystemSmtp } from "./GetSystemSmtp" -import { GetStore, getStore } from "../store/getStore" -import { - MountDependenciesOut, - mountDependencies, -} from "../dependency/mountDependencies" -import { - ManifestId, - VolumeName, - NamedPath, - Path, -} from "../dependency/setupDependencyMounts" -import { MultiHost, Scheme, SingleHost, StaticHost } from "../interfaces/Host" -import { ServiceInterfaceBuilder } from "../interfaces/ServiceInterfaceBuilder" -import { GetServiceInterface, getServiceInterface } from "./getServiceInterface" -import { - GetServiceInterfaces, - getServiceInterfaces, -} from "./getServiceInterfaces" -import * as CP from "node:child_process" -import { promisify } from "node:util" -import { splitCommand } from "./splitCommand" -import { SDKManifest } from "../manifest/ManifestTypes" -import { MountOptions, Overlay, CommandOptions } from "./Overlay" -export type Signals = NodeJS.Signals - -export const SIGTERM: Signals = "SIGTERM" -export const SIGKILL: Signals = "SIGTERM" -export const NO_TIMEOUT = -1 - -const childProcess = { - exec: promisify(CP.exec), - execFile: promisify(CP.execFile), -} -const cp = childProcess - -export type ServiceInterfaceType = "ui" | "p2p" | "api" - -export type Utils< - Manifest extends SDKManifest, - Store, - WrapperOverWrite = { const: never }, -> = { - childProcess: typeof childProcess - createInterface: (options: { - name: string - id: string - description: string - hasPrimary: boolean - disabled: boolean - type: ServiceInterfaceType - username: null | string - path: string - search: Record - schemeOverride: { ssl: Scheme; noSsl: Scheme } | null - masked: boolean - }) => ServiceInterfaceBuilder - getSystemSmtp: () => GetSystemSmtp & WrapperOverWrite - host: { - static: (id: string) => StaticHost - single: (id: string) => SingleHost - multi: (id: string) => MultiHost - } - mountDependencies: < - In extends - | Record>> - | Record> - | Record - | Path, - >( - value: In, - ) => Promise> - serviceInterface: { - getOwn: (id: ServiceInterfaceId) => GetServiceInterface & WrapperOverWrite - get: (opts: { - id: ServiceInterfaceId - packageId: PackageId - }) => GetServiceInterface & WrapperOverWrite - getAllOwn: () => GetServiceInterfaces & WrapperOverWrite - getAll: (opts: { - packageId: PackageId - }) => GetServiceInterfaces & WrapperOverWrite - } - nullIfEmpty: typeof nullIfEmpty - runCommand: ( - imageId: Manifest["images"][number], - command: ValidIfNoStupidEscape | [string, ...string[]], - options: CommandOptions & { - mounts?: { path: string; options: MountOptions }[] - }, - ) => Promise<{ stdout: string | Buffer; stderr: string | Buffer }> - runDaemon: ( - imageId: Manifest["images"][number], - command: ValidIfNoStupidEscape | [string, ...string[]], - options: CommandOptions & { - mounts?: { path: string; options: MountOptions }[] - overlay?: Overlay - }, - ) => Promise - store: { - get: ( - packageId: string, - path: EnsureStorePath, - ) => GetStore & WrapperOverWrite - getOwn: ( - path: EnsureStorePath, - ) => GetStore & WrapperOverWrite - setOwn: ( - path: EnsureStorePath, - value: ExtractStore, - ) => Promise - } -} -export const createUtils = < - Manifest extends SDKManifest, - Store = never, - WrapperOverWrite = { const: never }, ->( - effects: Effects, -): Utils => { - return { - createInterface: (options: { - name: string - id: string - description: string - hasPrimary: boolean - disabled: boolean - type: ServiceInterfaceType - username: null | string - path: string - search: Record - schemeOverride: { ssl: Scheme; noSsl: Scheme } | null - masked: boolean - }) => new ServiceInterfaceBuilder({ ...options, effects }), - childProcess, - getSystemSmtp: () => - new GetSystemSmtp(effects) as GetSystemSmtp & WrapperOverWrite, - - host: { - static: (id: string) => new StaticHost({ id, effects }), - single: (id: string) => new SingleHost({ id, effects }), - multi: (id: string) => new MultiHost({ id, effects }), - }, - nullIfEmpty, - - serviceInterface: { - getOwn: (id: ServiceInterfaceId) => - getServiceInterface(effects, { - id, - packageId: null, - }) as GetServiceInterface & WrapperOverWrite, - get: (opts: { id: ServiceInterfaceId; packageId: PackageId }) => - getServiceInterface(effects, opts) as GetServiceInterface & - WrapperOverWrite, - getAllOwn: () => - getServiceInterfaces(effects, { - packageId: null, - }) as GetServiceInterfaces & WrapperOverWrite, - getAll: (opts: { packageId: PackageId }) => - getServiceInterfaces(effects, opts) as GetServiceInterfaces & - WrapperOverWrite, - }, - store: { - get: ( - packageId: string, - path: EnsureStorePath, - ) => - getStore(effects, path as any, { - packageId, - }) as any, - getOwn: (path: EnsureStorePath) => - getStore(effects, path as any) as any, - setOwn: ( - path: EnsureStorePath, - value: ExtractStore, - ) => effects.store.set({ value, path: path as any }), - }, - - runCommand: async ( - imageId: Manifest["images"][number], - command: ValidIfNoStupidEscape | [string, ...string[]], - options: CommandOptions & { - mounts?: { path: string; options: MountOptions }[] - }, - ): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> => { - const commands = splitCommand(command) - const overlay = await Overlay.of(effects, imageId) - try { - for (let mount of options.mounts || []) { - await overlay.mount(mount.options, mount.path) - } - return await overlay.exec(commands) - } finally { - await overlay.destroy() - } - }, - runDaemon: async ( - imageId: Manifest["images"][number], - command: ValidIfNoStupidEscape | [string, ...string[]], - options: CommandOptions & { - mounts?: { path: string; options: MountOptions }[] - overlay?: Overlay - }, - ): Promise => { - 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 childProcess = await overlay.spawn(commands, { - env: options.env, - }) - const answer = new Promise((resolve, reject) => { - childProcess.stdout.on("data", (data: any) => { - console.log(data.toString()) - }) - childProcess.stderr.on("data", (data: any) => { - console.error(data.toString()) - }) - - childProcess.on("exit", (code: any) => { - if (code === 0) { - return resolve(null) - } - return reject(new Error(`${commands[0]} exited with code ${code}`)) - }) - }) - - const pid = childProcess.pid - return { - async wait() { - const pids = pid ? await psTree(pid, overlay) : [] - try { - return await answer - } finally { - for (const process of pids) { - cp.execFile("kill", [`-9`, String(process)]).catch((_) => {}) - } - } - }, - async term({ signal = SIGTERM, timeout = NO_TIMEOUT } = {}) { - const pids = pid ? await psTree(pid, overlay) : [] - try { - childProcess.kill(signal) - - if (timeout > NO_TIMEOUT) { - const didTimeout = await Promise.race([ - new Promise((resolve) => setTimeout(resolve, timeout)).then( - () => true, - ), - answer.then(() => false), - ]) - if (didTimeout) { - childProcess.kill(SIGKILL) - } - } else { - await answer - } - } finally { - await overlay.destroy() - } - - try { - for (const process of pids) { - await cp.execFile("kill", [`-${signal}`, String(process)]) - } - } finally { - for (const process of pids) { - cp.execFile("kill", [`-9`, String(process)]).catch((_) => {}) - } - } - }, - } - }, - - mountDependencies: < - In extends - | Record>> - | Record> - | Record - | Path, - >( - value: In, - ) => mountDependencies(effects, value), - } -} -function noop(): void {} - -async function psTree(pid: number, overlay: Overlay): Promise { - const { stdout } = await childProcess.exec(`pstree -p ${pid}`) - const regex: RegExp = /\((\d+)\)/g - return [...stdout.toString().matchAll(regex)].map(([_all, pid]) => - parseInt(pid), - ) -}