Skip to content

Commit

Permalink
src: write named pipe info in diagnostic report
Browse files Browse the repository at this point in the history
Writes pipe handles with `uv_pipe_getsockname()`
and `uv_pipe_getpeername()`.

PR-URL: #38637
Fixes: #38625
Reviewed-By: Richard Lau <rlau@redhat.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Gireesh Punathil <gpunathi@in.ibm.com>
  • Loading branch information
legendecas authored and danielleadams committed May 31, 2021
1 parent ba96f14 commit 61c95f0
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 45 deletions.
39 changes: 39 additions & 0 deletions src/node_report_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,42 @@ static void ReportEndpoints(uv_handle_t* h, JSONWriter* writer) {
ReportEndpoint(h, rc == 0 ? addr : nullptr, "remoteEndpoint", writer);
}

// Utility function to format libuv pipe information.
static void ReportPipeEndpoints(uv_handle_t* h, JSONWriter* writer) {
uv_any_handle* handle = reinterpret_cast<uv_any_handle*>(h);
MallocedBuffer<char> buffer(0);
size_t buffer_size = 0;
int rc = -1;

// First call to get required buffer size.
rc = uv_pipe_getsockname(&handle->pipe, buffer.data, &buffer_size);
if (rc == UV_ENOBUFS) {
buffer = MallocedBuffer<char>(buffer_size);
if (buffer.data != nullptr) {
rc = uv_pipe_getsockname(&handle->pipe, buffer.data, &buffer_size);
}
}
if (rc == 0 && buffer_size != 0 && buffer.data != nullptr) {
writer->json_keyvalue("localEndpoint", buffer.data);
} else {
writer->json_keyvalue("localEndpoint", null);
}

// First call to get required buffer size.
rc = uv_pipe_getpeername(&handle->pipe, buffer.data, &buffer_size);
if (rc == UV_ENOBUFS) {
buffer = MallocedBuffer<char>(buffer_size);
if (buffer.data != nullptr) {
rc = uv_pipe_getpeername(&handle->pipe, buffer.data, &buffer_size);
}
}
if (rc == 0 && buffer_size != 0 && buffer.data != nullptr) {
writer->json_keyvalue("remoteEndpoint", buffer.data);
} else {
writer->json_keyvalue("remoteEndpoint", null);
}
}

