misc improvements (#2836)

* misc improvements

* kill proc before destroying subcontainer fs

* version bump

* beta.11

* use bind mount explicitly

* Update sdk/base/lib/Effects.ts

Co-authored-by: Dominion5254 <musashidisciple@proton.me>

---------

Co-authored-by: Dominion5254 <musashidisciple@proton.me>
This commit is contained in:
Aiden McClelland
2025-02-21 15:08:22 -07:00
committed by GitHub
parent 40d194672b
commit 80461a78b0
36 changed files with 358 additions and 143 deletions

View File

@@ -26,7 +26,7 @@ import {
import * as patterns from "../../base/lib/util/patterns"
import { BackupSync, Backups } from "./backup/Backups"
import { smtpInputSpec } from "../../base/lib/actions/input/inputSpecConstants"
import { Daemons } from "./mainFn/Daemons"
import { CommandController, Daemons } from "./mainFn/Daemons"
import { healthCheck, HealthCheckParams } from "./health/HealthCheck"
import { checkPortListening } from "./health/checkFns/checkPortListening"
import { checkWebUrl, runHealthScript } from "./health/checkFns"
@@ -71,6 +71,7 @@ import { GetInput } from "../../base/lib/actions/setupActions"
import { Run } from "../../base/lib/actions/setupActions"
import * as actions from "../../base/lib/actions"
import { setupInit } from "./inits/setupInit"
import * as fs from "node:fs/promises"
export const SDKVersion = testTypeVersion("0.3.6")
@@ -124,6 +125,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
effects.getServicePortForward(...args),
clearBindings: (effects, ...args) => effects.clearBindings(...args),
getContainerIp: (effects, ...args) => effects.getContainerIp(...args),
getOsIp: (effects, ...args) => effects.getOsIp(...args),
getSslKey: (effects, ...args) => effects.getSslKey(...args),
setDataVersion: (effects, ...args) => effects.setDataVersion(...args),
getDataVersion: (effects, ...args) => effects.getDataVersion(...args),
@@ -219,6 +221,8 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
of: (effects: Effects, id: string) => new MultiHost({ id, effects }),
},
nullIfEmpty,
useEntrypoint: (overrideCmd?: string[]) =>
new T.UseEntrypoint(overrideCmd),
runCommand: async <A extends string>(
effects: Effects,
image: {
@@ -234,13 +238,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
*/
name?: string,
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> => {
return runCommand<Manifest>(
effects,
image,
command,
options,
name || (Array.isArray(command) ? command.join(" ") : command),
)
return runCommand<Manifest>(effects, image, command, options, name)
},
/**
* @description Use this class to create an Action. By convention, each Action should receive its own file.
@@ -1081,18 +1079,37 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
export async function runCommand<Manifest extends T.SDKManifest>(
effects: Effects,
image: { imageId: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean },
command: string | [string, ...string[]],
command: T.CommandType,
options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
},
name: string,
name?: string,
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> {
const commands = splitCommand(command)
let commands: string[]
if (command instanceof T.UseEntrypoint) {
const imageMeta: T.ImageMetadata = await fs
.readFile(`/media/startos/images/${image.imageId}.json`, {
encoding: "utf8",
})
.catch(() => "{}")
.then(JSON.parse)
commands = imageMeta.entrypoint ?? []
commands.concat(...(command.overridCmd ?? imageMeta.cmd ?? []))
} else commands = splitCommand(command)
return SubContainer.with(
effects,
image,
options.mounts || [],
name,
name ||
commands
.map((c) => {
if (c.includes(" ")) {
return `"${c.replace(/"/g, `\"`)}"`
} else {
return c
}
})
.join(" "),
(subcontainer) => subcontainer.exec(commands),
)
}

View File

@@ -11,14 +11,17 @@ export type HealthCheckParams = {
id: HealthCheckId
name: string
trigger?: Trigger
gracePeriod?: number
fn(): Promise<HealthCheckResult> | HealthCheckResult
onFirstSuccess?: () => unknown | Promise<unknown>
}
export function healthCheck(o: HealthCheckParams) {
new Promise(async () => {
const start = performance.now()
let currentValue: TriggerInput = {}
const getCurrentValue = () => currentValue
const gracePeriod = o.gracePeriod ?? 5000
const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue)
const triggerFirstSuccess = once(() =>
Promise.resolve(
@@ -33,7 +36,9 @@ export function healthCheck(o: HealthCheckParams) {
res = await trigger.next()
) {
try {
const { result, message } = await o.fn()
let { result, message } = await o.fn()
if (result === "failure" && performance.now() - start <= gracePeriod)
result = "starting"
await o.effects.setHealth({
name: o.name,
id: o.id,
@@ -48,7 +53,8 @@ export function healthCheck(o: HealthCheckParams) {
await o.effects.setHealth({
name: o.name,
id: o.id,
result: "failure",
result:
performance.now() - start <= gracePeriod ? "starting" : "failure",
message: asMessage(e) || "",
})
currentValue.lastResult = "failure"

View File

@@ -9,6 +9,7 @@ import {
} from "../util/SubContainer"
import { splitCommand } from "../util"
import * as cp from "child_process"
import * as fs from "node:fs/promises"
export class CommandController {
private constructor(
@@ -45,7 +46,17 @@ export class CommandController {
onStderr?: (chunk: Buffer | string | any) => void
},
) => {
const commands = splitCommand(command)
let commands: string[]
if (command instanceof T.UseEntrypoint) {
const imageMeta: T.ImageMetadata = await fs
.readFile(`/media/startos/images/${subcontainer.imageId}.json`, {
encoding: "utf8",
})
.catch(() => "{}")
.then(JSON.parse)
commands = imageMeta.entrypoint ?? []
commands.concat(...(command.overridCmd ?? imageMeta.cmd ?? []))
} else commands = splitCommand(command)
const subc =
subcontainer instanceof SubContainer
? subcontainer
@@ -55,10 +66,15 @@ export class CommandController {
subcontainer,
options?.subcontainerName || commands.join(" "),
)
for (let mount of options.mounts || []) {
await subc.mount(mount.options, mount.path)
try {
for (let mount of options.mounts || []) {
await subc.mount(mount.options, mount.path)
}
return subc
} catch (e) {
await subc.destroy()
throw e
}
return subc
})()
try {

View File

@@ -38,6 +38,12 @@ export type Ready = {
fn: (
spawnable: ExecSpawnable,
) => Promise<HealthCheckResult> | HealthCheckResult
/**
* A duration in milliseconds to treat a failing health check as "starting"
*
* defaults to 5000
*/
gracePeriod?: number
trigger?: Trigger
}

View File

@@ -25,6 +25,7 @@ export class HealthDaemon {
private _health: HealthCheckResult = { result: "starting", message: null }
private healthWatchers: Array<() => unknown> = []
private running = false
private started?: number
private resolveReady: (() => void) | undefined
private readyPromise: Promise<void>
constructor(
@@ -75,6 +76,7 @@ export class HealthDaemon {
if (newStatus) {
;(await this.daemon).start()
this.started = performance.now()
this.setupHealthCheck()
} else {
;(await this.daemon).stop()
@@ -146,14 +148,21 @@ export class HealthDaemon {
this._health = health
this.healthWatchers.forEach((watcher) => watcher())
const display = this.ready.display
const result = health.result
if (!display) {
return
}
let result = health.result
if (
result === "failure" &&
this.started &&
performance.now() - this.started <= (this.ready.gracePeriod ?? 5000)
)
result = "starting"
await this.effects.setHealth({
...health,
id: this.id,
name: display,
result,
} as SetHealth)
}

View File

@@ -46,6 +46,15 @@ export interface ExecSpawnable {
* @see {@link ExecSpawnable}
*/
export class SubContainer implements ExecSpawnable {
private static finalizationEffects: { effects?: T.Effects } = {}
private static registry = new FinalizationRegistry((guid: string) => {
if (this.finalizationEffects.effects) {
this.finalizationEffects.effects.subcontainer
.destroyFs({ guid })
.catch((e) => console.error("failed to cleanup SubContainer", guid, e))
}
})
private leader: cp.ChildProcess
private leaderExited: boolean = false
private waitProc: () => Promise<null>
@@ -55,6 +64,8 @@ export class SubContainer implements ExecSpawnable {
readonly rootfs: string,
readonly guid: T.Guid,
) {
if (!SubContainer.finalizationEffects.effects)
SubContainer.finalizationEffects.effects = effects
this.leaderExited = false
this.leader = cp.spawn("start-cli", ["subcontainer", "launch", rootfs], {
killSignal: "SIGKILL",
@@ -94,6 +105,8 @@ export class SubContainer implements ExecSpawnable {
imageId,
name,
})
const res = new SubContainer(effects, imageId, rootfs, guid)
SubContainer.registry.register(res, guid, res)
const shared = ["dev", "sys"]
if (!!sharedRun) {
@@ -111,7 +124,7 @@ export class SubContainer implements ExecSpawnable {
await execFile("mount", ["--rbind", from, to])
}
return new SubContainer(effects, imageId, rootfs, guid)
return res
}
static async with<T>(
@@ -202,6 +215,7 @@ export class SubContainer implements ExecSpawnable {
const guid = this.guid
await this.killLeader()
await this.effects.subcontainer.destroyFs({ guid })
SubContainer.registry.unregister(this)
return null
}
}
@@ -224,8 +238,9 @@ export class SubContainer implements ExecSpawnable {
.catch(() => "{}")
.then(JSON.parse)
let extra: string[] = []
let user = imageMeta.user || "root"
if (options?.user) {
extra.push(`--user=${options.user}`)
user = options.user
delete options.user
}
let workdir = imageMeta.workdir || "/"
@@ -239,6 +254,7 @@ export class SubContainer implements ExecSpawnable {
"subcontainer",
"exec",
`--env=/media/startos/images/${this.imageId}.env`,
`--user=${user}`,
`--workdir=${workdir}`,
...extra,
this.rootfs,
@@ -294,15 +310,16 @@ export class SubContainer implements ExecSpawnable {
options?: CommandOptions,
): Promise<cp.ChildProcessWithoutNullStreams> {
await this.waitProc()
const imageMeta: any = await fs
const imageMeta: T.ImageMetadata = await fs
.readFile(`/media/startos/images/${this.imageId}.json`, {
encoding: "utf8",
})
.catch(() => "{}")
.then(JSON.parse)
let extra: string[] = []
let user = imageMeta.user || "root"
if (options?.user) {
extra.push(`--user=${options.user}`)
user = options.user
delete options.user
}
let workdir = imageMeta.workdir || "/"
@@ -318,6 +335,7 @@ export class SubContainer implements ExecSpawnable {
"subcontainer",
"launch",
`--env=/media/startos/images/${this.imageId}.env`,
`--user=${user}`,
`--workdir=${workdir}`,
...extra,
this.rootfs,
@@ -336,15 +354,16 @@ export class SubContainer implements ExecSpawnable {
options: CommandOptions & StdioOptions = { stdio: "inherit" },
): Promise<cp.ChildProcess> {
await this.waitProc()
const imageMeta: any = await fs
const imageMeta: T.ImageMetadata = await fs
.readFile(`/media/startos/images/${this.imageId}.json`, {
encoding: "utf8",
})
.catch(() => "{}")
.then(JSON.parse)
let extra: string[] = []
if (options.user) {
extra.push(`--user=${options.user}`)
let user = imageMeta.user || "root"
if (options?.user) {
user = options.user
delete options.user
}
let workdir = imageMeta.workdir || "/"
@@ -358,6 +377,7 @@ export class SubContainer implements ExecSpawnable {
"subcontainer",
"exec",
`--env=/media/startos/images/${this.imageId}.env`,
`--user=${user}`,
`--workdir=${workdir}`,
...extra,
this.rootfs,

View File

@@ -1,12 +1,12 @@
{
"name": "@start9labs/start-sdk",
"version": "0.3.6-beta.9",
"version": "0.3.6-beta.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@start9labs/start-sdk",
"version": "0.3.6-beta.9",
"version": "0.3.6-beta.11",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^2.2.5",
@@ -28,7 +28,7 @@
"ts-node": "^10.9.1",
"ts-pegjs": "^4.2.1",
"tsx": "^4.7.1",
"typescript": "^5.0.4"
"typescript": "^5.7.3"
}
},
"../base": {
@@ -4438,16 +4438,17 @@
}
},
"node_modules/typescript": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=12.20"
"node": ">=14.17"
}
},
"node_modules/update-browserslist-db": {

View File

@@ -1,6 +1,6 @@
{
"name": "@start9labs/start-sdk",
"version": "0.3.6-beta.9",
"version": "0.3.6-beta.11",
"description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts",
@@ -55,6 +55,6 @@
"ts-node": "^10.9.1",
"ts-pegjs": "^4.2.1",
"tsx": "^4.7.1",
"typescript": "^5.0.4"
"typescript": "^5.7.3"
}
}

View File

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