@vbarbarosh/node-helpers 3.70.1 → 3.72.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.
Files changed (56) hide show
  1. package/README.md +1 -1
  2. package/dist/browser.js +289 -13
  3. package/package.json +8 -2
  4. package/src/HeartbeatServer.js +38 -10
  5. package/src/HeartbeatServer.test.js +31 -0
  6. package/src/array_gcd.js +3 -10
  7. package/src/array_gcd.md +2 -0
  8. package/src/array_lcm.js +8 -0
  9. package/src/array_lcm.md +2 -0
  10. package/src/array_lcm.test.js +12 -0
  11. package/src/array_max.md +2 -1
  12. package/src/array_min.md +2 -1
  13. package/src/cli-apps/watchdog.d/connect_close.js +17 -0
  14. package/src/cli-apps/watchdog.d/connect_close2.js +20 -0
  15. package/src/cli-apps/watchdog.d/connect_close3.js +25 -0
  16. package/src/cli-apps/watchdog.d/connect_no_ping.js +13 -0
  17. package/src/cli-apps/watchdog.d/early_exit.js +7 -0
  18. package/src/cli-apps/watchdog.d/flood.js +13 -0
  19. package/src/cli-apps/watchdog.d/flood2.js +15 -0
  20. package/src/cli-apps/watchdog.d/hang.js +8 -0
  21. package/src/cli-apps/watchdog.d/huge_delay.js +18 -0
  22. package/src/cli-apps/watchdog.d/ignore_sigterm.js +29 -0
  23. package/src/cli-apps/watchdog.d/late_heartbeat.js +21 -0
  24. package/src/cli-apps/watchdog.d/my-exit-after-2-pings.js +16 -0
  25. package/src/cli-apps/watchdog.d/my-happy-path.js +16 -0
  26. package/src/cli-apps/watchdog.d/my-ignore-sigterm.js +22 -0
  27. package/src/cli-apps/watchdog.d/my-never-ping.js +15 -0
  28. package/src/cli-apps/watchdog.d/no_heartbeat.js +5 -0
  29. package/src/cli-apps/watchdog.d/ok.js +17 -0
  30. package/src/cli-apps/watchdog.d/spawn_children.js +14 -0
  31. package/src/cli-apps/watchdog.js +146 -0
  32. package/src/cli.js +11 -3
  33. package/src/errors/ExitCodeError.js +9 -0
  34. package/src/fs_assert_file_socket.js +11 -0
  35. package/src/http_get_stream.js +2 -2
  36. package/src/math_gcd.js +10 -0
  37. package/src/math_gcd.md +2 -0
  38. package/src/math_lcm.js +8 -0
  39. package/src/math_lcm.md +2 -0
  40. package/src/parse_hms.js +43 -0
  41. package/src/parse_hms.md +9 -0
  42. package/src/parse_hms.test.js +32 -0
  43. package/src/pgid_exists.js +14 -0
  44. package/src/pgid_kill_grace.js +55 -0
  45. package/src/pid_exists.js +1 -0
  46. package/src/pid_kill_grace.d/ignore-sigterm-for-10ms.js +33 -0
  47. package/src/pid_kill_grace.d/ignore-sigterm.js +22 -0
  48. package/src/pid_kill_grace.js +35 -9
  49. package/src/pid_kill_grace.test.js +63 -0
  50. package/src/ping_socket.js +8 -8
  51. package/src/ping_socket.test.js +58 -0
  52. package/src/range.js +44 -0
  53. package/src/range.md +15 -0
  54. package/src/range.test.js +41 -0
  55. package/src/shell_spawn.js +26 -1
  56. package/src/strtr.js +15 -0
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ❌ 8. Client connects but NEVER sends PING
4
+ // Simulates misbehaving client.
5
+
6
+ const net = require('net');
7
+
8
+ net.connect(process.env.WATCHDOG_SOCKET, () => {
9
+ console.log('connected but no ping');
10
+ // do nothing
11
+ });
12
+
13
+ setInterval(() => {}, 1000);
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ❌ 4. Client that exits immediately
4
+ // Watchdog must detect child exit first.
5
+
6
+ console.log('bye');
7
+ process.exit(0);
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ❌ 7. Client that floods watchdog with 1GB of data
4
+ // Outcome: watchdog must handle MASSIVE reads and still time out.
5
+
6
+ const fs = require('fs');
7
+ const net = require('net');
8
+
9
+ const socket = net.connect(process.env.WATCHDOG_SOCKET, () => {
10
+ console.log('connected');
11
+ const stream = fs.createReadStream('/dev/zero');
12
+ stream.pipe(socket);
13
+ });
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ❌ 7. Client that floods watchdog with 1GB of data
4
+ // Outcome: watchdog must handle MASSIVE reads and still time out.
5
+
6
+ const fs = require('fs');
7
+ const net = require('net');
8
+
9
+ const socket = net.connect(process.env.WATCHDOG_SOCKET, () => {
10
+ console.log('connected');
11
+ const stream = fs.createReadStream('/dev/zero');
12
+ stream.pipe(socket).on('error', () => 0);
13
+ });
14
+
15
+ setInterval(() => {}, 1000);
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ❌ 5. Client that freezes (event loop blocked)
4
+ // Watchdog will not receive PINGs.
5
+
6
+ while (true) {
7
+ // infinite CPU loop
8
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ❌ 10. Client sends heartbeats but with huge delay
4
+ // Tests delayed heartbeat behavior.
5
+ // Expected: watchdog should kill before that.
6
+
7
+ const net = require('net');
8
+
9
+ const INTERVAL = Number(process.env.WATCHDOG_INTERVAL || 1000);
10
+
11
+ const socket = net.connect(process.env.WATCHDOG_SOCKET, () => {
12
+ console.log('connected');
13
+
14
+ setTimeout(() => {
15
+ console.log('sending late heartbeat');
16
+ socket.end('PING');
17
+ }, INTERVAL * 10);
18
+ });
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+
3
+ // 1️⃣ Client that ignores SIGTERM
4
+ // Goal: process stays alive after SIGTERM and only dies on SIGKILL (exactly what pid_kill_grace should handle).
5
+ //
6
+ // What this tests for watchdog:
7
+ // - watchdog sends SIGTERM → process prints “ignored” and keeps running.
8
+ // - pid_kill_grace waits grace_timeout_ms, sees PID still alive, sends SIGKILL.
9
+ // - Process must die after SIGKILL.
10
+ // - Watchdog should log the whole sequence correctly.
11
+ // This is a realistic scenario.
12
+
13
+ console.log('PID:', process.pid);
14
+
15
+ // Ignore SIGTERM
16
+ process.on('SIGTERM', function () {
17
+ console.log('SIGTERM received, but intentionally ignored');
18
+ });
19
+
20
+ // Still react to SIGINT (Ctrl+C) so you can stop it manually if needed
21
+ process.on('SIGINT', function () {
22
+ console.log('SIGINT received, exiting');
23
+ process.exit(130);
24
+ });
25
+
26
+ // Keep process alive
27
+ setInterval(() => {
28
+ // simulate work
29
+ }, 1000);
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ❌ 11. Client sends heartbeats sporadically (sometimes missing)
4
+ // Simulates flaky client.
5
+
6
+ const net = require('net');
7
+
8
+ function randomDelay() {
9
+ return Math.floor(Math.random() * 5000);
10
+ }
11
+
12
+ setTimeout(ping, randomDelay());
13
+
14
+ function ping()
15
+ {
16
+ const socket = net.connect(process.env.WATCHDOG_SOCKET);
17
+ socket.on('connect', () => {
18
+ socket.end('PING');
19
+ setTimeout(ping, randomDelay());
20
+ });
21
+ }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+
3
+ const Promise = require('bluebird');
4
+ const cli = require('../../cli');
5
+ const ping_socket = require('../../ping_socket');
6
+
7
+ cli(main);
8
+
9
+ async function main()
10
+ {
11
+ for (let iter = 1, last = 2; iter <= last; ++iter) {
12
+ await ping_socket(process.env.WATCHDOG_SOCKET);
13
+ await Promise.delay(1000);
14
+ }
15
+ console.log('Bye!');
16
+ }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+
3
+ const Promise = require('bluebird');
4
+ const cli = require('@vbarbarosh/node-helpers/src/cli');
5
+ const ping_socket = require('@vbarbarosh/node-helpers/src/ping_socket');
6
+
7
+ cli(main);
8
+
9
+ async function main()
10
+ {
11
+ for (let i = 0; i < 100; ++i) {
12
+ console.log('[client]', i);
13
+ await ping_socket(process.env.WATCHDOG_SOCKET);
14
+ await Promise.delay(1000);
15
+ }
16
+ }
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ const Promise = require('bluebird');
4
+ const cli = require('@vbarbarosh/node-helpers/src/cli');
5
+ const now_human = require('@vbarbarosh/node-helpers/src/now_human');
6
+
7
+ cli(main);
8
+
9
+ async function main()
10
+ {
11
+ process.on('SIGTERM', function () {
12
+ console.log(`[${now_human()}][ignore-sigterm] SIGTERM, ignoring...`);
13
+ });
14
+ process.on('SIGINT', function () {
15
+ console.log(`[${now_human()}][ignore-sigterm] SIGINT, ignoring...`);
16
+ });
17
+
18
+ for (let iter = 1, last = 100; iter <= last; ++iter) {
19
+ console.log(`[${now_human()}][ignore-sigterm] ${iter} of ${last}`);
20
+ await Promise.delay(100);
21
+ }
22
+ }
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+
3
+ const Promise = require('bluebird');
4
+ const cli = require('@vbarbarosh/node-helpers/src/cli');
5
+ const now_human = require('@vbarbarosh/node-helpers/src/now_human');
6
+
7
+ cli(main);
8
+
9
+ async function main()
10
+ {
11
+ for (let iter = 1, last = 100; iter <= last; ++iter) {
12
+ console.log(`[${now_human()}][never-ping] ${iter} of ${last}`);
13
+ await Promise.delay(100);
14
+ }
15
+ }
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ❌ 2. Client that never sends heartbeat (expected: watchdog kills it)
4
+
5
+ setInterval(() => {}, 1000); // alive but no PINGs
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ✅ 1. Client that works normally (sends correct PINGs)
4
+
5
+ const net = require('net');
6
+
7
+ const SOCKET = process.env.WATCHDOG_SOCKET;
8
+ const INTERVAL = Number(process.env.WATCHDOG_INTERVAL || 1000);
9
+
10
+ if (!SOCKET) throw new Error('WATCHDOG_SOCKET missing');
11
+
12
+ setInterval(ping, INTERVAL / 2);
13
+
14
+ function ping()
15
+ {
16
+ const socket = net.connect(SOCKET, () => socket.end('PING'));
17
+ }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ❌ 6. Client that spawns children
4
+ // This tests your process group killing.
5
+ // If watchdog does not kill the entire group, the sleeps will remain after watchdog exits.
6
+
7
+ const {spawn} = require('child_process');
8
+
9
+ spawn('sleep', ['9999'], {detached: false});
10
+ spawn('sleep', ['9999'], {detached: false});
11
+
12
+ console.log('spawned 2 children');
13
+
14
+ setInterval(() => {}, 1000);
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+
3
+ const ExitCodeError = require('../errors/ExitCodeError');
4
+ const HeartbeatServer = require('../HeartbeatServer');
5
+ const Promise = require('bluebird');
6
+ const cli = require('../cli');
7
+ const cuid = require('@paralleldrive/cuid2');
8
+ const make = require('@vbarbarosh/type-helpers/src/make');
9
+ const now_human = require('../now_human');
10
+ const pgid_exists = require('../pgid_exists');
11
+ const pgid_kill_grace = require('../pgid_kill_grace');
12
+ const shell_spawn = require('../shell_spawn');
13
+
14
+ const LOGS_ROOT_UID = cuid.createId();
15
+
16
+ // 💎 Only the lack of a valid PING within WATCHDOG_INTERVAL is fatal
17
+
18
+ // Ctrl-C should gracefully terminate a child process and close heartbeat server
19
+ // Test
20
+ // - heartbeat server starts failed
21
+ // - heartbeat server does not close connection
22
+ // - heartbeat server sends 1G of data
23
+ // - child spawn failed
24
+ // - child receives no valid WATCHDOG_SOCKET
25
+ // - child does not send heartbeat
26
+ // - child spawns several children (watchdog → child → subchild → subsubchild)
27
+ // - watchdog has global timeout, child terminated earlier: watchdog should return immediately
28
+
29
+ cli(main, report);
30
+
31
+ function report(error)
32
+ {
33
+ log(`[heartbeat_end_error] ${error.message}`);
34
+ }
35
+
36
+ async function main()
37
+ {
38
+ const args = process.argv.slice(2);
39
+ if (!args.length) {
40
+ usage();
41
+ process.exit(1);
42
+ }
43
+
44
+ log('[heartbeat_begin]');
45
+
46
+ const WATCHDOG_INTERVAL = make(process.env.WATCHDOG_INTERVAL, {type: 'int', min: 1000, default: 5000});
47
+ const heartbeat_server = new HeartbeatServer(WATCHDOG_INTERVAL);
48
+ const WATCHDOG_SOCKET = heartbeat_server.socket_path;
49
+ heartbeat_server.on('heartbeat', () => log('[heartbeat_ping] ❤️'))
50
+ heartbeat_server.on('warning', error => log(`[heartbeat_warn] ⚠️ ${error}`))
51
+
52
+ try {
53
+ log(`[heartbeat_socket_path] ${heartbeat_server.socket_path}`);
54
+ const env = {...process.env, WATCHDOG_INTERVAL, WATCHDOG_SOCKET};
55
+ const proc = await shell_spawn(args, {stdio: 'inherit', env, detached: true}).init();
56
+ const pgid = proc.pid; // group id == leader pid
57
+
58
+ const signal = new Promise(function (resolve, reject) {
59
+ process.on('SIGTERM', function () {
60
+ log('[heartbeat_sigterm]');
61
+ reject(new ExitCodeError(143, 'SIGTERM'));
62
+ });
63
+ process.on('SIGINT', function () {
64
+ log('[heartbeat_sigint]');
65
+ reject(new ExitCodeError(130, 'SIGINT'));
66
+ });
67
+ });
68
+
69
+ try {
70
+ await Promise.race([signal, heartbeat_server.promise(), proc.promise()]);
71
+ // happy path
72
+ }
73
+ finally {
74
+ if (pgid_exists(pgid)) {
75
+ await pgid_kill_grace(pgid, {
76
+ log: s => log(`[heartbeat_pgid_kill_grace] ${s}`),
77
+ });
78
+ }
79
+ }
80
+ }
81
+ finally {
82
+ log('[heartbeat_dispose] Closing heartbeat server...');
83
+ await Promise.resolve(heartbeat_server.dispose()).timeout(1000);
84
+ }
85
+
86
+ log('[heartbeat_end_ok]');
87
+ process.exit(0);
88
+ }
89
+
90
+ function log(s)
91
+ {
92
+ const ss = JSON.stringify(s).slice(1, -1).replaceAll('\\"', '"');
93
+ if (ss[0] === '[') {
94
+ console.log(`[${LOGS_ROOT_UID}][${now_human()}]${ss}`);
95
+ }
96
+ else {
97
+ console.log(`[${LOGS_ROOT_UID}][${now_human()}] ${ss}`);
98
+ }
99
+ }
100
+
101
+ function usage()
102
+ {
103
+ process.stdout.write(`
104
+ usage: watchdog <cmd> [args...]
105
+
106
+ Runs <cmd> under supervision. Watchdog starts a heartbeat server,
107
+ spawns the child process, and expects the child to periodically send
108
+ PING messages to the provided UNIX socket.
109
+
110
+ Environment variables passed to the child:
111
+
112
+ WATCHDOG_SOCKET Path to the UNIX socket that accepts heartbeat
113
+ messages. The child must connect and send 'PING'
114
+ frames at regular intervals.
115
+
116
+ WATCHDOG_INTERVAL Heartbeat interval in milliseconds. The child must
117
+ send at least one PING within each interval. If no
118
+ PING is received before the timeout expires, the
119
+ child is considered unresponsive.
120
+
121
+ Behavior:
122
+
123
+ 1) Watchdog creates a heartbeat server bound to WATCHDOG_SOCKET.
124
+ 2) Watchdog spawns <cmd> with WATCHDOG_SOCKET and WATCHDOG_INTERVAL
125
+ injected into the child's environment.
126
+ 3) Watchdog waits for either:
127
+ - a heartbeat timeout (no PING received in time), or
128
+ - the child process exiting.
129
+ 4) If the heartbeat timeout expires, watchdog terminates the child
130
+ process (SIGTERM → grace period → SIGKILL).
131
+ 5) Watchdog shuts down the heartbeat server and exits with a
132
+ non-zero status on failure or with the child's exit code on
133
+ normal termination.
134
+
135
+ Exit Codes:
136
+
137
+ ------------------------+---------
138
+ child exit 0 | 0
139
+ child exit non-zero | child exit code
140
+ heartbeat timeout | 124
141
+ SIGINT | 130
142
+ SIGTERM | 143
143
+ watchdog internal error | 1
144
+ -----------------------------------
145
+ `.trim() + '\n');
146
+ }
package/src/cli.js CHANGED
@@ -1,7 +1,10 @@
1
+ const ExitCodeError = require('./errors/ExitCodeError');
2
+ const Promise = require('bluebird');
3
+
1
4
  /**
2
5
  * Entry point for Node CLI apps
3
6
  */
4
- function cli(main)
7
+ function cli(main, report = error => console.error(error))
5
8
  {
6
9
  // https://stackoverflow.com/a/46916601/1478566
7
10
  const timer = setInterval(v => v, 1E9);
@@ -13,8 +16,13 @@ function cli(main)
13
16
  }
14
17
  function reject(error) {
15
18
  clearInterval(timer);
16
- console.error(error);
17
- process.exit(1);
19
+ report(error);
20
+ if (error instanceof ExitCodeError) {
21
+ process.exit(error.exit_code);
22
+ }
23
+ else {
24
+ process.exit(1);
25
+ }
18
26
  }
19
27
  }
