Chore/refactoring effects (#2644)

* fix mac build

* wip

* chore: Update the effects to get rid of bad pattern

* chore: Some small changes

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Jade
2024-06-14 14:16:12 -06:00
committed by GitHub
parent 3f380fa0da
commit bb514d6216
10 changed files with 704 additions and 693 deletions

View File

@@ -31,274 +31,273 @@ type RpcError = typeof matchRpcError._TYPE
const SOCKET_PATH = "/media/startos/rpc/host.sock" const SOCKET_PATH = "/media/startos/rpc/host.sock"
const MAIN = "/main" as const const MAIN = "/main" as const
export class HostSystemStartOs implements Effects { let hostSystemId = 0
procedureId: string | null = null export const hostSystemStartOs =
(callbackHolder: CallbackHolder) =>
static of(callbackHolder: CallbackHolder) { (procedureId: null | string): Effects => {
return new HostSystemStartOs(callbackHolder) const rpcRound = <K extends keyof Effects | "getStore" | "setStore">(
} method: K,
params: Record<string, unknown>,
constructor(readonly callbackHolder: CallbackHolder) {} ) => {
id = 0 const id = hostSystemId++
rpcRound<K extends keyof Effects | "getStore" | "setStore">( const client = net.createConnection({ path: SOCKET_PATH }, () => {
method: K, client.write(
params: Record<string, unknown>, JSON.stringify({
) { id,
const id = this.id++ method,
const client = net.createConnection({ path: SOCKET_PATH }, () => { params: { ...params, procedureId: procedureId },
client.write( }) + "\n",
JSON.stringify({ )
id, })
method, let bufs: Buffer[] = []
params: { ...params, procedureId: this.procedureId }, return new Promise((resolve, reject) => {
}) + "\n", client.on("data", (data) => {
) try {
}) bufs.push(data)
let bufs: Buffer[] = [] if (data.reduce((acc, x) => acc || x == 10, false)) {
return new Promise((resolve, reject) => { const res: unknown = JSON.parse(
client.on("data", (data) => { Buffer.concat(bufs).toString().split("\n")[0],
try { )
bufs.push(data) if (testRpcError(res)) {
if (data.reduce((acc, x) => acc || x == 10, false)) { let message = res.error.message
const res: unknown = JSON.parse( console.error({ method, params, hostSystemStartOs: true })
Buffer.concat(bufs).toString().split("\n")[0], if (string.test(res.error.data)) {
) message += ": " + res.error.data
if (testRpcError(res)) { console.error(res.error.data)
let message = res.error.message } else {
console.error({ method, params, hostSystemStartOs: true }) if (res.error.data?.details) {
if (string.test(res.error.data)) { message += ": " + res.error.data.details
message += ": " + res.error.data console.error(res.error.data.details)
console.error(res.error.data) }
if (res.error.data?.debug) {
message += "\n" + res.error.data.debug
console.error("Debug: " + res.error.data.debug)
}
}
reject(new Error(`${message}@${method}`))
} else if (testRpcResult(res)) {
resolve(res.result)
} else { } else {
if (res.error.data?.details) { reject(new Error(`malformed response ${JSON.stringify(res)}`))
message += ": " + res.error.data.details
console.error(res.error.data.details)
}
if (res.error.data?.debug) {
message += "\n" + res.error.data.debug
console.error("Debug: " + res.error.data.debug)
}
} }
reject(new Error(`${message}@${method}`))
} else if (testRpcResult(res)) {
resolve(res.result)
} else {
reject(new Error(`malformed response ${JSON.stringify(res)}`))
} }
} catch (error) {
reject(error)
} }
} catch (error) { client.end()
})
client.on("error", (error) => {
reject(error) reject(error)
} })
client.end()
}) })
client.on("error", (error) => {
reject(error)
})
})
}
bind(...[options]: Parameters<T.Effects["bind"]>) {
return this.rpcRound("bind", {
...options,
stack: new Error().stack,
}) as ReturnType<T.Effects["bind"]>
}
clearBindings(...[]: Parameters<T.Effects["clearBindings"]>) {
return this.rpcRound("clearBindings", {}) as ReturnType<
T.Effects["clearBindings"]
>
}
clearServiceInterfaces(
...[]: Parameters<T.Effects["clearServiceInterfaces"]>
) {
return this.rpcRound("clearServiceInterfaces", {}) as ReturnType<
T.Effects["clearServiceInterfaces"]
>
}
createOverlayedImage(options: {
imageId: string
}): Promise<[string, string]> {
return this.rpcRound("createOverlayedImage", options) as ReturnType<
T.Effects["createOverlayedImage"]
>
}
destroyOverlayedImage(options: { guid: string }): Promise<void> {
return this.rpcRound("destroyOverlayedImage", options) as ReturnType<
T.Effects["destroyOverlayedImage"]
>
}
executeAction(...[options]: Parameters<T.Effects["executeAction"]>) {
return this.rpcRound("executeAction", options) as ReturnType<
T.Effects["executeAction"]
>
}
exists(...[packageId]: Parameters<T.Effects["exists"]>) {
return this.rpcRound("exists", packageId) as ReturnType<T.Effects["exists"]>
}
exportAction(...[options]: Parameters<T.Effects["exportAction"]>) {
return this.rpcRound("exportAction", options) as ReturnType<
T.Effects["exportAction"]
>
}
exportServiceInterface: Effects["exportServiceInterface"] = (
...[options]: Parameters<Effects["exportServiceInterface"]>
) => {
return this.rpcRound("exportServiceInterface", options) as ReturnType<
T.Effects["exportServiceInterface"]
>
}
exposeForDependents(
...[options]: Parameters<T.Effects["exposeForDependents"]>
) {
return this.rpcRound("exposeForDependents", options) as ReturnType<
T.Effects["exposeForDependents"]
>
}
getConfigured(...[]: Parameters<T.Effects["getConfigured"]>) {
return this.rpcRound("getConfigured", {}) as ReturnType<
T.Effects["getConfigured"]
>
}
getContainerIp(...[]: Parameters<T.Effects["getContainerIp"]>) {
return this.rpcRound("getContainerIp", {}) as ReturnType<
T.Effects["getContainerIp"]
>
}
getHostInfo: Effects["getHostInfo"] = (...[allOptions]: any[]) => {
const options = {
...allOptions,
callback: this.callbackHolder.addCallback(allOptions.callback),
} }
return this.rpcRound("getHostInfo", options) as ReturnType< const self: Effects = {
T.Effects["getHostInfo"] bind(...[options]: Parameters<T.Effects["bind"]>) {
> as any return rpcRound("bind", {
} ...options,
getServiceInterface( stack: new Error().stack,
...[options]: Parameters<T.Effects["getServiceInterface"]> }) as ReturnType<T.Effects["bind"]>
) { },
return this.rpcRound("getServiceInterface", { clearBindings(...[]: Parameters<T.Effects["clearBindings"]>) {
...options, return rpcRound("clearBindings", {}) as ReturnType<
callback: this.callbackHolder.addCallback(options.callback), T.Effects["clearBindings"]
}) as ReturnType<T.Effects["getServiceInterface"]> >
} },
clearServiceInterfaces(
...[]: Parameters<T.Effects["clearServiceInterfaces"]>
) {
return rpcRound("clearServiceInterfaces", {}) as ReturnType<
T.Effects["clearServiceInterfaces"]
>
},
createOverlayedImage(options: {
imageId: string
}): Promise<[string, string]> {
return rpcRound("createOverlayedImage", options) as ReturnType<
T.Effects["createOverlayedImage"]
>
},
destroyOverlayedImage(options: { guid: string }): Promise<void> {
return rpcRound("destroyOverlayedImage", options) as ReturnType<
T.Effects["destroyOverlayedImage"]
>
},
executeAction(...[options]: Parameters<T.Effects["executeAction"]>) {
return rpcRound("executeAction", options) as ReturnType<
T.Effects["executeAction"]
>
},
exists(...[packageId]: Parameters<T.Effects["exists"]>) {
return rpcRound("exists", packageId) as ReturnType<T.Effects["exists"]>
},
exportAction(...[options]: Parameters<T.Effects["exportAction"]>) {
return rpcRound("exportAction", options) as ReturnType<
T.Effects["exportAction"]
>
},
exportServiceInterface: ((
...[options]: Parameters<Effects["exportServiceInterface"]>
) => {
return rpcRound("exportServiceInterface", options) as ReturnType<
T.Effects["exportServiceInterface"]
>
}) as Effects["exportServiceInterface"],
exposeForDependents(
...[options]: Parameters<T.Effects["exposeForDependents"]>
) {
return rpcRound("exposeForDependents", options) as ReturnType<
T.Effects["exposeForDependents"]
>
},
getConfigured(...[]: Parameters<T.Effects["getConfigured"]>) {
return rpcRound("getConfigured", {}) as ReturnType<
T.Effects["getConfigured"]
>
},
getContainerIp(...[]: Parameters<T.Effects["getContainerIp"]>) {
return rpcRound("getContainerIp", {}) as ReturnType<
T.Effects["getContainerIp"]
>
},
getHostInfo: ((...[allOptions]: any[]) => {
const options = {
...allOptions,
callback: callbackHolder.addCallback(allOptions.callback),
}
return rpcRound("getHostInfo", options) as ReturnType<
T.Effects["getHostInfo"]
> as any
}) as Effects["getHostInfo"],
getServiceInterface(
...[options]: Parameters<T.Effects["getServiceInterface"]>
) {
return rpcRound("getServiceInterface", {
...options,
callback: callbackHolder.addCallback(options.callback),
}) as ReturnType<T.Effects["getServiceInterface"]>
},
getPrimaryUrl(...[options]: Parameters<T.Effects["getPrimaryUrl"]>) { getPrimaryUrl(...[options]: Parameters<T.Effects["getPrimaryUrl"]>) {
return this.rpcRound("getPrimaryUrl", { return rpcRound("getPrimaryUrl", {
...options, ...options,
callback: this.callbackHolder.addCallback(options.callback), callback: callbackHolder.addCallback(options.callback),
}) as ReturnType<T.Effects["getPrimaryUrl"]> }) as ReturnType<T.Effects["getPrimaryUrl"]>
} },
getServicePortForward( getServicePortForward(
...[options]: Parameters<T.Effects["getServicePortForward"]> ...[options]: Parameters<T.Effects["getServicePortForward"]>
) { ) {
return this.rpcRound("getServicePortForward", options) as ReturnType< return rpcRound("getServicePortForward", options) as ReturnType<
T.Effects["getServicePortForward"] T.Effects["getServicePortForward"]
> >
} },
getSslCertificate(options: Parameters<T.Effects["getSslCertificate"]>[0]) { getSslCertificate(
return this.rpcRound("getSslCertificate", options) as ReturnType< options: Parameters<T.Effects["getSslCertificate"]>[0],
T.Effects["getSslCertificate"] ) {
> return rpcRound("getSslCertificate", options) as ReturnType<
} T.Effects["getSslCertificate"]
getSslKey(options: Parameters<T.Effects["getSslKey"]>[0]) { >
return this.rpcRound("getSslKey", options) as ReturnType< },
T.Effects["getSslKey"] getSslKey(options: Parameters<T.Effects["getSslKey"]>[0]) {
> return rpcRound("getSslKey", options) as ReturnType<
} T.Effects["getSslKey"]
getSystemSmtp(...[options]: Parameters<T.Effects["getSystemSmtp"]>) { >
return this.rpcRound("getSystemSmtp", { },
...options, getSystemSmtp(...[options]: Parameters<T.Effects["getSystemSmtp"]>) {
callback: this.callbackHolder.addCallback(options.callback), return rpcRound("getSystemSmtp", {
}) as ReturnType<T.Effects["getSystemSmtp"]> ...options,
} callback: callbackHolder.addCallback(options.callback),
listServiceInterfaces( }) as ReturnType<T.Effects["getSystemSmtp"]>
...[options]: Parameters<T.Effects["listServiceInterfaces"]> },
) { listServiceInterfaces(
return this.rpcRound("listServiceInterfaces", { ...[options]: Parameters<T.Effects["listServiceInterfaces"]>
...options, ) {
callback: this.callbackHolder.addCallback(options.callback), return rpcRound("listServiceInterfaces", {
}) as ReturnType<T.Effects["listServiceInterfaces"]> ...options,
} callback: callbackHolder.addCallback(options.callback),
mount(...[options]: Parameters<T.Effects["mount"]>) { }) as ReturnType<T.Effects["listServiceInterfaces"]>
return this.rpcRound("mount", options) as ReturnType<T.Effects["mount"]> },
} mount(...[options]: Parameters<T.Effects["mount"]>) {
removeAction(...[options]: Parameters<T.Effects["removeAction"]>) { return rpcRound("mount", options) as ReturnType<T.Effects["mount"]>
return this.rpcRound("removeAction", options) as ReturnType< },
T.Effects["removeAction"] removeAction(...[options]: Parameters<T.Effects["removeAction"]>) {
> return rpcRound("removeAction", options) as ReturnType<
} T.Effects["removeAction"]
removeAddress(...[options]: Parameters<T.Effects["removeAddress"]>) { >
return this.rpcRound("removeAddress", options) as ReturnType< },
T.Effects["removeAddress"] removeAddress(...[options]: Parameters<T.Effects["removeAddress"]>) {
> return rpcRound("removeAddress", options) as ReturnType<
} T.Effects["removeAddress"]
restart(...[]: Parameters<T.Effects["restart"]>) { >
return this.rpcRound("restart", {}) as ReturnType<T.Effects["restart"]> },
} restart(...[]: Parameters<T.Effects["restart"]>) {
running(...[packageId]: Parameters<T.Effects["running"]>) { return rpcRound("restart", {}) as ReturnType<T.Effects["restart"]>
return this.rpcRound("running", { packageId }) as ReturnType< },
T.Effects["running"] running(...[packageId]: Parameters<T.Effects["running"]>) {
> return rpcRound("running", { packageId }) as ReturnType<
} T.Effects["running"]
// runRsync(...[options]: Parameters<T.Effects[""]>) { >
// },
// return this.rpcRound('executeAction', options) as ReturnType<T.Effects["executeAction"]> // runRsync(...[options]: Parameters<T.Effects[""]>) {
// //
// return this.rpcRound('executeAction', options) as ReturnType<T.Effects["executeAction"]> // return rpcRound('executeAction', options) as ReturnType<T.Effects["executeAction"]>
// } //
setConfigured(...[configured]: Parameters<T.Effects["setConfigured"]>) { // return rpcRound('executeAction', options) as ReturnType<T.Effects["executeAction"]>
return this.rpcRound("setConfigured", { configured }) as ReturnType< // }
T.Effects["setConfigured"] setConfigured(...[configured]: Parameters<T.Effects["setConfigured"]>) {
> return rpcRound("setConfigured", { configured }) as ReturnType<
} T.Effects["setConfigured"]
setDependencies( >
dependencies: Parameters<T.Effects["setDependencies"]>[0], },
): ReturnType<T.Effects["setDependencies"]> { setDependencies(
return this.rpcRound("setDependencies", dependencies) as ReturnType< dependencies: Parameters<T.Effects["setDependencies"]>[0],
T.Effects["setDependencies"] ): ReturnType<T.Effects["setDependencies"]> {
> return rpcRound("setDependencies", dependencies) as ReturnType<
} T.Effects["setDependencies"]
checkDependencies( >
options: Parameters<T.Effects["checkDependencies"]>[0], },
): ReturnType<T.Effects["checkDependencies"]> { checkDependencies(
return this.rpcRound("checkDependencies", options) as ReturnType< options: Parameters<T.Effects["checkDependencies"]>[0],
T.Effects["checkDependencies"] ): ReturnType<T.Effects["checkDependencies"]> {
> return rpcRound("checkDependencies", options) as ReturnType<
} T.Effects["checkDependencies"]
getDependencies(): ReturnType<T.Effects["getDependencies"]> { >
return this.rpcRound("getDependencies", {}) as ReturnType< },
T.Effects["getDependencies"] getDependencies(): ReturnType<T.Effects["getDependencies"]> {
> return rpcRound("getDependencies", {}) as ReturnType<
} T.Effects["getDependencies"]
setHealth(...[options]: Parameters<T.Effects["setHealth"]>) { >
return this.rpcRound("setHealth", options) as ReturnType< },
T.Effects["setHealth"] setHealth(...[options]: Parameters<T.Effects["setHealth"]>) {
> return rpcRound("setHealth", options) as ReturnType<
} T.Effects["setHealth"]
>
},
setMainStatus(o: { status: "running" | "stopped" }): Promise<void> { setMainStatus(o: { status: "running" | "stopped" }): Promise<void> {
return this.rpcRound("setMainStatus", o) as ReturnType< return rpcRound("setMainStatus", o) as ReturnType<
T.Effects["setHealth"] T.Effects["setHealth"]
> >
} },
shutdown(...[]: Parameters<T.Effects["shutdown"]>) { shutdown(...[]: Parameters<T.Effects["shutdown"]>) {
return this.rpcRound("shutdown", {}) as ReturnType<T.Effects["shutdown"]> return rpcRound("shutdown", {}) as ReturnType<T.Effects["shutdown"]>
},
stopped(...[packageId]: Parameters<T.Effects["stopped"]>) {
return rpcRound("stopped", { packageId }) as ReturnType<
T.Effects["stopped"]
>
},
store: {
get: async (options: any) =>
rpcRound("getStore", {
...options,
callback: callbackHolder.addCallback(options.callback),
}) as any,
set: async (options: any) =>
rpcRound("setStore", options) as ReturnType<
T.Effects["store"]["set"]
>,
} as T.Effects["store"],
}
return self
} }
stopped(...[packageId]: Parameters<T.Effects["stopped"]>) {
return this.rpcRound("stopped", { packageId }) as ReturnType<
T.Effects["stopped"]
>
}
store: T.Effects["store"] = {
get: async (options: any) =>
this.rpcRound("getStore", {
...options,
callback: this.callbackHolder.addCallback(options.callback),
}) as any,
set: async (options: any) =>
this.rpcRound("setStore", options) as ReturnType<
T.Effects["store"]["set"]
>,
}
}

View File

@@ -247,7 +247,7 @@ export class RpcListener {
})), })),
) )
.when(exitType, async ({ id }) => { .when(exitType, async ({ id }) => {
if (this._system) await this._system.exit(this.effects) if (this._system) await this._system.exit(this.effects(null))
delete this._system delete this._system
delete this._effects delete this._effects

View File

@@ -1,9 +1,10 @@
import { PolyfillEffects } from "./polyfillEffects" import { polyfillEffects } from "./polyfillEffects"
import { DockerProcedureContainer } from "./DockerProcedureContainer" import { DockerProcedureContainer } from "./DockerProcedureContainer"
import { SystemForEmbassy } from "." import { SystemForEmbassy } from "."
import { HostSystemStartOs } from "../../HostSystemStartOs" import { hostSystemStartOs } from "../../HostSystemStartOs"
import { Daemons, T, daemons } from "@start9labs/start-sdk" import { Daemons, T, daemons } from "@start9labs/start-sdk"
import { Daemon } from "@start9labs/start-sdk/cjs/lib/mainFn/Daemon" import { Daemon } from "@start9labs/start-sdk/cjs/lib/mainFn/Daemon"
import { Effects } from "../../../Models/Effects"
const EMBASSY_HEALTH_INTERVAL = 15 * 1000 const EMBASSY_HEALTH_INTERVAL = 15 * 1000
const EMBASSY_PROPERTIES_LOOP = 30 * 1000 const EMBASSY_PROPERTIES_LOOP = 30 * 1000
@@ -27,7 +28,7 @@ export class MainLoop {
| undefined | undefined
constructor( constructor(
readonly system: SystemForEmbassy, readonly system: SystemForEmbassy,
readonly effects: HostSystemStartOs, readonly effects: Effects,
) { ) {
this.healthLoops = this.constructHealthLoops() this.healthLoops = this.constructHealthLoops()
this.mainEvent = this.constructMainEvent() this.mainEvent = this.constructMainEvent()
@@ -65,7 +66,7 @@ export class MainLoop {
} }
} }
private async setupInterfaces(effects: HostSystemStartOs) { private async setupInterfaces(effects: T.Effects) {
for (const interfaceId in this.system.manifest.interfaces) { for (const interfaceId in this.system.manifest.interfaces) {
const iface = this.system.manifest.interfaces[interfaceId] const iface = this.system.manifest.interfaces[interfaceId]
const internalPorts = new Set<number>() const internalPorts = new Set<number>()
@@ -211,7 +212,7 @@ export class MainLoop {
} }
const result = await method( const result = await method(
new PolyfillEffects(effects, this.system.manifest), polyfillEffects(effects, this.system.manifest),
timeChanged, timeChanged,
) )

View File

@@ -1,7 +1,7 @@
import { types as T, utils, EmVer } from "@start9labs/start-sdk" import { types as T, utils, EmVer } from "@start9labs/start-sdk"
import * as fs from "fs/promises" import * as fs from "fs/promises"
import { PolyfillEffects } from "./polyfillEffects" import { polyfillEffects } from "./polyfillEffects"
import { Duration, duration, fromDuration } from "../../../Models/Duration" import { Duration, duration, fromDuration } from "../../../Models/Duration"
import { System } from "../../../Interfaces/System" import { System } from "../../../Interfaces/System"
import { matchManifest, Manifest, Procedure } from "./matchManifest" import { matchManifest, Manifest, Procedure } from "./matchManifest"
@@ -27,7 +27,7 @@ import {
Parser, Parser,
array, array,
} from "ts-matches" } from "ts-matches"
import { HostSystemStartOs } from "../../HostSystemStartOs" import { hostSystemStartOs } from "../../HostSystemStartOs"
import { JsonPath, unNestPath } from "../../../Models/JsonPath" import { JsonPath, unNestPath } from "../../../Models/JsonPath"
import { RpcResult, matchRpcResult } from "../../RpcListener" import { RpcResult, matchRpcResult } from "../../RpcListener"
import { CT } from "@start9labs/start-sdk" import { CT } from "@start9labs/start-sdk"
@@ -41,6 +41,7 @@ import {
MultiHost, MultiHost,
} from "@start9labs/start-sdk/cjs/lib/interfaces/Host" } from "@start9labs/start-sdk/cjs/lib/interfaces/Host"
import { ServiceInterfaceBuilder } from "@start9labs/start-sdk/cjs/lib/interfaces/ServiceInterfaceBuilder" import { ServiceInterfaceBuilder } from "@start9labs/start-sdk/cjs/lib/interfaces/ServiceInterfaceBuilder"
import { Effects } from "../../../Models/Effects"
type Optional<A> = A | undefined | null type Optional<A> = A | undefined | null
function todo(): never { function todo(): never {
@@ -197,7 +198,7 @@ export class SystemForEmbassy implements System {
readonly moduleCode: Partial<U.ExpectedExports>, readonly moduleCode: Partial<U.ExpectedExports>,
) {} ) {}
async execute( async execute(
effects: HostSystemStartOs, effectCreator: ReturnType<typeof hostSystemStartOs>,
options: { options: {
id: string id: string
procedure: JsonPath procedure: JsonPath
@@ -205,8 +206,7 @@ export class SystemForEmbassy implements System {
timeout?: number | undefined timeout?: number | undefined
}, },
): Promise<RpcResult> { ): Promise<RpcResult> {
effects = Object.create(effects) const effects = effectCreator(options.id)
effects.procedureId = options.id
return this._execute(effects, options) return this._execute(effects, options)
.then((x) => .then((x) =>
matches(x) matches(x)
@@ -261,12 +261,12 @@ export class SystemForEmbassy implements System {
} }
}) })
} }
async exit(effects: HostSystemStartOs): Promise<void> { async exit(): Promise<void> {
if (this.currentRunning) await this.currentRunning.clean() if (this.currentRunning) await this.currentRunning.clean()
delete this.currentRunning delete this.currentRunning
} }
async _execute( async _execute(
effects: HostSystemStartOs, effects: Effects,
options: { options: {
procedure: JsonPath procedure: JsonPath
input: unknown input: unknown
@@ -340,7 +340,7 @@ export class SystemForEmbassy implements System {
throw new Error(`Could not find the path for ${options.procedure}`) throw new Error(`Could not find the path for ${options.procedure}`)
} }
private async init( private async init(
effects: HostSystemStartOs, effects: Effects,
previousVersion: Optional<string>, previousVersion: Optional<string>,
timeoutMs: number | null, timeoutMs: number | null,
): Promise<void> { ): Promise<void> {
@@ -350,7 +350,7 @@ export class SystemForEmbassy implements System {
await this.exportActions(effects) await this.exportActions(effects)
await this.exportNetwork(effects) await this.exportNetwork(effects)
} }
async exportNetwork(effects: HostSystemStartOs) { async exportNetwork(effects: Effects) {
for (const [id, interfaceValue] of Object.entries( for (const [id, interfaceValue] of Object.entries(
this.manifest.interfaces, this.manifest.interfaces,
)) { )) {
@@ -428,7 +428,7 @@ export class SystemForEmbassy implements System {
) )
} }
} }
async exportActions(effects: HostSystemStartOs) { async exportActions(effects: Effects) {
const manifest = this.manifest const manifest = this.manifest
if (!manifest.actions) return if (!manifest.actions) return
for (const [actionId, action] of Object.entries(manifest.actions)) { for (const [actionId, action] of Object.entries(manifest.actions)) {
@@ -457,7 +457,7 @@ export class SystemForEmbassy implements System {
} }
} }
private async uninit( private async uninit(
effects: HostSystemStartOs, effects: Effects,
nextVersion: Optional<string>, nextVersion: Optional<string>,
timeoutMs: number | null, timeoutMs: number | null,
): Promise<void> { ): Promise<void> {
@@ -465,7 +465,7 @@ export class SystemForEmbassy implements System {
await effects.setMainStatus({ status: "stopped" }) await effects.setMainStatus({ status: "stopped" })
} }
private async mainStart( private async mainStart(
effects: HostSystemStartOs, effects: Effects,
timeoutMs: number | null, timeoutMs: number | null,
): Promise<void> { ): Promise<void> {
if (!!this.currentRunning) return if (!!this.currentRunning) return
@@ -473,7 +473,7 @@ export class SystemForEmbassy implements System {
this.currentRunning = new MainLoop(this, effects) this.currentRunning = new MainLoop(this, effects)
} }
private async mainStop( private async mainStop(
effects: HostSystemStartOs, effects: Effects,
timeoutMs: number | null, timeoutMs: number | null,
): Promise<Duration> { ): Promise<Duration> {
const { currentRunning } = this const { currentRunning } = this
@@ -491,7 +491,7 @@ export class SystemForEmbassy implements System {
return durationValue return durationValue
} }
private async createBackup( private async createBackup(
effects: HostSystemStartOs, effects: Effects,
timeoutMs: number | null, timeoutMs: number | null,
): Promise<void> { ): Promise<void> {
const backup = this.manifest.backup.create const backup = this.manifest.backup.create
@@ -503,13 +503,11 @@ export class SystemForEmbassy implements System {
await container.execFail([backup.entrypoint, ...backup.args], timeoutMs) await container.execFail([backup.entrypoint, ...backup.args], timeoutMs)
} else { } else {
const moduleCode = await this.moduleCode const moduleCode = await this.moduleCode
await moduleCode.createBackup?.( await moduleCode.createBackup?.(polyfillEffects(effects, this.manifest))
new PolyfillEffects(effects, this.manifest),
)
} }
} }
private async restoreBackup( private async restoreBackup(
effects: HostSystemStartOs, effects: Effects,
timeoutMs: number | null, timeoutMs: number | null,
): Promise<void> { ): Promise<void> {
const restoreBackup = this.manifest.backup.restore const restoreBackup = this.manifest.backup.restore
@@ -528,19 +526,17 @@ export class SystemForEmbassy implements System {
) )
} else { } else {
const moduleCode = await this.moduleCode const moduleCode = await this.moduleCode
await moduleCode.restoreBackup?.( await moduleCode.restoreBackup?.(polyfillEffects(effects, this.manifest))
new PolyfillEffects(effects, this.manifest),
)
} }
} }
private async getConfig( private async getConfig(
effects: HostSystemStartOs, effects: Effects,
timeoutMs: number | null, timeoutMs: number | null,
): Promise<T.ConfigRes> { ): Promise<T.ConfigRes> {
return this.getConfigUncleaned(effects, timeoutMs).then(removePointers) return this.getConfigUncleaned(effects, timeoutMs).then(removePointers)
} }
private async getConfigUncleaned( private async getConfigUncleaned(
effects: HostSystemStartOs, effects: Effects,
timeoutMs: number | null, timeoutMs: number | null,
): Promise<T.ConfigRes> { ): Promise<T.ConfigRes> {
const config = this.manifest.config?.get const config = this.manifest.config?.get
@@ -564,7 +560,7 @@ export class SystemForEmbassy implements System {
const moduleCode = await this.moduleCode const moduleCode = await this.moduleCode
const method = moduleCode.getConfig const method = moduleCode.getConfig
if (!method) throw new Error("Expecting that the method getConfig exists") if (!method) throw new Error("Expecting that the method getConfig exists")
return (await method(new PolyfillEffects(effects, this.manifest)).then( return (await method(polyfillEffects(effects, this.manifest)).then(
(x) => { (x) => {
if ("result" in x) return x.result if ("result" in x) return x.result
if ("error" in x) throw new Error("Error getting config: " + x.error) if ("error" in x) throw new Error("Error getting config: " + x.error)
@@ -574,7 +570,7 @@ export class SystemForEmbassy implements System {
} }
} }
private async setConfig( private async setConfig(
effects: HostSystemStartOs, effects: Effects,
newConfigWithoutPointers: unknown, newConfigWithoutPointers: unknown,
timeoutMs: number | null, timeoutMs: number | null,
): Promise<void> { ): Promise<void> {
@@ -617,7 +613,7 @@ export class SystemForEmbassy implements System {
const answer = matchSetResult.unsafeCast( const answer = matchSetResult.unsafeCast(
await method( await method(
new PolyfillEffects(effects, this.manifest), polyfillEffects(effects, this.manifest),
newConfig as U.Config, newConfig as U.Config,
).then((x): T.SetResult => { ).then((x): T.SetResult => {
if ("result" in x) if ("result" in x)
@@ -636,7 +632,7 @@ export class SystemForEmbassy implements System {
} }
} }
private async setConfigSetConfig( private async setConfigSetConfig(
effects: HostSystemStartOs, effects: Effects,
dependsOn: { [x: string]: readonly string[] }, dependsOn: { [x: string]: readonly string[] },
) { ) {
await effects.setDependencies({ await effects.setDependencies({
@@ -660,7 +656,7 @@ export class SystemForEmbassy implements System {
} }
private async migration( private async migration(
effects: HostSystemStartOs, effects: Effects,
fromVersion: string, fromVersion: string,
timeoutMs: number | null, timeoutMs: number | null,
): Promise<T.MigrationRes> { ): Promise<T.MigrationRes> {
@@ -713,7 +709,7 @@ export class SystemForEmbassy implements System {
if (!method) if (!method)
throw new Error("Expecting that the method migration exists") throw new Error("Expecting that the method migration exists")
return (await method( return (await method(
new PolyfillEffects(effects, this.manifest), polyfillEffects(effects, this.manifest),
fromVersion as string, fromVersion as string,
).then((x) => { ).then((x) => {
if ("result" in x) return x.result if ("result" in x) return x.result
@@ -725,7 +721,7 @@ export class SystemForEmbassy implements System {
return { configured: true } return { configured: true }
} }
private async properties( private async properties(
effects: HostSystemStartOs, effects: Effects,
timeoutMs: number | null, timeoutMs: number | null,
): Promise<ReturnType<T.ExpectedExports.properties>> { ): Promise<ReturnType<T.ExpectedExports.properties>> {
// TODO BLU-J set the properties ever so often // TODO BLU-J set the properties ever so often
@@ -754,7 +750,7 @@ export class SystemForEmbassy implements System {
if (!method) if (!method)
throw new Error("Expecting that the method properties exists") throw new Error("Expecting that the method properties exists")
const properties = matchProperties.unsafeCast( const properties = matchProperties.unsafeCast(
await method(new PolyfillEffects(effects, this.manifest)).then((x) => { await method(polyfillEffects(effects, this.manifest)).then((x) => {
if ("result" in x) return x.result if ("result" in x) return x.result
if ("error" in x) throw new Error("Error getting config: " + x.error) if ("error" in x) throw new Error("Error getting config: " + x.error)
throw new Error("Error getting config: " + x["error-code"][1]) throw new Error("Error getting config: " + x["error-code"][1])
@@ -765,7 +761,7 @@ export class SystemForEmbassy implements System {
throw new Error(`Unknown type in the fetch properties: ${setConfigValue}`) throw new Error(`Unknown type in the fetch properties: ${setConfigValue}`)
} }
private async action( private async action(
effects: HostSystemStartOs, effects: Effects,
actionId: string, actionId: string,
formData: unknown, formData: unknown,
timeoutMs: number | null, timeoutMs: number | null,
@@ -795,7 +791,7 @@ export class SystemForEmbassy implements System {
const method = moduleCode.action?.[actionId] const method = moduleCode.action?.[actionId]
if (!method) throw new Error("Expecting that the method action exists") if (!method) throw new Error("Expecting that the method action exists")
return (await method( return (await method(
new PolyfillEffects(effects, this.manifest), polyfillEffects(effects, this.manifest),
formData as any, formData as any,
).then((x) => { ).then((x) => {
if ("result" in x) return x.result if ("result" in x) return x.result
@@ -805,7 +801,7 @@ export class SystemForEmbassy implements System {
} }
} }
private async dependenciesCheck( private async dependenciesCheck(
effects: HostSystemStartOs, effects: Effects,
id: string, id: string,
oldConfig: unknown, oldConfig: unknown,
timeoutMs: number | null, timeoutMs: number | null,
@@ -838,7 +834,7 @@ export class SystemForEmbassy implements System {
`Expecting that the method dependency check ${id} exists`, `Expecting that the method dependency check ${id} exists`,
) )
return (await method( return (await method(
new PolyfillEffects(effects, this.manifest), polyfillEffects(effects, this.manifest),
oldConfig as any, oldConfig as any,
).then((x) => { ).then((x) => {
if ("result" in x) return x.result if ("result" in x) return x.result
@@ -850,7 +846,7 @@ export class SystemForEmbassy implements System {
} }
} }
private async dependenciesAutoconfig( private async dependenciesAutoconfig(
effects: HostSystemStartOs, effects: Effects,
id: string, id: string,
oldConfig: unknown, oldConfig: unknown,
timeoutMs: number | null, timeoutMs: number | null,
@@ -863,7 +859,7 @@ export class SystemForEmbassy implements System {
`Expecting that the method dependency autoConfigure ${id} exists`, `Expecting that the method dependency autoConfigure ${id} exists`,
) )
return (await method( return (await method(
new PolyfillEffects(effects, this.manifest), polyfillEffects(effects, this.manifest),
oldConfig as any, oldConfig as any,
).then((x) => { ).then((x) => {
if ("result" in x) return x.result if ("result" in x) return x.result
@@ -961,7 +957,7 @@ function cleanConfigFromPointers<C, S>(
} }
async function updateConfig( async function updateConfig(
effects: HostSystemStartOs, effects: Effects,
manifest: Manifest, manifest: Manifest,
spec: unknown, spec: unknown,
mutConfigValue: unknown, mutConfigValue: unknown,

View File

@@ -4,390 +4,175 @@ import { Volume } from "../../../Models/Volume"
import * as child_process from "child_process" import * as child_process from "child_process"
import { promisify } from "util" import { promisify } from "util"
import { daemons, startSdk, T } from "@start9labs/start-sdk" import { daemons, startSdk, T } from "@start9labs/start-sdk"
import { HostSystemStartOs } from "../../HostSystemStartOs"
import "isomorphic-fetch" import "isomorphic-fetch"
import { Manifest } from "./matchManifest" import { Manifest } from "./matchManifest"
import { DockerProcedureContainer } from "./DockerProcedureContainer" import { DockerProcedureContainer } from "./DockerProcedureContainer"
import * as cp from "child_process" import * as cp from "child_process"
import { Effects } from "../../../Models/Effects"
export const execFile = promisify(cp.execFile) export const execFile = promisify(cp.execFile)
export class PolyfillEffects implements oet.Effects { export const polyfillEffects = (
constructor( effects: Effects,
readonly effects: HostSystemStartOs, manifest: Manifest,
private manifest: Manifest, ): oet.Effects => {
) {} const self = {
async writeFile(input: { effects,
path: string manifest,
volumeId: string async writeFile(input: {
toWrite: string path: string
}): Promise<void> { volumeId: string
await fs.writeFile( toWrite: string
new Volume(input.volumeId, input.path).path, }): Promise<void> {
input.toWrite, await fs.writeFile(
) new Volume(input.volumeId, input.path).path,
} input.toWrite,
async readFile(input: { volumeId: string; path: string }): Promise<string> { )
return ( },
await fs.readFile(new Volume(input.volumeId, input.path).path) async readFile(input: { volumeId: string; path: string }): Promise<string> {
).toString() return (
}
async metadata(input: {
volumeId: string
path: string
}): Promise<oet.Metadata> {
const stats = await fs.stat(new Volume(input.volumeId, input.path).path)
return {
fileType: stats.isFile() ? "file" : "directory",
gid: stats.gid,
uid: stats.uid,
mode: stats.mode,
isDir: stats.isDirectory(),
isFile: stats.isFile(),
isSymlink: stats.isSymbolicLink(),
len: stats.size,
readonly: (stats.mode & 0o200) > 0,
}
}
async createDir(input: { volumeId: string; path: string }): Promise<string> {
const path = new Volume(input.volumeId, input.path).path
await fs.mkdir(path, { recursive: true })
return path
}
async readDir(input: { volumeId: string; path: string }): Promise<string[]> {
return fs.readdir(new Volume(input.volumeId, input.path).path)
}
async removeDir(input: { volumeId: string; path: string }): Promise<string> {
const path = new Volume(input.volumeId, input.path).path
await fs.rmdir(new Volume(input.volumeId, input.path).path, {
recursive: true,
})
return path
}
removeFile(input: { volumeId: string; path: string }): Promise<void> {
return fs.rm(new Volume(input.volumeId, input.path).path)
}
async writeJsonFile(input: {
volumeId: string
path: string
toWrite: Record<string, unknown>
}): Promise<void> {
await fs.writeFile(
new Volume(input.volumeId, input.path).path,
JSON.stringify(input.toWrite),
)
}
async readJsonFile(input: {
volumeId: string
path: string
}): Promise<Record<string, unknown>> {
return JSON.parse(
(
await fs.readFile(new Volume(input.volumeId, input.path).path) await fs.readFile(new Volume(input.volumeId, input.path).path)
).toString(), ).toString()
) },
} async metadata(input: {
runCommand({ volumeId: string
command, path: string
args, }): Promise<oet.Metadata> {
timeoutMillis, const stats = await fs.stat(new Volume(input.volumeId, input.path).path)
}: { return {
command: string fileType: stats.isFile() ? "file" : "directory",
args?: string[] | undefined gid: stats.gid,
timeoutMillis?: number | undefined uid: stats.uid,
}): Promise<oet.ResultType<string>> { mode: stats.mode,
return startSdk isDir: stats.isDirectory(),
.runCommand( isFile: stats.isFile(),
this.effects, isSymlink: stats.isSymbolicLink(),
{ id: this.manifest.main.image }, len: stats.size,
[command, ...(args || [])], readonly: (stats.mode & 0o200) > 0,
{}, }
},
async createDir(input: {
volumeId: string
path: string
}): Promise<string> {
const path = new Volume(input.volumeId, input.path).path
await fs.mkdir(path, { recursive: true })
return path
},
async readDir(input: {
volumeId: string
path: string
}): Promise<string[]> {
return fs.readdir(new Volume(input.volumeId, input.path).path)
},
async removeDir(input: {
volumeId: string
path: string
}): Promise<string> {
const path = new Volume(input.volumeId, input.path).path
await fs.rmdir(new Volume(input.volumeId, input.path).path, {
recursive: true,
})
return path
},
removeFile(input: { volumeId: string; path: string }): Promise<void> {
return fs.rm(new Volume(input.volumeId, input.path).path)
},
async writeJsonFile(input: {
volumeId: string
path: string
toWrite: Record<string, unknown>
}): Promise<void> {
await fs.writeFile(
new Volume(input.volumeId, input.path).path,
JSON.stringify(input.toWrite),
) )
.then((x: any) => ({ },
stderr: x.stderr.toString(), async readJsonFile(input: {
stdout: x.stdout.toString(), volumeId: string
})) path: string
.then((x: any) => }): Promise<Record<string, unknown>> {
!!x.stderr ? { error: x.stderr } : { result: x.stdout }, return JSON.parse(
(
await fs.readFile(new Volume(input.volumeId, input.path).path)
).toString(),
) )
} },
runDaemon(input: { command: string; args?: string[] | undefined }): { runCommand({
wait(): Promise<oet.ResultType<string>> command,
term(): Promise<void> args,
} { timeoutMillis,
const dockerProcedureContainer = DockerProcedureContainer.of( }: {
this.effects, command: string
this.manifest.main, args?: string[] | undefined
this.manifest.volumes, timeoutMillis?: number | undefined
) }): Promise<oet.ResultType<string>> {
const daemon = dockerProcedureContainer.then((dockerProcedureContainer) => return startSdk
daemons.runCommand()( .runCommand(
this.effects, effects,
{ id: this.manifest.main.image }, { id: manifest.main.image },
[input.command, ...(input.args || [])], [command, ...(args || [])],
{ {},
overlay: dockerProcedureContainer.overlay, )
}, .then((x: any) => ({
), stderr: x.stderr.toString(),
) stdout: x.stdout.toString(),
return { }))
wait: () => .then((x: any) =>
daemon.then((daemon) => !!x.stderr ? { error: x.stderr } : { result: x.stdout },
daemon.wait().then(() => { )
return { result: "" } },
}), runDaemon(input: { command: string; args?: string[] | undefined }): {
wait(): Promise<oet.ResultType<string>>
term(): Promise<void>
} {
const dockerProcedureContainer = DockerProcedureContainer.of(
effects,
manifest.main,
manifest.volumes,
)
const daemon = dockerProcedureContainer.then((dockerProcedureContainer) =>
daemons.runCommand()(
effects,
{ id: manifest.main.image },
[input.command, ...(input.args || [])],
{
overlay: dockerProcedureContainer.overlay,
},
), ),
term: () => daemon.then((daemon) => daemon.term()),
}
}
async chown(input: {
volumeId: string
path: string
uid: string
}): Promise<null> {
await startSdk
.runCommand(
this.effects,
{ id: this.manifest.main.image },
["chown", "--recursive", input.uid, `/drive/${input.path}`],
{
mounts: [
{
path: "/drive",
options: {
type: "volume",
id: input.volumeId,
subpath: null,
readonly: false,
},
},
],
},
) )
.then((x: any) => ({ return {
stderr: x.stderr.toString(), wait: () =>
stdout: x.stdout.toString(), daemon.then((daemon) =>
})) daemon.wait().then(() => {
.then((x: any) => { return { result: "" }
if (!!x.stderr) { }),
throw new Error(x.stderr) ),
} term: () => daemon.then((daemon) => daemon.term()),
}) }
return null },
} async chown(input: {
async chmod(input: { volumeId: string
volumeId: string path: string
path: string uid: string
mode: string }): Promise<null> {
}): Promise<null> { await startSdk
await startSdk .runCommand(
.runCommand( effects,
this.effects, { id: manifest.main.image },
{ id: this.manifest.main.image }, ["chown", "--recursive", input.uid, `/drive/${input.path}`],
["chmod", "--recursive", input.mode, `/drive/${input.path}`], {
{ mounts: [
mounts: [ {
{ path: "/drive",
path: "/drive", options: {
options: { type: "volume",
type: "volume", id: input.volumeId,
id: input.volumeId, subpath: null,
subpath: null, readonly: false,
readonly: false, },
}, },
}, ],
], },
}, )
)
.then((x: any) => ({
stderr: x.stderr.toString(),
stdout: x.stdout.toString(),
}))
.then((x: any) => {
if (!!x.stderr) {
throw new Error(x.stderr)
}
})
return null
}
sleep(timeMs: number): Promise<null> {
return new Promise((resolve) => setTimeout(resolve, timeMs))
}
trace(whatToPrint: string): void {
console.trace(whatToPrint)
}
warn(whatToPrint: string): void {
console.warn(whatToPrint)
}
error(whatToPrint: string): void {
console.error(whatToPrint)
}
debug(whatToPrint: string): void {
console.debug(whatToPrint)
}
info(whatToPrint: string): void {
console.log(false)
}
is_sandboxed(): boolean {
return false
}
exists(input: { volumeId: string; path: string }): Promise<boolean> {
return this.metadata(input)
.then(() => true)
.catch(() => false)
}
async fetch(
url: string,
options?:
| {
method?:
| "GET"
| "POST"
| "PUT"
| "DELETE"
| "HEAD"
| "PATCH"
| undefined
headers?: Record<string, string> | undefined
body?: string | undefined
}
| undefined,
): Promise<{
method: string
ok: boolean
status: number
headers: Record<string, string>
body?: string | null | undefined
text(): Promise<string>
json(): Promise<unknown>
}> {
const fetched = await fetch(url, options)
return {
method: fetched.type,
ok: fetched.ok,
status: fetched.status,
headers: Object.fromEntries(fetched.headers.entries()),
body: await fetched.text(),
text: () => fetched.text(),
json: () => fetched.json(),
}
}
runRsync(rsyncOptions: {
srcVolume: string
dstVolume: string
srcPath: string
dstPath: string
options: oet.BackupOptions
}): {
id: () => Promise<string>
wait: () => Promise<null>
progress: () => Promise<number>
} {
let secondRun: ReturnType<typeof this._runRsync> | undefined
let firstRun = this._runRsync(rsyncOptions)
let waitValue = firstRun.wait().then((x) => {
secondRun = this._runRsync(rsyncOptions)
return secondRun.wait()
})
const id = async () => {
return secondRun?.id?.() ?? firstRun.id()
}
const wait = () => waitValue
const progress = async () => {
const secondProgress = secondRun?.progress?.()
if (secondProgress) {
return (await secondProgress) / 2.0 + 0.5
}
return (await firstRun.progress()) / 2.0
}
return { id, wait, progress }
}
_runRsync(rsyncOptions: {
srcVolume: string
dstVolume: string
srcPath: string
dstPath: string
options: oet.BackupOptions
}): {
id: () => Promise<string>
wait: () => Promise<null>
progress: () => Promise<number>
} {
const { srcVolume, dstVolume, srcPath, dstPath, options } = rsyncOptions
const command = "rsync"
const args: string[] = []
if (options.delete) {
args.push("--delete")
}
if (options.force) {
args.push("--force")
}
if (options.ignoreExisting) {
args.push("--ignore-existing")
}
for (const exclude of options.exclude) {
args.push(`--exclude=${exclude}`)
}
args.push("-actAXH")
args.push("--info=progress2")
args.push("--no-inc-recursive")
args.push(new Volume(srcVolume, srcPath).path)
args.push(new Volume(dstVolume, dstPath).path)
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(String(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 }
}
async diskUsage(
options?: { volumeId: string; path: string } | undefined,
): Promise<{ used: number; total: number }> {
const output = await execFile("df", ["--block-size=1", "-P", "/"])
.then((x: any) => ({
stderr: x.stderr.toString(),
stdout: x.stdout.toString(),
}))
.then((x: any) => {
if (!!x.stderr) {
throw new Error(x.stderr)
}
return parseDfOutput(x.stdout)
})
if (!!options) {
const used = await execFile("du", [
"-s",
"--block-size=1",
"-P",
new Volume(options.volumeId, options.path).path,
])
.then((x: any) => ({ .then((x: any) => ({
stderr: x.stderr.toString(), stderr: x.stderr.toString(),
stdout: x.stdout.toString(), stdout: x.stdout.toString(),
@@ -396,15 +181,244 @@ export class PolyfillEffects implements oet.Effects {
if (!!x.stderr) { if (!!x.stderr) {
throw new Error(x.stderr) throw new Error(x.stderr)
} }
return Number.parseInt(x.stdout.split(/\s+/)[0])
}) })
return null
},
async chmod(input: {
volumeId: string
path: string
mode: string
}): Promise<null> {
await startSdk
.runCommand(
effects,
{ id: manifest.main.image },
["chmod", "--recursive", input.mode, `/drive/${input.path}`],
{
mounts: [
{
path: "/drive",
options: {
type: "volume",
id: input.volumeId,
subpath: null,
readonly: false,
},
},
],
},
)
.then((x: any) => ({
stderr: x.stderr.toString(),
stdout: x.stdout.toString(),
}))
.then((x: any) => {
if (!!x.stderr) {
throw new Error(x.stderr)
}
})
return null
},
sleep(timeMs: number): Promise<null> {
return new Promise((resolve) => setTimeout(resolve, timeMs))
},
trace(whatToPrint: string): void {
console.trace(whatToPrint)
},
warn(whatToPrint: string): void {
console.warn(whatToPrint)
},
error(whatToPrint: string): void {
console.error(whatToPrint)
},
debug(whatToPrint: string): void {
console.debug(whatToPrint)
},
info(whatToPrint: string): void {
console.log(false)
},
is_sandboxed(): boolean {
return false
},
exists(input: { volumeId: string; path: string }): Promise<boolean> {
return self
.metadata(input)
.then(() => true)
.catch(() => false)
},
async fetch(
url: string,
options?:
| {
method?:
| "GET"
| "POST"
| "PUT"
| "DELETE"
| "HEAD"
| "PATCH"
| undefined
headers?: Record<string, string> | undefined
body?: string | undefined
}
| undefined,
): Promise<{
method: string
ok: boolean
status: number
headers: Record<string, string>
body?: string | null | undefined
text(): Promise<string>
json(): Promise<unknown>
}> {
const fetched = await fetch(url, options)
return { return {
...output, method: fetched.type,
used, ok: fetched.ok,
status: fetched.status,
headers: Object.fromEntries(fetched.headers.entries()),
body: await fetched.text(),
text: () => fetched.text(),
json: () => fetched.json(),
} }
} },
return output
runRsync(rsyncOptions: {
srcVolume: string
dstVolume: string
srcPath: string
dstPath: string
options: oet.BackupOptions
}): {
id: () => Promise<string>
wait: () => Promise<null>
progress: () => Promise<number>
} {
let secondRun: ReturnType<typeof self._runRsync> | undefined
let firstRun = self._runRsync(rsyncOptions)
let waitValue = firstRun.wait().then((x) => {
secondRun = self._runRsync(rsyncOptions)
return secondRun.wait()
})
const id = async () => {
return secondRun?.id?.() ?? firstRun.id()
}
const wait = () => waitValue
const progress = async () => {
const secondProgress = secondRun?.progress?.()
if (secondProgress) {
return (await secondProgress) / 2.0 + 0.5
}
return (await firstRun.progress()) / 2.0
}
return { id, wait, progress }
},
_runRsync(rsyncOptions: {
srcVolume: string
dstVolume: string
srcPath: string
dstPath: string
options: oet.BackupOptions
}): {
id: () => Promise<string>
wait: () => Promise<null>
progress: () => Promise<number>
} {
const { srcVolume, dstVolume, srcPath, dstPath, options } = rsyncOptions
const command = "rsync"
const args: string[] = []
if (options.delete) {
args.push("--delete")
}
if (options.force) {
args.push("--force")
}
if (options.ignoreExisting) {
args.push("--ignore-existing")
}
for (const exclude of options.exclude) {
args.push(`--exclude=${exclude}`)
}
args.push("-actAXH")
args.push("--info=progress2")
args.push("--no-inc-recursive")
args.push(new Volume(srcVolume, srcPath).path)
args.push(new Volume(dstVolume, dstPath).path)
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(String(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 }
},
async diskUsage(
options?: { volumeId: string; path: string } | undefined,
): Promise<{ used: number; total: number }> {
const output = await execFile("df", ["--block-size=1", "-P", "/"])
.then((x: any) => ({
stderr: x.stderr.toString(),
stdout: x.stdout.toString(),
}))
.then((x: any) => {
if (!!x.stderr) {
throw new Error(x.stderr)
}
return parseDfOutput(x.stdout)
})
if (!!options) {
const used = await execFile("du", [
"-s",
"--block-size=1",
"-P",
new Volume(options.volumeId, options.path).path,
])
.then((x: any) => ({
stderr: x.stderr.toString(),
stdout: x.stdout.toString(),
}))
.then((x: any) => {
if (!!x.stderr) {
throw new Error(x.stderr)
}
return Number.parseInt(x.stdout.split(/\s+/)[0])
})
return {
...output,
used,
}
}
return output
},
} }
return self
} }
function parseDfOutput(output: string): { used: number; total: number } { function parseDfOutput(output: string): { used: number; total: number } {

View File

@@ -1,7 +1,7 @@
import { ExecuteResult, System } from "../../Interfaces/System" import { ExecuteResult, System } from "../../Interfaces/System"
import { unNestPath } from "../../Models/JsonPath" import { unNestPath } from "../../Models/JsonPath"
import matches, { any, number, object, string, tuple } from "ts-matches" import matches, { any, number, object, string, tuple } from "ts-matches"
import { HostSystemStartOs } from "../HostSystemStartOs" import { hostSystemStartOs } from "../HostSystemStartOs"
import { Effects } from "../../Models/Effects" import { Effects } from "../../Models/Effects"
import { RpcResult, matchRpcResult } from "../RpcListener" import { RpcResult, matchRpcResult } from "../RpcListener"
import { duration } from "../../Models/Duration" import { duration } from "../../Models/Duration"
@@ -15,7 +15,7 @@ export class SystemForStartOs implements System {
} }
constructor(readonly abi: T.ABI) {} constructor(readonly abi: T.ABI) {}
async execute( async execute(
effects: HostSystemStartOs, effectCreator: ReturnType<typeof hostSystemStartOs>,
options: { options: {
id: string id: string
procedure: procedure:
@@ -36,8 +36,7 @@ export class SystemForStartOs implements System {
timeout?: number | undefined timeout?: number | undefined
}, },
): Promise<RpcResult> { ): Promise<RpcResult> {
effects = Object.create(effects) const effects = effectCreator(options.id)
effects.procedureId = options.id
return this._execute(effects, options) return this._execute(effects, options)
.then((x) => .then((x) =>
matches(x) matches(x)

View File

@@ -2,6 +2,7 @@ import { types as T } from "@start9labs/start-sdk"
import { CallbackHolder } from "../Models/CallbackHolder" import { CallbackHolder } from "../Models/CallbackHolder"
import { Effects } from "../Models/Effects" import { Effects } from "../Models/Effects"
export type HostSystem = Effects export type HostSystem = Effects
export type GetHostSystem = (callbackHolder: CallbackHolder) => HostSystem export type GetHostSystem = (
callbackHolder: CallbackHolder,
) => (procedureId: null | string) => Effects

View File

@@ -1,7 +1,7 @@
import { types as T } from "@start9labs/start-sdk" import { types as T } from "@start9labs/start-sdk"
import { JsonPath } from "../Models/JsonPath" import { JsonPath } from "../Models/JsonPath"
import { HostSystemStartOs } from "../Adapters/HostSystemStartOs"
import { RpcResult } from "../Adapters/RpcListener" import { RpcResult } from "../Adapters/RpcListener"
import { hostSystemStartOs } from "../Adapters/HostSystemStartOs"
export type ExecuteResult = export type ExecuteResult =
| { ok: unknown } | { ok: unknown }
| { err: { code: number; message: string } } | { err: { code: number; message: string } }
@@ -12,7 +12,7 @@ export interface System {
// stop(effects: Effects, options: { timeout: number, signal?: number }): Promise<void> // stop(effects: Effects, options: { timeout: number, signal?: number }): Promise<void>
execute( execute(
effects: T.Effects, effectCreator: ReturnType<typeof hostSystemStartOs>,
options: { options: {
id: string id: string
procedure: JsonPath procedure: JsonPath

View File

@@ -1,12 +1,12 @@
import { RpcListener } from "./Adapters/RpcListener" import { RpcListener } from "./Adapters/RpcListener"
import { SystemForEmbassy } from "./Adapters/Systems/SystemForEmbassy" import { SystemForEmbassy } from "./Adapters/Systems/SystemForEmbassy"
import { HostSystemStartOs } from "./Adapters/HostSystemStartOs" import { hostSystemStartOs } from "./Adapters/HostSystemStartOs"
import { AllGetDependencies } from "./Interfaces/AllGetDependencies" import { AllGetDependencies } from "./Interfaces/AllGetDependencies"
import { getSystem } from "./Adapters/Systems" import { getSystem } from "./Adapters/Systems"
const getDependencies: AllGetDependencies = { const getDependencies: AllGetDependencies = {
system: getSystem, system: getSystem,
hostSystem: () => HostSystemStartOs.of, hostSystem: () => hostSystemStartOs,
} }
new RpcListener(getDependencies) new RpcListener(getDependencies)

View File

@@ -16,7 +16,8 @@ mkdir -p ./firmware/$PLATFORM
cd ./firmware/$PLATFORM cd ./firmware/$PLATFORM
mapfile -t firmwares <<< "$(jq -c ".[] | select(.platform[] | contains(\"$PLATFORM\"))" ../../build/lib/firmware.json)" firmwares=()
while IFS= read -r line; do firmwares+=("$line"); done < <(jq -c ".[] | select(.platform[] | contains(\"$PLATFORM\"))" ../../build/lib/firmware.json)
for firmware in "${firmwares[@]}"; do for firmware in "${firmwares[@]}"; do
if [ -n "$firmware" ]; then if [ -n "$firmware" ]; then
id=$(echo "$firmware" | jq --raw-output '.id') id=$(echo "$firmware" | jq --raw-output '.id')