node-opcua-leak-detector 2.165.0 → 2.168.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "node-opcua-leak-detector",
3
- "version": "2.165.0",
3
+ "version": "2.168.0",
4
4
  "description": "pure nodejs OPCUA SDK - module leak-detector",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
7
  "scripts": {
8
8
  "lint": "eslint src/**/*.js",
9
9
  "clean": "npx rimraf -g node_modules dist *.tsbuildinfo",
10
- "test": "echo no test"
10
+ "test": "mocha test"
11
11
  },
12
12
  "author": "Etienne Rossignon",
13
13
  "license": "MIT",
@@ -24,14 +24,14 @@
24
24
  "internet of things"
25
25
  ],
26
26
  "homepage": "http://node-opcua.github.io/",
27
- "gitHead": "fe53ac03427fcb223996446c991f7496635ba193",
27
+ "gitHead": "653b6d6df801ca17298308089dee32e5b12102b6",
28
28
  "files": [
29
29
  "src"
30
30
  ],
31
31
  "dependencies": {
32
32
  "chalk": "4.1.2",
33
33
  "node-opcua-assert": "2.164.0",
34
- "node-opcua-object-registry": "2.165.0",
34
+ "node-opcua-object-registry": "2.168.0",
35
35
  "wtfnode": "^0.10.1"
36
36
  }
37
37
  }
@@ -1,5 +1,4 @@
1
- "use strict";
2
- Error.stackTraceLimit = Infinity;
1
+ Error.stackTraceLimit = 30;
3
2
  const chalk = require("chalk");
4
3
  const { assert } = require("node-opcua-assert");
5
4
 
