Skip to content

Commit 58c4a76

Browse files
committed
fix(node): Enforce that ContextLines integration does not leave open
file handles
1 parent 34d1fbe commit 58c4a76

File tree

12 files changed

+290
-5
lines changed

12 files changed

+290
-5
lines changed

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
1212

13-
Work in this release was contributed by @nwalters512, @aloisklink, @arturovt, @benjick and @maximepvrt. Thank you for your contributions!
13+
Work in this release was contributed by @nwalters512, @aloisklink, @arturovt, @benjick, @maximepvrt, and @mstrokin. Thank you for your contributions!
1414

1515
- **feat(solidstart)!: Default to `--import` setup and add `autoInjectServerSentry` ([#14862](https://github.com/getsentry/sentry-javascript/pull/14862))**
1616

dev-packages/node-integration-tests/suites/contextLines/test.ts renamed to dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { join } from 'path';
2-
import { createRunner } from '../../utils/runner';
2+
import { createRunner } from '../../../utils/runner';
33

44
describe('ContextLines integration in ESM', () => {
55
test('reads encoded context lines from filenames with spaces', done => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as Sentry from '@sentry/node';
2+
3+
export function captureException(i: number): void {
4+
Sentry.captureException(new Error(`error in loop ${i}`));
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { captureException } from './nested-file';
2+
3+
export function runSentry(): void {
4+
for (let i = 0; i < 10; i++) {
5+
captureException(i);
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as path from 'node:path';
2+
import { execSync } from 'node:child_process';
3+
4+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
5+
import * as Sentry from '@sentry/node';
6+
7+
Sentry.init({
8+
dsn: 'https://[email protected]/1337',
9+
release: '1.0',
10+
transport: loggingTransport,
11+
});
12+
13+
import { runSentry } from './other-file';
14+
15+
runSentry();
16+
17+
const lsofOutput = execSync(`lsof -p ${process.pid}`, { encoding: 'utf8' });
18+
const lsofTable = lsofOutput.split('\n');
19+
const mainPath = __dirname.replace(`${path.sep}suites${path.sep}contextLines${path.sep}memory-leak`, '');
20+
const numberOfLsofEntriesWithMainPath = lsofTable.filter(entry => entry.includes(mainPath));
21+
22+
// There should only be a single entry with the main path, otherwise we are leaking file handles from the
23+
// context lines integration.
24+
if (numberOfLsofEntriesWithMainPath.length > 1) {
25+
// eslint-disable-next-line no-console
26+
console.error('Leaked file handles detected');
27+
// eslint-disable-next-line no-console
28+
console.error(lsofTable);
29+
process.exit(1);
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
2+
3+
describe('ContextLines integration in CJS', () => {
4+
afterAll(() => {
5+
cleanupChildProcesses();
6+
});
7+
8+
// Regression test for: https://github.com/getsentry/sentry-javascript/issues/14892
9+
test('does not leak open file handles', done => {
10+
createRunner(__dirname, 'scenario.ts')
11+
.expectN(10, {
12+
event: {},
13+
})
14+
.start(done);
15+
});
16+
});

dev-packages/node-integration-tests/test.txt

+213
Large diffs are not rendered by default.

dev-packages/node-integration-tests/utils/runner.ts

+6
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ export function createRunner(...paths: string[]) {
168168
expectedEnvelopes.push(expected);
169169
return this;
170170
},
171+
expectN: function (n: number, expected: Expected) {
172+
for (let i = 0; i < n; i++) {
173+
expectedEnvelopes.push(expected);
174+
}
175+
return this;
176+
},
171177
expectHeader: function (expected: ExpectedEnvelopeHeader) {
172178
if (!expectedEnvelopeHeaders) {
173179
expectedEnvelopeHeaders = [];

packages/node/src/integrations/contextlines.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,21 @@ function getContextLinesFromFile(path: string, ranges: ReadlineRange[], output:
142142
input: stream,
143143
});
144144

145+
// We need to explicitly destroy the stream to prevent memory leaks,
146+
// removing the listeners on the readline interface is not enough.
147+
// See: https://github.com/nodejs/node/issues/9002 and https://github.com/getsentry/sentry-javascript/issues/14892
148+
function destroyStreamAndResolve(): void {
149+
stream.destroy();
150+
resolve();
151+
}
152+
145153
// Init at zero and increment at the start of the loop because lines are 1 indexed.
146154
let lineNumber = 0;
147155
let currentRangeIndex = 0;
148156
const range = ranges[currentRangeIndex];
149157
if (range === undefined) {
150158
// We should never reach this point, but if we do, we should resolve the promise to prevent it from hanging.
151-
resolve();
159+
destroyStreamAndResolve();
152160
return;
153161
}
154162
let rangeStart = range[0];
@@ -162,14 +170,14 @@ function getContextLinesFromFile(path: string, ranges: ReadlineRange[], output:
162170
DEBUG_BUILD && logger.error(`Failed to read file: ${path}. Error: ${e}`);
163171
lineReaded.close();
164172
lineReaded.removeAllListeners();
165-
resolve();
173+
destroyStreamAndResolve();
166174
}
167175

168176
// We need to handle the error event to prevent the process from crashing in < Node 16
169177
// https://github.com/nodejs/node/pull/31603
170178
stream.on('error', onStreamError);
171179
lineReaded.on('error', onStreamError);
172-
lineReaded.on('close', resolve);
180+
lineReaded.on('close', destroyStreamAndResolve);
173181

174182
lineReaded.on('line', line => {
175183
lineNumber++;

0 commit comments

Comments
 (0)