1'use strict'; 2 3// Testcase to check reporting of uv handles. 4const common = require('../common'); 5const tmpdir = require('../common/tmpdir'); 6const path = require('path'); 7if (common.isIBMi) 8 common.skip('IBMi does not support fs.watch()'); 9 10// This is quite similar to common.PIPE except that it uses an extended prefix 11// of "\\?\pipe" on windows. 12const PIPE = (() => { 13 const localRelative = path.relative(process.cwd(), `${tmpdir.path}/`); 14 const pipePrefix = common.isWindows ? '\\\\?\\pipe\\' : localRelative; 15 const pipeName = `node-test.${process.pid}.sock`; 16 return path.join(pipePrefix, pipeName); 17})(); 18 19function createFsHandle(childData) { 20 const fs = require('fs'); 21 // Watching files should result in fs_event/fs_poll uv handles. 22 let watcher; 23 try { 24 watcher = fs.watch(__filename); 25 } catch { 26 // fs.watch() unavailable 27 } 28 fs.watchFile(__filename, () => {}); 29 childData.skip_fs_watch = watcher === undefined; 30 31 return () => { 32 if (watcher) watcher.close(); 33 fs.unwatchFile(__filename); 34 }; 35} 36 37function createChildProcessHandle(childData) { 38 const spawn = require('child_process').spawn; 39 // Child should exist when this returns as child_process.pid must be set. 40 const cp = spawn(process.execPath, 41 ['-e', "process.stdin.on('data', (x) => " + 42 'console.log(x.toString()));']); 43 childData.pid = cp.pid; 44 45 return () => { 46 cp.kill(); 47 }; 48} 49 50function createTimerHandle() { 51 const timeout = setInterval(() => {}, 1000); 52 // Make sure the timer doesn't keep the test alive and let 53 // us check we detect unref'd handles correctly. 54 timeout.unref(); 55 return () => { 56 clearInterval(timeout); 57 }; 58} 59 60function createTcpHandle(childData) { 61 const http = require('http'); 62 63 return new Promise((resolve) => { 64 // Simple server/connection to create tcp uv handles. 65 const server = http.createServer((req, res) => { 66 req.on('end', () => { 67 resolve(() => { 68 res.writeHead(200, { 'Content-Type': 'text/plain' }); 69 res.end(); 70 server.close(); 71 }); 72 }); 73 req.resume(); 74 }); 75 server.listen(() => { 76 childData.tcp_address = server.address(); 77 http.get({ port: server.address().port }); 78 }); 79 }); 80} 81 82function createUdpHandle(childData) { 83 // Datagram socket for udp uv handles. 84 const dgram = require('dgram'); 85 const udpSocket = dgram.createSocket('udp4'); 86 const connectedUdpSocket = dgram.createSocket('udp4'); 87 88 return new Promise((resolve) => { 89 udpSocket.bind({}, common.mustCall(() => { 90 connectedUdpSocket.connect(udpSocket.address().port); 91 92 childData.udp_address = udpSocket.address(); 93 resolve(() => { 94 connectedUdpSocket.close(); 95 udpSocket.close(); 96 }); 97 })); 98 }); 99} 100 101function createNamedPipeHandle(childData) { 102 const net = require('net'); 103 const sockPath = PIPE; 104 return new Promise((resolve) => { 105 const server = net.createServer((socket) => { 106 childData.pipe_sock_path = server.address(); 107 resolve(() => { 108 socket.end(); 109 server.close(); 110 }); 111 }); 112 server.listen( 113 sockPath, 114 () => { 115 net.connect(sockPath, (socket) => {}); 116 }); 117 }); 118} 119 120async function child() { 121 // Exit on loss of parent process 122 const exit = () => process.exit(2); 123 process.on('disconnect', exit); 124 125 const childData = {}; 126 const disposes = await Promise.all([ 127 createFsHandle(childData), 128 createChildProcessHandle(childData), 129 createTimerHandle(childData), 130 createTcpHandle(childData), 131 createUdpHandle(childData), 132 createNamedPipeHandle(childData), 133 ]); 134 process.send(childData); 135 136 // Generate the report while the connection is active. 137 console.log(JSON.stringify(process.report.getReport(), null, 2)); 138 139 // Tidy up to allow process to exit cleanly. 140 disposes.forEach((it) => { 141 it(); 142 }); 143 process.removeListener('disconnect', exit); 144} 145 146if (process.argv[2] === 'child') { 147 child(); 148} else { 149 const helper = require('../common/report.js'); 150 const fork = require('child_process').fork; 151 const assert = require('assert'); 152 tmpdir.refresh(); 153 const options = { encoding: 'utf8', silent: true, cwd: tmpdir.path }; 154 const child = fork(__filename, ['child'], options); 155 let child_data; 156 child.on('message', (data) => { child_data = data; }); 157 let stderr = ''; 158 child.stderr.on('data', (chunk) => { stderr += chunk; }); 159 let stdout = ''; 160 const report_msg = 'Report files were written: unexpectedly'; 161 child.stdout.on('data', (chunk) => { stdout += chunk; }); 162 child.on('exit', common.mustCall((code, signal) => { 163 assert.strictEqual(stderr.trim(), ''); 164 assert.deepStrictEqual(code, 0, 'Process exited unexpectedly with code: ' + 165 `${code}`); 166 assert.deepStrictEqual(signal, null, 'Process should have exited cleanly,' + 167 ` but did not: ${signal}`); 168 169 const reports = helper.findReports(child.pid, tmpdir.path); 170 assert.deepStrictEqual(reports, [], report_msg, reports); 171 172 // Test libuv handle key order 173 { 174 const get_libuv = /"libuv":\s\[([\s\S]*?)\]/g; 175 const get_handle_inner = /{([\s\S]*?),*?}/g; 176 const libuv_handles_str = get_libuv.exec(stdout)[1]; 177 const libuv_handles_array = libuv_handles_str.match(get_handle_inner); 178 for (const i of libuv_handles_array) { 179 // Exclude nested structure 180 if (i.includes('type')) { 181 const handle_keys = i.match(/(".*"):/g); 182 assert(handle_keys[0], 'type'); 183 assert(handle_keys[1], 'is_active'); 184 } 185 } 186 } 187 188 const report = JSON.parse(stdout); 189 const prefix = common.isWindows ? '\\\\?\\' : ''; 190 const expected_filename = `${prefix}${__filename}`; 191 const found_tcp = []; 192 const found_udp = []; 193 const found_named_pipe = []; 194 // Functions are named to aid debugging when they are not called. 195 const validators = { 196 fs_event: common.mustCall(function fs_event_validator(handle) { 197 if (!child_data.skip_fs_watch) { 198 assert.strictEqual(handle.filename, expected_filename); 199 assert(handle.is_referenced); 200 } 201 }), 202 fs_poll: common.mustCall(function fs_poll_validator(handle) { 203 assert.strictEqual(handle.filename, expected_filename); 204 assert(handle.is_referenced); 205 }), 206 loop: common.mustCall(function loop_validator(handle) { 207 assert.strictEqual(typeof handle.loopIdleTimeSeconds, 'number'); 208 }), 209 pipe: common.mustCallAtLeast(function pipe_validator(handle) { 210 assert(handle.is_referenced); 211 // Pipe handles. The report should contain three pipes: 212 // 1. The server's listening pipe. 213 // 2. The inbound pipe making the request. 214 // 3. The outbound pipe sending the response. 215 // 216 // There is no way to distinguish inbound and outbound in a cross 217 // platform manner, so we just check inbound here. 218 const sockPath = child_data.pipe_sock_path; 219 if (handle.localEndpoint === sockPath) { 220 if (handle.writable === false) { 221 found_named_pipe.push('listening'); 222 } 223 } else if (handle.remoteEndpoint === sockPath) { 224 found_named_pipe.push('inbound'); 225 } 226 }), 227 process: common.mustCall(function process_validator(handle) { 228 assert.strictEqual(handle.pid, child_data.pid); 229 assert(handle.is_referenced); 230 }), 231 tcp: common.mustCall(function tcp_validator(handle) { 232 // TCP handles. The report should contain three sockets: 233 // 1. The server's listening socket. 234 // 2. The inbound socket making the request. 235 // 3. The outbound socket sending the response. 236 const port = child_data.tcp_address.port; 237 if (handle.localEndpoint.port === port) { 238 if (handle.remoteEndpoint === null) { 239 found_tcp.push('listening'); 240 } else { 241 found_tcp.push('inbound'); 242 } 243 } else if (handle.remoteEndpoint.port === port) { 244 found_tcp.push('outbound'); 245 } 246 assert(handle.is_referenced); 247 }, 3), 248 timer: common.mustCallAtLeast(function timer_validator(handle) { 249 assert(!handle.is_referenced); 250 assert.strictEqual(handle.repeat, 0); 251 }), 252 udp: common.mustCall(function udp_validator(handle) { 253 if (handle.remoteEndpoint === null) { 254 assert.strictEqual(handle.localEndpoint.port, 255 child_data.udp_address.port); 256 found_udp.push('unconnected'); 257 } else { 258 assert.strictEqual(handle.remoteEndpoint.port, 259 child_data.udp_address.port); 260 found_udp.push('connected'); 261 } 262 assert(handle.is_referenced); 263 }, 2), 264 }; 265 266 for (const entry of report.libuv) { 267 if (validators[entry.type]) validators[entry.type](entry); 268 } 269 for (const socket of ['listening', 'inbound', 'outbound']) { 270 assert(found_tcp.includes(socket), `${socket} TCP socket was not found`); 271 } 272 for (const socket of ['connected', 'unconnected']) { 273 assert(found_udp.includes(socket), `${socket} UDP socket was not found`); 274 } 275 for (const socket of ['listening', 'inbound']) { 276 assert(found_named_pipe.includes(socket), 277 `${socket} named pipe socket was not found`); 278 } 279 280 // Common report tests. 281 helper.validateContent(stdout); 282 })); 283} 284