UNPKG

3.96 kBJavaScriptView Raw
1const { promisify } = require("util");
2const { spawn, exec: execCallback } = require("child_process");
3const exec = promisify(execCallback);
4const bin = require("@expo/ngrok-bin");
5
6const ready = /starting web service.*addr=(\d+\.\d+\.\d+\.\d+:\d+)/;
7const inUse = /address already in use/;
8
9let processPromise, activeProcess;
10
11/*
12 ngrok process runs internal ngrok api
13 and should be spawned only ONCE
14 (respawn allowed if it fails or .kill method called)
15*/
16async function getProcess(opts) {
17 if (processPromise) return processPromise;
18 try {
19 processPromise = startProcess(opts);
20 return await processPromise;
21 } catch (ex) {
22 processPromise = null;
23 throw ex;
24 }
25}
26
27function getActiveProcess() {
28 return activeProcess;
29}
30
31function parseAddr(message) {
32 if (message[0] === "{") {
33 const parsed = JSON.parse(message);
34 return parsed.addr
35 } else {
36 const parsed = message.match(ready);
37 if (parsed) {
38 return parsed[1];
39 }
40 }
41}
42
43async function startProcess(opts) {
44 const start = ["start", "--none", "--log=stdout"];
45 if (opts.region) start.push("--region=" + opts.region);
46 if (opts.configPath) start.push("--config=" + opts.configPath);
47
48 const ngrok = spawn(bin, start, { windowsHide: true });
49
50 let resolve, reject;
51 const apiUrl = new Promise((res, rej) => {
52 resolve = res;
53 reject = rej;
54 });
55
56 ngrok.stdout.on("data", (data) => {
57 const msg = data.toString().trim();
58 if (opts.onLogEvent) {
59 opts.onLogEvent(msg);
60 }
61 if (opts.onStatusChange) {
62 if (msg.match("client session established")) {
63 opts.onStatusChange("connected");
64 } else if (msg.match("session closed, starting reconnect loop")) {
65 opts.onStatusChange("closed");
66 }
67 }
68
69 const msgs = msg.split(/\n/);
70 msgs.forEach(msg => {
71 const addr = parseAddr(msg);
72 if (addr) {
73 resolve(`http://${addr}`);
74 } else if (msg.match(inUse)) {
75 reject(new Error(msg.substring(0, 10000)));
76 }
77 })
78 });
79
80 ngrok.stderr.on("data", (data) => {
81 const msg = data.toString().substring(0, 10000);
82 reject(new Error(msg));
83 });
84
85 ngrok.on("exit", () => {
86 processPromise = null;
87 activeProcess = null;
88 });
89
90 ngrok.on("error", (err) => {
91 reject(err);
92 });
93
94 try {
95 const url = await apiUrl;
96 activeProcess = ngrok;
97 return url;
98 } catch (ex) {
99 ngrok.kill();
100 throw ex;
101 } finally {
102 // Remove the stdout listeners if nobody is interested in the content.
103 if (!opts.onLogEvent && !opts.onStatusChange) {
104 ngrok.stdout.removeAllListeners("data");
105 }
106 ngrok.stderr.removeAllListeners("data");
107 }
108}
109
110function killProcess() {
111 if (!activeProcess) {
112 return Promise.resolve();
113 }
114 return new Promise((resolve) => {
115 activeProcess.on("exit", () => resolve());
116 activeProcess.kill();
117 });
118}
119
120process.on("exit", () => {
121 if (activeProcess) {
122 activeProcess.kill();
123 }
124});
125
126/**
127 * @param {string | Ngrok.Options} optsOrToken
128 */
129async function setAuthtoken(optsOrToken) {
130 const isOpts = typeof optsOrToken !== "string";
131 const opts = isOpts ? optsOrToken : {};
132 const token = isOpts ? opts.authtoken : optsOrToken;
133
134 const authtoken = ["authtoken", token];
135 if (opts.configPath) authtoken.push("--config=" + opts.configPath);
136
137 const ngrok = spawn(bin, authtoken, { windowsHide: true });
138
139 const killed = new Promise((resolve, reject) => {
140 ngrok.stdout.once("data", () => resolve());
141 ngrok.stderr.once("data", () => reject(new Error("cant set authtoken")));
142 ngrok.on("error", (err) => reject(err));
143 });
144
145 try {
146 return await killed;
147 } finally {
148 ngrok.kill();
149 }
150}
151
152/**
153 * @param {Ngrok.Options | undefined} opts
154 */
155async function getVersion(opts = {}) {
156 const { stdout } = await exec(`${bin} --version`);
157 return stdout.replace("ngrok version", "").trim();
158}
159
160module.exports = {
161 getProcess,
162 getActiveProcess,
163 killProcess,
164 setAuthtoken,
165 getVersion,
166};