20
28
 
@@ -0,0 +1,9 @@
1
+ class ExitCodeError extends Error
2
+ {
3
+ constructor(exit_code, message = '') {
4
+ super(message);
5
+ this.exit_code = exit_code;
6
+ }
7
+ }
8
+
9
+ module.exports = ExitCodeError;
@@ -0,0 +1,11 @@
1
+ const fs_fi = require('./fs_fi');
2
+
3
+ async function fs_assert_file_socket(file)
4
+ {
5
+ const fi = await fs_fi(file);
6
+ if (!fi.isSocket()) {
7
+ throw new Error(`Not a socket: ${file}`);
8
+ }
9
+ }
10
+
11
+ module.exports = fs_assert_file_socket;
@@ -1,12 +1,12 @@
1
1
  const axios = require('axios');
2
- const make_int = require('@vbarbarosh/type-helpers/src/make_int');
2
+ const safe_int = require('@vbarbarosh/type-helpers/src/safe_int');
3
3
 
4
4
  async function http_get_stream(url, options)
5
5
  {
6
6
  const res = await axios.get(url, {responseType: 'stream', ...options});
7
7
  const out = res.data;
8
8
  out.headers = res.headers;
9
- out.total = make_int(res.headers['content-length'], null, 0);
9
+ out.total = safe_int(res.headers['content-length'], null, 0);
10
10
  return out;
11
11
  }
