dcp-worker 3.3.7 → 3.3.9-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/bin/dcp-worker CHANGED
@@ -21,7 +21,8 @@ const path = require('path');
21
21
  const crypto = require('crypto');
22
22
  const chalk = require('chalk');
23
23
  const telnetd = require('../lib/remote-console');
24
- const utils = require('../lib/utils');
24
+
25
+ const { slicesFetched, debugging, displayMaxDiagInfo } = require('../lib/utils');
25
26
 
26
27
  const configName = process.env.DCP_CONFIG || '../etc/dcp-worker-config';
27
28
  const EXIT_UNHANDLED = 5;
@@ -74,6 +75,12 @@ function parseCliArgs()
74
75
  describe: 'default proportion of CPU,GPU to use when cores not specified',
75
76
  type: 'string',
76
77
  },
78
+ maxSandboxes: {
79
+ alias: 'm',
80
+ describe: 'Maximum number of sandboxes',
81
+ type: 'number',
82
+ default: undefined,
83
+ },
77
84
  verbose: {
78
85
  alias: 'v',
79
86
  describe: 'Enable verbose output',
@@ -294,7 +301,7 @@ async function main()
294
301
  process.on('SIGQUIT', handleSigDeath);
295
302
  process.on('unhandledRejection', handleUnhandled);
296
303
  process.on('uncaughtException', handleUnhandled);
297
-
304
+
298
305
  const getOpts = {};
299
306
  if (cliArgs.defaultPaymentAddressToDCP)
300
307
  getOpts.oAuth = false;
@@ -343,7 +350,6 @@ async function main()
343
350
  const dcpWorkerOptions = dcpConfig.worker;
344
351
  const forceOptions = {
345
352
  paymentAddress,
346
- maxWorkingSandboxes: cliArgs.cores,
347
353
  };
348
354
  const defaultOptions = {
349
355
  sandboxOptions: {
@@ -357,7 +363,7 @@ async function main()
357
363
  forceOptions.publicGroupFallback = mkBool(cliArgs.publicGroupFallback);
358
364
 
359
365
  addConfig(dcpWorkerOptions, defaultOptions, dcpConfig.worker, forceOptions);
360
- processCoresAndDensity(dcpWorkerOptions, cliArgs);
366
+ processCoresAndMaxSandboxes(dcpWorkerOptions, cliArgs);
361
367
 
362
368
  /* Support magic value used by Windows screensaver configuration /wg June 2023 */
363
369
  if (dcpWorkerOptions.leavePublicGroup === 'fallback')
@@ -398,10 +404,23 @@ async function main()
398
404
  dcpWorkerOptions.watchdogInterval = cliArgs.watchdogInterval;
399
405
 
400
406
  worker = new DCPWorker(identityKeystore, dcpWorkerOptions);
401
- worker.on('error', (...payload) => console.error(...payload));
402
407
  worker.on('warning', (...payload) => console.warn (...payload));
403
408
  worker.on('stop', () => { console.log('Worker is stopping') });
404
409
  worker.on('end', () => { logClosing('log', 'Worker has stopped') });
410
+ // Display clean diagnostic when not debugging and env var
411
+ // DCP_SUPERVISOR_DEBUG_DISPLAY_MAX_INFO isn't set.
412
+ worker.on('error', (error) => {
413
+ if (displayMaxDiagInfo())
414
+ console.error(error);
415
+ else
416
+ {
417
+ const errorCode = error.code ?? error.errorCode;
418
+ if (errorCode)
419
+ console.error(`Error: ${error.message}: error.code ${errorCode}`);
420
+ else
421
+ console.error(`Error: ${error.message}`);
422
+ }
423
+ });
405
424
  require('../lib/default-ui-events').hook(worker, cliArgs);
406
425
 
407
426
  if (cliArgs.outputMode === 'dashboard')
@@ -431,7 +450,7 @@ async function main()
431
450
  if (ev instanceof Error)
432
451
  console.error('Error fetching task:', ev);
433
452
  else
434
- dcpWorkerOptions.leavePublicGroup = Boolean(utils.slicesFetched(ev) > 0);
453
+ dcpWorkerOptions.leavePublicGroup = Boolean(slicesFetched(ev) > 0);
435
454
  }
436
455
  }
437
456
  }
@@ -466,8 +485,8 @@ async function main()
466
485
  introBanner += ' . Leaving the public compute group' + '\n';
467
486
  if (dcpWorkerOptions.cores)
468
487
  introBanner += ` . Configured Cores: ${dcpWorkerOptions.cores.cpu},${dcpWorkerOptions.cores.gpu}\n`
469
- else
470
- introBanner += ` . Target core density: ${JSON.stringify(dcpWorkerOptions.defaultCoreDensity)}\n`;
488
+ if (typeof dcpWorkerOptions.maxSandboxes !== 'undefined')
489
+ introBanner += ` . Maximum Sandboxes: ${dcpWorkerOptions.maxSandboxes}\n`
471
490
  if (cliArgs.verbose)
472
491
  introBanner += ` + Verbosity level: ${cliArgs.verbose}` + '\n';
473
492
  if (telnetd.hasOwnProperty('port'))
@@ -480,7 +499,7 @@ async function main()
480
499
  for (const wt of worktimes)
481
500
  introBanner += ` -\t${wt.name}@${wt.versions.join(';')}\n`;
482
501
  }
483
-
502
+
484
503
  introBanner += ' . Supervisor version: ' + worker.supervisorVersion;
485
504
  introBanner += ' . Output mode: ' + cliArgs.outputMode + '\n';
486
505
  introBanner += ' * Ready' + '\n';
@@ -501,7 +520,8 @@ async function main()
501
520
  }
502
521
 
