dcp-worker 4.4.4 → 4.5.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/.gitlab-ci.yml CHANGED
@@ -17,6 +17,7 @@ default:
17
17
  image: node:lts
18
18
  before_script:
19
19
  - npm add --global npm@latest
20
+ - npm config set allow-git=all
20
21
  # Access private deps. e.g., `dcp-client` from GitLab.
21
22
  - git config --global
22
23
  url."${CI_SERVER_PROTOCOL}://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}:${CI_SERVER_PORT}".insteadOf
@@ -26,45 +26,36 @@ process.stdout.write(`
26
26
  *** This program collects information from your computer, and sends it to
27
27
  *** Distributive and our analytics partners. This includes your local DCP
28
28
  *** configuration, which might include sensitive information such as Compute
29
- *** Group credentials, or API keys.
29
+ *** Group information, or API keys.
30
30
 
31
31
  `);
32
32
 
33
- while (argv[0])
33
+ for (; argv.length; argv.shift())
34
34
  {
35
- if (argv[0] === '-f' || argv[0] === '--force')
36
- {
37
- argv.shift();
38
- force = true;
39
- continue;
40
- }
41
-
42
- if (argv[0] === '-s')
43
- {
44
- syslogUrl = argv[0];
45
- argv.shift();
46
- continue;
47
- }
48
-
49
35
  if (argv[0].startsWith('--syslog='))
50
- {
51
36
  syslogUrl = argv[0].slice(0);
52
- argv.shift();
37
+ else if (argv[0].startsWith('--dcp-'))
53
38
  continue;
39
+ else if (argv[0] === '-f' || argv[0] === '--force')
40
+ force = true;
41
+ else if (argv[0] === '-s')
42
+ {
43
+ syslogUrl = argv[1];
44
+ shift();
54
45
  }
55
-
56
- break;
46
+ else if (argv.length === 1)
47
+ email = argv[0];
48
+ else
49
+ throw new Error('invalid argument: ' + argv[0]);
57
50
  }
58
51
 
59
52
  if (force)
60
53
  {
61
54
  go();
62
- email = argv[0] || 'wes+worker-debug-info@distributive.network';
55
+ email = email || 'dcp+worker-debug-info@distributive.network';
63
56
  }
64
57
  else
65
58
  {
66
- email = argv[0];
67
-
68
59
  const rl = readline.createInterface({
69
60
  input: process.stdin,
70
61
  output: process.stdout
@@ -100,8 +91,6 @@ async function emailReport(report)
100
91
  {
101
92
  const protocol = require('dcp/protocol');
102
93
  const worker = require('dcp/worker');
103
-
104
- const conn = new protocol.Connection(dcpConfig.scheduler.services.mailgw);
105
94
  const message = {
106
95
  from: `${os.userInfo().username}-${worker.DistributiveWorker.obtainWorkerId()}-worker-${os.hostname()}@distributive.network`,
107
96
  fromName: 'collect-debug-info',
@@ -112,6 +101,15 @@ async function emailReport(report)
112
101
  body: `<html><pre>${report}</pre></html>`
113
102
  };
114
103
 
104
+ if (email === '-')
105
+ {
106
+ console.log(report);
107
+ return;
108
+ }
109
+
110
+ const mailgw = dcpConfig.scheduler.services.mailgw;
111
+ console.log(` - sending email via ${mailgw.location}`);
112
+ const conn = new protocol.Connection(mailgw);
115
113
  const response = await conn.request('sendmail', message);
116
114
  conn.close();
117
115
  console.log(' - sent report to', message.to);
@@ -126,7 +124,7 @@ async function cmd(prog, args)
126
124
  argv.unshift(prog);
127
125
  argv.unshift('/c');
128
126
  const s = execFileSync('cmd', argv, { encoding: 'utf8' });
129
- return `------ [ ${prog} ] ---------------------------------------------------------------\n${s}`;
127
+ return `------ [ ${prog} ] ------\n${s}`;
130
128
  }
131
129
 
132
130
  async function sh(prog, args)
@@ -134,7 +132,7 @@ async function sh(prog, args)
134
132
  const argv = args.slice();
135
133
  console.log(' -', prog, argv.join(' '));
136
134
  const s = execFileSync(prog, argv, { encoding: 'utf8', maxBuffer: 1e9 });
137
- return `------ [ ${prog} ] ---------------------------------------------------------------\n${s}`;
135
+ return `------ [ ${prog} ] ------\n${s}`;
138
136
  }
139
137
 
140
138
  async function go()
@@ -147,9 +145,29 @@ async function go()
147
145
  worker: { logging: { syslog: {} } },
148
146
  }
149
147
  });
