import { ExtendedVersion, types as T, utils, VersionRange, } from "@start9labs/start-sdk" import * as net from "net" import { object, string, number, literals, some, unknown } from "ts-matches" import { Effects } from "../Models/Effects" import { CallbackHolder } from "../Models/CallbackHolder" import { asError } from "@start9labs/start-sdk/base/lib/util" const matchRpcError = object({ error: object({ code: number, message: string, data: some( string, object({ details: string, debug: string.nullable().optional(), }), ) .nullable() .optional(), }), }) const testRpcError = matchRpcError.test const testRpcResult = object({ result: unknown, }).test type RpcError = typeof matchRpcError._TYPE const SOCKET_PATH = "/media/startos/rpc/host.sock" let hostSystemId = 0 export type EffectContext = { eventId: string | null callbacks?: CallbackHolder constRetry?: () => void } const rpcRoundFor = (eventId: string | null) => ( method: K, params: Record, ) => { const id = hostSystemId++ const client = net.createConnection({ path: SOCKET_PATH }, () => { client.write( JSON.stringify({ id, method, params: { ...params, eventId: eventId ?? undefined }, }) + "\n", ) }) let bufs: Buffer[] = [] return new Promise((resolve, reject) => { client.on("data", (data) => { try { bufs.push(data) if (data.reduce((acc, x) => acc || x == 10, false)) { const res: unknown = JSON.parse( Buffer.concat(bufs).toString().split("\n")[0], ) if (testRpcError(res)) { let message = res.error.message console.error( "Error in host RPC:", utils.asError({ method, params, error: res.error }), ) if (string.test(res.error.data)) { message += ": " + res.error.data console.error(`Details: ${res.error.data}`) } else { if (res.error.data?.details) { message += ": " + res.error.data.details console.error(`Details: ${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) } client.end() }) client.on("error", (error) => { reject(error) }) }) } export function makeEffects(context: EffectContext): Effects { const rpcRound = rpcRoundFor(context.eventId) const self: Effects = { eventId: context.eventId, child: (name) => makeEffects({ ...context, callbacks: context.callbacks?.child(name) }), constRetry: context.constRetry, isInContext: !!context.callbacks, onLeaveContext: context.callbacks?.onLeaveContext?.bind(context.callbacks) || (() => { console.warn( "no context for this effects object", new Error().stack?.replace(/^Error/, ""), ) }), clearCallbacks(...[options]: Parameters) { return rpcRound("clear-callbacks", { ...options, }) as ReturnType }, action: { clear(...[options]: Parameters) { return rpcRound("action.clear", { ...options, }) as ReturnType }, export(...[options]: Parameters) { return rpcRound("action.export", { ...options, }) as ReturnType }, getInput(...[options]: Parameters) { return rpcRound("action.get-input", { ...options, }) as ReturnType }, createTask(...[options]: Parameters) { return rpcRound("action.create-task", { ...options, }) as ReturnType }, run(...[options]: Parameters) { return rpcRound("action.run", { ...options, }) as ReturnType }, clearTasks(...[options]: Parameters) { return rpcRound("action.clear-tasks", { ...options, }) as ReturnType }, }, bind(...[options]: Parameters) { return rpcRound("bind", { ...options, stack: new Error().stack, }) as ReturnType }, clearBindings(...[options]: Parameters) { return rpcRound("clear-bindings", { ...options }) as ReturnType< T.Effects["clearBindings"] > }, clearServiceInterfaces( ...[options]: Parameters ) { return rpcRound("clear-service-interfaces", { ...options }) as ReturnType< T.Effects["clearServiceInterfaces"] > }, getInstalledPackages(...[]: Parameters) { return rpcRound("get-installed-packages", {}) as ReturnType< T.Effects["getInstalledPackages"] > }, subcontainer: { createFs(options: { imageId: string; name: string }) { return rpcRound("subcontainer.create-fs", options) as ReturnType< T.Effects["subcontainer"]["createFs"] > }, destroyFs(options: { guid: string }): Promise { return rpcRound("subcontainer.destroy-fs", options) as ReturnType< T.Effects["subcontainer"]["destroyFs"] > }, }, exportServiceInterface: (( ...[options]: Parameters ) => { return rpcRound("export-service-interface", options) as ReturnType< T.Effects["exportServiceInterface"] > }) as Effects["exportServiceInterface"], getContainerIp(...[options]: Parameters) { return rpcRound("get-container-ip", options) as ReturnType< T.Effects["getContainerIp"] > }, getOsIp(...[]: Parameters) { return rpcRound("get-os-ip", {}) as ReturnType }, getHostInfo: ((...[allOptions]: Parameters) => { const options = { ...allOptions, callback: context.callbacks?.addCallback(allOptions.callback) || null, } return rpcRound("get-host-info", options) as ReturnType< T.Effects["getHostInfo"] > as any }) as Effects["getHostInfo"], getServiceInterface( ...[options]: Parameters ) { return rpcRound("get-service-interface", { ...options, callback: context.callbacks?.addCallback(options.callback) || null, }) as ReturnType }, getServicePortForward( ...[options]: Parameters ) { return rpcRound("get-service-port-forward", options) as ReturnType< T.Effects["getServicePortForward"] > }, getSslCertificate(options: Parameters[0]) { return rpcRound("get-ssl-certificate", options) as ReturnType< T.Effects["getSslCertificate"] > }, getSslKey(options: Parameters[0]) { return rpcRound("get-ssl-key", options) as ReturnType< T.Effects["getSslKey"] > }, getSystemSmtp(...[options]: Parameters) { return rpcRound("get-system-smtp", { ...options, callback: context.callbacks?.addCallback(options.callback) || null, }) as ReturnType }, listServiceInterfaces( ...[options]: Parameters ) { return rpcRound("list-service-interfaces", { ...options, callback: context.callbacks?.addCallback(options.callback) || null, }) as ReturnType }, mount(...[options]: Parameters) { return rpcRound("mount", options) as ReturnType }, restart(...[]: Parameters) { console.log("Restarting service...") return rpcRound("restart", {}) as ReturnType }, setDependencies( dependencies: Parameters[0], ): ReturnType { return rpcRound("set-dependencies", dependencies) as ReturnType< T.Effects["setDependencies"] > }, checkDependencies( options: Parameters[0], ): ReturnType { return rpcRound("check-dependencies", options) as ReturnType< T.Effects["checkDependencies"] > }, getDependencies(): ReturnType { return rpcRound("get-dependencies", {}) as ReturnType< T.Effects["getDependencies"] > }, setHealth(...[options]: Parameters) { return rpcRound("set-health", options) as ReturnType< T.Effects["setHealth"] > }, getStatus(...[o]: Parameters) { return rpcRound("get-status", o) as ReturnType }, setMainStatus(o: { status: "running" | "stopped" }): Promise { return rpcRound("set-main-status", o) as ReturnType< T.Effects["setHealth"] > }, shutdown(...[]: Parameters) { return rpcRound("shutdown", {}) as ReturnType }, getDataVersion() { return rpcRound("get-data-version", {}) as ReturnType< T.Effects["getDataVersion"] > }, setDataVersion(...[options]: Parameters) { return rpcRound("set-data-version", options) as ReturnType< T.Effects["setDataVersion"] > }, } if (context.callbacks?.onLeaveContext) self.onLeaveContext(() => { self.isInContext = false self.onLeaveContext = () => { console.warn( "this effects object is already out of context", new Error().stack?.replace(/^Error/, ""), ) } }) return self }