503
522
  /**
504
- * Process the cores and density cli arguments.
523
+ * Process the cores and maxSandboxes cli arguments.
524
+ * Also processes density, but that isn't currently used and may be deprecated in the future.
505
525
  *
506
526
  * cliArgs.cores is the core count of the hardware to use.
507
527
  * It can be specified with only the cpu or gpu component, or both.
@@ -509,8 +529,10 @@ async function main()
509
529
  * E.g. -c 2,1 => cores = { cpu: 2, gpu: 1 }
510
530
  * -c 10 => cores = { cpu: 10, gpu: <default> }
511
531
  * -c ,10 => cores = { cpu: <default>, gpu: 10 }
532
+ * -m 5 => maxSandboxes = 5
533
+ * default maxSandboxes := Math.max( 1, Math.round((System RAM) / 2.5GB) );
512
534
  */
513
- function processCoresAndDensity (dcpWorkerOptions, cliArgs)
535
+ function processCoresAndMaxSandboxes (dcpWorkerOptions, cliArgs)
514
536
  {
515
537
  const DCPWorker = require('dcp/worker').Worker;
516
538
  const defaultTargets = {
@@ -532,10 +554,12 @@ function processCoresAndDensity (dcpWorkerOptions, cliArgs)
532
554
  parseArg('density');
533
555
  parseArg('cores');
534
556
 
535
- if (dcpWorkerOptions.cores)
536
- debugging() && console.debug('dcp-worker: cores =', dcpWorkerOptions.cores);
537
- if (dcpWorkerOptions.density)
538
- debugging() && console.debug('dcp-worker: core density =', dcpWorkerOptions.density);
557
+ if (typeof cliArgs['maxSandboxes'] !== 'undefined')
558
+ dcpWorkerOptions.maxSandboxes = Number(cliArgs['maxSandboxes']);
559
+ else
560
+ DCPWorker.defaultMaxSandboxes(dcpWorkerOptions);
561
+ debugging() && console.debug(`dcp-worker: cores = { ${dcpWorkerOptions.cores.cpu}, ${dcpWorkerOptions.cores.gpu} }`);
562
+ debugging() && console.debug('dcp-worker: core density =', dcpWorkerOptions.density);
539
563
  }
540
564
 
541
565
  /**
@@ -688,24 +712,44 @@ function sliceReport()
688
712
  /**
689
713
  * Handle a signal which requests our the death of the Worker by
690
714
  * - stopping the worker
691
- * - unregistering the handler (this allows a second signal to forcibly terminate the process
692
- * if that is the default behaviour)
715
+ * - unregistering the signal handler and replacing it with a call to process.exit() after
716
+ * two tries.
717
+ * -> this allows a third signal to forcibly terminate the process
693
718
  * - set a long timeout (dcpConfig.worker.cleanupTimeout seconds), after which the process
694
719
  * exits forcibly with a non-zero exit code (unix standard for various signals)
720
+ * - the worker is soft-stopped after the first signal if it is not SIGQUIT but hard stopped
721
+ * all other times.
695
722
  */
696
723
  function handleSigDeath(signalName, signal)
697
724
  {
698
- process.off(signalName, handleSigDeath);
725
+ handleSigDeath.count = Number(handleSigDeath.count || 0) + 1;
726
+
727
+ if (handleSigDeath.count > 2)
728
+ {
729
+ process.on(signalName, () => {
730
+ console.error(signalName);
731
+ process.exit(signal - 128)
732
+ });
733
+ }
699
734
 
700
735
  if (!worker)
701
736
  console.warn(`trapped ${signalName}, signal ${signal}`);
702
737
  else
703
738
  {
704
- console.warn(`trapped ${signalName}, signal ${signal} -- stopping worker`);
705
- worker.stop(signalName === 'SIGQUIT');
739
+ const immediate = signalName === 'SIGQUIT' || handleSigDeath.count > 1;
740
+ console.warn(`trapped ${signalName}, signal ${signal} -- stopping worker`,
741
+ immediate ? 'immediately' : `after ${handleSigDeath.count} slices have finished`);
742
+ worker.stop(immediate);
706
743
  }
707
744
 
708
- setTimeout(() => processExit(signal - 128), getCleanupTimeoutMs()).unref();
745
+ function die()
746
+ {
747
+ processExit(signal - 128);
748
+ }
749
+
750
+ setTimeout(die, getCleanupTimeoutMs()).unref();
751
+ if (handleSigDeath.count === 3)
752
+ die();
709
753
  }
710
754
 
711
755
  /**
@@ -714,7 +758,7 @@ function handleSigDeath(signalName, signal)
714
758
  function getCleanupTimeoutMs()
715
759
  {
716
760
  const defaultCT = 60;
717
- var cleanupTimeout = dcpConfig.worker.cleanupTimeout;
761
+ var cleanupTimeout = dcpConfig.worker?.cleanupTimeout;
718
762
 
719
763
  if (typeof cleanupTimeout === 'undefined')
720
764
  cleanupTimeout = defaultCT;
@@ -783,14 +827,6 @@ function verifyDefaultConfigIntegrity()
783
827
  }
784
828
  }
785
829
 
786
- /* thunk - ensures global debugging() symbol always available even if called before dcp-client init */
787
- function debugging()
788
- {
789
- require('dcp-client');
790
- debugging = require('dcp/internal/debugging').scope('dcp-worker');
791
- return debugging.apply(this, arguments);
792
- }
793
-
794
830
  /**
795
831
  * Cast b to boolean such that 'false' becomes false, falsey things become false, and everything else
796
832
  * becomes true.
package/docs/CODEOWNERS CHANGED
@@ -1,35 +1,28 @@
1
- # @file CODEOWNERS - machine-generated via dcp-docs-wes git@gitlab.com:Distributed-Compute-Protocol/dcp-docs-wes.git 95d8ee8e39d502a4e215311b8685a8c18c5dff99
1
+ # @file CODEOWNERS - machine-generated via dcp-docs-wes git@gitlab.com:Distributed-Compute-Protocol/dcp-docs-wes.git 0b99b9719e7523d036bbea5b3c3f069ad2f5579c
2
2
  # @author wes@people
3
- # @date Wed Apr 24 09:17:03 EDT 2024
4
-
5
- []
6
3
 
7
4
  [Brandon]
5
+ /publish-docs.sh @BChristieDistributive
8
6
  /catalog-info.yaml @BChristieDistributive
9
7
 
10
8
  [Eddie]
11
9
  /package-lock.json @eroosenmaallen
12
10
 
13
11
  [Wes]
14
- /npm-hooks/ @wesgarland
15
- /README.md @wesgarland
16
- /docs/ @wesgarland
17
- /package-lock.json @wesgarland
12
+ /etc/ @wesgarland
13
+ /lib/ @wesgarland
14
+ /bin/ @wesgarland
18
15
  /.eslintrc.js @wesgarland
16
+ /.git @wesgarland
17
+ /.gitignore @wesgarland
18
+ /.gitlab-ci.yml @wesgarland
19
19
  /.npmrc @wesgarland
20
20
  /.tidelift @wesgarland
21
- /lib/ @wesgarland
22
- /package.json @wesgarland
23
- /LICENSE.md @wesgarland
24
21
  /.trunk/ @wesgarland
25
- /.gitignore @wesgarland
26
- /etc/ @wesgarland
27
- /.gitlab-ci.yml @wesgarland
28
- /bin/ @wesgarland
29
- /catalog-info.yaml @wesgarland
30
- /etc/ @wesgarland
31
- /lib/ @wesgarland
32
- /bin/ @wesgarland
22
+ /LICENSE.md @wesgarland
23
+ /README.md @wesgarland
24
+ /docs/ @wesgarland
25
+ /npm-hooks/ @wesgarland
33
26
 
34
27
  [Wes or Eddie]
35
28
  /package.json @wesgarland @eroosenmaallen
@@ -2,7 +2,7 @@
2
2
  * @file blessed-components/log.js
3
3
  * @author Ryan Rossiter, ryan@kingsds.network
4
4
  * @date April 2020
5
- *
5
+ *
6
6
  * This blessed component is based on the blessed-contrib
7
7
  * log component, but features text wrapping and behaves
8
8
  * closer to the built-in console log methods.
@@ -11,29 +11,47 @@
11
11
 
12
12
  const { Box } = require('blessed');
13
13
 
14
- class Log extends Box {
15
- constructor(options={}) {
14
+ const defaultOptions = {
15
+ scrollable: true,
16
+ bufferLength: 1000,
17
+ mouse: true,
18
+ keys: true,
19
+ vi: true
20
+ };
21
+
22
+ class Log extends Box
23
+ {
24
+ paused = false;
25
+
26
+ constructor(options={})
27
+ {
28
+ options = Object.assign({}, defaultOptions, options);
16
29
  super(options);
17
-
18
- options.bufferLength = options.bufferLength || 35;
19
30
  this.options = options;
20
-
21
31
  this.logLines = [];
32
+
33
+ this.screen.key(['C-s'], () => this.paused = true);
34
+ this.screen.key(['C-q'], () => this.paused = false);
35
+ this.screen.key([' '], () => {
36
+ this.paused = false;
37
+ this.setScrollPerc(100);
38
+ });
22
39
  }
23
40
 
24
- log(...args) {
25
- let str = args.reduce(
41
+ log(...args)
42
+ {
43
+ const str = args.reduce(
26
44
  (s, arg) => (s += `${typeof arg === 'string'? arg : JSON.stringify(arg, null, 2)} `),
27
45
  '');
28
46
 
29
47
  this.logLines.push(str);
30
-
31
- if (this.logLines.length > this.options.bufferLength) {
48
+
49
+ if (this.logLines.length > this.options.bufferLength)
32
50
  this.logLines.shift();
33
- }
34
51
 
35
52
  this.setContent(this.logLines.join('\n'));
36
- this.setScrollPerc(100);
53
+ if (!this.paused)
54
+ this.setScrollPerc(100);
37
55
  }
38
56
  }
39
57
 
@@ -1,95 +1,116 @@
1
1
  /**
2
- * @file blessed-components/sandboxes.js
3
- * @author Ryan Rossiter, ryan@kingsds.network
4
- * @date April 2020
5
- *
6
- * This blessed component produces labeled progress bars
7
- * to render the progress of worker sandboxes.
2
+ * @file blessed-components/sandboxes.js
3
+ * @author Ryan Rossiter, ryan@kingsds.network
4
+ * @date April 2020
5
+ * @author Wes Garland, wes@distributive.network
6
+ * @date May 2024
7
+ *
8
+ * This blessed component produces labeled progress bars to render the progress of worker sandboxes.
9
+ * These are called sandboxRows.
8
10
  */
9
11
  'use strict';
10
12
 
11
- const { Box, ProgressBar, Text } = require('blessed');
13
+ const blessed = require('blessed');
12
14
 
13
- class Sandboxes extends Box {
14
- constructor(options) {
15
+ class SandboxPane extends blessed.Box
16
+ {
17
+ constructor(options)
18
+ {
15
19
  super(options);
16
-
17
- this.data = options.data || [];
18
- this.progressBars = [];
19
- if (options.defaultProgressBars) {
20
- [...Array(options.defaultProgressBars)].map(
21
- () => this.createProgressBar());
22
- }
23
-
20
+ this.sandboxRows = [];
24
21
  this.update();
25
22
  }
26
23
 
27
- createProgressBar({ progress=0, label='IDLE', indeterminate=true }={}) {
28
- let progressBar = new ProgressBar({
24
+ /**
25
+ * Create a new progress bar, and associate it with the sandboxData object that it will read to
26
+ * update its state.
27
+ */
28
+ createSandboxRow(sandboxData)
29
+ {
30
+ let progressBar = new blessed.ProgressBar({
29
31
  parent: this,
30
32
  orientation: 'horizontal',
31
- filled: progress,
32
- top: this.progressBars.length,
33
- left: 0, right: 1,
33
+ top: this.sandboxRows.length,
34
+ left: 0,
35
+ right: 0,
34
36
  height: 1,
35
- style: {
36
- bg: 'black',
37
- bar: {
38
- bg: indeterminate? 'blue' : 'green',
39
- },
40
- }
37
+ style: { bg: 'black' },
41
38
  });
42
- let labelElement = new Text({
39
+
40
+ const text = new blessed.Text({
43
41
  parent: this,
44
- content: label + ' ',
45
- top: this.progressBars.length,
46
- left: 0, right: 1,
47
- style: {
48
- transparent: true,
49
- }
42
+ content: '<initializing>X', /* bug: last char truncates */
43
+ wrap: false,
44
+ top: this.sandboxRows.length,
45
+ left: 0,
46
+ right: 0,
47
+ style: { transparent: true }
50
48
  });
51
49
 
52
- this.progressBars.push({
53
- progressBar,
54
- label: labelElement,
55
- });
56
- return progressBar;
50
+ const sandboxRow = { progressBar, text, sandboxData };
51
+ this.sandboxRows.push(sandboxRow);
52
+ this.updateSandboxRow(sandboxRow);
53
+ sandboxRow.update = () => this.updateSandboxRow(sandboxRow);
54
+ sandboxRow.delete = () => this.deleteSandboxRow(sandboxRow);
55
+ return sandboxRow;
57
56
  }
58
57
 
59
- updateProgressBar(i, { progress=0, label='IDLE', indeterminate=true }={}) {
60
- this.progressBars[i].progressBar.setProgress(progress);
61
- // Add a space because for some reason the last char gets truncated
62
- this.progressBars[i].label.setContent(label + ' ');
58
+ /* Update a single progress bar's appearance, based on sandboxData */
59
+ updateSandboxRow(sandboxRow)
60
+ {
61
+ const { progressBar, text, sandboxData } = sandboxRow;
63
62
 
64
- this.progressBars[i].progressBar.style.bar.bg = indeterminate? 'blue' : 'green';
65
- }
63
+ if (sandboxData.label)
64
+ text.setContent(sandboxData.label + ' ' /* bug - last char gets trunc'd */);
66
65
 
67
- update(data=this.data) {
68
- this.data = data;
69
- for (let i = 0; i < this.data.length; i++) {
70
- this.updateProgressBar(i, this.data[i]);
66
+ if (!sandboxData.slice.number)
67
+ {
68
+ progressBar.style.bar.bg = 'black';
69
+ text.style.fg = 'cyan';
70
+ progressBar.setProgress(100);
71
71
  }
72
+ else if (sandboxData.slice.progress >= 0)
73
+ {
74
+ progressBar.style.bar.bg = 'green';
75
+ text.style.fg = 'white';
76
+ progressBar.setProgress((sandboxData.slice.progress * 0.99) + 1); /* draw the first tick asap */
77
+ }
78
+ else /* indeterminate progress */
79
+ {
80
+ progressBar.style.bar.bg = 'blue';
81
+ text.style.fg = 'white';
82
+ progressBar.setProgress(100);
83
+ }
84
+ }
72
85
 
73
- if (this.data.length < this.progressBars.length) {
74
- for (let i = this.data.length; i < this.progressBars.length; i++) {
75
- this.updateProgressBar(i);
76
- }
86
+ deleteSandboxRow(sandboxRow)
87
+ {
88
+ const idx = this.sandboxRows.indexOf(sandboxRow);
89
+ if (idx === -1)
90
+ return;
91
+ this.sandboxRows.splice(idx, 1);
92
+ for (let i=idx; i < this.sandboxRows.length; i++)
93
+ {
94
+ this.sandboxRows[i].progressBar.position.top -= 1;
95
+ this.sandboxRows[i].text.position.top -= 1;
77
96
  }
78
97
 
79
- this.setLabel(`${this.options.label} (${this.progressBars.length})`);
98
+ sandboxRow.text.destroy();
99
+ sandboxRow.progressBar.destroy();
80
100
  }
81
101
 
82
- // Deletes last progress bar
83
- deleteProgressBar() {
84
- let i = this.progressBars.length - 1;
85
- this.progressBars[i].label.destroy()
86
- this.progressBars[i].progressBar.destroy()
87
- this.progressBars.pop(i)
102
+ /* Update all everything in the progress bar pane */
103
+ update()
104
+ {
105
+ this.sandboxRows.forEach(sandboxRow => this.updateSandboxRow(sandboxRow));
106
+ this.setLabel(`${this.options.label} (${this.sandboxRows.length})`);
107
+ this.screen.render();
88
108
  }
89
109
  }
90
110
 
91
- Object.assign(exports, {
92
- sandboxes(...args) {
93
- return new Sandboxes(...args);
94
- },
95
- });
111
+ /**
112
+ * Factory function for returning a sandbox pane. Wraps the SandboxPane ctor.
113
+ */
114
+ exports.sandboxPaneFactory = function sandboxPaneFactory(...args) {
115
+ return new SandboxPane(...args);
116
+ };
@@ -19,15 +19,14 @@ exports.check = function checkSchedulerVersion$$check(quiet)
19
19
  const schedulerConfig = dcpConfig.scheduler;
20
20
 
21
21
  // Check for old versions of the config
22
- if (!schedulerConfig.worker) {
22
+ if (!schedulerConfig.worker)
23
+ {
23
24
  schedulerConfig.worker = {};
24
25
  schedulerConfig.worker.operations = '1.0.0';
25
- if (dcpConfig.worker.nativeEvaluator){
26
+ if (dcpConfig.worker.nativeEvaluator)
26
27
  schedulerConfig.worker.types = ['v4'];
27
- }
28
- else {
28
+ else
29
29
  schedulerConfig.worker.types = ['v3'];
30
- }
31
30
  }
32
31
 
33
32
  // Check if scheduler supports current worker version
@@ -37,12 +36,13 @@ exports.check = function checkSchedulerVersion$$check(quiet)
37
36
  let currentWorkerType = require('dcp/build').workerType || 'v4';
38
37
  let currentWorkerVersion = require('dcp/build').workerVersion || '1.0.0';
39
38
 
40
- if (parseInt(currentWorkerVersion, 10) !== 1) {
39
+ if (parseInt(currentWorkerVersion, 10) !== 1)
41
40
  throw new Error('This version of dcp-client is not compatible with the standalone worker implemention in this dcp-worker package');
42
- }
43
41
 
44
- if (!Object.values(schedulerConfig.worker.types).includes(currentWorkerType) ||
45
- !require('semver').satisfies(schedulerConfig.worker.operations, '^'+currentWorkerVersion)) {
42
+ if (false
43
+ || !Object.values(schedulerConfig.worker.types).includes(currentWorkerType)
44
+ || !require('semver').satisfies(schedulerConfig.worker.operations, '^'+currentWorkerVersion))
45
+ {
46
46
  console.error('\b**** Please update ****\b');
47
47
  console.error('The selected scheduler is not capable of running this worker version.');
48
48
  console.log(`Scheduler href: ${dcpConfig.scheduler.location.href}\n` +
@@ -17,21 +17,21 @@ const contrib = require('blessed-contrib');
17
17
  const components = require('./blessed-components');
18
18
  const utils = require('../lib/utils');
19
19
 
20
- const { replaceWorkerEvent, replaceSandboxEvent } = require('./default-ui-events');
20
+ const { replaceWorkerEventHandler, replaceSandboxEventHandler, newSandboxCallbacks } = require('./default-ui-events');
21
21
 
22
22
  const SLICE_FETCH_STATUS = {
23
23
  IDLE: chalk.yellow('Idle'),
24
24
  FETCHING: chalk.blue('Fetching Work...'),
25
25
  WORKING: chalk.green('Working'),
26
26
  NO_WORK: chalk.red('No Work Available'),
27
- }
27
+ };
28
28
 
29
29
  const usingDebugger = require('module')._cache.niim instanceof require('module').Module;
30
30
  const screenConf = {
31
31
  input: usingDebugger ? new (require('events').EventEmitter) : undefined,
32
32
  output: usingDebugger ? new (require('events').EventEmitter) : undefined,
33
33
  };
34
- /**
34
+ /**
35
35
  * Initialize the blessed dashboard
36
36
  * @param {Worker} worker Reference to the DCP Worker
37
37
  * @param {object} options Options which may affect behaviour. Not currently used.
@@ -40,23 +40,25 @@ exports.init = function dashboard$$init(worker, options)
40
40
  {
41
41
  var sliceFetchStatus = SLICE_FETCH_STATUS.IDLE;
42
42
  var totalDCCs = 0;
43
- const screen = blessed.screen(screenConf);
44
- const grid = new contrib.grid({rows: 3, cols: 5, screen});
43
+ var screen = blessed.screen(screenConf);
44
+
45
+ worker.on('end', () => {
46
+ screen.destroy();
47
+ screen = false;
48
+ });
49
+ process.on('exit', () => {
50
+ if (screen)
51
+ screen.destroy();
52
+ });
53
+
54
+ const grid = new contrib.grid({ rows: 3, cols: 5, screen }); // eslint-disable-line new-cap
45
55
  const workerInfoPane = grid.set(2, 0, 1, 5, blessed.text);
46
-
47
56
  const logPane = grid.set(0, 2, 2, 3, components.log, {
48
57
  label: 'Worker Log',
49
- scrollable: true,
50
- alwaysScroll: true,
51
- mouse: true,
52
- scrollbar: {
53
- bg: 'blue',
54
- },
58
+ scrollbar: { bg: 'blue' },
55
59
  });
56
-
57
- const sandboxPane = grid.set(0, 0, 2, 2, components.sandboxes, {
60
+ const sandboxPane = grid.set(0, 0, 2, 2, components.sandboxPaneFactory, {
58
61
  label: 'Sandboxes',
59
- defaultProgressBars: 0,
60
62
  scrollable: true,
61
63
  alwaysScroll: true,
62
64
  mouse: true,
@@ -64,7 +66,6 @@ exports.init = function dashboard$$init(worker, options)
64
66
  bg: 'blue',
65
67
  },
66
68
  });
67
-
68
69
  const passwordBox = blessed.textbox({
69
70
  parent: screen,
70
71
  border: 'line',
@@ -81,6 +82,8 @@ exports.init = function dashboard$$init(worker, options)
81
82
  hidden: true,
82
83
  });
83
84
 
85
+ global.tui = { workerInfoPane, logPane, sandboxPane, screen, grid, passwordBox };
86
+
84
87
  function askPassword(promptMessage)
85
88
  {
86
89
  return new Promise((resolve, reject) => {
@@ -91,6 +94,7 @@ exports.init = function dashboard$$init(worker, options)
91
94
  function passwordSubmitFn(value)
92
95
  {
93
96
  passwordBox.hide();
97
+ screen.render();
94
98
  passwordBox.removeListener('submit', passwordSubmitFn);
95
99
  passwordBox.setValue('');
96
100
  resolve(value);
@@ -110,24 +114,19 @@ exports.init = function dashboard$$init(worker, options)
110
114
 
111
115
  if (!usingDebugger)
112
116
  exports.logPane = logPane; /* now dashboard log can find the pane */
113
- setInterval(() => screen.render(), 50).unref(); /* 50ms = 20 fps */
117
+ setInterval(() => screen.render(), 2000).unref(); /* ensure we didn't forget to render an important update */
114
118
  updateWorkerInfo();
115
- screen.render();
116
-
119
+
117
120
  /* Apply key bindings which mimic canonical input mode */
118
121
  screen.key(['C-c'], () => raise('SIGINT'));
119
122
  screen.key(['C-z'], () => raise('SIGTSTP'));
120
123
  screen.key(['\u001c'], () => raise('SIGQUIT')); /* C-\ */
121
-
122
- screen.key(['escape'], () => {
123
- console.log('Stopping worker...');
124
- worker.stop();
125
- });
124
+ screen.key(['escape'], () => raise('SIGINT'));
126
125
 
127
126
  function updateWorkerInfo()
128
127
  {
129
128
  const workerOptions = worker.workerOptions;
130
-
129
+
131
130
  workerInfoPane.setLabel(`Worker Status [${sliceFetchStatus}]`);
132
131
  workerInfoPane.setContent([
133
132
  chalk.green(` DCCs Earned: ${chalk.bold(totalDCCs.toFixed(3))}`),
@@ -140,64 +139,48 @@ exports.init = function dashboard$$init(worker, options)
140
139
  ` Priv Groups: ${Object.keys(workerOptions.computeGroups).length}`,
141
140
  ` Pub Group: ${workerOptions.leavePublicGroup ? 'no' : 'yes'}`,
142
141
  ].join('\n'));
142
+ screen.render();
143
143
  }
144
144
 
145
145
  /* Override default event behaviour to work better with the Dashboard. */
146
-
147
- /** XXXpfr @todo Is this correct? Or should we init progressData inside 'slice' like we used to. */
148
- replaceSandboxEvent('ready', function dashboard$$job(sandbox, sandboxData, ev) {
149
- sandboxData.progressData = {
150
- indeterminate: true,
151
- progress: 0,
152
- label: sandbox?.public ? sandbox.public.name: '<no-label>',
153
- };
146
+ replaceSandboxEventHandler('ready', function dashboard$$sandboxReady(sandbox, sandboxData) {
147
+ if (!sandboxData.label)
148
+ sandboxData.label = '<ready>';
149
+ sandboxData.slice.number = 0;
150
+ sandboxPane.update();
154
151
  });
155
-
156
- replaceSandboxEvent('slice', function dashboard$$slice(sandbox, sandboxData, ev) {
157
- sandboxPane.data.push(sandboxData.progressData);
152
+
153
+ replaceSandboxEventHandler('job', function dashboard$$sandboxJob(sandbox, sandboxData, job) {
154
+ sandboxData.job = job;
155
+ sandboxData.label = job.name ? `${job.name} ${job.address.slice(0,8)}` : `<job ${job.address}>`;
156
+ if (job.description)
157
+ sandboxData.label += ': ' + job.description;
158
158
  sandboxPane.update();
159
159
  });
160
160
 
161
- replaceSandboxEvent('progress', function dashboard$$progress(sandbox, sandboxData, ev) {
162
- if (!ev)
163
- {
164
- sandboxData.progressData.progress = 100;
165
- setTimeout(() => {
166
- if (sandboxData.progressData.indeterminate)
167
- {
168
- sandboxData.progressData.progress = 0;
169
- sandboxPane.update();
170
- }
171
- }, 500).unref();
172
- }
173
- else
174
- {
175
- sandboxData.progressData.progress = ev;
176
- sandboxData.progressData.indeterminate = false;
177
- }
161
+ replaceSandboxEventHandler('slice', function dashboard$$slice(sandbox, sandboxData, sliceNumber) {
162
+ sandboxData.slice.number = sliceNumber;
163
+ });
178
164
 
179
- sandboxPane.update();
165
+ replaceSandboxEventHandler('progress', function dashboard$$progress(sandbox, sandboxData, progress) {
166
+ sandboxData.slice.progress = progress;
180
167
  });
181
-
182
- replaceSandboxEvent('sliceEnd', function dashboard$$sliceEnd(sandbox, sandboxData, ev) {
183
- sandboxPane.data = sandboxPane.data.filter(d => d != sandboxData.progressData);
184
- sandboxData.progressData.progress = 0;
185
- sandboxPane.update();
168
+
169
+ replaceSandboxEventHandler('sliceEnd', function dashboard$$sliceEnd(sandbox, sandboxData, sliceNumber) {
170
+ sandboxData.slice.progress = 100;
186
171
  });
187
172
 
188
- replaceSandboxEvent('end', function dashboard$$end(sandbox, sandboxData, ev) {
189
- sandboxPane.data = sandboxPane.data.filter(d => d != sandboxData.progressData);
190
- sandboxPane.deleteProgressBar();
191
- sandboxData.progressData.progress = 0;
173
+ replaceSandboxEventHandler('end', function dashboard$$end(sandbox, sandboxData) {
174
+ sandboxPane.deleteSandboxRow(sandboxData.sandboxRow);
192
175
  sandboxPane.update();
193
176
  });
194
177
 
195
- replaceWorkerEvent('beforeFetch', function dashboard$$beforeFetch(ev) {
178
+ replaceWorkerEventHandler('beforeFetch', function dashboard$$beforeFetch(ev) {
196
179
  sliceFetchStatus = SLICE_FETCH_STATUS.FETCHING;
197
180
  updateWorkerInfo();
198
181
  });
199
-
200
- replaceWorkerEvent('fetch', function dashboard$$fetch(ev) {
182
+
183
+ replaceWorkerEventHandler('fetch', function dashboard$$fetch(ev) {
201
184
  sliceFetchStatus = SLICE_FETCH_STATUS.NO_WORK;
202
185
  if (ev instanceof Error)
203
186
  console.error('Error fetching slices:', ev);
@@ -206,16 +189,50 @@ exports.init = function dashboard$$init(worker, options)
206
189
  updateWorkerInfo();
207
190
  });
208
191
 
209
- worker.on('end', () => { screen.destroy(); });
192
+ /* Sandbox Data
193
+ * label - text that describes the sandbox
194
+ * slice - defined on first slice
195
+ * .number - holds current slice
196
+ * .progress - last progress update for this slice, 0 on new slice if last slice had numeric progress
197
+ * sandboxRow - TUI element from lib/blessed-components/sandboxes.js
198
+ * .text - text element
199
+ * .progressBar - progress bar element
200
+ * .sandboxData - this sandbox data object
201
+ */
202
+ function initSandboxData(_worker, sandbox, sandboxData)
203
+ {
204
+ sandboxData.seq = initSandboxData.seq = (initSandboxData.seq || 0) + 1;
205
+ sandboxData.slice = {};
206
+ sandboxData.sandboxRow = sandboxPane.createSandboxRow(sandboxData);
207
+
208
+ /* Updating an autoUpdate property cases that change to be immediately reflected in the
209
+ * data that blessed uses to draw the TUI dashboard. We build this object after initializing
210
+ * the sandbox row to keep race conditions at bay.
211
+ */
212
+ function autoUpdate(prop)
213
+ {
214
+ Object.defineProperty(autoUpdate.object, prop, {
215
+ get: () => autoUpdate.storage[prop],
216
+ set: (value) => {
217
+ autoUpdate.storage[prop] = value;
218
+ sandboxData.sandboxRow.update();
219
+ },
220
+ enumerable: true,
221
+ });
222
+ }
210
223
 
211
- worker.on('sandbox', function dashboard$$sandbox(ev) {
212
- sandboxPane.createProgressBar();
213
- sandboxPane.update();
214
- });
224
+ autoUpdate.storage = sandboxData.slice;
225
+ sandboxData.slice = autoUpdate.object = {};
226
+ autoUpdate('progress');
227
+ autoUpdate('number');
228
+ sandboxData.sandboxRow.update();
229
+ }
230
+ newSandboxCallbacks.push(initSandboxData);
215
231
 
232
+ worker.on('job', job => console.log(`new job: ${job.name} ${job.address.slice(0,8)} ${job.description || ''} ${job.link || ''}`));
216
233
  worker.on('payment', function dashboard$$payment(ev) {
217
234
  const payment = parseFloat(ev);
218
-
235
+
219
236
  if (!isNaN(payment))
220
237
  totalDCCs += payment;
221
238
 
@@ -26,23 +26,23 @@
26
26
  */
27
27
  'use strict';
28
28
 
29
- const utils = require('../lib/utils');
29
+ const { slicesFetched, debugging } = require('./utils');
30
30
 
31
31
  const sandboxEventHandlers = {};
32
32
  const workerEventHandlers = {};
33
33
 
34
- /**
35
- * Sandbox 1: Slice Started: slice 1, 0x5b5214D48F0428669c4E: Simple Job
36
- * Sandbox 1: Slice Completed: slice 1, 0x5b5214D48F0428669c4E: Simple Job: dt 114ms
37
- */
34
+ function placeholder()
35
+ {
36
+ /* event target for events not used by the default loggers */
37
+ }
38
38
 
39
39
  /**
40
- * Hook a worker's events
40
+ * Hook a worker's events and the events of the sandboxes it creates.
41
41
  *
42
42
  * @param worker The instance of Worker to hook
43
43
  * @param options cliArgs from worker
44
44
  */
45
- exports.hook = function hookWorkerEvents$$hook(worker, options)
45
+ exports.hook = function defaultUiEvents$$hook(worker, options)
46
46
  {
47
47
  const sliceMap = {}; // jobAddress --> ( sliceNumber, t0 )
48
48
  const truncationLength = 22; // Extra 2 for '0x'
@@ -71,9 +71,8 @@ exports.hook = function hookWorkerEvents$$hook(worker, options)
71
71
  console.log(` . Sandbox ${sandboxData.shortId}: Slice Started: ${makeSliceId(sandbox)}`);
72
72
  };
73
73
 
74
- sandboxEventHandlers.progress = function progressHandler(sandbox, sandboxdData, ev) {
75
- // Overridden in dashboard-tui.js
76
- };
74
+ sandboxEventHandlers.progress = placeholder;
75
+ sandboxEventHandlers.job = placeholder;
77
76
 
78
77
  sandboxEventHandlers.sliceEnd = function sliceEndHandler(sandbox, sandboxData, ev) {
79
78
  const sliceInfo = sliceMap[sandbox.id];
@@ -104,19 +103,18 @@ exports.hook = function hookWorkerEvents$$hook(worker, options)
104
103
 
105
104
  workerEventHandlers.fetch = function fetchHandler(ev) {
106
105
  if (ev instanceof Error)
107
- console.error(' ! Failed to fetch slices:', ev);
106
+ debugging() && options.verbose && console.error(' ! Failed to fetch slices:', ev); // redundant
108
107
  else
109
- options.verbose && console.log(' . Fetched', utils.slicesFetched(ev), 'slices');
108
+ options.verbose && console.log(' . Fetched', slicesFetched(ev), 'slices');
110
109
  };
111
110
 
112
-
113
111
  workerEventHandlers.beforeResult = function beforeResultHandler() {
114
112
  options.verbose >= 2 && console.log(' * Submitting results...');
115
113
  };
116
114
 
117
115
  workerEventHandlers.result = function resultHandler(ev) {
118
116
  if (ev instanceof Error)
119
- console.error(" ! Failed to submit results:", ev);
117
+ debugging() && options.verbose >= 2 && console.error(" ! Failed to submit results:", ev); // redundant
120
118
  else
121
119
  options.verbose >= 2 && console.log(' . Submitted');
122
120
  };
@@ -133,11 +131,11 @@ exports.hook = function hookWorkerEvents$$hook(worker, options)
133
131
  worker.on(eventName, (...args) => workerEventHandlers[eventName](...args));
134
132
 
135
133
  worker.on('sandbox', function newSandboxHandler(sandbox) {
136
- const sandboxData = {
137
- shortId: sandbox.id.toString(10).padStart(3)
138
- };
134
+ const sandboxData = { shortId: sandbox.id.toString(10).padStart(3) };
139
135
  for (let eventName in sandboxEventHandlers)
140
136
  sandbox.on(eventName, (...args) => sandboxEventHandlers[eventName](sandbox, sandboxData, ...args));
137
+ for (let callback of exports.newSandboxCallbacks)
138
+ callback(worker, sandbox, sandboxData);
141
139
  });
142
140
 
143
141
  exports.sandboxEventHandlers = sandboxEventHandlers;
@@ -150,7 +148,7 @@ exports.hook = function hookWorkerEvents$$hook(worker, options)
150
148
  * @param {string} eventName name of the event to replace
151
149
  * @param {function} eventHandler new event handler
152
150
  */
153
- exports.replaceWorkerEvent = function hookWorkerEvents$$replace(eventName, eventHandler)
151
+ exports.replaceWorkerEventHandler = function defaultUiEvents$$replaceWorkerEventHandler(eventName, eventHandler)
154
152
  {
155
153
  if (!workerEventHandlers.hasOwnProperty(eventName))
156
154
  throw new Error('unknown worker event: ' + eventName + `(${Object.keys(workerEventHandlers).join(', ')})`);
@@ -164,10 +162,16 @@ exports.replaceWorkerEvent = function hookWorkerEvents$$replace(eventName, event
164
162
  * @param {string} eventName name of the event to replace
165
163
  * @param {function} eventHandler new event handler
166
164
  */
167
- exports.replaceSandboxEvent = function hookSandboxEvents$$replace(eventName, eventHandler)
165
+ exports.replaceSandboxEventHandler = function defaultUiEvents$$replaceSandboxEventHandler(eventName, eventHandler)
168
166
  {
169
167
  if (!sandboxEventHandlers.hasOwnProperty(eventName))
170
168
  throw new Error('unknown sandbox event: ' + eventName + `(${Object.keys(sandboxEventHandlers).join(', ')})`);
171
169
 
172
170
  sandboxEventHandlers[eventName] = eventHandler;
173
171
  }
172
+
173
+ /**
174
+ * Functions which are fired every time the worker creates a new sandbox.
175
+ * Each function receives as its arguments worker, sandbox, sandboxData.
176
+ */
177
+ exports.newSandboxCallbacks = [];
@@ -25,28 +25,29 @@
25
25
 
26
26
  const path = require('path');
27
27
  const fs = require('fs');
28
+ const { debugging } = require('./utils');
28
29
  var dcpConfig;
29
30
  var mainEval;
30
31
  var ci;
31
32
 
32
33
  require('dcp-client'); /* plumb in modules from bundle even if library has not been initialized */
33
- const debugging = require('dcp/internal/debugging').scope('dcp-worker');
34
34
 
35
35
  function daemonEval()
36
36
  {
37
- try {
37
+ try
38
+ {
38
39
  if (typeof dcpConfig === 'undefined')
39
40
  dcpConfig = require('dcp/dcp-config');
40
41
  }
41
- catch(e)
42
- {}
42
+ catch(e) {} // eslint-disable-line no-empty
43
43
 
44
44
  if (mainEval)
45
45
  return mainEval(arguments[0]);
46
46
  return eval(arguments[0]); /* eslint-disable-line no-eval */
47
47
  }
48
48
 
49
- function callbackTelnet(port, client, registry) {
49
+ function callbackTelnet(port, client, registry)
50
+ {
50
51
  client.unref();
51
52
  debugging() && console.notice(' ! telnetd - listening on port', port);
52
53
  }
package/lib/utils.js CHANGED
@@ -8,6 +8,8 @@
8
8
  */
9
9
  'use strict';
10
10
 
11
+ const process = require('process');
12
+
11
13
  /**
12
14
  * Figure out #slices fetched from the different forms of the 'fetch' event.
13
15
  * @param {*|string|number} task
@@ -15,10 +17,6 @@
15
17
  */
16
18
  function slicesFetched (task)
17
19
  {
18
- if (typeof task === 'number') /* <= June 2023 Worker events: remove ~ Sep 2023 /wg */
19
- return task;
20
- if (typeof task === 'string') /* <= June 2023 Worker events: remove ~ Sep 2023 /wg */
21
- return parseInt(task, 10) || 0;
22
20
  /* eslint-disable-next-line no-shadow */
23
21
  let slicesFetched = 0;
24
22
  for (const job in task.slices)
@@ -26,4 +24,23 @@ function slicesFetched (task)
26
24
  return slicesFetched;
27
25
  }
28
26
 
27
+ /** thunk - ensures global debugging() symbol always available even if called before dcp-client init */
28
+ function debugging()
29
+ {
30
+ require('dcp-client');
31
+ debugging = require('dcp/internal/debugging').scope('dcp-worker'); // eslint-disable-line no-func-assign
32
+ return debugging.apply(this, arguments); // eslint-disable-line no-invalid-this
33
+ }
34
+
35
+ /**
36
+ * Flag to display detailed debug info in diagnostics.
37
+ * @return {boolean}
38
+ */
39
+ function displayMaxDiagInfo ()
40
+ {
41
+ return Boolean(process.env.DCP_SUPERVISOR_DEBUG_DISPLAY_MAX_INFO) || debugging() || false;
42
+ }
43
+
29
44
  exports.slicesFetched = slicesFetched;
45
+ exports.debugging = debugging;
46
+ exports.displayMaxDiagInfo = displayMaxDiagInfo;
@@ -9,7 +9,6 @@
9
9
  'use strict';
10
10
 
11
11
  const chalk = require('chalk');
12
- const _console = new (require('console').Console)(process);
13
12
 
14
13
  /**
15
14
  * Initialize the Blessed dashboard
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dcp-worker",
3
- "version": "3.3.7",
3
+ "version": "3.3.9-0",
4
4
  "description": "JavaScript portion of DCP Workers for Node.js",
5
5
  "main": "bin/dcp-worker",
6
6
  "keywords": [
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "git@github.com:Distributed-Compute-Labs/dcp-worker.git"
15
+ "url": "git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-worker.git"
16
16
  },
17
17
  "license": "MIT",
18
18
  "author": "Kings Distributed Systems",
@@ -40,7 +40,7 @@
40
40
  "blessed": "^0.1.81",
41
41
  "blessed-contrib": "4.11.0",
42
42
  "chalk": "^4.1.0",
43
- "dcp-client": "4.4.7",
43
+ "dcp-client": "4.4.9-0",
44
44
  "kvin": "^1.2.7",
45
45
  "posix-getopt": "^1.2.1",
46
46
  "semver": "^7.3.8",