Fix StartOS 0.4 TypeScript packaging to match SDK API
This commit is contained in:
+36
@@ -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>;
|
||||
+117
@@ -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
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"GetServiceManifest.js","sourceRoot":"","sources":["../../../../package/lib/util/GetServiceManifest.ts"],"names":[],"mappings":"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"}
|
||||
+33
@@ -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>;
|
||||
}
|
||||
+100
@@ -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
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"GetSslCertificate.js","sourceRoot":"","sources":["../../../../package/lib/util/GetSslCertificate.ts"],"names":[],"mappings":"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"}
|
||||
+340
@@ -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
@@ -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
|
||||
+1
File diff suppressed because one or more lines are too long
+48
@@ -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>;
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
"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.Volume = void 0;
|
||||
exports.createVolumes = createVolumes;
|
||||
const fs = __importStar(require("node:fs/promises"));
|
||||
/**
|
||||
* @description Represents a volume in the StartOS filesystem.
|
||||
* Provides utilities for reading and writing files within the volume.
|
||||
*/
|
||||
class Volume {
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
this.path = `/media/startos/volumes/${id}`;
|
||||
}
|
||||
/**
|
||||
* Get the absolute path to a file or directory within this volume
|
||||
* @param subpath Path relative to the volume root
|
||||
*/
|
||||
subpath(subpath) {
|
||||
return subpath.startsWith('/')
|
||||
? `${this.path}${subpath}`
|
||||
: `${this.path}/${subpath}`;
|
||||
}
|
||||
/**
|
||||
* @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)
|
||||
*/
|
||||
async readFile(subpath, options) {
|
||||
const fullPath = this.subpath(subpath);
|
||||
return fs.readFile(fullPath, options);
|
||||
}
|
||||
/**
|
||||
* @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)
|
||||
*/
|
||||
async writeFile(subpath, data, options) {
|
||||
const fullPath = this.subpath(subpath);
|
||||
const dir = fullPath.replace(/\/[^/]*\/?$/, '');
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
return fs.writeFile(fullPath, data, options);
|
||||
}
|
||||
}
|
||||
exports.Volume = Volume;
|
||||
/**
|
||||
* Creates a type-safe volumes object from a manifest
|
||||
*/
|
||||
function createVolumes(manifest) {
|
||||
const volumes = {};
|
||||
for (const volumeId of manifest.volumes) {
|
||||
;
|
||||
volumes[volumeId] = new Volume(volumeId);
|
||||
}
|
||||
return volumes;
|
||||
}
|
||||
//# sourceMappingURL=Volume.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"Volume.js","sourceRoot":"","sources":["../../../../package/lib/util/Volume.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+EA,sCAQC;AAvFD,qDAAsC;AAUtC;;;GAGG;AACH,MAAa,MAAM;IAMjB,YAAqB,EAAM;QAAN,OAAE,GAAF,EAAE,CAAI;QACzB,IAAI,CAAC,IAAI,GAAG,0BAA0B,EAAE,EAAE,CAAA;IAC5C,CAAC;IAED;;;OAGG;IACH,OAAO,CAAC,OAAe;QACrB,OAAO,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;YAC5B,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,GAAG,OAAO,EAAE;YAC1B,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,IAAI,OAAO,EAAE,CAAA;IAC/B,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,QAAQ,CACZ,OAAe,EACf,OAA2C;QAE3C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QACtC,OAAO,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;IACvC,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,SAAS,CACb,OAAe,EACf,IAIkD,EAClD,OAA4C;QAE5C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QACtC,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAA;QAC/C,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACxC,OAAO,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,CAAA;IAC9C,CAAC;CACF;AArDD,wBAqDC;AASD;;GAEG;AACH,SAAgB,aAAa,CAC3B,QAAkB;IAElB,MAAM,OAAO,GAAG,EAAuB,CAAA;IACvC,KAAK,MAAM,QAAQ,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;QACxC,CAAC;QAAC,OAAe,CAAC,QAAQ,CAAC,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAA;IACpD,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC"}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import { type X2jOptions, type XmlBuilderOptions } from 'fast-xml-parser';
|
||||
import * as INI from 'ini';
|
||||
import { z } from 'zod';
|
||||
import * as T from '../../../base/lib/types';
|
||||
import { PathBase } from './Volume';
|
||||
/**
|
||||
* Bidirectional transformers for converting between the raw file format and
|
||||
* the application-level data type. Used with FileHelper factory methods.
|
||||
*
|
||||
* @typeParam Raw - The native type the file format parses to (e.g. `Record<string, unknown>` for JSON)
|
||||
* @typeParam Transformed - The application-level type after transformation
|
||||
*/
|
||||
export type Transformers<Raw = unknown, Transformed = unknown, Validated extends Transformed = Transformed> = {
|
||||
/** Transform raw parsed data into the application type */
|
||||
onRead: (value: Raw) => Transformed;
|
||||
/** Transform application data back into the raw format for writing */
|
||||
onWrite: (value: Validated) => Raw;
|
||||
};
|
||||
type ToPath = string | {
|
||||
base: PathBase;
|
||||
subpath: string;
|
||||
};
|
||||
type Validator<_T, U> = z.ZodType<U>;
|
||||
type ReadType<A> = {
|
||||
once: () => Promise<A | null>;
|
||||
const: (effects: T.Effects) => Promise<A | null>;
|
||||
watch: (effects: T.Effects, abort?: AbortSignal) => AsyncGenerator<A | null, never, unknown>;
|
||||
onChange: (effects: T.Effects, callback: (value: A | null, error?: Error) => {
|
||||
cancel: boolean;
|
||||
} | Promise<{
|
||||
cancel: boolean;
|
||||
}>) => void;
|
||||
waitFor: (effects: T.Effects, pred: (value: A | null) => boolean) => Promise<A | null>;
|
||||
};
|
||||
/**
|
||||
* @description Use this class to read/write an underlying configuration file belonging to the upstream service.
|
||||
*
|
||||
* These type definitions should reflect the underlying file as closely as possible. For example, if the service does not require a particular value, it should be marked as optional(), even if your package requires it.
|
||||
*
|
||||
* It is recommended to use onMismatch() whenever possible. This provides an escape hatch in case the user edits the file manually and accidentally sets a value to an unsupported type.
|
||||
*
|
||||
* Officially supported file types are json, yaml, and toml. Other files types can use "raw"
|
||||
*
|
||||
* Choose between officially supported file formats (), or a custom format (raw).
|
||||
*
|
||||
* @example
|
||||
* Below are a few examples
|
||||
*
|
||||
* ```
|
||||
* import { matches, FileHelper } from '@start9labs/start-sdk'
|
||||
* const { arrayOf, boolean, literal, literals, object, natural, string } = matches
|
||||
*
|
||||
* export const jsonFile = FileHelper.json('./inputSpec.json', object({
|
||||
* passwords: arrayOf(string).onMismatch([])
|
||||
* type: literals('private', 'public').optional().onMismatch(undefined)
|
||||
* }))
|
||||
*
|
||||
* export const tomlFile = FileHelper.toml('./inputSpec.toml', object({
|
||||
* url: literal('https://start9.com').onMismatch('https://start9.com')
|
||||
* public: boolean.onMismatch(true)
|
||||
* }))
|
||||
*
|
||||
* export const yamlFile = FileHelper.yaml('./inputSpec.yml', object({
|
||||
* name: string.optional().onMismatch(undefined)
|
||||
* age: natural.optional().onMismatch(undefined)
|
||||
* }))
|
||||
*
|
||||
* export const bitcoinConfFile = FileHelper.raw(
|
||||
* './service.conf',
|
||||
* (obj: CustomType) => customConvertObjToFormattedString(obj),
|
||||
* (str) => customParseStringToTypedObj(str),
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export declare class FileHelper<A> {
|
||||
readonly path: string;
|
||||
readonly writeData: (dataIn: A) => string;
|
||||
readonly readData: (stringValue: string) => unknown;
|
||||
readonly validate: (value: unknown) => A;
|
||||
private consts;
|
||||
protected constructor(path: string, writeData: (dataIn: A) => string, readData: (stringValue: string) => unknown, validate: (value: unknown) => A);
|
||||
private writeFileRaw;
|
||||
/**
|
||||
* Accepts structured data and overwrites the existing file on disk.
|
||||
*/
|
||||
private writeFile;
|
||||
private readFileRaw;
|
||||
private readFile;
|
||||
/**
|
||||
* Reads the file from disk and converts it to structured data.
|
||||
*/
|
||||
private readOnce;
|
||||
private createFileWatchable;
|
||||
/**
|
||||
* Create a reactive reader for this file.
|
||||
*
|
||||
* Returns an object with multiple read strategies:
|
||||
* - `once()` - Read the file once and return the parsed value
|
||||
* - `const(effects)` - Read once but re-read when the file changes (for use with constRetry)
|
||||
* - `watch(effects)` - Async generator yielding new values on each file change
|
||||
* - `onChange(effects, callback)` - Fire a callback on each file change
|
||||
* - `waitFor(effects, predicate)` - Block until the file value satisfies a predicate
|
||||
*
|
||||
* @param map - Optional transform function applied after validation
|
||||
* @param eq - Optional equality function to deduplicate watch emissions
|
||||
*/
|
||||
read(): ReadType<A>;
|
||||
read<B>(map: (value: A) => B, eq?: (left: B | null, right: B | null) => boolean): ReadType<B>;
|
||||
/**
|
||||
* Accepts full structured data and overwrites the existing file on disk if it exists.
|
||||
*/
|
||||
write(effects: T.Effects, data: T.AllowReadonly<A> | A, options?: {
|
||||
allowWriteAfterConst?: boolean;
|
||||
}): Promise<null>;
|
||||
/**
|
||||
* Accepts partial structured data and performs a merge with the existing file on disk.
|
||||
*/
|
||||
merge(effects: T.Effects, data: T.AllowReadonly<T.DeepPartial<A>>, options?: {
|
||||
allowWriteAfterConst?: boolean;
|
||||
}): Promise<null>;
|
||||
/**
|
||||
* We wanted to be able to have a fileHelper, and just modify the path later in time.
|
||||
* Like one behavior of another dependency or something similar.
|
||||
*/
|
||||
withPath(path: ToPath): FileHelper<A>;
|
||||
/**
|
||||
* Create a File Helper for an arbitrary file type.
|
||||
*
|
||||
* Provide custom functions for translating data to/from the file format.
|
||||
*/
|
||||
static raw<A>(path: ToPath, toFile: (dataIn: A) => string, fromFile: (rawData: string) => unknown, validate: (data: unknown) => A): FileHelper<A>;
|
||||
private static rawTransformed;
|
||||
/**
|
||||
* Create a File Helper for a text file
|
||||
*/
|
||||
static string(path: ToPath): FileHelper<string>;
|
||||
static string<A extends string>(path: ToPath, shape: Validator<string, A>): FileHelper<A>;
|
||||
static string<A extends Transformed, Transformed = string>(path: ToPath, shape: Validator<Transformed, A>, transformers: Transformers<string, Transformed, A>): FileHelper<A>;
|
||||
/**
|
||||
* Create a File Helper for a .json file.
|
||||
*/
|
||||
static json<A>(path: ToPath, shape: Validator<unknown, A>): FileHelper<A>;
|
||||
static json<A extends Transformed, Transformed = unknown>(path: ToPath, shape: Validator<unknown, A>, transformers: Transformers<unknown, Transformed, A>): FileHelper<A>;
|
||||
/**
|
||||
* Create a File Helper for a .yaml file
|
||||
*/
|
||||
static yaml<A extends Record<string, unknown>>(path: ToPath, shape: Validator<Record<string, unknown>, A>): FileHelper<A>;
|
||||
static yaml<A extends Transformed, Transformed = Record<string, unknown>>(path: ToPath, shape: Validator<Transformed, A>, transformers: Transformers<Record<string, unknown>, Transformed, A>): FileHelper<A>;
|
||||
/**
|
||||
* Create a File Helper for a .toml file
|
||||
*/
|
||||
static toml<A extends Record<string, unknown>>(path: ToPath, shape: Validator<Record<string, unknown>, A>): FileHelper<A>;
|
||||
static toml<A extends Transformed, Transformed = Record<string, unknown>>(path: ToPath, shape: Validator<Transformed, A>, transformers: Transformers<Record<string, unknown>, Transformed, A>): FileHelper<A>;
|
||||
/**
|
||||
* Create a File Helper for a .ini file.
|
||||
*
|
||||
* Supports optional encode/decode options and custom transformers.
|
||||
*/
|
||||
static ini<A extends Record<string, unknown>>(path: ToPath, shape: Validator<Record<string, unknown>, A>, options?: INI.EncodeOptions & INI.DecodeOptions): FileHelper<A>;
|
||||
static ini<A extends Transformed, Transformed = Record<string, unknown>>(path: ToPath, shape: Validator<Transformed, A>, options: INI.EncodeOptions & INI.DecodeOptions, transformers: Transformers<Record<string, unknown>, Transformed, A>): FileHelper<A>;
|
||||
/**
|
||||
* Create a File Helper for a .env file (KEY=VALUE format, one per line).
|
||||
*
|
||||
* Lines starting with `#` are treated as comments and ignored on read.
|
||||
*/
|
||||
static env<A extends Record<string, string>>(path: ToPath, shape: Validator<Record<string, string>, A>): FileHelper<A>;
|
||||
static env<A extends Transformed, Transformed = Record<string, string>>(path: ToPath, shape: Validator<Transformed, A>, transformers: Transformers<Record<string, string>, Transformed, A>): FileHelper<A>;
|
||||
/**
|
||||
* Create a File Helper for an .xml file.
|
||||
*
|
||||
* Supports optional parser/builder options from `fast-xml-parser`.
|
||||
*/
|
||||
static xml<A extends Record<string, unknown>>(path: ToPath, shape: Validator<Record<string, unknown>, A>, options?: {
|
||||
parser?: X2jOptions;
|
||||
builder?: XmlBuilderOptions;
|
||||
}): FileHelper<A>;
|
||||
static xml<A extends Transformed, Transformed = Record<string, unknown>>(path: ToPath, shape: Validator<Transformed, A>, options: {
|
||||
parser?: X2jOptions;
|
||||
builder?: XmlBuilderOptions;
|
||||
}, transformers: Transformers<Record<string, unknown>, Transformed, A>): FileHelper<A>;
|
||||
}
|
||||
export default FileHelper;
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
"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.FileHelper = void 0;
|
||||
const TOML = __importStar(require("@iarna/toml"));
|
||||
const fast_xml_parser_1 = require("fast-xml-parser");
|
||||
const INI = __importStar(require("ini"));
|
||||
const fs = __importStar(require("node:fs/promises"));
|
||||
const YAML = __importStar(require("yaml"));
|
||||
const zod_1 = require("zod");
|
||||
const util_1 = require("../../../base/lib/util");
|
||||
const Watchable_1 = require("../../../base/lib/util/Watchable");
|
||||
const previousPath = /(.+?)\/([^/]*)$/;
|
||||
const exists = (path) => fs.access(path).then(() => true, () => false);
|
||||
async function onCreated(path) {
|
||||
if (path === '/')
|
||||
return;
|
||||
if (!path.startsWith('/'))
|
||||
path = `${process.cwd()}/${path}`;
|
||||
if (await exists(path)) {
|
||||
return;
|
||||
}
|
||||
const split = path.split('/');
|
||||
const filename = split.pop();
|
||||
const parent = split.join('/');
|
||||
await onCreated(parent);
|
||||
const ctrl = new AbortController();
|
||||
const watch = fs.watch(parent, { persistent: false, signal: ctrl.signal });
|
||||
if (await exists(path)) {
|
||||
ctrl.abort();
|
||||
return;
|
||||
}
|
||||
if (await fs.access(path).then(() => true, () => false)) {
|
||||
ctrl.abort();
|
||||
return;
|
||||
}
|
||||
for await (let event of watch) {
|
||||
if (event.filename === filename) {
|
||||
ctrl.abort('finished');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
function fileMerge(...args) {
|
||||
let res = args.shift();
|
||||
for (const arg of args) {
|
||||
if (res === arg)
|
||||
continue;
|
||||
else if (res &&
|
||||
arg &&
|
||||
typeof res === 'object' &&
|
||||
typeof arg === 'object' &&
|
||||
!Array.isArray(res) &&
|
||||
!Array.isArray(arg)) {
|
||||
for (const key of Object.keys(arg)) {
|
||||
res[key] = fileMerge(res[key], arg[key]);
|
||||
}
|
||||
}
|
||||
else
|
||||
res = arg;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
function filterUndefined(a) {
|
||||
if (a && typeof a === 'object') {
|
||||
if (Array.isArray(a)) {
|
||||
return a.map(filterUndefined);
|
||||
}
|
||||
return Object.entries(a).reduce((acc, [k, v]) => {
|
||||
if (v !== undefined) {
|
||||
acc[k] = filterUndefined(v);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
return a;
|
||||
}
|
||||
function toPath(path) {
|
||||
if (typeof path === 'string') {
|
||||
return path;
|
||||
}
|
||||
return path.base.subpath(path.subpath);
|
||||
}
|
||||
/**
|
||||
* @description Use this class to read/write an underlying configuration file belonging to the upstream service.
|
||||
*
|
||||
* These type definitions should reflect the underlying file as closely as possible. For example, if the service does not require a particular value, it should be marked as optional(), even if your package requires it.
|
||||
*
|
||||
* It is recommended to use onMismatch() whenever possible. This provides an escape hatch in case the user edits the file manually and accidentally sets a value to an unsupported type.
|
||||
*
|
||||
* Officially supported file types are json, yaml, and toml. Other files types can use "raw"
|
||||
*
|
||||
* Choose between officially supported file formats (), or a custom format (raw).
|
||||
*
|
||||
* @example
|
||||
* Below are a few examples
|
||||
*
|
||||
* ```
|
||||
* import { matches, FileHelper } from '@start9labs/start-sdk'
|
||||
* const { arrayOf, boolean, literal, literals, object, natural, string } = matches
|
||||
*
|
||||
* export const jsonFile = FileHelper.json('./inputSpec.json', object({
|
||||
* passwords: arrayOf(string).onMismatch([])
|
||||
* type: literals('private', 'public').optional().onMismatch(undefined)
|
||||
* }))
|
||||
*
|
||||
* export const tomlFile = FileHelper.toml('./inputSpec.toml', object({
|
||||
* url: literal('https://start9.com').onMismatch('https://start9.com')
|
||||
* public: boolean.onMismatch(true)
|
||||
* }))
|
||||
*
|
||||
* export const yamlFile = FileHelper.yaml('./inputSpec.yml', object({
|
||||
* name: string.optional().onMismatch(undefined)
|
||||
* age: natural.optional().onMismatch(undefined)
|
||||
* }))
|
||||
*
|
||||
* export const bitcoinConfFile = FileHelper.raw(
|
||||
* './service.conf',
|
||||
* (obj: CustomType) => customConvertObjToFormattedString(obj),
|
||||
* (str) => customParseStringToTypedObj(str),
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
class FileHelper {
|
||||
constructor(path, writeData, readData, validate) {
|
||||
this.path = path;
|
||||
this.writeData = writeData;
|
||||
this.readData = readData;
|
||||
this.validate = validate;
|
||||
this.consts = [];
|
||||
}
|
||||
async writeFileRaw(data) {
|
||||
const parent = previousPath.exec(this.path);
|
||||
if (parent) {
|
||||
await fs.mkdir(parent[1], { recursive: true });
|
||||
}
|
||||
await fs.writeFile(this.path, data);
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Accepts structured data and overwrites the existing file on disk.
|
||||
*/
|
||||
async writeFile(data) {
|
||||
return await this.writeFileRaw(this.writeData(data));
|
||||
}
|
||||
async readFileRaw() {
|
||||
if (!(await exists(this.path))) {
|
||||
return null;
|
||||
}
|
||||
return await fs.readFile(this.path).then((data) => data.toString('utf-8'));
|
||||
}
|
||||
async readFile() {
|
||||
const raw = await this.readFileRaw();
|
||||
if (raw === null) {
|
||||
return raw;
|
||||
}
|
||||
return this.readData(raw);
|
||||
}
|
||||
/**
|
||||
* Reads the file from disk and converts it to structured data.
|
||||
*/
|
||||
async readOnce(map) {
|
||||
const data = await this.readFile();
|
||||
if (!data)
|
||||
return null;
|
||||
return map(this.validate(data));
|
||||
}
|
||||
createFileWatchable(effects, map, eq) {
|
||||
const doRead = async () => {
|
||||
const data = await this.readFile();
|
||||
if (!data)
|
||||
return null;
|
||||
return this.validate(data);
|
||||
};
|
||||
const filePath = this.path;
|
||||
const fileHelper = this;
|
||||
const wrappedMap = (raw) => {
|
||||
if (raw === null)
|
||||
return null;
|
||||
return map(raw);
|
||||
};
|
||||
return new (class extends Watchable_1.Watchable {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.label = 'FileHelper';
|
||||
}
|
||||
async fetch() {
|
||||
return doRead();
|
||||
}
|
||||
async *produce(abort) {
|
||||
while (this.effects.isInContext && !abort.aborted) {
|
||||
if (await exists(filePath)) {
|
||||
const ctrl = new AbortController();
|
||||
abort.addEventListener('abort', () => ctrl.abort());
|
||||
const watch = fs.watch(filePath, {
|
||||
persistent: false,
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
yield await doRead();
|
||||
await Promise.resolve()
|
||||
.then(async () => {
|
||||
for await (const _ of watch) {
|
||||
ctrl.abort();
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.catch((e) => console.error((0, util_1.asError)(e)));
|
||||
}
|
||||
else {
|
||||
yield null;
|
||||
await onCreated(filePath).catch((e) => console.error((0, util_1.asError)(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
onConstRegistered(value) {
|
||||
if (!this.effects.constRetry)
|
||||
return;
|
||||
const record = [
|
||||
this.effects.constRetry,
|
||||
value,
|
||||
wrappedMap,
|
||||
eq,
|
||||
];
|
||||
fileHelper.consts.push(record);
|
||||
return () => {
|
||||
fileHelper.consts = fileHelper.consts.filter((r) => r !== record);
|
||||
};
|
||||
}
|
||||
})(effects, { map: wrappedMap, eq });
|
||||
}
|
||||
read(map, eq) {
|
||||
map = map ?? ((a) => a);
|
||||
eq = eq ?? util_1.deepEqual;
|
||||
return {
|
||||
once: () => this.readOnce(map),
|
||||
const: (effects) => this.createFileWatchable(effects, map, eq).const(),
|
||||
watch: (effects, abort) => this.createFileWatchable(effects, map, eq).watch(abort),
|
||||
onChange: (effects, callback) => this.createFileWatchable(effects, map, eq).onChange(callback),
|
||||
waitFor: (effects, pred) => this.createFileWatchable(effects, map, eq).waitFor(pred),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Accepts full structured data and overwrites the existing file on disk if it exists.
|
||||
*/
|
||||
async write(effects, data, options = {}) {
|
||||
const newData = this.validate(data);
|
||||
await this.writeFile(newData);
|
||||
if (!options.allowWriteAfterConst && effects.constRetry) {
|
||||
const records = this.consts.filter(([c]) => c === effects.constRetry);
|
||||
for (const record of records) {
|
||||
const [_, prev, map, eq] = record;
|
||||
if (!eq(prev, map(newData))) {
|
||||
throw new Error(`Canceled: write after const: ${this.path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Accepts partial structured data and performs a merge with the existing file on disk.
|
||||
*/
|
||||
async merge(effects, data, options = {}) {
|
||||
const fileDataRaw = await this.readFileRaw();
|
||||
let fileData = fileDataRaw === null ? null : this.readData(fileDataRaw);
|
||||
try {
|
||||
fileData = this.validate(fileData);
|
||||
}
|
||||
catch (_) { }
|
||||
const mergeData = this.validate(fileMerge({}, fileData, data));
|
||||
const toWrite = this.writeData(mergeData);
|
||||
if (toWrite !== fileDataRaw) {
|
||||
await this.writeFile(mergeData);
|
||||
if (!options.allowWriteAfterConst && effects.constRetry) {
|
||||
const records = this.consts.filter(([c]) => c === effects.constRetry);
|
||||
for (const record of records) {
|
||||
const [_, prev, map, eq] = record;
|
||||
if (!eq(prev, map(mergeData))) {
|
||||
throw new Error(`Canceled: write after const: ${this.path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* We wanted to be able to have a fileHelper, and just modify the path later in time.
|
||||
* Like one behavior of another dependency or something similar.
|
||||
*/
|
||||
withPath(path) {
|
||||
return new FileHelper(toPath(path), this.writeData, this.readData, this.validate);
|
||||
}
|
||||
/**
|
||||
* Create a File Helper for an arbitrary file type.
|
||||
*
|
||||
* Provide custom functions for translating data to/from the file format.
|
||||
*/
|
||||
static raw(path, toFile, fromFile, validate) {
|
||||
return new FileHelper(toPath(path), toFile, fromFile, validate);
|
||||
}
|
||||
static rawTransformed(path, toFile, fromFile, validate, transformers) {
|
||||
return FileHelper.raw(path, (inData) => {
|
||||
if (transformers) {
|
||||
return toFile(transformers.onWrite(inData));
|
||||
}
|
||||
return toFile(inData);
|
||||
}, (fileData) => {
|
||||
if (transformers) {
|
||||
return transformers.onRead(fromFile(fileData));
|
||||
}
|
||||
return fromFile(fileData);
|
||||
}, validate);
|
||||
}
|
||||
static string(path, shape, transformers) {
|
||||
return FileHelper.rawTransformed(path, (inData) => inData, (inString) => inString, (data) => (shape || zod_1.z.string()).parse(data), transformers);
|
||||
}
|
||||
static json(path, shape, transformers) {
|
||||
return FileHelper.rawTransformed(path, (inData) => JSON.stringify(inData, null, 2), (inString) => JSON.parse(inString), (data) => shape.parse(data), transformers);
|
||||
}
|
||||
static yaml(path, shape, transformers) {
|
||||
return FileHelper.rawTransformed(path, (inData) => YAML.stringify(inData, null, 2), (inString) => YAML.parse(inString), (data) => shape.parse(data), transformers);
|
||||
}
|
||||
static toml(path, shape, transformers) {
|
||||
return FileHelper.rawTransformed(path, (inData) => TOML.stringify(inData), (inString) => TOML.parse(inString), (data) => shape.parse(data), transformers);
|
||||
}
|
||||
static ini(path, shape, options, transformers) {
|
||||
return FileHelper.rawTransformed(path, (inData) => INI.stringify(filterUndefined(inData), options), (inString) => INI.parse(inString, options), (data) => shape.parse(data), transformers);
|
||||
}
|
||||
static env(path, shape, transformers) {
|
||||
return FileHelper.rawTransformed(path, (inData) => Object.entries(inData)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join('\n'), (inString) => Object.fromEntries(inString
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => !line.startsWith('#') && line.includes('='))
|
||||
.map((line) => {
|
||||
const pos = line.indexOf('=');
|
||||
return [line.slice(0, pos), line.slice(pos + 1)];
|
||||
})), (data) => shape.parse(data), transformers);
|
||||
}
|
||||
static xml(path, shape, options, transformers) {
|
||||
const parser = new fast_xml_parser_1.XMLParser(options?.parser);
|
||||
const builder = new fast_xml_parser_1.XMLBuilder(options?.builder);
|
||||
return FileHelper.rawTransformed(path, (inData) => builder.build(inData), (inString) => parser.parse(inString), (data) => shape.parse(data), transformers);
|
||||
}
|
||||
}
|
||||
exports.FileHelper = FileHelper;
|
||||
exports.default = FileHelper;
|
||||
//# sourceMappingURL=fileHelper.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+3
@@ -0,0 +1,3 @@
|
||||
export * from '../../../base/lib/util';
|
||||
export { Drop } from '../../../base/lib/util/Drop';
|
||||
export { Volume, Volumes } from './Volume';
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
"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.Volume = exports.Drop = void 0;
|
||||
__exportStar(require("../../../base/lib/util"), exports);
|
||||
var Drop_1 = require("../../../base/lib/util/Drop");
|
||||
Object.defineProperty(exports, "Drop", { enumerable: true, get: function () { return Drop_1.Drop; } });
|
||||
var Volume_1 = require("./Volume");
|
||||
Object.defineProperty(exports, "Volume", { enumerable: true, get: function () { return Volume_1.Volume; } });
|
||||
//# sourceMappingURL=index.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../package/lib/util/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,yDAAsC;AAEtC,oDAAkD;AAAzC,4FAAA,IAAI,OAAA;AACb,mCAA0C;AAAjC,gGAAA,MAAM,OAAA"}
|
||||
Reference in New Issue
Block a user