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 68ec875ee7
commit 8298c083c7
3436 changed files with 867051 additions and 92 deletions
File diff suppressed because it is too large Load Diff
+755
View File
@@ -0,0 +1,755 @@
"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.StartSdk = exports.OSVersion = void 0;
exports.runCommand = runCommand;
const fs = __importStar(require("node:fs/promises"));
const actions = __importStar(require("../../base/lib/actions"));
const inputSpec_1 = require("../../base/lib/actions/input/builder/inputSpec");
const list_1 = require("../../base/lib/actions/input/builder/list");
const value_1 = require("../../base/lib/actions/input/builder/value");
const variants_1 = require("../../base/lib/actions/input/builder/variants");
const inputSpecConstants_1 = require("../../base/lib/actions/input/inputSpecConstants");
const setupActions_1 = require("../../base/lib/actions/setupActions");
const dependencies_1 = require("../../base/lib/dependencies/dependencies");
const setupDependencies_1 = require("../../base/lib/dependencies/setupDependencies");
const exver_1 = require("../../base/lib/exver");
const inits_1 = require("../../base/lib/inits");
const Host_1 = require("../../base/lib/interfaces/Host");
const ServiceInterfaceBuilder_1 = require("../../base/lib/interfaces/ServiceInterfaceBuilder");
const setupExportedUrls_1 = require("../../base/lib/interfaces/setupExportedUrls");
const setupInterfaces_1 = require("../../base/lib/interfaces/setupInterfaces");
const T = __importStar(require("../../base/lib/types"));
const GetContainerIp_1 = require("../../base/lib/util/GetContainerIp");
const GetStatus_1 = require("../../base/lib/util/GetStatus");
const getServiceInterface_1 = require("../../base/lib/util/getServiceInterface");
const getServiceInterfaces_1 = require("../../base/lib/util/getServiceInterfaces");
const patterns = __importStar(require("../../base/lib/util/patterns"));
const Backups_1 = require("./backup/Backups");
const setupBackups_1 = require("./backup/setupBackups");
const checkFns_1 = require("./health/checkFns");
const checkPortListening_1 = require("./health/checkFns/checkPortListening");
const mainFn_1 = require("./mainFn");
const Daemons_1 = require("./mainFn/Daemons");
const Mounts_1 = require("./mainFn/Mounts");
const trigger_1 = require("./trigger");
const defaultTrigger_1 = require("./trigger/defaultTrigger");
const successFailure_1 = require("./trigger/successFailure");
const util_1 = require("./util");
const SubContainer_1 = require("./util/SubContainer");
const Volume_1 = require("./util/Volume");
const version_1 = require("./version");
/** The minimum StartOS version required by this SDK release */
exports.OSVersion = (0, exver_1.testTypeVersion)('0.4.0-alpha.23');
/**
* The top-level SDK facade for building StartOS service packages.
*
* Use `StartSdk.of()` to create an uninitialized instance, then call `.withManifest()`
* to bind it to a manifest, and finally `.build()` to obtain the full toolkit of helpers
* for actions, daemons, backups, interfaces, health checks, and more.
*
* @typeParam Manifest - The service manifest type; starts as `never` until `.withManifest()` is called.
*/
class StartSdk {
constructor(manifest) {
this.manifest = manifest;
}
/**
* Create an uninitialized StartSdk instance. Call `.withManifest()` next.
* @returns A new StartSdk with no manifest bound.
*/
static of() {
return new StartSdk(null);
}
/**
* Bind a manifest to the SDK, producing a typed SDK instance.
* @param manifest - The service manifest definition
* @returns A new StartSdk instance parameterized by the given manifest type
*/
withManifest(manifest) {
return new StartSdk(manifest);
}
ifPluginEnabled(plugin, value) {
if (this.manifest.plugins?.includes(plugin))
return value;
return null;
}
/**
* Finalize the SDK and return the full set of helpers for building a StartOS service.
*
* This method is only callable after `.withManifest()` has been called (enforced at the type level).
*
* @param isReady - Type-level gate; resolves to `true` only when a manifest is bound.
* @returns An object containing all SDK utilities: actions, daemons, backups, interfaces, health checks, volumes, triggers, and more.
*/
build(isReady) {
const startSdkEffectWrapper = {
restart: (effects, ...args) => effects.restart(...args),
setDependencies: (effects, ...args) => effects.setDependencies(...args),
checkDependencies: (effects, ...args) => effects.checkDependencies(...args),
mount: (effects, ...args) => effects.mount(...args),
getInstalledPackages: (effects, ...args) => effects.getInstalledPackages(...args),
getServicePortForward: (effects, ...args) => effects.getServicePortForward(...args),
clearBindings: (effects, ...args) => effects.clearBindings(...args),
getOsIp: (effects, ...args) => effects.getOsIp(...args),
getSslKey: (effects, ...args) => effects.getSslKey(...args),
shutdown: (effects, ...args) => effects.shutdown(...args),
getDependencies: (effects, ...args) => effects.getDependencies(...args),
setHealth: (effects, ...args) => effects.setHealth(...args),
};
return {
/** The bound service manifest */
manifest: this.manifest,
/** Volume path helpers derived from the manifest volume definitions */
volumes: (0, Volume_1.createVolumes)(this.manifest),
...startSdkEffectWrapper,
/** Persist the current data version to the StartOS effect system */
setDataVersion: version_1.setDataVersion,
/** Retrieve the current data version from the StartOS effect system */
getDataVersion: version_1.getDataVersion,
action: {
/** Execute an action by its ID, optionally providing input */
run: actions.runAction,
/** Create a task notification for a specific package's action */
createTask: (effects, packageId, action, severity, options) => actions.createTask({
effects,
packageId,
action,
severity,
options: options,
}),
/** Create a task notification for this service's own action (uses manifest.id automatically) */
createOwnTask: (effects, action, severity, options) => actions.createTask({
effects,
packageId: this.manifest.id,
action,
severity,
options: options,
}),
/**
* Clear one or more task notifications by their replay IDs
* @param effects - The effects context
* @param replayIds - One or more replay IDs of the tasks to clear
*/
clearTask: (effects, ...replayIds) => effects.action.clearTasks({ only: replayIds }),
},
/**
* Check whether the specified (or all) dependencies are satisfied.
* @param effects - The effects context
* @param packageIds - Optional subset of dependency IDs to check; defaults to all
* @returns An object describing which dependencies are satisfied and which are not
*/
checkDependencies: dependencies_1.checkDependencies,
serviceInterface: {
/** Retrieve a single service interface belonging to this package by its ID */
getOwn: getServiceInterface_1.getOwnServiceInterface,
/** Retrieve a single service interface from any package */
get: util_1.getServiceInterface,
/** Retrieve all service interfaces belonging to this package */
getAllOwn: getServiceInterfaces_1.getOwnServiceInterfaces,
/** Retrieve all service interfaces, optionally filtering by package */
getAll: util_1.getServiceInterfaces,
},
/**
* Get the container IP address with reactive subscription support.
*
* Returns an object with multiple read strategies: `const()` for a value
* that retries on change, `once()` for a single read, `watch()` for an async
* generator, `onChange()` for a callback, and `waitFor()` to block until a predicate is met.
*
* @param effects - The effects context
* @param options - Optional filtering options (e.g. `containerId`)
*/
getContainerIp: (effects, options = {}) => new GetContainerIp_1.GetContainerIp(effects, options),
/**
* Get the service's current status with reactive subscription support.
*
* Returns an object with multiple read strategies: `const()` for a value
* that retries on change, `once()` for a single read, `watch()` for an async
* generator, `onChange()` for a callback, and `waitFor()` to block until a predicate is met.
*
* @param effects - The effects context
* @param options - Optional filtering options (e.g. `packageId`)
*/
getStatus: (effects, options = {}) => new GetStatus_1.GetStatus(effects, options),
MultiHost: {
/**
* Create a new MultiHost instance for binding ports and exporting interfaces.
* @param effects - The effects context
* @param id - A unique identifier for this multi-host group
*/
of: (effects, id) => new Host_1.MultiHost({ id, effects }),
},
/**
* Return `null` if the given string is empty, otherwise return the string unchanged.
* Useful for converting empty user input into explicit null values.
*/
nullIfEmpty: util_1.nullIfEmpty,
/**
* Indicate that a daemon should use the container image's configured entrypoint.
* @param overrideCmd - Optional command arguments to append after the entrypoint
*/
useEntrypoint: (overrideCmd) => new T.UseEntrypoint(overrideCmd),
/**
* @description Use this class to create an Action. By convention, each Action should receive its own file.
*
*/
Action: {
/**
* @description Use this function to create an action that accepts form input
* @param id - a unique ID for this action
* @param metadata - information describing the action and its availability
* @param inputSpec - define the form input using the InputSpec and Value classes
* @param prefillFn - optionally fetch data from the file system to pre-fill the input form. Must returns a deep partial of the input spec
* @param executionFn - execute the action. Optionally return data for the user to view. Must be in the structure of an ActionResult, version "1"
* @example
* In this example, we create an action for a user to provide their name.
* We prefill the input form with their existing name from the service's yaml file.
* The new name is saved to the yaml file, and we return nothing to the user, which
* means they will receive a generic success message.
*
* ```
import { sdk } from '../sdk'
import { yamlFile } from '../file-models/config.yml'
const { InputSpec, Value } = sdk
export const inputSpec = InputSpec.of({
name: Value.text({
name: 'Name',
description:
'When you launch the Hello World UI, it will display "Hello [Name]"',
required: true,
default: 'World',
}),
})
export const setName = sdk.Action.withInput(
// id
'set-name',
// metadata
async ({ effects }) => ({
name: 'Set Name',
description: 'Set your name so Hello World can say hello to you',
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
// form input specification
inputSpec,
// optionally pre-fill the input form
async ({ effects }) => {
const name = await yamlFile.read.const(effects)?.name
return { name }
},
// the execution function
async ({ effects, input }) => yamlFile.merge(input)
)
* ```
*/
withInput: setupActions_1.Action.withInput,
/**
* @description Use this function to create an action that does not accept form input
* @param id - a unique ID for this action
* @param metadata - information describing the action and its availability
* @param executionFn - execute the action. Optionally return data for the user to view. Must be in the structure of an ActionResult, version "1"
* @example
* In this example, we create an action that returns a secret phrase for the user to see.
*
* ```
import { store } from '../file-models/store.json'
import { sdk } from '../sdk'
export const showSecretPhrase = sdk.Action.withoutInput(
// id
'show-secret-phrase',
// metadata
async ({ effects }) => ({
name: 'Show Secret Phrase',
description: 'Reveal the secret phrase for Hello World',
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
// the execution function
async ({ effects }) => ({
version: '1',
title: 'Secret Phrase',
message:
'Below is your secret phrase. Use it to gain access to extraordinary places',
result: {
type: 'single',
value: (await store.read.once())?.secretPhrase,
copyable: true,
qr: true,
masked: true,
},
}),
)
* ```
*/
withoutInput: (id, metadata, run) => setupActions_1.Action.withoutInput(id, metadata, run),
},
inputSpecConstants: {
smtpInputSpec: inputSpecConstants_1.smtpInputSpec,
systemSmtpSpec: inputSpecConstants_1.systemSmtpSpec,
customSmtp: inputSpecConstants_1.customSmtp,
smtpProviderVariants: inputSpecConstants_1.smtpProviderVariants,
},
/**
* @description Use this function to create a service interface.
* @param effects
* @param options
* @example
* In this example, we create a standard web UI
*
* ```
const ui = sdk.createInterface(effects, {
name: 'Web UI',
id: 'ui',
description: 'The primary web app for this service.',
type: 'ui',
masked: false,
schemeOverride: null,
username: null,
path: '',
query: {},
})
* ```
*/
createInterface: (effects, options) => new ServiceInterfaceBuilder_1.ServiceInterfaceBuilder({ ...options, effects }),
/**
* Get the system SMTP configuration with reactive subscription support.
* @param effects - The effects context
*/
getSystemSmtp: (effects) => new util_1.GetSystemSmtp(effects),
/**
* Get the outbound network gateway address with reactive subscription support.
* @param effects - The effects context
*/
getOutboundGateway: (effects) => new util_1.GetOutboundGateway(effects),
/**
* Get an SSL certificate for the given hostnames with reactive subscription support.
* @param effects - The effects context
* @param hostnames - The hostnames to obtain a certificate for
* @param algorithm - Optional algorithm preference (e.g. Ed25519)
*/
getSslCertificate: (effects, hostnames, algorithm) => new util_1.GetSslCertificate(effects, { hostnames, algorithm }),
/** Retrieve the manifest of any installed service package by its ID */
getServiceManifest: util_1.getServiceManifest,
healthCheck: {
checkPortListening: checkPortListening_1.checkPortListening,
checkWebUrl: checkFns_1.checkWebUrl,
runHealthScript: checkFns_1.runHealthScript,
},
/** Common utility patterns (e.g. hostname regex, port validators) */
patterns,
/**
* @description Use this function to list every Action offered by the service. Actions will be displayed in the provided order.
*
* By convention, each Action should receive its own file in the "actions" directory.
* @example
*
* ```
import { sdk } from '../sdk'
import { config } from './config'
import { nameToLogs } from './nameToLogs'
export const actions = sdk.Actions.of().addAction(config).addAction(nameToLogs)
* ```
*/
Actions: (setupActions_1.Actions),
/**
* @description Use this function to determine which volumes are backed up when a user creates a backup, including advanced options.
* @example
* In this example, we back up the entire "main" volume and nothing else.
*
* ```
import { sdk } from './sdk'
export const { createBackup, restoreBackup } = sdk.setupBackups(
async ({ effects }) => sdk.Backups.volumes('main'),
)
* ```
* @example
* In this example, we back up the "main" volume, but exclude hypothetical directory "excludedDir".
*
* ```
import { sdk } from './sdk'
export const { createBackup, restoreBackup } = sdk.setupBackups(async () =>
sdk.Backups.volumes('main').setOptions({
exclude: ['excludedDir'],
}),
)
* ```
*/
setupBackups: (options) => (0, setupBackups_1.setupBackups)(options),
/**
* @description Use this function to set dependency information.
* @example
* In this example, we create a dependency on Hello World >=1.0.0:0, where Hello World must be running and passing its "primary" health check.
*
* ```
export const setDependencies = sdk.setupDependencies(
async ({ effects }) => {
return {
'hello-world': {
kind: 'running',
versionRange: '>=1.0.0',
healthChecks: ['primary'],
},
}
},
)
* ```
*/
setupDependencies: (setupDependencies_1.setupDependencies),
/**
* @description Use this function to create an InitScript that runs every time the service initializes (install, update, restore, rebuild, and server bootup)
*/
setupOnInit: inits_1.setupOnInit,
/**
* @description Use this function to create an UninitScript that runs every time the service uninitializes (update, uninstall, and server shutdown)
*/
setupOnUninit: inits_1.setupOnUninit,
/**
* @description Use this function to setup what happens when the service initializes.
*
* This happens when the server boots, or a service is installed, updated, or restored
*
* Not every init script does something on every initialization. For example, versions only does something on install or update
*
* These scripts are run in the order they are supplied
* @example
*
* ```
export const init = sdk.setupInit(
restoreInit,
versions,
setDependencies,
setInterfaces,
actions,
postInstall,
)
* ```
*/
setupInit: inits_1.setupInit,
/**
* @description Use this function to setup what happens when the service uninitializes.
*
* This happens when the server shuts down, or a service is uninstalled or updated
*
* Not every uninit script does something on every uninitialization. For example, versions only does something on uninstall or update
*
* These scripts are run in the order they are supplied
* @example
*
* ```
export const uninit = sdk.setupUninit(
versions,
)
* ```
*/
setupUninit: inits_1.setupUninit,
/**
* @description Use this function to determine how this service will be hosted and served. The function executes on service install, service update, and inputSpec save.
* @param inputSpec - The inputSpec spec of this service as exported from /inputSpec/spec.
* @param fn - an async function that returns an array of interface receipts. The function always has access to `effects`; it has access to `input` only after inputSpec save, otherwise `input` will be null.
* @example
* In this example, we create two UIs from one multi-host, and one API from another multi-host.
*
* ```
export const setInterfaces = sdk.setupInterfaces(
async ({ effects }) => {
// ** UI multi-host **
const uiMulti = sdk.MultiHost.of(effects, 'ui-multi')
const uiMultiOrigin = await uiMulti.bindPort(80, {
protocol: 'http',
})
// Primary UI
const primaryUi = sdk.createInterface(effects, {
name: 'Primary UI',
id: 'primary-ui',
description: 'The primary web app for this service.',
type: 'ui',
masked: false,
schemeOverride: null,
username: null,
path: '',
query: {},
})
// Admin UI
const adminUi = sdk.createInterface(effects, {
name: 'Admin UI',
id: 'admin-ui',
description: 'The admin web app for this service.',
type: 'ui',
masked: false,
schemeOverride: null,
username: null,
path: '/admin',
query: {},
})
// UI receipt
const uiReceipt = await uiMultiOrigin.export([primaryUi, adminUi])
// ** API multi-host **
const apiMulti = sdk.MultiHost.of(effects, 'api-multi')
const apiMultiOrigin = await apiMulti.bindPort(5959, {
protocol: 'http',
})
// API
const api = sdk.createInterface(effects, {
name: 'Admin API',
id: 'api',
description: 'The advanced API for this service.',
type: 'api',
masked: false,
schemeOverride: null,
username: null,
path: '',
query: {},
})
// API receipt
const apiReceipt = await apiMultiOrigin.export([api])
// ** Return receipts **
return [uiReceipt, apiReceipt]
},
)
* ```
*/
setupInterfaces: setupInterfaces_1.setupServiceInterfaces,
/**
* Define the main entrypoint for the service. The provided function should
* configure and return a `Daemons` instance describing all long-running processes.
* @param fn - Async function that receives `effects` and returns a `Daemons` instance
*/
setupMain: (fn) => (0, mainFn_1.setupMain)(fn),
/** Built-in trigger strategies for controlling health-check polling intervals */
trigger: {
/** Default trigger: polls at a fixed interval */
defaultTrigger: defaultTrigger_1.defaultTrigger,
/** Trigger with a cooldown period between checks */
cooldownTrigger: trigger_1.cooldownTrigger,
/** Switches to a different interval after the first successful check */
changeOnFirstSuccess: trigger_1.changeOnFirstSuccess,
/** Uses different intervals based on success vs failure results */
successFailure: successFailure_1.successFailure,
},
Mounts: {
/**
* Create an empty Mounts builder for declaring volume, asset, dependency, and backup mounts.
* @returns A new Mounts instance with no mounts configured
*/
of: (Mounts_1.Mounts.of),
},
Backups: {
/**
* Create a Backups configuration that backs up entire volumes by name.
* @param volumeNames - Volume IDs from the manifest to include in backups
*/
ofVolumes: (Backups_1.Backups.ofVolumes),
/**
* Create a Backups configuration from explicit sync path pairs.
* @param syncs - Array of `{ dataPath, backupPath }` objects
*/
ofSyncs: (Backups_1.Backups.ofSyncs),
/**
* Create a Backups configuration with custom rsync options (e.g. exclude patterns).
* @param options - Partial sync options to override defaults
*/
withOptions: (Backups_1.Backups.withOptions),
/**
* Create a Backups configuration that uses pg_dump/pg_restore instead of
* rsyncing the raw PostgreSQL data directory. Chain `.addVolume()` to include
* additional volumes in the backup.
*/
withPgDump: (Backups_1.Backups.withPgDump),
/**
* Create a Backups configuration that uses mysqldump/mysql instead of
* rsyncing the raw MySQL/MariaDB data directory. Chain `.addVolume()` to
* include additional volumes in the backup.
*/
withMysqlDump: (Backups_1.Backups.withMysqlDump),
},
InputSpec: {
/**
* @description Use this function to define the inputSpec specification that will ultimately present to the user as validated form inputs.
*
* Most form controls are supported, including text, textarea, number, toggle, select, multiselect, list, color, datetime, object (sub form), and union (conditional sub form).
* @example
* In this example, we define a inputSpec form with two value: name and makePublic.
*
* ```
import { sdk } from '../sdk'
const { InputSpec, Value } = sdk
export const inputSpecSpec = InputSpec.of({
name: Value.text({
name: 'Name',
description:
'When you launch the Hello World UI, it will display "Hello [Name]"',
required: true,
default: 'World'
}),
makePublic: Value.toggle({
name: 'Make Public',
description: 'Whether or not to expose the service to the network',
default: false,
}),
})
* ```
*/
of: (spec) => inputSpec_1.InputSpec.of(spec),
},
Daemon: {
/**
* Create a single Daemon that wraps a long-running process with automatic restart logic.
* Returns a curried function: call with `(effects, subcontainer, exec)`.
*/
get of() {
return Daemons_1.Daemon.of();
},
},
Daemons: {
/**
* Create a new Daemons builder for defining the service's daemon topology.
* Chain `.addDaemon()` calls to register each long-running process.
* @param effects - The effects context
*/
of(effects) {
return Daemons_1.Daemons.of({ effects });
},
},
SubContainer: {
/**
* @description Create a new SubContainer
* @param effects
* @param image - what container image to use
* @param mounts - what to mount to the subcontainer
* @param name - a name to use to refer to the subcontainer for debugging purposes
*/
of(effects, image, mounts, name) {
return SubContainer_1.SubContainerOwned.of(effects, image, mounts, name).then((subc) => subc.rc());
},
/**
* @description Run a function with a temporary SubContainer
* @param effects
* @param image - what container image to use
* @param mounts - what to mount to the subcontainer
* @param name - a name to use to refer to the ephemeral subcontainer for debugging purposes
*/
withTemp(effects, image, mounts, name, fn) {
return SubContainer_1.SubContainerOwned.withTemp(effects, image, mounts, name, fn);
},
},
List: list_1.List,
Value: value_1.Value,
Variants: variants_1.Variants,
plugin: {
url: this.ifPluginEnabled('url-v0', {
register: (effects, options) => effects.plugin.url.register({
tableAction: options.tableAction.id,
}),
exportUrl: (effects, options) => effects.plugin.url.exportUrl({
hostnameInfo: options.hostnameInfo,
removeAction: options.removeAction?.id ?? null,
overflowActions: options.overflowActions.map((a) => a.id),
}),
setupExportedUrls: setupExportedUrls_1.setupExportedUrls, // similar to setupInterfaces
}),
},
};
}
}
exports.StartSdk = StartSdk;
/**
* Run a one-shot command inside a temporary subcontainer.
*
* Creates a subcontainer, executes the command, and destroys the subcontainer when finished.
* Throws an {@link ExitError} if the command exits with a non-zero code or signal.
*
* @param effects - The effects context
* @param image - The container image to use
* @param command - The command to execute (string array or UseEntrypoint)
* @param options - Mount and command options
* @param name - Optional human-readable name for debugging
* @returns The stdout and stderr output of the command
*/
async function runCommand(effects, image, command, options, name) {
let commands;
if (T.isUseEntrypoint(command)) {
const imageMeta = await fs
.readFile(`/media/startos/images/${image.imageId}.json`, {
encoding: 'utf8',
})
.catch(() => '{}')
.then(JSON.parse);
commands = imageMeta.entrypoint ?? [];
commands = commands.concat(...(command.overridCmd ?? imageMeta.cmd ?? []));
}
else
commands = (0, util_1.splitCommand)(command);
return SubContainer_1.SubContainerOwned.withTemp(effects, image, options.mounts, name ||
commands
.map((c) => {
if (c.includes(' ')) {
return `"${c.replace(/"/g, `\"`)}"`;
}
else {
return c;
}
})
.join(' '), async (subcontainer) => {
const res = await subcontainer.exec(commands);
if (res.exitCode || res.exitSignal) {
throw new SubContainer_1.ExitError(commands[0], res);
}
else {
return res;
}
});
}
//# sourceMappingURL=StartSdk.js.map
File diff suppressed because one or more lines are too long
+183
View File
@@ -0,0 +1,183 @@
import * as T from '../../../base/lib/types';
import { Affine } from '../util';
import { InitKind, InitScript } from '../../../base/lib/inits';
/** A password value, or a function that returns one. Functions are resolved lazily (only during restore). */
export type LazyPassword = string | (() => string | Promise<string>) | null;
/** Configuration for PostgreSQL dump-based backup */
export type PgDumpConfig<M extends T.SDKManifest> = {
/** Image ID of the PostgreSQL container (e.g. 'postgres') */
imageId: keyof M['images'] & T.ImageId;
/** Volume ID containing the PostgreSQL data directory */
dbVolume: M['volumes'][number];
/** Volume mountpoint (e.g. '/var/lib/postgresql') */
mountpoint: string;
/** Subpath from mountpoint to PGDATA (e.g. '/data', '/18/docker') */
pgdataPath: string;
/** PostgreSQL database name to dump */
database: string;
/** PostgreSQL user */
user: string;
/** PostgreSQL password (for restore). Can be a string, a function that returns one (resolved lazily after volumes are restored), or null for trust auth. */
password: LazyPassword;
/** Additional initdb arguments (e.g. ['--data-checksums']) */
initdbArgs?: string[];
/** Additional options passed to `pg_ctl start -o` (e.g. '-c shared_preload_libraries=vectorchord'). Appended after `-c listen_addresses=`. */
pgOptions?: string;
};
/** Configuration for MySQL/MariaDB dump-based backup */
export type MysqlDumpConfig<M extends T.SDKManifest> = {
/** Image ID of the MySQL/MariaDB container (e.g. 'mysql', 'mariadb') */
imageId: keyof M['images'] & T.ImageId;
/** Volume ID containing the MySQL data directory */
dbVolume: M['volumes'][number];
/** Path to MySQL data directory within the container (typically '/var/lib/mysql') */
datadir: string;
/** MySQL database name to dump */
database: string;
/** MySQL user for dump operations */
user: string;
/** MySQL password. Can be a string or a function that returns one — functions are resolved lazily after volumes are restored. */
password: LazyPassword;
/** Database engine: 'mysql' uses --initialize-insecure, 'mariadb' uses mysql_install_db */
engine: 'mysql' | 'mariadb';
/** Custom readiness check command (default: ['mysqladmin', 'ping', ...]) */
readyCommand?: string[];
/** Additional options passed to `mysqld` on startup (e.g. '--innodb-buffer-pool-size=256M'). Appended after `--bind-address=127.0.0.1`. */
mysqldOptions?: string[];
};
/** Default rsync options used for backup and restore operations */
export declare const DEFAULT_OPTIONS: T.SyncOptions;
/** A single source-to-destination sync pair for backup and restore */
export type BackupSync<Volumes extends string> = {
dataPath: `/media/startos/volumes/${Volumes}/${string}`;
backupPath: `/media/startos/backup/${string}`;
options?: Partial<T.SyncOptions>;
backupOptions?: Partial<T.SyncOptions>;
restoreOptions?: Partial<T.SyncOptions>;
};
/** Effects type narrowed for backup/restore contexts, preventing reuse outside that scope */
export type BackupEffects = T.Effects & Affine<'Backups'>;
/**
* Configures backup and restore operations using rsync.
*
* Supports syncing entire volumes or custom path pairs, with optional pre/post hooks
* for both backup and restore phases. Implements {@link InitScript} so it can be used
* as a restore-init step in `setupInit`.
*
* @typeParam M - The service manifest type
*/
export declare class Backups<M extends T.SDKManifest> implements InitScript {
private options;
private restoreOptions;
private backupOptions;
private backupSet;
private preBackup;
private postBackup;
private preRestore;
private postRestore;
private constructor();
/**
* Create a Backups configuration that backs up entire volumes by name.
* Each volume is synced to a corresponding directory under `/media/startos/backup/volumes/`.
* @param volumeNames - One or more volume IDs from the manifest
*/
static ofVolumes<M extends T.SDKManifest = never>(...volumeNames: Array<M['volumes'][number]>): Backups<M>;
/**
* Create a Backups configuration from explicit source/destination sync pairs.
* @param syncs - Array of `{ dataPath, backupPath }` objects with optional per-sync options
*/
static ofSyncs<M extends T.SDKManifest = never>(...syncs: BackupSync<M['volumes'][number]>[]): Backups<M>;
/**
* Create an empty Backups configuration with custom default rsync options.
* Chain `.addVolume()` or `.addSync()` to add sync targets.
* @param options - Partial rsync options to override defaults (e.g. `{ exclude: ['cache'] }`)
*/
static withOptions<M extends T.SDKManifest = never>(options?: Partial<T.SyncOptions>): Backups<M>;
/**
* Configure PostgreSQL dump-based backup for a volume.
*
* Instead of rsyncing the raw PostgreSQL data directory (which is slow and error-prone),
* this uses `pg_dump` to create a logical dump before backup and `pg_restore` to rebuild
* the database after restore.
*
* The dump file is written directly to the backup target — no data duplication on disk.
*
* @returns A configured Backups instance with pre/post hooks. Chain `.addVolume()` or
* `.addSync()` to include additional volumes/paths in the backup.
*/
static withPgDump<M extends T.SDKManifest = never>(config: PgDumpConfig<M>): Backups<M>;
/**
* Configure MySQL/MariaDB dump-based backup for a volume.
*
* Instead of rsyncing the raw MySQL data directory (which is slow and error-prone),
* this uses `mysqldump` to create a logical dump before backup and `mysql` to restore
* the database after restore.
*
* The dump file is stored temporarily in `dumpVolume` during backup and cleaned up afterward.
*
* @returns A configured Backups instance with pre/post hooks. Chain `.addVolume()` or
* `.addSync()` to include additional volumes/paths in the backup.
*/
static withMysqlDump<M extends T.SDKManifest = never>(config: MysqlDumpConfig<M>): Backups<M>;
/**
* Override the default rsync options for both backup and restore.
* @param options - Partial rsync options to merge with current defaults
*/
setOptions(options?: Partial<T.SyncOptions>): this;
/**
* Override rsync options used only during backup (not restore).
* @param options - Partial rsync options for the backup phase
*/
setBackupOptions(options?: Partial<T.SyncOptions>): this;
/**
* Override rsync options used only during restore (not backup).
* @param options - Partial rsync options for the restore phase
*/
setRestoreOptions(options?: Partial<T.SyncOptions>): this;
/**
* Register a hook to run before backup rsync begins (e.g. dump a database).
* @param fn - Async function receiving backup-scoped effects
*/
setPreBackup(fn: (effects: BackupEffects) => Promise<void>): this;
/**
* Register a hook to run after backup rsync completes.
* @param fn - Async function receiving backup-scoped effects
*/
setPostBackup(fn: (effects: BackupEffects) => Promise<void>): this;
/**
* Register a hook to run before restore rsync begins.
* @param fn - Async function receiving backup-scoped effects
*/
setPreRestore(fn: (effects: BackupEffects) => Promise<void>): this;
/**
* Register a hook to run after restore rsync completes.
* @param fn - Async function receiving backup-scoped effects
*/
setPostRestore(fn: (effects: BackupEffects) => Promise<void>): this;
/**
* Add a volume to the backup set by its ID.
* @param volume - The volume ID from the manifest
* @param options - Optional per-volume rsync overrides
*/
addVolume(volume: M['volumes'][number], options?: Partial<{
options: T.SyncOptions;
backupOptions: T.SyncOptions;
restoreOptions: T.SyncOptions;
}>): this;
/**
* Add a custom sync pair to the backup set.
* @param sync - A `{ dataPath, backupPath }` object with optional per-sync rsync options
*/
addSync(sync: BackupSync<M['volumes'][0]>): this;
/**
* Execute the backup: runs pre-hook, rsyncs all configured paths, saves the data version, then runs post-hook.
* @param effects - The effects context
*/
createBackup(effects: T.Effects): Promise<void>;
init(effects: T.Effects, kind: InitKind): Promise<void>;
/**
* Execute the restore: runs pre-hook, rsyncs all configured paths from backup to data, restores the data version, then runs post-hook.
* @param effects - The effects context
*/
restoreBackup(effects: T.Effects): Promise<void>;
}
+575
View File
@@ -0,0 +1,575 @@
"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.Backups = exports.DEFAULT_OPTIONS = void 0;
const child_process = __importStar(require("child_process"));
const fs = __importStar(require("fs/promises"));
const util_1 = require("../util");
const SubContainer_1 = require("../util/SubContainer");
const Mounts_1 = require("../mainFn/Mounts");
const BACKUP_HOST_PATH = '/media/startos/backup';
const BACKUP_CONTAINER_MOUNT = '/backup-target';
async function resolvePassword(pw) {
if (pw === null)
return null;
return typeof pw === 'function' ? pw() : pw;
}
/** Bind-mount the backup target into a SubContainer's rootfs */
async function mountBackupTarget(rootfs) {
const target = `${rootfs}${BACKUP_CONTAINER_MOUNT}`;
await fs.mkdir(target, { recursive: true });
await (0, SubContainer_1.execFile)('mount', ['--rbind', BACKUP_HOST_PATH, target]);
}
/** Default rsync options used for backup and restore operations */
exports.DEFAULT_OPTIONS = {
delete: true,
exclude: [],
};
/**
* Configures backup and restore operations using rsync.
*
* Supports syncing entire volumes or custom path pairs, with optional pre/post hooks
* for both backup and restore phases. Implements {@link InitScript} so it can be used
* as a restore-init step in `setupInit`.
*
* @typeParam M - The service manifest type
*/
class Backups {
constructor(options = exports.DEFAULT_OPTIONS, restoreOptions = {}, backupOptions = {}, backupSet = [], preBackup = async (effects) => { }, postBackup = async (effects) => { }, preRestore = async (effects) => { }, postRestore = async (effects) => { }) {
this.options = options;
this.restoreOptions = restoreOptions;
this.backupOptions = backupOptions;
this.backupSet = backupSet;
this.preBackup = preBackup;
this.postBackup = postBackup;
this.preRestore = preRestore;
this.postRestore = postRestore;
}
/**
* Create a Backups configuration that backs up entire volumes by name.
* Each volume is synced to a corresponding directory under `/media/startos/backup/volumes/`.
* @param volumeNames - One or more volume IDs from the manifest
*/
static ofVolumes(...volumeNames) {
return Backups.ofSyncs(...volumeNames.map((srcVolume) => ({
dataPath: `/media/startos/volumes/${srcVolume}/`,
backupPath: `/media/startos/backup/volumes/${srcVolume}/`,
})));
}
/**
* Create a Backups configuration from explicit source/destination sync pairs.
* @param syncs - Array of `{ dataPath, backupPath }` objects with optional per-sync options
*/
static ofSyncs(...syncs) {
return syncs.reduce((acc, x) => acc.addSync(x), new Backups());
}
/**
* Create an empty Backups configuration with custom default rsync options.
* Chain `.addVolume()` or `.addSync()` to add sync targets.
* @param options - Partial rsync options to override defaults (e.g. `{ exclude: ['cache'] }`)
*/
static withOptions(options) {
return new Backups({ ...exports.DEFAULT_OPTIONS, ...options });
}
/**
* Configure PostgreSQL dump-based backup for a volume.
*
* Instead of rsyncing the raw PostgreSQL data directory (which is slow and error-prone),
* this uses `pg_dump` to create a logical dump before backup and `pg_restore` to rebuild
* the database after restore.
*
* The dump file is written directly to the backup target — no data duplication on disk.
*
* @returns A configured Backups instance with pre/post hooks. Chain `.addVolume()` or
* `.addSync()` to include additional volumes/paths in the backup.
*/
static withPgDump(config) {
const { imageId, dbVolume, mountpoint, pgdataPath, database, user, password, initdbArgs = [], pgOptions, } = config;
const pgdata = `${mountpoint}${pgdataPath}`;
const dumpFile = `${BACKUP_CONTAINER_MOUNT}/${database}-db.dump`;
function dbMounts() {
return Mounts_1.Mounts.of().mountVolume({
volumeId: dbVolume,
mountpoint: mountpoint,
readonly: false,
subpath: null,
});
}
async function startPg(sub, label) {
await sub.exec(['rm', '-f', `${pgdata}/postmaster.pid`], {
user: 'postgres',
});
await sub.exec(['mkdir', '-p', '/var/run/postgresql'], {
user: 'root',
});
await sub.exec(['chown', 'postgres:postgres', '/var/run/postgresql'], {
user: 'root',
});
console.log(`[${label}] starting postgres`);
const pgStartOpts = pgOptions
? `-c listen_addresses= ${pgOptions}`
: '-c listen_addresses=';
await sub.execFail(['pg_ctl', 'start', '-D', pgdata, '-o', pgStartOpts], {
user: 'postgres',
});
for (let i = 0; i < 60; i++) {
const { exitCode } = await sub.exec(['pg_isready', '-U', user], {
user: 'postgres',
});
if (exitCode === 0) {
console.log(`[${label}] postgres is ready`);
return;
}
await new Promise((r) => setTimeout(r, 1000));
}
throw new Error('PostgreSQL failed to become ready within 60 seconds');
}
return new Backups()
.setPreBackup(async (effects) => {
await SubContainer_1.SubContainerRc.withTemp(effects, { imageId }, dbMounts(), 'pg-dump', async (sub) => {
console.log('[pg-dump] mounting backup target');
await mountBackupTarget(sub.rootfs);
await sub.exec(['touch', dumpFile], { user: 'root' });
await sub.exec(['chown', 'postgres:postgres', dumpFile], {
user: 'root',
});
await startPg(sub, 'pg-dump');
console.log('[pg-dump] dumping database');
await sub.execFail(['pg_dump', '-U', user, '-Fc', '-f', dumpFile, database], { user: 'postgres' }, null);
console.log('[pg-dump] stopping postgres');
await sub.execFail(['pg_ctl', 'stop', '-D', pgdata, '-w'], {
user: 'postgres',
});
console.log('[pg-dump] complete');
});
})
.setPostRestore(async (effects) => {
const resolvedPassword = await resolvePassword(password);
await SubContainer_1.SubContainerRc.withTemp(effects, { imageId }, dbMounts(), 'pg-restore', async (sub) => {
await mountBackupTarget(sub.rootfs);
await sub.execFail(['chown', '-R', 'postgres:postgres', mountpoint], { user: 'root' });
await sub.execFail(['initdb', '-D', pgdata, '-U', user, ...initdbArgs], { user: 'postgres' });
await startPg(sub, 'pg-restore');
await sub.execFail(['createdb', '-U', user, database], {
user: 'postgres',
});
await sub.execFail([
'pg_restore',
'-U',
user,
'-d',
database,
'--no-owner',
'--no-privileges',
dumpFile,
], { user: 'postgres' }, null);
if (resolvedPassword !== null) {
await sub.execFail([
'psql',
'-U',
user,
'-d',
database,
'-c',
`ALTER USER ${user} WITH PASSWORD '${resolvedPassword}'`,
], { user: 'postgres' });
}
await sub.execFail(['pg_ctl', 'stop', '-D', pgdata, '-w'], {
user: 'postgres',
});
});
});
}
/**
* Configure MySQL/MariaDB dump-based backup for a volume.
*
* Instead of rsyncing the raw MySQL data directory (which is slow and error-prone),
* this uses `mysqldump` to create a logical dump before backup and `mysql` to restore
* the database after restore.
*
* The dump file is stored temporarily in `dumpVolume` during backup and cleaned up afterward.
*
* @returns A configured Backups instance with pre/post hooks. Chain `.addVolume()` or
* `.addSync()` to include additional volumes/paths in the backup.
*/
static withMysqlDump(config) {
const { imageId, dbVolume, datadir, database, user, password, engine, readyCommand, mysqldOptions = [], } = config;
const dumpFile = `${BACKUP_CONTAINER_MOUNT}/${database}-db.dump`;
function dbMounts() {
return Mounts_1.Mounts.of().mountVolume({
volumeId: dbVolume,
mountpoint: datadir,
readonly: false,
subpath: null,
});
}
async function waitForMysql(sub, cmd) {
for (let i = 0; i < 30; i++) {
const { exitCode } = await sub.exec(cmd);
if (exitCode === 0)
return;
await new Promise((r) => setTimeout(r, 1000));
}
throw new Error('MySQL/MariaDB failed to become ready within 30 seconds');
}
async function startMysql(sub) {
if (engine === 'mariadb') {
// MariaDB doesn't support --daemonize; fire-and-forget the exec
sub
.exec([
'mysqld',
'--user=mysql',
`--datadir=${datadir}`,
'--bind-address=127.0.0.1',
...mysqldOptions,
], { user: 'root' })
.catch((e) => console.error('[mysql-backup] mysqld exited unexpectedly:', e));
}
else {
await sub.execFail([
'mysqld',
'--user=mysql',
`--datadir=${datadir}`,
'--bind-address=127.0.0.1',
'--daemonize',
...mysqldOptions,
], { user: 'root' }, null);
}
}
return new Backups()
.setPreBackup(async (effects) => {
const pw = await resolvePassword(password);
const readyCmd = readyCommand || [
'mysqladmin',
'ping',
'-u',
user,
...(pw !== null ? [`-p${pw}`] : []),
'--silent',
];
await SubContainer_1.SubContainerRc.withTemp(effects, { imageId }, dbMounts(), 'mysql-dump', async (sub) => {
await mountBackupTarget(sub.rootfs);
await sub.exec(['mkdir', '-p', '/var/run/mysqld'], {
user: 'root',
});
await sub.exec(['chown', 'mysql:mysql', '/var/run/mysqld'], {
user: 'root',
});
if (engine === 'mysql') {
await sub.execFail(['chown', '-R', 'mysql:mysql', datadir], {
user: 'root',
});
}
await startMysql(sub);
await waitForMysql(sub, readyCmd);
await sub.execFail([
'mysqldump',
'-u',
user,
...(pw !== null ? [`-p${pw}`] : []),
'--single-transaction',
`--result-file=${dumpFile}`,
database,
], { user: 'root' }, null);
// Graceful shutdown via SIGTERM; wait for exit
await sub.execFail([
'sh',
'-c',
'PID=$(cat /var/run/mysqld/mysqld.pid) && kill $PID && tail --pid=$PID -f /dev/null',
], { user: 'root' }, null);
});
})
.setPostRestore(async (effects) => {
const pw = await resolvePassword(password);
await SubContainer_1.SubContainerRc.withTemp(effects, { imageId }, dbMounts(), 'mysql-restore', async (sub) => {
await mountBackupTarget(sub.rootfs);
await sub.exec(['mkdir', '-p', '/var/run/mysqld'], {
user: 'root',
});
await sub.exec(['chown', 'mysql:mysql', '/var/run/mysqld'], {
user: 'root',
});
// Initialize fresh data directory
if (engine === 'mariadb') {
await sub.execFail(['mysql_install_db', '--user=mysql', `--datadir=${datadir}`], { user: 'root' });
}
else {
await sub.execFail([
'mysqld',
'--initialize-insecure',
'--user=mysql',
`--datadir=${datadir}`,
], { user: 'root' });
}
await startMysql(sub);
// After fresh init, root has no password
await waitForMysql(sub, [
'mysqladmin',
'ping',
'-u',
'root',
'--silent',
]);
// Create database, user, and set password
const grantSql = pw !== null
? `CREATE DATABASE IF NOT EXISTS \`${database}\`; CREATE USER IF NOT EXISTS '${user}'@'localhost' IDENTIFIED BY '${pw}'; GRANT ALL ON \`${database}\`.* TO '${user}'@'localhost'; ALTER USER 'root'@'localhost' IDENTIFIED BY '${pw}'; FLUSH PRIVILEGES;`
: `CREATE DATABASE IF NOT EXISTS \`${database}\`; CREATE USER IF NOT EXISTS '${user}'@'localhost'; GRANT ALL ON \`${database}\`.* TO '${user}'@'localhost'; FLUSH PRIVILEGES;`;
await sub.execFail(['mysql', '-u', 'root', '-e', grantSql], {
user: 'root',
});
// Restore from dump
await sub.execFail([
'sh',
'-c',
`mysql -u root ${pw !== null ? `-p'${pw}'` : ''} ${database} < ${dumpFile}`,
], { user: 'root' }, null);
// Graceful shutdown via SIGTERM; wait for exit
await sub.execFail([
'sh',
'-c',
'PID=$(cat /var/run/mysqld/mysqld.pid) && kill $PID && tail --pid=$PID -f /dev/null',
], { user: 'root' }, null);
});
});
}
/**
* Override the default rsync options for both backup and restore.
* @param options - Partial rsync options to merge with current defaults
*/
setOptions(options) {
this.options = {
...this.options,
...options,
};
return this;
}
/**
* Override rsync options used only during backup (not restore).
* @param options - Partial rsync options for the backup phase
*/
setBackupOptions(options) {
this.backupOptions = {
...this.backupOptions,
...options,
};
return this;
}
/**
* Override rsync options used only during restore (not backup).
* @param options - Partial rsync options for the restore phase
*/
setRestoreOptions(options) {
this.restoreOptions = {
...this.restoreOptions,
...options,
};
return this;
}
/**
* Register a hook to run before backup rsync begins (e.g. dump a database).
* @param fn - Async function receiving backup-scoped effects
*/
setPreBackup(fn) {
this.preBackup = fn;
return this;
}
/**
* Register a hook to run after backup rsync completes.
* @param fn - Async function receiving backup-scoped effects
*/
setPostBackup(fn) {
this.postBackup = fn;
return this;
}
/**
* Register a hook to run before restore rsync begins.
* @param fn - Async function receiving backup-scoped effects
*/
setPreRestore(fn) {
this.preRestore = fn;
return this;
}
/**
* Register a hook to run after restore rsync completes.
* @param fn - Async function receiving backup-scoped effects
*/
setPostRestore(fn) {
this.postRestore = fn;
return this;
}
/**
* Add a volume to the backup set by its ID.
* @param volume - The volume ID from the manifest
* @param options - Optional per-volume rsync overrides
*/
addVolume(volume, options) {
return this.addSync({
dataPath: `/media/startos/volumes/${volume}/`,
backupPath: `/media/startos/backup/volumes/${volume}/`,
...options,
});
}
/**
* Add a custom sync pair to the backup set.
* @param sync - A `{ dataPath, backupPath }` object with optional per-sync rsync options
*/
addSync(sync) {
this.backupSet.push(sync);
return this;
}
/**
* Execute the backup: runs pre-hook, rsyncs all configured paths, saves the data version, then runs post-hook.
* @param effects - The effects context
*/
async createBackup(effects) {
await this.preBackup(effects);
for (const item of this.backupSet) {
const rsyncResults = await runRsync({
srcPath: item.dataPath,
dstPath: item.backupPath,
options: {
...this.options,
...this.backupOptions,
...item.options,
...item.backupOptions,
},
});
await rsyncResults.wait();
}
const dataVersion = await effects.getDataVersion();
if (dataVersion)
await fs.writeFile('/media/startos/backup/dataVersion.txt', dataVersion, {
encoding: 'utf-8',
});
await this.postBackup(effects);
return;
}
async init(effects, kind) {
if (kind === 'restore') {
await this.restoreBackup(effects);
}
}
/**
* Execute the restore: runs pre-hook, rsyncs all configured paths from backup to data, restores the data version, then runs post-hook.
* @param effects - The effects context
*/
async restoreBackup(effects) {
await this.preRestore(effects);
for (const item of this.backupSet) {
const rsyncResults = await runRsync({
srcPath: item.backupPath,
dstPath: item.dataPath,
options: {
...this.options,
...this.restoreOptions,
...item.options,
...item.restoreOptions,
},
});
await rsyncResults.wait();
}
const dataVersion = await fs
.readFile('/media/startos/backup/dataVersion.txt', {
encoding: 'utf-8',
})
.catch((_) => null);
if (dataVersion)
await effects.setDataVersion({ version: dataVersion });
await this.postRestore(effects);
return;
}
}
exports.Backups = Backups;
async function runRsync(rsyncOptions) {
const { srcPath, dstPath, options } = rsyncOptions;
await fs.mkdir(dstPath, { recursive: true });
const command = 'rsync';
const args = [];
if (options.delete) {
args.push('--delete');
}
for (const exclude of options.exclude) {
args.push(`--exclude=${exclude}`);
}
args.push('-rlptgoAXH');
args.push('--partial');
args.push('--inplace');
args.push('--timeout=300');
args.push('--info=progress2');
args.push('--no-inc-recursive');
args.push(srcPath);
args.push(dstPath);
const spawned = child_process.spawn(command, args, { detached: true });
let percentage = 0.0;
spawned.stdout.on('data', (data) => {
const lines = String(data).replace(/\r/g, '\n').split('\n');
for (const line of lines) {
const parsed = /([0-9.]+)%/.exec(line)?.[1];
if (!parsed) {
if (line)
console.log(line);
continue;
}
percentage = Number.parseFloat(parsed);
}
});
let stderr = '';
spawned.stderr.on('data', (data) => {
const errString = data.toString('utf-8');
stderr += errString;
console.error(`Backups.runAsync`, (0, util_1.asError)(errString));
});
const id = async () => {
const pid = spawned.pid;
if (pid === undefined) {
throw new Error('rsync process has no pid');
}
return String(pid);
};
const waitPromise = new Promise((resolve, reject) => {
spawned.on('exit', (code) => {
if (code === 0) {
resolve(null);
}
else {
reject(new Error(`rsync exited with code ${code}\n${stderr}`));
}
});
});
const wait = () => waitPromise;
const progress = () => Promise.resolve(percentage);
return { id, wait, progress };
}
//# sourceMappingURL=Backups.js.map
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
import './Backups';
import './setupBackups';
+5
View File
@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
require("./Backups");
require("./setupBackups");
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../package/lib/backup/index.ts"],"names":[],"mappings":";;AAAA,qBAAkB;AAClB,0BAAuB"}
@@ -0,0 +1,26 @@
import { Backups } from './Backups';
import * as T from '../../../base/lib/types';
import { InitScript } from '../../../base/lib/inits';
/**
* Parameters for `setupBackups`. Either:
* - An array of volume IDs to back up entirely, or
* - An async factory function that returns a fully configured {@link Backups} instance
*/
export type SetupBackupsParams<M extends T.SDKManifest> = M['volumes'][number][] | ((_: {
effects: T.Effects;
}) => Promise<Backups<M>>);
type SetupBackupsRes = {
createBackup: T.ExpectedExports.createBackup;
restoreInit: InitScript;
};
/**
* Set up backup and restore exports for the service.
*
* Returns `{ createBackup, restoreInit }` which should be exported and wired into
* the service's init and backup entry points.
*
* @param options - Either an array of volume IDs or an async factory returning a Backups instance
* @returns An object with `createBackup` (the backup export) and `restoreInit` (an InitScript for restore)
*/
export declare function setupBackups<M extends T.SDKManifest>(options: SetupBackupsParams<M>): SetupBackupsRes;
export {};
+38
View File
@@ -0,0 +1,38 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupBackups = setupBackups;
const Backups_1 = require("./Backups");
/**
* Set up backup and restore exports for the service.
*
* Returns `{ createBackup, restoreInit }` which should be exported and wired into
* the service's init and backup entry points.
*
* @param options - Either an array of volume IDs or an async factory returning a Backups instance
* @returns An object with `createBackup` (the backup export) and `restoreInit` (an InitScript for restore)
*/
function setupBackups(options) {
let backupsFactory;
if (options instanceof Function) {
backupsFactory = options;
}
else {
backupsFactory = async () => Backups_1.Backups.ofVolumes(...options);
}
const answer = {
get createBackup() {
return (async (options) => {
return (await backupsFactory(options)).createBackup(options.effects);
});
},
get restoreInit() {
return {
init: async (effects, kind) => {
return (await backupsFactory({ effects })).init(effects, kind);
},
};
},
};
return answer;
}
//# sourceMappingURL=setupBackups.js.map
@@ -0,0 +1 @@
{"version":3,"file":"setupBackups.js","sourceRoot":"","sources":["../../../../package/lib/backup/setupBackups.ts"],"names":[],"mappings":";;AA4BA,oCAwBC;AApDD,uCAAmC;AAmBnC;;;;;;;;GAQG;AACH,SAAgB,YAAY,CAC1B,OAA8B;IAE9B,IAAI,cAAkE,CAAA;IACtE,IAAI,OAAO,YAAY,QAAQ,EAAE,CAAC;QAChC,cAAc,GAAG,OAAO,CAAA;IAC1B,CAAC;SAAM,CAAC;QACN,cAAc,GAAG,KAAK,IAAI,EAAE,CAAC,iBAAO,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC,CAAA;IAC5D,CAAC;IACD,MAAM,MAAM,GAAoB;QAC9B,IAAI,YAAY;YACd,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;gBACxB,OAAO,CAAC,MAAM,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;YACtE,CAAC,CAAmC,CAAA;QACtC,CAAC;QACD,IAAI,WAAW;YACb,OAAO;gBACL,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;oBAC5B,OAAO,CAAC,MAAM,cAAc,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;gBAChE,CAAC;aACF,CAAA;QACH,CAAC;KACF,CAAA;IACD,OAAO,MAAM,CAAA;AACf,CAAC"}
+40
View File
@@ -0,0 +1,40 @@
import { Effects, HealthCheckId } from '../../../base/lib/types';
import { HealthCheckResult } from './checkFns/HealthCheckResult';
import { Trigger } from '../trigger';
import { Drop } from '../util';
/** Parameters for creating a health check */
export type HealthCheckParams = {
id: HealthCheckId;
name: string;
trigger?: Trigger;
gracePeriod?: number;
fn(): Promise<HealthCheckResult> | HealthCheckResult;
};
/**
* A periodic health check that reports daemon readiness to the StartOS UI.
*
* Polls at an interval controlled by a {@link Trigger}, reporting results as
* "starting" (during the grace period), "success", or "failure". Automatically
* pauses when the daemon is stopped and resumes when restarted.
*/
export declare class HealthCheck extends Drop {
private started;
private setStarted;
private exited;
private exit;
private currentValue;
private promise;
private constructor();
/**
* Create a new HealthCheck instance and begin its polling loop.
* @param effects - The effects context for reporting health status
* @param options - Health check configuration (ID, name, check function, trigger, grace period)
* @returns A new HealthCheck instance
*/
static of(effects: Effects, options: HealthCheckParams): HealthCheck;
/** Signal that the daemon is running, enabling health check polling */
start(): void;
/** Signal that the daemon has stopped, pausing health check polling */
stop(): void;
onDrop(): void;
}
+122
View File
@@ -0,0 +1,122 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HealthCheck = void 0;
const defaultTrigger_1 = require("../trigger/defaultTrigger");
const util_1 = require("../util");
/**
* A periodic health check that reports daemon readiness to the StartOS UI.
*
* Polls at an interval controlled by a {@link Trigger}, reporting results as
* "starting" (during the grace period), "success", or "failure". Automatically
* pauses when the daemon is stopped and resumes when restarted.
*/
class HealthCheck extends util_1.Drop {
constructor(effects, o) {
super();
this.started = null;
this.setStarted = (started) => {
this.started = started;
};
this.exited = false;
this.exit = () => {
this.exited = true;
};
this.currentValue = {};
this.promise = Promise.resolve().then(async () => {
const getCurrentValue = () => this.currentValue;
const gracePeriod = o.gracePeriod ?? 10_000;
const trigger = (o.trigger ?? defaultTrigger_1.defaultTrigger)(getCurrentValue);
const checkStarted = () => [
this.started,
new Promise((resolve) => {
this.setStarted = (started) => {
this.started = started;
resolve();
};
this.exit = () => {
this.exited = true;
resolve();
};
}),
];
let triggered = false;
while (!this.exited) {
const [started, changed] = checkStarted();
let race = [
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;
}
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;
}
}
});
}
/**
* Create a new HealthCheck instance and begin its polling loop.
* @param effects - The effects context for reporting health status
* @param options - Health check configuration (ID, name, check function, trigger, grace period)
* @returns A new HealthCheck instance
*/
static of(effects, options) {
return new HealthCheck(effects, options);
}
/** Signal that the daemon is running, enabling health check polling */
start() {
if (this.started)
return;
this.setStarted(performance.now());
}
/** Signal that the daemon has stopped, pausing health check polling */
stop() {
if (!this.started)
return;
this.setStarted(null);
}
onDrop() {
this.exit();
}
}
exports.HealthCheck = HealthCheck;
function asMessage(e) {
if (typeof e === 'object' && e !== null && 'message' in e)
return String(e.message);
const value = String(e);
if (value.length == null)
return null;
return value;
}
//# sourceMappingURL=HealthCheck.js.map
@@ -0,0 +1 @@
{"version":3,"file":"HealthCheck.js","sourceRoot":"","sources":["../../../../package/lib/health/HealthCheck.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
import { T } from '../../../../base/lib';
/**
* The result of a single health check invocation.
*
* Contains a `result` field ("success", "failure", or "starting") and an optional `message`.
* This is the unnamed variant -- the health check name is added by the framework.
*/
export type HealthCheckResult = Omit<T.NamedHealthCheckResult, 'name'>;
@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=HealthCheckResult.js.map
@@ -0,0 +1 @@
{"version":3,"file":"HealthCheckResult.js","sourceRoot":"","sources":["../../../../../package/lib/health/checkFns/HealthCheckResult.ts"],"names":[],"mappings":""}
@@ -0,0 +1,13 @@
import { Effects } from '../../../../base/lib/types';
import { HealthCheckResult } from './HealthCheckResult';
export declare function containsAddress(x: string, port: number, address?: bigint): boolean;
/**
* This is used to check if a port is listening on the system.
* Used during the health check fn or the check main fn.
*/
export declare function checkPortListening(effects: Effects, port: number, options: {
errorMessage: string;
successMessage: string;
timeoutMessage?: string;
timeout?: number;
}): Promise<HealthCheckResult>;
@@ -0,0 +1,79 @@
"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.containsAddress = containsAddress;
exports.checkPortListening = checkPortListening;
const util_1 = require("../../util");
const node_util_1 = require("node:util");
const CP = __importStar(require("node:child_process"));
const cpExec = (0, node_util_1.promisify)(CP.exec);
function containsAddress(x, port, address) {
const readPorts = x
.split('\n')
.filter(Boolean)
.splice(1)
.map((x) => x.split(' ').filter(Boolean)[1]?.split(':'))
.filter((x) => x?.length > 1)
.map(([addr, p]) => [BigInt(`0x${addr}`), Number.parseInt(p, 16)]);
return !!readPorts.find(([addr, p]) => (address === undefined || address === addr) && port === p);
}
/**
* This is used to check if a port is listening on the system.
* Used during the health check fn or the check main fn.
*/
async function checkPortListening(effects, port, options) {
return Promise.race([
Promise.resolve().then(async () => {
const hasAddress = containsAddress(await cpExec(`cat /proc/net/tcp`, {}).then(util_1.stringFromStdErrOut), port) ||
containsAddress(await cpExec(`cat /proc/net/tcp6`, {}).then(util_1.stringFromStdErrOut), port, BigInt(0)) ||
containsAddress(await cpExec('cat /proc/net/udp', {}).then(util_1.stringFromStdErrOut), port) ||
containsAddress(await cpExec('cat /proc/net/udp6', {}).then(util_1.stringFromStdErrOut), port, BigInt(0));
if (hasAddress) {
return { result: 'success', message: options.successMessage };
}
return {
result: 'failure',
message: options.errorMessage,
};
}),
new Promise((resolve) => {
setTimeout(() => resolve({
result: 'failure',
message: options.timeoutMessage || `Timeout trying to check port ${port}`,
}), options.timeout ?? 1_000);
}),
]);
}
//# sourceMappingURL=checkPortListening.js.map
@@ -0,0 +1 @@
{"version":3,"file":"checkPortListening.js","sourceRoot":"","sources":["../../../../../package/lib/health/checkFns/checkPortListening.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAQA,0CAWC;AAMD,gDAmDC;AA3ED,qCAAgD;AAEhD,yCAAqC;AACrC,uDAAwC;AAExC,MAAM,MAAM,GAAG,IAAA,qBAAS,EAAC,EAAE,CAAC,IAAI,CAAC,CAAA;AAEjC,SAAgB,eAAe,CAAC,CAAS,EAAE,IAAY,EAAE,OAAgB;IACvE,MAAM,SAAS,GAAG,CAAC;SAChB,KAAK,CAAC,IAAI,CAAC;SACX,MAAM,CAAC,OAAO,CAAC;SACf,MAAM,CAAC,CAAC,CAAC;SACT,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;SACvD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC;SAC5B,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAU,CAAC,CAAA;IAC7E,OAAO,CAAC,CAAC,SAAS,CAAC,IAAI,CACrB,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC,CACzE,CAAA;AACH,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,kBAAkB,CACtC,OAAgB,EAChB,IAAY,EACZ,OAKC;IAED,OAAO,OAAO,CAAC,IAAI,CAAoB;QACrC,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;YAChC,MAAM,UAAU,GACd,eAAe,CACb,MAAM,MAAM,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,0BAAmB,CAAC,EAC/D,IAAI,CACL;gBACD,eAAe,CACb,MAAM,MAAM,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,0BAAmB,CAAC,EAChE,IAAI,EACJ,MAAM,CAAC,CAAC,CAAC,CACV;gBACD,eAAe,CACb,MAAM,MAAM,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,0BAAmB,CAAC,EAC/D,IAAI,CACL;gBACD,eAAe,CACb,MAAM,MAAM,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,0BAAmB,CAAC,EAChE,IAAI,EACJ,MAAM,CAAC,CAAC,CAAC,CACV,CAAA;YACH,IAAI,UAAU,EAAE,CAAC;gBACf,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,cAAc,EAAE,CAAA;YAC/D,CAAC;YACD,OAAO;gBACL,MAAM,EAAE,SAAS;gBACjB,OAAO,EAAE,OAAO,CAAC,YAAY;aAC9B,CAAA;QACH,CAAC,CAAC;QACF,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YACtB,UAAU,CACR,GAAG,EAAE,CACH,OAAO,CAAC;gBACN,MAAM,EAAE,SAAS;gBACjB,OAAO,EACL,OAAO,CAAC,cAAc,IAAI,gCAAgC,IAAI,EAAE;aACnE,CAAC,EACJ,OAAO,CAAC,OAAO,IAAI,KAAK,CACzB,CAAA;QACH,CAAC,CAAC;KACH,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,14 @@
import { Effects } from '../../../../base/lib/types';
import { HealthCheckResult } from './HealthCheckResult';
import 'isomorphic-fetch';
/**
* This is a helper function to check if a web url is reachable.
* @param url
* @param createSuccess
* @returns
*/
export declare const checkWebUrl: (effects: Effects, url: string, { timeout, successMessage, errorMessage, }?: {
timeout?: number | undefined;
successMessage?: string | undefined;
errorMessage?: string | undefined;
}) => Promise<HealthCheckResult>;
@@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkWebUrl = void 0;
const util_1 = require("../../util");
const index_1 = require("./index");
require("isomorphic-fetch");
/**
* This is a helper function to check if a web url is reachable.
* @param url
* @param createSuccess
* @returns
*/
const checkWebUrl = async (effects, url, { timeout = 1000, successMessage = `Reached ${url}`, errorMessage = `Error while fetching URL: ${url}`, } = {}) => {
return Promise.race([fetch(url), (0, index_1.timeoutPromise)(timeout)])
.then((x) => ({
result: 'success',
message: successMessage,
}))
.catch((e) => {
console.warn(`Error while fetching URL: ${url}`);
console.error(JSON.stringify(e));
console.error((0, util_1.asError)(e));
return { result: 'failure', message: errorMessage };
});
};
exports.checkWebUrl = checkWebUrl;
//# sourceMappingURL=checkWebUrl.js.map
@@ -0,0 +1 @@
{"version":3,"file":"checkWebUrl.js","sourceRoot":"","sources":["../../../../../package/lib/health/checkFns/checkWebUrl.ts"],"names":[],"mappings":";;;AACA,qCAAoC;AAEpC,mCAAwC;AACxC,4BAAyB;AAEzB;;;;;GAKG;AACI,MAAM,WAAW,GAAG,KAAK,EAC9B,OAAgB,EAChB,GAAW,EACX,EACE,OAAO,GAAG,IAAI,EACd,cAAc,GAAG,WAAW,GAAG,EAAE,EACjC,YAAY,GAAG,6BAA6B,GAAG,EAAE,GAClD,GAAG,EAAE,EACsB,EAAE;IAC9B,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,IAAA,sBAAc,EAAC,OAAO,CAAC,CAAC,CAAC;SACvD,IAAI,CACH,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC;QACC,MAAM,EAAE,SAAS;QACjB,OAAO,EAAE,cAAc;KACxB,CAAU,CACd;SACA,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;QACX,OAAO,CAAC,IAAI,CAAC,6BAA6B,GAAG,EAAE,CAAC,CAAA;QAChD,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;QAChC,OAAO,CAAC,KAAK,CAAC,IAAA,cAAO,EAAC,CAAC,CAAC,CAAC,CAAA;QACzB,OAAO,EAAE,MAAM,EAAE,SAAkB,EAAE,OAAO,EAAE,YAAY,EAAE,CAAA;IAC9D,CAAC,CAAC,CAAA;AACN,CAAC,CAAA;AAvBY,QAAA,WAAW,eAuBvB"}
@@ -0,0 +1,16 @@
import { runHealthScript } from './runHealthScript';
export { checkPortListening } from './checkPortListening';
export { HealthCheckResult } from './HealthCheckResult';
export { checkWebUrl } from './checkWebUrl';
/**
* Create a promise that rejects after the specified timeout.
* Useful for racing against long-running health checks.
*
* @param ms - Timeout duration in milliseconds
* @param options.message - Custom error message (defaults to "Timed out")
* @returns A promise that never resolves, only rejects after the timeout
*/
export declare function timeoutPromise(ms: number, { message }?: {
message?: string | undefined;
}): Promise<never>;
export { runHealthScript };
@@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.runHealthScript = exports.checkWebUrl = exports.checkPortListening = void 0;
exports.timeoutPromise = timeoutPromise;
const runHealthScript_1 = require("./runHealthScript");
Object.defineProperty(exports, "runHealthScript", { enumerable: true, get: function () { return runHealthScript_1.runHealthScript; } });
var checkPortListening_1 = require("./checkPortListening");
Object.defineProperty(exports, "checkPortListening", { enumerable: true, get: function () { return checkPortListening_1.checkPortListening; } });
var checkWebUrl_1 = require("./checkWebUrl");
Object.defineProperty(exports, "checkWebUrl", { enumerable: true, get: function () { return checkWebUrl_1.checkWebUrl; } });
/**
* Create a promise that rejects after the specified timeout.
* Useful for racing against long-running health checks.
*
* @param ms - Timeout duration in milliseconds
* @param options.message - Custom error message (defaults to "Timed out")
* @returns A promise that never resolves, only rejects after the timeout
*/
function timeoutPromise(ms, { message = 'Timed out' } = {}) {
return new Promise((resolve, reject) => setTimeout(() => reject(new Error(message)), ms));
}
//# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../package/lib/health/checkFns/index.ts"],"names":[],"mappings":";;;AAaA,wCAIC;AAjBD,uDAAmD;AAkB1C,gGAlBA,iCAAe,OAkBA;AAjBxB,2DAAyD;AAAhD,wHAAA,kBAAkB,OAAA;AAE3B,6CAA2C;AAAlC,0GAAA,WAAW,OAAA;AAEpB;;;;;;;GAOG;AACH,SAAgB,cAAc,CAAC,EAAU,EAAE,EAAE,OAAO,GAAG,WAAW,EAAE,GAAG,EAAE;IACvE,OAAO,IAAI,OAAO,CAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAC5C,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,CACjD,CAAA;AACH,CAAC"}
@@ -0,0 +1,15 @@
import { HealthCheckResult } from './HealthCheckResult';
import { SubContainer } from '../../util/SubContainer';
import { SDKManifest } from '../../types';
/**
* Running a health script, is used when we want to have a simple
* script in bash or something like that. It should return something that is useful
* in {result: string} else it is considered an error
* @param param0
* @returns
*/
export declare const runHealthScript: <Manifest extends SDKManifest>(runCommand: string[], subcontainer: SubContainer<Manifest>, { timeout, errorMessage, message, }?: {
timeout?: number | undefined;
errorMessage?: string | undefined;
message?: ((res: string) => string) | undefined;
}) => Promise<HealthCheckResult>;
@@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.runHealthScript = void 0;
const index_1 = require("./index");
/**
* Running a health script, is used when we want to have a simple
* script in bash or something like that. It should return something that is useful
* in {result: string} else it is considered an error
* @param param0
* @returns
*/
const runHealthScript = async (runCommand, subcontainer, { timeout = 30000, errorMessage = `Error while running command: ${runCommand}`, message = (res) => `Have ran script ${runCommand} and the result: ${res}`, } = {}) => {
const res = await Promise.race([
subcontainer.execFail(runCommand),
(0, index_1.timeoutPromise)(timeout),
]).catch((e) => {
console.warn(errorMessage);
console.warn(JSON.stringify(e));
console.warn(e.toString());
throw { result: 'failure', message: errorMessage };
});
return {
result: 'success',
message: message(res.stdout.toString()),
};
};
exports.runHealthScript = runHealthScript;
//# sourceMappingURL=runHealthScript.js.map
@@ -0,0 +1 @@
{"version":3,"file":"runHealthScript.js","sourceRoot":"","sources":["../../../../../package/lib/health/checkFns/runHealthScript.ts"],"names":[],"mappings":";;;AACA,mCAAwC;AAIxC;;;;;;GAMG;AACI,MAAM,eAAe,GAAG,KAAK,EAClC,UAAoB,EACpB,YAAoC,EACpC,EACE,OAAO,GAAG,KAAK,EACf,YAAY,GAAG,gCAAgC,UAAU,EAAE,EAC3D,OAAO,GAAG,CAAC,GAAW,EAAE,EAAE,CACxB,mBAAmB,UAAU,oBAAoB,GAAG,EAAE,MACtD,EAAE,EACsB,EAAE;IAC9B,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;QAC7B,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC;QACjC,IAAA,sBAAc,EAAC,OAAO,CAAC;KACxB,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;QACb,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;QAC1B,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;QAC/B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAA;QAC1B,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,EAAuB,CAAA;IACzE,CAAC,CAAC,CAAA;IACF,OAAO;QACL,MAAM,EAAE,SAAS;QACjB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;KACnB,CAAA;AACxB,CAAC,CAAA;AAvBY,QAAA,eAAe,mBAuB3B"}
+2
View File
@@ -0,0 +1,2 @@
import './checkFns';
export { HealthCheck } from './HealthCheck';
+7
View File
@@ -0,0 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HealthCheck = void 0;
require("./checkFns");
var HealthCheck_1 = require("./HealthCheck");
Object.defineProperty(exports, "HealthCheck", { enumerable: true, get: function () { return HealthCheck_1.HealthCheck; } });
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../package/lib/health/index.ts"],"names":[],"mappings":";;;AAAA,sBAAmB;AAEnB,6CAA2C;AAAlC,0GAAA,WAAW,OAAA"}
+24
View File
@@ -0,0 +1,24 @@
/**
* Internationalization (i18n) utilities for StartOS packages.
*
* @example
* ```typescript
* // In package's i18n/index.ts:
* import { setupI18n } from '@start9labs/start-sdk'
* import defaultDict, { DEFAULT_LANG } from './dictionaries/default'
* import translations from './dictionaries/translations'
*
* export const i18n = setupI18n(defaultDict, translations, DEFAULT_LANG)
* ```
*/
type ParamValue = string | number | Date;
/**
* Creates a typed i18n function for a package.
*
* @param defaultDict - The default language dictionary mapping strings to numeric indices
* @param translations - Translation dictionaries for each supported locale
* @param defaultLang - The default language code (e.g., 'en_US')
* @returns A typed i18n function that accepts dictionary keys and optional parameters
*/
export declare function setupI18n<Dict extends Record<string, number>, Translations extends Record<string, Record<number, string>>>(defaultDict: Dict, translations: Translations, defaultLang: string): (key: keyof Dict, params?: Record<string, ParamValue>) => string;
export {};
+59
View File
@@ -0,0 +1,59 @@
"use strict";
/**
* Internationalization (i18n) utilities for StartOS packages.
*
* @example
* ```typescript
* // In package's i18n/index.ts:
* import { setupI18n } from '@start9labs/start-sdk'
* import defaultDict, { DEFAULT_LANG } from './dictionaries/default'
* import translations from './dictionaries/translations'
*
* export const i18n = setupI18n(defaultDict, translations, DEFAULT_LANG)
* ```
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupI18n = setupI18n;
/**
* Creates a typed i18n function for a package.
*
* @param defaultDict - The default language dictionary mapping strings to numeric indices
* @param translations - Translation dictionaries for each supported locale
* @param defaultLang - The default language code (e.g., 'en_US')
* @returns A typed i18n function that accepts dictionary keys and optional parameters
*/
function setupI18n(defaultDict, translations, defaultLang) {
const lang = process.env.LANG?.replace(/\.UTF-8$/, '') || defaultLang;
// Convert locale format from en_US to en-US for Intl APIs
const intlLocale = lang.replace('_', '-');
function getTranslation() {
if (lang === defaultLang)
return null;
const availableLangs = Object.keys(translations);
const match = availableLangs.find((l) => l === lang) ??
availableLangs.find((l) => String(l).startsWith(lang.split('_')[0] + '_'));
return match ? translations[match] : null;
}
const translation = getTranslation();
function formatValue(value) {
if (typeof value === 'number') {
return new Intl.NumberFormat(intlLocale).format(value);
}
if (value instanceof Date) {
return new Intl.DateTimeFormat(intlLocale).format(value);
}
return value;
}
return function i18n(key, params) {
let result = translation
? translation[defaultDict[key]]
: key;
if (params) {
for (const [paramName, value] of Object.entries(params)) {
result = result.replace(`\${${paramName}}`, formatValue(value));
}
}
return result;
};
}
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../package/lib/i18n/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;AAYH,8BAiDC;AAzDD;;;;;;;GAOG;AACH,SAAgB,SAAS,CAGvB,WAAiB,EAAE,YAA0B,EAAE,WAAmB;IAClE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,WAAW,CAAA;IAErE,0DAA0D;IAC1D,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IAEzC,SAAS,cAAc;QACrB,IAAI,IAAI,KAAK,WAAW;YAAE,OAAO,IAAI,CAAA;QAErC,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAA2B,CAAA;QAE1E,MAAM,KAAK,GACT,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;YACtC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAA;QAE5E,OAAO,KAAK,CAAC,CAAC,CAAE,YAAY,CAAC,KAAK,CAA4B,CAAC,CAAC,CAAC,IAAI,CAAA;IACvE,CAAC;IAED,MAAM,WAAW,GAAG,cAAc,EAAE,CAAA;IAEpC,SAAS,WAAW,CAAC,KAAiB;QACpC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,OAAO,IAAI,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACxD,CAAC;QACD,IAAI,KAAK,YAAY,IAAI,EAAE,CAAC;YAC1B,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAC1D,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IAED,OAAO,SAAS,IAAI,CAClB,GAAe,EACf,MAAmC;QAEnC,IAAI,MAAM,GAAG,WAAW;YACtB,CAAC,CAAC,WAAW,CAAC,WAAW,CAAC,GAAa,CAAC,CAAC;YACzC,CAAC,CAAE,GAAc,CAAA;QAEnB,IAAI,MAAM,EAAE,CAAC;YACX,KAAK,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;gBACxD,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,SAAS,GAAG,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAA;YACjE,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAA;IACf,CAAC,CAAA;AACH,CAAC"}
+22
View File
@@ -0,0 +1,22 @@
import { S9pk, Version, VersionRange, ExtendedVersion, inputSpec, ISB, IST, types, z, utils } from '../../base/lib';
export { S9pk, Version, VersionRange, ExtendedVersion, inputSpec, ISB, IST, types, z, utils, };
export { setupI18n } from './i18n';
export * as T from './types';
export { Daemons } from './mainFn/Daemons';
export { SubContainer } from './util/SubContainer';
export { StartSdk } from './StartSdk';
export { setupManifest, buildManifest } from './manifest/setupManifest';
export { FileHelper } from './util/fileHelper';
export { smtpShape, smtpPrefill, type SmtpSelection, } from '../../base/lib/actions/input/inputSpecConstants';
export * as actions from '../../base/lib/actions';
export * as backup from './backup';
export * as daemons from './mainFn/Daemons';
export * as health from './health';
export * as healthFns from './health/checkFns';
export * as mainFn from './mainFn';
export * as toml from '@iarna/toml';
export * as yaml from 'yaml';
export * as startSdk from './StartSdk';
export * as YAML from 'yaml';
export * as TOML from '@iarna/toml';
export * from './version';
+78
View File
@@ -0,0 +1,78 @@
"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;
};
})();
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TOML = exports.YAML = exports.startSdk = exports.yaml = exports.toml = exports.mainFn = exports.healthFns = exports.health = exports.daemons = exports.backup = exports.actions = exports.smtpPrefill = exports.smtpShape = exports.FileHelper = exports.buildManifest = exports.setupManifest = exports.StartSdk = exports.Daemons = exports.T = exports.setupI18n = exports.utils = exports.z = exports.types = exports.IST = exports.ISB = exports.inputSpec = exports.ExtendedVersion = exports.VersionRange = exports.Version = exports.S9pk = void 0;
const lib_1 = require("../../base/lib");
Object.defineProperty(exports, "S9pk", { enumerable: true, get: function () { return lib_1.S9pk; } });
Object.defineProperty(exports, "Version", { enumerable: true, get: function () { return lib_1.Version; } });
Object.defineProperty(exports, "VersionRange", { enumerable: true, get: function () { return lib_1.VersionRange; } });
Object.defineProperty(exports, "ExtendedVersion", { enumerable: true, get: function () { return lib_1.ExtendedVersion; } });
Object.defineProperty(exports, "inputSpec", { enumerable: true, get: function () { return lib_1.inputSpec; } });
Object.defineProperty(exports, "ISB", { enumerable: true, get: function () { return lib_1.ISB; } });
Object.defineProperty(exports, "IST", { enumerable: true, get: function () { return lib_1.IST; } });
Object.defineProperty(exports, "types", { enumerable: true, get: function () { return lib_1.types; } });
Object.defineProperty(exports, "z", { enumerable: true, get: function () { return lib_1.z; } });
Object.defineProperty(exports, "utils", { enumerable: true, get: function () { return lib_1.utils; } });
var i18n_1 = require("./i18n");
Object.defineProperty(exports, "setupI18n", { enumerable: true, get: function () { return i18n_1.setupI18n; } });
exports.T = __importStar(require("./types"));
var Daemons_1 = require("./mainFn/Daemons");
Object.defineProperty(exports, "Daemons", { enumerable: true, get: function () { return Daemons_1.Daemons; } });
var StartSdk_1 = require("./StartSdk");
Object.defineProperty(exports, "StartSdk", { enumerable: true, get: function () { return StartSdk_1.StartSdk; } });
var setupManifest_1 = require("./manifest/setupManifest");
Object.defineProperty(exports, "setupManifest", { enumerable: true, get: function () { return setupManifest_1.setupManifest; } });
Object.defineProperty(exports, "buildManifest", { enumerable: true, get: function () { return setupManifest_1.buildManifest; } });
var fileHelper_1 = require("./util/fileHelper");
Object.defineProperty(exports, "FileHelper", { enumerable: true, get: function () { return fileHelper_1.FileHelper; } });
var inputSpecConstants_1 = require("../../base/lib/actions/input/inputSpecConstants");
Object.defineProperty(exports, "smtpShape", { enumerable: true, get: function () { return inputSpecConstants_1.smtpShape; } });
Object.defineProperty(exports, "smtpPrefill", { enumerable: true, get: function () { return inputSpecConstants_1.smtpPrefill; } });
exports.actions = __importStar(require("../../base/lib/actions"));
exports.backup = __importStar(require("./backup"));
exports.daemons = __importStar(require("./mainFn/Daemons"));
exports.health = __importStar(require("./health"));
exports.healthFns = __importStar(require("./health/checkFns"));
exports.mainFn = __importStar(require("./mainFn"));
exports.toml = __importStar(require("@iarna/toml"));
exports.yaml = __importStar(require("yaml"));
exports.startSdk = __importStar(require("./StartSdk"));
exports.YAML = __importStar(require("yaml"));
exports.TOML = __importStar(require("@iarna/toml"));
__exportStar(require("./version"), exports);
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../package/lib/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wCAWuB;AAGrB,qFAbA,UAAI,OAaA;AACJ,wFAbA,aAAO,OAaA;AACP,6FAbA,kBAAY,OAaA;AACZ,gGAbA,qBAAe,OAaA;AACf,0FAbA,eAAS,OAaA;AACT,oFAbA,SAAG,OAaA;AACH,oFAbA,SAAG,OAaA;AACH,sFAbA,WAAK,OAaA;AACL,kFAbA,OAAC,OAaA;AACD,sFAbA,WAAK,OAaA;AAEP,+BAAkC;AAAzB,iGAAA,SAAS,OAAA;AAClB,6CAA4B;AAC5B,4CAA0C;AAAjC,kGAAA,OAAO,OAAA;AAEhB,uCAAqC;AAA5B,oGAAA,QAAQ,OAAA;AACjB,0DAAuE;AAA9D,8GAAA,aAAa,OAAA;AAAE,8GAAA,aAAa,OAAA;AACrC,gDAA8C;AAArC,wGAAA,UAAU,OAAA;AACnB,sFAIwD;AAHtD,+GAAA,SAAS,OAAA;AACT,iHAAA,WAAW,OAAA;AAIb,kEAAiD;AACjD,mDAAkC;AAClC,4DAA2C;AAC3C,mDAAkC;AAClC,+DAA8C;AAC9C,mDAAkC;AAClC,oDAAmC;AACnC,6CAA4B;AAC5B,uDAAsC;AACtC,6CAA4B;AAC5B,oDAAmC;AACnC,4CAAyB"}
@@ -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":";;;AACA,4DAAwD;AACxD,sEAAkE;AAClE,kCAA8B;AAE9B,2DAAuD;AAIvD,MAAM,oBAAoB,GAAG,IAAI,CAAA;AACjC,MAAM,cAAc,GAAG,KAAK,CAAA;AAC5B;;;;;;;;;GASG;AACH,MAAa,MAGX,SAAQ,WAAI;IAMZ,YACU,YAAe,EACf,YAA2D,EAC1D,UAAmB,KAAK;QAEjC,KAAK,EAAE,CAAA;QAJC,iBAAY,GAAZ,YAAY,CAAG;QACf,iBAAY,GAAZ,YAAY,CAA+C;QAC1D,YAAO,GAAP,OAAO,CAAiB;QAR3B,sBAAiB,GAA0C,IAAI,CAAA;QAC7D,kBAAa,GAAG,KAAK,CAAA;QACvB,cAAS,GAAmC,EAAE,CAAA;QAC9C,SAAI,GAA2D,IAAI,CAAA;QACnE,aAAQ,GAAG,KAAK,CAAA;IAOxB,CAAC;IACD,8EAA8E;IAC9E,SAAS;QACP,OAAO,IAAI,CAAC,OAAO,CAAA;IACrB,CAAC;IACD;;;;;;OAMG;IACH,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,MAAM,GAAG,GAAG,IAAI,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;YAC1C,OAAO,CAAC,cAAc,CAAC,GAAG,EAAE;gBAC1B,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;oBAClB,GAAG,CAAC,IAAI,CAAC,EAAE,mBAAmB,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAA,2BAAY,EAAC,CAAC,CAAC,CAAC,CAAA;gBACvE,CAAC;YACH,CAAC,CAAC,CAAA;YACF,OAAO,GAAG,CAAA;QACZ,CAAC,CAAA;IACH,CAAC;IACD;;;;;OAKG;IACH,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,OAAM;QACR,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,eAAe,EAAE,CAAA;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QACvC,IAAI,CAAC,IAAI,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;IAC7B,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,MAAmB;QACvC,IAAI,cAAc,GAAG,CAAC,CAAA;QACtB,IAAI,CAAC;YACH,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACvB,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;oBAC3B,MAAM,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,2BAAY,CAAC,CAAA;oBACzD,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAA;gBAC/B,CAAC;gBACD,IAAI,CAAC;oBACH,IAAI,CAAC,iBAAiB,GAAG,MAAM,IAAI,CAAC,YAAY,EAAE,CAAA;oBAClD,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;wBACnB,MAAM,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,2BAAY,CAAC,CAAA;wBACzD,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAA;wBAC7B,MAAK;oBACP,CAAC;oBACD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,IAAI,CACtD,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,EACX,CAAC,GAAG,EAAE,EAAE;wBACN,IAAI,CAAC,MAAM,CAAC,OAAO;4BAAE,IAAA,2BAAY,EAAC,GAAG,CAAC,CAAA;wBACtC,OAAO,KAAK,CAAA;oBACd,CAAC,CACF,CAAA;oBACD,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAA;oBAC7B,IAAI,MAAM,CAAC,OAAO;wBAAE,MAAK;oBACzB,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;wBAChC,IAAI,CAAC;4BACH,EAAE,CAAC,OAAO,CAAC,CAAA;wBACb,CAAC;wBAAC,OAAO,CAAC,EAAE,CAAC;4BACX,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC,CAAC,CAAA;wBAClC,CAAC;oBACH,CAAC;oBACD,IAAI,OAAO,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;wBAC5B,IAAI,CAAC,aAAa,GAAG,IAAI,CAAA;wBACzB,MAAK;oBACP,CAAC;gBACH,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACX,IAAI,CAAC,MAAM,CAAC,OAAO;wBAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;gBACvC,CAAC;gBACD,IAAI,MAAM,CAAC,OAAO;oBAAE,MAAK;gBACzB,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;oBAClC,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,EAAE,cAAc,CAAC,CAAA;oBACjD,MAAM,CAAC,gBAAgB,CACrB,OAAO,EACP,GAAG,EAAE;wBACH,YAAY,CAAC,KAAK,CAAC,CAAA;wBACnB,OAAO,EAAE,CAAA;oBACX,CAAC,EACD,EAAE,IAAI,EAAE,IAAI,EAAE,CACf,CAAA;gBACH,CAAC,CAAC,CAAA;gBACF,cAAc,IAAI,oBAAoB,CAAA;gBACtC,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,cAAc,CAAC,CAAA;YAC3D,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAClB,CAAC;IACH,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,IAAI,CAAC,WAIV;QACC,IAAI,CAAC,aAAa,GAAG,KAAK,CAAA;QAC1B,IAAI,CAAC,SAAS,GAAG,EAAE,CAAA;QAEnB,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;QACzB,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,iBAAiB,EAAE,IAAI,CAAC,EAAE,GAAG,WAAW,EAAE,CAAC,CAAA;QAChE,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAA;QAC7B,IAAI,OAAO;YAAE,MAAM,OAAO,CAAC,KAAK,CAAC,2BAAY,CAAC,CAAA;QAE9C,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAA;QACtB,CAAC;QAED,IAAI,WAAW,EAAE,mBAAmB,EAAE,CAAC;YACrC,MAAM,IAAI,CAAC,YAAY,EAAE,OAAO,EAAE,CAAA;QACpC,CAAC;IACH,CAAC;IACD;;;;OAIG;IACH,WAAW;QACT,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAA;IACtB,CAAC;IACD,4FAA4F;IAC5F,cAAc;QACZ,OAAO,IAAI,CAAC,YAAY,EAAE,EAAE,EAAE,IAAI,IAAI,CAAA;IACxC,CAAC;IACD,+EAA+E;IAC/E,sBAAsB,CACpB,KAAsD;QAEtD,OAAO,IAAI,CAAC,YAAY,EAAE,IAAI,KAAK,KAAK,CAAC,YAAY,EAAE,IAAI,CAAA;IAC7D,CAAC;IACD;;;OAGG;IACH,MAAM,CAAC,EAA8B;QACnC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACzB,CAAC;IACD,MAAM;QACJ,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAA,2BAAY,EAAC,IAAA,iBAAO,EAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACpD,CAAC;CACF;AA1LD,wBA0LC"}
+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"}
@@ -0,0 +1,28 @@
import * as T from '../../../base/lib/types';
import { ImageId, VolumeId } from '../../../base/lib/types';
import { SDKManifest, SDKImageInputSpec } from '../../../base/lib/types/ManifestTypes';
import { VersionGraph } from '../version/VersionGraph';
/**
* @description Use this function to define critical information about your package
*
* @param manifest Static properties of the package
*/
export declare function setupManifest<Id extends string, VolumesTypes extends VolumeId, Manifest extends {
id: Id;
volumes: VolumesTypes[];
} & SDKManifest>(manifest: Manifest & SDKManifest): Manifest;
/**
* Build the final publishable manifest by combining the SDK manifest definition
* with version graph metadata, OS version, SDK version, and computed fields
* (migration ranges, hardware requirements, alerts, etc.).
*
* @param versions - The service's VersionGraph, used to extract the current version, release notes, and migration ranges
* @param manifest - The SDK manifest definition (from `setupManifest`)
* @returns A fully resolved Manifest ready for packaging
*/
export declare function buildManifest<Id extends string, Version extends string, Dependencies extends Record<string, unknown>, VolumesTypes extends VolumeId, ImagesTypes extends ImageId, Manifest extends {
dependencies: Dependencies;
id: Id;
images: Record<ImagesTypes, SDKImageInputSpec>;
volumes: VolumesTypes[];
}>(versions: VersionGraph<Version>, manifest: SDKManifest & Manifest): Manifest & T.Manifest;
@@ -0,0 +1,71 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupManifest = setupManifest;
exports.buildManifest = buildManifest;
const StartSdk_1 = require("../StartSdk");
const package_json_1 = require("../../package.json");
/**
* @description Use this function to define critical information about your package
*
* @param manifest Static properties of the package
*/
function setupManifest(manifest) {
return manifest;
}
/**
* Build the final publishable manifest by combining the SDK manifest definition
* with version graph metadata, OS version, SDK version, and computed fields
* (migration ranges, hardware requirements, alerts, etc.).
*
* @param versions - The service's VersionGraph, used to extract the current version, release notes, and migration ranges
* @param manifest - The SDK manifest definition (from `setupManifest`)
* @returns A fully resolved Manifest ready for packaging
*/
function buildManifest(versions, manifest) {
const images = Object.entries(manifest.images).reduce((images, [k, v]) => {
v.arch = v.arch ?? ['aarch64', 'x86_64', 'riscv64'];
if (v.emulateMissingAs === undefined)
v.emulateMissingAs = v.arch.includes('x86_64')
? 'x86_64'
: (v.arch[0] ?? null);
v.nvidiaContainer = !!v.nvidiaContainer;
images[k] = v;
return images;
}, {});
return {
...manifest,
gitHash: null,
osVersion: manifest.osVersion ?? StartSdk_1.OSVersion,
sdkVersion: package_json_1.version,
version: versions.current.options.version,
releaseNotes: versions.current.options.releaseNotes,
satisfies: versions.current.options.satisfies || [],
canMigrateTo: versions.canMigrateTo().toString(),
canMigrateFrom: versions.canMigrateFrom().toString(),
images,
alerts: {
install: manifest.alerts?.install || null,
update: manifest.alerts?.update || null,
uninstall: manifest.alerts?.uninstall || null,
restore: manifest.alerts?.restore || null,
start: manifest.alerts?.start || null,
stop: manifest.alerts?.stop || null,
},
hardwareRequirements: {
device: manifest.hardwareRequirements?.device || [],
ram: manifest.hardwareRequirements?.ram || null,
arch: Object.values(images).reduce((arch, inputSpec) => {
if (inputSpec.emulateMissingAs) {
return arch;
}
if (arch === null) {
return inputSpec.arch;
}
return arch.filter((a) => inputSpec.arch.includes(a));
}, null),
},
hardwareAcceleration: manifest.hardwareAcceleration ?? false,
plugins: manifest.plugins ?? [],
};
}
//# sourceMappingURL=setupManifest.js.map
@@ -0,0 +1 @@
{"version":3,"file":"setupManifest.js","sourceRoot":"","sources":["../../../../package/lib/manifest/setupManifest.ts"],"names":[],"mappings":"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"}
File diff suppressed because it is too large Load Diff
+47
View File
@@ -0,0 +1,47 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sdk = void 0;
const StartSdk_1 = require("../StartSdk");
const setupManifest_1 = require("../manifest/setupManifest");
exports.sdk = StartSdk_1.StartSdk.of()
.withManifest((0, setupManifest_1.setupManifest)({
id: 'testOutput',
title: '',
license: '',
packageRepo: '',
upstreamRepo: '',
marketingUrl: '',
donationUrl: null,
docsUrls: [],
description: {
short: '',
long: '',
},
images: {
main: {
source: {
dockerTag: 'start9/hello-world',
},
arch: ['aarch64', 'x86_64'],
emulateMissingAs: 'aarch64',
},
},
volumes: [],
alerts: {
install: null,
update: null,
uninstall: null,
restore: null,
start: null,
stop: null,
},
dependencies: {
'remote-test': {
description: '',
optional: false,
s9pk: 'https://example.com/remote-test.s9pk',
},
},
}))
.build(true);
//# sourceMappingURL=output.sdk.js.map
@@ -0,0 +1 @@
{"version":3,"file":"output.sdk.js","sourceRoot":"","sources":["../../../../package/lib/test/output.sdk.ts"],"names":[],"mappings":";;;AAAA,0CAAsC;AACtC,6DAAyD;AAI5C,QAAA,GAAG,GAAG,mBAAQ,CAAC,EAAE,EAAE;KAC7B,YAAY,CACX,IAAA,6BAAa,EAAC;IACZ,EAAE,EAAE,YAAY;IAChB,KAAK,EAAE,EAAE;IACT,OAAO,EAAE,EAAE;IACX,WAAW,EAAE,EAAE;IACf,YAAY,EAAE,EAAE;IAChB,YAAY,EAAE,EAAE;IAChB,WAAW,EAAE,IAAI;IACjB,QAAQ,EAAE,EAAE;IACZ,WAAW,EAAE;QACX,KAAK,EAAE,EAAE;QACT,IAAI,EAAE,EAAE;KACT;IACD,MAAM,EAAE;QACN,IAAI,EAAE;YACJ,MAAM,EAAE;gBACN,SAAS,EAAE,oBAAoB;aAChC;YACD,IAAI,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC;YAC3B,gBAAgB,EAAE,SAAS;SAC5B;KACF;IACD,OAAO,EAAE,EAAE;IACX,MAAM,EAAE;QACN,OAAO,EAAE,IAAI;QACb,MAAM,EAAE,IAAI;QACZ,SAAS,EAAE,IAAI;QACf,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,IAAI;QACX,IAAI,EAAE,IAAI;KACX;IACD,YAAY,EAAE;QACZ,aAAa,EAAE;YACb,WAAW,EAAE,EAAE;YACf,QAAQ,EAAE,KAAK;YACf,IAAI,EAAE,sCAAsC;SAC7C;KACF;CACF,CAAC,CACH;KACA,KAAK,CAAC,IAAI,CAAC,CAAA"}
@@ -0,0 +1,4 @@
import { HealthStatus } from '../../../base/lib/types';
export type TriggerInput = {
lastResult?: HealthStatus;
};
@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=TriggerInput.js.map
@@ -0,0 +1 @@
{"version":3,"file":"TriggerInput.js","sourceRoot":"","sources":["../../../../package/lib/trigger/TriggerInput.ts"],"names":[],"mappings":""}
@@ -0,0 +1,5 @@
import { Trigger } from './index';
export declare function changeOnFirstSuccess(o: {
beforeFirstSuccess: Trigger;
afterFirstSuccess: Trigger;
}): Trigger;
@@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.changeOnFirstSuccess = changeOnFirstSuccess;
function changeOnFirstSuccess(o) {
return async function* (getInput) {
let currentValue = getInput();
while (!currentValue.lastResult) {
yield;
currentValue = getInput();
}
const beforeFirstSuccess = o.beforeFirstSuccess(getInput);
for (let res = await beforeFirstSuccess.next(); currentValue?.lastResult !== 'success' && !res.done; res = await beforeFirstSuccess.next()) {
yield;
currentValue = getInput();
}
const afterFirstSuccess = o.afterFirstSuccess(getInput);
for (let res = await afterFirstSuccess.next(); !res.done; res = await afterFirstSuccess.next()) {
yield;
currentValue = getInput();
}
};
}
//# sourceMappingURL=changeOnFirstSuccess.js.map
@@ -0,0 +1 @@
{"version":3,"file":"changeOnFirstSuccess.js","sourceRoot":"","sources":["../../../../package/lib/trigger/changeOnFirstSuccess.ts"],"names":[],"mappings":";;AAEA,oDA6BC;AA7BD,SAAgB,oBAAoB,CAAC,CAGpC;IACC,OAAO,KAAK,SAAS,CAAC,EAAE,QAAQ;QAC9B,IAAI,YAAY,GAAG,QAAQ,EAAE,CAAA;QAC7B,OAAO,CAAC,YAAY,CAAC,UAAU,EAAE,CAAC;YAChC,KAAK,CAAA;YACL,YAAY,GAAG,QAAQ,EAAE,CAAA;QAC3B,CAAC;QACD,MAAM,kBAAkB,GAAG,CAAC,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAA;QACzD,KACE,IAAI,GAAG,GAAG,MAAM,kBAAkB,CAAC,IAAI,EAAE,EACzC,YAAY,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC,GAAG,CAAC,IAAI,EACnD,GAAG,GAAG,MAAM,kBAAkB,CAAC,IAAI,EAAE,EACrC,CAAC;YACD,KAAK,CAAA;YACL,YAAY,GAAG,QAAQ,EAAE,CAAA;QAC3B,CAAC;QACD,MAAM,iBAAiB,GAAG,CAAC,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAA;QACvD,KACE,IAAI,GAAG,GAAG,MAAM,iBAAiB,CAAC,IAAI,EAAE,EACxC,CAAC,GAAG,CAAC,IAAI,EACT,GAAG,GAAG,MAAM,iBAAiB,CAAC,IAAI,EAAE,EACpC,CAAC;YACD,KAAK,CAAA;YACL,YAAY,GAAG,QAAQ,EAAE,CAAA;QAC3B,CAAC;IACH,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1 @@
export declare function cooldownTrigger(timeMs: number): () => AsyncGenerator<undefined, never, unknown>;
@@ -0,0 +1,12 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.cooldownTrigger = cooldownTrigger;
function cooldownTrigger(timeMs) {
return async function* () {
while (true) {
await new Promise((resolve) => setTimeout(resolve, timeMs));
yield;
}
};
}
//# sourceMappingURL=cooldownTrigger.js.map
@@ -0,0 +1 @@
{"version":3,"file":"cooldownTrigger.js","sourceRoot":"","sources":["../../../../package/lib/trigger/cooldownTrigger.ts"],"names":[],"mappings":";;AAAA,0CAOC;AAPD,SAAgB,eAAe,CAAC,MAAc;IAC5C,OAAO,KAAK,SAAS,CAAC;QACpB,OAAO,IAAI,EAAE,CAAC;YACZ,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAA;YAC3D,KAAK,CAAA;QACP,CAAC;IACH,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1 @@
export declare const defaultTrigger: import(".").Trigger;
@@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultTrigger = void 0;
const cooldownTrigger_1 = require("./cooldownTrigger");
const changeOnFirstSuccess_1 = require("./changeOnFirstSuccess");
exports.defaultTrigger = (0, changeOnFirstSuccess_1.changeOnFirstSuccess)({
beforeFirstSuccess: (0, cooldownTrigger_1.cooldownTrigger)(1000),
afterFirstSuccess: (0, cooldownTrigger_1.cooldownTrigger)(30000),
});
//# sourceMappingURL=defaultTrigger.js.map
@@ -0,0 +1 @@
{"version":3,"file":"defaultTrigger.js","sourceRoot":"","sources":["../../../../package/lib/trigger/defaultTrigger.ts"],"names":[],"mappings":";;;AAAA,uDAAmD;AACnD,iEAA6D;AAEhD,QAAA,cAAc,GAAG,IAAA,2CAAoB,EAAC;IACjD,kBAAkB,EAAE,IAAA,iCAAe,EAAC,IAAI,CAAC;IACzC,iBAAiB,EAAE,IAAA,iCAAe,EAAC,KAAK,CAAC;CAC1C,CAAC,CAAA"}
+4
View File
@@ -0,0 +1,4 @@
import { TriggerInput } from './TriggerInput';
export { changeOnFirstSuccess } from './changeOnFirstSuccess';
export { cooldownTrigger } from './cooldownTrigger';
export type Trigger = (getInput: () => TriggerInput) => AsyncIterator<unknown, unknown, never>;
+8
View File
@@ -0,0 +1,8 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.cooldownTrigger = exports.changeOnFirstSuccess = void 0;
var changeOnFirstSuccess_1 = require("./changeOnFirstSuccess");
Object.defineProperty(exports, "changeOnFirstSuccess", { enumerable: true, get: function () { return changeOnFirstSuccess_1.changeOnFirstSuccess; } });
var cooldownTrigger_1 = require("./cooldownTrigger");
Object.defineProperty(exports, "cooldownTrigger", { enumerable: true, get: function () { return cooldownTrigger_1.cooldownTrigger; } });
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../package/lib/trigger/index.ts"],"names":[],"mappings":";;;AACA,+DAA6D;AAApD,4HAAA,oBAAoB,OAAA;AAC7B,qDAAmD;AAA1C,kHAAA,eAAe,OAAA"}
@@ -0,0 +1,8 @@
import { Trigger } from '.';
import { HealthStatus } from '../../../base/lib/types';
export type LastStatusTriggerParams = {
[k in HealthStatus]?: Trigger;
} & {
default: Trigger;
};
export declare function lastStatus(o: LastStatusTriggerParams): Trigger;
+28
View File
@@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.lastStatus = lastStatus;
function lastStatus(o) {
return async function* (getInput) {
let trigger = o.default(getInput);
const triggers = {
default: trigger,
};
while (true) {
let currentValue = getInput();
let prev = currentValue.lastResult;
if (!prev) {
yield;
continue;
}
if (!(prev in o)) {
prev = 'default';
}
if (!triggers[prev]) {
triggers[prev] = o[prev](getInput);
}
await triggers[prev]?.next();
yield;
}
};
}
//# sourceMappingURL=lastStatus.js.map
@@ -0,0 +1 @@
{"version":3,"file":"lastStatus.js","sourceRoot":"","sources":["../../../../package/lib/trigger/lastStatus.ts"],"names":[],"mappings":";;AAOA,gCAyBC;AAzBD,SAAgB,UAAU,CAAC,CAA0B;IACnD,OAAO,KAAK,SAAS,CAAC,EAAE,QAAQ;QAC9B,IAAI,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;QACjC,MAAM,QAAQ,GAE4C;YACxD,OAAO,EAAE,OAAO;SACjB,CAAA;QACD,OAAO,IAAI,EAAE,CAAC;YACZ,IAAI,YAAY,GAAG,QAAQ,EAAE,CAAA;YAC7B,IAAI,IAAI,GAAyC,YAAY,CAAC,UAAU,CAAA;YACxE,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,KAAK,CAAA;gBACL,SAAQ;YACV,CAAC;YACD,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,CAAC;gBACjB,IAAI,GAAG,SAAS,CAAA;YAClB,CAAC;YACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;gBACpB,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAE,CAAC,QAAQ,CAAC,CAAA;YACrC,CAAC;YACD,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAA;YAC5B,KAAK,CAAA;QACP,CAAC;IACH,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,5 @@
import { Trigger } from '.';
export declare const successFailure: (o: {
duringSuccess: Trigger;
duringError: Trigger;
}) => Trigger;
@@ -0,0 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.successFailure = void 0;
const lastStatus_1 = require("./lastStatus");
const successFailure = (o) => (0, lastStatus_1.lastStatus)({ success: o.duringSuccess, default: o.duringError });
exports.successFailure = successFailure;
//# sourceMappingURL=successFailure.js.map
@@ -0,0 +1 @@
{"version":3,"file":"successFailure.js","sourceRoot":"","sources":["../../../../package/lib/trigger/successFailure.ts"],"names":[],"mappings":";;;AACA,6CAAyC;AAElC,MAAM,cAAc,GAAG,CAAC,CAG9B,EAAE,EAAE,CAAC,IAAA,uBAAU,EAAC,EAAE,OAAO,EAAE,CAAC,CAAC,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAA;AAHzD,QAAA,cAAc,kBAG2C"}
+2
View File
@@ -0,0 +1,2 @@
export * from '../../base/lib/types';
export { HealthCheck } from './health';
+21
View File
@@ -0,0 +1,21 @@
"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 __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HealthCheck = void 0;
__exportStar(require("../../base/lib/types"), exports);
var health_1 = require("./health");
Object.defineProperty(exports, "HealthCheck", { enumerable: true, get: function () { return health_1.HealthCheck; } });
//# sourceMappingURL=types.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../package/lib/types.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,uDAAoC;AACpC,mCAAsC;AAA7B,qGAAA,WAAW,OAAA"}
@@ -0,0 +1,36 @@
import { Effects } from '../../../base/lib/Effects';
import { Manifest, PackageId } from '../../../base/lib/osBindings';
export declare class GetServiceManifest<Mapped = Manifest> {
readonly effects: Effects;
readonly packageId: PackageId;
readonly map: (manifest: Manifest | null) => Mapped;
readonly eq: (a: Mapped, b: Mapped) => boolean;
constructor(effects: Effects, packageId: PackageId, map: (manifest: Manifest | null) => Mapped, eq: (a: Mapped, b: Mapped) => boolean);
/**
* Returns the manifest of a service. Reruns the context from which it has been called if the underlying value changes
*/
const(): Promise<Mapped>;
/**
* Returns the manifest of a service. Does nothing if it changes
*/
once(): Promise<Mapped>;
private watchGen;
/**
* Watches the manifest of a service. Returns an async iterator that yields whenever the value changes
*/
watch(abort?: AbortSignal): AsyncGenerator<Mapped, never, unknown>;
/**
* Watches the manifest of a service. Takes a custom callback function to run whenever it changes
*/
onChange(callback: (value: Mapped | null, error?: Error) => {
cancel: boolean;
} | Promise<{
cancel: boolean;
}>): void;
/**
* Watches the manifest of a service. Returns when the predicate is true
*/
waitFor(pred: (value: Mapped) => boolean): Promise<Mapped>;
}
export declare function getServiceManifest(effects: Effects, packageId: PackageId): GetServiceManifest<Manifest>;
export declare function getServiceManifest<Mapped>(effects: Effects, packageId: PackageId, map: (manifest: Manifest | null) => Mapped, eq?: (a: Mapped, b: Mapped) => boolean): GetServiceManifest<Mapped>;
@@ -0,0 +1,117 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GetServiceManifest = void 0;
exports.getServiceManifest = getServiceManifest;
const AbortedError_1 = require("../../../base/lib/util/AbortedError");
const Drop_1 = require("../../../base/lib/util/Drop");
const deepEqual_1 = require("../../../base/lib/util/deepEqual");
class GetServiceManifest {
constructor(effects, packageId, map, eq) {
this.effects = effects;
this.packageId = packageId;
this.map = map;
this.eq = eq;
}
/**
* Returns the manifest of a service. Reruns the context from which it has been called if the underlying value changes
*/
async const() {
let abort = new AbortController();
const watch = this.watch(abort.signal);
const res = await watch.next();
if (this.effects.constRetry) {
watch
.next()
.then(() => {
abort.abort();
this.effects.constRetry && this.effects.constRetry();
})
.catch();
}
return res.value;
}
/**
* Returns the manifest of a service. Does nothing if it changes
*/
async once() {
const manifest = await this.effects.getServiceManifest({
packageId: this.packageId,
});
return this.map(manifest);
}
async *watchGen(abort) {
let prev = null;
const resolveCell = { resolve: () => { } };
this.effects.onLeaveContext(() => {
resolveCell.resolve();
});
abort?.addEventListener('abort', () => resolveCell.resolve());
while (this.effects.isInContext && !abort?.aborted) {
let callback = () => { };
const waitForNext = new Promise((resolve) => {
callback = resolve;
resolveCell.resolve = resolve;
});
const next = this.map(await this.effects.getServiceManifest({
packageId: this.packageId,
callback: () => callback(),
}));
if (!prev || !this.eq(prev.value, next)) {
prev = { value: next };
yield next;
}
await waitForNext;
}
return new Promise((_, rej) => rej(new AbortedError_1.AbortedError()));
}
/**
* Watches the manifest of a service. Returns an async iterator that yields whenever the value changes
*/
watch(abort) {
const ctrl = new AbortController();
abort?.addEventListener('abort', () => ctrl.abort());
return Drop_1.DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort());
}
/**
* Watches the manifest of a service. Takes a custom callback function to run whenever it changes
*/
onChange(callback) {
;
(async () => {
const ctrl = new AbortController();
for await (const value of this.watch(ctrl.signal)) {
try {
const res = await callback(value);
if (res.cancel) {
ctrl.abort();
break;
}
}
catch (e) {
console.error('callback function threw an error @ GetServiceManifest.onChange', e);
}
}
})()
.catch((e) => callback(null, e))
.catch((e) => console.error('callback function threw an error @ GetServiceManifest.onChange', e));
}
/**
* Watches the manifest of a service. Returns when the predicate is true
*/
waitFor(pred) {
const ctrl = new AbortController();
return Drop_1.DropPromise.of(Promise.resolve().then(async () => {
for await (const next of this.watchGen(ctrl.signal)) {
if (pred(next)) {
return next;
}
}
throw new Error('context left before predicate passed');
}), () => ctrl.abort());
}
}
exports.GetServiceManifest = GetServiceManifest;
function getServiceManifest(effects, packageId, map, eq) {
return new GetServiceManifest(effects, packageId, map ?? ((a) => a), eq ?? ((a, b) => (0, deepEqual_1.deepEqual)(a, b)));
}
//# sourceMappingURL=GetServiceManifest.js.map
@@ -0,0 +1 @@
{"version":3,"file":"GetServiceManifest.js","sourceRoot":"","sources":["../../../../package/lib/util/GetServiceManifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
import { T } from '..';
import { Effects } from '../../../base/lib/Effects';
export declare class GetSslCertificate {
readonly effects: Effects;
readonly hostnames: string[];
readonly algorithm?: T.Algorithm | undefined;
constructor(effects: Effects, hostnames: string[], algorithm?: T.Algorithm | undefined);
/**
* Returns the an SSL Certificate for the given hostnames if permitted. Restarts the service if it changes
*/
const(): Promise<[string, string, string]>;
/**
* Returns the an SSL Certificate for the given hostnames if permitted. Does nothing if it changes
*/
once(): Promise<[string, string, string]>;
private watchGen;
/**
* Watches the SSL Certificate for the given hostnames if permitted. Returns an async iterator that yields whenever the value changes
*/
watch(abort?: AbortSignal): AsyncGenerator<[string, string, string], never, unknown>;
/**
* Watches the SSL Certificate for the given hostnames if permitted. Takes a custom callback function to run whenever it changes
*/
onChange(callback: (value: [string, string, string] | null, error?: Error) => {
cancel: boolean;
} | Promise<{
cancel: boolean;
}>): void;
/**
* Watches the SSL Certificate for the given hostnames if permitted. Returns when the predicate is true
*/
waitFor(pred: (value: [string, string, string] | null) => boolean): Promise<[string, string, string] | null>;
}
@@ -0,0 +1,100 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GetSslCertificate = void 0;
const AbortedError_1 = require("../../../base/lib/util/AbortedError");
const Drop_1 = require("../../../base/lib/util/Drop");
class GetSslCertificate {
constructor(effects, hostnames, algorithm) {
this.effects = effects;
this.hostnames = hostnames;
this.algorithm = algorithm;
}
/**
* Returns the an SSL Certificate for the given hostnames if permitted. Restarts the service if it changes
*/
const() {
return this.effects.getSslCertificate({
hostnames: this.hostnames,
algorithm: this.algorithm,
callback: this.effects.constRetry &&
(() => this.effects.constRetry && this.effects.constRetry()),
});
}
/**
* Returns the an SSL Certificate for the given hostnames if permitted. Does nothing if it changes
*/
once() {
return this.effects.getSslCertificate({
hostnames: this.hostnames,
algorithm: this.algorithm,
});
}
async *watchGen(abort) {
const resolveCell = { resolve: () => { } };
this.effects.onLeaveContext(() => {
resolveCell.resolve();
});
abort?.addEventListener('abort', () => resolveCell.resolve());
while (this.effects.isInContext && !abort?.aborted) {
let callback = () => { };
const waitForNext = new Promise((resolve) => {
callback = resolve;
resolveCell.resolve = resolve;
});
yield await this.effects.getSslCertificate({
hostnames: this.hostnames,
algorithm: this.algorithm,
callback: () => callback(),
});
await waitForNext;
}
return new Promise((_, rej) => rej(new AbortedError_1.AbortedError()));
}
/**
* Watches the SSL Certificate for the given hostnames if permitted. Returns an async iterator that yields whenever the value changes
*/
watch(abort) {
const ctrl = new AbortController();
abort?.addEventListener('abort', () => ctrl.abort());
return Drop_1.DropGenerator.of(this.watchGen(ctrl.signal), () => ctrl.abort());
}
/**
* Watches the SSL Certificate for the given hostnames if permitted. Takes a custom callback function to run whenever it changes
*/
onChange(callback) {
;
(async () => {
const ctrl = new AbortController();
for await (const value of this.watch(ctrl.signal)) {
try {
const res = await callback(value);
if (res.cancel) {
ctrl.abort();
break;
}
}
catch (e) {
console.error('callback function threw an error @ GetSslCertificate.onChange', e);
}
}
})()
.catch((e) => callback(null, e))
.catch((e) => console.error('callback function threw an error @ GetSslCertificate.onChange', e));
}
/**
* Watches the SSL Certificate for the given hostnames if permitted. Returns when the predicate is true
*/
waitFor(pred) {
const ctrl = new AbortController();
return Drop_1.DropPromise.of(Promise.resolve().then(async () => {
for await (const next of this.watchGen(ctrl.signal)) {
if (pred(next)) {
return next;
}
}
return null;
}), () => ctrl.abort());
}
}
exports.GetSslCertificate = GetSslCertificate;
//# sourceMappingURL=GetSslCertificate.js.map
@@ -0,0 +1 @@
{"version":3,"file":"GetSslCertificate.js","sourceRoot":"","sources":["../../../../package/lib/util/GetSslCertificate.ts"],"names":[],"mappings":"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"}
+340
View File
@@ -0,0 +1,340 @@
import * as fs from 'fs/promises';
import * as T from '../../../base/lib/types';
import * as cp from 'child_process';
import { Buffer } from 'node:buffer';
import { Drop } from '../../../base/lib/util/Drop';
import { Mounts } from '../mainFn/Mounts';
import { BackupEffects } from '../backup/Backups';
import { PathBase } from './Volume';
export declare const execFile: typeof cp.execFile.__promisify__;
export type ExecOptions = {
input?: string | Buffer;
};
/**
* Interface representing an isolated container environment for running service processes.
*
* Provides methods for executing commands, spawning processes, mounting filesystems,
* and writing files within the container's rootfs. Comes in two flavors:
* {@link SubContainerOwned} (owns the underlying filesystem) and
* {@link SubContainerRc} (reference-counted handle to a shared container).
*/
export interface SubContainer<Manifest extends T.SDKManifest, Effects extends T.Effects = T.Effects> extends Drop, PathBase {
readonly imageId: keyof Manifest['images'] & T.ImageId;
readonly rootfs: string;
readonly guid: T.Guid;
/**
* Get the absolute path to a file or directory within this subcontainer's rootfs
* @param path Path relative to the rootfs
*/
subpath(path: string): string;
/**
* Apply filesystem mounts (volumes, assets, dependencies, backups) to this subcontainer.
* @param mounts - The Mounts configuration to apply
* @returns This subcontainer instance for chaining
*/
mount(mounts: Effects extends BackupEffects ? Mounts<Manifest, {
subpath: string | null;
mountpoint: string;
}> : Mounts<Manifest, never>): Promise<this>;
/** Destroy this subcontainer and clean up its filesystem */
destroy: () => Promise<null>;
/**
* @description run a command inside this subcontainer
* DOES NOT THROW ON NONZERO EXIT CODE (see execFail)
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
exec(command: string[], options?: CommandOptions & ExecOptions, timeoutMs?: number | null, abort?: AbortController): Promise<{
throw: () => {
stdout: string | Buffer;
stderr: string | Buffer;
};
exitCode: number | null;
exitSignal: NodeJS.Signals | null;
stdout: string | Buffer;
stderr: string | Buffer;
}>;
/**
* @description run a command inside this subcontainer, throwing on non-zero exit status
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
execFail(command: string[], options?: CommandOptions & ExecOptions, timeoutMs?: number | null, abort?: AbortController): Promise<{
stdout: string | Buffer;
stderr: string | Buffer;
}>;
/**
* Launch a command as the init (PID 1) process of the subcontainer.
* Replaces the current leader process.
* @param command - The command and arguments to execute
* @param options - Optional environment, working directory, and user overrides
*/
launch(command: string[], options?: CommandOptions): Promise<cp.ChildProcessWithoutNullStreams>;
/**
* Spawn a command inside the subcontainer as a non-init process.
* @param command - The command and arguments to execute
* @param options - Optional environment, working directory, user, and stdio overrides
*/
spawn(command: string[], options?: CommandOptions & StdioOptions): Promise<cp.ChildProcess>;
/**
* @description Write a file to the subcontainer's filesystem
* @param path Path relative to the subcontainer rootfs (e.g. "/etc/config.json")
* @param data The data to write
* @param options Optional write options (same as node:fs/promises writeFile)
*/
writeFile(path: string, data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView>, options?: Parameters<typeof fs.writeFile>[2]): Promise<void>;
/**
* Create a reference-counted handle to this subcontainer.
* The underlying container is only destroyed when all handles are released.
*/
rc(): SubContainerRc<Manifest, Effects>;
/** Returns true if this is an owned subcontainer (not a reference-counted handle) */
isOwned(): this is SubContainerOwned<Manifest, Effects>;
}
/**
* Want to limit what we can do in a container, so we want to launch a container with a specific image and the mounts.
*/
export declare class SubContainerOwned<Manifest extends T.SDKManifest, Effects extends T.Effects = T.Effects> extends Drop implements SubContainer<Manifest, Effects> {
readonly effects: Effects;
readonly imageId: keyof Manifest['images'] & T.ImageId;
readonly rootfs: string;
readonly guid: T.Guid;
private destroyed;
rcs: number;
private leader;
private leaderExited;
private waitProc;
private constructor();
static of<Manifest extends T.SDKManifest, Effects extends T.Effects>(effects: Effects, image: {
imageId: keyof Manifest['images'] & T.ImageId;
sharedRun?: boolean;
}, mounts: (Effects extends BackupEffects ? Mounts<Manifest, {
subpath: string | null;
mountpoint: string;
}> : Mounts<Manifest, never>) | null, name: string): Promise<SubContainerOwned<Manifest, Effects>>;
static withTemp<Manifest extends T.SDKManifest, T, Effects extends T.Effects>(effects: Effects, image: {
imageId: keyof Manifest['images'] & T.ImageId;
sharedRun?: boolean;
}, mounts: (Effects extends BackupEffects ? Mounts<Manifest, {
subpath: string | null;
mountpoint: string;
}> : Mounts<Manifest, never>) | null, name: string, fn: (subContainer: SubContainer<Manifest, Effects>) => Promise<T>): Promise<T>;
subpath(path: string): string;
mount(mounts: Effects extends BackupEffects ? Mounts<Manifest, {
subpath: string | null;
mountpoint: string;
}> : Mounts<Manifest, never>): Promise<this>;
private killLeader;
get destroy(): () => Promise<null>;
onDrop(): void;
/**
* @description run a command inside this subcontainer
* DOES NOT THROW ON NONZERO EXIT CODE (see execFail)
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
exec(command: string[], options?: CommandOptions & ExecOptions, timeoutMs?: number | null, abort?: AbortController): Promise<{
throw: () => {
stdout: string | Buffer;
stderr: string | Buffer;
};
exitCode: number | null;
exitSignal: NodeJS.Signals | null;
stdout: string | Buffer;
stderr: string | Buffer;
}>;
/**
* @description run a command inside this subcontainer, throwing on non-zero exit status
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
execFail(command: string[], options?: CommandOptions & ExecOptions, timeoutMs?: number | null, abort?: AbortController): Promise<{
stdout: string | Buffer;
stderr: string | Buffer;
}>;
launch(command: string[], options?: CommandOptions): Promise<cp.ChildProcessWithoutNullStreams>;
spawn(command: string[], options?: CommandOptions & StdioOptions): Promise<cp.ChildProcess>;
/**
* @description Write a file to the subcontainer's filesystem
* @param path Path relative to the subcontainer rootfs (e.g. "/etc/config.json")
* @param data The data to write
* @param options Optional write options (same as node:fs/promises writeFile)
*/
writeFile(path: string, data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView>, options?: Parameters<typeof fs.writeFile>[2]): Promise<void>;
rc(): SubContainerRc<Manifest, Effects>;
isOwned(): this is SubContainerOwned<Manifest, Effects>;
}
/**
* A reference-counted handle to a {@link SubContainerOwned}.
*
* Multiple `SubContainerRc` instances can share one underlying subcontainer.
* The subcontainer is destroyed only when the last reference is released via `destroy()`.
*/
export declare class SubContainerRc<Manifest extends T.SDKManifest, Effects extends T.Effects = T.Effects> extends Drop implements SubContainer<Manifest, Effects> {
private readonly subcontainer;
get imageId(): keyof Manifest["images"] & string;
get rootfs(): string;
get guid(): string;
subpath(path: string): string;
private destroyed;
private destroying;
constructor(subcontainer: SubContainerOwned<Manifest, Effects>);
static of<Manifest extends T.SDKManifest, Effects extends T.Effects>(effects: Effects, image: {
imageId: keyof Manifest['images'] & T.ImageId;
sharedRun?: boolean;
}, mounts: (Effects extends BackupEffects ? Mounts<Manifest, {
subpath: string | null;
mountpoint: string;
}> : Mounts<Manifest, never>) | null, name: string): Promise<SubContainerRc<Manifest, Effects>>;
static withTemp<Manifest extends T.SDKManifest, T, Effects extends T.Effects>(effects: Effects, image: {
imageId: keyof Manifest['images'] & T.ImageId;
sharedRun?: boolean;
}, mounts: (Effects extends BackupEffects ? Mounts<Manifest, {
subpath: string | null;
mountpoint: string;
}> : Mounts<Manifest, never>) | null, name: string, fn: (subContainer: SubContainer<Manifest, Effects>) => Promise<T>): Promise<T>;
mount(mounts: Effects extends BackupEffects ? Mounts<Manifest, {
subpath: string | null;
mountpoint: string;
}> : Mounts<Manifest, never>): Promise<this>;
get destroy(): () => Promise<null>;
onDrop(): void;
/**
* @description run a command inside this subcontainer
* DOES NOT THROW ON NONZERO EXIT CODE (see execFail)
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
exec(command: string[], options?: CommandOptions & ExecOptions, timeoutMs?: number | null, abort?: AbortController): Promise<{
throw: () => {
stdout: string | Buffer;
stderr: string | Buffer;
};
exitCode: number | null;
exitSignal: NodeJS.Signals | null;
stdout: string | Buffer;
stderr: string | Buffer;
}>;
/**
* @description run a command inside this subcontainer, throwing on non-zero exit status
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
execFail(command: string[], options?: CommandOptions & ExecOptions, timeoutMs?: number | null, abort?: AbortController): Promise<{
stdout: string | Buffer;
stderr: string | Buffer;
}>;
launch(command: string[], options?: CommandOptions): Promise<cp.ChildProcessWithoutNullStreams>;
spawn(command: string[], options?: CommandOptions & StdioOptions): Promise<cp.ChildProcess>;
/**
* @description Write a file to the subcontainer's filesystem
* @param path Path relative to the subcontainer rootfs (e.g. "/etc/config.json")
* @param data The data to write
* @param options Optional write options (same as node:fs/promises writeFile)
*/
writeFile(path: string, data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView>, options?: Parameters<typeof fs.writeFile>[2]): Promise<void>;
rc(): SubContainerRc<Manifest, Effects>;
isOwned(): this is SubContainerOwned<Manifest, Effects>;
}
export type CommandOptions = {
/**
* Environment variables to set for this command
*/
env?: {
[variable in string]?: string;
};
/**
* the working directory to run this command in
*/
cwd?: string;
/**
* the user to run this command as
*/
user?: string;
};
export type StdioOptions = {
stdio?: cp.IOType;
};
/** UID/GID mapping for mount id-remapping (see kernel idmappings docs) */
export type IdMap = {
fromId: number;
toId: number;
range: number;
};
/** Union of all mount option types supported by the subcontainer runtime */
export type MountOptions = MountOptionsVolume | MountOptionsAssets | MountOptionsPointer | MountOptionsBackup;
/** Mount options for binding a service volume into a subcontainer */
export type MountOptionsVolume = {
type: 'volume';
volumeId: string;
subpath: string | null;
readonly: boolean;
filetype: 'file' | 'directory' | 'infer';
idmap: IdMap[];
};
/** Mount options for binding packaged static assets into a subcontainer */
export type MountOptionsAssets = {
type: 'assets';
subpath: string | null;
filetype: 'file' | 'directory' | 'infer';
idmap: {
fromId: number;
toId: number;
range: number;
}[];
};
/** Mount options for binding a dependency package's volume into a subcontainer */
export type MountOptionsPointer = {
type: 'pointer';
packageId: string;
volumeId: string;
subpath: string | null;
readonly: boolean;
idmap: {
fromId: number;
toId: number;
range: number;
}[];
};
/** Mount options for binding the backup directory into a subcontainer */
export type MountOptionsBackup = {
type: 'backup';
subpath: string | null;
filetype: 'file' | 'directory' | 'infer';
idmap: {
fromId: number;
toId: number;
range: number;
}[];
};
/**
* Error thrown when a subcontainer command exits with a non-zero code or signal.
* Contains the full result including stdout, stderr, exit code, and exit signal.
*/
export declare class ExitError extends Error {
readonly command: string;
readonly result: {
exitCode: number | null;
exitSignal: T.Signals | null;
stdout: string | Buffer;
stderr: string | Buffer;
};
constructor(command: string, result: {
exitCode: number | null;
exitSignal: T.Signals | null;
stdout: string | Buffer;
stderr: string | Buffer;
});
}
+582
View File
@@ -0,0 +1,582 @@
"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.ExitError = exports.SubContainerRc = exports.SubContainerOwned = exports.execFile = void 0;
const fs = __importStar(require("fs/promises"));
const cp = __importStar(require("child_process"));
const util_1 = require("util");
const node_buffer_1 = require("node:buffer");
const once_1 = require("../../../base/lib/util/once");
const Drop_1 = require("../../../base/lib/util/Drop");
exports.execFile = (0, util_1.promisify)(cp.execFile);
const False = () => false;
const TIMES_TO_WAIT_FOR_PROC = 100;
async function prepBind(from, to, type) {
const fromMeta = from ? await fs.stat(from).catch((_) => null) : null;
const toMeta = await fs.stat(to).catch((_) => null);
if (type === 'file' || (type === 'infer' && from && fromMeta?.isFile())) {
if (toMeta && toMeta.isDirectory())
await fs.rmdir(to, { recursive: false });
if (from && !fromMeta) {
await fs.mkdir(from.replace(/\/[^\/]*\/?$/, ''), { recursive: true });
await fs.writeFile(from, '');
}
if (!toMeta) {
await fs.mkdir(to.replace(/\/[^\/]*\/?$/, ''), { recursive: true });
await fs.writeFile(to, '');
}
}
else {
if (toMeta && toMeta.isFile() && !toMeta.size)
await fs.rm(to);
if (from && !fromMeta)
await fs.mkdir(from, { recursive: true });
if (!toMeta)
await fs.mkdir(to, { recursive: true });
}
}
async function bind(from, to, type, idmap) {
await prepBind(from, to, type);
const args = ['--bind'];
if (idmap.length) {
args.push(`-oX-mount.idmap=${idmap.map((i) => `b:${i.fromId}:${i.toId}:${i.range}`).join(' ')}`);
}
await (0, exports.execFile)('mount', [...args, from, to]);
}
/**
* Want to limit what we can do in a container, so we want to launch a container with a specific image and the mounts.
*/
class SubContainerOwned extends Drop_1.Drop {
constructor(effects, imageId, rootfs, guid) {
super();
this.effects = effects;
this.imageId = imageId;
this.rootfs = rootfs;
this.guid = guid;
this.destroyed = false;
this.rcs = 0;
this.leaderExited = false;
this.leaderExited = false;
this.leader = cp.spawn('start-container', ['subcontainer', 'launch', rootfs], {
killSignal: 'SIGKILL',
stdio: 'inherit',
});
this.leader.on('exit', () => {
this.leaderExited = true;
});
this.waitProc = (0, once_1.once)(() => new Promise(async (resolve, reject) => {
let count = 0;
while (!(await fs.stat(`${this.rootfs}/proc/1`).then((x) => !!x, False))) {
if (count++ > TIMES_TO_WAIT_FOR_PROC) {
console.debug('Failed to start subcontainer', {
guid: this.guid,
imageId: this.imageId,
rootfs: this.rootfs,
});
return reject(new Error(`Failed to start subcontainer ${this.imageId}`));
}
await wait(1);
}
resolve(null);
}));
}
static async of(effects, image, mounts, name) {
const { imageId, sharedRun } = image;
const [rootfs, guid] = await effects.subcontainer.createFs({
imageId,
name,
});
const res = new SubContainerOwned(effects, imageId, rootfs, guid);
try {
if (mounts) {
await res.mount(mounts);
}
const shared = ['dev', 'sys'];
if (!!sharedRun) {
shared.push('run');
}
await fs.mkdir(`${rootfs}/etc`, { recursive: true });
await fs.copyFile('/etc/resolv.conf', `${rootfs}/etc/resolv.conf`);
for (const dirPart of shared) {
const from = `/${dirPart}`;
const to = `${rootfs}/${dirPart}`;
await fs.mkdir(from, { recursive: true });
await fs.mkdir(to, { recursive: true });
await (0, exports.execFile)('mount', ['--rbind', from, to]);
}
return res;
}
catch (e) {
await res.destroy();
throw e;
}
}
static async withTemp(effects, image, mounts, name, fn) {
const subContainer = await SubContainerOwned.of(effects, image, mounts, name);
try {
return await fn(subContainer);
}
finally {
await subContainer.destroy();
}
}
subpath(path) {
return path.startsWith('/')
? `${this.rootfs}${path}`
: `${this.rootfs}/${path}`;
}
async mount(mounts) {
for (let mount of mounts.build()) {
let { options, mountpoint } = mount;
const path = mountpoint.startsWith('/')
? `${this.rootfs}${mountpoint}`
: `${this.rootfs}/${mountpoint}`;
if (options.type === 'volume') {
const subpath = options.subpath
? options.subpath.startsWith('/')
? options.subpath
: `/${options.subpath}`
: '/';
const from = `/media/startos/volumes/${options.volumeId}${subpath}`;
await bind(from, path, options.filetype, options.idmap);
}
else if (options.type === 'assets') {
const subpath = options.subpath
? options.subpath.startsWith('/')
? options.subpath
: `/${options.subpath}`
: '/';
const from = `/media/startos/assets/${subpath}`;
await bind(from, path, options.filetype, options.idmap);
}
else if (options.type === 'pointer') {
await prepBind(null, path, 'directory');
await this.effects.mount({ location: path, target: options });
}
else if (options.type === 'backup') {
const subpath = options.subpath
? options.subpath.startsWith('/')
? options.subpath
: `/${options.subpath}`
: '/';
const from = `/media/startos/backup${subpath}`;
await bind(from, path, options.filetype, options.idmap);
}
else {
throw new Error(`unknown type ${options.type}`);
}
}
return this;
}
async killLeader() {
if (this.leaderExited) {
return;
}
return new Promise((resolve, reject) => {
try {
let timeout = setTimeout(() => this.leader.kill('SIGKILL'), 30000);
this.leader.on('exit', () => {
clearTimeout(timeout);
resolve(null);
});
if (!this.leader.kill('SIGTERM')) {
reject(new Error('kill(2) failed'));
}
}
catch (e) {
reject(e);
}
});
}
get destroy() {
return async () => {
if (!this.destroyed) {
const guid = this.guid;
await this.killLeader();
await this.effects.subcontainer.destroyFs({ guid });
this.destroyed = true;
}
return null;
};
}
onDrop() {
console.log(`Cleaning up dangling subcontainer ${this.guid}`);
this.destroy();
}
/**
* @description run a command inside this subcontainer
* DOES NOT THROW ON NONZERO EXIT CODE (see execFail)
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
async exec(command, options, timeoutMs = 30000, abort) {
await this.waitProc();
const imageMeta = await fs
.readFile(`/media/startos/images/${this.imageId}.json`, {
encoding: 'utf8',
})
.catch(() => '{}')
.then(JSON.parse);
let extra = [];
let user = imageMeta.user || 'root';
if (options?.user) {
user = options.user;
delete options.user;
}
let workdir = imageMeta.workdir || '/';
if (options?.cwd) {
workdir = options.cwd;
delete options.cwd;
}
if (options?.env) {
for (let [k, v] of Object.entries(options.env)) {
extra.push(`--env=${k}=${v}`);
}
}
const child = cp.spawn('start-container', [
'subcontainer',
'exec',
`--env-file=/media/startos/images/${this.imageId}.env`,
`--user=${user}`,
`--workdir=${workdir}`,
...extra,
this.rootfs,
...command,
], options || {});
abort?.signal.addEventListener('abort', () => child.kill('SIGKILL'));
if (options?.input) {
await new Promise((resolve, reject) => {
try {
child.stdin.on('error', (e) => reject(e));
child.stdin.write(options.input, (e) => {
if (e) {
reject(e);
}
else {
resolve(null);
}
});
}
catch (e) {
reject(e);
}
});
await new Promise((resolve, reject) => {
try {
child.stdin.end(resolve);
}
catch (e) {
reject(e);
}
});
}
const stdout = { data: '' };
const stderr = { data: '' };
const appendData = (appendTo) => (chunk) => {
if (typeof chunk === 'string' || chunk instanceof node_buffer_1.Buffer) {
appendTo.data += chunk.toString();
}
else {
console.error('received unexpected chunk', chunk);
}
};
return new Promise((resolve, reject) => {
child.on('error', reject);
let killTimeout;
if (timeoutMs !== null && child.pid) {
killTimeout = setTimeout(() => child.kill('SIGKILL'), timeoutMs);
}
child.stdout.on('data', appendData(stdout));
child.stderr.on('data', appendData(stderr));
child.on('exit', (code, signal) => {
clearTimeout(killTimeout);
const result = {
exitCode: code,
exitSignal: signal,
stdout: stdout.data,
stderr: stderr.data,
};
resolve({
throw: () => !code && !signal
? { stdout: stdout.data, stderr: stderr.data }
: (() => {
throw new ExitError(command[0], result);
})(),
...result,
});
});
});
}
/**
* @description run a command inside this subcontainer, throwing on non-zero exit status
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
async execFail(command, options, timeoutMs, abort) {
return this.exec(command, options, timeoutMs, abort).then((res) => res.throw());
}
async launch(command, options) {
await this.waitProc();
const imageMeta = await fs
.readFile(`/media/startos/images/${this.imageId}.json`, {
encoding: 'utf8',
})
.catch(() => '{}')
.then(JSON.parse);
let extra = [];
let user = imageMeta.user || 'root';
if (options?.user) {
user = options.user;
delete options.user;
}
let workdir = imageMeta.workdir || '/';
if (options?.cwd) {
workdir = options.cwd;
delete options.cwd;
}
if (options?.env) {
for (let [k, v] of Object.entries(options.env).filter(([_, v]) => v != undefined)) {
extra.push(`--env=${k}=${v}`);
}
}
await this.killLeader();
this.leaderExited = false;
this.leader = cp.spawn('start-container', [
'subcontainer',
'launch',
`--env-file=/media/startos/images/${this.imageId}.env`,
`--user=${user}`,
`--workdir=${workdir}`,
...extra,
this.rootfs,
...command,
], { ...options, stdio: 'inherit' });
this.leader.on('exit', () => {
this.leaderExited = true;
});
return this.leader;
}
async spawn(command, options = { stdio: 'inherit' }) {
await this.waitProc();
const imageMeta = await fs
.readFile(`/media/startos/images/${this.imageId}.json`, {
encoding: 'utf8',
})
.catch(() => '{}')
.then(JSON.parse);
let extra = [];
let user = imageMeta.user || 'root';
if (options?.user) {
user = options.user;
delete options.user;
}
let workdir = imageMeta.workdir || '/';
if (options.cwd) {
workdir = options.cwd;
delete options.cwd;
}
if (options?.env) {
for (let [k, v] of Object.entries(options.env).filter(([_, v]) => v != undefined)) {
extra.push(`--env=${k}=${v}`);
}
}
return cp.spawn('start-container', [
'subcontainer',
'exec',
`--env-file=/media/startos/images/${this.imageId}.env`,
`--user=${user}`,
`--workdir=${workdir}`,
...extra,
this.rootfs,
...command,
], options);
}
/**
* @description Write a file to the subcontainer's filesystem
* @param path Path relative to the subcontainer rootfs (e.g. "/etc/config.json")
* @param data The data to write
* @param options Optional write options (same as node:fs/promises writeFile)
*/
async writeFile(path, data, options) {
const fullPath = this.subpath(path);
const dir = fullPath.replace(/\/[^/]*\/?$/, '');
await fs.mkdir(dir, { recursive: true });
return fs.writeFile(fullPath, data, options);
}
rc() {
return new SubContainerRc(this);
}
isOwned() {
return true;
}
}
exports.SubContainerOwned = SubContainerOwned;
/**
* A reference-counted handle to a {@link SubContainerOwned}.
*
* Multiple `SubContainerRc` instances can share one underlying subcontainer.
* The subcontainer is destroyed only when the last reference is released via `destroy()`.
*/
class SubContainerRc extends Drop_1.Drop {
get imageId() {
return this.subcontainer.imageId;
}
get rootfs() {
return this.subcontainer.rootfs;
}
get guid() {
return this.subcontainer.guid;
}
subpath(path) {
return this.subcontainer.subpath(path);
}
constructor(subcontainer) {
subcontainer.rcs++;
super();
this.subcontainer = subcontainer;
this.destroyed = false;
this.destroying = null;
}
static async of(effects, image, mounts, name) {
return new SubContainerRc(await SubContainerOwned.of(effects, image, mounts, name));
}
static async withTemp(effects, image, mounts, name, fn) {
const subContainer = await SubContainerRc.of(effects, image, mounts, name);
try {
return await fn(subContainer);
}
finally {
await subContainer.destroy();
}
}
async mount(mounts) {
await this.subcontainer.mount(mounts);
return this;
}
get destroy() {
return async () => {
if (!this.destroyed && !this.destroying) {
const rcs = --this.subcontainer.rcs;
if (rcs <= 0) {
this.destroying = this.subcontainer.destroy();
if (rcs < 0)
console.error(new Error('UNREACHABLE: rcs < 0').stack);
}
}
if (this.destroying) {
await this.destroying;
}
this.destroyed = true;
this.destroying = null;
return null;
};
}
onDrop() {
this.destroy();
}
/**
* @description run a command inside this subcontainer
* DOES NOT THROW ON NONZERO EXIT CODE (see execFail)
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
async exec(command, options, timeoutMs, abort) {
return this.subcontainer.exec(command, options, timeoutMs, abort);
}
/**
* @description run a command inside this subcontainer, throwing on non-zero exit status
* @param commands an array representing the command and args to execute
* @param options
* @param timeoutMs how long to wait before killing the command in ms
* @returns
*/
async execFail(command, options, timeoutMs, abort) {
return this.subcontainer.execFail(command, options, timeoutMs, abort);
}
async launch(command, options) {
return this.subcontainer.launch(command, options);
}
async spawn(command, options = { stdio: 'inherit' }) {
return this.subcontainer.spawn(command, options);
}
/**
* @description Write a file to the subcontainer's filesystem
* @param path Path relative to the subcontainer rootfs (e.g. "/etc/config.json")
* @param data The data to write
* @param options Optional write options (same as node:fs/promises writeFile)
*/
async writeFile(path, data, options) {
return this.subcontainer.writeFile(path, data, options);
}
rc() {
return this.subcontainer.rc();
}
isOwned() {
return false;
}
}
exports.SubContainerRc = SubContainerRc;
function wait(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
/**
* Error thrown when a subcontainer command exits with a non-zero code or signal.
* Contains the full result including stdout, stderr, exit code, and exit signal.
*/
class ExitError extends Error {
constructor(command, result) {
let message;
if (result.exitCode) {
message = `${command} failed with exit code ${result.exitCode}: ${result.stderr}`;
}
else if (result.exitSignal) {
message = `${command} terminated with signal ${result.exitSignal}: ${result.stderr}`;
}
else {
message = `${command} succeeded: ${result.stdout}`;
}
super(message);
this.command = command;
this.result = result;
}
}
exports.ExitError = ExitError;
//# sourceMappingURL=SubContainer.js.map
File diff suppressed because one or more lines are too long
+48
View File
@@ -0,0 +1,48 @@
import * as fs from 'node:fs/promises';
import * as T from '../../../base/lib/types';
/**
* Common interface for objects that have a subpath method (Volume, SubContainer, etc.)
*/
export interface PathBase {
subpath(path: string): string;
}
/**
* @description Represents a volume in the StartOS filesystem.
* Provides utilities for reading and writing files within the volume.
*/
export declare class Volume<Id extends string = string> implements PathBase {
readonly id: Id;
/**
* The absolute path to this volume's root directory
*/
readonly path: string;
constructor(id: Id);
/**
* Get the absolute path to a file or directory within this volume
* @param subpath Path relative to the volume root
*/
subpath(subpath: string): string;
/**
* @description Read a file from this volume
* @param subpath Path relative to the volume root (e.g. "config.json" or "/data/file.txt")
* @param options Optional read options (same as node:fs/promises readFile)
*/
readFile(subpath: string, options?: Parameters<typeof fs.readFile>[1]): Promise<Buffer | string>;
/**
* @description Write a file to this volume
* @param subpath Path relative to the volume root (e.g. "config.json" or "/data/file.txt")
* @param data The data to write
* @param options Optional write options (same as node:fs/promises writeFile)
*/
writeFile(subpath: string, data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView>, options?: Parameters<typeof fs.writeFile>[2]): Promise<void>;
}
/**
* Type-safe volumes object that provides Volume instances for each volume defined in the manifest
*/
export type Volumes<Manifest extends T.SDKManifest> = {
[K in Manifest['volumes'][number]]: Volume<K>;
};
/**
* Creates a type-safe volumes object from a manifest
*/
export declare function createVolumes<Manifest extends T.SDKManifest>(manifest: Manifest): Volumes<Manifest>;

Some files were not shown because too many files have changed in this diff Show More