Refactor/sdk init (#2947)

* fixes for main

* refactor package initialization

* fixes from testing

* more fixes

* beta.21

* do not use instanceof

* closes #2921

* beta22

* allow disabling kiosk

* migration

* fix /etc/shadow

* actionRequest -> task

* beta.23
This commit is contained in:
Aiden McClelland
2025-05-21 10:24:37 -06:00
committed by GitHub
parent 46fd01c264
commit 44560c8da8
237 changed files with 1827 additions and 98800 deletions

View File

@@ -1,3 +1,4 @@
import { ExtendedVersion, VersionRange } from "./exver"
import {
ActionId,
ActionInput,
@@ -12,7 +13,7 @@ import {
Host,
ExportServiceInterfaceParams,
ServiceInterface,
RequestActionParams,
CreateTaskParams,
MainStatus,
} from "./osBindings"
import {
@@ -50,10 +51,8 @@ export type Effects = {
actionId: ActionId
input?: Input
}): Promise<ActionResult | null>
request<Input extends Record<string, unknown>>(
options: RequestActionParams,
): Promise<null>
clearRequests(
createTask(options: CreateTaskParams): Promise<null>
clearTasks(
options: { only: string[] } | { except: string[] },
): Promise<null>
}
@@ -168,7 +167,7 @@ export type Effects = {
}) => Promise<string>
/** sets the version that this service's data has been migrated to */
setDataVersion(options: { version: string }): Promise<null>
setDataVersion(options: { version: string | null }): Promise<null>
/** returns the version that this service's data has been migrated to */
getDataVersion(): Promise<string | null>

View File

@@ -1,6 +1,6 @@
import * as T from "../types"
import * as IST from "../actions/input/inputSpecTypes"
import { Action } from "./setupActions"
import { Action, ActionInfo } from "./setupActions"
import { ExtractInputSpecType } from "./input/builder/inputSpec"
export type RunActionInput<Input> =
@@ -45,45 +45,41 @@ export const runAction = async <
})
}
}
type GetActionInputType<A extends Action<T.ActionId, any>> =
type GetActionInputType<A extends ActionInfo<T.ActionId, any>> =
A extends Action<T.ActionId, infer I> ? ExtractInputSpecType<I> : never
type ActionRequestBase = {
type TaskBase = {
reason?: string
replayId?: string
}
type ActionRequestInput<T extends Action<T.ActionId, any>> = {
type TaskInput<T extends ActionInfo<T.ActionId, any>> = {
kind: "partial"
value: T.DeepPartial<GetActionInputType<T>>
}
export type ActionRequestOptions<T extends Action<T.ActionId, any>> =
ActionRequestBase &
(
| {
when?: Exclude<
T.ActionRequestTrigger,
{ condition: "input-not-matches" }
>
input?: ActionRequestInput<T>
}
| {
when: T.ActionRequestTrigger & { condition: "input-not-matches" }
input: ActionRequestInput<T>
}
)
export type TaskOptions<T extends ActionInfo<T.ActionId, any>> = TaskBase &
(
| {
when?: Exclude<T.TaskTrigger, { condition: "input-not-matches" }>
input?: TaskInput<T>
}
| {
when: T.TaskTrigger & { condition: "input-not-matches" }
input: TaskInput<T>
}
)
const _validate: T.ActionRequest = {} as ActionRequestOptions<any> & {
const _validate: T.Task = {} as TaskOptions<any> & {
actionId: string
packageId: string
severity: T.ActionSeverity
severity: T.TaskSeverity
}
export const requestAction = <T extends Action<T.ActionId, any>>(options: {
export const createTask = <T extends ActionInfo<T.ActionId, any>>(options: {
effects: T.Effects
packageId: T.PackageId
action: T
severity: T.ActionSeverity
options?: ActionRequestOptions<T>
severity: T.TaskSeverity
options?: TaskOptions<T>
}) => {
const request = options.options || {}
const actionId = options.action.id
@@ -96,5 +92,5 @@ export const requestAction = <T extends Action<T.ActionId, any>>(options: {
replayId: request.replayId || `${options.packageId}:${actionId}`,
}
delete req.action
return options.effects.action.request(req)
return options.effects.action.createTask(req)
}

View File

@@ -919,36 +919,6 @@ export class Value<Type> {
aVariants.validator,
)
}
static filteredUnion<
VariantValues extends {
[K in string]: {
name: string
spec: InputSpec<any>
}
},
>(
getDisabledFn: LazyBuild<string[] | false | string>,
a: {
name: string
description?: string | null
warning?: string | null
default: keyof VariantValues & string
},
aVariants: Variants<VariantValues>,
) {
return new Value<typeof aVariants.validator._TYPE>(
async (options) => ({
type: "union" as const,
description: null,
warning: null,
...a,
variants: await aVariants.build(options as any),
disabled: (await getDisabledFn(options)) || false,
immutable: false,
}),
aVariants.validator,
)
}
static dynamicUnion<
VariantValues extends {
[K in string]: {

View File

@@ -45,15 +45,16 @@ export const customSmtp = InputSpec.of<InputSpecOf<SmtpValue>>({
/**
* For service inputSpec. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings
*/
export const smtpInputSpec = Value.filteredUnion(
export const smtpInputSpec = Value.dynamicUnion(
async ({ effects }) => {
const smtp = await new GetSystemSmtp(effects).once()
return smtp ? [] : ["system"]
},
{
name: "SMTP",
description: "Optionally provide an SMTP server for sending emails",
default: "disabled",
const disabled = smtp ? [] : ["system"]
return {
name: "SMTP",
description: "Optionally provide an SMTP server for sending emails",
default: "disabled",
disabled,
}
},
Variants.of({
disabled: { name: "Disabled", spec: InputSpec.of({}) },

View File

@@ -5,6 +5,7 @@ import {
} from "./input/builder/inputSpec"
import * as T from "../types"
import { once } from "../util"
import { InitScript } from "../inits"
export type Run<
A extends Record<string, any> | InputSpec<Record<string, any>>,
@@ -40,10 +41,20 @@ function mapMaybeFn<T, U>(
}
}
export class Action<
export interface ActionInfo<
Id extends T.ActionId,
InputSpecType extends Record<string, any> | InputSpec<any>,
> {
readonly id: Id
readonly _INPUT: InputSpecType
}
export class Action<
Id extends T.ActionId,
InputSpecType extends Record<string, any> | InputSpec<any>,
> implements ActionInfo<Id, InputSpecType>
{
readonly _INPUT: InputSpecType = null as any as InputSpecType
private constructor(
readonly id: Id,
private readonly metadataFn: MaybeFn<T.ActionMetadata>,
@@ -111,7 +122,8 @@ export class Action<
export class Actions<
AllActions extends Record<T.ActionId, Action<T.ActionId, any>>,
> {
> implements InitScript
{
private constructor(private readonly actions: AllActions) {}
static of(): Actions<{}> {
return new Actions({})
@@ -121,13 +133,26 @@ export class Actions<
): Actions<AllActions & { [id in A["id"]]: A }> {
return new Actions({ ...this.actions, [action.id]: action })
}
async update(options: { effects: T.Effects }): Promise<null> {
async init(effects: T.Effects): Promise<void> {
for (let action of Object.values(this.actions)) {
await action.exportMetadata(options)
const fn = async () => {
let res: (value?: undefined) => void = () => {}
const complete = new Promise((resolve) => {
res = resolve
})
const e: T.Effects = effects.child(action.id)
e.constRetry = once(() =>
complete.then(() => fn()).catch(console.error),
)
try {
await action.exportMetadata({ effects: e })
} finally {
res()
}
}
await fn()
}
await options.effects.action.clear({ except: Object.keys(this.actions) })
return null
await effects.action.clear({ except: Object.keys(this.actions) })
}
get<Id extends T.ActionId>(actionId: Id): AllActions[Id] {
return this.actions[actionId]

View File

@@ -1,208 +0,0 @@
import * as T from "../types"
import * as child_process from "child_process"
import { asError } from "../util"
export const DEFAULT_OPTIONS: T.SyncOptions = {
delete: true,
exclude: [],
}
export type BackupSync<Volumes extends string> = {
dataPath: `/media/startos/volumes/${Volumes}/${string}`
backupPath: `/media/startos/backup/${string}`
options?: Partial<T.SyncOptions>
backupOptions?: Partial<T.SyncOptions>
restoreOptions?: Partial<T.SyncOptions>
}
/**
* This utility simplifies the volume backup process.
* ```ts
* export const { createBackup, restoreBackup } = Backups.volumes("main").build();
* ```
*
* Changing the options of the rsync, (ie excludes) use either
* ```ts
* Backups.volumes("main").set_options({exclude: ['bigdata/']}).volumes('excludedVolume').build()
* // or
* Backups.with_options({exclude: ['bigdata/']}).volumes('excludedVolume').build()
* ```
*
* Using the more fine control, using the addSets for more control
* ```ts
* Backups.addSets({
* srcVolume: 'main', srcPath:'smallData/', dstPath: 'main/smallData/', dstVolume: : Backups.BACKUP
* }, {
* srcVolume: 'main', srcPath:'bigData/', dstPath: 'main/bigData/', dstVolume: : Backups.BACKUP, options: {exclude:['bigData/excludeThis']}}
* ).build()q
* ```
*/
export class Backups<M extends T.Manifest> {
private constructor(
private options = DEFAULT_OPTIONS,
private restoreOptions: Partial<T.SyncOptions> = {},
private backupOptions: Partial<T.SyncOptions> = {},
private backupSet = [] as BackupSync<M["volumes"][number]>[],
) {}
static withVolumes<M extends T.Manifest = never>(
...volumeNames: Array<M["volumes"][number]>
): Backups<M> {
return Backups.withSyncs(
...volumeNames.map((srcVolume) => ({
dataPath: `/media/startos/volumes/${srcVolume}/` as const,
backupPath: `/media/startos/backup/${srcVolume}/` as const,
})),
)
}
static withSyncs<M extends T.Manifest = never>(
...syncs: BackupSync<M["volumes"][number]>[]
) {
return syncs.reduce((acc, x) => acc.addSync(x), new Backups<M>())
}
static withOptions<M extends T.Manifest = never>(
options?: Partial<T.SyncOptions>,
) {
return new Backups<M>({ ...DEFAULT_OPTIONS, ...options })
}
setOptions(options?: Partial<T.SyncOptions>) {
this.options = {
...this.options,
...options,
}
return this
}
setBackupOptions(options?: Partial<T.SyncOptions>) {
this.backupOptions = {
...this.backupOptions,
...options,
}
return this
}
setRestoreOptions(options?: Partial<T.SyncOptions>) {
this.restoreOptions = {
...this.restoreOptions,
...options,
}
return this
}
mountVolume(
volume: M["volumes"][number],
options?: Partial<{
options: T.SyncOptions
backupOptions: T.SyncOptions
restoreOptions: T.SyncOptions
}>,
) {
return this.addSync({
dataPath: `/media/startos/volumes/${volume}/` as const,
backupPath: `/media/startos/backup/${volume}/` as const,
...options,
})
}
addSync(sync: BackupSync<M["volumes"][0]>) {
this.backupSet.push({
...sync,
options: { ...this.options, ...sync.options },
})
return this
}
async createBackup() {
for (const item of this.backupSet) {
const rsyncResults = await runRsync({
srcPath: item.dataPath,
dstPath: item.backupPath,
options: {
...this.options,
...this.backupOptions,
...item.options,
...item.backupOptions,
},
})
await rsyncResults.wait()
}
return
}
async restoreBackup() {
for (const item of this.backupSet) {
const rsyncResults = await runRsync({
srcPath: item.backupPath,
dstPath: item.dataPath,
options: {
...this.options,
...this.backupOptions,
...item.options,
...item.backupOptions,
},
})
await rsyncResults.wait()
}
return
}
}
async function runRsync(rsyncOptions: {
srcPath: string
dstPath: string
options: T.SyncOptions
}): Promise<{
id: () => Promise<string>
wait: () => Promise<null>
progress: () => Promise<number>
}> {
const { srcPath, dstPath, options } = rsyncOptions
const command = "rsync"
const args: string[] = []
if (options.delete) {
args.push("--delete")
}
for (const exclude of options.exclude) {
args.push(`--exclude=${exclude}`)
}
args.push("-actAXH")
args.push("--info=progress2")
args.push("--no-inc-recursive")
args.push(srcPath)
args.push(dstPath)
const spawned = child_process.spawn(command, args, { detached: true })
let percentage = 0.0
spawned.stdout.on("data", (data: unknown) => {
const lines = String(data).replace("\r", "\n").split("\n")
for (const line of lines) {
const parsed = /$([0-9.]+)%/.exec(line)?.[1]
if (!parsed) continue
percentage = Number.parseFloat(parsed)
}
})
spawned.stderr.on("data", (data: unknown) => {
console.error(`Backups.runAsync`, asError(data))
})
const id = async () => {
const pid = spawned.pid
if (pid === undefined) {
throw new Error("rsync process has no pid")
}
return String(pid)
}
const waitPromise = new Promise<null>((resolve, reject) => {
spawned.on("exit", (code: any) => {
if (code === 0) {
resolve(null)
} else {
reject(new Error(`rsync exited with code ${code}`))
}
})
})
const wait = () => waitPromise
const progress = () => Promise.resolve(percentage)
return { id, wait, progress }
}

View File

@@ -1,39 +0,0 @@
import { Backups } from "./Backups"
import * as T from "../types"
import { _ } from "../util"
export type SetupBackupsParams<M extends T.Manifest> =
| M["volumes"][number][]
| ((_: { effects: T.Effects }) => Promise<Backups<M>>)
type SetupBackupsRes = {
createBackup: T.ExpectedExports.createBackup
restoreBackup: T.ExpectedExports.restoreBackup
}
export function setupBackups<M extends T.Manifest>(
options: SetupBackupsParams<M>,
) {
let backupsFactory: (_: { effects: T.Effects }) => Promise<Backups<M>>
if (options instanceof Function) {
backupsFactory = options
} else {
backupsFactory = async () => Backups.withVolumes(...options)
}
const answer: {
createBackup: T.ExpectedExports.createBackup
restoreBackup: T.ExpectedExports.restoreBackup
} = {
get createBackup() {
return (async (options) => {
return (await backupsFactory(options)).createBackup()
}) as T.ExpectedExports.createBackup
},
get restoreBackup() {
return (async (options) => {
return (await backupsFactory(options)).restoreBackup()
}) as T.ExpectedExports.restoreBackup
},
}
return answer
}

View File

@@ -6,7 +6,7 @@ export type CheckDependencies<DependencyId extends PackageId = PackageId> = {
installedSatisfied: (packageId: DependencyId) => boolean
installedVersionSatisfied: (packageId: DependencyId) => boolean
runningSatisfied: (packageId: DependencyId) => boolean
actionsSatisfied: (packageId: DependencyId) => boolean
tasksSatisfied: (packageId: DependencyId) => boolean
healthCheckSatisfied: (
packageId: DependencyId,
healthCheckId: HealthCheckId,
@@ -16,7 +16,7 @@ export type CheckDependencies<DependencyId extends PackageId = PackageId> = {
throwIfInstalledNotSatisfied: (packageId: DependencyId) => null
throwIfInstalledVersionNotSatisfied: (packageId: DependencyId) => null
throwIfRunningNotSatisfied: (packageId: DependencyId) => null
throwIfActionsNotSatisfied: (packageId: DependencyId) => null
throwIfTasksNotSatisfied: (packageId: DependencyId) => null
throwIfHealthNotSatisfied: (
packageId: DependencyId,
healthCheckId?: HealthCheckId,
@@ -65,9 +65,9 @@ export async function checkDependencies<
const dep = find(packageId)
return dep.requirement.kind !== "running" || dep.result.isRunning
}
const actionsSatisfied = (packageId: DependencyId) =>
Object.entries(find(packageId).result.requestedActions).filter(
([_, req]) => req.active && req.request.severity === "critical",
const tasksSatisfied = (packageId: DependencyId) =>
Object.entries(find(packageId).result.tasks).filter(
([_, t]) => t.active && t.task.severity === "critical",
).length === 0
const healthCheckSatisfied = (
packageId: DependencyId,
@@ -90,7 +90,7 @@ export async function checkDependencies<
installedSatisfied(packageId) &&
installedVersionSatisfied(packageId) &&
runningSatisfied(packageId) &&
actionsSatisfied(packageId) &&
tasksSatisfied(packageId) &&
healthCheckSatisfied(packageId)
const satisfied = (packageId?: DependencyId) =>
packageId
@@ -129,10 +129,10 @@ export async function checkDependencies<
}
return null
}
const throwIfActionsNotSatisfied = (packageId: DependencyId) => {
const throwIfTasksNotSatisfied = (packageId: DependencyId) => {
const dep = find(packageId)
const reqs = Object.entries(dep.result.requestedActions)
.filter(([_, req]) => req.active && req.request.severity === "critical")
const reqs = Object.entries(dep.result.tasks)
.filter(([_, t]) => t.active && t.task.severity === "critical")
.map(([id, _]) => id)
if (reqs.length) {
throw new Error(
@@ -172,7 +172,7 @@ export async function checkDependencies<
throwIfInstalledNotSatisfied(packageId)
throwIfInstalledVersionNotSatisfied(packageId)
throwIfRunningNotSatisfied(packageId)
throwIfActionsNotSatisfied(packageId)
throwIfTasksNotSatisfied(packageId)
throwIfHealthNotSatisfied(packageId)
return null
}
@@ -199,13 +199,13 @@ export async function checkDependencies<
installedSatisfied,
installedVersionSatisfied,
runningSatisfied,
actionsSatisfied,
tasksSatisfied,
healthCheckSatisfied,
satisfied,
throwIfInstalledNotSatisfied,
throwIfInstalledVersionNotSatisfied,
throwIfRunningNotSatisfied,
throwIfActionsNotSatisfied,
throwIfTasksNotSatisfied,
throwIfHealthNotSatisfied,
throwIfNotSatisfied,
}

View File

@@ -37,16 +37,10 @@ export function setupDependencies<Manifest extends T.SDKManifest>(
fn: (options: {
effects: T.Effects
}) => Promise<CurrentDependenciesResult<Manifest>>,
): (options: { effects: T.Effects }) => Promise<null> {
const cell = { updater: async (_: { effects: T.Effects }) => null }
cell.updater = async (options: { effects: T.Effects }) => {
const childEffects = options.effects.child("setupDependencies")
childEffects.constRetry = once(() => {
cell.updater({ effects: options.effects })
})
const dependencyType = await fn({ effects: childEffects })
return await options.effects.setDependencies({
): (effects: T.Effects) => Promise<null> {
return async (effects: T.Effects) => {
const dependencyType = await fn({ effects })
return await effects.setDependencies({
dependencies: Object.entries(dependencyType)
.map(([k, v]) => [k, v as DependencyRequirement] as const)
.map(
@@ -59,5 +53,4 @@ export function setupDependencies<Manifest extends T.SDKManifest>(
),
})
}
return cell.updater
}

View File

@@ -7,6 +7,7 @@ export * as IST from "./actions/input/inputSpecTypes"
export * as types from "./types"
export * as T from "./types"
export * as yaml from "yaml"
export * as inits from "./inits"
export * as matches from "ts-matches"
export * as utils from "./util"

View File

@@ -0,0 +1,2 @@
export * from "./setupInit"
export * from "./setupUninit"

View File

@@ -0,0 +1,91 @@
import { VersionRange } from "../../../base/lib/exver"
import * as T from "../../../base/lib/types"
import { once } from "../util"
export type InitKind = "install" | "update" | "restore" | null
export type InitFn<Kind extends InitKind = InitKind> = (
effects: T.Effects,
kind: Kind,
) => Promise<void | null | undefined>
export interface InitScript<Kind extends InitKind = InitKind> {
init(effects: T.Effects, kind: Kind): Promise<void>
}
export type InitScriptOrFn<Kind extends InitKind = InitKind> =
| InitScript<Kind>
| InitFn<Kind>
export function setupInit(...inits: InitScriptOrFn[]): T.ExpectedExports.init {
return async (opts) => {
for (const idx in inits) {
const init = inits[idx]
const fn = async () => {
let res: (value?: undefined) => void = () => {}
const complete = new Promise((resolve) => {
res = resolve
})
const e: T.Effects = opts.effects.child(`init_${idx}`)
e.constRetry = once(() =>
complete.then(() => fn()).catch(console.error),
)
try {
if ("init" in init) await init.init(e, opts.kind)
else await init(e, opts.kind)
} finally {
res()
}
}
await fn()
}
}
}
export function setupOnInit(onInit: InitScriptOrFn): InitScript {
return "init" in onInit
? onInit
: {
init: async (effects, kind) => {
await onInit(effects, kind)
},
}
}
export function setupOnInstall(
onInstall: InitScriptOrFn<"install">,
): InitScript {
return {
init: async (effects, kind) => {
if (kind === "install") {
if ("init" in onInstall) await onInstall.init(effects, kind)
else await onInstall(effects, kind)
}
},
}
}
export function setupOnUpdate(onUpdate: InitScriptOrFn<"update">): InitScript {
return {
init: async (effects, kind) => {
if (kind === "update") {
if ("init" in onUpdate) await onUpdate.init(effects, kind)
else await onUpdate(effects, kind)
}
},
}
}
export function setupOnInstallOrUpdate(
onInstallOrUpdate: InitScriptOrFn<"install" | "update">,
): InitScript {
return {
init: async (effects, kind) => {
if (kind === "install" || kind === "update") {
if ("init" in onInstallOrUpdate)
await onInstallOrUpdate.init(effects, kind)
else await onInstallOrUpdate(effects, kind)
}
},
}
}

View File

@@ -0,0 +1,25 @@
import { ExtendedVersion, VersionRange } from "../../../base/lib/exver"
import * as T from "../../../base/lib/types"
export type UninitFn = (
effects: T.Effects,
target: VersionRange | ExtendedVersion | null,
) => Promise<void | null | undefined>
export interface UninitScript {
uninit(
effects: T.Effects,
target: VersionRange | ExtendedVersion | null,
): Promise<void>
}
export function setupUninit(
...uninits: (UninitScript | UninitFn)[]
): T.ExpectedExports.uninit {
return async (opts) => {
for (const uninit of uninits) {
if ("uninit" in uninit) await uninit.uninit(opts.effects, opts.target)
else await uninit(opts.effects, opts.target)
}
}
}

View File

@@ -11,7 +11,12 @@ export class Origin {
readonly sslScheme: string | null,
) {}
build({ username, path, search, schemeOverride }: BuildOptions): AddressInfo {
build({
username,
path,
query: search,
schemeOverride,
}: BuildOptions): AddressInfo {
const qpEntries = Object.entries(search)
.map(
([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`,
@@ -50,7 +55,7 @@ export class Origin {
type,
username,
path,
search,
query: search,
schemeOverride,
masked,
} = serviceInterface.options
@@ -58,7 +63,7 @@ export class Origin {
const addressInfo = this.build({
username,
path,
search,
query: search,
schemeOverride,
})
@@ -82,5 +87,5 @@ type BuildOptions = {
schemeOverride: { ssl: Scheme; noSsl: Scheme } | null
username: string | null
path: string
search: Record<string, string>
query: Record<string, string>
}

View File

@@ -23,7 +23,7 @@ export class ServiceInterfaceBuilder {
type: ServiceInterfaceType
username: string | null
path: string
search: Record<string, string>
query: Record<string, string>
schemeOverride: { ssl: Scheme; noSsl: Scheme } | null
masked: boolean
},

View File

@@ -10,46 +10,34 @@ export type UpdateServiceInterfacesReceipt = {
export type ServiceInterfacesReceipt = Array<T.AddressInfo[] & AddressReceipt>
export type SetServiceInterfaces<Output extends ServiceInterfacesReceipt> =
(opts: { effects: T.Effects }) => Promise<Output>
export type UpdateServiceInterfaces<Output extends ServiceInterfacesReceipt> =
(opts: {
effects: T.Effects
}) => Promise<Output & UpdateServiceInterfacesReceipt>
export type UpdateServiceInterfaces = (effects: T.Effects) => Promise<null>
export type SetupServiceInterfaces = <Output extends ServiceInterfacesReceipt>(
fn: SetServiceInterfaces<Output>,
) => UpdateServiceInterfaces<Output>
) => UpdateServiceInterfaces
export const NO_INTERFACE_CHANGES = {} as UpdateServiceInterfacesReceipt
export const setupServiceInterfaces: SetupServiceInterfaces = <
Output extends ServiceInterfacesReceipt,
>(
fn: SetServiceInterfaces<Output>,
) => {
const cell = {
updater: (async (options: { effects: T.Effects }) =>
[] as any as Output) as UpdateServiceInterfaces<Output>,
}
cell.updater = (async (options: { effects: T.Effects }) => {
const childEffects = options.effects.child("setupInterfaces")
childEffects.constRetry = once(() => {
cell.updater({ effects: options.effects })
})
return (async (effects: T.Effects) => {
const bindings: T.BindId[] = []
const interfaces: T.ServiceInterfaceId[] = []
const res = await fn({
await fn({
effects: {
...childEffects,
...effects,
bind: (params: T.BindParams) => {
bindings.push({ id: params.id, internalPort: params.internalPort })
return childEffects.bind(params)
return effects.bind(params)
},
exportServiceInterface: (params: T.ExportServiceInterfaceParams) => {
interfaces.push(params.id)
return childEffects.exportServiceInterface(params)
return effects.exportServiceInterface(params)
},
},
})
await options.effects.clearBindings({ except: bindings })
await options.effects.clearServiceInterfaces({ except: interfaces })
return res
}) as UpdateServiceInterfaces<Output>
return cell.updater
await effects.clearBindings({ except: bindings })
await effects.clearServiceInterfaces({ except: interfaces })
return null
}) as UpdateServiceInterfaces
}

View File

@@ -1,15 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ActionId } from "./ActionId"
import type { ActionRequestInput } from "./ActionRequestInput"
import type { ActionRequestTrigger } from "./ActionRequestTrigger"
import type { ActionSeverity } from "./ActionSeverity"
import type { PackageId } from "./PackageId"
export type ActionRequest = {
packageId: PackageId
actionId: ActionId
severity: ActionSeverity
reason?: string
when?: ActionRequestTrigger
input?: ActionRequestInput
}

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ActionRequest } from "./ActionRequest"
export type ActionRequestEntry = { request: ActionRequest; active: boolean }

View File

@@ -1,7 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ActionRequestCondition } from "./ActionRequestCondition"
export type ActionRequestTrigger = {
once: boolean
condition: ActionRequestCondition
}

View File

@@ -4,4 +4,5 @@ import type { EncryptedWire } from "./EncryptedWire"
export type AttachParams = {
startOsPassword: EncryptedWire | null
guid: string
kiosk?: boolean
}

View File

@@ -1,9 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ActionRequestEntry } from "./ActionRequestEntry"
import type { HealthCheckId } from "./HealthCheckId"
import type { NamedHealthCheckResult } from "./NamedHealthCheckResult"
import type { PackageId } from "./PackageId"
import type { ReplayId } from "./ReplayId"
import type { TaskEntry } from "./TaskEntry"
import type { Version } from "./Version"
export type CheckDependenciesResult = {
@@ -12,6 +12,6 @@ export type CheckDependenciesResult = {
installedVersion: Version | null
satisfies: Array<Version>
isRunning: boolean
requestedActions: { [key: ReplayId]: ActionRequestEntry }
tasks: { [key: ReplayId]: TaskEntry }
healthChecks: { [key: HealthCheckId]: NamedHealthCheckResult }
}

View File

@@ -1,5 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ClearActionRequestsParams =
| { only: string[] }
| { except: string[] }
export type ClearTasksParams = { only: string[] } | { except: string[] }

View File

@@ -1,17 +1,17 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ActionId } from "./ActionId"
import type { ActionRequestInput } from "./ActionRequestInput"
import type { ActionRequestTrigger } from "./ActionRequestTrigger"
import type { ActionSeverity } from "./ActionSeverity"
import type { PackageId } from "./PackageId"
import type { ReplayId } from "./ReplayId"
import type { TaskInput } from "./TaskInput"
import type { TaskSeverity } from "./TaskSeverity"
import type { TaskTrigger } from "./TaskTrigger"
export type RequestActionParams = {
export type CreateTaskParams = {
replayId: ReplayId
packageId: PackageId
actionId: ActionId
severity: ActionSeverity
severity: TaskSeverity
reason?: string
when?: ActionRequestTrigger
input?: ActionRequestInput
when?: TaskTrigger
input?: TaskInput
}

View File

@@ -12,7 +12,6 @@ export type MainStatus =
}
| { main: "stopped" }
| { main: "restarting" }
| { main: "restoring" }
| { main: "stopping" }
| {
main: "starting"

View File

@@ -1,7 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ActionId } from "./ActionId"
import type { ActionMetadata } from "./ActionMetadata"
import type { ActionRequestEntry } from "./ActionRequestEntry"
import type { CurrentDependencies } from "./CurrentDependencies"
import type { DataUrl } from "./DataUrl"
import type { Hosts } from "./Hosts"
@@ -9,11 +8,11 @@ import type { MainStatus } from "./MainStatus"
import type { PackageState } from "./PackageState"
import type { ServiceInterface } from "./ServiceInterface"
import type { ServiceInterfaceId } from "./ServiceInterfaceId"
import type { Version } from "./Version"
import type { TaskEntry } from "./TaskEntry"
export type PackageDataEntry = {
stateInfo: PackageState
dataVersion: Version | null
dataVersion: string | null
status: MainStatus
registry: string | null
developerKey: string
@@ -21,7 +20,7 @@ export type PackageDataEntry = {
lastBackup: string | null
currentDependencies: CurrentDependencies
actions: { [key: ActionId]: ActionMetadata }
requestedActions: { [key: string]: ActionRequestEntry }
tasks: { [key: string]: TaskEntry }
serviceInterfaces: { [key: ServiceInterfaceId]: ServiceInterface }
hosts: Hosts
storeExposedDependents: string[]

View File

@@ -26,4 +26,5 @@ export type ServerInfo = {
smtp: SmtpValue | null
ram: number
devices: Array<LshwDevice>
kiosk: boolean | null
}

View File

@@ -6,4 +6,5 @@ export type SetupExecuteParams = {
startOsLogicalname: string
startOsPassword: EncryptedWire
recoverySource: RecoverySource<EncryptedWire> | null
kiosk?: boolean
}

View File

@@ -0,0 +1,15 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ActionId } from "./ActionId"
import type { PackageId } from "./PackageId"
import type { TaskInput } from "./TaskInput"
import type { TaskSeverity } from "./TaskSeverity"
import type { TaskTrigger } from "./TaskTrigger"
export type Task = {
packageId: PackageId
actionId: ActionId
severity: TaskSeverity
reason?: string
when?: TaskTrigger
input?: TaskInput
}

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ActionSeverity = "critical" | "important"
export type TaskCondition = "input-not-matches"

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Task } from "./Task"
export type TaskEntry = { task: Task; active: boolean }

View File

@@ -1,6 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ActionRequestInput = {
kind: "partial"
value: Record<string, unknown>
}
export type TaskInput = { kind: "partial"; value: Record<string, unknown> }

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ActionRequestCondition = "input-not-matches"
export type TaskSeverity = "optional" | "important" | "critical"

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { TaskCondition } from "./TaskCondition"
export type TaskTrigger = { once: boolean; condition: TaskCondition }

View File

@@ -4,17 +4,11 @@ export { AcmeSettings } from "./AcmeSettings"
export { ActionId } from "./ActionId"
export { ActionInput } from "./ActionInput"
export { ActionMetadata } from "./ActionMetadata"
export { ActionRequestCondition } from "./ActionRequestCondition"
export { ActionRequestEntry } from "./ActionRequestEntry"
export { ActionRequestInput } from "./ActionRequestInput"
export { ActionRequestTrigger } from "./ActionRequestTrigger"
export { ActionRequest } from "./ActionRequest"
export { ActionResultMember } from "./ActionResultMember"
export { ActionResult } from "./ActionResult"
export { ActionResultV0 } from "./ActionResultV0"
export { ActionResultV1 } from "./ActionResultV1"
export { ActionResultValue } from "./ActionResultValue"
export { ActionSeverity } from "./ActionSeverity"
export { ActionVisibility } from "./ActionVisibility"
export { AddAdminParams } from "./AddAdminParams"
export { AddAssetParams } from "./AddAssetParams"
@@ -51,14 +45,15 @@ export { Celsius } from "./Celsius"
export { CheckDependenciesParam } from "./CheckDependenciesParam"
export { CheckDependenciesResult } from "./CheckDependenciesResult"
export { Cifs } from "./Cifs"
export { ClearActionRequestsParams } from "./ClearActionRequestsParams"
export { ClearActionsParams } from "./ClearActionsParams"
export { ClearBindingsParams } from "./ClearBindingsParams"
export { ClearCallbacksParams } from "./ClearCallbacksParams"
export { ClearServiceInterfacesParams } from "./ClearServiceInterfacesParams"
export { ClearTasksParams } from "./ClearTasksParams"
export { CliSetIconParams } from "./CliSetIconParams"
export { ContactInfo } from "./ContactInfo"
export { CreateSubcontainerFsParams } from "./CreateSubcontainerFsParams"
export { CreateTaskParams } from "./CreateTaskParams"
export { CurrentDependencies } from "./CurrentDependencies"
export { CurrentDependencyInfo } from "./CurrentDependencyInfo"
export { DataUrl } from "./DataUrl"
@@ -173,7 +168,6 @@ export { RemovePackageFromCategoryParams } from "./RemovePackageFromCategoryPara
export { RemovePackageParams } from "./RemovePackageParams"
export { RemoveVersionParams } from "./RemoveVersionParams"
export { ReplayId } from "./ReplayId"
export { RequestActionParams } from "./RequestActionParams"
export { RequestCommitment } from "./RequestCommitment"
export { RunActionParams } from "./RunActionParams"
export { Security } from "./Security"
@@ -201,6 +195,12 @@ export { SignAssetParams } from "./SignAssetParams"
export { SignerInfo } from "./SignerInfo"
export { SmtpValue } from "./SmtpValue"
export { StartStop } from "./StartStop"
export { TaskCondition } from "./TaskCondition"
export { TaskEntry } from "./TaskEntry"
export { TaskInput } from "./TaskInput"
export { TaskSeverity } from "./TaskSeverity"
export { TaskTrigger } from "./TaskTrigger"
export { Task } from "./Task"
export { TestSmtpParams } from "./TestSmtpParams"
export { UnsetInboundParams } from "./UnsetInboundParams"
export { UpdatingState } from "./UpdatingState"

View File

@@ -1,7 +1,7 @@
import { Effects } from "../types"
import {
CheckDependenciesParam,
ClearActionRequestsParams,
ClearTasksParams,
ClearActionsParams,
ClearBindingsParams,
ClearCallbacksParams,
@@ -9,7 +9,7 @@ import {
GetActionInputParams,
GetContainerIpParams,
GetStatusParams,
RequestActionParams,
CreateTaskParams,
RunActionParams,
SetDataVersionParams,
SetMainStatus,
@@ -30,6 +30,7 @@ import { ListServiceInterfacesParams } from ".././osBindings"
import { ExportActionParams } from ".././osBindings"
import { MountParams } from ".././osBindings"
import { StringObject } from "../util"
import { ExtendedVersion, VersionRange } from "../exver"
function typeEquality<ExpectedType>(_a: ExpectedType) {}
type WithCallback<T> = Omit<T, "callback"> & { callback: () => void }
@@ -54,8 +55,8 @@ describe("startosTypeValidation ", () => {
export: {} as ExportActionParams,
getInput: {} as GetActionInputParams,
run: {} as RunActionParams,
request: {} as RequestActionParams,
clearRequests: {} as ClearActionRequestsParams,
createTask: {} as CreateTaskParams,
clearTasks: {} as ClearTasksParams,
},
subcontainer: {
createFs: {} as CreateSubcontainerFsParams,

View File

@@ -10,6 +10,7 @@ import {
import { Affine, StringObject, ToKebab } from "./util"
import { Action, Actions } from "./actions/setupActions"
import { Effects } from "./Effects"
import { ExtendedVersion, VersionRange } from "./exver"
export { Effects }
export * from "./osBindings"
export { SDKManifest } from "./types/ManifestTypes"
@@ -38,10 +39,6 @@ export namespace ExpectedExports {
/** For backing up service data though the startOS UI */
export type createBackup = (options: { effects: Effects }) => Promise<unknown>
/** For restoring service data that was previously backed up using the startOS UI create backup flow. Backup restores are also triggered via the startOS UI, or doing a system restore flow during setup. */
export type restoreBackup = (options: {
effects: Effects
}) => Promise<unknown>
/**
* This is the entrypoint for the main container. Used to start up something like the service that the
@@ -52,33 +49,20 @@ export namespace ExpectedExports {
started(onTerm: () => PromiseLike<void>): PromiseLike<null>
}) => Promise<DaemonBuildable>
/**
* After a shutdown, if we wanted to do any operations to clean up things, like
* set the action as unavailable or something.
*/
export type afterShutdown = (options: {
effects: Effects
}) => Promise<unknown>
/**
* Every time a service launches (both on startup, and on install) this function is called before packageInit
* Can be used to register callbacks
*/
export type containerInit = (options: {
export type init = (options: {
effects: Effects
kind: "install" | "update" | "restore" | null
}) => Promise<unknown>
/**
* Every time a package completes an install, this function is called before the main.
* Can be used to do migration like things.
*/
export type packageInit = (options: { effects: Effects }) => Promise<unknown>
/** This will be ran during any time a package is uninstalled, for example during a update
* this will be called.
*/
export type packageUninit = (options: {
export type uninit = (options: {
effects: Effects
nextVersion: null | string
target: ExtendedVersion | VersionRange | null
}) => Promise<unknown>
export type manifest = Manifest
@@ -87,12 +71,9 @@ export namespace ExpectedExports {
}
export type ABI = {
createBackup: ExpectedExports.createBackup
restoreBackup: ExpectedExports.restoreBackup
main: ExpectedExports.main
afterShutdown: ExpectedExports.afterShutdown
containerInit: ExpectedExports.containerInit
packageInit: ExpectedExports.packageInit
packageUninit: ExpectedExports.packageUninit
init: ExpectedExports.init
uninit: ExpectedExports.uninit
manifest: ExpectedExports.manifest
actions: ExpectedExports.actions
}
@@ -119,8 +100,14 @@ export type SmtpValue = {
}
export class UseEntrypoint {
readonly USE_ENTRYPOINT = "USE_ENTRYPOINT"
constructor(readonly overridCmd?: string[]) {}
}
export function isUseEntrypoint(
command: CommandType,
): command is UseEntrypoint {
return typeof command === "object" && "ENTRYPOINT" in command
}
export type CommandType = string | [string, ...string[]] | UseEntrypoint