Fix StartOS 0.4 TypeScript packaging to match SDK API

This commit is contained in:
MacPro
2026-04-09 15:10:44 -05:00
parent d5046a0daf
commit 0b70cbb2bf
3436 changed files with 867051 additions and 92 deletions
@@ -0,0 +1,50 @@
import * as T from '../../../base/lib/types';
import { SubContainer } from '../util/SubContainer';
import { Drop } from '../util';
import { DaemonCommandType } from './Daemons';
/**
* Low-level controller for a single running process inside a subcontainer (or as a JS function).
*
* Manages the child process lifecycle: spawning, waiting, and signal-based termination.
* Used internally by {@link Daemon} to manage individual command executions.
*
* @typeParam Manifest - The service manifest type
* @typeParam C - The subcontainer type, or `null` for JS-only commands
*/
export declare class CommandController<Manifest extends T.SDKManifest, C extends SubContainer<Manifest> | null> extends Drop {
readonly runningAnswer: Promise<null>;
private state;
private readonly subcontainer;
private process;
readonly sigtermTimeout: number;
private constructor();
/**
* Factory method to create a new CommandController.
*
* Returns a curried async function: `(effects, subcontainer, exec) => CommandController`.
* If the exec spec has an `fn` property, runs the function; otherwise spawns a shell command
* in the subcontainer.
*/
static of<Manifest extends T.SDKManifest, C extends SubContainer<Manifest> | null>(): (effects: T.Effects, subcontainer: C, exec: DaemonCommandType<Manifest, C>) => Promise<CommandController<Manifest, C>>;
/**
* Wait for the command to finish. Optionally terminate after a timeout.
* @param options.timeout - Milliseconds to wait before terminating. Defaults to no timeout.
*/
wait({ timeout }?: {
timeout?: number | undefined;
}): Promise<void>;
/**
* Terminate the running command by sending a signal.
*
* Sends the specified signal (default: SIGTERM), then escalates to SIGKILL
* after the timeout expires. Destroys the subcontainer after the process exits.
*
* @param options.signal - The signal to send (default: SIGTERM)
* @param options.timeout - Milliseconds before escalating to SIGKILL
*/
term({ signal, timeout }?: {
signal?: NodeJS.Signals | undefined;
timeout?: number | undefined;
}): Promise<void>;
onDrop(): void;
}
@@ -0,0 +1,217 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CommandController = void 0;
const _1 = require(".");
const types_1 = require("../../../base/lib/types");
const T = __importStar(require("../../../base/lib/types"));
const util_1 = require("../util");
const fs = __importStar(require("node:fs/promises"));
const logErrorOnce_1 = require("../../../base/lib/util/logErrorOnce");
/**
* Low-level controller for a single running process inside a subcontainer (or as a JS function).
*
* Manages the child process lifecycle: spawning, waiting, and signal-based termination.
* Used internally by {@link Daemon} to manage individual command executions.
*
* @typeParam Manifest - The service manifest type
* @typeParam C - The subcontainer type, or `null` for JS-only commands
*/
class CommandController extends util_1.Drop {
constructor(runningAnswer, state, subcontainer, process, sigtermTimeout = _1.DEFAULT_SIGTERM_TIMEOUT) {
super();
this.runningAnswer = runningAnswer;
this.state = state;
this.subcontainer = subcontainer;
this.process = process;
this.sigtermTimeout = sigtermTimeout;
}
/**
* Factory method to create a new CommandController.
*
* Returns a curried async function: `(effects, subcontainer, exec) => CommandController`.
* If the exec spec has an `fn` property, runs the function; otherwise spawns a shell command
* in the subcontainer.
*/
static of() {
return async (effects, subcontainer, exec) => {
try {
if ('fn' in exec) {
const abort = new AbortController();
const cell = {
ctrl: new CommandController(exec.fn(subcontainer, abort.signal).then(async (command) => {
if (subcontainer && command && !abort.signal.aborted) {
const newCtrl = (await CommandController.of()(effects, subcontainer, command)).leak();
Object.assign(cell.ctrl, newCtrl);
return await cell.ctrl.runningAnswer;
}
else {
cell.ctrl.state.exited = true;
}
return null;
}), { exited: false }, subcontainer, abort, exec.sigtermTimeout),
};
return cell.ctrl;
}
let commands;
if (T.isUseEntrypoint(exec.command)) {
const imageMeta = await fs
.readFile(`/media/startos/images/${subcontainer.imageId}.json`, {
encoding: 'utf8',
})
.catch(() => '{}')
.then(JSON.parse);
commands = imageMeta.entrypoint ?? [];
commands = commands.concat(...(exec.command.overridCmd ?? imageMeta.cmd ?? []));
}
else
commands = (0, util_1.splitCommand)(exec.command);
let childProcess;
if (exec.runAsInit) {
childProcess = await subcontainer.launch(commands, {
env: exec.env,
user: exec.user,
cwd: exec.cwd,
});
}
else {
childProcess = await subcontainer.spawn(commands, {
env: exec.env,
user: exec.user,
cwd: exec.cwd,
stdio: exec.onStdout || exec.onStderr ? 'pipe' : 'inherit',
});
}
if (exec.onStdout)
childProcess.stdout?.on('data', exec.onStdout);
if (exec.onStderr)
childProcess.stderr?.on('data', exec.onStderr);
const state = { exited: false };
const answer = new Promise((resolve, reject) => {
childProcess.on('exit', (code) => {
state.exited = true;
if (code === 0 ||
code === 143 ||
(code === null && childProcess.signalCode == 'SIGTERM')) {
return resolve(null);
}
if (code) {
return reject(new Error(`${commands[0]} exited with code ${code}`));
}
else {
return reject(new Error(`${commands[0]} exited with signal ${childProcess.signalCode}`));
}
});
});
return new CommandController(answer, state, subcontainer, childProcess, exec.sigtermTimeout);
}
catch (e) {
await subcontainer?.destroy();
throw e;
}
};
}
/**
* Wait for the command to finish. Optionally terminate after a timeout.
* @param options.timeout - Milliseconds to wait before terminating. Defaults to no timeout.
*/
async wait({ timeout = types_1.NO_TIMEOUT } = {}) {
if (timeout > 0)
setTimeout(() => {
this.term();
}, timeout);
try {
if (timeout > 0 && this.process instanceof AbortController)
await Promise.race([
this.runningAnswer,
new Promise((_, reject) => setTimeout(() => reject(new Error('Timed out waiting for js command to exit')), timeout * 2)),
]);
else
await this.runningAnswer;
}
finally {
if (!this.state.exited) {
if (this.process instanceof AbortController)
this.process.abort();
else
this.process.kill('SIGKILL');
}
await this.subcontainer?.destroy();
}
}
/**
* Terminate the running command by sending a signal.
*
* Sends the specified signal (default: SIGTERM), then escalates to SIGKILL
* after the timeout expires. Destroys the subcontainer after the process exits.
*
* @param options.signal - The signal to send (default: SIGTERM)
* @param options.timeout - Milliseconds before escalating to SIGKILL
*/
async term({ signal = types_1.SIGTERM, timeout = this.sigtermTimeout } = {}) {
try {
if (!this.state.exited) {
if (this.process instanceof AbortController)
return this.process.abort();
if (signal !== 'SIGKILL') {
setTimeout(() => {
if (this.process instanceof AbortController)
this.process.abort();
else
this.process.kill('SIGKILL');
}, timeout);
}
if (!this.process.kill(signal)) {
console.error(`failed to send signal ${signal} to pid ${this.process.pid}`);
}
}
if (this.process instanceof AbortController)
await Promise.race([
this.runningAnswer,
new Promise((_, reject) => setTimeout(() => reject(new Error('Timed out waiting for js command to exit')), timeout * 2)),
]);
else
await this.runningAnswer;
}
finally {
await this.subcontainer?.destroy();
}
}
onDrop() {
this.term().catch(logErrorOnce_1.logErrorOnce);
}
}
exports.CommandController = CommandController;
//# sourceMappingURL=CommandController.js.map
File diff suppressed because one or more lines are too long
+77
View File
@@ -0,0 +1,77 @@
import * as T from '../../../base/lib/types';
import { Drop } from '../util';
import { SubContainer, SubContainerRc } from '../util/SubContainer';
import { CommandController } from './CommandController';
import { DaemonCommandType } from './Daemons';
import { Oneshot } from './Oneshot';
/**
* A managed long-running process wrapper around {@link CommandController}.
*
* When started, the daemon automatically restarts its underlying command on failure
* with exponential backoff (up to 30 seconds). When stopped, the command is terminated
* gracefully. Implements {@link Drop} for automatic cleanup when the context is left.
*
* @typeParam Manifest - The service manifest type
* @typeParam C - The subcontainer type, or `null` for JS-only daemons
*/
export declare class Daemon<Manifest extends T.SDKManifest, C extends SubContainer<Manifest> | null = SubContainer<Manifest> | null> extends Drop {
private subcontainer;
private startCommand;
readonly oneshot: boolean;
private commandController;
protected exitedSuccess: boolean;
private onExitFns;
private loop;
private _managed;
protected constructor(subcontainer: C, startCommand: () => Promise<CommandController<Manifest, C>>, oneshot?: boolean);
/** Returns true if this daemon is a one-shot process (exits after success) */
isOneshot(): this is Oneshot<Manifest>;
/**
* Factory method to create a new Daemon.
*
* Returns a curried function: `(effects, subcontainer, exec) => Daemon`.
* Registers an `onLeaveContext` callback that terminates the daemon when the
* effects context is left.
*/
static of<Manifest extends T.SDKManifest>(): <C extends SubContainer<Manifest> | null>(effects: T.Effects, subcontainer: C, exec: DaemonCommandType<Manifest, C>) => Daemon<Manifest, SubContainer<Manifest, T.Effects> | null>;
/**
* Start the daemon. If it is already running, this is a no-op.
*
* The daemon will automatically restart on failure with increasing backoff
* until {@link term} is called.
*/
start(): Promise<void>;
private runLoop;
/**
* Terminate the daemon, stopping its underlying command.
*
* Sends the configured signal (default SIGTERM) and waits for the process to exit.
* Optionally destroys the subcontainer after termination.
*
* @param termOptions - Optional termination settings
* @param termOptions.signal - The signal to send (default: SIGTERM)
* @param termOptions.timeout - Milliseconds to wait before SIGKILL
* @param termOptions.destroySubcontainer - Whether to destroy the subcontainer after exit
*/
term(termOptions?: {
signal?: NodeJS.Signals | undefined;
timeout?: number | undefined;
destroySubcontainer?: boolean;
}): Promise<void>;
/**
* Mark this daemon as managed by a {@link Daemons} instance.
* Suppresses the individual `onLeaveContext` termination since the
* `Daemons` instance handles ordered shutdown.
*/
markManaged(): void;
/** Get a reference-counted handle to the daemon's subcontainer, or null if there is none */
subcontainerRc(): SubContainerRc<Manifest> | null;
/** Check whether this daemon shares the same subcontainer as another daemon */
sharesSubcontainerWith(other: Daemon<Manifest, SubContainer<Manifest> | null>): boolean;
/**
* Register a callback to be invoked each time the daemon's process exits.
* @param fn - Callback receiving `true` on clean exit, `false` on error
*/
onExit(fn: (success: boolean) => void): void;
onDrop(): void;
}
+185
View File
@@ -0,0 +1,185 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Daemon = void 0;
const asError_1 = require("../../../base/lib/util/asError");
const logErrorOnce_1 = require("../../../base/lib/util/logErrorOnce");
const util_1 = require("../util");
const CommandController_1 = require("./CommandController");
const TIMEOUT_INCREMENT_MS = 1000;
const MAX_TIMEOUT_MS = 30000;
/**
* A managed long-running process wrapper around {@link CommandController}.
*
* When started, the daemon automatically restarts its underlying command on failure
* with exponential backoff (up to 30 seconds). When stopped, the command is terminated
* gracefully. Implements {@link Drop} for automatic cleanup when the context is left.
*
* @typeParam Manifest - The service manifest type
* @typeParam C - The subcontainer type, or `null` for JS-only daemons
*/
class Daemon extends util_1.Drop {
constructor(subcontainer, startCommand, oneshot = false) {
super();
this.subcontainer = subcontainer;
this.startCommand = startCommand;
this.oneshot = oneshot;
this.commandController = null;
this.exitedSuccess = false;
this.onExitFns = [];
this.loop = null;
this._managed = false;
}
/** Returns true if this daemon is a one-shot process (exits after success) */
isOneshot() {
return this.oneshot;
}
/**
* Factory method to create a new Daemon.
*
* Returns a curried function: `(effects, subcontainer, exec) => Daemon`.
* Registers an `onLeaveContext` callback that terminates the daemon when the
* effects context is left.
*/
static of() {
return (effects, subcontainer, exec) => {
let subc = subcontainer;
if (subcontainer && subcontainer.isOwned())
subc = subcontainer.rc();
const startCommand = () => CommandController_1.CommandController.of()(effects, (subc?.rc() ?? null), exec);
const res = new Daemon(subc, startCommand);
effects.onLeaveContext(() => {
if (!res._managed) {
res.term({ destroySubcontainer: true }).catch((e) => (0, logErrorOnce_1.logErrorOnce)(e));
}
});
return res;
};
}
/**
* Start the daemon. If it is already running, this is a no-op.
*
* The daemon will automatically restart on failure with increasing backoff
* until {@link term} is called.
*/
async start() {
if (this.loop) {
return;
}
const abort = new AbortController();
const done = this.runLoop(abort.signal);
this.loop = { abort, done };
}
async runLoop(signal) {
let timeoutCounter = 0;
try {
while (!signal.aborted) {
if (this.commandController) {
await this.commandController.term({}).catch(logErrorOnce_1.logErrorOnce);
this.commandController = null;
}
try {
this.commandController = await this.startCommand();
if (signal.aborted) {
await this.commandController.term({}).catch(logErrorOnce_1.logErrorOnce);
this.commandController = null;
break;
}
const success = await this.commandController.wait().then((_) => true, (err) => {
if (!signal.aborted)
(0, logErrorOnce_1.logErrorOnce)(err);
return false;
});
this.commandController = null;
if (signal.aborted)
break;
for (const fn of this.onExitFns) {
try {
fn(success);
}
catch (e) {
console.error('EXIT handler', e);
}
}
if (success && this.oneshot) {
this.exitedSuccess = true;
break;
}
}
catch (e) {
if (!signal.aborted)
console.error(e);
}
if (signal.aborted)
break;
await new Promise((resolve) => {
const timer = setTimeout(resolve, timeoutCounter);
signal.addEventListener('abort', () => {
clearTimeout(timer);
resolve();
}, { once: true });
});
timeoutCounter += TIMEOUT_INCREMENT_MS;
timeoutCounter = Math.min(MAX_TIMEOUT_MS, timeoutCounter);
}
}
finally {
this.loop = null;
}
}
/**
* Terminate the daemon, stopping its underlying command.
*
* Sends the configured signal (default SIGTERM) and waits for the process to exit.
* Optionally destroys the subcontainer after termination.
*
* @param termOptions - Optional termination settings
* @param termOptions.signal - The signal to send (default: SIGTERM)
* @param termOptions.timeout - Milliseconds to wait before SIGKILL
* @param termOptions.destroySubcontainer - Whether to destroy the subcontainer after exit
*/
async term(termOptions) {
this.exitedSuccess = false;
this.onExitFns = [];
if (this.loop) {
this.loop.abort.abort();
}
const exiting = this.commandController?.term({ ...termOptions });
this.commandController = null;
if (exiting)
await exiting.catch(logErrorOnce_1.logErrorOnce);
if (this.loop) {
await this.loop.done;
}
if (termOptions?.destroySubcontainer) {
await this.subcontainer?.destroy();
}
}
/**
* Mark this daemon as managed by a {@link Daemons} instance.
* Suppresses the individual `onLeaveContext` termination since the
* `Daemons` instance handles ordered shutdown.
*/
markManaged() {
this._managed = true;
}
/** Get a reference-counted handle to the daemon's subcontainer, or null if there is none */
subcontainerRc() {
return this.subcontainer?.rc() ?? null;
}
/** Check whether this daemon shares the same subcontainer as another daemon */
sharesSubcontainerWith(other) {
return this.subcontainer?.guid === other.subcontainer?.guid;
}
/**
* Register a callback to be invoked each time the daemon's process exits.
* @param fn - Callback receiving `true` on clean exit, `false` on error
*/
onExit(fn) {
this.onExitFns.push(fn);
}
onDrop() {
this.term().catch((e) => (0, logErrorOnce_1.logErrorOnce)((0, asError_1.asError)(e)));
}
}
exports.Daemon = Daemon;
//# sourceMappingURL=Daemon.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"Daemon.js","sourceRoot":"","sources":["../../../../package/lib/mainFn/Daemon.ts"],"names":[],"mappings":"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"}
+195
View File
@@ -0,0 +1,195 @@
import { HealthCheckResult } from '../health/checkFns';
import { Trigger } from '../trigger';
import * as T from '../../../base/lib/types';
import { SubContainer } from '../util/SubContainer';
import * as CP from 'node:child_process';
export { Daemon } from './Daemon';
export { CommandController } from './CommandController';
import { HealthDaemon } from './HealthDaemon';
import { Daemon } from './Daemon';
import { CommandController } from './CommandController';
/** Promisified version of `child_process.exec` */
export declare const cpExec: typeof CP.exec.__promisify__;
/** Promisified version of `child_process.execFile` */
export declare const cpExecFile: typeof CP.execFile.__promisify__;
/**
* Configuration for a daemon's health-check readiness probe.
*
* Determines how the system knows when a daemon is healthy and ready to serve.
*/
export type Ready = {
/** A human-readable display name for the health check. If null, the health check itself will be from the UI */
display: string | null;
/**
* @description The function to determine the health status of the daemon
*
* The SDK provides some built-in health checks. To see them, type sdk.healthCheck.
*
* @example
* ```
fn: () =>
sdk.healthCheck.checkPortListening(effects, 80, {
successMessage: 'service listening on port 80',
errorMessage: 'service is unreachable',
})
* ```
*/
fn: () => Promise<HealthCheckResult> | HealthCheckResult;
/**
* A duration in milliseconds to treat a failing health check as "starting"
*
* defaults to 5000
*/
gracePeriod?: number;
trigger?: Trigger;
};
/**
* Options for running a daemon as a shell command inside a subcontainer.
* Includes the command to run, optional signal/timeout, environment, user, and stdio callbacks.
*/
export type ExecCommandOptions = {
command: T.CommandType;
sigtermTimeout?: number;
runAsInit?: boolean;
env?: {
[variable in string]?: string;
} | undefined;
cwd?: string | undefined;
user?: string | undefined;
onStdout?: (chunk: Buffer | string | any) => void;
onStderr?: (chunk: Buffer | string | any) => void;
};
/**
* Options for running a daemon via an async function that may optionally return
* a command to execute in the subcontainer. The function receives an `AbortSignal`
* for cooperative cancellation.
*/
export type ExecFnOptions<Manifest extends T.SDKManifest, C extends SubContainer<Manifest> | null> = {
fn: (subcontainer: C, abort: AbortSignal) => Promise<C extends null ? null : ExecCommandOptions | null>;
sigtermTimeout?: number;
};
/**
* The execution specification for a daemon: either an {@link ExecFnOptions} (async function)
* or an {@link ExecCommandOptions} (shell command, only valid when a subcontainer is provided).
*/
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, 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<Manifest, C>;
/** The subcontainer in which the daemon runs */
subcontainer: C;
};
type OptionalParamSync<T> = T | (() => T | null);
type OptionalParamAsync<T> = () => Promise<T | null>;
type AddDaemonParams<Manifest extends T.SDKManifest, Ids extends string, Id extends string, C extends SubContainer<Manifest> | null> = (NewDaemonParams<Manifest, C> | {
daemon: Daemon<Manifest>;
}) & {
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, 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>[];
};
type ErrorDuplicateId<Id extends string> = `The id '${Id}' is already used`;
export declare const runCommand: <Manifest extends T.SDKManifest>() => (effects: T.Effects, subcontainer: SubContainer<Manifest, T.Effects>, exec: DaemonCommandType<Manifest, SubContainer<Manifest, T.Effects>>) => Promise<CommandController<Manifest, SubContainer<Manifest, T.Effects>>>;
/**
* A class for defining and controlling the service daemons
```ts
Daemons.of({
effects,
started,
interfaceReceipt, // Provide the interfaceReceipt to prove it was completed
healthReceipts, // Provide the healthReceipts or [] to prove they were at least considered
}).addDaemon('webui', {
command: 'hello-world', // The command to start the daemon
ready: {
display: 'Web Interface',
// The function to run to determine the health status of the daemon
fn: () =>
checkPortListening(effects, 80, {
successMessage: 'The web interface is ready',
errorMessage: 'The web interface is not ready',
}),
},
requires: [],
})
```
*/
export declare class Daemons<Manifest extends T.SDKManifest, Ids extends string> implements T.DaemonBuildable {
readonly effects: T.Effects;
readonly ids: Ids[];
readonly healthDaemons: HealthDaemon<Manifest>[];
private termPromise;
private constructor();
/**
* Returns an empty new Daemons class with the provided inputSpec.
*
* Call .addDaemon() on the returned class to add a daemon.
*
* Daemons run in the order they are defined, with latter daemons being capable of
* depending on prior daemons
*
* @param effects
*
* @param started
* @returns
*/
static of<Manifest extends T.SDKManifest>(options: {
effects: T.Effects;
}): Daemons<Manifest, never>;
private addDaemonImpl;
/**
* Returns the complete list of daemons, including the one defined here
* @param id
* @param options
* @returns a new Daemons object
*/
addDaemon<Id extends string, C extends SubContainer<Manifest> | null>(id: "" extends Id ? never : ErrorDuplicateId<Id> extends Id ? never : Id extends Ids ? ErrorDuplicateId<Id> : Id, options: OptionalParamSync<AddDaemonParams<Manifest, Ids, Id, C>>): Daemons<Manifest, Ids | Id>;
addDaemon<Id extends string, C extends SubContainer<Manifest> | null>(id: "" extends Id ? never : ErrorDuplicateId<Id> extends Id ? never : Id extends Ids ? ErrorDuplicateId<Id> : Id, options: OptionalParamAsync<AddDaemonParams<Manifest, Ids, Id, C>>): Promise<Daemons<Manifest, Ids | Id>>;
/**
* 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, C extends SubContainer<Manifest> | null>(id: "" extends Id ? never : ErrorDuplicateId<Id> extends Id ? never : Id extends Ids ? ErrorDuplicateId<Id> : Id, options: OptionalParamSync<AddOneshotParams<Manifest, Ids, Id, C>>): Daemons<Manifest, Ids | Id>;
addOneshot<Id extends string, C extends SubContainer<Manifest> | null>(id: "" extends Id ? never : ErrorDuplicateId<Id> extends Id ? never : Id extends Ids ? ErrorDuplicateId<Id> : Id, options: OptionalParamAsync<AddOneshotParams<Manifest, Ids, Id, C>>): Promise<Daemons<Manifest, Ids | Id>>;
/**
* 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: OptionalParamSync<AddHealthCheckParams<Ids, Id>>): Daemons<Manifest, Ids | Id>;
addHealthCheck<Id extends string>(id: "" extends Id ? never : ErrorDuplicateId<Id> extends Id ? never : Id extends Ids ? ErrorDuplicateId<Id> : Id, options: OptionalParamAsync<AddHealthCheckParams<Ids, Id>>): Promise<Daemons<Manifest, Ids | Id>>;
/**
* Runs the entire system until all daemons have returned `ready`.
* @param id
* @param options
* @returns a new Daemons object
*/
runUntilSuccess(timeout: number | null): Promise<null>;
/**
* Gracefully terminate all daemons in reverse dependency order.
*
* Daemons with no remaining dependents are shut down first, proceeding
* until all daemons have been terminated. Falls back to a bulk shutdown
* if a dependency cycle is detected.
*/
term(): Promise<void>;
private _term;
/**
* Start all registered daemons and their health checks.
* @returns This `Daemons` instance, now running
*/
build(): Promise<this>;
}
+253
View File
@@ -0,0 +1,253 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Daemons = exports.runCommand = exports.cpExecFile = exports.cpExec = exports.CommandController = exports.Daemon = void 0;
const node_util_1 = require("node:util");
const CP = __importStar(require("node:child_process"));
var Daemon_1 = require("./Daemon");
Object.defineProperty(exports, "Daemon", { enumerable: true, get: function () { return Daemon_1.Daemon; } });
var CommandController_1 = require("./CommandController");
Object.defineProperty(exports, "CommandController", { enumerable: true, get: function () { return CommandController_1.CommandController; } });
const HealthDaemon_1 = require("./HealthDaemon");
const Daemon_2 = require("./Daemon");
const CommandController_2 = require("./CommandController");
const Oneshot_1 = require("./Oneshot");
/** Promisified version of `child_process.exec` */
exports.cpExec = (0, node_util_1.promisify)(CP.exec);
/** Promisified version of `child_process.execFile` */
exports.cpExecFile = (0, node_util_1.promisify)(CP.execFile);
const runCommand = () => CommandController_2.CommandController.of();
exports.runCommand = runCommand;
/**
* A class for defining and controlling the service daemons
```ts
Daemons.of({
effects,
started,
interfaceReceipt, // Provide the interfaceReceipt to prove it was completed
healthReceipts, // Provide the healthReceipts or [] to prove they were at least considered
}).addDaemon('webui', {
command: 'hello-world', // The command to start the daemon
ready: {
display: 'Web Interface',
// The function to run to determine the health status of the daemon
fn: () =>
checkPortListening(effects, 80, {
successMessage: 'The web interface is ready',
errorMessage: 'The web interface is not ready',
}),
},
requires: [],
})
```
*/
class Daemons {
constructor(effects, ids, healthDaemons) {
this.effects = effects;
this.ids = ids;
this.healthDaemons = healthDaemons;
this.termPromise = null;
}
/**
* Returns an empty new Daemons class with the provided inputSpec.
*
* Call .addDaemon() on the returned class to add a daemon.
*
* Daemons run in the order they are defined, with latter daemons being capable of
* depending on prior daemons
*
* @param effects
*
* @param started
* @returns
*/
static of(options) {
return new Daemons(options.effects, [], []);
}
addDaemonImpl(id, daemon, requires, ready) {
const healthDaemon = new HealthDaemon_1.HealthDaemon(daemon, requires
.map((x) => this.ids.indexOf(x))
.filter((x) => x >= 0)
.map((id) => this.healthDaemons[id]), id, ready, this.effects);
const ids = [...this.ids, id];
const healthDaemons = [...this.healthDaemons, healthDaemon];
return new Daemons(this.effects, ids, healthDaemons);
}
addDaemon(id, options) {
const prev = this;
const res = (options) => {
if (!options)
return prev;
const daemon = 'daemon' in options
? options.daemon
: Daemon_2.Daemon.of()(this.effects, options.subcontainer, options.exec);
return prev.addDaemonImpl(id, daemon, options.requires, options.ready);
};
if (options instanceof Function) {
const opts = options();
if (opts instanceof Promise) {
return opts.then(res);
}
return res(opts);
}
return res(options);
}
addOneshot(id, options) {
const prev = this;
const res = (options) => {
if (!options)
return prev;
const daemon = Oneshot_1.Oneshot.of()(this.effects, options.subcontainer, options.exec);
return prev.addDaemonImpl(id, daemon, options.requires, HealthDaemon_1.EXIT_SUCCESS);
};
if (options instanceof Function) {
const opts = options();
if (opts instanceof Promise) {
return opts.then(res);
}
return res(opts);
}
return res(options);
}
addHealthCheck(id, options) {
const prev = this;
const res = (options) => {
if (!options)
return prev;
return prev.addDaemonImpl(id, null, options.requires, options.ready);
};
if (options instanceof Function) {
const opts = options();
if (opts instanceof Promise) {
return opts.then(res);
}
return res(opts);
}
return res(options);
}
/**
* Runs the entire system until all daemons have returned `ready`.
* @param id
* @param options
* @returns a new Daemons object
*/
async runUntilSuccess(timeout) {
let resolve = (_) => { };
const res = new Promise((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_1.Oneshot.of()(this.effects, null, {
fn: async () => {
resolve();
return null;
},
});
const healthDaemon = new HealthDaemon_1.HealthDaemon(daemon, [...this.healthDaemons], '__RUN_UNTIL_SUCCESS', 'EXIT_SUCCESS', this.effects);
const daemons = await new Daemons(this.effects, this.ids, [
...this.healthDaemons,
healthDaemon,
]).build();
try {
await res;
}
finally {
await daemons.term();
}
return null;
}
/**
* Gracefully terminate all daemons in reverse dependency order.
*
* Daemons with no remaining dependents are shut down first, proceeding
* until all daemons have been terminated. Falls back to a bulk shutdown
* if a dependency cycle is detected.
*/
async term() {
if (!this.termPromise) {
this.termPromise = this._term();
}
return this.termPromise;
}
async _term() {
const remaining = new Set(this.healthDaemons);
while (remaining.size > 0) {
// Find daemons with no remaining dependents
const canShutdown = [...remaining].filter((daemon) => ![...remaining].some((other) => other.dependencies.some((dep) => dep.id === daemon.id)));
if (canShutdown.length === 0) {
// Dependency cycle that should not happen, just shutdown remaining daemons
console.warn('Dependency cycle detected, shutting down remaining daemons');
canShutdown.push(...[...remaining].reverse());
}
// remove from remaining set
canShutdown.forEach((daemon) => remaining.delete(daemon));
// Shutdown daemons with no remaining dependents concurrently
await Promise.allSettled(canShutdown.map(async (daemon) => {
try {
console.debug(`Terminating daemon ${daemon.id}`);
const destroySubcontainer = daemon.daemon
? ![...remaining].some((d) => d.daemon?.sharesSubcontainerWith(daemon.daemon))
: false;
await daemon.term({ destroySubcontainer });
}
catch (e) {
console.error(e);
}
}));
}
}
/**
* Start all registered daemons and their health checks.
* @returns This `Daemons` instance, now running
*/
async build() {
this.effects.onLeaveContext(() => {
this.term().catch((e) => console.error(e));
});
for (const daemon of this.healthDaemons) {
daemon.daemon?.markManaged();
await daemon.updateStatus();
}
return this;
}
}
exports.Daemons = Daemons;
//# sourceMappingURL=Daemons.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,46 @@
import { HealthCheckResult } from '../health/checkFns';
import { Ready } from './Daemons';
import { Daemon } from './Daemon';
import { Effects, SDKManifest } from '../../../base/lib/types';
export declare const EXIT_SUCCESS: "EXIT_SUCCESS";
/**
* Wanted a structure that deals with controlling daemons by their health status
* States:
* -- Waiting for dependencies to be success
* -- Running: Daemon is running and the status is in the health
*
*/
export declare class HealthDaemon<Manifest extends SDKManifest> {
readonly daemon: Daemon<Manifest> | null;
readonly dependencies: HealthDaemon<Manifest>[];
readonly id: string;
readonly ready: Ready | typeof EXIT_SUCCESS;
readonly effects: Effects;
private _health;
private healthWatchers;
private running;
private started?;
private resolveReady;
private resolvedReady;
private readyPromise;
private session;
constructor(daemon: Daemon<Manifest> | null, dependencies: HealthDaemon<Manifest>[], id: string, ready: Ready | typeof EXIT_SUCCESS, effects: Effects);
/** Run after we want to do cleanup */
term(termOptions?: {
signal?: NodeJS.Signals | undefined;
timeout?: number | undefined;
destroySubcontainer?: boolean;
}): Promise<void>;
/** Want to add another notifier that the health might have changed */
addWatcher(watcher: () => unknown): void;
get health(): Readonly<HealthCheckResult>;
private changeRunning;
private stopSession;
private resetReady;
private startSession;
private runHealthCheckLoop;
onReady(): Promise<void>;
get isReady(): boolean;
private setHealth;
updateStatus(): Promise<void>;
}
+189
View File
@@ -0,0 +1,189 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HealthDaemon = exports.EXIT_SUCCESS = void 0;
const defaultTrigger_1 = require("../trigger/defaultTrigger");
exports.EXIT_SUCCESS = 'EXIT_SUCCESS';
/**
* Wanted a structure that deals with controlling daemons by their health status
* States:
* -- Waiting for dependencies to be success
* -- Running: Daemon is running and the status is in the health
*
*/
class HealthDaemon {
constructor(daemon, dependencies, id, ready, effects) {
this.daemon = daemon;
this.dependencies = dependencies;
this.id = id;
this.ready = ready;
this.effects = effects;
this._health = { result: 'waiting', message: null };
this.healthWatchers = [];
this.running = false;
this.resolvedReady = false;
this.session = null;
this.readyPromise = new Promise((resolve) => (this.resolveReady = () => {
resolve();
this.resolvedReady = true;
}));
this.dependencies.forEach((d) => d.addWatcher(() => this.updateStatus()));
}
/** Run after we want to do cleanup */
async term(termOptions) {
this.healthWatchers = [];
this.running = false;
await this.stopSession();
await this.daemon?.term({
...termOptions,
});
}
/** Want to add another notifier that the health might have changed */
addWatcher(watcher) {
this.healthWatchers.push(watcher);
}
get health() {
return Object.freeze(this._health);
}
async changeRunning(newStatus) {
if (this.running === newStatus)
return;
this.running = newStatus;
if (newStatus) {
console.debug(`Launching ${this.id}...`);
this.startSession();
this.daemon?.start();
this.started = performance.now();
}
else {
console.debug(`Stopping ${this.id}...`);
await this.stopSession();
await this.daemon?.term();
}
}
async stopSession() {
if (!this.session)
return;
this.session.abort.abort();
await this.session.done;
this.session = null;
this.resetReady();
}
resetReady() {
this.resolvedReady = false;
this.readyPromise = new Promise((resolve) => (this.resolveReady = () => {
resolve();
this.resolvedReady = true;
}));
}
startSession() {
this.session?.abort.abort();
const abort = new AbortController();
this.daemon?.onExit((success) => {
if (abort.signal.aborted)
return;
if (success && this.ready === 'EXIT_SUCCESS') {
this.setHealth({ result: 'success', message: null });
}
else if (!success) {
this.setHealth({
result: 'failure',
message: `${this.id} daemon crashed`,
});
}
else if (!this.daemon?.isOneshot()) {
this.setHealth({
result: 'failure',
message: `${this.id} daemon exited`,
});
}
});
const done = this.ready === 'EXIT_SUCCESS'
? Promise.resolve()
: this.runHealthCheckLoop(abort.signal);
this.session = { abort, done };
}
async runHealthCheckLoop(signal) {
if (this.ready === 'EXIT_SUCCESS')
return;
const trigger = (this.ready.trigger ?? defaultTrigger_1.defaultTrigger)(() => ({
lastResult: this._health.result,
}));
const aborted = new Promise((resolve) => signal.addEventListener('abort', () => resolve({ done: true }), {
once: true,
}));
try {
for (let res = await Promise.race([aborted, trigger.next()]); !res.done; res = await Promise.race([aborted, trigger.next()])) {
const response = await Promise.resolve(this.ready.fn()).catch((err) => {
return {
result: 'failure',
message: 'message' in err ? err.message : String(err),
};
});
if (signal.aborted)
break;
await this.setHealth(response);
}
}
catch (err) {
if (!signal.aborted) {
console.error(`Daemon ${this.id} health check failed: ${err}`);
}
}
}
onReady() {
return this.readyPromise;
}
get isReady() {
return this.resolvedReady;
}
async setHealth(health) {
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;
const display = this.ready.display;
if (!display) {
return;
}
let result = health.result;
if (result === 'failure' &&
this.started &&
performance.now() - this.started <= (this.ready.gracePeriod ?? 10_000))
result = 'starting';
if (result === 'failure') {
console.error(`Health Check ${this.id} failed:`, health.message);
}
await this.effects.setHealth({
...health,
id: this.id,
name: display,
result,
});
}
async updateStatus() {
const healths = this.dependencies.map((d) => ({
health: d.running && d._health,
id: d.id,
display: typeof d.ready === 'object' ? d.ready.display : null,
}));
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)}`);
if (waitingOn.length) {
const waitingOnNames = waitingOn.flatMap((w) => w.display ? [w.display] : []);
const message = waitingOnNames.length ? waitingOnNames.join(', ') : null;
await this.setHealth({ result: 'waiting', message });
}
else {
await this.setHealth({ result: 'starting', message: null });
}
await this.changeRunning(!waitingOn.length);
}
}
exports.HealthDaemon = HealthDaemon;
//# sourceMappingURL=HealthDaemon.js.map
File diff suppressed because one or more lines are too long
+87
View File
@@ -0,0 +1,87 @@
import * as T from '../../../base/lib/types';
import { MountOptions } from '../util/SubContainer';
type MountArray = {
mountpoint: string;
options: MountOptions;
}[];
type SharedOptions = {
/** The path within the resource to mount. Use `null` to mount the entire resource */
subpath: string | null;
/** Where to mount the resource. e.g. /data */
mountpoint: string;
/**
* Whether to mount this as a file or directory
*
* defaults to "directory"
* */
type?: 'file' | 'directory' | 'infer';
};
type VolumeOpts<Manifest extends T.SDKManifest> = {
/** The ID of the volume to mount. Must be one of the volume IDs defined in the manifest */
volumeId: Manifest['volumes'][number];
/** Whether or not the resource should be readonly for this subcontainer */
readonly: boolean;
} & SharedOptions;
type DependencyOpts<Manifest extends T.SDKManifest> = {
/** The ID of the dependency */
dependencyId: Manifest['id'];
/** The ID of the volume to mount. Must be one of the volume IDs defined in the manifest of the dependency */
volumeId: Manifest['volumes'][number];
/** Whether or not the resource should be readonly for this subcontainer */
readonly: boolean;
} & SharedOptions;
/**
* Immutable builder for declaring filesystem mounts into a subcontainer.
*
* Supports mounting volumes, static assets, dependency volumes, and backup directories.
* Each `mount*` method returns a new `Mounts` instance (immutable builder pattern).
*
* @typeParam Manifest - The service manifest type
* @typeParam Backups - Tracks whether backup mounts have been added (type-level flag)
*/
export declare class Mounts<Manifest extends T.SDKManifest, Backups extends SharedOptions = never> {
readonly volumes: VolumeOpts<Manifest>[];
readonly assets: SharedOptions[];
readonly dependencies: DependencyOpts<T.SDKManifest>[];
readonly backups: Backups[];
private constructor();
/**
* Create an empty Mounts builder with no mounts configured.
* @returns A new Mounts instance ready for chaining mount declarations
*/
static of<Manifest extends T.SDKManifest>(): Mounts<Manifest, never>;
/**
* Add a volume mount from the service's own volumes.
* @param options - Volume ID, mountpoint, readonly flag, and optional subpath
* @returns A new Mounts instance with this volume added
*/
mountVolume(options: VolumeOpts<Manifest>): Mounts<Manifest, Backups>;
/**
* Add a read-only mount of the service's packaged static assets.
* @param options - Mountpoint and optional subpath within the assets directory
* @returns A new Mounts instance with this asset mount added
*/
mountAssets(options: SharedOptions): Mounts<Manifest, Backups>;
/**
* Add a mount from a dependency package's volume.
* @param options - Dependency ID, volume ID, mountpoint, readonly flag, and optional subpath
* @returns A new Mounts instance with this dependency mount added
*/
mountDependency<DependencyManifest extends T.SDKManifest>(options: DependencyOpts<DependencyManifest>): Mounts<Manifest, Backups>;
/**
* Add a mount of the backup directory. Only valid during backup/restore operations.
* @param options - Mountpoint and optional subpath within the backup directory
* @returns A new Mounts instance with this backup mount added
*/
mountBackups(options: SharedOptions): Mounts<Manifest, {
subpath: string | null;
mountpoint: string;
}>;
/**
* Compile all declared mounts into the low-level mount array consumed by the subcontainer runtime.
* @throws If any two mounts share the same mountpoint
* @returns An array of `{ mountpoint, options }` objects
*/
build(): MountArray;
}
export {};
+114
View File
@@ -0,0 +1,114 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Mounts = void 0;
/**
* Immutable builder for declaring filesystem mounts into a subcontainer.
*
* Supports mounting volumes, static assets, dependency volumes, and backup directories.
* Each `mount*` method returns a new `Mounts` instance (immutable builder pattern).
*
* @typeParam Manifest - The service manifest type
* @typeParam Backups - Tracks whether backup mounts have been added (type-level flag)
*/
class Mounts {
constructor(volumes, assets, dependencies, backups) {
this.volumes = volumes;
this.assets = assets;
this.dependencies = dependencies;
this.backups = backups;
}
/**
* Create an empty Mounts builder with no mounts configured.
* @returns A new Mounts instance ready for chaining mount declarations
*/
static of() {
return new Mounts([], [], [], []);
}
/**
* Add a volume mount from the service's own volumes.
* @param options - Volume ID, mountpoint, readonly flag, and optional subpath
* @returns A new Mounts instance with this volume added
*/
mountVolume(options) {
return new Mounts([...this.volumes, options], [...this.assets], [...this.dependencies], [...this.backups]);
}
/**
* Add a read-only mount of the service's packaged static assets.
* @param options - Mountpoint and optional subpath within the assets directory
* @returns A new Mounts instance with this asset mount added
*/
mountAssets(options) {
return new Mounts([...this.volumes], [...this.assets, options], [...this.dependencies], [...this.backups]);
}
/**
* Add a mount from a dependency package's volume.
* @param options - Dependency ID, volume ID, mountpoint, readonly flag, and optional subpath
* @returns A new Mounts instance with this dependency mount added
*/
mountDependency(options) {
return new Mounts([...this.volumes], [...this.assets], [...this.dependencies, options], [...this.backups]);
}
/**
* Add a mount of the backup directory. Only valid during backup/restore operations.
* @param options - Mountpoint and optional subpath within the backup directory
* @returns A new Mounts instance with this backup mount added
*/
mountBackups(options) {
return new Mounts([...this.volumes], [...this.assets], [...this.dependencies], [...this.backups, options]);
}
/**
* Compile all declared mounts into the low-level mount array consumed by the subcontainer runtime.
* @throws If any two mounts share the same mountpoint
* @returns An array of `{ mountpoint, options }` objects
*/
build() {
const mountpoints = new Set();
for (let mountpoint of this.volumes
.map((v) => v.mountpoint)
.concat(this.assets.map((a) => a.mountpoint))
.concat(this.dependencies.map((d) => d.mountpoint))) {
if (mountpoints.has(mountpoint)) {
throw new Error(`cannot mount more than once to mountpoint ${mountpoint}`);
}
mountpoints.add(mountpoint);
}
return []
.concat(this.volumes.map((v) => ({
mountpoint: v.mountpoint,
options: {
type: 'volume',
volumeId: v.volumeId,
subpath: v.subpath,
readonly: v.readonly,
filetype: v.type ?? 'directory',
idmap: [],
},
})))
.concat(this.assets.map((a) => ({
mountpoint: a.mountpoint,
options: {
type: 'assets',
subpath: a.subpath,
filetype: a.type ?? 'directory',
idmap: [],
},
})))
.concat(this.dependencies.map((d) => ({
mountpoint: d.mountpoint,
options: {
type: 'pointer',
packageId: d.dependencyId,
volumeId: d.volumeId,
subpath: d.subpath,
readonly: d.readonly,
filetype: d.type ?? 'directory',
idmap: [],
},
})));
}
}
exports.Mounts = Mounts;
const a = Mounts.of().mountBackups({ subpath: null, mountpoint: '' });
// @ts-expect-error
const m = a;
//# sourceMappingURL=Mounts.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"Mounts.js","sourceRoot":"","sources":["../../../../package/lib/mainFn/Mounts.ts"],"names":[],"mappings":"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"}
+12
View File
@@ -0,0 +1,12 @@
import * as T from '../../../base/lib/types';
import { SubContainer } from '../util/SubContainer';
import { Daemon } from './Daemon';
import { DaemonCommandType } from './Daemons';
/**
* 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 declare class Oneshot<Manifest extends T.SDKManifest, C extends SubContainer<Manifest> | null = SubContainer<Manifest> | null> extends Daemon<Manifest, C> {
static of<Manifest extends T.SDKManifest>(): <C extends SubContainer<Manifest> | null>(effects: T.Effects, subcontainer: C, exec: DaemonCommandType<Manifest, C>) => Oneshot<Manifest, C>;
}
+23
View File
@@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Oneshot = void 0;
const CommandController_1 = require("./CommandController");
const Daemon_1 = require("./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
*/
class Oneshot extends Daemon_1.Daemon {
static of() {
return (effects, subcontainer, exec) => {
let subc = subcontainer;
if (subcontainer && subcontainer.isOwned())
subc = subcontainer.rc();
const startCommand = () => CommandController_1.CommandController.of()(effects, (subc?.rc() ?? null), exec);
return new Oneshot(subcontainer, startCommand, true);
};
}
}
exports.Oneshot = Oneshot;
//# sourceMappingURL=Oneshot.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"Oneshot.js","sourceRoot":"","sources":["../../../../package/lib/mainFn/Oneshot.ts"],"names":[],"mappings":";;;AAEA,2DAAuD;AACvD,qCAAiC;AAGjC;;;;GAIG;AAEH,MAAa,OAGX,SAAQ,eAAmB;IAC3B,MAAM,CAAC,EAAE;QACP,OAAO,CACL,OAAkB,EAClB,YAAe,EACf,IAAoC,EACpC,EAAE;YACF,IAAI,IAAI,GAAkC,YAAY,CAAA;YACtD,IAAI,YAAY,IAAI,YAAY,CAAC,OAAO,EAAE;gBAAE,IAAI,GAAG,YAAY,CAAC,EAAE,EAAE,CAAA;YACpE,MAAM,YAAY,GAAG,GAAG,EAAE,CACxB,qCAAiB,CAAC,EAAE,EAAe,CACjC,OAAO,EACP,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,IAAI,CAAM,EACzB,IAAI,CACL,CAAA;YACH,OAAO,IAAI,OAAO,CAAc,YAAY,EAAE,YAAY,EAAE,IAAI,CAAC,CAAA;QACnE,CAAC,CAAA;IACH,CAAC;CACF;AArBD,0BAqBC"}
+19
View File
@@ -0,0 +1,19 @@
import * as T from '../../../base/lib/types';
import { Daemons } from './Daemons';
import '../../../base/lib/interfaces/ServiceInterfaceBuilder';
import '../../../base/lib/interfaces/Origin';
/** Default time in milliseconds to wait for a process to exit after SIGTERM before escalating to SIGKILL */
export declare const DEFAULT_SIGTERM_TIMEOUT = 60000;
/**
* Used to ensure that the main function is running with the valid proofs.
* We first do the folowing order of things
* 1. We get the interfaces
* 2. We setup all the commands to setup the system
* 3. We create the health checks
* 4. We setup the daemons init system
* @param fn
* @returns
*/
export declare const setupMain: <Manifest extends T.SDKManifest>(fn: (o: {
effects: T.Effects;
}) => Promise<Daemons<Manifest, any>>) => T.ExpectedExports.main;
+25
View File
@@ -0,0 +1,25 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupMain = exports.DEFAULT_SIGTERM_TIMEOUT = void 0;
require("../../../base/lib/interfaces/ServiceInterfaceBuilder");
require("../../../base/lib/interfaces/Origin");
/** Default time in milliseconds to wait for a process to exit after SIGTERM before escalating to SIGKILL */
exports.DEFAULT_SIGTERM_TIMEOUT = 60_000;
/**
* Used to ensure that the main function is running with the valid proofs.
* We first do the folowing order of things
* 1. We get the interfaces
* 2. We setup all the commands to setup the system
* 3. We create the health checks
* 4. We setup the daemons init system
* @param fn
* @returns
*/
const setupMain = (fn) => {
return async (options) => {
const result = await fn(options);
return result;
};
};
exports.setupMain = setupMain;
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../package/lib/mainFn/index.ts"],"names":[],"mappings":";;;AAEA,gEAA6D;AAC7D,+CAA4C;AAE5C,4GAA4G;AAC/F,QAAA,uBAAuB,GAAG,MAAM,CAAA;AAC7C;;;;;;;;;GASG;AACI,MAAM,SAAS,GAAG,CACvB,EAAkE,EAC1C,EAAE;IAC1B,OAAO,KAAK,EAAE,OAAO,EAAE,EAAE;QACvB,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,CAAA;QAChC,OAAO,MAAM,CAAA;IACf,CAAC,CAAA;AACH,CAAC,CAAA;AAPY,QAAA,SAAS,aAOrB"}