mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
Bugfix/sdk misc (#2847)
* misc sdk fixes * version bump * formatting * add missing dependency to root * alpha.16 and beta.17 * beta.18
This commit is contained in:
@@ -19,7 +19,6 @@ import {
|
||||
SyncOptions,
|
||||
ServiceInterfaceId,
|
||||
PackageId,
|
||||
HealthReceipt,
|
||||
ServiceInterfaceType,
|
||||
Effects,
|
||||
} from "../../base/lib/types"
|
||||
@@ -27,7 +26,7 @@ import * as patterns from "../../base/lib/util/patterns"
|
||||
import { BackupSync, Backups } from "./backup/Backups"
|
||||
import { smtpInputSpec } from "../../base/lib/actions/input/inputSpecConstants"
|
||||
import { CommandController, Daemons } from "./mainFn/Daemons"
|
||||
import { healthCheck, HealthCheckParams } from "./health/HealthCheck"
|
||||
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"
|
||||
@@ -73,7 +72,7 @@ import * as actions from "../../base/lib/actions"
|
||||
import { setupInit } from "./inits/setupInit"
|
||||
import * as fs from "node:fs/promises"
|
||||
|
||||
export const OSVersion = testTypeVersion("0.3.6-alpha.15")
|
||||
export const OSVersion = testTypeVersion("0.3.6-alpha.16")
|
||||
|
||||
// prettier-ignore
|
||||
type AnyNeverCond<T extends any[], Then, Else> =
|
||||
@@ -231,7 +230,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
},
|
||||
command: T.CommandType,
|
||||
options: CommandOptions & {
|
||||
mounts?: { path: string; options: MountOptions }[]
|
||||
mounts?: { mountpoint: string; options: MountOptions }[]
|
||||
},
|
||||
/**
|
||||
* A name to use to refer to the ephemeral subcontainer for debugging purposes
|
||||
@@ -420,11 +419,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
hostnames: string[],
|
||||
algorithm?: T.Algorithm,
|
||||
) => new GetSslCertificate(effects, hostnames, algorithm),
|
||||
HealthCheck: {
|
||||
of(effects: T.Effects, o: Omit<HealthCheckParams, "effects">) {
|
||||
return healthCheck({ effects, ...o })
|
||||
},
|
||||
},
|
||||
HealthCheck,
|
||||
healthCheck: {
|
||||
checkPortListening,
|
||||
checkWebUrl,
|
||||
@@ -677,9 +672,9 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
of(
|
||||
effects: Effects,
|
||||
started: (onTerm: () => PromiseLike<void>) => PromiseLike<null>,
|
||||
healthReceipts: HealthReceipt[],
|
||||
healthChecks: HealthCheck[],
|
||||
) {
|
||||
return Daemons.of<Manifest>({ effects, started, healthReceipts })
|
||||
return Daemons.of<Manifest>({ effects, started, healthChecks })
|
||||
},
|
||||
},
|
||||
SubContainer: {
|
||||
@@ -699,7 +694,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
|
||||
imageId: T.ImageId & keyof Manifest["images"]
|
||||
sharedRun?: boolean
|
||||
},
|
||||
mounts: { options: MountOptions; path: string }[],
|
||||
mounts: { options: MountOptions; mountpoint: string }[],
|
||||
name: string,
|
||||
fn: (subContainer: SubContainer) => Promise<T>,
|
||||
): Promise<T> {
|
||||
@@ -1081,7 +1076,7 @@ export async function runCommand<Manifest extends T.SDKManifest>(
|
||||
image: { imageId: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean },
|
||||
command: T.CommandType,
|
||||
options: CommandOptions & {
|
||||
mounts?: { path: string; options: MountOptions }[]
|
||||
mounts?: { mountpoint: string; options: MountOptions }[]
|
||||
},
|
||||
name?: string,
|
||||
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Effects, HealthCheckId, HealthReceipt } from "../../../base/lib/types"
|
||||
import { Effects, HealthCheckId } from "../../../base/lib/types"
|
||||
import { HealthCheckResult } from "./checkFns/HealthCheckResult"
|
||||
import { Trigger } from "../trigger"
|
||||
import { TriggerInput } from "../trigger/TriggerInput"
|
||||
import { defaultTrigger } from "../trigger/defaultTrigger"
|
||||
import { once, asError } from "../util"
|
||||
import { once, asError, Drop } from "../util"
|
||||
import { object, unknown } from "ts-matches"
|
||||
|
||||
export type HealthCheckParams = {
|
||||
effects: Effects
|
||||
id: HealthCheckId
|
||||
name: string
|
||||
trigger?: Trigger
|
||||
@@ -16,53 +15,110 @@ export type HealthCheckParams = {
|
||||
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(
|
||||
"onFirstSuccess" in o && o.onFirstSuccess
|
||||
? o.onFirstSuccess()
|
||||
: undefined,
|
||||
),
|
||||
)
|
||||
for (
|
||||
let res = await trigger.next();
|
||||
!res.done;
|
||||
res = await trigger.next()
|
||||
) {
|
||||
try {
|
||||
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,
|
||||
result,
|
||||
message: message || "",
|
||||
})
|
||||
currentValue.lastResult = result
|
||||
await triggerFirstSuccess().catch((err) => {
|
||||
console.error(asError(err))
|
||||
})
|
||||
} catch (e) {
|
||||
await o.effects.setHealth({
|
||||
name: o.name,
|
||||
id: o.id,
|
||||
result:
|
||||
performance.now() - start <= gracePeriod ? "starting" : "failure",
|
||||
message: asMessage(e) || "",
|
||||
})
|
||||
currentValue.lastResult = "failure"
|
||||
export class HealthCheck extends Drop {
|
||||
private started: number | null = null
|
||||
private setStarted = (started: number | null) => {
|
||||
this.started = started
|
||||
}
|
||||
private exited = false
|
||||
private exit = () => {
|
||||
this.exited = true
|
||||
}
|
||||
private currentValue: TriggerInput = {}
|
||||
private promise: Promise<void>
|
||||
private constructor(effects: Effects, o: HealthCheckParams) {
|
||||
super()
|
||||
this.promise = Promise.resolve().then(async () => {
|
||||
const getCurrentValue = () => this.currentValue
|
||||
const gracePeriod = o.gracePeriod ?? 5000
|
||||
const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue)
|
||||
const triggerFirstSuccess = once(() =>
|
||||
Promise.resolve(
|
||||
"onFirstSuccess" in o && o.onFirstSuccess
|
||||
? o.onFirstSuccess()
|
||||
: undefined,
|
||||
),
|
||||
)
|
||||
const checkStarted = () =>
|
||||
[
|
||||
this.started,
|
||||
new Promise<void>((resolve) => {
|
||||
this.setStarted = (started: number | null) => {
|
||||
this.started = started
|
||||
resolve()
|
||||
}
|
||||
this.exit = () => {
|
||||
this.exited = true
|
||||
resolve()
|
||||
}
|
||||
}),
|
||||
] as const
|
||||
let triggered = false
|
||||
while (!this.exited) {
|
||||
const [started, changed] = checkStarted()
|
||||
let race:
|
||||
| [Promise<void>]
|
||||
| [Promise<void>, Promise<IteratorResult<unknown, unknown>>] = [
|
||||
changed,
|
||||
]
|
||||
if (started) {
|
||||
race = [...race, trigger.next()]
|
||||
if (triggered) {
|
||||
try {
|
||||
let { result, message } = await o.fn()
|
||||
if (
|
||||
result === "failure" &&
|
||||
performance.now() - started <= gracePeriod
|
||||
)
|
||||
result = "starting"
|
||||
await effects.setHealth({
|
||||
name: o.name,
|
||||
id: o.id,
|
||||
result,
|
||||
message: message || "",
|
||||
})
|
||||
this.currentValue.lastResult = result
|
||||
await triggerFirstSuccess().catch((err) => {
|
||||
console.error(asError(err))
|
||||
})
|
||||
} catch (e) {
|
||||
await effects.setHealth({
|
||||
name: o.name,
|
||||
id: o.id,
|
||||
result:
|
||||
performance.now() - started <= gracePeriod
|
||||
? "starting"
|
||||
: "failure",
|
||||
message: asMessage(e) || "",
|
||||
})
|
||||
this.currentValue.lastResult = "failure"
|
||||
}
|
||||
}
|
||||
} else triggered = false
|
||||
const raced = await Promise.race(race)
|
||||
if (raced) {
|
||||
if (raced.done) break
|
||||
triggered = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return {} as HealthReceipt
|
||||
})
|
||||
}
|
||||
static of(effects: Effects, options: HealthCheckParams): HealthCheck {
|
||||
return new HealthCheck(effects, options)
|
||||
}
|
||||
start() {
|
||||
if (this.started) return
|
||||
this.setStarted(performance.now())
|
||||
}
|
||||
stop() {
|
||||
if (!this.started) return
|
||||
this.setStarted(null)
|
||||
}
|
||||
onDrop(): void {
|
||||
this.exit()
|
||||
}
|
||||
}
|
||||
|
||||
function asMessage(e: unknown) {
|
||||
if (object({ message: unknown }).test(e)) return String(e.message)
|
||||
const value = String(e)
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
import "./checkFns"
|
||||
|
||||
export { HealthCheck } from "./HealthCheck"
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
ISB,
|
||||
IST,
|
||||
types,
|
||||
T,
|
||||
matches,
|
||||
utils,
|
||||
} from "../../base/lib"
|
||||
@@ -21,10 +20,10 @@ export {
|
||||
ISB,
|
||||
IST,
|
||||
types,
|
||||
T,
|
||||
matches,
|
||||
utils,
|
||||
}
|
||||
export * as T from "./types"
|
||||
export { Daemons } from "./mainFn/Daemons"
|
||||
export { SubContainer } from "./util/SubContainer"
|
||||
export { StartSdk } from "./StartSdk"
|
||||
|
||||
@@ -7,18 +7,20 @@ import {
|
||||
SubContainerHandle,
|
||||
SubContainer,
|
||||
} from "../util/SubContainer"
|
||||
import { splitCommand } from "../util"
|
||||
import { Drop, splitCommand } from "../util"
|
||||
import * as cp from "child_process"
|
||||
import * as fs from "node:fs/promises"
|
||||
|
||||
export class CommandController {
|
||||
export class CommandController extends Drop {
|
||||
private constructor(
|
||||
readonly runningAnswer: Promise<unknown>,
|
||||
private state: { exited: boolean },
|
||||
private readonly subcontainer: SubContainer,
|
||||
private process: cp.ChildProcess,
|
||||
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
|
||||
) {}
|
||||
) {
|
||||
super()
|
||||
}
|
||||
static of<Manifest extends T.SDKManifest>() {
|
||||
return async <A extends string>(
|
||||
effects: T.Effects,
|
||||
@@ -33,7 +35,7 @@ export class CommandController {
|
||||
subcontainerName?: string
|
||||
// Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms
|
||||
sigtermTimeout?: number
|
||||
mounts?: { path: string; options: MountOptions }[]
|
||||
mounts?: { mountpoint: string; options: MountOptions }[]
|
||||
runAsInit?: boolean
|
||||
env?:
|
||||
| {
|
||||
@@ -68,7 +70,7 @@ export class CommandController {
|
||||
)
|
||||
try {
|
||||
for (let mount of options.mounts || []) {
|
||||
await subc.mount(mount.options, mount.path)
|
||||
await subc.mount(mount.options, mount.mountpoint)
|
||||
}
|
||||
return subc
|
||||
} catch (e) {
|
||||
@@ -135,37 +137,42 @@ export class CommandController {
|
||||
return new SubContainerHandle(this.subcontainer)
|
||||
}
|
||||
async wait({ timeout = NO_TIMEOUT } = {}) {
|
||||
const self = this.weak()
|
||||
if (timeout > 0)
|
||||
setTimeout(() => {
|
||||
this.term()
|
||||
self.term()
|
||||
}, timeout)
|
||||
try {
|
||||
return await this.runningAnswer
|
||||
return await self.runningAnswer
|
||||
} finally {
|
||||
if (!this.state.exited) {
|
||||
this.process.kill("SIGKILL")
|
||||
if (!self.state.exited) {
|
||||
self.process.kill("SIGKILL")
|
||||
}
|
||||
await this.subcontainer.destroy().catch((_) => {})
|
||||
await self.subcontainer.destroy().catch((_) => {})
|
||||
}
|
||||
}
|
||||
async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) {
|
||||
const self = this.weak()
|
||||
try {
|
||||
if (!this.state.exited) {
|
||||
if (!self.state.exited) {
|
||||
if (signal !== "SIGKILL") {
|
||||
setTimeout(() => {
|
||||
if (!this.state.exited) this.process.kill("SIGKILL")
|
||||
if (!self.state.exited) self.process.kill("SIGKILL")
|
||||
}, timeout)
|
||||
}
|
||||
if (!this.process.kill(signal)) {
|
||||
if (!self.process.kill(signal)) {
|
||||
console.error(
|
||||
`failed to send signal ${signal} to pid ${this.process.pid}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await this.runningAnswer
|
||||
await self.runningAnswer
|
||||
} finally {
|
||||
await this.subcontainer.destroy()
|
||||
await self.subcontainer.destroy()
|
||||
}
|
||||
}
|
||||
onDrop(): void {
|
||||
this.term().catch(console.error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export class Daemon {
|
||||
command: T.CommandType,
|
||||
options: {
|
||||
subcontainerName?: string
|
||||
mounts?: { path: string; options: MountOptions }[]
|
||||
mounts?: { mountpoint: string; options: MountOptions }[]
|
||||
env?:
|
||||
| {
|
||||
[variable: string]: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HealthReceipt, Signals } from "../../../base/lib/types"
|
||||
import { Signals } from "../../../base/lib/types"
|
||||
|
||||
import { HealthCheckResult } from "../health/checkFns"
|
||||
|
||||
@@ -15,6 +15,7 @@ export { CommandController } from "./CommandController"
|
||||
import { HealthDaemon } from "./HealthDaemon"
|
||||
import { Daemon } from "./Daemon"
|
||||
import { CommandController } from "./CommandController"
|
||||
import { HealthCheck } from "../health/HealthCheck"
|
||||
|
||||
export const cpExec = promisify(CP.exec)
|
||||
export const cpExecFile = promisify(CP.execFile)
|
||||
@@ -115,6 +116,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
readonly daemons: Promise<Daemon>[],
|
||||
readonly ids: Ids[],
|
||||
readonly healthDaemons: HealthDaemon[],
|
||||
readonly healthChecks: HealthCheck[],
|
||||
) {}
|
||||
/**
|
||||
* Returns an empty new Daemons class with the provided inputSpec.
|
||||
@@ -129,7 +131,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
static of<Manifest extends T.SDKManifest>(options: {
|
||||
effects: T.Effects
|
||||
started: (onTerm: () => PromiseLike<void>) => PromiseLike<null>
|
||||
healthReceipts: HealthReceipt[]
|
||||
healthChecks: HealthCheck[]
|
||||
}) {
|
||||
return new Daemons<Manifest, never>(
|
||||
options.effects,
|
||||
@@ -137,6 +139,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
options.healthChecks,
|
||||
)
|
||||
}
|
||||
/**
|
||||
@@ -187,28 +190,33 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
daemons,
|
||||
ids,
|
||||
healthDaemons,
|
||||
this.healthChecks,
|
||||
)
|
||||
}
|
||||
|
||||
async build() {
|
||||
const built = {
|
||||
term: async () => {
|
||||
try {
|
||||
for (let result of await Promise.allSettled(
|
||||
this.healthDaemons.map((x) =>
|
||||
x.term({ timeout: x.sigtermTimeout }),
|
||||
),
|
||||
)) {
|
||||
if (result.status === "rejected") {
|
||||
console.error(result.reason)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.effects.setMainStatus({ status: "stopped" })
|
||||
async term() {
|
||||
try {
|
||||
this.healthChecks.forEach((health) => health.stop())
|
||||
for (let result of await Promise.allSettled(
|
||||
this.healthDaemons.map((x) => x.term({ timeout: x.sigtermTimeout })),
|
||||
)) {
|
||||
if (result.status === "rejected") {
|
||||
console.error(result.reason)
|
||||
}
|
||||
},
|
||||
}
|
||||
} finally {
|
||||
this.effects.setMainStatus({ status: "stopped" })
|
||||
}
|
||||
this.started(() => built.term())
|
||||
return built
|
||||
}
|
||||
|
||||
async build() {
|
||||
for (const daemon of this.healthDaemons) {
|
||||
await daemon.updateStatus()
|
||||
}
|
||||
for (const health of this.healthChecks) {
|
||||
health.start()
|
||||
}
|
||||
this.started(() => this.term())
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ export class HealthDaemon {
|
||||
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
|
||||
) {
|
||||
this.readyPromise = new Promise((resolve) => (this.resolveReady = resolve))
|
||||
this.updateStatus()
|
||||
this.dependencies.forEach((d) => d.addWatcher(() => this.updateStatus()))
|
||||
}
|
||||
|
||||
@@ -166,8 +165,8 @@ export class HealthDaemon {
|
||||
} as SetHealth)
|
||||
}
|
||||
|
||||
private async updateStatus() {
|
||||
const healths = this.dependencies.map((d) => d._health)
|
||||
this.changeRunning(healths.every((x) => x.result === "success"))
|
||||
async updateStatus() {
|
||||
const healths = this.dependencies.map((d) => d.running && d._health)
|
||||
this.changeRunning(healths.every((x) => x && x.result === "success"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { MountOptions } from "../util/SubContainer"
|
||||
|
||||
type MountArray = { path: string; options: MountOptions }[]
|
||||
type MountArray = { mountpoint: string; options: MountOptions }[]
|
||||
|
||||
export class Mounts<Manifest extends T.SDKManifest> {
|
||||
private constructor(
|
||||
@@ -12,7 +12,6 @@ export class Mounts<Manifest extends T.SDKManifest> {
|
||||
readonly: boolean
|
||||
}[],
|
||||
readonly assets: {
|
||||
id: Manifest["assets"][number]
|
||||
subpath: string | null
|
||||
mountpoint: string
|
||||
}[],
|
||||
@@ -49,15 +48,12 @@ export class Mounts<Manifest extends T.SDKManifest> {
|
||||
}
|
||||
|
||||
addAssets(
|
||||
/** The ID of the asset directory to mount. This is typically the same as the folder name in your assets directory */
|
||||
id: Manifest["assets"][number],
|
||||
/** The path within the asset directory to mount. Use `null` to mount the entire volume */
|
||||
subpath: string | null,
|
||||
/** Where to mount the asset. e.g. /asset */
|
||||
mountpoint: string,
|
||||
) {
|
||||
this.assets.push({
|
||||
id,
|
||||
subpath,
|
||||
mountpoint,
|
||||
})
|
||||
@@ -102,7 +98,7 @@ export class Mounts<Manifest extends T.SDKManifest> {
|
||||
return ([] as MountArray)
|
||||
.concat(
|
||||
this.volumes.map((v) => ({
|
||||
path: v.mountpoint,
|
||||
mountpoint: v.mountpoint,
|
||||
options: {
|
||||
type: "volume",
|
||||
id: v.id,
|
||||
@@ -113,17 +109,16 @@ export class Mounts<Manifest extends T.SDKManifest> {
|
||||
)
|
||||
.concat(
|
||||
this.assets.map((a) => ({
|
||||
path: a.mountpoint,
|
||||
mountpoint: a.mountpoint,
|
||||
options: {
|
||||
type: "assets",
|
||||
id: a.id,
|
||||
subpath: a.subpath,
|
||||
},
|
||||
})),
|
||||
)
|
||||
.concat(
|
||||
this.dependencies.map((d) => ({
|
||||
path: d.mountpoint,
|
||||
mountpoint: d.mountpoint,
|
||||
options: {
|
||||
type: "pointer",
|
||||
packageId: d.dependencyId,
|
||||
|
||||
@@ -16,10 +16,8 @@ import { execSync } from "child_process"
|
||||
export function setupManifest<
|
||||
Id extends string,
|
||||
VolumesTypes extends VolumeId,
|
||||
AssetTypes extends VolumeId,
|
||||
Manifest extends {
|
||||
id: Id
|
||||
assets: AssetTypes[]
|
||||
volumes: VolumesTypes[]
|
||||
} & SDKManifest,
|
||||
>(manifest: Manifest & SDKManifest): Manifest {
|
||||
@@ -31,12 +29,10 @@ export function buildManifest<
|
||||
Version extends string,
|
||||
Dependencies extends Record<string, unknown>,
|
||||
VolumesTypes extends VolumeId,
|
||||
AssetTypes extends VolumeId,
|
||||
ImagesTypes extends ImageId,
|
||||
Manifest extends {
|
||||
dependencies: Dependencies
|
||||
id: Id
|
||||
assets: AssetTypes[]
|
||||
images: Record<ImagesTypes, SDKImageInputSpec>
|
||||
volumes: VolumesTypes[]
|
||||
},
|
||||
|
||||
@@ -18,7 +18,9 @@ export class GetStore<Store, StoreValue> {
|
||||
return this.effects.store.get<Store, StoreValue>({
|
||||
...this.options,
|
||||
path: extractJsonPath(this.path),
|
||||
callback: () => this.effects.constRetry(),
|
||||
callback:
|
||||
this.effects.constRetry &&
|
||||
(() => this.effects.constRetry && this.effects.constRetry()),
|
||||
})
|
||||
}
|
||||
/**
|
||||
|
||||
2
sdk/package/lib/types.ts
Normal file
2
sdk/package/lib/types.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "../../base/lib/types"
|
||||
export { HealthCheck } from "./health"
|
||||
26
sdk/package/lib/util/Drop.ts
Normal file
26
sdk/package/lib/util/Drop.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export abstract class Drop {
|
||||
private static weak: { [id: number]: Drop } = {}
|
||||
private static registry = new FinalizationRegistry((id: number) => {
|
||||
Drop.weak[id].drop()
|
||||
})
|
||||
private static idCtr: number = 0
|
||||
private id: number
|
||||
private ref: { id: number } | WeakRef<{ id: number }>
|
||||
protected constructor() {
|
||||
this.id = Drop.idCtr++
|
||||
this.ref = { id: this.id }
|
||||
Drop.weak[this.id] = this.weak()
|
||||
Drop.registry.register(this, this.id, this)
|
||||
}
|
||||
protected weak(): this {
|
||||
const weak = Object.assign(Object.create(Object.getPrototypeOf(this)), this)
|
||||
weak.ref = new WeakRef(this.ref)
|
||||
return weak
|
||||
}
|
||||
abstract onDrop(): void
|
||||
drop(): void {
|
||||
this.onDrop()
|
||||
Drop.registry.unregister(this)
|
||||
delete Drop.weak[this.id]
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,9 @@ export class GetSslCertificate {
|
||||
return this.effects.getSslCertificate({
|
||||
hostnames: this.hostnames,
|
||||
algorithm: this.algorithm,
|
||||
callback: () => this.effects.constRetry(),
|
||||
callback:
|
||||
this.effects.constRetry &&
|
||||
(() => this.effects.constRetry && this.effects.constRetry()),
|
||||
})
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -130,14 +130,14 @@ export class SubContainer implements ExecSpawnable {
|
||||
static async with<T>(
|
||||
effects: T.Effects,
|
||||
image: { imageId: T.ImageId; sharedRun?: boolean },
|
||||
mounts: { options: MountOptions; path: string }[],
|
||||
mounts: { options: MountOptions; mountpoint: string }[],
|
||||
name: string,
|
||||
fn: (subContainer: SubContainer) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const subContainer = await SubContainer.of(effects, image, name)
|
||||
try {
|
||||
for (let mount of mounts) {
|
||||
await subContainer.mount(mount.options, mount.path)
|
||||
await subContainer.mount(mount.options, mount.mountpoint)
|
||||
}
|
||||
return await fn(subContainer)
|
||||
} finally {
|
||||
@@ -166,7 +166,7 @@ export class SubContainer implements ExecSpawnable {
|
||||
? options.subpath
|
||||
: `/${options.subpath}`
|
||||
: "/"
|
||||
const from = `/media/startos/assets/${options.id}${subpath}`
|
||||
const from = `/media/startos/assets/${subpath}`
|
||||
|
||||
await fs.mkdir(from, { recursive: true })
|
||||
await fs.mkdir(path, { recursive: true })
|
||||
@@ -449,7 +449,6 @@ export type MountOptionsVolume = {
|
||||
|
||||
export type MountOptionsAssets = {
|
||||
type: "assets"
|
||||
id: string
|
||||
subpath: string | null
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as YAML from "yaml"
|
||||
import * as TOML from "@iarna/toml"
|
||||
import * as T from "../../../base/lib/types"
|
||||
import * as fs from "node:fs/promises"
|
||||
import { asError } from "../../../base/lib/util"
|
||||
import { asError, partialDiff } from "../../../base/lib/util"
|
||||
|
||||
const previousPath = /(.+?)\/([^/]*)$/
|
||||
|
||||
@@ -101,6 +101,7 @@ function fileMerge(...args: any[]): any {
|
||||
* ```
|
||||
*/
|
||||
export class FileHelper<A> {
|
||||
private consts: (() => void)[] = []
|
||||
protected constructor(
|
||||
readonly path: string,
|
||||
readonly writeData: (dataIn: A) => string,
|
||||
@@ -108,27 +109,37 @@ export class FileHelper<A> {
|
||||
readonly validate: (value: unknown) => A,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Accepts structured data and overwrites the existing file on disk.
|
||||
*/
|
||||
private async writeFile(data: A): Promise<null> {
|
||||
private async writeFileRaw(data: string): Promise<null> {
|
||||
const parent = previousPath.exec(this.path)
|
||||
if (parent) {
|
||||
await fs.mkdir(parent[1], { recursive: true })
|
||||
}
|
||||
|
||||
await fs.writeFile(this.path, this.writeData(data))
|
||||
await fs.writeFile(this.path, data)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private async readFile(): Promise<unknown> {
|
||||
/**
|
||||
* Accepts structured data and overwrites the existing file on disk.
|
||||
*/
|
||||
private async writeFile(data: A): Promise<null> {
|
||||
return await this.writeFileRaw(this.writeData(data))
|
||||
}
|
||||
|
||||
private async readFileRaw(): Promise<string | null> {
|
||||
if (!(await exists(this.path))) {
|
||||
return null
|
||||
}
|
||||
return this.readData(
|
||||
await fs.readFile(this.path).then((data) => data.toString("utf-8")),
|
||||
)
|
||||
return await fs.readFile(this.path).then((data) => data.toString("utf-8"))
|
||||
}
|
||||
|
||||
private async readFile(): Promise<unknown> {
|
||||
const raw = await this.readFileRaw()
|
||||
if (raw === null) {
|
||||
return raw
|
||||
}
|
||||
return this.readData(raw)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,7 +154,14 @@ export class FileHelper<A> {
|
||||
private async readConst(effects: T.Effects): Promise<A | null> {
|
||||
const watch = this.readWatch()
|
||||
const res = await watch.next()
|
||||
watch.next().then(effects.constRetry)
|
||||
if (effects.constRetry) {
|
||||
if (!this.consts.includes(effects.constRetry))
|
||||
this.consts.push(effects.constRetry)
|
||||
watch.next().then(() => {
|
||||
this.consts = this.consts.filter((a) => a === effects.constRetry)
|
||||
effects.constRetry && effects.constRetry()
|
||||
})
|
||||
}
|
||||
return res.value
|
||||
}
|
||||
|
||||
@@ -213,17 +231,35 @@ export class FileHelper<A> {
|
||||
/**
|
||||
* Accepts full structured data and overwrites the existing file on disk if it exists.
|
||||
*/
|
||||
async write(data: A) {
|
||||
return await this.writeFile(this.validate(data))
|
||||
async write(effects: T.Effects, data: A) {
|
||||
await this.writeFile(this.validate(data))
|
||||
if (effects.constRetry && this.consts.includes(effects.constRetry))
|
||||
throw new Error(`Canceled: write after const: ${this.path}`)
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts partial structured data and performs a merge with the existing file on disk.
|
||||
*/
|
||||
async merge(data: T.DeepPartial<A>) {
|
||||
const fileData = (await this.readFile()) || null
|
||||
const mergeData = fileMerge(fileData, data)
|
||||
return await this.writeFile(this.validate(mergeData))
|
||||
async merge(effects: T.Effects, data: T.DeepPartial<A>) {
|
||||
const fileDataRaw = await this.readFileRaw()
|
||||
let fileData: any = fileDataRaw === null ? null : this.readData(fileDataRaw)
|
||||
try {
|
||||
fileData = this.validate(fileData)
|
||||
} catch (_) {}
|
||||
const mergeData = this.validate(fileMerge({}, fileData, data))
|
||||
const toWrite = this.writeData(mergeData)
|
||||
if (toWrite !== fileDataRaw) {
|
||||
this.writeFile(mergeData)
|
||||
if (effects.constRetry && this.consts.includes(effects.constRetry)) {
|
||||
const diff = partialDiff(fileData, mergeData as any)
|
||||
if (!diff) {
|
||||
return null
|
||||
}
|
||||
throw new Error(`Canceled: write after const: ${this.path}`)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "../../../base/lib/util"
|
||||
export { GetSslCertificate } from "./GetSslCertificate"
|
||||
|
||||
export { hostnameInfoToAddress } from "../../../base/lib/util/Hostname"
|
||||
export { Drop } from "./Drop"
|
||||
|
||||
23
sdk/package/package-lock.json
generated
23
sdk/package/package-lock.json
generated
@@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.3.6-beta.14",
|
||||
"version": "0.3.6-beta.18",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.3.6-beta.14",
|
||||
"version": "0.3.6-beta.18",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@noble/curves": "^1.4.0",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"deep-equality-data-structures": "^1.5.1",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"mime-types": "^2.1.35",
|
||||
@@ -1802,6 +1803,15 @@
|
||||
"integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/deep-equality-data-structures": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/deep-equality-data-structures/-/deep-equality-data-structures-1.5.1.tgz",
|
||||
"integrity": "sha512-P7zsL2/AbZIGHDxbo/LLEhCp11AttRp8GvzXOXudqMT/qiGCLo/pyI4lAZvjUZyQnlIbPna3fv8DMsuRvLt4ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-hash": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
@@ -3238,6 +3248,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/object-hash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.3.6-beta.14",
|
||||
"version": "0.3.6-beta.18",
|
||||
"description": "Software development kit to facilitate packaging services for StartOS",
|
||||
"main": "./package/lib/index.js",
|
||||
"types": "./package/lib/index.d.ts",
|
||||
@@ -35,6 +35,7 @@
|
||||
"mime-types": "^2.1.35",
|
||||
"ts-matches": "^6.2.1",
|
||||
"yaml": "^2.2.2",
|
||||
"deep-equality-data-structures": "^1.5.1",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@noble/curves": "^1.4.0",
|
||||
"@noble/hashes": "^1.4.0"
|
||||
|
||||
Reference in New Issue
Block a user