@vbarbarosh/node-helpers 3.72.3 → 3.72.5

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 CHANGED
@@ -20,7 +20,7 @@ npm install @vbarbarosh/node-helpers
20
20
  A subset of `@vbarbarosh/node-helpers` is available for browser:
21
21
 
22
22
  ```html
23
- <script src="https://unpkg.com/@vbarbarosh/node-helpers@3.72.3/dist/browser.js"></script>
23
+ <script src="https://unpkg.com/@vbarbarosh/node-helpers@3.72.5/dist/browser.js"></script>
24
24
  ```
25
25
 
26
26
  ⚠️ All browser functions are exposed globally.
package/dist/browser.js CHANGED
@@ -72,10 +72,10 @@ function serializer(replacer, cycleReplacer) {
72
72
 
73
73
  /***/ }),
74
74
 
75
- /***/ "./src sync recursive \\b(array%7Cdate%7Chttp_delete%7Chttp_get_blob%7Chttp_get_buffer%7Chttp_get_json%7Chttp_get_utf8%7Chttp_head%7Chttp_patch_json%7Chttp_post_json%7Chttp_post_multipart%7Chttp_put_buffer%7Chttp_put_json%7Chttp_put_utf8%7Cidentity%7Cignore%7Cfcmp%7Cfilter%7Cformat%7Cplural%7Crandom_int%7Curlmod%7Cwaitcb)[^/]*(?<%21\\.test)\\.js$":
76
- /*!*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
77
- !*** ./src/ sync \b(array%7Cdate%7Chttp_delete%7Chttp_get_blob%7Chttp_get_buffer%7Chttp_get_json%7Chttp_get_utf8%7Chttp_head%7Chttp_patch_json%7Chttp_post_json%7Chttp_post_multipart%7Chttp_put_buffer%7Chttp_put_json%7Chttp_put_utf8%7Cidentity%7Cignore%7Cfcmp%7Cfilter%7Cformat%7Cplural%7Crandom_int%7Curlmod%7Cwaitcb)[^/]*(?<%21\.test)\.js$ ***!
78
- \*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
75
+ /***/ "./src sync recursive ^(?%21.*(cli-apps%7Cbrowser%7C\\.d)\\/).*\\b(array%7Cdate%7Chttp_delete%7Chttp_get_blob%7Chttp_get_buffer%7Chttp_get_json%7Chttp_get_utf8%7Chttp_head%7Chttp_patch_json%7Chttp_post_json%7Chttp_post_multipart%7Chttp_put_buffer%7Chttp_put_json%7Chttp_put_utf8%7Cidentity%7Cignore%7Cfcmp%7Cfilter%7Cformat%7Cplural%7Crandom_int%7Curlmod%7Cwaitcb)[^/]*(?<%21\\.test)\\.js$":
76
+ /*!**********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
77
+ !*** ./src/ sync ^(?%21.*(cli-apps%7Cbrowser%7C\.d)\/).*\b(array%7Cdate%7Chttp_delete%7Chttp_get_blob%7Chttp_get_buffer%7Chttp_get_json%7Chttp_get_utf8%7Chttp_head%7Chttp_patch_json%7Chttp_post_json%7Chttp_post_multipart%7Chttp_put_buffer%7Chttp_put_json%7Chttp_put_utf8%7Cidentity%7Cignore%7Cfcmp%7Cfilter%7Cformat%7Cplural%7Crandom_int%7Curlmod%7Cwaitcb)[^/]*(?<%21\.test)\.js$ ***!
78
+ \**********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
79
79
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
80
80
 
81
81
  var map = {
@@ -94,8 +94,6 @@ var map = {
94
94
  "./array_sum.js": "./src/array_sum.js",
95
95
  "./array_unique.js": "./src/array_unique.js",
96
96
  "./array_unique_last.js": "./src/array_unique_last.js",
97
- "./cli-apps/watchdog.d/ignore_sigterm.js": "./src/cli-apps/watchdog.d/ignore_sigterm.js",
98
- "./cli-apps/watchdog.d/my-ignore-sigterm.js": "./src/cli-apps/watchdog.d/my-ignore-sigterm.js",
99
97
  "./date_add_hours.js": "./src/date_add_hours.js",
100
98
  "./date_add_milliseconds.js": "./src/date_add_milliseconds.js",
101
99
  "./date_add_minutes.js": "./src/date_add_minutes.js",
@@ -146,8 +144,6 @@ var map = {
146
144
  "./http_put_utf8.js": "./src/http_put_utf8.js",
147
145
  "./identity.js": "./src/identity.js",
148
146
  "./ignore.js": "./src/ignore.js",
149
- "./pid_kill_grace.d/ignore-sigterm-for-10ms.js": "./src/pid_kill_grace.d/ignore-sigterm-for-10ms.js",
150
- "./pid_kill_grace.d/ignore-sigterm.js": "./src/pid_kill_grace.d/ignore-sigterm.js",
151
147
  "./plural.js": "./src/plural.js",
152
148
  "./random_int.js": "./src/random_int.js",
153
149
  "./urlmod.js": "./src/urlmod.js",
@@ -172,7 +168,7 @@ webpackContext.keys = function webpackContextKeys() {
172
168
  };
173
169
  webpackContext.resolve = webpackContextResolve;
174
170
  module.exports = webpackContext;
175
- webpackContext.id = "./src sync recursive \\b(array%7Cdate%7Chttp_delete%7Chttp_get_blob%7Chttp_get_buffer%7Chttp_get_json%7Chttp_get_utf8%7Chttp_head%7Chttp_patch_json%7Chttp_post_json%7Chttp_post_multipart%7Chttp_put_buffer%7Chttp_put_json%7Chttp_put_utf8%7Cidentity%7Cignore%7Cfcmp%7Cfilter%7Cformat%7Cplural%7Crandom_int%7Curlmod%7Cwaitcb)[^/]*(?<%21\\.test)\\.js$";
171
+ webpackContext.id = "./src sync recursive ^(?%21.*(cli-apps%7Cbrowser%7C\\.d)\\/).*\\b(array%7Cdate%7Chttp_delete%7Chttp_get_blob%7Chttp_get_buffer%7Chttp_get_json%7Chttp_get_utf8%7Chttp_head%7Chttp_patch_json%7Chttp_post_json%7Chttp_post_multipart%7Chttp_put_buffer%7Chttp_put_json%7Chttp_put_utf8%7Cidentity%7Cignore%7Cfcmp%7Cfilter%7Cformat%7Cplural%7Crandom_int%7Curlmod%7Cwaitcb)[^/]*(?<%21\\.test)\\.js$";
176
172
 
177
173
  /***/ }),
