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
38 changes: 38 additions & 0 deletions src/node_report_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace report {

using node::JSONWriter;
using node::MallocedBuffer;
using node::MaybeStackBuffer;

static constexpr auto null = JSONWriter::Null{};

Expand Down Expand Up @@ -82,6 +83,40 @@ 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);
MaybeStackBuffer<char> buffer;
size_t buffer_size = buffer.capacity();
int rc = -1;

rc = uv_pipe_getsockname(&handle->pipe, buffer.out(), &buffer_size);
if (rc == UV_ENOBUFS) {
// Buffer is not large enough, reallocate to the updated buffer_size
// and fetch the value again.
buffer.AllocateSufficientStorage(buffer_size);
legendecas marked this conversation as resolved.
Show resolved Hide resolved
rc = uv_pipe_getsockname(&handle->pipe, buffer.out(), &buffer_size);
}
if (rc == 0 && buffer_size != 0) {
writer->json_keyvalue("localEndpointName", buffer.out());
legendecas marked this conversation as resolved.
Show resolved Hide resolved
} else {
writer->json_keyvalue("localEndpointName", null);
}

rc = uv_pipe_getpeername(&handle->pipe, buffer.out(), &buffer_size);
if (rc == UV_ENOBUFS) {
// Buffer is not large enough, reallocate to the updated buffer_size
// and fetch the value again.
buffer.AllocateSufficientStorage(buffer_size);
rc = uv_pipe_getpeername(&handle->pipe, buffer.out(), &buffer_size);
}
if (rc == 0 && buffer_size != 0) {
writer->json_keyvalue("remoteEndpointName", buffer.out());
} else {
writer->json_keyvalue("remoteEndpointName", 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 +182,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
170 changes: 128 additions & 42 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,54 +17,127 @@ 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;
Expand Down Expand Up @@ -116,6 +184,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 +202,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.localEndpointName === sockPath) {
if (handle.writable === false) {
found_named_pipe.push('listening');
} else {
found_named_pipe.push('inbound');
}
} else if (handle.remoteEndpointName === 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 +255,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 +265,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`);
legendecas marked this conversation as resolved.
Show resolved Hide resolved
}

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