addHealthCheck instead of additionalHealthChecks for Daemons (#2962)

* addHealthCheck on Daemons

* fix bug that prevents domains without protocols from being deleted

* fixes from testing

* version bump

* add sdk version to UI

* fix useEntrypoint

* fix dependency health check error display

* minor fixes

* beta.29

* fixes from testing

* beta.30

* set /etc/os-release (#2918)

* remove check-monitor from kiosk (#2059)

* add units for progress (#2693)

* use new progress type

* alpha.7

* fix up pwa stuff

* fix wormhole-squashfs and prune boot (#2964)

* don't exit on expected errors

* use bash

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Aiden McClelland
2025-06-17 23:50:01 +00:00
committed by GitHub
parent f5688e077a
commit 3ec4db0225
100 changed files with 846 additions and 757 deletions

View File

@@ -3,7 +3,6 @@
export type GetOsVersionParams = {
sourceVersion: string | null
targetVersion: string | null
includePrerelease: boolean | null
serverId: string | null
platform: string | null
}

View File

@@ -32,4 +32,5 @@ export type Manifest = {
hardwareRequirements: HardwareRequirements
gitHash?: GitHash
osVersion: string
sdkVersion: string | null
}

View File

@@ -1,3 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ProgressUnits } from "./ProgressUnits"
export type Progress = null | boolean | { done: number; total: number | null }
export type Progress =
| null
| boolean
| { done: number; total: number | null; units: ProgressUnits | null }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ProgressUnits = "bytes" | "steps"

View File

@@ -157,6 +157,7 @@ export { PathOrUrl } from "./PathOrUrl"
export { Percentage } from "./Percentage"
export { ProcedureId } from "./ProcedureId"
export { Progress } from "./Progress"
export { ProgressUnits } from "./ProgressUnits"
export { Public } from "./Public"
export { RecoverySource } from "./RecoverySource"
export { RegistryAsset } from "./RegistryAsset"

View File

@@ -106,7 +106,7 @@ export class UseEntrypoint {
export function isUseEntrypoint(
command: CommandType,
): command is UseEntrypoint {
return typeof command === "object" && "ENTRYPOINT" in command
return typeof command === "object" && "USE_ENTRYPOINT" in command
}
export type CommandType = string | [string, ...string[]] | UseEntrypoint

View File

@@ -17,7 +17,6 @@ import * as patterns from "../../base/lib/util/patterns"
import { BackupSync, Backups } from "./backup/Backups"
import { smtpInputSpec } from "../../base/lib/actions/input/inputSpecConstants"
import { Daemon, Daemons } from "./mainFn/Daemons"
import { HealthCheck } from "./health/HealthCheck"
import { checkPortListening } from "./health/checkFns/checkPortListening"
import { checkWebUrl, runHealthScript } from "./health/checkFns"
import { List } from "../../base/lib/actions/input/builder/list"
@@ -25,10 +24,7 @@ import { SetupBackupsParams, setupBackups } from "./backup/setupBackups"
import { setupMain } from "./mainFn"
import { defaultTrigger } from "./trigger/defaultTrigger"
import { changeOnFirstSuccess, cooldownTrigger } from "./trigger"
import {
UpdateServiceInterfaces,
setupServiceInterfaces,
} from "../../base/lib/interfaces/setupInterfaces"
import { setupServiceInterfaces } from "../../base/lib/interfaces/setupInterfaces"
import { successFailure } from "./trigger/successFailure"
import { MultiHost, Scheme } from "../../base/lib/interfaces/Host"
import { ServiceInterfaceBuilder } from "../../base/lib/interfaces/ServiceInterfaceBuilder"
@@ -45,17 +41,13 @@ import { splitCommand } from "./util"
import { Mounts } from "./mainFn/Mounts"
import { setupDependencies } from "../../base/lib/dependencies/setupDependencies"
import * as T from "../../base/lib/types"
import {
ExtendedVersion,
testTypeVersion,
VersionRange,
} from "../../base/lib/exver"
import { testTypeVersion } from "../../base/lib/exver"
import {
CheckDependencies,
checkDependencies,
} from "../../base/lib/dependencies/dependencies"
import { GetSslCertificate } from "./util"
import { getDataVersion, setDataVersion, VersionGraph } from "./version"
import { getDataVersion, setDataVersion } from "./version"
import { MaybeFn } from "../../base/lib/actions/setupActions"
import { GetInput } from "../../base/lib/actions/setupActions"
import { Run } from "../../base/lib/actions/setupActions"
@@ -68,7 +60,7 @@ import {
setupOnUninit,
} from "../../base/lib/inits"
export const OSVersion = testTypeVersion("0.4.0-alpha.6")
export const OSVersion = testTypeVersion("0.4.0-alpha.7")
// prettier-ignore
type AnyNeverCond<T extends any[], Then, Else> =
@@ -95,7 +87,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
| "clearServiceInterfaces"
| "bind"
| "getHostInfo"
type MainUsedEffects = "setMainStatus" | "setHealth"
type MainUsedEffects = "setMainStatus"
type CallbackEffects =
| "child"
| "constRetry"
@@ -129,6 +121,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
shutdown: (effects, ...args) => effects.shutdown(...args),
getDependencies: (effects, ...args) => effects.getDependencies(...args),
getStatus: (effects, ...args) => effects.getStatus(...args),
setHealth: (effects, ...args) => effects.setHealth(...args),
}
return {
@@ -454,7 +447,6 @@ export class StartSdk<Manifest extends T.SDKManifest> {
hostnames: string[],
algorithm?: T.Algorithm,
) => new GetSslCertificate(effects, hostnames, algorithm),
HealthCheck,
healthCheck: {
checkPortListening,
checkWebUrl,
@@ -652,19 +644,12 @@ export class StartSdk<Manifest extends T.SDKManifest> {
successFailure,
},
Mounts: {
of() {
return Mounts.of<Manifest>()
},
of: Mounts.of<Manifest>,
},
Backups: {
volumes: (
...volumeNames: Array<Manifest["volumes"][number] & string>
) => Backups.withVolumes<Manifest>(...volumeNames),
addSets: (
...options: BackupSync<Manifest["volumes"][number] & string>[]
) => Backups.withSyncs<Manifest>(...options),
withOptions: (options?: Partial<SyncOptions>) =>
Backups.withOptions<Manifest>(options),
ofVolumes: Backups.ofVolumes<Manifest>,
ofSyncs: Backups.ofSyncs<Manifest>,
withOptions: Backups.withOptions<Manifest>,
},
InputSpec: {
/**
@@ -705,10 +690,11 @@ export class StartSdk<Manifest extends T.SDKManifest> {
Daemons: {
of(
effects: Effects,
started: (onTerm: () => PromiseLike<void>) => PromiseLike<null>,
healthChecks: HealthCheck[],
started:
| ((onTerm: () => PromiseLike<void>) => PromiseLike<null>)
| null,
) {
return Daemons.of<Manifest>({ effects, started, healthChecks })
return Daemons.of<Manifest>({ effects, started })
},
},
SubContainer: {

View File

@@ -31,10 +31,10 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
private postRestore = async (effects: BackupEffects) => {},
) {}
static withVolumes<M extends T.SDKManifest = never>(
static ofVolumes<M extends T.SDKManifest = never>(
...volumeNames: Array<M["volumes"][number]>
): Backups<M> {
return Backups.withSyncs(
return Backups.ofSyncs(
...volumeNames.map((srcVolume) => ({
dataPath: `/media/startos/volumes/${srcVolume}/` as const,
backupPath: `/media/startos/backup/volumes/${srcVolume}/` as const,
@@ -42,7 +42,7 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
)
}
static withSyncs<M extends T.SDKManifest = never>(
static ofSyncs<M extends T.SDKManifest = never>(
...syncs: BackupSync<M["volumes"][number]>[]
) {
return syncs.reduce((acc, x) => acc.addSync(x), new Backups<M>())
@@ -112,11 +112,9 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
...options,
})
}
addSync(sync: BackupSync<M["volumes"][0]>) {
this.backupSet.push({
...sync,
options: { ...this.options, ...sync.options },
})
this.backupSet.push(sync)
return this
}

View File

@@ -19,7 +19,7 @@ export function setupBackups<M extends T.SDKManifest>(
if (options instanceof Function) {
backupsFactory = options
} else {
backupsFactory = async () => Backups.withVolumes(...options)
backupsFactory = async () => Backups.ofVolumes(...options)
}
const answer: SetupBackupsRes = {
get createBackup() {

View File

@@ -12,7 +12,6 @@ export type HealthCheckParams = {
trigger?: Trigger
gracePeriod?: number
fn(): Promise<HealthCheckResult> | HealthCheckResult
onFirstSuccess?: () => unknown | Promise<unknown>
}
export class HealthCheck extends Drop {
@@ -32,13 +31,6 @@ export class HealthCheck extends Drop {
const getCurrentValue = () => this.currentValue
const gracePeriod = o.gracePeriod ?? 10_000
const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue)
const triggerFirstSuccess = once(() =>
Promise.resolve(
"onFirstSuccess" in o && o.onFirstSuccess
? o.onFirstSuccess()
: undefined,
),
)
const checkStarted = () =>
[
this.started,
@@ -78,9 +70,6 @@ export class HealthCheck extends Drop {
message: message || "",
})
this.currentValue.lastResult = result
await triggerFirstSuccess().catch((err) => {
console.error(asError(err))
})
} catch (e) {
await effects.setHealth({
name: o.name,

View File

@@ -2,44 +2,49 @@ import { DEFAULT_SIGTERM_TIMEOUT } from "."
import { NO_TIMEOUT, SIGTERM } from "../../../base/lib/types"
import * as T from "../../../base/lib/types"
import { MountOptions, SubContainer } from "../util/SubContainer"
import { SubContainer } from "../util/SubContainer"
import { Drop, splitCommand } from "../util"
import * as cp from "child_process"
import * as fs from "node:fs/promises"
import { Mounts } from "./Mounts"
import { DaemonCommandType } from "./Daemons"
import { DaemonCommandType, ExecCommandOptions, ExecFnOptions } from "./Daemons"
export class CommandController<Manifest extends T.SDKManifest> extends Drop {
export class CommandController<
Manifest extends T.SDKManifest,
C extends SubContainer<Manifest> | null,
> extends Drop {
private constructor(
readonly runningAnswer: Promise<null>,
private state: { exited: boolean },
private readonly subcontainer: SubContainer<Manifest>,
private readonly subcontainer: C,
private process: cp.ChildProcess | AbortController,
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
) {
super()
}
static of<Manifest extends T.SDKManifest>() {
static of<
Manifest extends T.SDKManifest,
C extends SubContainer<Manifest> | null,
>() {
return async (
effects: T.Effects,
subcontainer: SubContainer<Manifest>,
exec: DaemonCommandType,
subcontainer: C,
exec: DaemonCommandType<Manifest, C>,
) => {
try {
if ("fn" in exec) {
const abort = new AbortController()
const cell: { ctrl: CommandController<Manifest> } = {
ctrl: new CommandController(
const cell: { ctrl: CommandController<Manifest, C> } = {
ctrl: new CommandController<Manifest, C>(
exec.fn(subcontainer, abort).then(async (command) => {
if (command && !abort.signal.aborted) {
Object.assign(
cell.ctrl,
await CommandController.of<Manifest>()(
effects,
subcontainer,
command,
),
)
if (subcontainer && command && !abort.signal.aborted) {
const newCtrl = (
await CommandController.of<
Manifest,
SubContainer<Manifest>
>()(effects, subcontainer, command as ExecCommandOptions)
).leak()
Object.assign(cell.ctrl, newCtrl)
return await cell.ctrl.runningAnswer
} else {
cell.ctrl.state.exited = true
@@ -57,7 +62,7 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
let commands: string[]
if (T.isUseEntrypoint(exec.command)) {
const imageMeta: T.ImageMetadata = await fs
.readFile(`/media/startos/images/${subcontainer.imageId}.json`, {
.readFile(`/media/startos/images/${subcontainer!.imageId}.json`, {
encoding: "utf8",
})
.catch(() => "{}")
@@ -70,11 +75,11 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
let childProcess: cp.ChildProcess
if (exec.runAsInit) {
childProcess = await subcontainer.launch(commands, {
childProcess = await subcontainer!.launch(commands, {
env: exec.env,
})
} else {
childProcess = await subcontainer.spawn(commands, {
childProcess = await subcontainer!.spawn(commands, {
env: exec.env,
stdio: exec.onStdout || exec.onStderr ? "pipe" : "inherit",
})
@@ -108,7 +113,7 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
})
})
return new CommandController(
return new CommandController<Manifest, C>(
answer,
state,
subcontainer,
@@ -116,7 +121,7 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
exec.sigtermTimeout,
)
} catch (e) {
await subcontainer.destroy()
await subcontainer?.destroy()
throw e
}
}
@@ -144,7 +149,7 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
if (this.process instanceof AbortController) this.process.abort()
else this.process.kill("SIGKILL")
}
await this.subcontainer.destroy()
await this.subcontainer?.destroy()
}
}
async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) {
@@ -178,7 +183,7 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
])
else await this.runningAnswer
} finally {
await this.subcontainer.destroy()
await this.subcontainer?.destroy()
}
}
onDrop(): void {

View File

@@ -17,14 +17,17 @@ const MAX_TIMEOUT_MS = 30000
* and the others state of running, where it will keep a living running command
*/
export class Daemon<Manifest extends T.SDKManifest> extends Drop {
private commandController: CommandController<Manifest> | null = null
export class Daemon<
Manifest extends T.SDKManifest,
C extends SubContainer<Manifest> | null = SubContainer<Manifest> | null,
> extends Drop {
private commandController: CommandController<Manifest, C> | null = null
private shouldBeRunning = false
protected exitedSuccess = false
private onExitFns: ((success: boolean) => void)[] = []
protected constructor(
private subcontainer: SubContainer<Manifest>,
private startCommand: (() => Promise<CommandController<Manifest>>) | null,
private subcontainer: C,
private startCommand: () => Promise<CommandController<Manifest, C>>,
readonly oneshot: boolean = false,
) {
super()
@@ -33,17 +36,20 @@ export class Daemon<Manifest extends T.SDKManifest> extends Drop {
return this.oneshot
}
static of<Manifest extends T.SDKManifest>() {
return async (
return async <C extends SubContainer<Manifest> | null>(
effects: T.Effects,
subcontainer: SubContainer<Manifest>,
exec: DaemonCommandType | null,
subcontainer: C,
exec: DaemonCommandType<Manifest, C>,
) => {
if (subcontainer.isOwned()) subcontainer = subcontainer.rc()
const startCommand = exec
? () =>
CommandController.of<Manifest>()(effects, subcontainer.rc(), exec)
: null
return new Daemon(subcontainer, startCommand)
let subc: SubContainer<Manifest> | null = subcontainer
if (subcontainer && subcontainer.isOwned()) subc = subcontainer.rc()
const startCommand = () =>
CommandController.of<Manifest, C>()(
effects,
(subc?.rc() ?? null) as C,
exec,
)
return new Daemon(subc, startCommand)
}
}
async start() {
@@ -53,7 +59,7 @@ export class Daemon<Manifest extends T.SDKManifest> extends Drop {
this.shouldBeRunning = true
let timeoutCounter = 0
;(async () => {
while (this.startCommand && this.shouldBeRunning) {
while (this.shouldBeRunning) {
if (this.commandController)
await this.commandController
.term({})
@@ -106,10 +112,10 @@ export class Daemon<Manifest extends T.SDKManifest> extends Drop {
.catch((e) => console.error(asError(e)))
this.commandController = null
this.onExitFns = []
await this.subcontainer.destroy()
await this.subcontainer?.destroy()
}
subcontainerRc(): SubContainerRc<Manifest> {
return this.subcontainer.rc()
subcontainerRc(): SubContainerRc<Manifest> | null {
return this.subcontainer?.rc() ?? null
}
onExit(fn: (success: boolean) => void) {
this.onExitFns.push(fn)

View File

@@ -37,9 +37,7 @@ export type Ready = {
})
* ```
*/
fn: (
subcontainer: SubContainer<Manifest>,
) => Promise<HealthCheckResult> | HealthCheckResult
fn: () => Promise<HealthCheckResult> | HealthCheckResult
/**
* A duration in milliseconds to treat a failing health check as "starting"
*
@@ -65,30 +63,40 @@ export type ExecCommandOptions = {
onStderr?: (chunk: Buffer | string | any) => void
}
export type ExecFnOptions = {
export type ExecFnOptions<
Manifest extends T.SDKManifest,
C extends SubContainer<Manifest> | null,
> = {
fn: (
subcontainer: SubContainer<Manifest>,
subcontainer: C,
abort: AbortController,
) => Promise<ExecCommandOptions | null>
) => Promise<C extends null ? null : ExecCommandOptions | null>
// Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms
sigtermTimeout?: number
}
export type DaemonCommandType = ExecCommandOptions | ExecFnOptions
export type DaemonCommandType<
Manifest extends T.SDKManifest,
C extends SubContainer<Manifest> | null,
> = ExecFnOptions<Manifest, C> | (C extends null ? never : ExecCommandOptions)
type NewDaemonParams<Manifest extends T.SDKManifest> = {
type NewDaemonParams<
Manifest extends T.SDKManifest,
C extends SubContainer<Manifest> | null,
> = {
/** What to run as the daemon: either an async fn or a commandline command to run in the subcontainer */
exec: DaemonCommandType | null
/** Information about the subcontainer in which the daemon runs */
subcontainer: SubContainer<Manifest>
exec: DaemonCommandType<Manifest, C>
/** The subcontainer in which the daemon runs */
subcontainer: C
}
type AddDaemonParams<
Manifest extends T.SDKManifest,
Ids extends string,
Id extends string,
C extends SubContainer<Manifest> | null,
> = (
| NewDaemonParams<Manifest>
| NewDaemonParams<Manifest, C>
| {
daemon: Daemon<Manifest>
}
@@ -102,8 +110,15 @@ type AddOneshotParams<
Manifest extends T.SDKManifest,
Ids extends string,
Id extends string,
> = NewDaemonParams<Manifest> & {
exec: DaemonCommandType
C extends SubContainer<Manifest> | null,
> = NewDaemonParams<Manifest, C> & {
exec: DaemonCommandType<Manifest, C>
/** An array of IDs of prior daemons whose successful initializations are required before this daemon will initialize */
requires: Exclude<Ids, Id>[]
}
type AddHealthCheckParams<Ids extends string, Id extends string> = {
ready: Ready
/** An array of IDs of prior daemons whose successful initializations are required before this daemon will initialize */
requires: Exclude<Ids, Id>[]
}
@@ -111,7 +126,7 @@ type AddOneshotParams<
type ErrorDuplicateId<Id extends string> = `The id '${Id}' is already used`
export const runCommand = <Manifest extends T.SDKManifest>() =>
CommandController.of<Manifest>()
CommandController.of<Manifest, SubContainer<Manifest>>()
/**
* A class for defining and controlling the service daemons
@@ -141,11 +156,12 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
{
private constructor(
readonly effects: T.Effects,
readonly started: (onTerm: () => PromiseLike<void>) => PromiseLike<null>,
readonly started:
| ((onTerm: () => PromiseLike<void>) => PromiseLike<null>)
| null,
readonly daemons: Promise<Daemon<Manifest>>[],
readonly ids: Ids[],
readonly healthDaemons: HealthDaemon<Manifest>[],
readonly healthChecks: HealthCheck[],
) {}
/**
* Returns an empty new Daemons class with the provided inputSpec.
@@ -154,13 +170,18 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
*
* Daemons run in the order they are defined, with latter daemons being capable of
* depending on prior daemons
* @param options
*
* @param effects
*
* @param started
* @returns
*/
static of<Manifest extends T.SDKManifest>(options: {
effects: T.Effects
started: (onTerm: () => PromiseLike<void>) => PromiseLike<null>
healthChecks: HealthCheck[]
/**
* A closure to run once the system is launched. If you are in main, provide the `started` argument you receive from the function arguments
*/
started: ((onTerm: () => PromiseLike<void>) => PromiseLike<null>) | null
}) {
return new Daemons<Manifest, never>(
options.effects,
@@ -168,7 +189,6 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
[],
[],
[],
options.healthChecks,
)
}
/**
@@ -177,19 +197,19 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
* @param options
* @returns a new Daemons object
*/
addDaemon<Id extends string>(
addDaemon<Id extends string, C extends SubContainer<Manifest> | null>(
// prettier-ignore
id:
"" extends Id ? never :
ErrorDuplicateId<Id> extends Id ? never :
Id extends Ids ? ErrorDuplicateId<Id> :
Id,
options: AddDaemonParams<Manifest, Ids, Id>,
options: AddDaemonParams<Manifest, Ids, Id, C>,
) {
const daemon =
"daemon" in options
? Promise.resolve(options.daemon)
: Daemon.of<Manifest>()(
: Daemon.of<Manifest>()<C>(
this.effects,
options.subcontainer,
options.exec,
@@ -201,11 +221,10 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
.filter((x) => x >= 0)
.map((id) => this.healthDaemons[id]),
id,
this.ids,
options.ready,
this.effects,
)
const daemons = this.daemons.concat(daemon)
const daemons = [...this.daemons, daemon]
const ids = [...this.ids, id] as (Ids | Id)[]
const healthDaemons = [...this.healthDaemons, healthDaemon]
return new Daemons<Manifest, Ids | Id>(
@@ -214,7 +233,6 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
daemons,
ids,
healthDaemons,
this.healthChecks,
)
}
@@ -225,7 +243,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
* @param options
* @returns a new Daemons object
*/
addOneshot<Id extends string>(
addOneshot<Id extends string, C extends SubContainer<Manifest> | null>(
id: "" extends Id
? never
: ErrorDuplicateId<Id> extends Id
@@ -233,9 +251,9 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
: Id extends Ids
? ErrorDuplicateId<Id>
: Id,
options: AddOneshotParams<Manifest, Ids, Id>,
options: AddOneshotParams<Manifest, Ids, Id, C>,
) {
const daemon = Oneshot.of<Manifest>()(
const daemon = Oneshot.of<Manifest>()<C>(
this.effects,
options.subcontainer,
options.exec,
@@ -247,11 +265,10 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
.filter((x) => x >= 0)
.map((id) => this.healthDaemons[id]),
id,
this.ids,
"EXIT_SUCCESS",
this.effects,
)
const daemons = this.daemons.concat(daemon)
const daemons = [...this.daemons, daemon]
const ids = [...this.ids, id] as (Ids | Id)[]
const healthDaemons = [...this.healthDaemons, healthDaemon]
return new Daemons<Manifest, Ids | Id>(
@@ -260,13 +277,95 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
daemons,
ids,
healthDaemons,
this.healthChecks,
)
}
/**
* Returns the complete list of daemons, including a new HealthCheck defined here
* @param id
* @param options
* @returns a new Daemons object
*/
addHealthCheck<Id extends string>(
id: "" extends Id
? never
: ErrorDuplicateId<Id> extends Id
? never
: Id extends Ids
? ErrorDuplicateId<Id>
: Id,
options: AddHealthCheckParams<Ids, Id>,
) {
const healthDaemon = new HealthDaemon<Manifest>(
null,
options.requires
.map((x) => this.ids.indexOf(x))
.filter((x) => x >= 0)
.map((id) => this.healthDaemons[id]),
id,
options.ready,
this.effects,
)
const daemons = [...this.daemons]
const ids = [...this.ids, id] as (Ids | Id)[]
const healthDaemons = [...this.healthDaemons, healthDaemon]
return new Daemons<Manifest, Ids | Id>(
this.effects,
this.started,
daemons,
ids,
healthDaemons,
)
}
/**
* Runs the entire system until all daemons have returned `ready`.
* @param id
* @param options
* @returns a new Daemons object
*/
async runUntilSuccess(timeout: number | null) {
let resolve = (_: void) => {}
const res = new Promise<void>((res, rej) => {
resolve = res
if (timeout)
setTimeout(() => {
const notReady = this.healthDaemons
.filter((d) => !d.isReady)
.map((d) => d.id)
rej(new Error(`Timed out waiting for ${notReady}`))
}, timeout)
})
const daemon = Oneshot.of()(this.effects, null, {
fn: async () => {
resolve()
return null
},
})
const healthDaemon = new HealthDaemon<Manifest>(
daemon,
[...this.healthDaemons],
"__RUN_UNTIL_SUCCESS",
"EXIT_SUCCESS",
this.effects,
)
const daemons = await new Daemons<Manifest, Ids>(
this.effects,
this.started,
[...this.daemons, daemon],
this.ids,
[...this.healthDaemons, healthDaemon],
).build()
try {
await res
} finally {
await daemons.term()
}
return null
}
async term() {
try {
this.healthChecks.forEach((health) => health.stop())
for (let result of await Promise.allSettled(
this.healthDaemons.map((x) => x.term()),
)) {
@@ -283,10 +382,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
for (const daemon of this.healthDaemons) {
await daemon.init()
}
for (const health of this.healthChecks) {
health.start()
}
this.started(() => this.term())
this.started?.(() => this.term())
return this
}
}

View File

@@ -6,6 +6,7 @@ import { SetHealth, Effects, SDKManifest } from "../../../base/lib/types"
import { DEFAULT_SIGTERM_TIMEOUT } from "."
import { asError } from "../../../base/lib/util/asError"
import { Oneshot } from "./Oneshot"
import { SubContainer } from "../util/SubContainer"
const oncePromise = <T>() => {
let resolve: (value: T) => void
@@ -30,16 +31,22 @@ export class HealthDaemon<Manifest extends SDKManifest> {
private running = false
private started?: number
private resolveReady: (() => void) | undefined
private resolvedReady: boolean = false
private readyPromise: Promise<void>
constructor(
private readonly daemon: Promise<Daemon<Manifest>>,
private readonly daemon: Promise<Daemon<Manifest>> | null,
private readonly dependencies: HealthDaemon<Manifest>[],
readonly id: string,
readonly ids: string[],
readonly ready: Ready | typeof EXIT_SUCCESS,
readonly effects: Effects,
) {
this.readyPromise = new Promise((resolve) => (this.resolveReady = resolve))
this.readyPromise = new Promise(
(resolve) =>
(this.resolveReady = () => {
resolve()
this.resolvedReady = true
}),
)
this.dependencies.forEach((d) => d.addWatcher(() => this.updateStatus()))
}
@@ -52,7 +59,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
this.running = false
this.healthCheckCleanup?.()
await this.daemon.then((d) =>
await this.daemon?.then((d) =>
d.term({
...termOptions,
}),
@@ -74,11 +81,13 @@ export class HealthDaemon<Manifest extends SDKManifest> {
this.running = newStatus
if (newStatus) {
;(await this.daemon).start()
this.started = performance.now()
console.debug(`Launching ${this.id}...`)
this.setupHealthCheck()
;(await this.daemon)?.start()
this.started = performance.now()
} else {
;(await this.daemon).stop()
console.debug(`Stopping ${this.id}...`)
;(await this.daemon)?.stop()
this.turnOffHealthCheck()
this.setHealth({ result: "starting", message: null })
@@ -88,10 +97,19 @@ export class HealthDaemon<Manifest extends SDKManifest> {
private healthCheckCleanup: (() => null) | null = null
private turnOffHealthCheck() {
this.healthCheckCleanup?.()
this.resolvedReady = false
this.readyPromise = new Promise(
(resolve) =>
(this.resolveReady = () => {
resolve()
this.resolvedReady = true
}),
)
}
private async setupHealthCheck() {
const daemon = await this.daemon
daemon.onExit((success) => {
daemon?.onExit((success) => {
if (success && this.ready === "EXIT_SUCCESS") {
this.setHealth({ result: "success", message: null })
} else if (!success) {
@@ -122,28 +140,17 @@ export class HealthDaemon<Manifest extends SDKManifest> {
!res.done;
res = await Promise.race([status, trigger.next()])
) {
const handle = (await this.daemon).subcontainerRc()
try {
const response: HealthCheckResult = await Promise.resolve(
this.ready.fn(handle),
).catch((err) => {
console.error(asError(err))
return {
result: "failure",
message: "message" in err ? err.message : String(err),
}
})
if (
this.resolveReady &&
(response.result === "success" || response.result === "disabled")
) {
this.resolveReady()
const response: HealthCheckResult = await Promise.resolve(
this.ready.fn(),
).catch((err) => {
console.error(asError(err))
return {
result: "failure",
message: "message" in err ? err.message : String(err),
}
await this.setHealth(response)
} finally {
await handle.destroy()
}
})
await this.setHealth(response)
}
}).catch((err) => console.error(`Daemon ${this.id} failed: ${err}`))
@@ -158,10 +165,18 @@ export class HealthDaemon<Manifest extends SDKManifest> {
return this.readyPromise
}
get isReady() {
return this.resolvedReady
}
private async setHealth(health: HealthCheckResult) {
const changed = this._health.result !== health.result
this._health = health
if (this.resolveReady && health.result === "success") {
this.resolveReady()
}
if (changed) this.healthWatchers.forEach((watcher) => watcher())
if (this.ready === "EXIT_SUCCESS") return
this.healthWatchers.forEach((watcher) => watcher())
const display = this.ready.display
if (!display) {
return
@@ -182,8 +197,18 @@ export class HealthDaemon<Manifest extends SDKManifest> {
}
async updateStatus() {
const healths = this.dependencies.map((d) => d.running && d._health)
this.changeRunning(healths.every((x) => x && x.result === "success"))
const healths = this.dependencies.map((d) => ({
health: d.running && d._health,
id: d.id,
}))
const waitingOn = healths.filter(
(h) => !h.health || h.health.result !== "success",
)
if (waitingOn.length)
console.debug(
`daemon ${this.id} waiting on ${waitingOn.map((w) => w.id)}`,
)
this.changeRunning(!waitingOn.length)
}
async init() {

View File

@@ -10,19 +10,25 @@ import { DaemonCommandType } from "./Daemons"
* unlike Daemon, does not restart on success
*/
export class Oneshot<Manifest extends T.SDKManifest> extends Daemon<Manifest> {
export class Oneshot<
Manifest extends T.SDKManifest,
C extends SubContainer<Manifest> | null = SubContainer<Manifest> | null,
> extends Daemon<Manifest, C> {
static of<Manifest extends T.SDKManifest>() {
return async (
return async <C extends SubContainer<Manifest> | null>(
effects: T.Effects,
subcontainer: SubContainer<Manifest>,
exec: DaemonCommandType | null,
subcontainer: C,
exec: DaemonCommandType<Manifest, C>,
) => {
if (subcontainer.isOwned()) subcontainer = subcontainer.rc()
const startCommand = exec
? () =>
CommandController.of<Manifest>()(effects, subcontainer.rc(), exec)
: null
return new Oneshot(subcontainer, startCommand, true)
let subc: SubContainer<Manifest> | null = subcontainer
if (subcontainer && subcontainer.isOwned()) subc = subcontainer.rc()
const startCommand = () =>
CommandController.of<Manifest, C>()(
effects,
(subc?.rc() ?? null) as C,
exec,
)
return new Oneshot<Manifest, C>(subcontainer, startCommand, true)
}
}
}

View File

@@ -6,7 +6,7 @@ import {
} from "../../../base/lib/types/ManifestTypes"
import { OSVersion } from "../StartSdk"
import { VersionGraph } from "../version/VersionGraph"
import { execSync } from "child_process"
import { version as sdkVersion } from "../../package.json"
/**
* @description Use this function to define critical information about your package
@@ -55,6 +55,7 @@ export function buildManifest<
return {
...manifest,
osVersion: manifest.osVersion ?? OSVersion,
sdkVersion,
version: versions.current.options.version,
releaseNotes: versions.current.options.releaseNotes,
satisfies: versions.current.options.satisfies || [],

View File

@@ -5,18 +5,18 @@ export abstract class Drop {
if (weak) weak.drop()
})
private static idCtr: number = 0
private id: number
private ref: { id: number } | WeakRef<{ id: number }>
private dropId?: number
private dropRef?: { id: number } | WeakRef<{ id: number }>
protected constructor() {
this.id = Drop.idCtr++
this.ref = { id: this.id }
this.dropId = Drop.idCtr++
this.dropRef = { id: this.dropId }
const weak = this.weak()
Drop.weak[this.id] = weak
Drop.registry.register(this.ref, this.id, this.ref)
Drop.weak[this.dropId] = weak
Drop.registry.register(this.dropRef, this.dropId, this.dropRef)
return new Proxy(this, {
set(target: any, prop, value) {
if (prop === "ref") return false
if (prop === "dropRef" || prop == "dropId") return false
target[prop] = value
;(weak as any)[prop] = value
return true
@@ -26,13 +26,21 @@ export abstract class Drop {
protected register() {}
protected weak(): this {
const weak = Object.assign(Object.create(Object.getPrototypeOf(this)), this)
weak.ref = new WeakRef(this.ref)
if (this.dropRef) weak.ref = new WeakRef(this.dropRef)
return weak
}
abstract onDrop(): void
drop(): void {
if (!this.dropRef || !this.dropId) return
this.onDrop()
Drop.registry.unregister(this.ref)
delete Drop.weak[this.id]
this.leak()
}
leak(): this {
if (!this.dropRef || !this.dropId) return this
Drop.registry.unregister(this.dropRef)
delete Drop.weak[this.dropId]
delete this.dropRef
delete this.dropId
return this
}
}

View File

@@ -340,9 +340,17 @@ export class FileHelper<A> {
/**
* Accepts full structured data and overwrites the existing file on disk if it exists.
*/
async write(effects: T.Effects, data: T.AllowReadonly<A> | A) {
async write(
effects: T.Effects,
data: T.AllowReadonly<A> | A,
options: { allowWriteAfterConst?: boolean } = {},
) {
await this.writeFile(this.validate(data))
if (effects.constRetry && this.consts.includes(effects.constRetry))
if (
!options.allowWriteAfterConst &&
effects.constRetry &&
this.consts.includes(effects.constRetry)
)
throw new Error(`Canceled: write after const: ${this.path}`)
return null
}
@@ -350,7 +358,11 @@ export class FileHelper<A> {
/**
* Accepts partial structured data and performs a merge with the existing file on disk.
*/
async merge(effects: T.Effects, data: T.AllowReadonly<T.DeepPartial<A>>) {
async merge(
effects: T.Effects,
data: T.AllowReadonly<T.DeepPartial<A>>,
options: { allowWriteAfterConst?: boolean } = {},
) {
const fileDataRaw = await this.readFileRaw()
let fileData: any = fileDataRaw === null ? null : this.readData(fileDataRaw)
try {
@@ -360,7 +372,11 @@ export class FileHelper<A> {
const toWrite = this.writeData(mergeData)
if (toWrite !== fileDataRaw) {
this.writeFile(mergeData)
if (effects.constRetry && this.consts.includes(effects.constRetry)) {
if (
!options.allowWriteAfterConst &&
effects.constRetry &&
this.consts.includes(effects.constRetry)
) {
const diff = partialDiff(fileData, mergeData as any)
if (!diff) {
return null

View File

@@ -7,6 +7,7 @@ import {
InitScriptOrFn,
UninitFn,
UninitScript,
UninitScriptOrFn,
} from "../../../base/lib/inits"
import { Graph, Vertex, once } from "../util"
import { IMPOSSIBLE, VersionInfo } from "./VersionInfo"
@@ -171,11 +172,11 @@ export class VersionGraph<CurrentVersion extends string>
/**
* A script to run only on fresh install
*/
preInstall?: InitScript | InitFn
preInstall?: InitScriptOrFn<"install">
/**
* A script to run only on uninstall
*/
uninstall?: UninitScript | UninitFn
uninstall?: UninitScriptOrFn
}) {
return new VersionGraph(
options.current,

View File

@@ -1,12 +1,12 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.27",
"version": "0.4.0-beta.30",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.27",
"version": "0.4.0-beta.30",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.27",
"version": "0.4.0-beta.30",
"description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts",
@@ -22,14 +22,14 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/Start9Labs/start-sdk.git"
"url": "git+https://github.com/Start9Labs/start-os.git"
},
"author": "Start9 Labs",
"license": "MIT",
"bugs": {
"url": "https://github.com/Start9Labs/start-sdk/issues"
"url": "https://github.com/Start9Labs/start-os/issues"
},
"homepage": "https://github.com/Start9Labs/start-sdk#readme",
"homepage": "https://github.com/Start9Labs/start-os#readme",
"dependencies": {
"isomorphic-fetch": "^3.0.0",
"mime": "^4.0.7",

View File

@@ -12,7 +12,8 @@
"skipLibCheck": true,
"module": "commonjs",
"outDir": "../dist",
"target": "es2021"
"target": "es2021",
"resolveJsonModule": true
},
"include": ["lib/**/*", "../base/lib/util/Hostname.ts"],
"exclude": ["lib/**/*.spec.ts", "lib/**/*.gen.ts", "list", "node_modules"]