178
174
 
@@ -648,116 +644,6 @@ function array_unique_last(array, fn = identity)
648
644
  module.exports = array_unique_last;
649
645
 
650
646
 
651
- /***/ }),
652
-
653
- /***/ "./src/cli-apps/watchdog.d/ignore_sigterm.js":
654
- /*!***************************************************!*\
655
- !*** ./src/cli-apps/watchdog.d/ignore_sigterm.js ***!
656
- \***************************************************/
657
- /***/ (() => {
658
-
659
- //#!/usr/bin/env node
660
-
661
- // 1️⃣ Client that ignores SIGTERM
662
- // Goal: process stays alive after SIGTERM and only dies on SIGKILL (exactly what pid_kill_grace should handle).
663
- //
664
- // What this tests for watchdog:
665
- // - watchdog sends SIGTERM → process prints “ignored” and keeps running.
666
- // - pid_kill_grace waits grace_timeout_ms, sees PID still alive, sends SIGKILL.
667
- // - Process must die after SIGKILL.
668
- // - Watchdog should log the whole sequence correctly.
669
- // This is a realistic scenario.
670
-
671
- console.log('PID:', process.pid);
672
-
673
- // Ignore SIGTERM
674
- process.on('SIGTERM', function () {
675
- console.log('SIGTERM received, but intentionally ignored');
676
- });
677
-
678
- // Still react to SIGINT (Ctrl+C) so you can stop it manually if needed
679
- process.on('SIGINT', function () {
680
- console.log('SIGINT received, exiting');
681
- process.exit(130);
682
- });
683
-
684
- // Keep process alive
685
- setInterval(() => {
686
- // simulate work
687
- }, 1000);
688
-
689
-
690
- /***/ }),
691
-
692
- /***/ "./src/cli-apps/watchdog.d/my-ignore-sigterm.js":
693
- /*!******************************************************!*\
694
- !*** ./src/cli-apps/watchdog.d/my-ignore-sigterm.js ***!
695
- \******************************************************/
696
- /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
697
-
698
- //#!/usr/bin/env node
699
-
700
- const Promise = __webpack_require__(/*! bluebird */ "bluebird");
701
- const cli = __webpack_require__(/*! @vbarbarosh/node-helpers/src/cli */ "./src/cli.js");
702
- const now_human = __webpack_require__(/*! @vbarbarosh/node-helpers/src/now_human */ "./src/now_human.js");
703
-
704
- cli(main);
705
-
706
- async function main()
707
- {
708
- process.on('SIGTERM', function () {
709
- console.log(`[${now_human()}][ignore-sigterm] SIGTERM, ignoring...`);
710
- });
711
- process.on('SIGINT', function () {
712
- console.log(`[${now_human()}][ignore-sigterm] SIGINT, ignoring...`);
713
- });
714
-
715
- for (let iter = 1, last = 100; iter <= last; ++iter) {
716
- console.log(`[${now_human()}][ignore-sigterm] ${iter} of ${last}`);
717
- await Promise.delay(100);
718
- }
719
- }
720
-
721
-
722
- /***/ }),
723
-
724
- /***/ "./src/cli.js":
725
- /*!********************!*\
726
- !*** ./src/cli.js ***!
727
- \********************/
728
- /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
729
-
730
- const ExitCodeError = __webpack_require__(/*! ./errors/ExitCodeError */ "./src/errors/ExitCodeError.js");
731
- const Promise = __webpack_require__(/*! bluebird */ "bluebird");
732
-
733
- /**
734
- * Entry point for Node CLI apps
735
- */
736
- function cli(main, report = error => console.error(error))
737
- {
738
- // https://stackoverflow.com/a/46916601/1478566
739
- const timer = setInterval(v => v, 1E9);
740
-
741
- Promise.resolve(main()).then(resolve, reject);
742
-
743
- function resolve() {
744
- clearInterval(timer);
745
- }
746
- function reject(error) {
747
- clearInterval(timer);
748
- report(error);
749
- if (error instanceof ExitCodeError) {
750
- process.exit(error.exit_code);
751
- }
752
- else {
753
- process.exit(1);
754
- }
755
- }
756
- }
757
-
758
- module.exports = cli;
759
-
760
-
761
647
  /***/ }),
