diff --git a/binding.gyp b/binding.gyp index b3d05ac..1a17381 100644 --- a/binding.gyp +++ b/binding.gyp @@ -10,8 +10,8 @@ "cflags": [ "-g", "-O2", "-std=c++11", ], }], ["OS=='win'", { - "libraries": [ "dbghelp.lib", "Netapi32.lib", "PsApi.lib" ], - "dll_files": [ "dbghelp.dll", "Netapi32.dll", "PsApi.dll" ], + "libraries": [ "dbghelp.lib", "Netapi32.lib", "PsApi.lib", "Ws2_32.lib" ], + "dll_files": [ "dbghelp.dll", "Netapi32.dll", "PsApi.dll", "Ws2_32.dll" ], }], ], "defines": [ diff --git a/src/node_report.cc b/src/node_report.cc index c9edb54..7cd9574 100644 --- a/src/node_report.cc +++ b/src/node_report.cc @@ -1,5 +1,6 @@ #include "node_report.h" #include "v8.h" +#include "uv.h" #include "time.h" #include @@ -7,8 +8,10 @@ #include #include #include +#include #include #include +#include #if !defined(_MSC_VER) #include @@ -489,48 +492,236 @@ void GetNodeReport(Isolate* isolate, DumpEvent event, const char* message, const WriteNodeReport(isolate, event, message, location, nullptr, out, error, &tm_struct); } +static void reportEndpoints(uv_handle_t* h, std::ostringstream& out) { + struct sockaddr_storage addr_storage; + struct sockaddr* addr = (sockaddr*)&addr_storage; + char hostbuf[NI_MAXHOST]; + char portbuf[NI_MAXSERV]; + uv_any_handle* handle = (uv_any_handle*)h; + int addr_size = sizeof(addr_storage); + int rc = -1; + + switch (h->type) { + case UV_UDP: { + rc = uv_udp_getsockname(&(handle->udp), addr, &addr_size); + break; + } + case UV_TCP: { + rc = uv_tcp_getsockname(&(handle->tcp), addr, &addr_size); + break; + } + default: break; + } + if (rc == 0) { + // getnameinfo will format host and port and handle IPv4/IPv6. + rc = getnameinfo(addr, addr_size, hostbuf, sizeof(hostbuf), portbuf, + sizeof(portbuf), NI_NUMERICSERV); + if (rc == 0) { + out << std::string(hostbuf) << ":" << std::string(portbuf); + } + + if (h->type == UV_TCP) { + // Get the remote end of the connection. + rc = uv_tcp_getpeername(&(handle->tcp), addr, &addr_size); + if (rc == 0) { + rc = getnameinfo(addr, addr_size, hostbuf, sizeof(hostbuf), portbuf, + sizeof(portbuf), NI_NUMERICSERV); + if (rc == 0) { + out << " connected to "; + out << std::string(hostbuf) << ":" << std::string(portbuf); + } + } else if (rc == UV_ENOTCONN) { + out << " (not connected)"; + } + } + } +} + +static void reportPath(uv_handle_t* h, std::ostringstream& out) { + char *buffer = nullptr; + int rc = -1; + size_t size = 0; + uv_any_handle* handle = (uv_any_handle*)h; + // First call to get required buffer size. + switch (h->type) { + case UV_FS_EVENT: { + rc = uv_fs_event_getpath(&(handle->fs_event), buffer, &size); + break; + } + case UV_FS_POLL: { + rc = uv_fs_poll_getpath(&(handle->fs_poll), buffer, &size); + break; + } + default: break; + } + if (rc == UV_ENOBUFS) { + buffer = static_cast(malloc(size)); + switch (h->type) { + case UV_FS_EVENT: { + rc = uv_fs_event_getpath(&(handle->fs_event), buffer, &size); + break; + } + case UV_FS_POLL: { + rc = uv_fs_poll_getpath(&(handle->fs_poll), buffer, &size); + break; + } + default: break; + } + if (rc == 0) { + // buffer is not null terminated. + std::string name(buffer, size); + out << "filename: " << name; + } + free(buffer); + } +} + static void walkHandle(uv_handle_t* h, void* arg) { std::string type; - std::string data = ""; + std::ostringstream data; std::ostream* out = reinterpret_cast(arg); - char buf[64]; + uv_any_handle* handle = (uv_any_handle*)h; // List all the types so we get a compile warning if we've missed one, - // (using default: supresses the compiler warning.) + // (using default: supresses the compiler warning). switch (h->type) { case UV_UNKNOWN_HANDLE: type = "unknown"; break; case UV_ASYNC: type = "async"; break; case UV_CHECK: type = "check"; break; - case UV_FS_EVENT: type = "fs_event"; break; - case UV_FS_POLL: type = "fs_poll"; break; + case UV_FS_EVENT: { + type = "fs_event"; + reportPath(h, data); + break; + } + case UV_FS_POLL: { + type = "fs_poll"; + reportPath(h, data); + break; + } case UV_HANDLE: type = "handle"; break; case UV_IDLE: type = "idle"; break; case UV_NAMED_PIPE: type = "pipe"; break; case UV_POLL: type = "poll"; break; case UV_PREPARE: type = "prepare"; break; - case UV_PROCESS: type = "process"; break; + case UV_PROCESS: { + type = "process"; + data << "pid: " << handle->process.pid; + break; + } case UV_STREAM: type = "stream"; break; - case UV_TCP: type = "tcp"; break; - case UV_TIMER: type = "timer"; break; - case UV_TTY: type = "tty"; break; - case UV_UDP: type = "udp"; break; - case UV_SIGNAL: type = "signal"; break; + case UV_TCP: { + type = "tcp"; + reportEndpoints(h, data); + break; + } + case UV_TIMER: { + // TODO timeout/due is not actually public however it is present + // in all current versions of libuv. Once uv_timer_get_timeout is + // in a supported level of libuv we should test for it with dlsym + // and use it instead, in case timeout moves in the future. +#ifdef _WIN32 + uint64_t due = handle->timer.due; +#else + uint64_t due = handle->timer.timeout; +#endif + uint64_t now = uv_now(handle->timer.loop); + type = "timer"; + data << "repeat: " << uv_timer_get_repeat(&(handle->timer)); + if (due > now) { + data << ", timeout in: " << (due - now) << " ms"; + } else { + data << ", timeout expired: " << (now - due) << " ms ago"; + } + break; + } + case UV_TTY: { + int height, width, rc; + type = "tty"; + rc = uv_tty_get_winsize(&(handle->tty), &width, &height); + if (rc == 0) { + data << "width: " << width << ", height: " << height; + } + break; + } + case UV_UDP: { + type = "udp"; + reportEndpoints(h, data); + break; + } + case UV_SIGNAL: { + // SIGWINCH is used by libuv so always appears. + // See http://docs.libuv.org/en/v1.x/signal.html + type = "signal"; + data << "signum: " << handle->signal.signum + // node::signo_string() is not exported by Node.js on Windows. +#ifndef _WIN32 + << " (" << node::signo_string(handle->signal.signum) << ")" +#endif + ; + break; + } case UV_FILE: type = "file"; break; // We shouldn't see "max" type - case UV_HANDLE_TYPE_MAX : break; + case UV_HANDLE_TYPE_MAX : type = "max"; break; } - snprintf(buf, sizeof(buf), -#ifdef _WIN32 - "[%c%c] %-10s0x%p\n", -#else - "[%c%c] %-10s%p\n", + if (h->type == UV_TCP || h->type == UV_UDP +#ifndef _WIN32 + || h->type == UV_NAMED_PIPE #endif - uv_has_ref(h)?'R':'-', - uv_is_active(h)?'A':'-', - type.c_str(), (void*)h); + ) { + // These *must* be 0 or libuv will set the buffer sizes to the non-zero + // values they contain. + int send_size = 0; + int recv_size = 0; + if (h->type == UV_TCP || h->type == UV_UDP) { + data << ", "; + } + uv_send_buffer_size(h, &send_size); + uv_recv_buffer_size(h, &recv_size); + data << "send buffer size: " << send_size + << ", recv buffer size: " << recv_size; + } - *out << buf; + if (h->type == UV_TCP || h->type == UV_NAMED_PIPE || h->type == UV_TTY || + h->type == UV_UDP || h->type == UV_POLL) { + uv_os_fd_t fd_v; + uv_os_fd_t* fd = &fd_v; + int rc = uv_fileno(h, fd); + // uv_os_fd_t is an int on Unix and HANDLE on Windows. +#ifndef _WIN32 + if (rc == 0) { + switch (fd_v) { + case 0: + data << ", stdin"; break; + case 1: + data << ", stdout"; break; + case 2: + data << ", stderr"; break; + default: + data << ", file descriptor: " << static_cast(fd_v); + break; + } + } +#endif + } + + if (h->type == UV_TCP || h->type == UV_NAMED_PIPE || h->type == UV_TTY) { + + data << ", write queue size: " + << handle->stream.write_queue_size; + data << (uv_is_readable(&handle->stream) ? ", readable" : "") + << (uv_is_writable(&handle->stream) ? ", writable": ""); + + } + + *out << std::left << "[" << (uv_has_ref(h) ? 'R' : '-') + << (uv_is_active(h) ? 'A' : '-') << "] " << std::setw(10) << type + << std::internal << std::setw(2 + 2 * sizeof(void*)); + char prev_fill = out->fill('0'); + *out << static_cast(h) << std::left; + out->fill(prev_fill); + *out << " " << std::left << data.str() << std::endl; } static void WriteNodeReport(Isolate* isolate, DumpEvent event, const char* message, const char* location, char* filename, std::ostream &out, MaybeLocal error, TIME_TYPE* tm_struct) { @@ -541,6 +732,10 @@ static void WriteNodeReport(Isolate* isolate, DumpEvent event, const char* messa pid_t pid = getpid(); #endif + // Save formatting for output stream. + std::ios oldState(nullptr); + oldState.copyfmt(out); + // File stream opened OK, now start printing the report content, starting with the title // and header information (event, filename, timestamp and pid) out << "================================================================================\n"; @@ -610,7 +805,9 @@ static void WriteNodeReport(Isolate* isolate, DumpEvent event, const char* messa out << "\n================================================================================"; out << "\n==== Node.js libuv Handle Summary ==============================================\n"; out << "\n(Flags: R=Ref, A=Active)\n"; - out << "\nFlags Type Address\n"; + out << std::left << std::setw(7) << "Flags" << std::setw(10) << "Type" + << std::setw(4 + 2 * sizeof(void*)) << "Address" << "Details" + << std::endl; uv_walk(uv_default_loop(), walkHandle, (void*)&out); // Print operating system information @@ -619,6 +816,9 @@ static void WriteNodeReport(Isolate* isolate, DumpEvent event, const char* messa out << "\n================================================================================\n"; out << std::flush; + // Restore output stream formatting. + out.copyfmt(oldState); + report_active = false; } diff --git a/test/common.js b/test/common.js index dbf24d8..89ab764 100644 --- a/test/common.js +++ b/test/common.js @@ -185,7 +185,7 @@ const getLibcVersion = (path) => { return (match != null ? match[1] : undefined); }; -const getSection = (report, section) => { +const getSection = exports.getSection = (report, section) => { const re = new RegExp('==== ' + section + ' =+' + reNewline + '+([\\S\\s]+?)' + reNewline + '+={80}' + reNewline); const match = re.exec(report); diff --git a/test/test-api-uvhandles.js b/test/test-api-uvhandles.js new file mode 100644 index 0000000..37e8f26 --- /dev/null +++ b/test/test-api-uvhandles.js @@ -0,0 +1,151 @@ +'use strict'; + +// Testcase to check reporting of uv handles. +if (process.argv[2] === 'child') { + // Exit on loss of parent process + const exit = () => process.exit(2); + process.on('disconnect', exit); + + const fs = require('fs'); + const http = require('http'); + const node_report = require('../'); + const spawn = require('child_process').spawn; + + // Watching files should result in fs_event/fs_poll uv handles. + let watcher; + try { + watcher = fs.watch(__filename); + } catch (exception) { + // fs.watch() unavailable + } + fs.watchFile(__filename, () => {}); + + // Child should exist when this returns as child_process.pid must be set. + const child_process = spawn(process.execPath, + ['-e', "process.stdin.on('data', (x) => console.log(x.toString()));"]); + + let timeout_count = 0; + const timeout = setInterval(() => { timeout_count++ }, 1000); + // Make sure the timer doesn't keep the test alive and let + // us check we detect unref'd handles correctly. + timeout.unref(); + + // Datagram socket for udp uv handles. + const dgram = require('dgram'); + const udp_socket = dgram.createSocket('udp4'); + udp_socket.bind({}); + + // Simple server/connection to create tcp uv handles. + const server = http.createServer((req, res) => { + req.on('end', () => { + // Generate the report while the connection is active. + console.log(node_report.getReport()); + child_process.kill(); + + res.writeHead(200, {'Content-Type': 'text/plain'}); + res.end(); + + // Tidy up to allow process to exit cleanly. + server.close(() => { + if (watcher) watcher.close(); + fs.unwatchFile(__filename); + udp_socket.close(); + process.removeListener('disconnect', exit); + }); + }); + req.resume(); + }); + server.listen(() => { + const data = { pid: child_process.pid, + tcp_address: server.address(), + udp_address: udp_socket.address(), + skip_fs_watch: (watcher === undefined ? + 'fs.watch() unavailable': + false) }; + process.send(data); + http.get({port: server.address().port}); + }); +} else { + const common = require('./common.js'); + const fork = require('child_process').fork; + const tap = require('tap'); + + const options = { encoding: 'utf8', silent: true }; + const child = fork(__filename, ['child'], options); + let child_data; + child.on('message', (data) => { child_data = data }); + let stderr = ''; + child.stderr.on('data', (chunk) => { stderr += chunk; }); + let stdout = ''; + child.stdout.on('data', (chunk) => { stdout += chunk; }); + child.on('exit', (code, signal) => { + tap.plan(14); + tap.strictSame(code, 0, 'Process should exit with expected exit code'); + tap.strictSame(signal, null, 'Process should exit cleanly'); + tap.strictSame(stderr, '', 'Checking no messages on stderr'); + const reports = common.findReports(child.pid); + tap.same(reports, [], 'Checking no report files were written'); + + // uv handle specific tests. + const address_re_str = '\\b(?:0+x)?[0-9a-fA-F]+\\b' + // fs_event and fs_poll handles for file watching. + // libuv returns file paths on Windows starting with '\\?\'. + const summary = common.getSection(stdout, 'Node.js libuv Handle Summary'); + const fs_event_re = new RegExp('\\[RA]\\s+fs_event\\s+' + address_re_str + + '\\s+filename: (\\\\\\\\\\\?\\\\)?' + + __filename.replace(/\\/g,'\\\\')); + tap.match(summary, fs_event_re, 'Checking fs_event uv handle', + { skip: child_data.skip_fs_watch }); + const fs_poll_re = new RegExp('\\[RA]\\s+fs_poll\\s+' + address_re_str + + '\\s+filename: (\\\\\\\\\\\?\\\\)?' + + __filename.replace(/\\/g,'\\\\')); + tap.match(summary, fs_poll_re, 'Checking fs_poll uv handle'); + + // pid handle for the process created by child_process.spawn(); + const pid_re = new RegExp('\\[RA]\\s+process\\s+' + address_re_str + + '.+\\bpid:\\s' + child_data.pid + '\\b'); + tap.match(summary, pid_re, 'Checking process uv handle'); + + // timer handle created by setInterval and unref'd. + const timeout_re = new RegExp('\\[-A]\\s+timer\\s+' + address_re_str + + '.+\\brepeat: 0, timeout in: \\d+ ms\\b'); + tap.match(summary, timeout_re, 'Checking timer uv handle'); + + // pipe handle for the IPC channel used by child_process_fork(). + const pipe_re = new RegExp('\\[RA]\\s+pipe\\s+' + address_re_str + + '.+\\breadable, writable\\b'); + tap.match(summary, pipe_re, 'Checking pipe uv handle'); + + // tcp handles. The report should contain three sockets: + // 1. The server's listening socket. + // 2. The inbound socket making the request. + // 3. The outbound socket sending the response. + const port = child_data.tcp_address.port; + const tcp_re = new RegExp('\\[RA]\\s+tcp\\s+' + address_re_str + + '\\s+\\S+:' + port + ' \\(not connected\\)'); + tap.match(summary, tcp_re, 'Checking listening socket tcp uv handle'); + const in_tcp_re = new RegExp('\\[RA]\\s+tcp\\s+' + address_re_str + + '\\s+\\S+:\\d+ connected to \\S+:' + + port + '\\b'); + tap.match(summary, in_tcp_re, + 'Checking inbound connection tcp uv handle'); + const out_tcp_re = new RegExp('\\[RA]\\s+tcp\\s+' + address_re_str + + '\\s+\\S+:' + port + + ' connected to \\S+:\\d+\\b'); + tap.match(summary, out_tcp_re, + 'Checking outbound connection tcp uv handle'); + + // udp handles. + const udp_re = new RegExp('\\[RA]\\s+udp\\s+' + address_re_str + + '\\s+\\S+:' + child_data.udp_address.port + + '\\b'); + tap.match(summary, udp_re, 'Checking udp uv handle'); + + // Common report tests. + tap.test('Validating report content', (t) => { + common.validateContent(stdout, t, {pid: child.pid, + commandline: child.spawnargs.join(' ') + }); + }); + }); +}