improve daemons init system (#2960)

* repeatable command launch fn

* allow js fn for daemon exec

* improve daemon init system

* fixes from testing
This commit is contained in:
Aiden McClelland
2025-06-06 14:35:03 -06:00
committed by GitHub
parent 586d950b8c
commit 2464d255d5
13 changed files with 187 additions and 162 deletions

View File

@@ -38,7 +38,7 @@
}, },
"../sdk/dist": { "../sdk/dist": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.26", "version": "0.4.0-beta.27",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^3.0.0", "@iarna/toml": "^3.0.0",

View File

@@ -67,20 +67,14 @@ export class MainLoop {
this.system.manifest.volumes, this.system.manifest.volumes,
`Main - ${currentCommand.join(" ")}`, `Main - ${currentCommand.join(" ")}`,
) )
const daemon = await Daemon.of()( const daemon = await Daemon.of()(this.effects, subcontainer, {
this.effects, command: currentCommand,
subcontainer, runAsInit: true,
currentCommand, env: {
{ TINI_SUBREAPER: "true",
runAsInit: true,
env: {
TINI_SUBREAPER: "true",
},
sigtermTimeout: utils.inMs(
this.system.manifest.main["sigterm-timeout"],
),
}, },
) sigtermTimeout: utils.inMs(this.system.manifest.main["sigterm-timeout"]),
})
daemon.start() daemon.start()
return { return {

View File

@@ -135,12 +135,9 @@ export const polyfillEffects = (
[input.command, ...(input.args || [])].join(" "), [input.command, ...(input.args || [])].join(" "),
) )
const daemon = promiseSubcontainer.then((subcontainer) => const daemon = promiseSubcontainer.then((subcontainer) =>
daemons.runCommand()( daemons.runCommand()(effects, subcontainer, {
effects, command: [input.command, ...(input.args || [])],
subcontainer, }),
[input.command, ...(input.args || [])],
{},
),
) )
return { return {
wait: () => wait: () =>

View File

@@ -265,6 +265,7 @@ pub async fn run_script<P: AsRef<Path>>(path: P, mut progress: PhaseProgressTrac
.input(Some(&mut reader)) .input(Some(&mut reader))
.invoke(ErrorKind::Unknown) .invoke(ErrorKind::Unknown)
.await?; .await?;
// TODO: inherit?
Ok::<_, Error>(()) Ok::<_, Error>(())
} }

View File

@@ -140,8 +140,6 @@ pub struct GetOsVersionParams {
#[ts(type = "string | null")] #[ts(type = "string | null")]
#[arg(long)] #[arg(long)]
pub target_version: Option<VersionRange>, pub target_version: Option<VersionRange>,
#[arg(long)]
pub include_prerelease: Option<bool>,
#[arg(long = "id")] #[arg(long = "id")]
server_id: Option<String>, server_id: Option<String>,
#[ts(type = "string | null")] #[ts(type = "string | null")]
@@ -158,7 +156,6 @@ pub async fn get_version(
GetOsVersionParams { GetOsVersionParams {
source_version: source, source_version: source,
target_version: target, target_version: target,
include_prerelease,
server_id, server_id,
platform, platform,
device_info, device_info,
@@ -166,9 +163,6 @@ pub async fn get_version(
) -> Result<BTreeMap<Version, OsVersionInfo>, Error> { ) -> Result<BTreeMap<Version, OsVersionInfo>, Error> {
let source = source.or_else(|| device_info.as_ref().map(|d| d.os.version.clone())); let source = source.or_else(|| device_info.as_ref().map(|d| d.os.version.clone()));
let platform = platform.or_else(|| device_info.as_ref().map(|d| d.os.platform.clone())); let platform = platform.or_else(|| device_info.as_ref().map(|d| d.os.platform.clone()));
let include_prerelease = include_prerelease
.or_else(|| source.as_ref().map(|s| !s.prerelease().is_empty()))
.unwrap_or(cfg!(feature = "dev"));
if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, &platform) { if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, &platform) {
let created_at = Utc::now(); let created_at = Utc::now();
@@ -192,10 +186,9 @@ pub async fn get_version(
.into_iter() .into_iter()
.map(|(v, i)| i.de().map(|i| (v, i))) .map(|(v, i)| i.de().map(|i| (v, i)))
.filter_ok(|(version, info)| { .filter_ok(|(version, info)| {
(version.prerelease().is_empty() || include_prerelease) platform
&& platform .as_ref()
.as_ref() .map_or(true, |p| info.squashfs.contains_key(p))
.map_or(true, |p| info.squashfs.contains_key(p))
&& version.satisfies(&target) && version.satisfies(&target)
&& source && source
.as_ref() .as_ref()

View File

@@ -7,13 +7,14 @@ import { Drop, splitCommand } from "../util"
import * as cp from "child_process" import * as cp from "child_process"
import * as fs from "node:fs/promises" import * as fs from "node:fs/promises"
import { Mounts } from "./Mounts" import { Mounts } from "./Mounts"
import { DaemonCommandType } from "./Daemons"
export class CommandController<Manifest extends T.SDKManifest> extends Drop { export class CommandController<Manifest extends T.SDKManifest> extends Drop {
private constructor( private constructor(
readonly runningAnswer: Promise<unknown>, readonly runningAnswer: Promise<null>,
private state: { exited: boolean }, private state: { exited: boolean },
private readonly subcontainer: SubContainer<Manifest>, private readonly subcontainer: SubContainer<Manifest>,
private process: cp.ChildProcess, private process: cp.ChildProcess | AbortController,
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT, readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
) { ) {
super() super()
@@ -22,25 +23,39 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
return async ( return async (
effects: T.Effects, effects: T.Effects,
subcontainer: SubContainer<Manifest>, subcontainer: SubContainer<Manifest>,
command: T.CommandType, exec: DaemonCommandType,
options: {
// Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms
sigtermTimeout?: number
runAsInit?: boolean
env?:
| {
[variable: string]: string
}
| undefined
cwd?: string | undefined
user?: string | undefined
onStdout?: (chunk: Buffer | string | any) => void
onStderr?: (chunk: Buffer | string | any) => void
},
) => { ) => {
try { try {
if ("fn" in exec) {
const abort = new AbortController()
const cell: { ctrl: CommandController<Manifest> } = {
ctrl: new CommandController(
exec.fn(subcontainer, abort).then(async (command) => {
if (command && !abort.signal.aborted) {
Object.assign(
cell.ctrl,
await CommandController.of<Manifest>()(
effects,
subcontainer,
command,
),
)
return await cell.ctrl.runningAnswer
} else {
cell.ctrl.state.exited = true
}
return null
}),
{ exited: false },
subcontainer,
abort,
exec.sigtermTimeout,
),
}
return cell.ctrl
}
let commands: string[] let commands: string[]
if (T.isUseEntrypoint(command)) { if (T.isUseEntrypoint(exec.command)) {
const imageMeta: T.ImageMetadata = await fs const imageMeta: T.ImageMetadata = await fs
.readFile(`/media/startos/images/${subcontainer.imageId}.json`, { .readFile(`/media/startos/images/${subcontainer.imageId}.json`, {
encoding: "utf8", encoding: "utf8",
@@ -49,24 +64,24 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
.then(JSON.parse) .then(JSON.parse)
commands = imageMeta.entrypoint ?? [] commands = imageMeta.entrypoint ?? []
commands = commands.concat( commands = commands.concat(
...(command.overridCmd ?? imageMeta.cmd ?? []), ...(exec.command.overridCmd ?? imageMeta.cmd ?? []),
) )
} else commands = splitCommand(command) } else commands = splitCommand(exec.command)
let childProcess: cp.ChildProcess let childProcess: cp.ChildProcess
if (options.runAsInit) { if (exec.runAsInit) {
childProcess = await subcontainer.launch(commands, { childProcess = await subcontainer.launch(commands, {
env: options.env, env: exec.env,
}) })
} else { } else {
childProcess = await subcontainer.spawn(commands, { childProcess = await subcontainer.spawn(commands, {
env: options.env, env: exec.env,
stdio: options.onStdout || options.onStderr ? "pipe" : "inherit", stdio: exec.onStdout || exec.onStderr ? "pipe" : "inherit",
}) })
} }
if (options.onStdout) childProcess.stdout?.on("data", options.onStdout) if (exec.onStdout) childProcess.stdout?.on("data", exec.onStdout)
if (options.onStderr) childProcess.stderr?.on("data", options.onStderr) if (exec.onStderr) childProcess.stderr?.on("data", exec.onStderr)
const state = { exited: false } const state = { exited: false }
const answer = new Promise<null>((resolve, reject) => { const answer = new Promise<null>((resolve, reject) => {
@@ -98,7 +113,7 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
state, state,
subcontainer, subcontainer,
childProcess, childProcess,
options.sigtermTimeout, exec.sigtermTimeout,
) )
} catch (e) { } catch (e) {
await subcontainer.destroy() await subcontainer.destroy()
@@ -112,10 +127,22 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
this.term() this.term()
}, timeout) }, timeout)
try { try {
return await this.runningAnswer 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 { } finally {
if (!this.state.exited) { if (!this.state.exited) {
this.process.kill("SIGKILL") if (this.process instanceof AbortController) this.process.abort()
else this.process.kill("SIGKILL")
} }
await this.subcontainer.destroy() await this.subcontainer.destroy()
} }
@@ -123,9 +150,12 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) { async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) {
try { try {
if (!this.state.exited) { if (!this.state.exited) {
if (this.process instanceof AbortController) return this.process.abort()
if (signal !== "SIGKILL") { if (signal !== "SIGKILL") {
setTimeout(() => { setTimeout(() => {
if (!this.state.exited) this.process.kill("SIGKILL") if (this.process instanceof AbortController) this.process.abort()
else this.process.kill("SIGKILL")
}, timeout) }, timeout)
} }
if (!this.process.kill(signal)) { if (!this.process.kill(signal)) {
@@ -135,7 +165,18 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
} }
} }
await this.runningAnswer 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 { } finally {
await this.subcontainer.destroy() await this.subcontainer.destroy()
} }

View File

@@ -7,6 +7,7 @@ import {
SubContainerRc, SubContainerRc,
} from "../util/SubContainer" } from "../util/SubContainer"
import { CommandController } from "./CommandController" import { CommandController } from "./CommandController"
import { DaemonCommandType } from "./Daemons"
import { Oneshot } from "./Oneshot" import { Oneshot } from "./Oneshot"
const TIMEOUT_INCREMENT_MS = 1000 const TIMEOUT_INCREMENT_MS = 1000
@@ -20,11 +21,11 @@ export class Daemon<Manifest extends T.SDKManifest> extends Drop {
private commandController: CommandController<Manifest> | null = null private commandController: CommandController<Manifest> | null = null
private shouldBeRunning = false private shouldBeRunning = false
protected exitedSuccess = false protected exitedSuccess = false
private onExitFns: ((success: boolean) => void)[] = []
protected constructor( protected constructor(
private subcontainer: SubContainer<Manifest>, private subcontainer: SubContainer<Manifest>,
private startCommand: () => Promise<CommandController<Manifest>>, private startCommand: (() => Promise<CommandController<Manifest>>) | null,
readonly oneshot: boolean = false, readonly oneshot: boolean = false,
protected onExitSuccessFns: (() => void)[] = [],
) { ) {
super() super()
} }
@@ -35,29 +36,13 @@ export class Daemon<Manifest extends T.SDKManifest> extends Drop {
return async ( return async (
effects: T.Effects, effects: T.Effects,
subcontainer: SubContainer<Manifest>, subcontainer: SubContainer<Manifest>,
command: T.CommandType, exec: DaemonCommandType | null,
options: {
runAsInit?: boolean
env?:
| {
[variable: string]: string
}
| undefined
cwd?: string | undefined
user?: string | undefined
onStdout?: (chunk: Buffer | string | any) => void
onStderr?: (chunk: Buffer | string | any) => void
sigtermTimeout?: number
},
) => { ) => {
if (subcontainer.isOwned()) subcontainer = subcontainer.rc() if (subcontainer.isOwned()) subcontainer = subcontainer.rc()
const startCommand = () => const startCommand = exec
CommandController.of<Manifest>()( ? () =>
effects, CommandController.of<Manifest>()(effects, subcontainer.rc(), exec)
subcontainer.rc(), : null
command,
options,
)
return new Daemon(subcontainer, startCommand) return new Daemon(subcontainer, startCommand)
} }
} }
@@ -66,35 +51,35 @@ export class Daemon<Manifest extends T.SDKManifest> extends Drop {
return return
} }
this.shouldBeRunning = true this.shouldBeRunning = true
this.exitedSuccess = false
let timeoutCounter = 0 let timeoutCounter = 0
;(async () => { ;(async () => {
while (this.shouldBeRunning) { while (this.startCommand && this.shouldBeRunning) {
if (this.commandController) if (this.commandController)
await this.commandController await this.commandController
.term({}) .term({})
.catch((err) => console.error(err)) .catch((err) => console.error(err))
this.commandController = await this.startCommand() try {
if ( this.commandController = await this.startCommand()
(await this.commandController.wait().then( const success = await this.commandController.wait().then(
(_) => true, (_) => true,
(err) => { (err) => {
console.error(err) console.error(err)
return false return false
}, },
)) && )
this.oneshot for (const fn of this.onExitFns) {
) {
for (const fn of this.onExitSuccessFns) {
try { try {
fn() fn(success)
} catch (e) { } catch (e) {
console.error("EXIT_SUCCESS handler", e) console.error("EXIT handler", e)
} }
} }
this.onExitSuccessFns = [] if (success && this.oneshot) {
this.exitedSuccess = true this.exitedSuccess = true
break break
}
} catch (e) {
console.error(e)
} }
await new Promise((resolve) => setTimeout(resolve, timeoutCounter)) await new Promise((resolve) => setTimeout(resolve, timeoutCounter))
timeoutCounter += TIMEOUT_INCREMENT_MS timeoutCounter += TIMEOUT_INCREMENT_MS
@@ -115,15 +100,20 @@ export class Daemon<Manifest extends T.SDKManifest> extends Drop {
timeout?: number | undefined timeout?: number | undefined
}) { }) {
this.shouldBeRunning = false this.shouldBeRunning = false
this.exitedSuccess = false
await this.commandController await this.commandController
?.term({ ...termOptions }) ?.term({ ...termOptions })
.catch((e) => console.error(asError(e))) .catch((e) => console.error(asError(e)))
this.commandController = null this.commandController = null
this.onExitFns = []
await this.subcontainer.destroy() await this.subcontainer.destroy()
} }
subcontainerRc(): SubContainerRc<Manifest> { subcontainerRc(): SubContainerRc<Manifest> {
return this.subcontainer.rc() return this.subcontainer.rc()
} }
onExit(fn: (success: boolean) => void) {
this.onExitFns.push(fn)
}
onDrop(): void { onDrop(): void {
this.stop().catch((e) => console.error(asError(e))) this.stop().catch((e) => console.error(asError(e)))
} }

View File

@@ -4,8 +4,7 @@ import { HealthCheckResult } from "../health/checkFns"
import { Trigger } from "../trigger" import { Trigger } from "../trigger"
import * as T from "../../../base/lib/types" import * as T from "../../../base/lib/types"
import { Mounts } from "./Mounts" import { SubContainer } from "../util/SubContainer"
import { MountOptions, SubContainer } from "../util/SubContainer"
import { promisify } from "node:util" import { promisify } from "node:util"
import * as CP from "node:child_process" import * as CP from "node:child_process"
@@ -50,20 +49,40 @@ export type Ready = {
trigger?: Trigger trigger?: Trigger
} }
type NewDaemonParams<Manifest extends T.SDKManifest> = { export type ExecCommandOptions = {
/** The command line command to start the daemon */
command: T.CommandType command: T.CommandType
/** Information about the subcontainer in which the daemon runs */ // Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms
subcontainer: SubContainer<Manifest>
runAsInit?: boolean
env?: Record<string, string>
cwd?: string
user?: string
sigtermTimeout?: number sigtermTimeout?: number
runAsInit?: boolean
env?:
| {
[variable: string]: string
}
| undefined
cwd?: string | undefined
user?: string | undefined
onStdout?: (chunk: Buffer | string | any) => void onStdout?: (chunk: Buffer | string | any) => void
onStderr?: (chunk: Buffer | string | any) => void onStderr?: (chunk: Buffer | string | any) => void
} }
export type ExecFnOptions = {
fn: (
subcontainer: SubContainer<Manifest>,
abort: AbortController,
) => Promise<ExecCommandOptions | null>
// Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms
sigtermTimeout?: number
}
export type DaemonCommandType = ExecCommandOptions | ExecFnOptions
type NewDaemonParams<Manifest extends T.SDKManifest> = {
/** What to run as the daemon: either an async fn or a commandline command to run in the subcontainer */
exec: DaemonCommandType | null
/** Information about the subcontainer in which the daemon runs */
subcontainer: SubContainer<Manifest>
}
type AddDaemonParams< type AddDaemonParams<
Manifest extends T.SDKManifest, Manifest extends T.SDKManifest,
Ids extends string, Ids extends string,
@@ -84,6 +103,7 @@ type AddOneshotParams<
Ids extends string, Ids extends string,
Id extends string, Id extends string,
> = NewDaemonParams<Manifest> & { > = NewDaemonParams<Manifest> & {
exec: DaemonCommandType
/** An array of IDs of prior daemons whose successful initializations are required before this daemon will initialize */ /** An array of IDs of prior daemons whose successful initializations are required before this daemon will initialize */
requires: Exclude<Ids, Id>[] requires: Exclude<Ids, Id>[]
} }
@@ -172,10 +192,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
: Daemon.of<Manifest>()( : Daemon.of<Manifest>()(
this.effects, this.effects,
options.subcontainer, options.subcontainer,
options.command, options.exec,
{
...options,
},
) )
const healthDaemon = new HealthDaemon( const healthDaemon = new HealthDaemon(
daemon, daemon,
@@ -221,10 +238,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
const daemon = Oneshot.of<Manifest>()( const daemon = Oneshot.of<Manifest>()(
this.effects, this.effects,
options.subcontainer, options.subcontainer,
options.command, options.exec,
{
...options,
},
) )
const healthDaemon = new HealthDaemon<Manifest>( const healthDaemon = new HealthDaemon<Manifest>(
daemon, daemon,

View File

@@ -90,15 +90,23 @@ export class HealthDaemon<Manifest extends SDKManifest> {
this.healthCheckCleanup?.() this.healthCheckCleanup?.()
} }
private async setupHealthCheck() { private async setupHealthCheck() {
if (this.ready === "EXIT_SUCCESS") { const daemon = await this.daemon
const daemon = await this.daemon daemon.onExit((success) => {
if (daemon.isOneshot()) { if (success && this.ready === "EXIT_SUCCESS") {
daemon.onExitSuccess(() => this.setHealth({ result: "success", message: null })
this.setHealth({ result: "success", message: null }), } else if (!success) {
) this.setHealth({
result: "failure",
message: `${this.id} daemon crashed`,
})
} else if (!daemon.isOneshot()) {
this.setHealth({
result: "failure",
message: `${this.id} daemon exited`,
})
} }
return })
} if (this.ready === "EXIT_SUCCESS") return
if (this.healthCheckCleanup) return if (this.healthCheckCleanup) return
const trigger = (this.ready.trigger ?? defaultTrigger)(() => ({ const trigger = (this.ready.trigger ?? defaultTrigger)(() => ({
lastResult: this._health.result, lastResult: this._health.result,

View File

@@ -2,6 +2,7 @@ import * as T from "../../../base/lib/types"
import { SubContainer, SubContainerOwned } from "../util/SubContainer" import { SubContainer, SubContainerOwned } from "../util/SubContainer"
import { CommandController } from "./CommandController" import { CommandController } from "./CommandController"
import { Daemon } from "./Daemon" 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 * This is a wrapper around CommandController that has a state of off, where the command shouldn't be running
@@ -14,37 +15,14 @@ export class Oneshot<Manifest extends T.SDKManifest> extends Daemon<Manifest> {
return async ( return async (
effects: T.Effects, effects: T.Effects,
subcontainer: SubContainer<Manifest>, subcontainer: SubContainer<Manifest>,
command: T.CommandType, exec: DaemonCommandType | null,
options: {
env?:
| {
[variable: string]: string
}
| undefined
cwd?: string | undefined
user?: string | undefined
onStdout?: (chunk: Buffer | string | any) => void
onStderr?: (chunk: Buffer | string | any) => void
sigtermTimeout?: number
},
) => { ) => {
if (subcontainer.isOwned()) subcontainer = subcontainer.rc() if (subcontainer.isOwned()) subcontainer = subcontainer.rc()
const startCommand = () => const startCommand = exec
CommandController.of<Manifest>()( ? () =>
effects, CommandController.of<Manifest>()(effects, subcontainer.rc(), exec)
subcontainer.rc(), : null
command, return new Oneshot(subcontainer, startCommand, true)
options,
)
return new Oneshot(subcontainer, startCommand, true, [])
}
}
onExitSuccess(fn: () => void) {
if (this.exitedSuccess) {
fn()
} else {
this.onExitSuccessFns.push(fn)
} }
} }
} }

View File

@@ -92,6 +92,7 @@ export interface SubContainer<
command: string[], command: string[],
options?: CommandOptions & ExecOptions, options?: CommandOptions & ExecOptions,
timeoutMs?: number | null, timeoutMs?: number | null,
abort?: AbortController,
): Promise<{ ): Promise<{
throw: () => { stdout: string | Buffer; stderr: string | Buffer } throw: () => { stdout: string | Buffer; stderr: string | Buffer }
exitCode: number | null exitCode: number | null
@@ -111,6 +112,7 @@ export interface SubContainer<
command: string[], command: string[],
options?: CommandOptions & ExecOptions, options?: CommandOptions & ExecOptions,
timeoutMs?: number | null, timeoutMs?: number | null,
abort?: AbortController,
): Promise<{ ): Promise<{
stdout: string | Buffer stdout: string | Buffer
stderr: string | Buffer stderr: string | Buffer
@@ -378,6 +380,7 @@ export class SubContainerOwned<
command: string[], command: string[],
options?: CommandOptions & ExecOptions, options?: CommandOptions & ExecOptions,
timeoutMs: number | null = 30000, timeoutMs: number | null = 30000,
abort?: AbortController,
): Promise<{ ): Promise<{
throw: () => { stdout: string | Buffer; stderr: string | Buffer } throw: () => { stdout: string | Buffer; stderr: string | Buffer }
exitCode: number | null exitCode: number | null
@@ -417,6 +420,7 @@ export class SubContainerOwned<
], ],
options || {}, options || {},
) )
abort?.signal.addEventListener("abort", () => child.kill("SIGKILL"))
if (options?.input) { if (options?.input) {
await new Promise<null>((resolve, reject) => { await new Promise<null>((resolve, reject) => {
try { try {
@@ -489,12 +493,15 @@ export class SubContainerOwned<
async execFail( async execFail(
command: string[], command: string[],
options?: CommandOptions & ExecOptions, options?: CommandOptions & ExecOptions,
timeoutMs: number | null = 30000, timeoutMs?: number | null,
abort?: AbortController,
): Promise<{ ): Promise<{
stdout: string | Buffer stdout: string | Buffer
stderr: string | Buffer stderr: string | Buffer
}> { }> {
return this.exec(command, options, timeoutMs).then((res) => res.throw()) return this.exec(command, options, timeoutMs, abort).then((res) =>
res.throw(),
)
} }
async launch( async launch(
@@ -711,7 +718,8 @@ export class SubContainerRc<
async exec( async exec(
command: string[], command: string[],
options?: CommandOptions & ExecOptions, options?: CommandOptions & ExecOptions,
timeoutMs: number | null = 30000, timeoutMs?: number | null,
abort?: AbortController,
): Promise<{ ): Promise<{
throw: () => { stdout: string | Buffer; stderr: string | Buffer } throw: () => { stdout: string | Buffer; stderr: string | Buffer }
exitCode: number | null exitCode: number | null
@@ -719,7 +727,7 @@ export class SubContainerRc<
stdout: string | Buffer stdout: string | Buffer
stderr: string | Buffer stderr: string | Buffer
}> { }> {
return this.subcontainer.exec(command, options, timeoutMs) return this.subcontainer.exec(command, options, timeoutMs, abort)
} }
/** /**
@@ -732,12 +740,13 @@ export class SubContainerRc<
async execFail( async execFail(
command: string[], command: string[],
options?: CommandOptions & ExecOptions, options?: CommandOptions & ExecOptions,
timeoutMs: number | null = 30000, timeoutMs?: number | null,
abort?: AbortController,
): Promise<{ ): Promise<{
stdout: string | Buffer stdout: string | Buffer
stderr: string | Buffer stderr: string | Buffer
}> { }> {
return this.subcontainer.execFail(command, options, timeoutMs) return this.subcontainer.execFail(command, options, timeoutMs, abort)
} }
async launch( async launch(

View File

@@ -1,12 +1,12 @@
{ {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.26", "version": "0.4.0-beta.27",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.26", "version": "0.4.0-beta.27",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^3.0.0", "@iarna/toml": "^3.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.26", "version": "0.4.0-beta.27",
"description": "Software development kit to facilitate packaging services for StartOS", "description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js", "main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts", "types": "./package/lib/index.d.ts",