@vbarbarosh/node-helpers 3.71.0 → 3.72.1
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 +9 -3
- 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 +161 -0
- package/src/cli-apps/watchdog.test.js +7 -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/shell_spawn.test.js +23 -0
- package/src/strtr.js +15 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
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(10);
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/pid_kill_grace.js
CHANGED
|
@@ -3,26 +3,52 @@ const format_thousands = require('./format_thousands');
|
|
|
3
3
|
const ignore = require('./ignore');
|
|
4
4
|
const pid_exists = require('./pid_exists');
|
|
5
5
|
|
|
6
|
+
// keep in sync with pgid_kill_grace
|
|
6
7
|
async function pid_kill_grace(pid, {grace_timeout_ms = 5000, log = ignore} = {})
|
|
7
8
|
{
|
|
8
|
-
log(
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
log(`Terminating process ${pid}: sending SIGTERM`);
|
|
10
|
+
try {
|
|
11
|
+
process.kill(pid, 'SIGTERM');
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
throw new Error(`Failed to send SIGTERM to process ${pid}: ${error.message}`);
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
const end = Date.now() + grace_timeout_ms;
|
|
14
|
-
while (
|
|
18
|
+
while (true) {
|
|
15
19
|
if (!pid_exists(pid)) {
|
|
16
20
|
return;
|
|
17
21
|
}
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
if (Date.now() >= end) {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
const remain = end - Date.now();
|
|
26
|
+
log(`Process ${pid} still alive; grace time remaining: ${format_thousands(remain)}ms`);
|
|
27
|
+
await Promise.delay(Math.max(0, Math.min(100, remain)));
|
|
20
28
|
}
|
|
21
29
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
try {
|
|
31
|
+
log(`SIGTERM grace period expired for process ${pid}; sending SIGKILL`);
|
|
32
|
+
process.kill(pid, 'SIGKILL');
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error.code === 'ESRCH' && error.syscall === 'kill') {
|
|
36
|
+
log(`Process ${pid} already terminated`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
25
40
|
}
|
|
41
|
+
|
|
42
|
+
// Give kernel a moment to deliver SIGKILL
|
|
43
|
+
const final_deadline = Date.now() + 200;
|
|
44
|
+
while (Date.now() < final_deadline) {
|
|
45
|
+
if (!pid_exists(pid)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
await Promise.delay(10);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new Error(`Process ${pid} survived SIGKILL`);
|
|
26
52
|
}
|
|
27
53
|
|
|
28
54
|
module.exports = pid_kill_grace;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const pid_exists = require('./pid_exists');
|
|
3
|
+
const pid_kill_grace = require('./pid_kill_grace');
|
|
4
|
+
const shell_spawn = require('./shell_spawn');
|
|
5
|
+
const stream = require('stream');
|
|
6
|
+
const stream_each = require('./stream_each');
|
|
7
|
+
const stream_lines = require('./stream_lines');
|
|
8
|
+
const wait_while = require('./wait_while');
|
|
9
|
+
|
|
10
|
+
describe('pid_kill_grace', function() {
|
|
11
|
+
|
|
12
|
+
it('happy path', async function () {
|
|
13
|
+
const proc = await shell_spawn(['sleep', '1m']).init();
|
|
14
|
+
assert(pid_exists(proc.pid));
|
|
15
|
+
await pid_kill_grace(proc.pid);
|
|
16
|
+
assert(!pid_exists(proc.pid));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// handling errors and edge cases
|
|
20
|
+
|
|
21
|
+
it('should throw "Failed to send SIGTERM to process 123"', async function () {
|
|
22
|
+
const pid = 999999; // 🎲 unlikely to exist
|
|
23
|
+
assert(!pid_exists(pid));
|
|
24
|
+
await assert.rejects(pid_kill_grace(pid), /Failed to send SIGTERM to process \d+:/);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it.skip('should throw "Process 123 survived SIGKILL"', async function () {
|
|
28
|
+
// How to implement this behavior?
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should KILL process after grace period', async function () {
|
|
32
|
+
const logs = [];
|
|
33
|
+
const stdout = [];
|
|
34
|
+
const proc = shell_spawn([`${__dirname}/pid_kill_grace.d/ignore-sigterm.js`]);
|
|
35
|
+
const r = stream.promises.pipeline(proc.stdout, stream_lines(), stream_each(v => stdout.push(v)));
|
|
36
|
+
await wait_while(() => !stdout.length);
|
|
37
|
+
await Promise.all([
|
|
38
|
+
r,
|
|
39
|
+
pid_kill_grace(proc.pid, {grace_timeout_ms: 50, log: s => logs.push(s)})
|
|
40
|
+
]);
|
|
41
|
+
assert(!pid_exists(proc.pid));
|
|
42
|
+
assert(stdout.some(v => v.includes('SIGTERM_ignoring')));
|
|
43
|
+
assert(logs.some(v => v.match(/Terminating process \d+: sending SIGTERM/)));
|
|
44
|
+
assert(logs.some(v => v.match(/SIGTERM grace period expired for process \d+; sending SIGKILL/)));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('Child ignores SIGTERM for 10ms, but dies naturally before the grace loop ends', async function () {
|
|
48
|
+
const logs = [];
|
|
49
|
+
const stdout = [];
|
|
50
|
+
const proc = shell_spawn([`${__dirname}/pid_kill_grace.d/ignore-sigterm-for-10ms.js`]);
|
|
51
|
+
const r = stream.promises.pipeline(proc.stdout, stream_lines(), stream_each(v => stdout.push(v)));
|
|
52
|
+
await wait_while(() => !stdout.length);
|
|
53
|
+
await Promise.all([
|
|
54
|
+
r,
|
|
55
|
+
pid_kill_grace(proc.pid, {grace_timeout_ms: 50, log: s => logs.push(s)})
|
|
56
|
+
]);
|
|
57
|
+
assert(!pid_exists(proc.pid));
|
|
58
|
+
assert(stdout.some(v => v.includes('SIGTERM_ignoring')));
|
|
59
|
+
assert(stdout.some(v => v.includes('TERMINATE_AFTER_10MS')));
|
|
60
|
+
assert(logs.some(v => v.match(/Terminating process \d+: sending SIGTERM/)));
|
|
61
|
+
assert(!logs.some(v => v.match(/SIGTERM grace period expired for process \d+; sending SIGKILL/)));
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/ping_socket.js
CHANGED
|
@@ -11,19 +11,19 @@ function ping_socket(socket_path, data = 'PING', timeout_ms = 1000)
|
|
|
11
11
|
return new Promise(function (resolve, reject) {
|
|
12
12
|
let done = false;
|
|
13
13
|
const buf = [];
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
const client = net.connect(socket_path);
|
|
15
|
+
client.setTimeout(timeout_ms, () => finish(new Error('Socket Timeout')));
|
|
16
|
+
client.on('end', () => finish(null, Buffer.concat(buf)))
|
|
17
|
+
client.on('error', finish);
|
|
18
|
+
client.on('connect', () => client.end(data));
|
|
19
|
+
client.on('data', chunk => buf.push(chunk));
|
|
20
20
|
function finish(error, out) {
|
|
21
21
|
if (done) {
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
done = true;
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
client.removeAllListeners();
|
|
26
|
+
client.destroy();
|
|
27
27
|
error ? reject(error) : resolve(out);
|
|
28
28
|
}
|
|
29
29
|
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const Promise = require('bluebird');
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const fs_rmf = require('./fs_rmf');
|
|
4
|
+
const net = require('net');
|
|
5
|
+
const ping_socket = require('./ping_socket');
|
|
6
|
+
const random_hex = require('./random_hex');
|
|
7
|
+
|
|
8
|
+
const socket = '/tmp/ping_socket_test.sock';
|
|
9
|
+
|
|
10
|
+
describe('ping_socket', function () {
|
|
11
|
+
|
|
12
|
+
afterEach(async function () {
|
|
13
|
+
await fs_rmf(socket);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should receive exact string from server', async function () {
|
|
17
|
+
const uid = random_hex();
|
|
18
|
+
const server = net.createServer(client => client.resume().end(uid));
|
|
19
|
+
try {
|
|
20
|
+
await new Promise(resolve => server.listen(socket, resolve));
|
|
21
|
+
const buf = await ping_socket(socket);
|
|
22
|
+
assert.strictEqual(buf.toString(), uid);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
await server_close(server);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should throw Socket Timeout', async function () {
|
|
30
|
+
const clients = [];
|
|
31
|
+
const server = net.createServer(client => clients.push(client));
|
|
32
|
+
try {
|
|
33
|
+
await new Promise(resolve => server.listen(socket, resolve));
|
|
34
|
+
await assert.rejects(ping_socket(socket, 'PING', 100), {message: 'Socket Timeout'});
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
clients.forEach(v => v.resume().end());
|
|
38
|
+
await server_close(server);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should throw ENOENT', async function () {
|
|
43
|
+
await assert.rejects(ping_socket(socket), {
|
|
44
|
+
code: 'ENOENT',
|
|
45
|
+
syscall: 'connect',
|
|
46
|
+
address: socket,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
async function server_close(server)
|
|
52
|
+
{
|
|
53
|
+
return new Promise(function (resolve, reject) {
|
|
54
|
+
server.close(function (error) {
|
|
55
|
+
error ? reject(error) : resolve();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
package/src/range.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const na = Symbol();
|
|
2
|
+
|
|
3
|
+
function range(begin, end = na, step = 1, limit = 1E6)
|
|
4
|
+
{
|
|
5
|
+
if (end === na) {
|
|
6
|
+
if (begin < 0) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
return range_forward(0, begin, 1, limit);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (begin < end) {
|
|
13
|
+
return range_forward(begin, end, step, limit);
|
|
14
|
+
}
|
|
15
|
+
return range_backward(begin, end, step, limit);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function range_forward(begin, end, step, limit)
|
|
19
|
+
{
|
|
20
|
+
if (step < 0) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const out = [];
|
|
25
|
+
for (let value = begin; value < end && out.length < limit; value += step) {
|
|
26
|
+
out.push(value);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function range_backward(begin, end, step, limit)
|
|
32
|
+
{
|
|
33
|
+
if (step > 0) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const out = [];
|
|
38
|
+
for (let value = begin; value > end && out.length < limit; value += step) {
|
|
39
|
+
out.push(value);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = range;
|
package/src/range.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Generates a numeric sequence.
|
|
2
|
+
|
|
3
|
+
**Supported forms:**
|
|
4
|
+
- `range(stop)` — from `0` up to `stop` (exclusive)
|
|
5
|
+
- `range(start, stop)` — from `start` up to `stop` (exclusive)
|
|
6
|
+
- `range(start, stop, step)` — with custom step
|
|
7
|
+
|
|
8
|
+
**Rules:**
|
|
9
|
+
- The `stop` value is **exclusive**
|
|
10
|
+
- Positive `step` counts **up** while `value < stop`
|
|
11
|
+
- Negative `step` counts **down** while `value > stop`
|
|
12
|
+
- If `step` direction does not allow progress, the result is an empty array
|
|
13
|
+
|
|
14
|
+
**Returns:**
|
|
15
|
+
An array of numbers.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const range = require('./range');
|
|
3
|
+
|
|
4
|
+
const cases = [
|
|
5
|
+
// 1-argument: (stop)
|
|
6
|
+
{args: [10], expected: [0,1,2,3,4,5,6,7,8,9]},
|
|
7
|
+
{args: [-10], expected: []}, // 💎 Consider non-standard semantics: range(-5) -> [0,-1,-2,-3,-4]
|
|
8
|
+
|
|
9
|
+
// 2-arguments: (start, stop)
|
|
10
|
+
{args: [3, 7], expected: [3,4,5,6]},
|
|
11
|
+
{args: [0, 0], expected: []},
|
|
12
|
+
{args: [5, 3], expected: []}, // positive direction, start > stop → empty
|
|
13
|
+
|
|
14
|
+
// 3-arguments: (start, stop, step)
|
|
15
|
+
{args: [3, 10, 2], expected: [3,5,7,9]},
|
|
16
|
+
{args: [3, 10, 3], expected: [3,6,9]},
|
|
17
|
+
|
|
18
|
+
// Negative step
|
|
19
|
+
{args: [10, 3, -2], expected: [10,8,6,4]},
|
|
20
|
+
{args: [10, 3, -3], expected: [10,7,4]},
|
|
21
|
+
|
|
22
|
+
// Negative step but no progression
|
|
23
|
+
{args: [3, 10, -1], expected: []}, // step direction does not match range
|
|
24
|
+
|
|
25
|
+
// Step = 1 edge
|
|
26
|
+
{args: [5, 5, 1], expected: []},
|
|
27
|
+
{args: [5, 6, 1], expected: [5]},
|
|
28
|
+
|
|
29
|
+
// Step = -1 edge
|
|
30
|
+
{args: [5, 5, -1], expected: []},
|
|
31
|
+
{args: [6, 5, -1], expected: [6]},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
describe('range', function () {
|
|
35
|
+
cases.forEach(function (item, i) {
|
|
36
|
+
it(`range #${i+1}: ${JSON.stringify(item)}`, function () {
|
|
37
|
+
const actual = range(...item.args);
|
|
38
|
+
assert.deepStrictEqual(actual, item.expected);
|
|
39
|
+
})
|
|
40
|
+
});
|
|
41
|
+
});
|
package/src/shell_spawn.js
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
|
+
const ExitCodeError = require('./errors/ExitCodeError');
|
|
1
2
|
const Promise = require('bluebird');
|
|
2
3
|
const child_process = require('child_process');
|
|
3
4
|
|
|
5
|
+
// 📕 Notes by ChatGPT:
|
|
6
|
+
// If the child is killed by a signal (e.g. SIGKILL from outside),
|
|
7
|
+
// Node’s exit event gives code = null, signal = 'SIGKILL'.
|
|
8
|
+
// Your code treats this as success (resolve).
|
|
9
|
+
// That might be fine for your use case, but be aware:
|
|
10
|
+
//
|
|
11
|
+
// If you want “child died by signal” to be treated as non-zero,
|
|
12
|
+
// you’d need to change this check to something like:
|
|
13
|
+
//
|
|
14
|
+
// > function promise_exit(code, signal) {
|
|
15
|
+
// > out.off('error', promise_error);
|
|
16
|
+
// > if (code === 0) {
|
|
17
|
+
// > resolve(signal);
|
|
18
|
+
// > } else {
|
|
19
|
+
// > reject(new ExitCodeError(code || 128, `Process terminated with code ${code} and signal ${signal}`));
|
|
20
|
+
// > }
|
|
21
|
+
// > }
|
|
22
|
+
|
|
23
|
+
// ⚠️ Both .init and .promise are targets for race conditions!
|
|
4
24
|
function shell_spawn(args, options)
|
|
5
25
|
{
|
|
6
26
|
let init, promise;
|
|
@@ -29,7 +49,12 @@ function shell_spawn(args, options)
|
|
|
29
49
|
}
|
|
30
50
|
function promise_exit(code, signal) {
|
|
31
51
|
out.off('error', promise_error);
|
|
32
|
-
|
|
52
|
+
if (code === 0) {
|
|
53
|
+
resolve();
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
reject(new ExitCodeError(code || 128, `Process terminated with code ${code} and signal ${signal}`));
|
|
57
|
+
}
|
|
33
58
|
}
|
|
34
59
|
});
|
|
35
60
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const Promise = require('bluebird');
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const pid_exists = require('./pid_exists');
|
|
4
|
+
const shell_spawn = require('./shell_spawn');
|
|
5
|
+
|
|
6
|
+
describe.skip('shell_spawn', function () {
|
|
7
|
+
describe('Happy Path', function () {
|
|
8
|
+
it('basic', async function () {
|
|
9
|
+
await shell_spawn(['true']).promise().timeout(100);
|
|
10
|
+
});
|
|
11
|
+
it('init + promise', async function () {
|
|
12
|
+
const proc = await shell_spawn(['true']).init();
|
|
13
|
+
await proc.promise().timeout(100);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
it('🏎️ race condition: proc.promise is called after process was terminated', async function () {
|
|
17
|
+
const proc = shell_spawn(['true']);
|
|
18
|
+
assert(pid_exists(proc.pid));
|
|
19
|
+
await Promise.delay(10);
|
|
20
|
+
assert(!pid_exists(proc.pid));
|
|
21
|
+
await proc.promise().timeout(100);
|
|
22
|
+
});
|
|
23
|
+
});
|
package/src/strtr.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// PHP’s strtr tries the longest possible key first when multiple keys overlap.
|
|
2
|
+
// echo strtr("hello", ["he" => "X", "hell" => "Y"]);
|
|
3
|
+
function strtr(subject, map)
|
|
4
|
+
{
|
|
5
|
+
let out = subject;
|
|
6
|
+
Object.entries(map).sort(fcmp).forEach(function ([search, replace]) {
|
|
7
|
+
out = out.replaceAll(search, replace);
|
|
8
|
+
});
|
|
9
|
+
return out;
|
|
10
|
+
function fcmp(b, a) {
|
|
11
|
+
return a[0].length - b[0].length;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = strtr;
|