148
+ const { DcpURL } = require('dcp/dcp-url');
149
+ DcpURL.prototype.toJSON = function() { return { href: this.href } };
150
+ URL.prototype.toJSON = function() { return { href: this.href } };
151
+ const dcpConfigNoCreds = Object.assign({}, dcpConfig);
152
+ dcpConfigNoCreds.worker = Object.assign({}, dcpConfigNoCreds.worker);
153
+ delete dcpConfigNoCreds.worker.computeGroups;
154
+
155
+ for (let serviceGroup in dcpConfig)
156
+ {
157
+ if (dcpConfig[serviceGroup].location)
158
+ dcpConfig[serviceGroup].location = new URL(dcpConfig[serviceGroup].location);
159
+ if (dcpConfig[serviceGroup].services)
160
+ {
161
+ for (let service in dcpConfig[serviceGroup].services)
162
+ {
163
+ if (dcpConfig[serviceGroup].services[service].location)
164
+ dcpConfig[serviceGroup].services[service].location = new URL(dcpConfig[serviceGroup].services[service].location);
165
+ }
166
+ }
167
+ }
150
168
 
151
169
  console.log(' - host information');
152
- var report = `------ [ ${new Date()} ] ---------------------------------------------------------------
170
+ var report = `------ [ ${new Date()} ] ------
153
171
  os: ${os.version()}
154
172
  platform: node ${process.version} ${os.platform()} ${os.release()} ${os.machine()} ${os.endianness()}
155
173
  uptime: ${Math.floor(os.uptime() / 86400)} days ${Number(((os.uptime() / 86400) % 1) * 86400).toFixed(1)} seconds, load average: ${os.loadavg().join(', ')}
@@ -161,11 +179,12 @@ cpus: ${JSON.stringify(os.cpus, null, 2)}
161
179
  network: ${JSON.stringify(os.networkInterfaces, null, 2)}
162
180
  more versions: ${JSON.stringify(process.versions, null, 2)}
163
181
  `;
164
-
182
+ console.log(' - worker information');
183
+ report += await createWorkerReport();
165
184
  console.log(' - dcp information');
166
185
  report +=
167
186
  `
