1 | const { promisify } = require("util");
|
2 | const { spawn, exec: execCallback } = require("child_process");
|
3 | const exec = promisify(execCallback);
|
4 | const bin = require("@expo/ngrok-bin");
|
5 |
|
6 | const ready = /starting web service.*addr=(\d+\.\d+\.\d+\.\d+:\d+)/;
|
7 | const inUse = /address already in use/;
|
8 |
|
9 | let processPromise, activeProcess;
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | async 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 |
|
27 | function getActiveProcess() {
|
28 | return activeProcess;
|
29 | }
|
30 |
|
31 | function 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 |
|
43 | async 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 |
|
103 | if (!opts.onLogEvent && !opts.onStatusChange) {
|
104 | ngrok.stdout.removeAllListeners("data");
|
105 | }
|
106 | ngrok.stderr.removeAllListeners("data");
|
107 | }
|
108 | }
|
109 |
|
110 | function 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 |
|
120 | process.on("exit", () => {
|
121 | if (activeProcess) {
|
122 | activeProcess.kill();
|
123 | }
|
124 | });
|
125 |
|
126 |
|
127 |
|
128 |
|
129 | async 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 |
|
154 |
|
155 | async function getVersion(opts = {}) {
|
156 | const { stdout } = await exec(`${bin} --version`);
|
157 | return stdout.replace("ngrok version", "").trim();
|
158 | }
|
159 |
|
160 | module.exports = {
|
161 | getProcess,
|
162 | getActiveProcess,
|
163 | killProcess,
|
164 | setAuthtoken,
|
165 | getVersion,
|
166 | };
|