chore: Remove the utils

This commit is contained in:
J H
2024-03-18 14:31:01 -06:00
parent 8e2dc8b3ee
commit 5f40fd6038
28 changed files with 424 additions and 543 deletions

View File

@@ -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<T extends any[], Then, Else> =
@@ -63,6 +87,17 @@ type AnyNeverCond<T extends any[], Then, Else> =
T extends [any, ...infer U] ? AnyNeverCond<U,Then, Else> :
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<E>() {
return <T>(t: T) => t as T & (E extends MainEffects ? {} : { const: never })
}
export class StartSdk<Manifest extends SDKManifest, Store> {
private constructor(readonly manifest: Manifest) {}
static of() {
@@ -77,7 +112,78 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
build(isReady: AnyNeverCond<[Manifest, Store], "Build not ready", true>) {
return {
serviceInterface: {
getOwn: <E extends Effects>(effects: E, id: ServiceInterfaceId) =>
removeConstType<E>()(
getServiceInterface(effects, {
id,
packageId: null,
}),
),
get: <E extends Effects>(
effects: E,
opts: { id: ServiceInterfaceId; packageId: PackageId },
) => removeConstType<E>()(getServiceInterface(effects, opts)),
getAllOwn: <E extends Effects>(effects: E) =>
removeConstType<E>()(
getServiceInterfaces(effects, {
packageId: null,
}),
),
getAll: <E extends Effects>(
effects: E,
opts: { packageId: PackageId },
) => removeConstType<E>()(getServiceInterfaces(effects, opts)),
},
store: {
get: <E extends Effects, Path extends string = never>(
effects: E,
packageId: string,
path: EnsureStorePath<Store, Path>,
) =>
removeConstType<E>()(
getStore<Store, Path>(effects, path as any, {
packageId,
}),
),
getOwn: <E extends Effects, Path extends string>(
effects: E,
path: EnsureStorePath<Store, Path>,
) => removeConstType<E>()(getStore<Store, Path>(effects, path as any)),
setOwn: <E extends Effects, Path extends string | never>(
effects: E,
path: EnsureStorePath<Store, Path>,
value: ExtractStore<Store, Path>,
) => effects.store.set<Store, Path>({ 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<string, string>
schemeOverride: { ssl: Scheme; noSsl: Scheme } | null
masked: boolean
},
) => new ServiceInterfaceBuilder({ ...options, effects }),
createAction: <
ConfigType extends
| Record<string, any>
@@ -90,13 +196,44 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
},
fn: (options: {
effects: Effects
utils: Utils<Manifest, Store>
input: Type
}) => Promise<ActionResult>,
) => {
const { input, ...rest } = metaData
return createAction<Manifest, Store, ConfigType, Type>(rest, fn, input)
},
getSystemSmtp: <E extends Effects>(effects: E) =>
removeConstType<E>()(new GetSystemSmtp(effects)),
runCommand: async <A extends string>(
effects: Effects,
imageId: Manifest["images"][number],
command: ValidIfNoStupidEscape<A> | [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<ManifestId, Record<VolumeName, Record<NamedPath, Path>>>
| Record<VolumeName, Record<NamedPath, Path>>
| Record<NamedPath, Path>
| Path,
>(
effects: Effects,
value: In,
) => mountDependencies(effects, value),
createDynamicAction: <
ConfigType extends
| Record<string, any>
@@ -106,11 +243,9 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
>(
metaData: (options: {
effects: Effects
utils: Utils<Manifest, Store>
}) => MaybePromise<Omit<ActionMetadata, "input">>,
fn: (options: {
effects: Effects
utils: Utils<Manifest, Store>
input: Type
}) => Promise<ActionResult>,
input: Config<Type, Store> | Config<Type, never>,
@@ -196,9 +331,8 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
) => setupInterfaces(config, fn),
setupMain: (
fn: (o: {
effects: Effects
effects: MainEffects
started(onTerm: () => PromiseLike<void>): PromiseLike<void>
utils: Utils<Manifest, Store, {}>
}) => Promise<Daemons<Manifest, any>>,
) => setupMain<Manifest, Store>(fn),
setupMigrations: <
@@ -259,7 +393,6 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
dependencyConfig: (options: {
effects: Effects
localConfig: LocalConfig
utils: Utils<Manifest, Store>
}) => Promise<void | DeepPartial<RemoteConfig>>
update?: Update<void | DeepPartial<RemoteConfig>, RemoteConfig>
}) {
@@ -343,14 +476,8 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
Migration: {
of: <Version extends ManifestVersion>(options: {
version: Version
up: (opts: {
effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<void>
down: (opts: {
effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<void>
up: (opts: { effects: Effects }) => Promise<void>
down: (opts: { effects: Effects }) => Promise<void>
}) => Migration.of<Manifest, Store, Version>(options),
},
Value: {

View File

@@ -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<Manifest extends SDKManifest, Store, Value> =
| Value
| ((options: {
effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<Value> | Value)
| ((options: { effects: Effects }) => Promise<Value> | Value)
export class CreatedAction<
Manifest extends SDKManifest,
Store,
@@ -27,7 +22,6 @@ export class CreatedAction<
>,
readonly fn: (options: {
effects: Effects
utils: Utils<Manifest, Store>
input: Type
}) => Promise<ActionResult>,
readonly input: Config<Type, Store>,
@@ -44,11 +38,7 @@ export class CreatedAction<
Type extends Record<string, any> = ExtractConfigType<ConfigType>,
>(
metaData: MaybeFn<Manifest, Store, Omit<ActionMetadata, "input">>,
fn: (options: {
effects: Effects
utils: Utils<Manifest, Store>
input: Type
}) => Promise<ActionResult>,
fn: (options: { effects: Effects; input: Type }) => Promise<ActionResult>,
inputConfig: Config<Type, Store> | Config<Type, never>,
) {
return new CreatedAction<Manifest, Store, ConfigType, Type>(
@@ -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<Manifest, Store> }) {
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<Manifest, Store>
}): Promise<ActionMetadata> {
async ActionMetadata(options: { effects: Effects }): Promise<ActionMetadata> {
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,
})
}
}

View File

@@ -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<Manifest extends SDKManifest, Store>(
...createdActions: CreatedAction<Manifest, Store, any>[]
) {
const myActions = async (options: {
effects: Effects
utils: Utils<Manifest, Store>
}) => {
const myActions = async (options: { effects: Effects }) => {
const actions: Record<string, CreatedAction<Manifest, Store, any>> = {}
for (const action of createdActions) {
const actionMetadata = await action.metaData(options)
@@ -24,17 +19,11 @@ export function setupActions<Manifest extends SDKManifest, Store>(
actionsMetadata: ExpectedExports.actionsMetadata
} = {
actions(options: { effects: Effects }) {
const utils = createUtils<Manifest, Store>(options.effects)
return myActions({
...options,
utils,
})
return myActions(options)
},
async actionsMetadata({ effects }: { effects: Effects }) {
const utils = createUtils<Manifest, Store>(effects)
return Promise.all(
createdActions.map((x) => x.ActionMetadata({ effects, utils })),
createdActions.map((x) => x.ActionMetadata({ effects })),
)
},
}

View File

@@ -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<Store> = {
effects: Effects
utils: Utils<any, Store>
}
export type LazyBuild<Store, ExpectedOut> = (
options: LazyBuildOptions<Store>,

View File

@@ -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<ConfigSpecOf<SmtpValue>, 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"]
},
{

View File

@@ -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<A> & Record<string, any>
utils: Utils<Manifest, Store>
dependencies: D.ConfigDependencies<Manifest>
}) => Promise<{
dependenciesReceipt: DependenciesReceipt
@@ -38,7 +36,6 @@ export type Read<
| Config<Record<string, any>, never>,
> = (options: {
effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<void | (ExtractConfigType<A> & Record<string, any>)>
/**
* 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<Manifest>(),
})
if (restart) {
@@ -80,14 +76,10 @@ export function setupConfig<
}
}) as ExpectedExports.setConfig,
getConfig: (async ({ effects }) => {
const myUtils = createUtils<Manifest, Store>(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,
}

View File

@@ -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<Manifest, Store>
}) => Promise<void | DeepPartial<RemoteConfig>>,
readonly update: Update<
void | DeepPartial<RemoteConfig>,
@@ -41,7 +39,6 @@ export class DependencyConfig<
return this.dependencyConfig({
localConfig: options.localConfig as Input,
effects: options.effects,
utils: createUtils<Manifest, Store>(options.effects),
})
}
}

View File

@@ -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> | CheckResult
fn(overlay: Overlay): Promise<CheckResult> | CheckResult
onFirstSuccess?: () => unknown | Promise<unknown>
}) {
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

View File

@@ -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<CheckResult> {
const utils = createUtils(effects)
return Promise.race<CheckResult>([
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) {

View File

@@ -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<CheckResult> => {
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
}

View File

@@ -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"

View File

@@ -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<Manifest, Store>
}) => Promise<void>
down: (opts: {
effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<void>
up: (opts: { effects: Effects }) => Promise<void>
down: (opts: { effects: Effects }) => Promise<void>
},
) {}
static of<
@@ -26,23 +19,17 @@ export class Migration<
Version extends ManifestVersion,
>(options: {
version: Version
up: (opts: {
effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<void>
down: (opts: {
effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<void>
up: (opts: { effects: Effects }) => Promise<void>
down: (opts: { effects: Effects }) => Promise<void>
}) {
return new Migration<Manifest, Store, Version>(options)
}
async up(opts: { effects: Effects; utils: Utils<Manifest, Store> }) {
async up(opts: { effects: Effects }) {
this.up(opts)
}
async down(opts: { effects: Effects; utils: Utils<Manifest, Store> }) {
async down(opts: { effects: Effects }) {
this.down(opts)
}
}

View File

@@ -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<Manifest extends SDKManifest, Store> {
effects,
previousVersion,
}: Parameters<ExpectedExports.init>[0]) {
const utils = createUtils<Manifest, Store>(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<Manifest extends SDKManifest, Store> {
effects,
nextVersion,
}: Parameters<ExpectedExports.uninit>[0]) {
const utils = createUtils<Manifest, Store>(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 })
}
}
}

View File

@@ -1,10 +1,6 @@
import { Effects, ExposeServicePaths, ExposeUiPaths } from "../types"
import { Utils } from "../util/utils"
export type SetupExports<Store> = (opts: {
effects: Effects
utils: Utils<any, Store>
}) =>
export type SetupExports<Store> = (opts: { effects: Effects }) =>
| {
ui: { [k: string]: ExposeUiPaths<Store> }
services: ExposeServicePaths<Store>

View File

@@ -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<Manifest extends SDKManifest, Store>(
} {
return {
init: async (opts) => {
const utils = createUtils<Manifest, Store>(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({

View File

@@ -1,10 +1,8 @@
import { SDKManifest } from "../manifest/ManifestTypes"
import { Effects, ExpectedExports } from "../types"
import { Utils, createUtils } from "../util/utils"
export type InstallFn<Manifest extends SDKManifest, Store> = (opts: {
effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<void>
export class Install<Manifest extends SDKManifest, Store> {
private constructor(readonly fn: InstallFn<Manifest, Store>) {}
@@ -21,7 +19,6 @@ export class Install<Manifest extends SDKManifest, Store> {
if (!previousVersion)
await this.fn({
effects,
utils: createUtils(effects),
})
}
}

View File

@@ -1,10 +1,8 @@
import { SDKManifest } from "../manifest/ManifestTypes"
import { Effects, ExpectedExports } from "../types"
import { Utils, createUtils } from "../util/utils"
export type UninstallFn<Manifest extends SDKManifest, Store> = (opts: {
effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<void>
export class Uninstall<Manifest extends SDKManifest, Store> {
private constructor(readonly fn: UninstallFn<Manifest, Store>) {}
@@ -21,7 +19,6 @@ export class Uninstall<Manifest extends SDKManifest, Store> {
if (!nextVersion)
await this.fn({
effects,
utils: createUtils(effects),
})
}
}

View File

@@ -1,5 +1,5 @@
import { ServiceInterfaceType } from "../StartSdk"
import { Effects } from "../types"
import { ServiceInterfaceType } from "../util/utils"
import { Scheme } from "./Host"
/**

View File

@@ -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<AddressInfo[] & AddressReceipt>
@@ -10,11 +9,7 @@ export type SetInterfaces<
Store,
ConfigInput extends Record<string, any>,
Output extends InterfacesReceipt,
> = (opts: {
effects: Effects
input: null | ConfigInput
utils: Utils<Manifest, Store>
}) => Promise<Output>
> = (opts: { effects: Effects; input: null | ConfigInput }) => Promise<Output>
export type SetupInterfaces = <
Manifest extends SDKManifest,
Store,

View File

@@ -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<number[]> {
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<Id extends string> = `The id '${Id}' is already used`
const runDaemon =
<Manifest extends SDKManifest>() =>
async <A extends string>(
effects: Effects,
imageId: Manifest["images"][number],
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
overlay?: Overlay
},
): Promise<DaemonReturned> => {
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<null>((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<Manifest extends SDKManifest, Ids extends string> {
)
daemonsStarted[daemon.id] = requiredPromise.then(async () => {
const { command, imageId } = daemon
const utils = createUtils<Manifest>(effects)
const child = utils.runDaemon(imageId, command, { env: daemon.env })
const child = runDaemon<Manifest>()(effects, imageId, command, {
env: daemon.env,
})
let currentInput: TriggerInput = {}
const getCurrentInput = () => currentInput
const trigger = (daemon.ready.trigger ?? defaultTrigger)(

View File

@@ -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 = <Manifest extends SDKManifest, Store>(
fn: (o: {
effects: Effects
effects: MainEffects
started(onTerm: () => PromiseLike<void>): PromiseLike<void>
utils: Utils<Manifest, Store, {}>
}) => Promise<Daemons<Manifest, any>>,
): ExpectedExports.main => {
return async (options) => {
const result = await fn({
...options,
utils: createMainUtils<Manifest, Store>(options.effects),
})
const result = await fn(options)
return result
}
}

View File

@@ -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)

View File

@@ -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<never, never>(effects)
const foo = utils.host.multi("foo")
const foo = sdk.host.multi(effects, "foo")
const fooOrigin = await foo.bindPort(80, {
protocol: "http" as const,
})

View File

@@ -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>(): A => {
throw new Error("not implemented")
}
const noop = () => {}
const sdk = StartSdk.of()
.withManifest({} as Manifest)
.withStore<Store>()
.build(true)
describe("Store", () => {
test("types", async () => {
;async () => {
createUtils<Manifest, Store>(todo<Effects>()).store.setOwn("/config", {
sdk.store.setOwn(todo<Effects>(), "/config", {
someValue: "a",
})
createUtils<Manifest, Store>(todo<Effects>()).store.setOwn(
"/config/someValue",
"b",
)
createUtils<Manifest, Store>(todo<Effects>()).store.setOwn("", {
sdk.store.setOwn(todo<Effects>(), "/config/someValue", "b")
sdk.store.setOwn(todo<Effects>(), "", {
config: { someValue: "b" },
})
createUtils<Manifest, Store>(todo<Effects>()).store.setOwn(
sdk.store.setOwn(
todo<Effects>(),
"/config/someValue",
// @ts-expect-error Type is wrong for the setting value
5,
)
createUtils(todo<Effects>()).store.setOwn(
sdk.store.setOwn(
todo<Effects>(),
// @ts-expect-error Path is wrong
"/config/someVae3lue",
"someValue",
@@ -52,49 +56,47 @@ describe("Store", () => {
path: "/config/some2Value",
value: "a",
})
;(await createMainUtils<Manifest, Store>(todo<Effects>())
.store.getOwn("/config/someValue")
;(await sdk.store
.getOwn(todo<MainEffects>(), "/config/someValue")
.const()) satisfies string
;(await createMainUtils<Manifest, Store>(todo<Effects>())
.store.getOwn("/config")
;(await sdk.store
.getOwn(todo<MainEffects>(), "/config")
.const()) satisfies Store["config"]
await createMainUtils(todo<Effects>())
// @ts-expect-error Path is wrong
.store.getOwn("/config/somdsfeValue")
await sdk.store // @ts-expect-error Path is wrong
.getOwn(todo<MainEffects>(), "/config/somdsfeValue")
.const()
/// ----------------- ERRORS -----------------
createUtils<Manifest, Store>(todo<Effects>()).store.setOwn("", {
sdk.store.setOwn(todo<MainEffects>(), "", {
// @ts-expect-error Type is wrong for the setting value
config: { someValue: "notInAOrB" },
})
createUtils<Manifest, Store>(todo<Effects>()).store.setOwn(
sdk.store.setOwn(
todo<MainEffects>(),
"/config/someValue",
// @ts-expect-error Type is wrong for the setting value
"notInAOrB",
)
;(await createUtils<Manifest, Store>(todo<Effects>())
.store.getOwn("/config/someValue")
;(await sdk.store
.getOwn(todo<Effects>(), "/config/someValue")
// @ts-expect-error Const should normally not be callable
.const()) satisfies string
;(await createUtils<Manifest, Store>(todo<Effects>())
.store.getOwn("/config")
;(await sdk.store
.getOwn(todo<Effects>(), "/config")
// @ts-expect-error Const should normally not be callable
.const()) satisfies Store["config"]
await createUtils<Manifest, Store>(todo<Effects>())
// @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<Manifest, Store>(todo<Effects>())
.store.getOwn("/config/someValue")
;(await sdk.store
.getOwn(todo<MainEffects>(), "/config/someValue")
// @ts-expect-error satisfies type is wrong
.const()) satisfies number
;(await createMainUtils(todo<Effects>())
// @ts-expect-error Path is wrong
.store.getOwn("/config/")
;(await sdk.store // @ts-expect-error Path is wrong
.getOwn(todo<MainEffects>(), "/config/")
.const()) satisfies Store["config"]
;(await todo<Effects>().store.get<Store, "/config/someValue">({
path: "/config/someValue",

View File

@@ -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<void>): PromiseLike<void>
}) => Promise<Daemons<any, any>>

View File

@@ -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

View File

@@ -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 = <Manifest extends SDKManifest, Store>(
effects: T.Effects,
) => createUtils<Manifest, Store, {}>(effects)
type NeverPossible = { [affine]: string }
export type NoAny<A> = NeverPossible extends A
? keyof NeverPossible extends keyof A

View File

@@ -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<string, string>
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<ManifestId, Record<VolumeName, Record<NamedPath, Path>>>
| Record<VolumeName, Record<NamedPath, Path>>
| Record<NamedPath, Path>
| Path,
>(
value: In,
) => Promise<MountDependenciesOut<In>>
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: <A extends string>(
imageId: Manifest["images"][number],
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
},
) => Promise<{ stdout: string | Buffer; stderr: string | Buffer }>
runDaemon: <A extends string>(
imageId: Manifest["images"][number],
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
overlay?: Overlay
},
) => Promise<DaemonReturned>
store: {
get: <Path extends string>(
packageId: string,
path: EnsureStorePath<Store, Path>,
) => GetStore<Store, Path> & WrapperOverWrite
getOwn: <Path extends string>(
path: EnsureStorePath<Store, Path>,
) => GetStore<Store, Path> & WrapperOverWrite
setOwn: <Path extends string | never>(
path: EnsureStorePath<Store, Path>,
value: ExtractStore<Store, Path>,
) => Promise<void>
}
}
export const createUtils = <
Manifest extends SDKManifest,
Store = never,
WrapperOverWrite = { const: never },
>(
effects: Effects,
): Utils<Manifest, Store, WrapperOverWrite> => {
return {
createInterface: (options: {
name: string
id: string
description: string
hasPrimary: boolean
disabled: boolean
type: ServiceInterfaceType
username: null | string
path: string
search: Record<string, string>
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: <Path extends string = never>(
packageId: string,
path: EnsureStorePath<Store, Path>,
) =>
getStore<Store, Path>(effects, path as any, {
packageId,
}) as any,
getOwn: <Path extends string>(path: EnsureStorePath<Store, Path>) =>
getStore<Store, Path>(effects, path as any) as any,
setOwn: <Path extends string | never>(
path: EnsureStorePath<Store, Path>,
value: ExtractStore<Store, Path>,
) => effects.store.set<Store, Path>({ value, path: path as any }),
},
runCommand: async <A extends string>(
imageId: Manifest["images"][number],
command: ValidIfNoStupidEscape<A> | [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 <A extends string>(
imageId: Manifest["images"][number],
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
overlay?: Overlay
},
): Promise<DaemonReturned> => {
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<null>((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<ManifestId, Record<VolumeName, Record<NamedPath, Path>>>
| Record<VolumeName, Record<NamedPath, Path>>
| Record<NamedPath, Path>
| Path,
>(
value: In,
) => mountDependencies(effects, value),
}
}
function noop(): void {}
async function psTree(pid: number, overlay: Overlay): Promise<number[]> {
const { stdout } = await childProcess.exec(`pstree -p ${pid}`)
const regex: RegExp = /\((\d+)\)/g
return [...stdout.toString().matchAll(regex)].map(([_all, pid]) =>
parseInt(pid),
)
}