mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 20:14:49 +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) {
|
if (jsMain) {
|
||||||
throw new Error("Unreachable")
|
throw new Error("Unreachable")
|
||||||
}
|
}
|
||||||
const daemon = new Daemon(async () => {
|
const subcontainer = await DockerProcedureContainer.createSubContainer(
|
||||||
const subcontainer = await DockerProcedureContainer.createSubContainer(
|
effects,
|
||||||
effects,
|
this.system.manifest.id,
|
||||||
this.system.manifest.id,
|
this.system.manifest.main,
|
||||||
this.system.manifest.main,
|
this.system.manifest.volumes,
|
||||||
this.system.manifest.volumes,
|
`Main - ${currentCommand.join(" ")}`,
|
||||||
`Main - ${currentCommand.join(" ")}`,
|
)
|
||||||
)
|
const daemon = await Daemon.of()(
|
||||||
return CommandController.of()(
|
this.effects,
|
||||||
this.effects,
|
subcontainer,
|
||||||
subcontainer,
|
currentCommand,
|
||||||
currentCommand,
|
{
|
||||||
{
|
runAsInit: true,
|
||||||
runAsInit: true,
|
env: {
|
||||||
env: {
|
TINI_SUBREAPER: "true",
|
||||||
TINI_SUBREAPER: "true",
|
|
||||||
},
|
|
||||||
sigtermTimeout: utils.inMs(
|
|
||||||
this.system.manifest.main["sigterm-timeout"],
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
)
|
sigtermTimeout: utils.inMs(
|
||||||
})
|
this.system.manifest.main["sigterm-timeout"],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
daemon.start()
|
daemon.start()
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import * as T from "../../../base/lib/types"
|
import * as T from "../../../base/lib/types"
|
||||||
import { asError } from "../../../base/lib/util/asError"
|
import { asError } from "../../../base/lib/util/asError"
|
||||||
import { Drop } from "../util"
|
import { Drop } from "../util"
|
||||||
import { ExecSpawnable, MountOptions, SubContainer } from "../util/SubContainer"
|
import { ExecSpawnable, SubContainer } from "../util/SubContainer"
|
||||||
import { CommandController } from "./CommandController"
|
import { CommandController } from "./CommandController"
|
||||||
import { Mounts } from "./Mounts"
|
|
||||||
|
|
||||||
const TIMEOUT_INCREMENT_MS = 1000
|
const TIMEOUT_INCREMENT_MS = 1000
|
||||||
const MAX_TIMEOUT_MS = 30000
|
const MAX_TIMEOUT_MS = 30000
|
||||||
@@ -15,8 +14,11 @@ const MAX_TIMEOUT_MS = 30000
|
|||||||
export class Daemon<Manifest extends T.SDKManifest> extends Drop {
|
export class Daemon<Manifest extends T.SDKManifest> extends Drop {
|
||||||
private commandController: CommandController<Manifest> | null = null
|
private commandController: CommandController<Manifest> | null = null
|
||||||
private shouldBeRunning = false
|
private shouldBeRunning = false
|
||||||
constructor(
|
protected exitedSuccess = false
|
||||||
|
protected constructor(
|
||||||
private startCommand: () => Promise<CommandController<Manifest>>,
|
private startCommand: () => Promise<CommandController<Manifest>>,
|
||||||
|
readonly oneshot: boolean = false,
|
||||||
|
protected onExitSuccessFns: (() => void)[] = [],
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
@@ -29,6 +31,7 @@ export class Daemon<Manifest extends T.SDKManifest> extends Drop {
|
|||||||
subcontainer: SubContainer<Manifest>,
|
subcontainer: SubContainer<Manifest>,
|
||||||
command: T.CommandType,
|
command: T.CommandType,
|
||||||
options: {
|
options: {
|
||||||
|
runAsInit?: boolean
|
||||||
env?:
|
env?:
|
||||||
| {
|
| {
|
||||||
[variable: string]: string
|
[variable: string]: string
|
||||||
@@ -56,6 +59,7 @@ export class Daemon<Manifest extends T.SDKManifest> extends Drop {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.shouldBeRunning = true
|
this.shouldBeRunning = true
|
||||||
|
this.exitedSuccess = false
|
||||||
let timeoutCounter = 0
|
let timeoutCounter = 0
|
||||||
;(async () => {
|
;(async () => {
|
||||||
while (this.shouldBeRunning) {
|
while (this.shouldBeRunning) {
|
||||||
@@ -64,9 +68,27 @@ export class Daemon<Manifest extends T.SDKManifest> extends Drop {
|
|||||||
.term({ keepSubcontainer: true })
|
.term({ keepSubcontainer: true })
|
||||||
.catch((err) => console.error(err))
|
.catch((err) => console.error(err))
|
||||||
this.commandController = await this.startCommand()
|
this.commandController = await this.startCommand()
|
||||||
await this.commandController
|
if (
|
||||||
.wait({ keepSubcontainer: true })
|
this.oneshot &&
|
||||||
.catch((err) => console.error(err))
|
(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))
|
await new Promise((resolve) => setTimeout(resolve, timeoutCounter))
|
||||||
timeoutCounter += TIMEOUT_INCREMENT_MS
|
timeoutCounter += TIMEOUT_INCREMENT_MS
|
||||||
timeoutCounter = Math.min(MAX_TIMEOUT_MS, timeoutCounter)
|
timeoutCounter = Math.min(MAX_TIMEOUT_MS, timeoutCounter)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { HealthDaemon } from "./HealthDaemon"
|
|||||||
import { Daemon } from "./Daemon"
|
import { Daemon } from "./Daemon"
|
||||||
import { CommandController } from "./CommandController"
|
import { CommandController } from "./CommandController"
|
||||||
import { HealthCheck } from "../health/HealthCheck"
|
import { HealthCheck } from "../health/HealthCheck"
|
||||||
|
import { Oneshot } from "./Oneshot"
|
||||||
|
|
||||||
export const cpExec = promisify(CP.exec)
|
export const cpExec = promisify(CP.exec)
|
||||||
export const cpExecFile = promisify(CP.execFile)
|
export const cpExecFile = promisify(CP.execFile)
|
||||||
@@ -48,29 +49,41 @@ export type Ready = {
|
|||||||
trigger?: Trigger
|
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,
|
Manifest extends T.SDKManifest,
|
||||||
Ids extends string,
|
Ids extends string,
|
||||||
Id extends string,
|
Id extends string,
|
||||||
> =
|
> = (
|
||||||
| {
|
| NewDaemonParams<Manifest>
|
||||||
/** 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
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
daemon: Daemon<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`
|
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
|
* Returns the complete list of daemons, including the one defined here
|
||||||
* @param id
|
* @param id
|
||||||
* @param newDaemon
|
* @param options
|
||||||
* @returns
|
* @returns a new Daemons object
|
||||||
*/
|
*/
|
||||||
addDaemon<Id extends string>(
|
addDaemon<Id extends string>(
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
@@ -148,7 +161,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
|||||||
ErrorDuplicateId<Id> extends Id ? never :
|
ErrorDuplicateId<Id> extends Id ? never :
|
||||||
Id extends Ids ? ErrorDuplicateId<Id> :
|
Id extends Ids ? ErrorDuplicateId<Id> :
|
||||||
Id,
|
Id,
|
||||||
options: DaemonsParams<Manifest, Ids, Id>,
|
options: AddDaemonParams<Manifest, Ids, Id>,
|
||||||
) {
|
) {
|
||||||
const daemon =
|
const daemon =
|
||||||
"daemon" in options
|
"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() {
|
async term() {
|
||||||
try {
|
try {
|
||||||
this.healthChecks.forEach((health) => health.stop())
|
this.healthChecks.forEach((health) => health.stop())
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Daemon } from "./Daemon"
|
|||||||
import { SetHealth, Effects, SDKManifest } from "../../../base/lib/types"
|
import { SetHealth, Effects, SDKManifest } from "../../../base/lib/types"
|
||||||
import { DEFAULT_SIGTERM_TIMEOUT } from "."
|
import { DEFAULT_SIGTERM_TIMEOUT } from "."
|
||||||
import { asError } from "../../../base/lib/util/asError"
|
import { asError } from "../../../base/lib/util/asError"
|
||||||
|
import { Oneshot } from "./Oneshot"
|
||||||
|
|
||||||
const oncePromise = <T>() => {
|
const oncePromise = <T>() => {
|
||||||
let resolve: (value: T) => void
|
let resolve: (value: T) => void
|
||||||
@@ -14,6 +15,8 @@ const oncePromise = <T>() => {
|
|||||||
return { resolve: resolve!, promise }
|
return { resolve: resolve!, promise }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EXIT_SUCCESS = "EXIT_SUCCESS" as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wanted a structure that deals with controlling daemons by their health status
|
* Wanted a structure that deals with controlling daemons by their health status
|
||||||
* States:
|
* States:
|
||||||
@@ -33,7 +36,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
|||||||
private readonly dependencies: HealthDaemon<Manifest>[],
|
private readonly dependencies: HealthDaemon<Manifest>[],
|
||||||
readonly id: string,
|
readonly id: string,
|
||||||
readonly ids: string[],
|
readonly ids: string[],
|
||||||
readonly ready: Ready,
|
readonly ready: Ready | typeof EXIT_SUCCESS,
|
||||||
readonly effects: Effects,
|
readonly effects: Effects,
|
||||||
) {
|
) {
|
||||||
this.readyPromise = new Promise((resolve) => (this.resolveReady = resolve))
|
this.readyPromise = new Promise((resolve) => (this.resolveReady = resolve))
|
||||||
@@ -87,6 +90,14 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
|||||||
this.healthCheckCleanup?.()
|
this.healthCheckCleanup?.()
|
||||||
}
|
}
|
||||||
private async setupHealthCheck() {
|
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
|
if (this.healthCheckCleanup) return
|
||||||
const trigger = (this.ready.trigger ?? defaultTrigger)(() => ({
|
const trigger = (this.ready.trigger ?? defaultTrigger)(() => ({
|
||||||
lastResult: this._health.result,
|
lastResult: this._health.result,
|
||||||
@@ -96,6 +107,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
|||||||
done: true
|
done: true
|
||||||
}>()
|
}>()
|
||||||
new Promise(async () => {
|
new Promise(async () => {
|
||||||
|
if (this.ready === "EXIT_SUCCESS") return
|
||||||
for (
|
for (
|
||||||
let res = await Promise.race([status, trigger.next()]);
|
let res = await Promise.race([status, trigger.next()]);
|
||||||
!res.done;
|
!res.done;
|
||||||
@@ -142,6 +154,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
|||||||
|
|
||||||
private async setHealth(health: HealthCheckResult) {
|
private async setHealth(health: HealthCheckResult) {
|
||||||
this._health = health
|
this._health = health
|
||||||
|
if (this.ready === "EXIT_SUCCESS") return
|
||||||
this.healthWatchers.forEach((watcher) => watcher())
|
this.healthWatchers.forEach((watcher) => watcher())
|
||||||
const display = this.ready.display
|
const display = this.ready.display
|
||||||
if (!display) {
|
if (!display) {
|
||||||
@@ -168,7 +181,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
if (this.ready.display) {
|
if (this.ready !== "EXIT_SUCCESS" && this.ready.display) {
|
||||||
this.effects.setHealth({
|
this.effects.setHealth({
|
||||||
id: this.id,
|
id: this.id,
|
||||||
message: null,
|
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",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "0.4.0-beta.12",
|
"version": "0.4.0-beta.13",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@start9labs/start-sdk",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "0.4.0-beta.12",
|
"version": "0.4.0-beta.13",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iarna/toml": "^3.0.0",
|
"@iarna/toml": "^3.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@start9labs/start-sdk",
|
"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",
|
"description": "Software development kit to facilitate packaging services for StartOS",
|
||||||
"main": "./package/lib/index.js",
|
"main": "./package/lib/index.js",
|
||||||
"types": "./package/lib/index.d.ts",
|
"types": "./package/lib/index.d.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user