UNPKG

@expo/ngrok

Version:

node wrapper for ngrok

167 lines (143 loc) 3.96 kB
const { promisify } = require("util"); const { spawn, exec: execCallback } = require("child_process"); const exec = promisify(execCallback); const bin = require("@expo/ngrok-bin"); const ready = /starting web service.*addr=(\d+\.\d+\.\d+\.\d+:\d+)/; const inUse = /address already in use/; let processPromise, activeProcess; /* ngrok process runs internal ngrok api and should be spawned only ONCE (respawn allowed if it fails or .kill method called) */ async function getProcess(opts) { if (processPromise) return processPromise; try { processPromise = startProcess(opts); return await processPromise; } catch (ex) { processPromise = null; throw ex; } } function getActiveProcess() { return activeProcess; } function parseAddr(message) { if (message[0] === "{") { const parsed = JSON.parse(message); return parsed.addr } else { const parsed = message.match(ready); if (parsed) { return parsed[1]; } } } async function startProcess(opts) { const start = ["start", "--none", "--log=stdout"]; if (opts.region) start.push("--region=" + opts.region); if (opts.configPath) start.push("--config=" + opts.configPath); const ngrok = spawn(bin, start, { windowsHide: true }); let resolve, reject; const apiUrl = new Promise((res, rej) => { resolve = res; reject = rej; }); ngrok.stdout.on("data", (data) => { const msg = data.toString().trim(); if (opts.onLogEvent) { opts.onLogEvent(msg); } if (opts.onStatusChange) { if (msg.match("client session established")) { opts.onStatusChange("connected"); } else if (msg.match("session closed, starting reconnect loop")) { opts.onStatusChange("closed"); } } const msgs = msg.split(/\n/); msgs.forEach(msg => { const addr = parseAddr(msg); if (addr) { resolve(`http://${addr}`); } else if (msg.match(inUse)) { reject(new Error(msg.substring(0, 10000))); } }) }); ngrok.stderr.on("data", (data) => { const msg = data.toString().substring(0, 10000); reject(new Error(msg)); }); ngrok.on("exit", () => { processPromise = null; activeProcess = null; }); ngrok.on("error", (err) => { reject(err); }); try { const url = await apiUrl; activeProcess = ngrok; return url; } catch (ex) { ngrok.kill(); throw ex; } finally { // Remove the stdout listeners if nobody is interested in the content. if (!opts.onLogEvent && !opts.onStatusChange) { ngrok.stdout.removeAllListeners("data"); } ngrok.stderr.removeAllListeners("data"); } } function killProcess() { if (!activeProcess) { return Promise.resolve(); } return new Promise((resolve) => { activeProcess.on("exit", () => resolve()); activeProcess.kill(); }); } process.on("exit", () => { if (activeProcess) { activeProcess.kill(); } }); /** * @param {string | Ngrok.Options} optsOrToken */ async function setAuthtoken(optsOrToken) { const isOpts = typeof optsOrToken !== "string"; const opts = isOpts ? optsOrToken : {}; const token = isOpts ? opts.authtoken : optsOrToken; const authtoken = ["authtoken", token]; if (opts.configPath) authtoken.push("--config=" + opts.configPath); const ngrok = spawn(bin, authtoken, { windowsHide: true }); const killed = new Promise((resolve, reject) => { ngrok.stdout.once("data", () => resolve()); ngrok.stderr.once("data", () => reject(new Error("cant set authtoken"))); ngrok.on("error", (err) => reject(err)); }); try { return await killed; } finally { ngrok.kill(); } } /** * @param {Ngrok.Options | undefined} opts */ async function getVersion(opts = {}) { const { stdout } = await exec(`${bin} --version`); return stdout.replace("ngrok version", "").trim(); } module.exports = { getProcess, getActiveProcess, killProcess, setAuthtoken, getVersion, };