Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major

This commit is contained in:
Aiden McClelland
2025-04-07 14:00:42 -06:00
77 changed files with 1474 additions and 716 deletions

View File

@@ -47,7 +47,12 @@ import { GetSystemSmtp } from "./util"
import { nullIfEmpty } from "./util"
import { getServiceInterface, getServiceInterfaces } from "./util"
import { getStore } from "./store/getStore"
import { CommandOptions, MountOptions, SubContainer } from "./util/SubContainer"
import {
CommandOptions,
ExitError,
MountOptions,
SubContainer,
} from "./util/SubContainer"
import { splitCommand } from "./util"
import { Mounts } from "./mainFn/Mounts"
import { setupDependencies } from "../../base/lib/dependencies/setupDependencies"
@@ -104,7 +109,10 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
| "getHostInfo"
type MainUsedEffects = "setMainStatus" | "setHealth"
type CallbackEffects = "constRetry" | "clearCallbacks"
type AlreadyExposed = "getSslCertificate" | "getSystemSmtp"
type AlreadyExposed =
| "getSslCertificate"
| "getSystemSmtp"
| "getContainerIp"
// prettier-ignore
type StartSdkEffectWrapper = {
@@ -123,7 +131,6 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
getServicePortForward: (effects, ...args) =>
effects.getServicePortForward(...args),
clearBindings: (effects, ...args) => effects.clearBindings(...args),
getContainerIp: (effects, ...args) => effects.getContainerIp(...args),
getOsIp: (effects, ...args) => effects.getOsIp(...args),
getSslKey: (effects, ...args) => effects.getSslKey(...args),
setDataVersion: (effects, ...args) => effects.setDataVersion(...args),
@@ -191,7 +198,61 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
opts: { packageId: PackageId },
) => getServiceInterfaces(effects, opts),
},
getContainerIp: (
effects: T.Effects,
options: Omit<
Parameters<T.Effects["getContainerIp"]>[0],
"callback"
> = {},
) => {
async function* watch() {
while (true) {
let callback: () => void = () => {}
const waitForNext = new Promise<void>((resolve) => {
callback = resolve
})
yield await effects.getContainerIp({ ...options, callback })
await waitForNext
}
}
return {
const: () =>
effects.getContainerIp({
...options,
callback:
effects.constRetry &&
(() => effects.constRetry && effects.constRetry()),
}),
once: () => effects.getContainerIp(options),
watch,
onChange: (
callback: (
value: string | null,
error?: Error,
) => void | Promise<void>,
) => {
;(async () => {
for await (const value of watch()) {
try {
await callback(value)
} catch (e) {
console.error(
"callback function threw an error @ getContainerIp.onChange",
e,
)
}
}
})()
.catch((e) => callback(null, e))
.catch((e) =>
console.error(
"callback function threw an error @ getContainerIp.onChange",
e,
),
)
},
}
},
store: {
get: <E extends Effects, StoreValue = unknown>(
effects: E,
@@ -230,7 +291,7 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
},
command: T.CommandType,
options: CommandOptions & {
mounts?: { mountpoint: string; options: MountOptions }[]
mounts: Mounts<Manifest>
},
/**
* A name to use to refer to the ephemeral subcontainer for debugging purposes
@@ -527,7 +588,10 @@ export class StartSdk<Manifest extends T.SDKManifest, Store> {
})
* ```
*/
setupInstall: (fn: InstallFn<Manifest, Store>) => Install.of(fn),
setupInstall: (
fn: InstallFn<Manifest, Store>,
preFn?: InstallFn<Manifest, Store>,
) => Install.of(fn, preFn),
/**
* @description Use this function to determine how this service will be hosted and served. The function executes on service install, service update, and inputSpec save.
*
@@ -1076,7 +1140,7 @@ export async function runCommand<Manifest extends T.SDKManifest>(
image: { imageId: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean },
command: T.CommandType,
options: CommandOptions & {
mounts?: { mountpoint: string; options: MountOptions }[]
mounts: Mounts<Manifest>
},
name?: string,
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> {
@@ -1094,7 +1158,7 @@ export async function runCommand<Manifest extends T.SDKManifest>(
return SubContainer.with(
effects,
image,
options.mounts || [],
options.mounts.build(),
name ||
commands
.map((c) => {
@@ -1105,6 +1169,13 @@ export async function runCommand<Manifest extends T.SDKManifest>(
}
})
.join(" "),
(subcontainer) => subcontainer.exec(commands),
async (subcontainer) => {
const res = await subcontainer.exec(commands)
if (res.exitCode || res.exitSignal) {
throw new ExitError(commands[0], res)
} else {
return res
}
},
)
}

View File

@@ -3,6 +3,7 @@ import { ExtendedVersion } from "../../../base/lib/exver"
import { UpdateServiceInterfaces } from "../../../base/lib/interfaces/setupInterfaces"
import { ExposedStorePaths } from "../../../base/lib/types"
import * as T from "../../../base/lib/types"
import { StorePath } from "../util"
import { VersionGraph } from "../version/VersionGraph"
import { Install } from "./setupInstall"
import { Uninstall } from "./setupUninstall"
@@ -16,6 +17,7 @@ export function setupInit<Manifest extends T.SDKManifest, Store>(
effects: T.Effects
}) => Promise<null | void | undefined>,
actions: Actions<Store, any>,
initStore: Store,
exposedStore: ExposedStorePaths,
): {
packageInit: T.ExpectedExports.packageInit
@@ -53,6 +55,14 @@ export function setupInit<Manifest extends T.SDKManifest, Store>(
}
},
containerInit: async (opts) => {
const prev = await opts.effects.getDataVersion()
if (!prev) {
await opts.effects.store.set({
path: "" as StorePath,
value: initStore,
})
await install.preInstall(opts)
}
await setServiceInterfaces({
...opts,
})

View File

@@ -4,11 +4,15 @@ export type InstallFn<Manifest extends T.SDKManifest, Store> = (opts: {
effects: T.Effects
}) => Promise<null | void | undefined>
export class Install<Manifest extends T.SDKManifest, Store> {
private constructor(readonly fn: InstallFn<Manifest, Store>) {}
private constructor(
readonly fn: InstallFn<Manifest, Store>,
readonly preFn?: InstallFn<Manifest, Store>,
) {}
static of<Manifest extends T.SDKManifest, Store>(
fn: InstallFn<Manifest, Store>,
preFn?: InstallFn<Manifest, Store>,
) {
return new Install(fn)
return new Install(fn, preFn)
}
async install({ effects }: Parameters<T.ExpectedExports.packageInit>[0]) {
@@ -16,10 +20,18 @@ export class Install<Manifest extends T.SDKManifest, Store> {
effects,
})
}
async preInstall({ effects }: Parameters<T.ExpectedExports.packageInit>[0]) {
this.preFn &&
(await this.preFn({
effects,
}))
}
}
export function setupInstall<Manifest extends T.SDKManifest, Store>(
fn: InstallFn<Manifest, Store>,
preFn?: InstallFn<Manifest, Store>,
) {
return Install.of(fn)
return Install.of(fn, preFn)
}

View File

@@ -172,7 +172,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
daemon,
daemonIndex,
options.requires
.map((x) => this.ids.indexOf(id as any))
.map((x) => this.ids.indexOf(x))
.filter((x) => x >= 0)
.map((id) => this.healthDaemons[id]),
id,

View File

@@ -467,3 +467,25 @@ export type MountOptionsBackup = {
function wait(time: number) {
return new Promise((resolve) => setTimeout(resolve, time))
}
export class ExitError extends Error {
constructor(
readonly command: string,
readonly result: {
exitCode: number | null
exitSignal: T.Signals | null
stdout: string | Buffer
stderr: string | Buffer
},
) {
let message: string
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)
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "@start9labs/start-sdk",
"version": "0.3.6-beta.18",
"version": "0.3.6-beta.20",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@start9labs/start-sdk",
"version": "0.3.6-beta.18",
"version": "0.3.6-beta.20",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^2.2.5",

View File

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