12
12
 
@@ -0,0 +1,10 @@
1
+ // https://stackoverflow.com/a/39764792/1478566
2
+ function math_gcd(a, b)
3
+ {
4
+ if (b) {
5
+ return math_gcd(b, a % b);
6
+ }
7
+ return a;
8
+ }
9
+
10
+ module.exports = math_gcd;
@@ -0,0 +1,2 @@
1
+ Returns the greatest common divisor (GCD) of two integers a and b
2
+ using the Euclidean algorithm.
@@ -0,0 +1,8 @@
1
+ const math_gcd = require('./math_gcd');
2
+
3
+ function math_lcm(a, b)
4
+ {
5
+ return Math.abs(a*b) / math_gcd(a, b);
6
+ }
7
+
8
+ module.exports = math_lcm;
@@ -0,0 +1,2 @@
1
+ Returns the least common multiple (LCM) of two integers a and b
2
+ using the relationship `LCM(a, b) = |a * b| / GCD(a, b)`.
@@ -0,0 +1,43 @@
1
+ function parse_hms(expr)
2
+ {
3
+ if (!expr && expr !== 0) {
4
+ return 0;
5
+ }
6
+
7
+ expr = String(expr).trim();
8
+ if (!expr) {
9
+ return 0;
10
+ }
11
+
12
+ const parts = expr.split(':');
13
+
14
+ let h = 0, m = 0, s = 0;
15
+
16
+ if (parts.length === 1) {
17
+ // "SS(.fff)"
18
+ s = parseFloat(parts[0]);
19
+ }
20
+ else if (parts.length === 2) {
21
+ // "MM:SS(.fff)"
22
+ m = parseFloat(parts[0]);
23
+ s = parseFloat(parts[1]);
24
+ }
25
+ else if (parts.length === 3) {
26
+ // "HH:MM:SS(.fff)"
27
+ h = parseFloat(parts[0]);
28
+ m = parseFloat(parts[1]);
29
+ s = parseFloat(parts[2]);
30
+ }
31
+ else {
32
+ return Number.NaN;
33
+ }
34
+
35
+ // Any failed numeric parse ⇒ NaN
36
+ if ([h, m, s].some(n => Number.isNaN(n))) {
37
+ return Number.NaN;
38
+ }
39
+
40
+ return h*3600 + m*60 + s;
41
+ }
42
+
43
+ module.exports = parse_hms;
@@ -0,0 +1,9 @@
1
+ Parses a time expression into seconds.
2
+
3
+ **Supported formats:**
4
+ - `SS(.fff)` — seconds
5
+ - `MM:SS(.fff)` — minutes and seconds
6
+ - `HH:MM:SS(.fff)` — hours, minutes, and seconds
7
+
8
+ **Returns:**
9
+ A number representing total seconds, or `NaN` if the input is invalid.
@@ -0,0 +1,32 @@
1
+ const assert = require('assert');
2
+ const parse_hms = require('./parse_hms');
3
+
4
+ const items = [
5
+ ['', 0],
6
+ [null, 0],
7
+ [undefined, 0],
8
+ ['0', 0],
9
+ ['0:5', 5],
10
+ ['00:05', 5],
11
+ ['0:15', 15],
12
+ ['0:15.123', 15.123],
13
+ ['0:75.123', 75.123],
14
+ [' 5', 5],
15
+ [' 0:05 ', 5],
16
+ [100, 100],
17
+ ['1:2:3:4', NaN, 'Too many segments'],
18
+ ['abc', NaN, 'Non-numeric'],
19
+ ['0:ab', NaN, 'Non-numeric'],
20
+ ['1:x:3', NaN, 'Non-numeric'],
21
+ ];
22
+
23
+ describe('parse_hms', function () {
24
+ items.forEach(function ([input, expected, description]) {
25
+ const title = description
26
+ ? `${input} → ${expected} | ${description}`
27
+ : `${input} → ${expected}`;
28
+ it(title, function () {
29
+ assert.strictEqual(expected, parse_hms(input));
30
+ });
31
+ });
32
+ });
@@ -0,0 +1,14 @@
1
+ // group id == leader pid
2
+ // keep in sync with pid_exists
3
+ function pgid_exists(pgid)
4
+ {
5
+ try {
6
+ process.kill(-pgid, 0);
7
+ return true;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ module.exports = pgid_exists;
@@ -0,0 +1,55 @@
1
+ const Promise = require('bluebird');
2
+ const format_thousands = require('./format_thousands');
3
+ const ignore = require('./ignore');
4
+ const pgid_exists = require('./pgid_exists');
5
+
6
+ // group id == leader pid
7
+ // keep in sync with pid_kill_grace
8
+ async function pgid_kill_grace(pgid, {grace_timeout_ms = 5000, log = ignore} = {})
9
+ {
10
+ log(`Terminating process group ${pgid}: sending SIGTERM`);
11
+ try {
12
+ process.kill(-pgid, 'SIGTERM');
13
+ }
14
+ catch (error) {
15
+ throw new Error(`Failed to send SIGTERM to process group ${pgid}: ${error.message}`);
16
+ }
17
+
18
+ const end = Date.now() + grace_timeout_ms;
19
+ while (true) {
20
+ if (!pgid_exists(pgid)) {
21
+ return;
22
+ }
23
+ if (Date.now() >= end) {
24
+ break;
25
+ }
26
+ const remain = end - Date.now();
27
+ log(`Process group ${pgid} still alive; grace time remaining: ${format_thousands(remain)}ms`);
28
+ await Promise.delay(Math.max(0, Math.min(100, remain)));
29
+ }
30
+
31
+ try {
32
+ log(`SIGTERM grace period expired for group ${pgid}; sending SIGKILL`);
33
+ process.kill(-pgid, 'SIGKILL');
34
+ }
35
+ catch (error) {
36
+ if (error.code === 'ESRCH' && error.syscall === 'kill') {
37
+ log(`Process group ${pgid} already terminated`);
38
+ return;
39
+ }
40
+ throw error;
41
+ }
42
+
43
+ // Wait for kernel to deliver SIGKILL
44
+ const final_deadline = Date.now() + 200;
45
+ while (Date.now() < final_deadline) {
46
+ if (!pgid_exists(pgid)) {
47
+ return;
48
+ }
49
+ await Promise.delay(10);
50
+ }
51
+
52
+ throw new Error(`Process group ${pgid} survived SIGKILL`);
53
+ }
54
+
55
+ module.exports = pgid_kill_grace;
package/src/pid_exists.js CHANGED
@@ -1,3 +1,4 @@
1
+ // keep in sync with pgid_exists
1
2
  function pid_exists(pid)
2
3
  {
3
4
  try {