mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
add support for "oneshot" daemons (#2917)
* add support for "oneshot" daemons * add docs for oneshot * add support for runAsInit in daemon.of * beta.13
This commit is contained in:
@@ -56,29 +56,27 @@ export class MainLoop {
|
||||
if (jsMain) {
|
||||
throw new Error("Unreachable")
|
||||
}
|
||||
const daemon = new Daemon(async () => {
|
||||
const subcontainer = await DockerProcedureContainer.createSubContainer(
|
||||
effects,
|
||||
this.system.manifest.id,
|
||||
this.system.manifest.main,
|
||||
this.system.manifest.volumes,
|
||||
`Main - ${currentCommand.join(" ")}`,
|
||||
)
|
||||
return CommandController.of()(
|
||||
this.effects,
|
||||
subcontainer,
|
||||
currentCommand,
|
||||
{
|
||||
runAsInit: true,
|
||||
env: {
|
||||
TINI_SUBREAPER: "true",
|
||||
},
|
||||
sigtermTimeout: utils.inMs(
|
||||
this.system.manifest.main["sigterm-timeout"],
|
||||
),
|
||||
const subcontainer = await DockerProcedureContainer.createSubContainer(
|
||||
effects,
|
||||
this.system.manifest.id,
|
||||
this.system.manifest.main,
|
||||
this.system.manifest.volumes,
|
||||
`Main - ${currentCommand.join(" ")}`,
|
||||
)
|
||||
const daemon = await Daemon.of()(
|
||||
this.effects,
|
||||
subcontainer,
|
||||
currentCommand,
|
||||
{
|
||||
runAsInit: true,
|
||||
env: {
|
||||
TINI_SUBREAPER: "true",
|
||||
},
|
||||
)
|
||||
})
|
||||
sigtermTimeout: utils.inMs(
|
||||
this.system.manifest.main["sigterm-timeout"],
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
daemon.start()
|
||||
return {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { asError } from "../../../base/lib/util/asError"
|
||||
import { Drop } from "../util"
|
||||
import { ExecSpawnable, MountOptions, SubContainer } from "../util/SubContainer"
|
||||
import { ExecSpawnable, SubContainer } from "../util/SubContainer"
|
||||
import { CommandController } from "./CommandController"
|
||||
import { Mounts } from "./Mounts"
|
||||
|
||||
const TIMEOUT_INCREMENT_MS = 1000
|
||||
const MAX_TIMEOUT_MS = 30000
|
||||
@@ -15,8 +14,11 @@ const MAX_TIMEOUT_MS = 30000
|
||||
export class Daemon<Manifest extends T.SDKManifest> extends Drop {
|
||||
private commandController: CommandController<Manifest> | null = null
|
||||
private shouldBeRunning = false
|
||||
constructor(
|
||||
protected exitedSuccess = false
|
||||
protected constructor(
|
||||
private startCommand: () => Promise<CommandController<Manifest>>,
|
||||
readonly oneshot: boolean = false,
|
||||
protected onExitSuccessFns: (() => void)[] = [],
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -29,6 +31,7 @@ export class Daemon<Manifest extends T.SDKManifest> extends Drop {
|
||||
subcontainer: SubContainer<Manifest>,
|
||||
command: T.CommandType,
|
||||
options: {
|
||||
runAsInit?: boolean
|
||||
env?:
|
||||
| {
|
||||
[variable: string]: string
|
||||
@@ -56,6 +59,7 @@ export class Daemon<Manifest extends T.SDKManifest> extends Drop {
|
||||
return
|
||||
}
|
||||
this.shouldBeRunning = true
|
||||
this.exitedSuccess = false
|
||||
let timeoutCounter = 0
|
||||
;(async () => {
|
||||
while (this.shouldBeRunning) {
|
||||
@@ -64,9 +68,27 @@ export class Daemon<Manifest extends T.SDKManifest> extends Drop {
|
||||
.term({ keepSubcontainer: true })
|
||||
.catch((err) => console.error(err))
|
||||
this.commandController = await this.startCommand()
|
||||
await this.commandController
|
||||
.wait({ keepSubcontainer: true })
|
||||
.catch((err) => console.error(err))
|
||||
if (
|
||||
this.oneshot &&
|
||||
(await this.commandController.wait({ keepSubcontainer: true }).then(
|
||||
(_) => true,
|
||||
(err) => {
|
||||
console.error(err)
|
||||
return false
|
||||
},
|
||||
))
|
||||
) {
|
||||
for (const fn of this.onExitSuccessFns) {
|
||||
try {
|
||||
fn()
|
||||
} catch (e) {
|
||||
console.error("EXIT_SUCCESS handler", e)
|
||||
}
|
||||
}
|
||||
this.onExitSuccessFns = []
|
||||
this.exitedSuccess = true
|
||||
break
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, timeoutCounter))
|
||||
timeoutCounter += TIMEOUT_INCREMENT_MS
|
||||
timeoutCounter = Math.min(MAX_TIMEOUT_MS, timeoutCounter)
|
||||
|
||||
@@ -16,6 +16,7 @@ import { HealthDaemon } from "./HealthDaemon"
|
||||
import { Daemon } from "./Daemon"
|
||||
import { CommandController } from "./CommandController"
|
||||
import { HealthCheck } from "../health/HealthCheck"
|
||||
import { Oneshot } from "./Oneshot"
|
||||
|
||||
export const cpExec = promisify(CP.exec)
|
||||
export const cpExecFile = promisify(CP.execFile)
|
||||
@@ -48,29 +49,41 @@ export type Ready = {
|
||||
trigger?: Trigger
|
||||
}
|
||||
|
||||
type DaemonsParams<
|
||||
type NewDaemonParams<Manifest extends T.SDKManifest> = {
|
||||
/** The command line command to start the daemon */
|
||||
command: T.CommandType
|
||||
/** Information about the subcontainer in which the daemon runs */
|
||||
subcontainer: SubContainer<Manifest>
|
||||
runAsInit?: boolean
|
||||
env?: Record<string, string>
|
||||
sigtermTimeout?: number
|
||||
onStdout?: (chunk: Buffer | string | any) => void
|
||||
onStderr?: (chunk: Buffer | string | any) => void
|
||||
}
|
||||
|
||||
type AddDaemonParams<
|
||||
Manifest extends T.SDKManifest,
|
||||
Ids extends string,
|
||||
Id extends string,
|
||||
> =
|
||||
| {
|
||||
/** The command line command to start the daemon */
|
||||
command: T.CommandType
|
||||
/** Information about the subcontainer in which the daemon runs */
|
||||
subcontainer: SubContainer<Manifest>
|
||||
env?: Record<string, string>
|
||||
ready: Ready
|
||||
/** An array of IDs of prior daemons whose successful initializations are required before this daemon will initialize */
|
||||
requires: Exclude<Ids, Id>[]
|
||||
sigtermTimeout?: number
|
||||
onStdout?: (chunk: Buffer | string | any) => void
|
||||
onStderr?: (chunk: Buffer | string | any) => void
|
||||
}
|
||||
> = (
|
||||
| NewDaemonParams<Manifest>
|
||||
| {
|
||||
daemon: Daemon<Manifest>
|
||||
ready: Ready
|
||||
requires: Exclude<Ids, Id>[]
|
||||
}
|
||||
) & {
|
||||
ready: Ready
|
||||
/** An array of IDs of prior daemons whose successful initializations are required before this daemon will initialize */
|
||||
requires: Exclude<Ids, Id>[]
|
||||
}
|
||||
|
||||
type AddOneshotParams<
|
||||
Manifest extends T.SDKManifest,
|
||||
Ids extends string,
|
||||
Id extends string,
|
||||
> = NewDaemonParams<Manifest> & {
|
||||
/** An array of IDs of prior daemons whose successful initializations are required before this daemon will initialize */
|
||||
requires: Exclude<Ids, Id>[]
|
||||
}
|
||||
|
||||
type ErrorDuplicateId<Id extends string> = `The id '${Id}' is already used`
|
||||
|
||||
@@ -138,8 +151,8 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
/**
|
||||
* Returns the complete list of daemons, including the one defined here
|
||||
* @param id
|
||||
* @param newDaemon
|
||||
* @returns
|
||||
* @param options
|
||||
* @returns a new Daemons object
|
||||
*/
|
||||
addDaemon<Id extends string>(
|
||||
// prettier-ignore
|
||||
@@ -148,7 +161,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
ErrorDuplicateId<Id> extends Id ? never :
|
||||
Id extends Ids ? ErrorDuplicateId<Id> :
|
||||
Id,
|
||||
options: DaemonsParams<Manifest, Ids, Id>,
|
||||
options: AddDaemonParams<Manifest, Ids, Id>,
|
||||
) {
|
||||
const daemon =
|
||||
"daemon" in options
|
||||
@@ -180,6 +193,55 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the complete list of daemons, including a "oneshot" daemon one defined here
|
||||
* a oneshot daemon is a command that executes once when started, and is considered "running" once it exits successfully
|
||||
* @param id
|
||||
* @param options
|
||||
* @returns a new Daemons object
|
||||
*/
|
||||
addOneshot<Id extends string>(
|
||||
id: "" extends Id
|
||||
? never
|
||||
: ErrorDuplicateId<Id> extends Id
|
||||
? never
|
||||
: Id extends Ids
|
||||
? ErrorDuplicateId<Id>
|
||||
: Id,
|
||||
options: AddOneshotParams<Manifest, Ids, Id>,
|
||||
) {
|
||||
const daemon = Oneshot.of()(
|
||||
this.effects,
|
||||
options.subcontainer,
|
||||
options.command,
|
||||
{
|
||||
...options,
|
||||
},
|
||||
)
|
||||
const healthDaemon = new HealthDaemon(
|
||||
daemon,
|
||||
options.requires
|
||||
.map((x) => this.ids.indexOf(x))
|
||||
.filter((x) => x >= 0)
|
||||
.map((id) => this.healthDaemons[id]),
|
||||
id,
|
||||
this.ids,
|
||||
"EXIT_SUCCESS",
|
||||
this.effects,
|
||||
)
|
||||
const daemons = this.daemons.concat(daemon)
|
||||
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,
|
||||
this.healthChecks,
|
||||
)
|
||||
}
|
||||
|
||||
async term() {
|
||||
try {
|
||||
this.healthChecks.forEach((health) => health.stop())
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Daemon } from "./Daemon"
|
||||
import { SetHealth, Effects, SDKManifest } from "../../../base/lib/types"
|
||||
import { DEFAULT_SIGTERM_TIMEOUT } from "."
|
||||
import { asError } from "../../../base/lib/util/asError"
|
||||
import { Oneshot } from "./Oneshot"
|
||||
|
||||
const oncePromise = <T>() => {
|
||||
let resolve: (value: T) => void
|
||||
@@ -14,6 +15,8 @@ const oncePromise = <T>() => {
|
||||
return { resolve: resolve!, promise }
|
||||
}
|
||||
|
||||
export const EXIT_SUCCESS = "EXIT_SUCCESS" as const
|
||||
|
||||
/**
|
||||
* Wanted a structure that deals with controlling daemons by their health status
|
||||
* States:
|
||||
@@ -33,7 +36,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
private readonly dependencies: HealthDaemon<Manifest>[],
|
||||
readonly id: string,
|
||||
readonly ids: string[],
|
||||
readonly ready: Ready,
|
||||
readonly ready: Ready | typeof EXIT_SUCCESS,
|
||||
readonly effects: Effects,
|
||||
) {
|
||||
this.readyPromise = new Promise((resolve) => (this.resolveReady = resolve))
|
||||
@@ -87,6 +90,14 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
this.healthCheckCleanup?.()
|
||||
}
|
||||
private async setupHealthCheck() {
|
||||
if (this.ready === "EXIT_SUCCESS") {
|
||||
if (this.daemon instanceof Oneshot) {
|
||||
this.daemon.onExitSuccess(() =>
|
||||
this.setHealth({ result: "success", message: null }),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (this.healthCheckCleanup) return
|
||||
const trigger = (this.ready.trigger ?? defaultTrigger)(() => ({
|
||||
lastResult: this._health.result,
|
||||
@@ -96,6 +107,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
done: true
|
||||
}>()
|
||||
new Promise(async () => {
|
||||
if (this.ready === "EXIT_SUCCESS") return
|
||||
for (
|
||||
let res = await Promise.race([status, trigger.next()]);
|
||||
!res.done;
|
||||
@@ -142,6 +154,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
|
||||
private async setHealth(health: HealthCheckResult) {
|
||||
this._health = health
|
||||
if (this.ready === "EXIT_SUCCESS") return
|
||||
this.healthWatchers.forEach((watcher) => watcher())
|
||||
const display = this.ready.display
|
||||
if (!display) {
|
||||
@@ -168,7 +181,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.ready.display) {
|
||||
if (this.ready !== "EXIT_SUCCESS" && this.ready.display) {
|
||||
this.effects.setHealth({
|
||||
id: this.id,
|
||||
message: null,
|
||||
|
||||
49
sdk/package/lib/mainFn/Oneshot.ts
Normal file
49
sdk/package/lib/mainFn/Oneshot.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { SubContainer } from "../util/SubContainer"
|
||||
import { CommandController } from "./CommandController"
|
||||
import { Daemon } from "./Daemon"
|
||||
|
||||
/**
|
||||
* This is a wrapper around CommandController that has a state of off, where the command shouldn't be running
|
||||
* and the others state of running, where it will keep a living running command
|
||||
* unlike Daemon, does not restart on success
|
||||
*/
|
||||
|
||||
export class Oneshot<Manifest extends T.SDKManifest> extends Daemon<Manifest> {
|
||||
static of<Manifest extends T.SDKManifest>() {
|
||||
return async (
|
||||
effects: T.Effects,
|
||||
subcontainer: SubContainer<Manifest>,
|
||||
command: T.CommandType,
|
||||
options: {
|
||||
env?:
|
||||
| {
|
||||
[variable: string]: string
|
||||
}
|
||||
| undefined
|
||||
cwd?: string | undefined
|
||||
user?: string | undefined
|
||||
onStdout?: (chunk: Buffer | string | any) => void
|
||||
onStderr?: (chunk: Buffer | string | any) => void
|
||||
sigtermTimeout?: number
|
||||
},
|
||||
) => {
|
||||
const startCommand = () =>
|
||||
CommandController.of<Manifest>()(
|
||||
effects,
|
||||
subcontainer,
|
||||
command,
|
||||
options,
|
||||
)
|
||||
return new Oneshot(startCommand, true, [])
|
||||
}
|
||||
}
|
||||
|
||||
onExitSuccess(fn: () => void) {
|
||||
if (this.exitedSuccess) {
|
||||
fn()
|
||||
} else {
|
||||
this.onExitSuccessFns.push(fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
4
sdk/package/package-lock.json
generated
4
sdk/package/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.12",
|
||||
"version": "0.4.0-beta.13",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.12",
|
||||
"version": "0.4.0-beta.13",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^3.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.12",
|
||||
"version": "0.4.0-beta.13",
|
||||
"description": "Software development kit to facilitate packaging services for StartOS",
|
||||
"main": "./package/lib/index.js",
|
||||
"types": "./package/lib/index.d.ts",
|
||||
|
||||
Reference in New Issue
Block a user