import { ExtendedVersion, types as T, utils, VersionRange, z, } from "@start9labs/start-sdk" import * as net from "net" import { Effects } from "../Models/Effects" import { CallbackHolder } from "../Models/CallbackHolder" import { asError } from "@start9labs/start-sdk/base/lib/util" const matchRpcError = z.object({ error: z.object({ code: z.number(), message: z.string(), data: z .union([ z.string(), z.object({ details: z.string(), debug: z.string().nullable().optional(), }), ]) .nullable() .optional(), }), }) function testRpcError(v: unknown): v is RpcError { return matchRpcError.safeParse(v).success } const matchRpcResult = z.object({ result: z.unknown(), }) function testRpcResult(v: unknown): v is z.infer { return matchRpcResult.safeParse(v).success } type RpcError = z.infer 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 (typeof res.error.data === "string") { 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"] > }, getServiceManifest( ...[options]: Parameters ) { return rpcRound("get-service-manifest", options) as ReturnType< T.Effects["getServiceManifest"] > }, 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 }, getOutboundGateway( ...[options]: Parameters ) { return rpcRound("get-outbound-gateway", { ...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 }, /// DEPRECATED 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"] > }, plugin: { url: { register( ...[options]: Parameters ) { return rpcRound("plugin.url.register", options) as ReturnType< T.Effects["plugin"]["url"]["register"] > }, exportUrl( ...[options]: Parameters ) { return rpcRound("plugin.url.export-url", options) as ReturnType< T.Effects["plugin"]["url"]["exportUrl"] > }, clearUrls( ...[options]: Parameters ) { return rpcRound("plugin.url.clear-urls", options) as ReturnType< T.Effects["plugin"]["url"]["clearUrls"] > }, }, }, } if (context.callbacks?.onLeaveContext) self.onLeaveContext(() => { self.constRetry = undefined self.isInContext = false self.onLeaveContext = () => { console.warn( "this effects object is already out of context", new Error().stack?.replace(/^Error/, ""), ) } }) return self }