@vbarbarosh/node-helpers 3.71.0 → 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.
- package/README.md +1 -1
- package/dist/browser.js +289 -13
- package/package.json +8 -2
- package/src/HeartbeatServer.js +38 -10
- package/src/HeartbeatServer.test.js +31 -0
- package/src/array_gcd.js +3 -10
- package/src/array_gcd.md +2 -0
- package/src/array_lcm.js +8 -0
- package/src/array_lcm.md +2 -0
- package/src/array_lcm.test.js +12 -0
- package/src/array_max.md +2 -1
- package/src/array_min.md +2 -1
- package/src/cli-apps/watchdog.d/connect_close.js +17 -0
- package/src/cli-apps/watchdog.d/connect_close2.js +20 -0
- package/src/cli-apps/watchdog.d/connect_close3.js +25 -0
- package/src/cli-apps/watchdog.d/connect_no_ping.js +13 -0
- package/src/cli-apps/watchdog.d/early_exit.js +7 -0
- package/src/cli-apps/watchdog.d/flood.js +13 -0
- package/src/cli-apps/watchdog.d/flood2.js +15 -0
- package/src/cli-apps/watchdog.d/hang.js +8 -0
- package/src/cli-apps/watchdog.d/huge_delay.js +18 -0
- package/src/cli-apps/watchdog.d/ignore_sigterm.js +29 -0
- package/src/cli-apps/watchdog.d/late_heartbeat.js +21 -0
- package/src/cli-apps/watchdog.d/my-exit-after-2-pings.js +16 -0
- package/src/cli-apps/watchdog.d/my-happy-path.js +16 -0
- package/src/cli-apps/watchdog.d/my-ignore-sigterm.js +22 -0
- package/src/cli-apps/watchdog.d/my-never-ping.js +15 -0
- package/src/cli-apps/watchdog.d/no_heartbeat.js +5 -0
- package/src/cli-apps/watchdog.d/ok.js +17 -0
- package/src/cli-apps/watchdog.d/spawn_children.js +14 -0
- package/src/cli-apps/watchdog.js +146 -0
- package/src/cli.js +11 -3
- package/src/errors/ExitCodeError.js +9 -0
- package/src/fs_assert_file_socket.js +11 -0
- package/src/math_gcd.js +10 -0
- package/src/math_gcd.md +2 -0
- package/src/math_lcm.js +8 -0
- package/src/math_lcm.md +2 -0
- package/src/parse_hms.js +43 -0
- package/src/parse_hms.md +9 -0
- package/src/parse_hms.test.js +32 -0
- package/src/pgid_exists.js +14 -0
- package/src/pgid_kill_grace.js +55 -0
- package/src/pid_exists.js +1 -0
- package/src/pid_kill_grace.d/ignore-sigterm-for-10ms.js +33 -0
- package/src/pid_kill_grace.d/ignore-sigterm.js +22 -0
- package/src/pid_kill_grace.js +35 -9
- package/src/pid_kill_grace.test.js +63 -0
- package/src/ping_socket.js +8 -8
- package/src/ping_socket.test.js +58 -0
- package/src/range.js +44 -0
- package/src/range.md +15 -0
- package/src/range.test.js +41 -0
- package/src/shell_spawn.js +26 -1
- 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,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,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,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
|
-
|
|
17
|
-
|
|
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
|
|
package/src/math_gcd.js
ADDED
package/src/math_gcd.md
ADDED
package/src/math_lcm.js
ADDED
package/src/math_lcm.md
ADDED
package/src/parse_hms.js
ADDED
|
@@ -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;
|
package/src/parse_hms.md
ADDED
|
@@ -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,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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const Promise = require('bluebird');
|
|
4
|
+
const cli = require('../cli');
|
|
5
|
+
const now_human = require('../now_human');
|
|
6
|
+
|
|
7
|
+
// Child ignores SIGTERM for 10 ms, but dies naturally before the grace loop ends
|
|
8
|
+
|
|
9
|
+
cli(main);
|
|
10
|
+
|
|
11
|
+
async function main()
|
|
12
|
+
{
|
|
13
|
+
let timer;
|
|
14
|
+
|
|
15
|
+
process.on('SIGTERM', function () {
|
|
16
|
+
console.log(`[${now_human()}][ignore-sigterm-for-10ms] SIGTERM_ignoring...`);
|
|
17
|
+
timer ??= setTimeout(terminate, 10);
|
|
18
|
+
});
|
|
19
|
+
process.on('SIGINT', function () {
|
|
20
|
+
console.log(`[${now_human()}][ignore-sigterm-for-10ms] SIGINT_ignoring...`);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
for (let iter = 1, last = 100; iter <= last; ++iter) {
|
|
24
|
+
console.log(`[${now_human()}][ignore-sigterm-for-10ms] ${iter} of ${last}`);
|
|
25
|
+
await Promise.delay(10);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function terminate()
|
|
30
|
+
{
|
|
31
|
+
console.log(`[${now_human()}][ignore-sigterm-for-10ms] TERMINATE_AFTER_10MS`);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|