const { spawn } = require('child_process'); const platform = require('os').platform(); const defaultDir = __dirname + '/bin'; const bin = './ngrok' + (platform === 'win32' ? '.exe' : ''); 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; } } async function startProcess (opts) { let dir = defaultDir; const start = ['start', '--none', '--log=stdout']; if (opts.region) start.push('--region=' + opts.region); if (opts.configPath) start.push('--config=' + opts.configPath); if (opts.binPath) dir = opts.binPath(dir); const ngrok = spawn(bin, start, {cwd: dir}); let resolve, reject; const apiUrl = new Promise((res, rej) => { resolve = res; reject = rej; }); ngrok.stdout.on('data', data => { const msg = data.toString(); const addr = msg.match(ready); if (opts.onLogEvent) { opts.onLogEvent(msg.trim()); } if (opts.onStatusChange) { if (msg.match('client session established')) { opts.onStatusChange('connected'); } else if (msg.match('session closed, starting reconnect loop')) { opts.onStatusChange('closed'); } } if (addr) { resolve(`http://${addr[1]}`); } 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; }); process.on('exit', async () => await killProcess()); 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; return new Promise(resolve => { activeProcess.on('exit', () => resolve()); activeProcess.kill(); }); } /** * @param {string | INgrokOptions} 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); let dir = defaultDir; if (opts.binPath) dir = opts.binPath(dir) const ngrok = spawn(bin, authtoken, {cwd: dir}); const killed = new Promise((resolve, reject) => { ngrok.stdout.once('data', () => resolve()); ngrok.stderr.once('data', () => reject(new Error('cant set authtoken'))); }); try { return await killed; } finally { ngrok.kill(); } } module.exports = { getProcess, killProcess, setAuthtoken };