762
648
 
763
649
  /***/ "./src/date_add_hours.js":
@@ -901,25 +787,6 @@ function date_is_leap_year(d)
901
787
  module.exports = date_is_leap_year;
902
788
 
903
789
 
904
- /***/ }),
905
-
906
- /***/ "./src/errors/ExitCodeError.js":
907
- /*!*************************************!*\
908
- !*** ./src/errors/ExitCodeError.js ***!
909
- \*************************************/
910
- /***/ ((module) => {
911
-
912
- class ExitCodeError extends Error
913
- {
914
- constructor(exit_code, message = '') {
915
- super(message);
916
- this.exit_code = exit_code;
917
- }
918
- }
919
-
920
- module.exports = ExitCodeError;
921
-
922
-
923
790
  /***/ }),
924
791
 
925
792
  /***/ "./src/factorize_ms.js":
@@ -1725,7 +1592,7 @@ const factorize_ms = __webpack_require__(/*! ./factorize_ms */ "./src/factorize_
1725
1592
  /**
1726
1593
  * Format milliseconds
1727
1594
  */
1728
- function format_ms3(ms)
1595
+ function format_ms3(ms, include_zero_hours = false)
1729
1596
  {
1730
1597
  if (!ms) {
1731
1598
  ms = 0;
@@ -1733,7 +1600,7 @@ function format_ms3(ms)
1733
1600
 
1734
1601
  // 00:05.445
1735
1602
  const [h, m, s, xx] = factorize_ms(ms);
1736
- if (h) {
1603
+ if (h || include_zero_hours) {
1737
1604
  return `0${h}:0${m}:0${s}.00${xx}`.replace(/0+(?=\d\d+[:.]|\d\d\d)/g, '');
1738
1605
  }
1739
1606
  return `0${m}:0${s}.00${xx}`.replace(/0+(?=\d\d+[:.]|\d\d\d)/g, '')
@@ -2219,99 +2086,6 @@ function math_lcm(a, b)
2219
2086
  module.exports = math_lcm;
2220
2087
 
2221
2088
 
2222
- /***/ }),
2223
-
2224
- /***/ "./src/now_human.js":
2225
- /*!**************************!*\
2226
- !*** ./src/now_human.js ***!
2227
- \**************************/
2228
- /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
2229
-
2230
- const format_date_human = __webpack_require__(/*! ./format_date_human */ "./src/format_date_human.js");
2231
-
2232
- function now_human()
2233
- {
2234
- return format_date_human(new Date());
2235
- }
2236
-
2237
- module.exports = now_human;
2238
-
2239
-
2240
- /***/ }),
2241
-
2242
- /***/ "./src/pid_kill_grace.d/ignore-sigterm-for-10ms.js":
2243
- /*!*********************************************************!*\
2244
- !*** ./src/pid_kill_grace.d/ignore-sigterm-for-10ms.js ***!
2245
- \*********************************************************/
2246
- /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
2247
-
2248
- //#!/usr/bin/env node
2249
-
2250
- const Promise = __webpack_require__(/*! bluebird */ "bluebird");
2251
- const cli = __webpack_require__(/*! ../cli */ "./src/cli.js");
2252
- const now_human = __webpack_require__(/*! ../now_human */ "./src/now_human.js");
2253
-
2254
- // Child ignores SIGTERM for 10 ms, but dies naturally before the grace loop ends
2255
-
2256
- cli(main);
2257
-
2258
- async function main()
2259
- {
2260
- let timer;
2261
-
2262
- process.on('SIGTERM', function () {
2263
- console.log(`[${now_human()}][ignore-sigterm-for-10ms] SIGTERM_ignoring...`);
2264
- timer ??= setTimeout(terminate, 10);
2265
- });
2266
- process.on('SIGINT', function () {
2267
- console.log(`[${now_human()}][ignore-sigterm-for-10ms] SIGINT_ignoring...`);
2268
- });
2269
-
2270
- for (let iter = 1, last = 100; iter <= last; ++iter) {
2271
- console.log(`[${now_human()}][ignore-sigterm-for-10ms] ${iter} of ${last}`);
2272
- await Promise.delay(10);
2273
- }
2274
- }
2275
-
2276
- function terminate()
2277
- {
2278
- console.log(`[${now_human()}][ignore-sigterm-for-10ms] TERMINATE_AFTER_10MS`);
2279
- process.exit(0);
2280
- }
2281
-
2282
-
2283
- /***/ }),
2284
-
2285
- /***/ "./src/pid_kill_grace.d/ignore-sigterm.js":
2286
- /*!************************************************!*\
2287
- !*** ./src/pid_kill_grace.d/ignore-sigterm.js ***!
2288
- \************************************************/
2289
- /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
2290
-
2291
- //#!/usr/bin/env node
2292
-
2293
- const Promise = __webpack_require__(/*! bluebird */ "bluebird");
2294
- const cli = __webpack_require__(/*! ../cli */ "./src/cli.js");
2295
- const now_human = __webpack_require__(/*! ../now_human */ "./src/now_human.js");
2296
-
2297
- cli(main);
2298
-
2299
- async function main()
2300
- {
2301
- process.on('SIGTERM', function () {
2302
- console.log(`[${now_human()}][ignore-sigterm] SIGTERM_ignoring...`);
2303
- });
2304
- process.on('SIGINT', function () {
2305
- console.log(`[${now_human()}][ignore-sigterm] SIGINT_ignoring...`);
2306
- });
2307
-
2308
- for (let iter = 1, last = 100; iter <= last; ++iter) {
2309
- console.log(`[${now_human()}][ignore-sigterm] ${iter} of ${last}`);
2310
- await Promise.delay(10);
2311
- }
2312
- }
2313
-
2314
-
2315
2089
  /***/ }),