168
- dcpConfig: ${JSON.stringify(dcpConfig, null, 2)}
187
+ dcpConfig: ${JSON.stringify(dcpConfigNoCreds, null, 2)}
169
188
  `;
170
189
 
171
190
  if (os.platform() === 'win32')
@@ -190,3 +209,27 @@ dcpConfig: ${JSON.stringify(dcpConfig, null, 2)}
190
209
  else if (syslogUrl)
191
210
  syslogReport(report);
192
211
  }
212
+
213
+ async function createWorkerReport()
214
+ {
215
+ const identity = require('dcp/identity');
216
+ const wallet = require('dcp/wallet');
217
+ const utils = require('../lib/utils');
218
+ const { DistributiveWorker } = require('dcp/worker');
219
+ const worker = new DistributiveWorker(Object.assign({
220
+ unmanaged: true
221
+ }, dcpConfig.worker), Function).unref();
222
+ identity.interactive = false;
223
+ const rootIdentity = await wallet.getId();//identity.get();
224
+ await identity.masquerade(DistributiveWorker.obtainWorkerId(), '$login');
225
+ await worker.configure();
226
+
227
+ return `
228
+ root identity: ${rootIdentity.address}
229
+ masquerade: ${identity.get().address}
230
+ worker id: ${worker.id}
231
+ earnings acct: ${worker.config.paymentAddress}
232
+ join keys: ${Object.values(worker.config.computeGroups).map(cg => cg.joinKey)}
233
+ worker config: ${JSON.stringify(Object.assign({}, worker.config, { computeGroups: 'HIDDEN' }), null, 2)}
234
+ `
235
+ }
@@ -19,10 +19,10 @@
19
19
  */
20
20
  'use strict';
21
21
 
22
+ const net = require('mux-net').hook();
22
23
  const os = require('os');
23
24
  const fs = require('fs');
24
25
  const path = require('path');
25
- const net = require('net');
26
26
  const dns = require('dns');
27
27
  const debug = require('debug');
28
28
  const pidfile = require('../lib/pidfile');
@@ -64,6 +64,10 @@ const daemonConfig = {
64
64
 
65
65
  async function processExit(code)
66
66
  {
67
+ if (processExit.p$exiting)
68
+ return processExit.p$exiting;
69
+ processExit.p$exiting = new Promise(()=>1); /* never resolves, other "thread" will exit program */
70
+
67
71
  if (typeof code === 'undefined')
68
72
  code = process.exitCode;
69
73
  console.debug('Exit, code', code);
@@ -80,7 +84,7 @@ Released under the terms of the MIT License.
80
84
 
81
85
  Usage: ${process.argv[0]} [-h | --help] [--prefix=<dir>] [--pts=<dir>]
82
86
  [--disable-monitor=<session|screensaver|signal>] [-i <timeout>]
83
- [-p port] [-H hostname]
87
+ [-p port] [-H hostname[,hostname...]]
84
88
  [-a] [-l | --max-load=<number>] [-r | --rate=<number>] [-L]
85
89
  [-s | --signal=[name or number][,<toggle|quick>]]
86
90
  [-T | --loadavg-type=<short,medium,long,random>]
@@ -127,32 +131,56 @@ Environment:
127
131
  */
128
132
  async function listenForConnections(config)
129
133
  {
130
- var server = net.createServer((socket) => handleConnection(socket, config));
131
- var hostaddr;
134
+ const server = net.createServer((socket) => handleConnection(socket, config));
135
+ const hostnames = config.net.hostnames || [ config.net.hostname ];
136
+ const hosts = [];
132
137
 
133
- if (config.net.hostname === 'localhost')
134
- hostaddr = '::';
138
+ if (!hostnames[0])
139
+ {
140
+ hosts[0] = '::1';
141
+ hosts[1] = '127.0.0.1';
142
+ }
135
143
  else
136
- hostaddr = (await dns.promises.lookup(config.net.hostname, { family: 4 })).address || config.net.hostname;
144
+ {
145
+ for (let host of hostnames)
146
+ {
147
+ try
148
+ {
149
+ const hostaddrs = await dns.promises.lookup(host, { all: true, order: 'verbatim' });
150
+ for (let hostaddr of hostaddrs)
151
+ hosts.push(hostaddr.address);
152
+ }
153
+ catch(error)
154
+ {
155
+ if (error.code === 'EAI_AGAIN')
156
+ console.error(`hostname ${host} not found - check /etc/hosts or DNS configuration`);
157
+ else
158
+ console.error(`error ${error.code} resolving host ${host}:`, error.message);
159
+ await processExit(4);
160
+ }
161
+ }
162
+ }
137
163
 
138
164
  server.on('error', (error) => {
139
165
  delete error.stack;
166
+ if (processExit.p$exiting)
167
+ return;
140
168
  console.error(error);
141
169
  processExit(2);
142
170
  });
143
171
 
144
- server.listen({ host: hostaddr, port: config.net.port }, function daemonReady() {
145
- console.log('Listening for connections on', server._connectionKey);
172
+ console.log('Evaluator command is', config.proc, config.argv);
173
+ server.listen({ hosts, port: config.net.port }, function daemonReady() {
174
+ // eslint-ignore-block no-invalid-this
175
+ console.log(`Listening for connections on ${config.net.hostname} (${this._connectionKey})`);
146
176
  if (Number(config.net.port) === 0)
147
177
  {
148
178
  console.log('Using dynamic port', server.address().port);
149
179
  // Send info by IPC, if the spawn is setup for that. Probably don't need to try/catch.
150
180
  /* WHAT IS THIS FOR? /wg feb 2026 */
151
181
  if (process.send)
152
- try { process.send(server.address().port); } catch (e) {}
182
+ try { process.send(server.address().port); } catch (e) { e; 'dontcare'; }
153
183
  }
154
-
155
- console.log('Evaluator command is', config.proc, config.argv);
156
184
  });
157
185
  }
158
186
 
@@ -430,7 +458,7 @@ async function dbusScreenSaverMonitor()
430
458
  {
431
459
  const prettyName = chosen.split('.')[1];
432
460
  console.error(`${prettyName} screensaver doesn't support activeness query, screensaver is not started, or user ${os.userInfo().username} doesn't have access to dbus\n ${error}`);
433
- processExit(3);
461
+ await processExit(3);
434
462
  }
435
463
  }
436
464
 
@@ -654,7 +682,9 @@ async function main()
654
682
 
655
683
  case 'H':
656
684
  {
657
- daemonConfig.net.hostname = optarg;
685
+ delete daemonConfig.net.hostname;
686
+ daemonConfig.net.hostnames = optarg.split(/[, ]+/);
687
+ console.log(daemonConfig.net.hostnames);
658
688
  break;
659
689
  }
660
690
 
@@ -763,7 +793,8 @@ async function main()
763
793
  }
764
794
 
765
795
  /* Initialize dcp-client to use only local resources before launching the main function */
766
- try {
796
+ try
797
+ {
767
798
  process.on('SIGHUP', () => processExit( 1 - 128));
768
799
  process.on('SIGINT', () => processExit( 2 - 128));
769
800
  process.on('SIGTERM', () => processExit(15 - 128));
@@ -641,7 +641,7 @@ async function main()
641
641
  console.log('Worker Id: ', workerId);
642
642
  console.log('Max Sandboxes: ', workerConfig.maxSandboxes);
643
643
  console.log('Cores: ', Object.assign({}, workerConfig.cores));
644
- console.log('Utilization: ', workerConfig.utilization);
644
+ console.log('Utilization: ', Object.assign({}, workerConfig.utilization));
645
645
  console.log('DCP Address: ', String(identity.get().address));
646
646
 
647
647
  if (!task)
@@ -705,7 +705,4 @@ async function main()
705
705
  }
706
706
 
707
707
  /* Initialize dcp-client to use only local resources before launching the main function */
708
- require('dcp-client').init({
709
- programName: 'dcp-worker',
710
- parseArgv: false,
711
- }).then(main);
708
+ require('dcp-client').init({ programName: 'dcp-worker' }).then(main);
package/bin/dcp-worker CHANGED
@@ -116,6 +116,12 @@ Options:
116
116
  --disable-httpd Disable management service
117
117
  --management-hostname= Override the management service hostname
118
118
  --management-port= Override the management service port
119
+ --management-path= Management service will listen on a unix
120
+ domain socket instead of a tcp socket,
121
+ setting this overrides options
122
+ --management-hostname and --management-port.
123
+ In addition, it will break all webpages that
124
+ come with the management daemon.
119
125
  --register=key[,label] Use the given key to register this worker
120
126
  with your Distributive account and exit
121
127
  --unmanaged Do not report statistics to or read config
@@ -148,8 +154,8 @@ function* parseCliOptions(argv)
148
154
  + 'h(help)*:(earnings-account)*:(identity-keystore)'
149
155
  + 'c:(cores)u:(utilization)m:(max-sandboxes)H:(hostname)p:(port)'
150
156
  + 'o:(output)l:(logger)*:(logfile)*(overwrite-logfile)*:(syslog)*:(min-level)j:(job)g:(join)'
151
- + 'a:(allow-origin)*(zero-trust)*(no-global)*(kvin)*(json)*:(show)*'
152
- + '(unmanaged)*:(management-hostname)*:(management-port)'
157
+ + 'a:(allow-origin)*(zero-trust)*(no-global)*(kvin)*(json)*:(show)'
158
+ + '*(unmanaged)*:(management-hostname)*:(management-port)*:(management-path)'
153
159
  + '*(no-masquerade)*:(force-worker-id)*:(lookup-worker-id)'
154
160
  + '*(no-claim-earnings)*:(register)*:(claim-earnings)*(reset)f:(pidfile)*(no-pidfile)v(verbose)';
155
161
 
@@ -314,6 +320,7 @@ function processCliOptsPhase1(argv, operatingMode)
314
320
  case 'claim-earnings':
315
321
  case 'register':
316
322
  operatingMode.zeroTrust = true;
323
+ /* fallthrough */
317
324
  case 'show':
318
325
  operatingMode.consoleType = 'stdio';
319
326
  /* fallthrough */
@@ -460,6 +467,9 @@ async function processCliOptsPhase2(argv, operatingMode, loggingOptions)
460
467
  case 'management-port':
461
468
  httpd.config.port = Number(optarg);
462
469
  break;
470
+ case 'management-path':
471
+ httpd.config.path = optarg;
472
+ break;
463
473
  case 'disable-httpd':
464
474
  operatingMode.startHttpd = false;
465
475
  break;
@@ -519,7 +529,6 @@ async function mergeDbAppConfig(remainArgv, operatingMode, loggingOptions)
519
529
  */
520
530
  async function main()
521
531
  {
522
- var workerIsStopping = false;
523
532
  const identity = require('dcp/identity');
524
533
  /* handle file-wide dcp-client imports */
525
534
  wallet = require('dcp/wallet');
@@ -582,11 +591,11 @@ async function main()
582
591
  worker.runInfo.type = 'dcp-worker';
583
592
  worker.on('warning', (...payload) => console.warn (...payload));
584
593
  worker.on('claim', amount => console.log(` . transfered ${amount} ⊇ to ${worker.config.paymentAddress}`));
585
-
586
594
  await processCliOptsPhase2(remainArgv, operatingMode, loggingOptions);
587
595
 
588
596
  if (operatingMode.show)
589
597
  {
598
+ await worker.configure();
590
599
  const output = await require('../lib/show').show(worker, operatingMode.show);
591
600
  if (operatingMode.showSerializer)
592
601
  console.log(operatingMode.showSerializer.stringify(output, null, 2));
@@ -706,14 +715,11 @@ async function main()
706
715
  }
707
716
  }
708
717
 
709
- if (!workerIsStopping)
710
- {
711
- console.info(` . Utilization Target: cpu: ${String(Math.round(worker.config.utilization.cpu * 100)).padStart(3, ' ')}% `
712
- + `gpu: ${Math.round(worker.config.utilization.gpu * 100)}%`);
713
- console.info(` . Configured Cores: cpu: ${String(worker.config.cores.cpu).padEnd(3, ' ')} `
718
+ console.info(` . Utilization Target: cpu: ${String(Math.round(worker.config.utilization.cpu * 100)).padStart(3, ' ')}% `
719
+ + `gpu: ${Math.round(worker.config.utilization.gpu * 100)}%`);
720
+ console.info(` . Configured Cores: cpu: ${String(worker.config.cores.cpu).padEnd(3, ' ')} `
714
721
  + ` gpu: ${worker.config.cores.gpu}`);
715
- console.info(` . Maximum Sandboxes: ${worker.config.maxSandboxes}`);
716
- }
722
+ console.info(` . Maximum Sandboxes: ${worker.config.maxSandboxes}`);
717
723
  console.info(' . Supervisor version: ' + worker.supervisorVersion);
718
724
  console.info(' . Console type: ' + operatingMode.consoleType);
719
725
  console.info(' . Logging: ' + (loggingOptions.loggers.join(', ') || 'none'));
@@ -729,10 +735,22 @@ async function main()
729
735
  return;
730
736
  require('../lib/check-scheduler-version').check();
731
737
 
732
- worker.on('end', async () => {
733
- if (process.exitCode === exitCodes.lostRef)
734
- process.exitCode = exitCodes.normal;
735
- await cleanupThenExit(exitCodes.normal);
738
+ /* Hold the worker.p$stop promise between the stop and end events, for use by cleanUpThenExit() */
739
+ worker.on('stop', () => {
740
+ debug('dcp-worker:exit')('Trapped DistributiveWorker<stop> stop event with running worker');
741
+ const pr$stop = Promise.withResolvers();
742
+ worker.p$stop = pr$stop.promise;
743
+ worker.ref();
744
+ worker.on('end', () => {
745
+ debug('dcp-worker:exit')('Trapped DistributiveWorker<end> event after stop event');
746
+ pr$stop.resolve();
747
+ worker.unref();
748
+ cleanupThenExit(exitCodes.normal);
749
+ });
750
+ worker.on('error', (error) => {
751
+ debug('dcp-worker:exit')('Trapped DistributiveWorker<error> event after stop event');
752
+ pr$stop.reject(error);
753
+ });
736
754
  });
737
755
 
738
756
  worker.on('error', function handleWorkerError(error) {
@@ -781,10 +799,11 @@ async function main()
781
799
  console.info(` ! not claming earnings in the amount of ${amount} ⊇ (can claim later)`);
782
800
  }
783
801
 
784
- worker.on('stop', () => { console.log(' - Worker is stopping'); workerIsStopping = true });
785
- worker.on('end', () => { console.log(' * Worker has stopped') });
802
+ worker.on('stop', () => { console.log(' - Worker is stopping') });
803
+ worker.on('end', () => { console.log(' * Worker has stopped') });
786
804
  worker.on('job', job => console.log(` . Job: ${job.name} ${job.id.slice(0,8)} ${job.description || ''} ${job.link || ''}`));
787
- console.info(' * Ready.\n');
805
+ worker.on('start', () => console.info(' * Ready.\n'));
806
+
788
807
  await worker.start();
789
808
  } /* main() */
790
809
 
@@ -824,6 +843,10 @@ function parseResourceOption(option)
824
843
  */
825
844
  async function handleUnhandled(error)
826
845
  {
846
+ /* Wrap null/undefined throws so the Android embedder doesn't surface them as a bare "JS Exception: null". */
847
+ if (error === null || error === undefined)
848
+ error = new Error(`Non-Error thrown: ${error === null ? 'null' : 'undefined'}`);
849
+
827
850
  if (error instanceof utils.UserError)
828
851
  console.error('Error:', error?.message);
829
852
  else if (!error.request || !error.code?.startsWith('EHTTP_'))
@@ -862,30 +885,34 @@ async function handleUnhandled(error)
862
885
  async function cleanupThenExit(exitCode)
863
886
  {
864
887
  debug('dcp-worker:exit')('cleanup and exit, code', exitCode);
865
- if (worker)
866
- worker.stop(true); /* toggle internal worker state so it won't surprise us by starting sandboxes */
867
-
868
- const p$note = utils.noteWorkerShutdown(worker);
869
- if (cleanupThenExit.busy)
888
+ if (process.exitCode === exitCodes.lostRef || process.exitCode === exitCodes.normal)
889
+ process.exitCode = exitCode;
890
+ if (!cleanupThenExit.busy)
891
+ cleanupThenExit.busy = true;
892
+ else
870
893
  {
871
- if (!process.exitCode)
872
- process.exitCode = exitCode;
873
894
  debug('dcp-worker:exit')('exit race, bailing on cleanup for exit code', exitCode);
874
895
  return;
875
896
  }
876
897
 
877
- worker = false;
878
- cleanupThenExit.busy = true;
879
- process.exitCode = exitCode;
898
+ if (worker)
899
+ worker.stop(true); /* toggle internal worker state so it won't surprise us by starting sandboxes */
900
+
901
+ const p$workerShutdownTasks = Promise.all([
902
+ utils.noteWorkerShutdown(worker),
903
+ worker?.p$stop,
904
+ ]);
880
905
 
881
906
  /* Final event loop reference; keeps the process alive while the loggers unhook */
882
907
  setTimeout(() => {
908
+ debug('dcp-worker:exit')('logger cleanup timeout expired');
883
909
  console.warn(' *** logger cleanup timeout expired ***');
884
910
  process.exit();
885
911
  }, 10 * 1e3).ref();
886
912
 
887
913
  try
888
914
  {
915
+ debug('dcp-worker:exit')('unhooking process events');
889
916
  process.off('SIGESC', handleSigDeath);
890
917
  process.off('SIGINT', handleSigDeath);
891
918
  process.off('SIGTERM', handleSigDeath);
@@ -893,11 +920,16 @@ async function cleanupThenExit(exitCode)
893
920
  process.off('unhandledRejection', handleUnhandled);
894
921
  process.off('uncaughtException', handleUnhandled);
895
922
  if (console.close)
923
+ {
924
+ debug('dcp-worker:exit')('closing console');
896
925
  console.close();
897
- loggers.unhook()
926
+ }
927
+ debug('dcp-worker:exit')('unhooking loggers');
928
+ await loggers.unhook()
929
+ .then(() => debug('dcp-worker:exit')('loggers unhooked'))
898
930
  .catch(error => { console.error('Error unhooking loggers:', error) })
899
931
  .finally(async () => {
900
- await p$note;
932
+ await p$workerShutdownTasks.catch(console.error);
901
933
  debug('dcp-worker:exit')('done cleanup, exitCode=' + process.exitCode);
902
934
  process.exit();
903
935
  });
package/lib/utils.js CHANGED
@@ -176,7 +176,6 @@ function makeSyslogUrl(existingUrl, newArg)
176
176
  async function getOwner(worker)
177
177
  {
178
178
  const { DistributiveWorker } = require('dcp/worker');
179
- const identity = require('dcp/identity');
180
179
 
181
180
  if (worker.config.unmanaged)
182
181
  return undefined;
@@ -385,6 +384,7 @@ exports.noteWorkerStart = async function noteWorkerStart(worker)
385
384
  if (worker.config?.unmanaged)
386
385
  return;
387
386
 
387
+ debug('dcp-worker:start')('noting worker start');
388
388
  return worker.updateRunInfo({
389
389
  lastStartTime: new Date(),
390
390
  status: 'running',
package/lib/web-iface.js CHANGED
@@ -35,8 +35,17 @@ const bootDate = new Date();
35
35
  var worker;
36
36
  var totalEarnings = 0;
37
37
 
38
- /** Configuration for the server, must be mutated before a$init */
39
- exports.config = { hostname: 'localhost', port: 9080 };
38
+ /** Configuration for where the server binds, we support TCP and unix domain sockets, the default is TCP.
39
+ * Must be mutated before a$init.
40
+ */
41
+ exports.config = {
42
+ // TCP
43
+ hostname: 'localhost',
44
+ port: 9080,
45
+
46
+ // unix domain socket, setting this will override the TCP settings
47
+ path: undefined,
48
+ };
40
49
 
41
50
  /**
42
51
  * Web server initialization.
@@ -52,12 +61,17 @@ exports.a$init = function init(__worker)
52
61
  try
53
62
  {
54
63
  worker = __worker;
55
- console.info(` . Starting management service on ${exports.config.hostname}:${exports.config.port}`);
64
+ if (exports.config.path)
65
+ console.info(` . Starting management service on ${exports.config.path}`);
66
+ else
67
+ console.info(` . Starting management service on ${exports.config.hostname}:${exports.config.port}`);
56
68
  const target = new protocol.Target(
57
69
  ['socketio'],
58
70
  {
59
71
  identity: identity.get(),
60
- listen: new URL(`http://${exports.config.hostname}:${exports.config.port}/dcp/`),
72
+ listen: exports.config.path ?
73
+ new URL(`unix://${exports.config.path}`)
74
+ : new URL(`http://${exports.config.hostname}:${exports.config.port}/dcp/`),
61
75
  },
