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