2316
2090
 
2317
2091
  /***/ "./src/plural.js":
@@ -2499,14 +2273,14 @@ var __webpack_exports__ = {};
2499
2273
  \******************************/
2500
2274
  const ns = new URL(document.currentScript.src).searchParams.get('var') ?? 'h';
2501
2275
  if (typeof window[ns] !== 'undefined') {
2502
- console.log(`❌ @vbarbarosh/node-helpers@${"3.72.3"} was not injected — window.${ns} is already in use`);
2276
+ console.log(`❌ @vbarbarosh/node-helpers@${"3.72.5"} was not injected — window.${ns} is already in use`);
2503
2277
  }
2504
2278
  else {
2505
- console.log(`🎉 @vbarbarosh/node-helpers@${"3.72.3"} successfully exposed as window.${ns}`);
2279
+ console.log(`🎉 @vbarbarosh/node-helpers@${"3.72.5"} successfully exposed as window.${ns}`);
2506
2280
  window[ns] = {};
2507
2281
  // https://github.com/webpack/webpack/issues/625
2508
2282
  // https://webpack.js.org/guides/dependency-management/#require-context
2509
- const require_tmp = __webpack_require__("./src sync recursive \\b(array%7Cdate%7Chttp_delete%7Chttp_get_blob%7Chttp_get_buffer%7Chttp_get_json%7Chttp_get_utf8%7Chttp_head%7Chttp_patch_json%7Chttp_post_json%7Chttp_post_multipart%7Chttp_put_buffer%7Chttp_put_json%7Chttp_put_utf8%7Cidentity%7Cignore%7Cfcmp%7Cfilter%7Cformat%7Cplural%7Crandom_int%7Curlmod%7Cwaitcb)[^/]*(?<%21\\.test)\\.js$");
2283
+ const require_tmp = __webpack_require__("./src sync recursive ^(?%21.*(cli-apps%7Cbrowser%7C\\.d)\\/).*\\b(array%7Cdate%7Chttp_delete%7Chttp_get_blob%7Chttp_get_buffer%7Chttp_get_json%7Chttp_get_utf8%7Chttp_head%7Chttp_patch_json%7Chttp_post_json%7Chttp_post_multipart%7Chttp_put_buffer%7Chttp_put_json%7Chttp_put_utf8%7Cidentity%7Cignore%7Cfcmp%7Cfilter%7Cformat%7Cplural%7Crandom_int%7Curlmod%7Cwaitcb)[^/]*(?<%21\\.test)\\.js$");
2510
2284
  require_tmp.keys().forEach(function (key) {
2511
2285
  const [, basename] = key.match(/([^/]+)\.js$/);
2512
2286
  window[ns][basename] = require_tmp(key);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "Vladimir Barbarosh <vladimir.barbarosh@gmail.com>",
3
3
  "license": "MIT",
4
4
  "name": "@vbarbarosh/node-helpers",
5
- "version": "3.72.3",
5
+ "version": "3.72.5",
6
6
  "description": "A set of helpers for JavaScript/Node.js",
7
7
  "unpkg": "dist/browser.js",
8
8
  "jsdelivr": "dist/browser.js",
@@ -7,7 +7,7 @@ else {
7
7
  window[ns] = {};
8
8
  // https://github.com/webpack/webpack/issues/625
9
9
  // https://webpack.js.org/guides/dependency-management/#require-context
10
- const require_tmp = require.context('..', true, /\b(array|date|http_delete|http_get_blob|http_get_buffer|http_get_json|http_get_utf8|http_head|http_patch_json|http_post_json|http_post_multipart|http_put_buffer|http_put_json|http_put_utf8|identity|ignore|fcmp|filter|format|plural|random_int|urlmod|waitcb)[^/]*(?<!\.test)\.js$/);
10
+ const require_tmp = require.context('..', true, /^(?!.*(cli-apps|browser|\.d)\/).*\b(array|date|http_delete|http_get_blob|http_get_buffer|http_get_json|http_get_utf8|http_head|http_patch_json|http_post_json|http_post_multipart|http_put_buffer|http_put_json|http_put_utf8|identity|ignore|fcmp|filter|format|plural|random_int|urlmod|waitcb)[^/]*(?<!\.test)\.js$/);
11
11
  require_tmp.keys().forEach(function (key) {
12
12
  const [, basename] = key.match(/([^/]+)\.js$/);
13
13
  window[ns][basename] = require_tmp(key);
package/src/clamp.js ADDED
@@ -0,0 +1,6 @@
1
+ function clamp(min, max, value)
2
+ {
3
+ return Math.max(min, Math.min(max, value));
4
+ }
5
+
6
+ module.exports = clamp;
@@ -0,0 +1,15 @@
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(10);
14
+ }
15
+ }
@@ -0,0 +1,15 @@
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
+ await ping_socket(process.env.WATCHDOG_SOCKET);
12
+ while (true) {
13
+ await Promise.delay(1000);
14
+ }
15
+ }
@@ -5,14 +5,19 @@ const HeartbeatServer = require('../HeartbeatServer');
5
5
  const Promise = require('bluebird');
6
6
  const cli = require('../cli');
7
7
  const cuid = require('@paralleldrive/cuid2');
8
+ const format_ms3 = require('../format_ms3');
9
+ const format_thousands = require('../format_thousands');
8
10
  const make = require('@vbarbarosh/type-helpers/src/make');
9
- const now_human = require('../now_human');
10
11
  const pgid_exists = require('../pgid_exists');
11
12
  const pgid_kill_grace = require('../pgid_kill_grace');
12
13
  const pkg = require('../../package.json');
13
14
  const shell_spawn = require('../shell_spawn');
14
15
 
16
+ const STARTED_AT = Date.now();
15
17
  const LOGS_ROOT_UID = cuid.createId();
18
+ const ARGS = process.argv.slice(2);
19
+
20
+ let PING_COUNTER = 0;
16
21
 
17
22
  // 💎 Only the lack of a valid PING within WATCHDOG_INTERVAL is fatal
18
23
 
@@ -31,18 +36,17 @@ cli(main, report);
31
36
 
32
37
  function report(error)
33
38
  {
34
- log(`[watchdog_end_error] ${error.message}`);
39
+ log(`[watchdog_end_error] ${render_stats()} | ${error.message} | ${ARGS.join(' ')}`);
35
40
  }
36
41
 
37
42
  async function main()
38
43
  {
39
- const args = process.argv.slice(2);
40
- if (!args.length) {
44
+ if (!ARGS.length) {
41
45
  usage();
42
46
  process.exit(1);
43
47
  }
44
48
 
45
- switch (args[0]) {
49
+ switch (ARGS[0]) {
46
50
  case '-v':
47
51
  case '--version':
48
52
  version();
@@ -52,18 +56,20 @@ async function main()
52
56
 
53
57
  log(`[watchdog_begin] v${pkg.version}`);
54
58
 
55
- const WATCHDOG_INTERVAL = make(process.env.WATCHDOG_INTERVAL, {type: 'int', min: 1000, default: 5000});
59
+ const WATCHDOG_INTERVAL = process.env.__TESTING__
60
+ ? make(process.env.WATCHDOG_INTERVAL, {type: 'int', min: 1, default: 5000})
61
+ : make(process.env.WATCHDOG_INTERVAL, {type: 'int', min: 1000, default: 5000});
56
62
  const heartbeat_server = new HeartbeatServer(WATCHDOG_INTERVAL);
57
63
  const WATCHDOG_SOCKET = heartbeat_server.socket_path;
58
- heartbeat_server.on('heartbeat', () => log('[watchdog_ping] ❤️'))
64
+ heartbeat_server.on('heartbeat', () => log(`[watchdog_ping] ❤️ ${render_stats(++PING_COUNTER)}`))
59
65
  heartbeat_server.on('warning', error => log(`[watchdog_warn] ⚠️ ${error}`))
60
66
 
61
67
  try {
62
- log(`[watchdog_args] ${args.join(' ')}`);
68
+ log(`[watchdog_args] ${ARGS.join(' ')}`);
63
69
  log(`[watchdog_interval] ${WATCHDOG_INTERVAL}`);
64
70
  log(`[watchdog_socket_path] ${heartbeat_server.socket_path}`);
65
71
  const env = {...process.env, WATCHDOG_INTERVAL, WATCHDOG_SOCKET};
66
- const proc = await shell_spawn(args, {stdio: 'inherit', env, detached: true}).init();
72
+ const proc = await shell_spawn(ARGS, {stdio: 'inherit', env, detached: true}).init();
67
73
  const pgid = proc.pid; // group id == leader pid
68
74
 
69
75
  const signal = new Promise(function (resolve, reject) {
@@ -94,7 +100,7 @@ async function main()
94
100
  await Promise.resolve(heartbeat_server.dispose()).timeout(1000);
95
101
  }
96
102
 
97
- log('[watchdog_end_ok]');
103
+ log(`[watchdog_end_ok] ${render_stats()} | ${ARGS.join(' ')}`);
98
104
  process.exit(0);
99
105
  }
100
106
 
@@ -102,13 +108,18 @@ function log(s)
102
108
  {
103
109
  const ss = JSON.stringify(s).slice(1, -1).replaceAll('\\"', '"');
104
110
  if (ss[0] === '[') {
105
- console.log(`[${LOGS_ROOT_UID}][${now_human()}]${ss}`);
111
+ console.log(`[${LOGS_ROOT_UID}][${format_ms3(Date.now() - STARTED_AT, true)}]${ss}`);
106
112
  }
107
113
  else {
108
- console.log(`[${LOGS_ROOT_UID}][${now_human()}] ${ss}`);
114
+ console.log(`[${LOGS_ROOT_UID}][${format_ms3(Date.now() - STARTED_AT, true)}] ${ss}`);
109
115
  }
110
116
  }
111
117
 
118
+ function render_stats()
119
+ {
120
+ return `pings=${format_thousands(PING_COUNTER)}`;
121
+ }
122
+
112
123
  function usage()
113
124
  {
114
125
  process.stdout.write(`
@@ -1,7 +1,48 @@
1
1
  const assert = require('assert');
2
+ const pid_exists = require('../pid_exists');
3
+ const shell_spawn = require('../shell_spawn');
4
+ const stream = require('stream');
5
+ const stream_each = require('../stream_each');
6
+ const stream_lines = require('../stream_lines');
2
7
 
3
8
  // watchdog true -> race condition; true exists too quickly; bug in shell_spawn
4
9
 
5
- describe.skip('watchdog', function () {
6
-
10
+ describe('watchdog', function () {
11
+ this.timeout(1000);
12
+ it('Happy Path', async function () {
13
+ const env = {...process.env, WATCHDOG_INTERVAL: 100, __TESTING__: 1};
14
+ const proc = shell_spawn([`${__dirname}/watchdog.js`, `${__dirname}/watchdog.d/quick-exit-after-2-pings.js`], {env});
15
+ try {
16
+ const stdout = [];
17
+ await Promise.all([
18
+ stream.promises.pipeline(proc.stdout, stream_lines(), stream_each(v => stdout.push(v))),
19
+ proc.promise(),
20
+ ]);
21
+ assert.strictEqual(stdout.filter(v => v.includes('[watchdog_ping]')).length, 2);
22
+ assert.strictEqual(stdout.filter(v => v.includes('[watchdog_end_ok] pings=2')).length, 1);
23
+ }
24
+ finally {
25
+ if (pid_exists(proc.pid)) {
26
+ process.kill(proc.pid, 'SIGTERM');
27
+ }
28
+ }
29
+ });
30
+ it('Hang after 1 ping', async function () {
31
+ const env = {...process.env, WATCHDOG_INTERVAL: 100, __TESTING__: 1};
32
+ const proc = shell_spawn([`${__dirname}/watchdog.js`, `${__dirname}/watchdog.d/quick-hang-after-1-ping.js`], {env});
33
+ const stdout = [];
34
+ try {
35
+ await Promise.all([
36
+ stream.promises.pipeline(proc.stdout, stream_lines(), stream_each(v => stdout.push(v))),
37
+ assert.rejects(proc.promise().timeout(500), {message: 'Process terminated with code 124 and signal null'}),
38
+ ]);
39
+ assert.strictEqual(stdout.filter(v => v.includes('[watchdog_ping]')).length, 1);
40
+ assert.strictEqual(stdout.filter(v => v.includes('[watchdog_end_error] pings=1 | No heartbeat for the last 100ms')).length, 1);
41
+ }
42
+ finally {
43
+ if (pid_exists(proc.pid)) {
44
+ process.kill(proc.pid, 'SIGTERM');
45
+ }
46
+ }
47
+ });
7
48
  });
package/src/format_ms3.js CHANGED
@@ -3,7 +3,7 @@ const factorize_ms = require('./factorize_ms');
3
3
  /**
4
4
  * Format milliseconds
5
5
  */
6
- function format_ms3(ms)
6
+ function format_ms3(ms, include_zero_hours = false)
7
7
  {
8
8
  if (!ms) {
9
9
  ms = 0;
@@ -11,7 +11,7 @@ function format_ms3(ms)
11
11
 
12
12
  // 00:05.445
13
13
  const [h, m, s, xx] = factorize_ms(ms);
14
- if (h) {
14
+ if (h || include_zero_hours) {
15
15
  return `0${h}:0${m}:0${s}.00${xx}`.replace(/0+(?=\d\d+[:.]|\d\d\d)/g, '');
16
16
  }
17
17
  return `0${m}:0${s}.00${xx}`.replace(/0+(?=\d\d+[:.]|\d\d\d)/g, '')
package/src/round.js ADDED
@@ -0,0 +1,24 @@
1
+ // // round(34.037531, 0.001) -> 34.038000000000004
2
+ // function round(value, precision = 1)
3
+ // {
4
+ // return Math.round(value / precision) * precision;
5
+ // }
6
+
7
+ // // round(1.005, 0.01) -> 1
8
+ // function round(value, precision = 1)
9
+ // {
10
+ // const p = 1 / precision;
11
+ // return Math.round(value * p) / p;
12
+ // }
13
+
14
+ function round(value, precision = 1)
15
+ {
16
+ if (precision === 0) {
17
+ return value;
18
+ }
19
+ const factor = 1 / precision;
20
+ const epsilon = 1e-12 * Math.sign(value);
21
+ return Math.round((value * factor) + epsilon) / factor;
22
+ }
23
+
24
+ module.exports = round;
@@ -0,0 +1,38 @@
1
+ const assert = require('assert');
2
+ const round = require('./round');
3
+
4
+ const items = [
5
+ [34, 25, 25],
6
+ [37.5, 25, 50],
7
+ [34.037531, 0.001, 34.038],
8
+ [1.005, 0.01, 1.01],
9
+
10
+ // Synthetic cases generated by DeepSeek
11
+ [1.005, 0.001, 1.005], // More precise rounding
12
+ [1.005, 0.1, 1.0], // Less precise rounding
13
+ [1.015, 0.01, 1.02], // Another edge case with 5
14
+ [1.025, 0.01, 1.03], // Yet another edge case
15
+ [0.5, 1, 1], // Round to nearest integer
16
+ [0.4, 1, 0], // Round down to integer
17
+ [123.456, 0.01, 123.46], // Standard decimal rounding
18
+ [123.456, 0.1, 123.5], // Different precision
19
+ [123.456, 1, 123], // Round to whole number
20
+ [123.456, 10, 120], // Round to tens
21
+ [123.456, 100, 100], // Round to hundreds
22
+ [0, 0.01, 0], // Zero value
23
+ [-1.005, 0.01, -1.01], // Negative numbers
24
+ [-1.015, 0.01, -1.02], // Negative edge case
25
+ [-37.5, 25, -50], // Negative with integer precision
26
+ [1.2345e-5, 1e-6, 0.000012], // Very small numbers
27
+ [999.999, 0.001, 999.999], // Rounding that causes carry-over
28
+ [0.0000000000001, 0.01, 0], // Extremely small number
29
+ [Number.MAX_SAFE_INTEGER, 1000, 9007199254741000], // Large numbers
30
+ ];
31
+
32
+ describe('round', function () {
33
+ items.forEach(function ([value, precision, expected],) {
34
+ it(`${value} [${precision}] → ${expected}`, function () {
35
+ assert.deepStrictEqual(round(value, precision), expected);
36
+ });
37
+ });
38
+ });
package/src/shell.md ADDED
@@ -0,0 +1,16 @@
1
+ Basic helper for running external commands via `execFile`.
2
+
3
+ - Fails if the command exits with non-zero code.
4
+ - Fails if anything is printed to `stderr`.
5
+ - Resolves with `stdout` (string), exactly as produced.
6
+
7
+ ## Signature
8
+
9
+ ```
10
+ shell(args, options) → Promise<string>
11
+ ```
12
+
13
+ ## Notes
14
+
15
+ - `args[0]` is the executable; `args[1..]` are arguments.
16
+ - Suitable for commands that produce clean stdout and no warnings.
@@ -0,0 +1,12 @@
1
+ Basic helper for running external commands via `execFile`, and parse result by JSON.parse.
2
+
3
+ - Fails if the command exits with non-zero code.
4
+ - Fails if anything is printed to `stderr`.
5
+ - Fails if stdout is not valid JSON.
6
+ - Resolves with parsed JSON value.
7
+
8
+ ## Signature
9
+
10
+ ```
11
+ shell_json(args, options) → Promise<any>
12
+ ```
@@ -0,0 +1,12 @@
1
+ Basic helper for running external commands via `execFile`, and splits stdout into lines.
2
+
3
+ - Fails if the command exits with non-zero code.
4
+ - Fails if anything is printed to `stderr`.
5
+ - Strips trailing newline(s).
6
+ - Returns array of lines.
7
+
8
+ ## Signature
9
+
10
+ ```
11
+ shell_lines(args, options) → Promise<string[]>
12
+ ```
@@ -1,6 +1,7 @@
1
1
  const ExitCodeError = require('./errors/ExitCodeError');
2
2
  const Promise = require('bluebird');
3
3
  const child_process = require('child_process');
4
+ const ignore = require('./ignore');
4
5
 
5
6
  // 📕 Notes by ChatGPT:
6
7
  // If the child is killed by a signal (e.g. SIGKILL from outside),
@@ -20,45 +21,44 @@ const child_process = require('child_process');
20
21
  // > }
21
22
  // > }
22
23
 
23
- // ⚠️ Both .init and .promise are targets for race conditions!
24
24
  function shell_spawn(args, options)
25
25
  {
26
- let init, promise;
26
+ const init = {};
27
+ const run = {};
28
+
29
+ init.promise = new Promise(function (resolve, reject) {
30
+ init.resolve = resolve;
31
+ init.reject = reject;
32
+ });
33
+ run.promise = new Promise(function (resolve, reject) {
34
+ run.resolve = resolve;
35
+ run.reject = reject;
36
+ });
37
+
27
38
  const out = child_process.spawn(args[0], args.slice(1), options);
28
- out.init = function () {
29
- return init = init || new Promise(function (resolve, reject) {
30
- out.once('error', init_error);
31
- out.once('spawn', init_spawn);
32
- function init_error(error) {
33
- out.off('spawn', init_spawn);
34
- reject(error);
35
- }
36
- function init_spawn() {
37
- out.off('error', init_error);
38
- resolve(out);
39
- }
40
- });
41
- };
42
- out.promise = function () {
43
- return promise = promise || new Promise(function (resolve, reject) {
44
- out.once('error', promise_error);
45
- out.once('exit', promise_exit);
46
- function promise_error(error) {
47
- out.off('exit', promise_exit);
48
- reject(error);
49
- }
50
- function promise_exit(code, signal) {
51
- out.off('error', promise_error);
52
- if (code === 0) {
53
- resolve();
54
- }
55
- else {
56
- reject(new ExitCodeError(code || 128, `Process terminated with code ${code} and signal ${signal}`));
57
- }
58
- }
59
- });
60
- };
39
+ out.once('error', onerror);
40
+ out.once('spawn', onspawn);
41
+ out.once('exit', onexit);
42
+ out.init = () => init.promise;
43
+ out.promise = () => run.promise;
61
44
  return out;
45
+
46
+ function onerror(error) {
47
+ init.reject(error);
48
+ run.reject(error);
49
+ }
50
+ function onexit(code, signal) {
51
+ if (code === 0) {
52
+ run.resolve();
53
+ }
54
+ else {
55
+ run.reject(new ExitCodeError(code || 128, `Process terminated with code ${code} and signal ${signal}`));
56
+ }
57
+ }
58
+ function onspawn() {
59
+ init.resolve(out);
60
+ init.reject = ignore;
61
+ }
62
62
  }
63
63
 
64
64
  module.exports = shell_spawn;
@@ -0,0 +1,25 @@
1
+ Spawn-based helper for long-running or streaming commands.
2
+
3
+ Provides two promises:
4
+
5
+ - `proc.init()` — resolves when the child emits `spawn`.
6
+ - `proc.promise()` — resolves when the child exits with code 0.
7
+
8
+ ## Failure semantics
9
+
10
+ - Rejects if `error` event occurs.
11
+ - Rejects if exit code is non-zero.
12
+ - If child dies by signal, treated as non-zero exit (uses exitCode = 128).
13
+
14
+ ## Signature
15
+
16
+ ```
17
+ const proc = shell_spawn(args, options)
18
+ proc.init() → Promise<ChildProcess>
19
+ proc.promise() → Promise<void>
20
+ ```
21
+
22
+ ## Notes
23
+
24
+ - Output is streamed (not buffered).
25
+ - Use when you need live stdout/stderr or long-running jobs.
@@ -3,7 +3,7 @@ const assert = require('assert');
3
3
  const pid_exists = require('./pid_exists');
4
4
  const shell_spawn = require('./shell_spawn');
5
5
 
6
- describe.skip('shell_spawn', function () {
6
+ describe('shell_spawn', function () {
7
7
  describe('Happy Path', function () {
8
8
  it('basic', async function () {
9
9
  await shell_spawn(['true']).promise().timeout(100);
@@ -20,4 +20,11 @@ describe.skip('shell_spawn', function () {
20
20
  assert(!pid_exists(proc.pid));
21
21
  await proc.promise().timeout(100);
22
22
  });
23
+ it('should throw when init fails', async function () {
24
+ await assert.rejects(shell_spawn(['ggg111']).init(), /^Error: spawn ggg111 ENOENT$/);
25
+ });
26
+ it('should throw when process exists with non zero code', async function () {
27
+ const proc = await shell_spawn(['false']).init();
28
+ await assert.rejects(proc.promise().timeout(100), /^Error: Process terminated with code 1 and signal null$/);
29
+ });
23
30
  });
@@ -0,0 +1,11 @@
1
+ Runs a command via `execFile` and returns both stdout and stderr.
2
+
3
+ - Rejects only on exec error.
4
+ - Resolves with `{stdout, stderr}`.
5
+ - Does NOT treat stderr output as failure.
6
+
7
+ ## Signature
8
+
9
+ ```
10
+ shell_stdall(args, options) → Promise<{stdout: string, stderr: string}>
11
+ ```
@@ -0,0 +1,10 @@
1
+ Convenience wrapper over `shell_spawn` with `stdio: 'inherit'`.
2
+
3
+ - Child stdout/stderr are streamed directly to parent terminal.
4
+ - Supports `proc.init()` and `proc.promise()` like `shell_spawn`.
5
+
6
+ ## Signature
7
+
8
+ ```
9
+ shell_thru(args, options) → ChildProcess
10
+ ```