@@ -46,23 +45,23 @@ function ResourceLeakDetector() {
46
45
  * @param info
47
46
  * @return {boolean}
48
47
  */
49
- ResourceLeakDetector.prototype.verify_registry_counts = function(info) {
48
+ ResourceLeakDetector.prototype.verify_registry_counts = (info) => {
49
+ const self = ResourceLeakDetector.singleton;
50
50
  const errorMessages = [];
51
51
 
52
- if (this.clearIntervalCallCount !== this.setIntervalCallCount) {
53
- errorMessages.push(" setInterval doesn't match number of clearInterval calls : \n " +
54
- " setIntervalCallCount = " + this.setIntervalCallCount +
55
- " clearIntervalCallCount = " + this.clearIntervalCallCount);
52
+ if (self.clearIntervalCallCount !== self.setIntervalCallCount) {
53
+ errorMessages.push(` setInterval doesn't match number of clearInterval calls : \n ` +
54
+ ` setIntervalCallCount = ${self.setIntervalCallCount}` +
55
+ ` clearIntervalCallCount = ${self.clearIntervalCallCount}`);
56
56
  }
57
- if ((this.clearTimeoutCallCount + this.honoredTimeoutFuncCallCount) !== this.setTimeoutCallCount) {
58
- errorMessages.push(" setTimeout doesn't match number of clearTimeout or achieved timer calls : \n " +
59
- " setTimeoutCallCount = " + this.setTimeoutCallCount +
60
- " clearTimeoutCallCount = " + this.clearTimeoutCallCount +
61
- " honoredTimeoutFuncCallCount = " + this.honoredTimeoutFuncCallCount);
57
+ if ((self.clearTimeoutCallCount + self.honoredTimeoutFuncCallCount) !== self.setTimeoutCallCount) {
58
+ errorMessages.push(` setTimeout doesn't match number of clearTimeout or achieved timer calls : \n ` +
59
+ ` setTimeoutCallCount = ${self.setTimeoutCallCount}` +
60
+ ` clearTimeoutCallCount = ${self.clearTimeoutCallCount}` +
61
+ ` honoredTimeoutFuncCallCount = ${self.honoredTimeoutFuncCallCount}`);
62
62
  }
63
- if (this.setTimeoutCallPendingCount !== 0) {
64
- errorMessages.push(" setTimeoutCallPendingCount is not zero: some timer are still pending " +
65
- this.setTimeoutCallPendingCount);
63
+ if (self.setTimeoutCallPendingCount !== 0) {
64
+ errorMessages.push(` setTimeoutCallPendingCount is not zero: some timer are still pending ${self.setTimeoutCallPendingCount}`);
66
65
  }
67
66
 
68
67
  const monitoredResource = ObjectRegistry.registries;
@@ -72,7 +71,7 @@ ResourceLeakDetector.prototype.verify_registry_counts = function(info) {
72
71
  const res = monitoredResource[i];
73
72
  if (res.count() !== 0) {
74
73
  errorMessages.push(chalk.cyan(" some Resource have not been properly terminated: \n"));
75
- errorMessages.push(" " + res.toString());
74
+ errorMessages.push(` ${res.toString()}`);
76
75
  }
77
76
  totalLeak += res.count();
78
77
  }
@@ -87,19 +86,19 @@ ResourceLeakDetector.prototype.verify_registry_counts = function(info) {
87
86
 
88
87
  console.log("----------------------------------------------- more info");
89
88
 
90
- console.log(chalk.cyan("test filename : "), this.ctx ? this.ctx.test.parent.file + " " + this.ctx.test.parent.title : "???");
91
- console.log(chalk.cyan("setInterval/clearInterval leaks : "), Object.entries(this.interval_map).length);
92
- for (const [key, value] of Object.entries(this.interval_map)) {
89
+ console.log(chalk.cyan("test filename : "), self.ctx ? `${self.ctx.test.parent.file} ${self.ctx.test.parent.title}` : "???");
90
+ console.log(chalk.cyan("setInterval/clearInterval leaks : "), Object.entries(self.interval_map).length);
91
+ for (const [key, value] of Object.entries(self.interval_map)) {
93
92
  if (value && !value.disposed) {
94
93
  console.log("key =", key, "value.disposed = ", value.disposed);
95
- console.log(value.stack);//.split("\n"));
94
+ console.log(value.stack);
96
95
  }
97
96
  }
98
- console.log(chalk.cyan("setTimeout/clearTimeout leaks : "), Object.entries(this.timeout_map).length);
99
- for (const [key, value] of Object.entries(this.timeout_map)) {
97
+ console.log(chalk.cyan("setTimeout/clearTimeout leaks : "), Object.entries(self.timeout_map).length);
98
+ for (const [key, value] of Object.entries(self.timeout_map)) {
100
99
  if (value && !value.disposed) {
101
100
  console.log("setTimeout key =", key, "value.disposed = ", value.disposed);
102
- console.log(value.stack);//.split("\n"));
101
+ console.log(value.stack);
103
102
  }
104
103
  }
105
104
  console.log(chalk.cyan("object leaks : "), totalLeak);
@@ -111,21 +110,22 @@ ResourceLeakDetector.prototype.verify_registry_counts = function(info) {
111
110
 
112
111
  console.log(errorMessages.join("\n"));
113
112
 
114
-
115
- console.log("you can get trace information if you set NODEOPCUA_REGISTRY=DEBUG and rerun")
113
+ console.log("you can get trace information if you set NODEOPCUA_REGISTRY=DEBUG and rerun");
116
114
  //
117
- throw new Error("LEAKS !!!" + errorMessages.join("\n"));
115
+ throw new Error(`LEAKS !!!${errorMessages.join("\n")}`);
118
116
  }
119
117
  }
120
118
  };
121
119
 
122
120
  global.hasResourceLeakDetector = false;
123
- ResourceLeakDetector.prototype.start = function(info) {
121
+ ResourceLeakDetector.prototype.start = (info) => {
124
122
 
125
123
  global.ResourceLeakDetectorStarted = true;
126
124
 
127
125
  const self = ResourceLeakDetector.singleton;
128
126
 
127
+
128
+
129
129
  if (trace) {
130
130
  console.log("[LeakDetector] 🚀 starting resourceLeakDetector");
131
131
  }
@@ -152,16 +152,21 @@ ResourceLeakDetector.prototype.start = function(info) {
152
152
 
153
153
  self.verify_registry_counts(self, info);
154
154
 
155
+ // Track active timer handles for cleanup in stop().
156
+ // This is a lightweight tracking that doesn't wrap the API (no assertions,
157
+ // no wrapper objects) — it just records handles so stop() can clear them.
158
+ self._activeTimeouts = new Set();
159
+
155
160
  if (monitor_intervals) {
156
- global.setTimeout = function(func, delay) {
161
+ global.setTimeout = (func, delay, ...extra) => {
157
162
 
158
- assert(arguments.length === 2, "current limitation: setTimeout must be called with 2 arguments");
163
+ assert(extra.length === 0, "current limitation: setTimeout must be called with 2 arguments");
159
164
  // detect invalid delays
160
165
  assert(delay !== undefined);
161
- assert(isFinite(delay));
166
+ assert(Number.isFinite(delay));
162
167
  if (delay < 0) {
163
- console.log("[LeakDetector] ❌ GLOBAL#setTimeout called with a too small delay = " + delay.toString());
164
- throw new Error("[LeakDetector] ❌ GLOBAL#setTimeout called with a too small delay = " + delay.toString());
168
+ console.log(`[LeakDetector] ❌ GLOBAL#setTimeout called with a too small delay = ${delay}`);
169
+ throw new Error(`[LeakDetector] ❌ GLOBAL#setTimeout called with a too small delay = ${delay}`);
165
170
  }
166
171
 
167
172
  // increase number of pending timers
@@ -172,15 +177,14 @@ ResourceLeakDetector.prototype.start = function(info) {
172
177
 
173
178
  const key = self.setTimeoutCallCount;
174
179
 
175
- const timeoutId = self.setTimeout_old(function() {
180
+ const timeoutId = self.setTimeout_old(() => {
176
181
 
177
182
  if (!self.timeout_map[key] || self.timeout_map[key].isCleared) {
178
- // throw new Error("Invalid timeoutId, timer has already been cleared - " + key);
179
- console.log("[LeakDetector] ❌ WARNING : setTimeout: Invalid timeoutId, timer has already been cleared - " + key);
183
+ console.log(`[LeakDetector] WARNING : setTimeout: Invalid timeoutId, timer has already been cleared - ${key}`);
180
184
  return;
181
185
  }
182
186
  if (self.timeout_map[key].hasBeenHonored) {
183
- throw new Error("[LeakDetector] ❌ setTimeout: " + key + " time out has already been honored");
187
+ throw new Error(`[LeakDetector] ❌ setTimeout: ${key} time out has already been honored`);
184
188
  }
185
189
  self.honoredTimeoutFuncCallCount += 1;
186
190
  self.setTimeoutCallPendingCount -= 1;
@@ -192,7 +196,7 @@ ResourceLeakDetector.prototype.start = function(info) {
192
196
  }, delay);
193
197
 
194
198
  self.timeout_map[key] = {
195
- timeoutId: timeoutId,
199
+ timeoutId,
196
200
  disposed: false,
197
201
  stack: get_stack() // stack when created
198
202
  };
@@ -209,10 +213,10 @@ ResourceLeakDetector.prototype.start = function(info) {
209
213
  return wrapper;
210
214
  };
211
215
 
212
- global.clearTimeout = function(timeoutId) {
213
- // workaround for a bug in 'backoff' module, which call clearTimeout with -1 ( invalid ide)
216
+ global.clearTimeout = (timeoutId) => {
217
+ // workaround for a bug in 'backoff' module, which call clearTimeout with -1 ( invalid id)
214
218
  if (timeoutId === -1) {
215
- console.log("[LeakDetector] ❌ warning clearTimeout is called with illegal timeoutId === 1, this call will be ignored ( backoff module bug?)");
219
+ console.log("[LeakDetector] ❌ warning clearTimeout is called with illegal timeoutId === -1, this call will be ignored ( backoff module bug?)");
216
220
  return;
217
221
  }
218
222
 
@@ -225,15 +229,15 @@ ResourceLeakDetector.prototype.start = function(info) {
225
229
  timeoutId -= 100000;
226
230
 
227
231
  if (!self.timeout_map[timeoutId]) {
228
- console.log("timeoutId" + timeoutId, " has already been discarded or doesn't exist");
232
+ console.log(`timeoutId${timeoutId}`, " has already been discarded or doesn't exist");
229
233
  console.log("self.timeout_map", self.timeout_map);
230
- throw new Error("clearTimeout: Invalid timeoutId " + timeoutId + " this may happen if clearTimeout is called inside the setTimeout function");
234
+ throw new Error(`clearTimeout: Invalid timeoutId ${timeoutId} this may happen if clearTimeout is called inside the setTimeout function`);
231
235
  }
232
236
  if (self.timeout_map[timeoutId].isCleared) {
233
- throw new Error("clearTimeout: Invalid timeoutId " + timeoutId + " time out has already been cleared");
237
+ throw new Error(`clearTimeout: Invalid timeoutId ${timeoutId} time out has already been cleared`);
234
238
  }
235
239
  if (self.timeout_map[timeoutId].hasBeenHonored) {
236
- throw new Error("clearTimeout: Invalid timeoutId " + timeoutId + " time out has already been honored");
240
+ throw new Error(`clearTimeout: Invalid timeoutId ${timeoutId} time out has already been honored`);
237
241
  }
238
242
 
239
243
  const data = self.timeout_map[timeoutId];
@@ -250,18 +254,33 @@ ResourceLeakDetector.prototype.start = function(info) {
250
254
  // call original clearTimeout
251
255
  const retValue = self.clearTimeout_old(data.timeoutId);
252
256
 
253
- //xx delete self.timeout_map[timeoutId];
254
257
  return retValue;
255
258
  };
256
259
 
260
+ } else {
261
+ // Lightweight setTimeout/clearTimeout tracking (no assertions, no wrapper IDs).
262
+ // We just record raw timer handles so stop() can clear leaked ones.
263
+ global.setTimeout = (fn, ...rest) => {
264
+ const handle = self.setTimeout_old(() => {
265
+ // Timer fired naturally — remove from tracking
266
+ self._activeTimeouts.delete(handle);
267
+ fn();
268
+ }, ...rest);
269
+ self._activeTimeouts.add(handle);
270
+ return handle;
271
+ };
272
+ global.clearTimeout = (handle) => {
273
+ self._activeTimeouts.delete(handle);
274
+ return self.clearTimeout_old(handle);
275
+ };
257
276
  }
258
277
 
259
- global.setInterval = function(func, delay) {
260
- assert(arguments.length === 2);
278
+ global.setInterval = (func, delay, ...extra) => {
279
+ assert(extra.length === 0);
261
280
  assert(delay !== undefined);
262
- assert(isFinite(delay));
281
+ assert(Number.isFinite(delay));
263
282
  if (delay <= 10) {
264
- throw new Error("[LeakDetector] ⏰ GLOBAL#setInterval called with a too small delay = " + delay.toString());
283
+ throw new Error(`[LeakDetector] ⏰ GLOBAL#setInterval called with a too small delay = ${delay}`);
265
284
  }
266
285
 
267
286
  // increase number of pending timers
@@ -275,13 +294,13 @@ ResourceLeakDetector.prototype.start = function(info) {
275
294
  try {
276
295
  stack = get_stack();
277
296
  }
278
- catch (err) {
297
+ catch (_err) {
279
298
  /** */
280
299
  }
281
300
  self.interval_map[key] = {
282
- intervalId: intervalId,
301
+ intervalId,
283
302
  disposed: false,
284
- stack: stack
303
+ stack
285
304
  };
286
305
 
287
306
  if (trace) {
@@ -300,7 +319,7 @@ ResourceLeakDetector.prototype.start = function(info) {
300
319
  return wrapper;
301
320
  };
302
321
 
303
- global.clearInterval = function(intervalId) {
322
+ global.clearInterval = (intervalId) => {
304
323
 
305
324
  // Support both raw keys (number) and wrapper objects (from our proxy)
306
325
  const key = (intervalId && intervalId._key !== undefined) ? intervalId._key : +intervalId;
@@ -311,7 +330,7 @@ ResourceLeakDetector.prototype.start = function(info) {
311
330
  self.clearIntervalCallCount += 1;
312
331
 
313
332
  if (trace) {
314
- console.log("[LeakDetector] 🔄 clearInterval " + key, get_stack());
333
+ console.log(`[LeakDetector] 🔄 clearInterval ${key}`, get_stack());
315
334
  }
316
335
 
317
336
  const data = self.interval_map[key];
@@ -327,11 +346,11 @@ ResourceLeakDetector.prototype.start = function(info) {
327
346
 
328
347
  };
329
348
 
330
- ResourceLeakDetector.prototype.check = function() {
349
+ ResourceLeakDetector.prototype.check = () => {
331
350
  /** */
332
351
  };
333
352
 
334
- ResourceLeakDetector.prototype.stop = function(info) {
353
+ ResourceLeakDetector.prototype.stop = (info) => {
335
354
  if (!global.ResourceLeakDetectorStarted) {
336
355
  return;
337
356
  }
@@ -358,23 +377,58 @@ ResourceLeakDetector.prototype.stop = function(info) {
358
377
 
359
378
  const results = self.verify_registry_counts(info);
360
379
 
380
+ // Clear any remaining tracked timeouts/intervals to prevent process hang.
381
+ // Mocha 12+ does not call process.exit() unless --exit is set, so any
382
+ // ref'd timer will keep the event loop alive indefinitely.
383
+ for (const [, data] of Object.entries(self.interval_map)) {
384
+ if (data && !data.disposed) {
385
+ global.clearInterval(data.intervalId);
386
+ }
387
+ }
388
+ for (const [, data] of Object.entries(self.timeout_map)) {
389
+ if (data && !data.disposed) {
390
+ global.clearTimeout(data.timeoutId);
391
+ }
392
+ }
393
+ // Also clear lightweight-tracked timeout handles (monitor_intervals=false mode)
394
+ if (self._activeTimeouts && self._activeTimeouts.size > 0) {
395
+ // Count only ref'd handles (those that would prevent process exit)
396
+ const leakedRefHandles = [...self._activeTimeouts].filter(
397
+ (h) => typeof h.hasRef === "function" && h.hasRef()
398
+ );
399
+ if (leakedRefHandles.length > 0 && !info.silent) {
400
+ console.log(chalk.yellow(`[LeakDetector] ⚠️ ${leakedRefHandles.length} setTimeout handle(s) were not cleared!`));
401
+ }
402
+ for (const handle of self._activeTimeouts) {
403
+ global.clearTimeout(handle);
404
+ }
405
+ self._activeTimeouts.clear();
406
+ }
361
407
  self.interval_map = {};
362
408
  self.timeout_map = {};
363
409
 
364
-
365
410
  // call garbage collector
366
411
  if (typeof global.gc === "function") {
367
412
  global.gc(true);
368
413
  }
369
414
 
370
- const doHeapdump = false;
371
- if (doHeapdump) {
372
- const heapdump = require('heapdump');
373
- heapdump.writeSnapshot(function(err, filename) {
374
- console.log('dump written to', filename);
375
- });
415
+ // Diagnostic: dump active handles to identify what keeps the event loop alive
416
+ if (trace && typeof process._getActiveHandles === "function") {
417
+ const handles = process._getActiveHandles();
418
+ const refHandles = handles.filter((h) => typeof h.hasRef !== "function" || h.hasRef());
419
+ console.log(`[LeakDetector] 🔍 stop(): checking ${refHandles.length} active handles`);
420
+ for (const h of refHandles) {
421
+ const type = h.constructor ? h.constructor.name : typeof h;
422
+ console.log(`[LeakDetector] handle: ${type}`);
423
+ }
376
424
  }
377
425
 
426
+ // Note: we no longer schedule a process.exit() timer here.
427
+ // The previous 2s unref'd timer was meant to handle tsx keeping
428
+ // ref'd stdout handles alive, but it caused premature exits when
429
+ // run_all_mocha_tests.js runs multiple suites in a single process.
430
+ // The test runner's own process.exit(failures) handles cleanup.
431
+
378
432
  return results;
379
433
  };
380
434
 
@@ -468,7 +522,7 @@ exports.installResourceLeakDetector = function(isGlobal, func) {
468
522
  global.gc(true);
469
523
  }
470
524
  // give some time to relax
471
- await new Promise(resolve => setTimeout(resolve, 200));
525
+ await new Promise((resolve) => setTimeout(resolve, 200));
472
526
  afterSnapshot = takeMemorySnapshot();
473
527
  checkForMemoryLeak(beforeSnapshot, afterSnapshot);
474
528
  });
@@ -488,7 +542,7 @@ function replacement_it(testName, f) {
488
542
  }
489
543
  done(err);
490
544
  });
491
- }
545
+ };
492
546
  global_it(testName, f1);
493
547
  return;
494
548
  }
@@ -502,7 +556,7 @@ function replacement_it(testName, f) {
502
556
  throw err;
503
557
  }
504
558
  return r;
505
- }
559
+ };
506
560
  global_it(testName, ff);
507
561
  }
508
562
  assert(typeof global_describe === "function", " expecting mocha to be defined");
@@ -526,6 +580,3 @@ exports.describeWithLeakDetector = function(message, func) {
526
580
  global.it = global_it;
527
581
  });
528
582
  };
529
-
530
-
531
-