62
76
  listeningHandler,
63
77
  { socketio: { module: require('socket.io') } },
@@ -213,7 +227,7 @@ function setupWebServer(app)
213
227
  KVIN.userCtors.dcpUrl$$DcpURL = require('dcp/dcp-url').DcpURL;
214
228
 
215
229
  app.use((req, res) => {
216
- const ipAddress = req.socket.address().address;
230
+ const ipAddress = req.socket.address().address || '';
217
231
  const originalUrl = req.originalUrl;
218
232
 
219
233
  let endFired = false;
@@ -353,7 +367,7 @@ async function workerRegisterPostHandler(query, req, res)
353
367
  {
354
368
  var failExpn;
355
369
  const utils = require('./utils');
356
- const reg = await utils.registerWorker(worker, query.rkey, msg => failExpn = msg);
370
+ const reg = await utils.registerWorker(worker, query.rkey, undefined /* no label */, msg => failExpn = msg);
357
371
  reg.failExpn = failExpn;
358
372
  reg.pid = process.pid;
359
373
 
@@ -163,7 +163,7 @@ function createDashboard()
163
163
  function setWorker(worker)
164
164
  {
165
165
  const identity = require('dcp/identity');
166
-
166
+
167
167
  console.log(' . Starting dashboard');
168
168
  setInterval(() => screen && screen.render(), 2000).unref(); /* ensure we didn't forget to render an important update */
169
169
  updateWorkerInfo();
@@ -321,7 +321,12 @@ exports.init = function dashboardConsole$$Init()
321
321
  const logHistory = new RingBuffer(closeLogDumpSize);
322
322
  const inspect = Symbol.for('nodejs.util.inspect.custom');
323
323
 
324
+ /* createDashoard adds a SIGINT event handler which intereferes with orderly shutdown, so we remove it */
325
+ const sigintHandlers = process.listeners('SIGINT');
324
326
  createDashboard();
327
+ for (let sigintHandler of process.listeners('SIGINT'))
328
+ if (sigintHandlers.indexOf(sigintHandler) === -1)
329
+ process.off('SIGINT', sigintHandler);
325
330
 
326
331
  function consoleMethodFactory(logLevel)
327
332
  {
@@ -367,7 +372,8 @@ exports.init = function dashboardConsole$$Init()
367
372
  }
368
373
 
369
374
  /**
370
- * Destroy the dashboard, then dump the most recent lines back to stdout/stderr.
375
+ * Destroy the dashboard, dump the most recent lines back to stdout/stderr, then replace the console
376
+ * object's methods with methods that will write to stdio to capture shutdown error locally
371
377
  */
372
378
  console.close = function dashboardConsole$$close()
373
379
  {
@@ -382,6 +388,7 @@ exports.init = function dashboardConsole$$Init()
382
388
  const { logLevel, argv } = entry;
383
389
  stdioConsole[logLevel](...argv);
384
390
  });
391
+ Object.assign(console, stdioConsole);
385
392
  }
386
393
 
387
394
  console.setWorker = setWorker;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dcp-worker",
3
- "version": "4.4.4",
3
+ "version": "4.5.1",
4
4
  "description": "Node.js Worker for Distributive Compute Platform",
5
5
  "main": "bin/dcp-worker",
6
6
  "keywords": [
@@ -39,8 +39,9 @@
39
39
  "blessed": "0.1.81",
40
40
  "blessed-contrib": "4.11.0",
41
41
  "chalk": "4.1.2",
42
- "dcp-client": "^5.6.0",
43
- "kvin": "^9.0.0",
42
+ "dcp-client": "^5.7.4",
43
+ "kvin": "9.1.0",
44
+ "mux-net": "github:wesgarland/mux-net#e7f81800db4a84ec880026596f7ee68549a6d0f5",
44
45
  "posix-getopt": "1.2.1",
45
46
  "semver": "7.7.3",
46
47
  "socket.io": "4.8.1",
@@ -53,8 +54,11 @@
53
54
  },
54
55
  "devDependencies": {
55
56
  "@distributive/eslint-config": "2.1.4",
57
+ "@stylistic/eslint-plugin": "3.1.0",
56
58
  "@trunkio/launcher": "1.2.7",
57
59
  "eslint": "9.39.1",
60
+ "eslint-plugin-jsdoc": "47.0.2",
61
+ "eslint-plugin-unicorn": "49.0.0",
58
62
  "peter": "2.4.7"
59
63
  },
60
64
  "peerDependencies": {
@@ -71,11 +75,12 @@
71
75
  },
72
76
  "overrides": {
73
77
  "blessed-contrib": {
78
+ "lodash": "4.18.1",
74
79
  "xml2js": "0.6.2"
75
80
  },
76
81
  "dbus-next": {
77
82
  "xml2js": "0.6.2",
78
- "tar": "7.5.11",
83
+ "tar": "7.5.22",
79
84
  "usocket": "1.0.2"
80
85
  }
81
86
  }
@@ -67,7 +67,11 @@ async function main()
67
67
  strictEqual(confObj.leaveGlobalGroup, true, `leaveGlobalGroup is ${confObj.leaveGlobalGroup}`);
68
68
  deepEqual(confObj.cores, { cpu: 3, gpu: 1 }, `Mismatch in cores! cpu: ${confObj.cores.cpu}, gpu: ${confObj.cores.gpu}`);
69
69
  deepEqual(confObj.utilization, { cpu: 0.1, gpu: 0.5 }, `Mismatch in utilization! cpu: ${confObj.utilization.cpu}, gpu: ${confObj.utilization.gpu}`);
70
- deepEqual(confObj.computeGroups, { joinKey1: { joinKey: 'joinKey1', joinSecret: 'joinHash1' }, joinKey2: { joinKey: 'joinKey2', joinSecret: 'joinHash2' } }, 'Mismatch in computeGroups')
70
+
71
+ let cgCompare = { joinKey1: { joinKey: 'joinKey1', joinSecret: 'joinHash1' }, joinKey2: { joinKey: 'joinKey2', joinSecret: 'joinHash2' } };
72
+ if (Array.isArray(confObj.computeGroups))
73
+ cgCompare = Object.assign([], cgCompare);
74
+ deepEqual(confObj.computeGroups, cgCompare, 'Mismatch in computeGroups');
71
75
  deepEqual(confObj.maxSandboxes, 5, `Mismatch! maxSandboxes:${confObj.maxSandboxes}`);
72
76
  deepEqual(confObj.jobIds, ['09fd0909Bd93f7bad915726C3AB4'], `Mismatch in job ids! jobIds:${confObj.jobIds}`);
73
77
 
@@ -0,0 +1,103 @@
1
+ #! /usr/bin/env node
2
+
3
+ /**
4
+ * @file worker-with-unix-management.simple.skip
5
+ *
6
+ * Tests and demonstrates how to bind the management daemon to a unix domain socket and
7
+ * query over it. It's marked skip because the worker requires an evaluator and doing so in CI
8
+ * is cumbersome. Run it manually.
9
+ *
10
+ * @author Liang Wang <liang@distributive.network>
11
+ * @date June 2026
12
+ *
13
+ * peter-timeout: 5
14
+ */
15
+ 'use strict';
16
+
17
+ const assert = require('node:assert');
18
+
19
+ const { tmpdir } = require('node:os');
20
+ const { spawn } = require('node:child_process');
21
+ const { mkdtempSync, rmSync, existsSync, writeFileSync } = require('node:fs');
22
+ const { join, parse, basename } = require('node:path');
23
+ const { setTimeout } = require('node:timers/promises');
24
+ const { request } = require('node:http');
25
+ const { strictEqual } = assert;
26
+
27
+ const filename = parse(basename(__filename)).name;
28
+ const tempd = mkdtempSync(join(tmpdir(), `${filename}-`));
29
+ const socketPath = join(tempd, `${filename}.sock`);
30
+ process.chdir(__dirname);
31
+
32
+ process.on('exit', () => existsSync(tempd) && rmSync(tempd, { recursive: true }));
33
+
34
+ async function main()
35
+ {
36
+ const { Keystore } = require('dcp/wallet');
37
+ const ks = await new Keystore(null);
38
+ const id = await new Keystore(null);
39
+
40
+ writeFileSync(`${tempd}/default.keystore`, JSON.stringify(ks.toJSON()), { encoding: 'utf-8' });
41
+ writeFileSync(`${tempd}/id.keystore`, JSON.stringify(id.toJSON()), { encoding: 'utf-8' });
42
+
43
+ const args = [
44
+ `--management-path=${socketPath}`,
45
+ ];
46
+
47
+ const worker = spawn(
48
+ '../bin/dcp-worker', args,
49
+ {
50
+ encoding: 'utf-8',
51
+ env: {
52
+ ...process.env,
53
+ DCP_RESOURCE_DIR: tempd
54
+ }
55
+ }
56
+ );
57
+
58
+ await setTimeout(1000);
59
+ const requestComplete = new Promise((resolve, reject) => {
60
+ const req = request(
61
+ {
62
+ socketPath,
63
+ path: "/worker/id",
64
+ headers: {
65
+ Accept: "*/*",
66
+ },
67
+ },
68
+ (res) => {
69
+ let body = "";
70
+
71
+ res.setEncoding("utf8");
72
+ res.on("data", (chunk) => {
73
+ body += chunk;
74
+ });
75
+
76
+ res.on("end", () => {
77
+ strictEqual(res.statusCode, 200);
78
+ console.log("DONE");
79
+ console.log("headers:", res.headers);
80
+ console.log("body:", body);
81
+ resolve();
82
+ });
83
+ }
84
+ );
85
+
86
+ req.on("error", (err) => {
87
+ console.error(`request failed ${err}`);
88
+ reject(err);
89
+ });
90
+
91
+ req.end();
92
+ });
93
+
94
+ try {
95
+ await requestComplete;
96
+ } finally {
97
+ worker.kill();
98
+ }
99
+ }
100
+
101
+ require('dcp-client')
102
+ .init()
103
+ .then(main);
@@ -7,6 +7,7 @@
7
7
  -->
8
8
  <html lang="en">
9
9
  <head>
10
+ <meta charset="UTF-8">
10
11
  <script src="/dcp-client/dcp-client.js"></script>
11
12
  <script>
12
13
  window.addEventListener('error', ev => dcp['dom-tk'].modals.alert(ev));