// Utility function to format libuv path information.
static void ReportPath(uv_handle_t* h, JSONWriter* writer) {
MallocedBuffer<char> buffer(0);
Expand Down Expand Up @@ -147,6 +183,9 @@ void WalkHandle(uv_handle_t* h, void* arg) {
case UV_UDP:
ReportEndpoints(h, writer);
break;
case UV_NAMED_PIPE:
ReportPipeEndpoints(h, writer);
break;
case UV_TIMER: {
uint64_t due = handle->timer.timeout;
uint64_t now = uv_now(handle->timer.loop);
Expand Down
2 changes: 1 addition & 1 deletion test/report/test-report-uncaught-exception-primitives.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ process.on('uncaughtException', common.mustCall((err) => {
assert.strictEqual(err, exception);
const reports = helper.findReports(process.pid, tmpdir.path);
assert.strictEqual(reports.length, 1);
console.log(reports[0]);

helper.validate(reports[0], [
['header.event', 'Exception'],
['javascriptStack.message', `${exception}`],
Expand Down
2 changes: 1 addition & 1 deletion test/report/test-report-uncaught-exception-symbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ process.on('uncaughtException', common.mustCall((err) => {
assert.strictEqual(err, exception);
const reports = helper.findReports(process.pid, tmpdir.path);
assert.strictEqual(reports.length, 1);
console.log(reports[0]);

helper.validate(reports[0], [
['header.event', 'Exception'],
['javascriptStack.message', 'Symbol(foobar)'],
Expand Down
179 changes: 136 additions & 43 deletions test/report/test-report-uv-handles.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@

// Testcase to check reporting of uv handles.
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const path = require('path');
if (common.isIBMi)
common.skip('IBMi does not support fs.watch()');

if (process.argv[2] === 'child') {
// Exit on loss of parent process
const exit = () => process.exit(2);
process.on('disconnect', exit);
// This is quite similar to common.PIPE except that it uses an extended prefix
// of "\\?\pipe" on windows.
const PIPE = (() => {
const localRelative = path.relative(process.cwd(), `${tmpdir.path}/`);
const pipePrefix = common.isWindows ? '\\\\?\\pipe\\' : localRelative;
const pipeName = `node-test.${process.pid}.sock`;
return path.join(pipePrefix, pipeName);
})();

function createFsHandle(childData) {
const fs = require('fs');
const http = require('http');
const spawn = require('child_process').spawn;

// Watching files should result in fs_event/fs_poll uv handles.
let watcher;
try {
Expand All @@ -22,59 +26,129 @@ if (process.argv[2] === 'child') {
// fs.watch() unavailable
}
fs.watchFile(__filename, () => {});
childData.skip_fs_watch = watcher === undefined;

return () => {
if (watcher) watcher.close();
fs.unwatchFile(__filename);
};
}

function createChildProcessHandle(childData) {
const spawn = require('child_process').spawn;
// 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()));']);
const cp = spawn(process.execPath,
['-e', "process.stdin.on('data', (x) => " +
'console.log(x.toString()));']);
childData.pid = cp.pid;

return () => {
cp.kill();
};
}

function createTimerHandle() {
const timeout = setInterval(() => {}, 1000);
// Make sure the timer doesn't keep the test alive and let
// us check we detect unref'd handles correctly.
timeout.unref();
return () => {
clearInterval(timeout);
};
}

function createTcpHandle(childData) {
const http = require('http');

return new Promise((resolve) => {
// Simple server/connection to create tcp uv handles.
const server = http.createServer((req, res) => {
req.on('end', () => {
resolve(() => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end();
server.close();
});
});
req.resume();
});
server.listen(() => {
childData.tcp_address = server.address();
http.get({ port: server.address().port });
});
});
}

function createUdpHandle(childData) {
// Datagram socket for udp uv handles.
const dgram = require('dgram');
const udp_socket = dgram.createSocket('udp4');
const connected_udp_socket = dgram.createSocket('udp4');
udp_socket.bind({}, common.mustCall(() => {
connected_udp_socket.connect(udp_socket.address().port);
}));
const udpSocket = dgram.createSocket('udp4');
const connectedUdpSocket = dgram.createSocket('udp4');

return new Promise((resolve) => {
udpSocket.bind({}, common.mustCall(() => {
connectedUdpSocket.connect(udpSocket.address().port);

// 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(JSON.stringify(process.report.getReport(), null, 2));
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);
connected_udp_socket.close();
udp_socket.close();
process.removeListener('disconnect', exit);
childData.udp_address = udpSocket.address();
resolve(() => {
connectedUdpSocket.close();
udpSocket.close();
});
}));
});
}

function createNamedPipeHandle(childData) {
const net = require('net');
const sockPath = PIPE;
return new Promise((resolve) => {
const server = net.createServer((socket) => {
childData.pipe_sock_path = server.address();
resolve(() => {
socket.end();
server.close();
});
});
req.resume();
server.listen(
sockPath,
() => {
net.connect(sockPath, (socket) => {});
});
});
server.listen(() => {
const data = { pid: child_process.pid,
tcp_address: server.address(),
udp_address: udp_socket.address(),
skip_fs_watch: (watcher === undefined) };
process.send(data);
http.get({ port: server.address().port });
}

async function child() {
// Exit on loss of parent process
const exit = () => process.exit(2);
process.on('disconnect', exit);

const childData = {};
const disposes = await Promise.all([
createFsHandle(childData),
createChildProcessHandle(childData),
createTimerHandle(childData),
createTcpHandle(childData),
createUdpHandle(childData),
createNamedPipeHandle(childData),
]);
process.send(childData);

// Generate the report while the connection is active.
console.log(JSON.stringify(process.report.getReport(), null, 2));

// Tidy up to allow process to exit cleanly.
disposes.forEach((it) => {
it();
});
process.removeListener('disconnect', exit);
}

if (process.argv[2] === 'child') {
child();
} else {
const helper = require('../common/report.js');
const fork = require('child_process').fork;
const assert = require('assert');
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();
const options = { encoding: 'utf8', silent: true, cwd: tmpdir.path };
const child = fork(__filename, ['child'], options);
Expand All @@ -86,11 +160,11 @@ if (process.argv[2] === 'child') {
const report_msg = 'Report files were written: unexpectedly';
child.stdout.on('data', (chunk) => { stdout += chunk; });
child.on('exit', common.mustCall((code, signal) => {
assert.strictEqual(stderr.trim(), '');
assert.deepStrictEqual(code, 0, 'Process exited unexpectedly with code: ' +
`${code}`);
assert.deepStrictEqual(signal, null, 'Process should have exited cleanly,' +
` but did not: ${signal}`);
assert.strictEqual(stderr.trim(), '');

const reports = helper.findReports(child.pid, tmpdir.path);
assert.deepStrictEqual(reports, [], report_msg, reports);
Expand All @@ -116,6 +190,7 @@ if (process.argv[2] === 'child') {
const expected_filename = `${prefix}${__filename}`;
const found_tcp = [];
const found_udp = [];
const found_named_pipe = [];
// Functions are named to aid debugging when they are not called.
const validators = {
fs_event: common.mustCall(function fs_event_validator(handle) {
Expand All @@ -133,6 +208,21 @@ if (process.argv[2] === 'child') {
}),
pipe: common.mustCallAtLeast(function pipe_validator(handle) {
assert(handle.is_referenced);
// Pipe handles. The report should contain three pipes:
// 1. The server's listening pipe.
// 2. The inbound pipe making the request.
// 3. The outbound pipe sending the response.
//
// There is no way to distinguish inbound and outbound in a cross
// platform manner, so we just check inbound here.
const sockPath = child_data.pipe_sock_path;
if (handle.localEndpoint === sockPath) {
if (handle.writable === false) {
found_named_pipe.push('listening');
}
} else if (handle.remoteEndpoint === sockPath) {
found_named_pipe.push('inbound');
}
}),
process: common.mustCall(function process_validator(handle) {
assert.strictEqual(handle.pid, child_data.pid);
Expand Down Expand Up @@ -172,7 +262,7 @@ if (process.argv[2] === 'child') {
assert(handle.is_referenced);
}, 2),
};
console.log(report.libuv);

for (const entry of report.libuv) {
if (validators[entry.type]) validators[entry.type](entry);
}
Expand All @@ -182,6 +272,9 @@ if (process.argv[2] === 'child') {
for (const socket of ['connected', 'unconnected']) {
assert(found_udp.includes(socket), `${socket} UDP socket was not found`);
}
for (const socket of ['listening', 'inbound']) {
assert(found_named_pipe.includes(socket), `${socket} named pipe socket was not found`);
}

// Common report tests.
helper.validateContent(stdout);
Expand Down

0 comments on commit 61c95f0

Please sign in to comment.