mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
* help ios downlaod .crt and add begin add masked for addresses * only require and show CA for public domain if addSsl * fix type and revert i18n const * feat: add address masking and adjust design (#3088) * feat: add address masking and adjust design * update lockfile * chore: move eye button to actions * chore: refresh notifications and handle action error * static width for health check name --------- Co-authored-by: Matt Hill <mattnine@protonmail.com> * hide certificate authorities tab * alpha.17 * add waiting health check status * remove "on" from waiting message * reject on abort in `.watch` * id migration: nostr -> nostr-rs-relay * health check waiting state * use interface type for launch button * better wording for masked * cleaner * sdk improvements * fix type error * fix notification badge issue --------- Co-authored-by: Alex Inkin <alexander@inkin.ru> Co-authored-by: Aiden McClelland <me@drbonez.dev>
346 lines
11 KiB
TypeScript
346 lines
11 KiB
TypeScript
import { polyfillEffects } from "./polyfillEffects"
|
|
import { DockerProcedureContainer } from "./DockerProcedureContainer"
|
|
import { SystemForEmbassy } from "."
|
|
import { T, utils } from "@start9labs/start-sdk"
|
|
import { Daemon } from "@start9labs/start-sdk/package/lib/mainFn/Daemon"
|
|
import { Effects } from "../../../Models/Effects"
|
|
import { off } from "node:process"
|
|
import { CommandController } from "@start9labs/start-sdk/package/lib/mainFn/CommandController"
|
|
import { SDKManifest } from "@start9labs/start-sdk/base/lib/types"
|
|
import { SubContainerRc } from "@start9labs/start-sdk/package/lib/util/SubContainer"
|
|
|
|
const EMBASSY_HEALTH_INTERVAL = 15 * 1000
|
|
/**
|
|
* We wanted something to represent what the main loop is doing, and
|
|
* in this case it used to run the properties, health, and the docker/ js main.
|
|
* Also, this has an ability to clean itself up too if need be.
|
|
*/
|
|
export class MainLoop {
|
|
private subcontainerRc?: SubContainerRc<SDKManifest>
|
|
get mainSubContainerHandle() {
|
|
this.subcontainerRc =
|
|
this.subcontainerRc ??
|
|
this.mainEvent?.daemon?.subcontainerRc() ??
|
|
undefined
|
|
return this.subcontainerRc
|
|
}
|
|
private healthLoops?: {
|
|
name: string
|
|
interval: NodeJS.Timeout
|
|
}[]
|
|
|
|
private mainEvent?: {
|
|
daemon: Daemon<SDKManifest>
|
|
}
|
|
|
|
private constructor(
|
|
readonly system: SystemForEmbassy,
|
|
readonly effects: Effects,
|
|
) {}
|
|
|
|
static async of(
|
|
system: SystemForEmbassy,
|
|
effects: Effects,
|
|
): Promise<MainLoop> {
|
|
const res = new MainLoop(system, effects)
|
|
res.healthLoops = res.constructHealthLoops()
|
|
res.mainEvent = await res.constructMainEvent()
|
|
return res
|
|
}
|
|
|
|
private async constructMainEvent() {
|
|
const { system, effects } = this
|
|
const currentCommand: [string, ...string[]] = [
|
|
system.manifest.main.entrypoint,
|
|
...system.manifest.main.args,
|
|
]
|
|
|
|
await this.setupInterfaces(effects)
|
|
await effects.setMainStatus({ status: "running" })
|
|
const jsMain = (this.system.moduleCode as any)?.jsMain
|
|
if (jsMain) {
|
|
throw new Error("Unreachable")
|
|
}
|
|
const subcontainer = await DockerProcedureContainer.createSubContainer(
|
|
effects,
|
|
this.system.manifest.id,
|
|
this.system.manifest.main,
|
|
this.system.manifest.volumes,
|
|
`Main - ${currentCommand.join(" ")}`,
|
|
)
|
|
const daemon = await Daemon.of()(this.effects, subcontainer, {
|
|
command: currentCommand,
|
|
runAsInit: true,
|
|
env: {
|
|
TINI_SUBREAPER: "true",
|
|
},
|
|
sigtermTimeout: utils.inMs(this.system.manifest.main["sigterm-timeout"]),
|
|
})
|
|
|
|
daemon.start()
|
|
return {
|
|
daemon,
|
|
}
|
|
}
|
|
|
|
private async setupInterfaces(effects: T.Effects) {
|
|
for (const interfaceId in this.system.manifest.interfaces) {
|
|
const iface = this.system.manifest.interfaces[interfaceId]
|
|
const internalPorts = new Set<number>()
|
|
for (const port of Object.values(
|
|
iface["tor-config"]?.["port-mapping"] || {},
|
|
)) {
|
|
internalPorts.add(parseInt(port))
|
|
}
|
|
for (const port of Object.values(iface["lan-config"] || {})) {
|
|
internalPorts.add(port.internal)
|
|
}
|
|
for (const internalPort of internalPorts) {
|
|
const torConf = Object.entries(
|
|
iface["tor-config"]?.["port-mapping"] || {},
|
|
)
|
|
.map(([external, internal]) => ({
|
|
internal: parseInt(internal),
|
|
external: parseInt(external),
|
|
}))
|
|
.find((conf) => conf.internal == internalPort)
|
|
const lanConf = Object.entries(iface["lan-config"] || {})
|
|
.map(([external, conf]) => ({
|
|
external: parseInt(external),
|
|
...conf,
|
|
}))
|
|
.find((conf) => conf.internal == internalPort)
|
|
await effects.bind({
|
|
id: interfaceId,
|
|
internalPort,
|
|
preferredExternalPort: torConf?.external || internalPort,
|
|
secure: null,
|
|
addSsl: lanConf?.ssl
|
|
? {
|
|
preferredExternalPort: lanConf.external,
|
|
alpn: { specified: ["http/1.1"] },
|
|
addXForwardedHeaders: false,
|
|
}
|
|
: null,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
public async clean(options?: { timeout?: number }) {
|
|
const { mainEvent, healthLoops } = this
|
|
const main = await mainEvent
|
|
delete this.mainEvent
|
|
delete this.healthLoops
|
|
await main?.daemon
|
|
.term()
|
|
.catch((e: unknown) => console.error(`Main loop error`, utils.asError(e)))
|
|
this.effects.setMainStatus({ status: "stopped" })
|
|
if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval))
|
|
}
|
|
|
|
private constructHealthLoops() {
|
|
const { manifest } = this.system
|
|
const effects = this.effects
|
|
const start = Date.now()
|
|
return Object.entries(manifest["health-checks"]).map(
|
|
([healthId, value]) => {
|
|
effects
|
|
.setHealth({
|
|
id: healthId,
|
|
name: value.name,
|
|
result: "starting",
|
|
message: null,
|
|
})
|
|
.catch((e) => console.error(utils.asError(e)))
|
|
const interval = setInterval(async () => {
|
|
const actionProcedure = value
|
|
const timeChanged = Date.now() - start
|
|
if (actionProcedure.type === "docker") {
|
|
const subcontainer = actionProcedure.inject
|
|
? this.mainSubContainerHandle
|
|
: undefined
|
|
const commands = [
|
|
actionProcedure.entrypoint,
|
|
...actionProcedure.args,
|
|
]
|
|
const container = await DockerProcedureContainer.of(
|
|
effects,
|
|
manifest.id,
|
|
actionProcedure,
|
|
manifest.volumes,
|
|
`Health Check - ${commands.join(" ")}`,
|
|
{
|
|
subcontainer,
|
|
},
|
|
)
|
|
const env: Record<string, string> = actionProcedure.inject
|
|
? {
|
|
HOME: "/root",
|
|
}
|
|
: {}
|
|
const executed = await container.exec(commands, {
|
|
input: JSON.stringify(timeChanged),
|
|
env,
|
|
})
|
|
|
|
if (executed.exitCode === 0) {
|
|
await effects.setHealth({
|
|
id: healthId,
|
|
name: value.name,
|
|
result: "success",
|
|
message: actionProcedure["success-message"] ?? null,
|
|
})
|
|
return
|
|
}
|
|
if (executed.exitCode === 59) {
|
|
await effects.setHealth({
|
|
id: healthId,
|
|
name: value.name,
|
|
result: "disabled",
|
|
message:
|
|
executed.stderr.toString() || executed.stdout.toString(),
|
|
})
|
|
return
|
|
}
|
|
if (executed.exitCode === 60) {
|
|
await effects.setHealth({
|
|
id: healthId,
|
|
name: value.name,
|
|
result: "starting",
|
|
message:
|
|
executed.stderr.toString() || executed.stdout.toString(),
|
|
})
|
|
return
|
|
}
|
|
if (executed.exitCode === 61) {
|
|
await effects.setHealth({
|
|
id: healthId,
|
|
name: value.name,
|
|
result: "loading",
|
|
message:
|
|
executed.stderr.toString() || executed.stdout.toString(),
|
|
})
|
|
return
|
|
}
|
|
const errorMessage = executed.stderr.toString()
|
|
const message = executed.stdout.toString()
|
|
if (!!errorMessage) {
|
|
await effects.setHealth({
|
|
id: healthId,
|
|
name: value.name,
|
|
result: "failure",
|
|
message: errorMessage,
|
|
})
|
|
return
|
|
}
|
|
if (executed.exitCode && executed.exitCode > 0) {
|
|
await effects.setHealth({
|
|
id: healthId,
|
|
name: value.name,
|
|
result: "failure",
|
|
message:
|
|
executed.stderr.toString() ||
|
|
executed.stdout.toString() ||
|
|
`Program exited with code ${executed.exitCode}:`,
|
|
})
|
|
return
|
|
}
|
|
await effects.setHealth({
|
|
id: healthId,
|
|
name: value.name,
|
|
result: "success",
|
|
message,
|
|
})
|
|
return
|
|
} else {
|
|
actionProcedure
|
|
const moduleCode = await this.system.moduleCode
|
|
const method = moduleCode.health?.[healthId]
|
|
if (!method) {
|
|
await effects.setHealth({
|
|
id: healthId,
|
|
name: value.name,
|
|
result: "failure",
|
|
message: `Expecting that the js health check ${healthId} exists`,
|
|
})
|
|
return
|
|
}
|
|
|
|
const result = await method(
|
|
polyfillEffects(effects, this.system.manifest),
|
|
timeChanged,
|
|
)
|
|
|
|
if ("result" in result) {
|
|
await effects.setHealth({
|
|
id: healthId,
|
|
name: value.name,
|
|
result: "success",
|
|
message: null,
|
|
})
|
|
return
|
|
}
|
|
if ("error" in result) {
|
|
await effects.setHealth({
|
|
id: healthId,
|
|
name: value.name,
|
|
result: "failure",
|
|
message: result.error,
|
|
})
|
|
return
|
|
}
|
|
if (!("error-code" in result)) {
|
|
await effects.setHealth({
|
|
id: healthId,
|
|
name: value.name,
|
|
result: "failure",
|
|
message: `Unknown error type ${JSON.stringify(result)}`,
|
|
})
|
|
return
|
|
}
|
|
const [code, message] = result["error-code"]
|
|
if (code === 59) {
|
|
await effects.setHealth({
|
|
id: healthId,
|
|
name: value.name,
|
|
result: "disabled",
|
|
message,
|
|
})
|
|
return
|
|
}
|
|
if (code === 60) {
|
|
await effects.setHealth({
|
|
id: healthId,
|
|
name: value.name,
|
|
result: "starting",
|
|
message,
|
|
})
|
|
return
|
|
}
|
|
if (code === 61) {
|
|
await effects.setHealth({
|
|
id: healthId,
|
|
name: value.name,
|
|
result: "loading",
|
|
message,
|
|
})
|
|
return
|
|
}
|
|
|
|
await effects.setHealth({
|
|
id: healthId,
|
|
name: value.name,
|
|
result: "failure",
|
|
message: `${result["error-code"][0]}: ${result["error-code"][1]}`,
|
|
})
|
|
return
|
|
}
|
|
}, EMBASSY_HEALTH_INTERVAL)
|
|
|
|
return { name: healthId, interval }
|
|
},
|
|
)
|
|
}
|
|
}
|