helpers.js (6283B)
1 const { spawn } = require('child_process'); 2 const net = require('net'); 3 const dgram = require('dgram'); 4 const path = require('path'); 5 6 const DAEMON_PATH = path.join(__dirname, '..', 'udphole'); 7 const TIMEOUT = 2000; 8 9 function sleep(ms) { 10 return new Promise(resolve => setTimeout(resolve, ms)); 11 } 12 13 function findFreePort() { 14 return new Promise((resolve, reject) => { 15 const server = net.createServer(); 16 server.unref(); 17 server.on('error', reject); 18 server.listen(0, () => { 19 const addr = server.address(); 20 server.close(() => resolve(addr.port)); 21 }); 22 }); 23 } 24 25 function spawnDaemon(configPath) { 26 return new Promise(async (resolve, reject) => { 27 await sleep(500); 28 const daemon = spawn(DAEMON_PATH, ['-f', configPath, 'daemon', '-D'], { 29 stdio: ['ignore', 'pipe', 'pipe', 'pipe'] 30 }); 31 32 let output = ''; 33 const startTimeout = setTimeout(() => { 34 reject(new Error(`Daemon start timeout. Output: ${output}`)); 35 }, TIMEOUT); 36 37 daemon.stderr.on('data', (data) => { 38 process.stderr.write(data.toString()); 39 output += data.toString(); 40 if (output.includes('daemon started')) { 41 clearTimeout(startTimeout); 42 sleep(200).then(() => resolve(daemon)); 43 } 44 }); 45 46 daemon.on('error', (err) => { 47 clearTimeout(startTimeout); 48 reject(err); 49 }); 50 }); 51 } 52 53 function killAllDaemons() { 54 return new Promise((resolve) => { 55 const { execSync } = require('child_process'); 56 try { execSync('pkill -9 udphole 2>/dev/null', { stdio: 'ignore' }); } catch(e) {} 57 sleep(1000).then(resolve); 58 }); 59 } 60 61 function killDaemon(daemon) { 62 return new Promise((resolve) => { 63 if (!daemon || daemon.killed) { 64 resolve(); 65 return; 66 } 67 daemon.once('exit', resolve); 68 daemon.kill('SIGTERM'); 69 setTimeout(() => { 70 if (!daemon.killed) daemon.kill('SIGKILL'); 71 resolve(); 72 }, 1000); 73 }); 74 } 75 76 function connectApi(port) { 77 return new Promise((resolve, reject) => { 78 const sock = net.createConnection({ port, host: '127.0.0.1', noDelay: true }); 79 sock.setEncoding('utf8'); 80 81 const timeout = setTimeout(() => { 82 sock.destroy(); 83 reject(new Error('Connection timeout')); 84 }, TIMEOUT); 85 86 sock.on('connect', () => { 87 clearTimeout(timeout); 88 resolve(sock); 89 }); 90 91 sock.on('error', reject); 92 }); 93 } 94 95 function connectUnixApi(socketPath) { 96 return new Promise((resolve, reject) => { 97 const sock = net.createConnection({ path: socketPath, noDelay: true }); 98 sock.setEncoding('utf8'); 99 100 const timeout = setTimeout(() => { 101 sock.destroy(); 102 reject(new Error('Connection timeout')); 103 }, TIMEOUT); 104 105 sock.on('connect', () => { 106 clearTimeout(timeout); 107 resolve(sock); 108 }); 109 110 sock.on('error', reject); 111 }); 112 } 113 114 function encodeResp(...args) { 115 const n = args.length; 116 let cmd = `*${n}\r\n`; 117 for (const arg of args) { 118 const s = String(arg); 119 cmd += `$${s.length}\r\n${s}\r\n`; 120 } 121 return cmd; 122 } 123 124 function apiCommand(sock, ...args) { 125 return new Promise((resolve, reject) => { 126 const cmd = encodeResp(...args); 127 128 let response = ''; 129 const timeout = setTimeout(() => { 130 sock.destroy(); 131 reject(new Error('API command timeout')); 132 }, TIMEOUT); 133 134 sock.once('data', (data) => { 135 response += data; 136 clearTimeout(timeout); 137 resolve(parseResp(response)); 138 }); 139 140 sock.write(cmd); 141 }); 142 } 143 144 function parseResp(data) { 145 data = data.trim(); 146 if (data.startsWith('+')) return data.substring(1).trim(); 147 if (data.startsWith('-')) throw new Error(data.substring(1).trim()); 148 if (data.startsWith(':')) return parseInt(data.substring(1), 10); 149 if (data.startsWith('*')) { 150 const count = parseInt(data.substring(1), 10); 151 if (count === 0) return []; 152 const lines = data.split('\r\n'); 153 const result = []; 154 let i = 1; 155 for (let j = 0; j < count && i < lines.length; j++) { 156 if (lines[i].startsWith('$')) { 157 i++; 158 if (i < lines.length) result.push(lines[i]); 159 } else if (lines[i].startsWith(':')) { 160 result.push(parseInt(lines[i].substring(1), 10)); 161 } else if (lines[i].startsWith('+')) { 162 result.push(lines[i].substring(1)); 163 } else if (lines[i].startsWith('-')) { 164 throw new Error(lines[i].substring(1)); 165 } 166 i++; 167 } 168 return result; 169 } 170 if (data.startsWith('$')) { 171 const len = parseInt(data.substring(1), 10); 172 if (len === -1) return null; 173 const idx = data.indexOf('\r\n'); 174 if (idx >= 0) return data.substring(idx + 2); 175 return ''; 176 } 177 return data; 178 } 179 180 function createUdpEchoServer() { 181 return new Promise(async (resolve, reject) => { 182 const server = dgram.createSocket('udp4'); 183 const messages = []; 184 185 server.on('message', (msg, rinfo) => { 186 messages.push({ data: msg.toString(), rinfo }); 187 server.send(msg, rinfo.port, rinfo.address); 188 }); 189 190 server.on('error', reject); 191 192 const port = await findFreePort(); 193 server.bind(port, '127.0.0.1', () => { 194 resolve({ 195 port, 196 socket: server, 197 getMessages: () => messages, 198 clearMessages: () => { messages.length = 0; } 199 }); 200 }); 201 }); 202 } 203 204 function sendUdp(port, host, message) { 205 return new Promise((resolve, reject) => { 206 const sock = dgram.createSocket('udp4'); 207 const buf = Buffer.from(message); 208 sock.send(buf, 0, buf.length, port, host, (err) => { 209 sock.close(); 210 if (err) reject(err); 211 else resolve(); 212 }); 213 }); 214 } 215 216 function recvUdp(port, timeout) { 217 return new Promise((resolve, reject) => { 218 const sock = dgram.createSocket('udp4'); 219 sock.bind(port, '127.0.0.1'); 220 221 const timer = setTimeout(() => { 222 sock.close(); 223 reject(new Error('Receive timeout')); 224 }, timeout || TIMEOUT); 225 226 sock.on('message', (msg, rinfo) => { 227 clearTimeout(timer); 228 sock.close(); 229 resolve({ data: msg.toString(), rinfo }); 230 }); 231 232 sock.on('error', (err) => { 233 clearTimeout(timer); 234 reject(err); 235 }); 236 }); 237 } 238 239 module.exports = { 240 sleep, 241 findFreePort, 242 spawnDaemon, 243 killDaemon, 244 killAllDaemons, 245 connectApi, 246 connectUnixApi, 247 apiCommand, 248 createUdpEchoServer, 249 sendUdp, 250 recvUdp, 251 TIMEOUT 252 };