mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 18:31:52 +00:00
* import marketplac preview for sideload * fix: improve state service (#2977) * fix: fix sideload DI * fix: update Angular * fix: cleanup * fix: fix version selection * Bump node version to fix build for Angular * misc fixes - update node to v22 - fix chroot-and-upgrade access to prune-images - don't self-migrate legacy packages - #2985 - move dataVersion to volume folder - remove "instructions.md" from s9pk - add "docsUrl" to manifest * version bump * include flavor when clicking view listing from updates tab * closes #2980 * fix: fix select button * bring back ssh keys * fix: drop 'portal' from all routes * fix: implement longtap action to select table rows * fix description for ssh page * replace instructions with docsLink and refactor marketplace preview * delete unused translations * fix patchdb diffing algorithm * continue refactor of marketplace lib show components * Booting StartOS instead of Setting up your server on init * misc fixes - closes #2990 - closes #2987 * fix build * docsUrl and clickable service headers * don't cleanup after update until new service install succeeds * update types * misc fixes * beta.35 * sdkversion, githash for sideload, correct logs for init, startos pubkey display * bring back reboot button on install * misc fixes * beta.36 * better handling of setup and init for websocket errors * reopen init and setup logs even on graceful closure * better logging, misc fixes * fix build * dont let package stats hang * dont show docsurl in marketplace if no docsurl * re-add needs-config * show error if init fails, shorten hover state on header icons * fix operator precedemce --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> Co-authored-by: Alex Inkin <alexander@inkin.ru> Co-authored-by: Mariusz Kogen <k0gen@pm.me>
193 lines
5.9 KiB
TypeScript
193 lines
5.9 KiB
TypeScript
import { DEFAULT_SIGTERM_TIMEOUT } from "."
|
|
import { NO_TIMEOUT, SIGTERM } from "../../../base/lib/types"
|
|
|
|
import * as T from "../../../base/lib/types"
|
|
import { SubContainer } from "../util/SubContainer"
|
|
import { Drop, splitCommand } from "../util"
|
|
import * as cp from "child_process"
|
|
import * as fs from "node:fs/promises"
|
|
import { DaemonCommandType, ExecCommandOptions, ExecFnOptions } from "./Daemons"
|
|
|
|
export class CommandController<
|
|
Manifest extends T.SDKManifest,
|
|
C extends SubContainer<Manifest> | null,
|
|
> extends Drop {
|
|
private constructor(
|
|
readonly runningAnswer: Promise<null>,
|
|
private state: { exited: boolean },
|
|
private readonly subcontainer: C,
|
|
private process: cp.ChildProcess | AbortController,
|
|
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
|
|
) {
|
|
super()
|
|
}
|
|
static of<
|
|
Manifest extends T.SDKManifest,
|
|
C extends SubContainer<Manifest> | null,
|
|
>() {
|
|
return async (
|
|
effects: T.Effects,
|
|
subcontainer: C,
|
|
exec: DaemonCommandType<Manifest, C>,
|
|
) => {
|
|
try {
|
|
if ("fn" in exec) {
|
|
const abort = new AbortController()
|
|
const cell: { ctrl: CommandController<Manifest, C> } = {
|
|
ctrl: new CommandController<Manifest, C>(
|
|
exec.fn(subcontainer, abort.signal).then(async (command) => {
|
|
if (subcontainer && command && !abort.signal.aborted) {
|
|
const newCtrl = (
|
|
await CommandController.of<
|
|
Manifest,
|
|
SubContainer<Manifest>
|
|
>()(effects, subcontainer, command as ExecCommandOptions)
|
|
).leak()
|
|
|
|
Object.assign(cell.ctrl, newCtrl)
|
|
return await cell.ctrl.runningAnswer
|
|
} else {
|
|
cell.ctrl.state.exited = true
|
|
}
|
|
return null
|
|
}),
|
|
{ exited: false },
|
|
subcontainer,
|
|
abort,
|
|
exec.sigtermTimeout,
|
|
),
|
|
}
|
|
return cell.ctrl
|
|
}
|
|
let commands: string[]
|
|
if (T.isUseEntrypoint(exec.command)) {
|
|
const imageMeta: T.ImageMetadata = await fs
|
|
.readFile(`/media/startos/images/${subcontainer!.imageId}.json`, {
|
|
encoding: "utf8",
|
|
})
|
|
.catch(() => "{}")
|
|
.then(JSON.parse)
|
|
commands = imageMeta.entrypoint ?? []
|
|
commands = commands.concat(
|
|
...(exec.command.overridCmd ?? imageMeta.cmd ?? []),
|
|
)
|
|
} else commands = splitCommand(exec.command)
|
|
|
|
let childProcess: cp.ChildProcess
|
|
if (exec.runAsInit) {
|
|
childProcess = await subcontainer!.launch(commands, {
|
|
env: exec.env,
|
|
})
|
|
} else {
|
|
childProcess = await subcontainer!.spawn(commands, {
|
|
env: exec.env,
|
|
stdio: exec.onStdout || exec.onStderr ? "pipe" : "inherit",
|
|
})
|
|
}
|
|
|
|
if (exec.onStdout) childProcess.stdout?.on("data", exec.onStdout)
|
|
if (exec.onStderr) childProcess.stderr?.on("data", exec.onStderr)
|
|
|
|
const state = { exited: false }
|
|
const answer = new Promise<null>((resolve, reject) => {
|
|
childProcess.on("exit", (code) => {
|
|
state.exited = true
|
|
if (
|
|
code === 0 ||
|
|
code === 143 ||
|
|
(code === null && childProcess.signalCode == "SIGTERM")
|
|
) {
|
|
return resolve(null)
|
|
}
|
|
if (code) {
|
|
return reject(
|
|
new Error(`${commands[0]} exited with code ${code}`),
|
|
)
|
|
} else {
|
|
return reject(
|
|
new Error(
|
|
`${commands[0]} exited with signal ${childProcess.signalCode}`,
|
|
),
|
|
)
|
|
}
|
|
})
|
|
})
|
|
|
|
return new CommandController<Manifest, C>(
|
|
answer,
|
|
state,
|
|
subcontainer,
|
|
childProcess,
|
|
exec.sigtermTimeout,
|
|
)
|
|
} catch (e) {
|
|
await subcontainer?.destroy()
|
|
throw e
|
|
}
|
|
}
|
|
}
|
|
async wait({ timeout = NO_TIMEOUT } = {}) {
|
|
if (timeout > 0)
|
|
setTimeout(() => {
|
|
this.term()
|
|
}, timeout)
|
|
try {
|
|
if (timeout > 0 && this.process instanceof AbortController)
|
|
await Promise.race([
|
|
this.runningAnswer,
|
|
new Promise((_, reject) =>
|
|
setTimeout(
|
|
() =>
|
|
reject(new Error("Timed out waiting for js command to exit")),
|
|
timeout * 2,
|
|
),
|
|
),
|
|
])
|
|
else await this.runningAnswer
|
|
} finally {
|
|
if (!this.state.exited) {
|
|
if (this.process instanceof AbortController) this.process.abort()
|
|
else this.process.kill("SIGKILL")
|
|
}
|
|
await this.subcontainer?.destroy()
|
|
}
|
|
}
|
|
async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) {
|
|
try {
|
|
if (!this.state.exited) {
|
|
if (this.process instanceof AbortController) return this.process.abort()
|
|
|
|
if (signal !== "SIGKILL") {
|
|
setTimeout(() => {
|
|
if (this.process instanceof AbortController) this.process.abort()
|
|
else this.process.kill("SIGKILL")
|
|
}, timeout)
|
|
}
|
|
if (!this.process.kill(signal)) {
|
|
console.error(
|
|
`failed to send signal ${signal} to pid ${this.process.pid}`,
|
|
)
|
|
}
|
|
}
|
|
|
|
if (this.process instanceof AbortController)
|
|
await Promise.race([
|
|
this.runningAnswer,
|
|
new Promise((_, reject) =>
|
|
setTimeout(
|
|
() =>
|
|
reject(new Error("Timed out waiting for js command to exit")),
|
|
timeout * 2,
|
|
),
|
|
),
|
|
])
|
|
else await this.runningAnswer
|
|
} finally {
|
|
await this.subcontainer?.destroy()
|
|
}
|
|
}
|
|
onDrop(): void {
|
|
this.term().catch(console.error)
|
|
}
|
|
}
|