UNPKG

@fastlanejs/base

Version:

Base Interface for communicating with the fastlane socket server

247 lines (245 loc) 6.47 kB
import { connect, SocketConnectOpts, Socket } from "net"; import { spawn, ChildProcessWithoutNullStreams } from "child_process"; class Deferred { public resolve: (...args: any) => void; public reject: (reason: any) => void; public promise: Promise<any>; constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } } class FastlaneError extends Error { public class: string; public information: string[]; constructor({ failureClass, message, stackTrace, }: { failureClass: string; message: string; stackTrace: string[]; }) { super(message); this.class = failureClass; this.information = stackTrace; } } function makeFastlaneError({ failure_class: failureClass, failure_message: message, failure_information: stackTrace, }: { failure_class: string; failure_message: string; failure_information: string[]; }) { return new FastlaneError({ failureClass, message, stackTrace }); } class FastlaneBase { /** @private */ protected port?: number = undefined; /** @private */ protected socket?: Socket = undefined; /** @private */ protected isInteractive: boolean = true; /** @private */ protected childProcess?: ChildProcessWithoutNullStreams = undefined; /** @private */ protected _debug: boolean = false; constructor(port = 2000, isInteractive = true, debug = false) { this.port = port; this.socket = null; this.isInteractive = isInteractive; this.childProcess = null; this._debug = debug; } setDebug(newValue: boolean = true) { this._debug = newValue; } isDebug() { return this._debug; } log(...args: any) { if (this._debug) console.log(args); } async start() { if (!this.childProcess) this.childProcess = await launch(this.isInteractive, this.port); if (!this.socket) this.socket = await init(this.port); } async close() { const { resolve, promise } = new Deferred(); if (!this.socket) { this.socket = null; if (this.childProcess) this.childProcess.kill("SIGHUP"); this.childProcess = null; return; } const remove = once(this.socket, "error", () => {}); this.socket.end(() => { remove(); this.socket = null; this.childProcess.kill("SIGHUP"); this.childProcess = null; resolve(); }); return promise; } async send({ commandType, command }: { commandType: string; command: {} }) { if (!this.socket) throw "Socket not initialized"; const json = JSON.stringify({ commandType, command }); this.log("Transmitting JSON", json); this.socket.write(json); const { resolve, promise, reject } = new Deferred(); this.socket.setEncoding("utf8"); const removeError = once(this.socket, "error", (d) => reject(d)); once(this.socket, "data", (d) => { try { removeError(); const o = JSON.parse(d); try { if (o.payload) { if (o.payload.status === "failure") { this.log("RECEIVED FAILURE", o); const e = makeFastlaneError(o.payload); reject(e); } else if (typeof o.payload.return_object === "undefined") { reject(o); } const result = o.payload.return_object; resolve(result); } } catch (e) { reject(e); } } catch (e) { removeError(); reject(e); } }); return promise; } async doAction(action: string, argObj: {}): Promise<any> { await this.start(); const args = argObj ? Object.entries(argObj).map(([name, value]) => ({ name, value })) : undefined; const command = { commandType: "action", command: { methodName: action, args }, }; this.log("Do action sending command ", command); return this.send(command); } } //#region Internal utility functions const asyncConnect = async (options: SocketConnectOpts): Promise<Socket> => { const { resolve, reject, promise } = new Deferred(); const initError = (e: Error) => reject(e); try { const c = connect(options, () => { c.removeListener("error", initError); resolve(c); }); c.on("error", initError); } catch (e) { reject(e); } const out = await promise; return out; }; const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(() => r(), ms)); const launch = ( interactive: boolean = true, port: number = 2000 ): ChildProcessWithoutNullStreams => { return spawn( "bundle", [ "exec", "fastlane", "socket_server", "--verbose", "-c", "30", "-s", "-p", port.toString(), ], { ...(interactive ? { stdio: "inherit" } : {}), } ); }; const init = async (port: number = 2000): Promise<Socket> => { while (true) { let s; try { s = ( await Promise.all( ["::1", "127.0.0.1"].map(async (host) => { try { const socket = await asyncConnect({ host, port }); return socket; } catch (e) { return null; } }) ) ).find(Boolean); } catch (e) {} if (s) { return s; } await sleep(500); } }; const once = (socket: Socket, event: string, f: (data: string) => void) => { const data: Buffer[] = []; const listener = (d: Buffer) => { data.push(d); if (d.length < 65536) { socket.removeListener(event, listener); const text = data.map((b) => b.toString("utf8")).join(""); f(text); } }; socket.on(event, listener); return () => socket.removeListener(event, listener); }; //#endregion //#region Exported Functions const withFastlaneBase = async ( f: (fastlane: FastlaneBase) => Promise<any>, { port = 2000, isInteractive = true, }: { port?: number; isInteractive?: boolean } ) => { const fastlane = new FastlaneBase(port, isInteractive); try { const result = await f(fastlane); fastlane.close(); return result; } catch (e) { await fastlane.close(); throw e; } }; const doActionOnce = async ( action: string, argobj: {}, isInteractive: boolean = true, port: number = 2000 ) => withFastlaneBase(({ doAction }) => doAction(action, argobj), { port, isInteractive, }); //#endregion export { FastlaneBase, doActionOnce, withFastlaneBase };