Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

src: write named pipe info in diagnostic report #38637

Closed
wants to merge 10 commits into from
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);
addaleax marked this conversation as resolved.
Show resolved Hide resolved
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
171 changes: 128 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,13 @@

// Testcase to check reporting of uv handles.
const common = require('../common');
const tmpdir = require('../common/tmpdir');
richardlau marked this conversation as resolved.
Show resolved Hide resolved
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);

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 +17,131 @@ 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 fs = require('fs');
fs.mkdirSync(tmpdir.path, { recursive: true });
const sockPath = path.join(tmpdir.path, 'test-report-uv-handles.sock');
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 Down Expand Up @@ -116,6 +183,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 +201,20 @@ 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.
const sockPath = child_data.pipe_sock_path;
if (handle.localEndpoint === sockPath) {
if (handle.writable === false) {
found_named_pipe.push('listening');
} else {
found_named_pipe.push('inbound');
}
} else if (handle.remoteEndpoint === sockPath) {
found_named_pipe.push('outbound');
}
}),
process: common.mustCall(function process_validator(handle) {
assert.strictEqual(handle.pid, child_data.pid);
Expand Down Expand Up @@ -172,7 +254,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 +264,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', 'outbound']) {
assert(found_named_pipe.includes(socket), `${socket} named pipe socket was not found`);
}

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