dcp-worker 4.5.1 → 4.5.2
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 +80 -49
- package/lib/loggers.js +1 -1
- package/lib/p2p-shared.js +314 -0
- package/lib/pidfile.js +2 -2
- package/lib/utils.js +33 -13
- package/lib/web-iface.js +5 -19
- package/lib/worker-loggers/event-log.js +3 -0
- package/lib/worker-loggers/logfile.js +2 -0
- package/lib/worker-loggers/p2p.js +197 -0
- package/lib/worker-loggers/syslog.js +2 -0
- package/package.json +2 -2
- package/tests/worker-with-unix-management.simple.skip +0 -103
package/bin/dcp-worker
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
'use strict';
|
|
15
15
|
var worker;
|
|
16
|
+
var workerStarted = false;
|
|
16
17
|
|
|
17
18
|
const process = require('process');
|
|
18
19
|
const fs = require('fs');
|
|
@@ -26,6 +27,7 @@ const reports = require('../lib/reports');
|
|
|
26
27
|
const utils = require('../lib/utils');
|
|
27
28
|
const loggers = require('../lib/loggers');
|
|
28
29
|
var wallet, a$sleep, DistributiveWorker; /* populate in main() */
|
|
30
|
+
var noStartWorker = false;
|
|
29
31
|
|
|
30
32
|
const { exitCodes,
|
|
31
33
|
logLevels } = require('../lib/consts');
|
|
@@ -81,6 +83,7 @@ Options:
|
|
|
81
83
|
-o, --output=mode Set the console mode: text|dashboard|none
|
|
82
84
|
-l, --logger=type Add an additional logger: syslog, logfile
|
|
83
85
|
(all platforms), event-log (win32)
|
|
86
|
+
--no-logger=type Remove a logger
|
|
84
87
|
--logfile=filename Change filename, implies --logger=logfile
|
|
85
88
|
--overwrite-logfile Overwrite log files instead of appending
|
|
86
89
|
--syslog=url|facility Change syslog target config, implies
|
|
@@ -116,12 +119,6 @@ Options:
|
|
|
116
119
|
--disable-httpd Disable management service
|
|
117
120
|
--management-hostname= Override the management service hostname
|
|
118
121
|
--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.
|
|
125
122
|
--register=key[,label] Use the given key to register this worker
|
|
126
123
|
with your Distributive account and exit
|
|
127
124
|
--unmanaged Do not report statistics to or read config
|
|
@@ -154,8 +151,8 @@ function* parseCliOptions(argv)
|
|
|
154
151
|
+ 'h(help)*:(earnings-account)*:(identity-keystore)'
|
|
155
152
|
+ 'c:(cores)u:(utilization)m:(max-sandboxes)H:(hostname)p:(port)'
|
|
156
153
|
+ 'o:(output)l:(logger)*:(logfile)*(overwrite-logfile)*:(syslog)*:(min-level)j:(job)g:(join)'
|
|
157
|
-
+ 'a:(allow-origin)*(zero-trust)*(no-global)*(kvin)*(json)*:(show)'
|
|
158
|
-
+ '
|
|
154
|
+
+ 'a:(allow-origin)*(zero-trust)*(no-global)*(kvin)*(json)*:(show)*'
|
|
155
|
+
+ '(unmanaged)*:(management-hostname)*:(management-port)'
|
|
159
156
|
+ '*(no-masquerade)*:(force-worker-id)*:(lookup-worker-id)'
|
|
160
157
|
+ '*(no-claim-earnings)*:(register)*:(claim-earnings)*(reset)f:(pidfile)*(no-pidfile)v(verbose)';
|
|
161
158
|
|
|
@@ -263,6 +260,9 @@ function processCliOptsPhase1(argv, operatingMode)
|
|
|
263
260
|
case 'l':
|
|
264
261
|
loggingOptions.loggers.push(optarg);
|
|
265
262
|
break;
|
|
263
|
+
case 'no-logger':
|
|
264
|
+
loggingOptions.loggers = loggingOptions.loggers.filter(logger => logger !== optarg);
|
|
265
|
+
break;
|
|
266
266
|
case 'syslog':
|
|
267
267
|
loggingOptions.loggers.push('syslog');
|
|
268
268
|
loggingOptions.syslogUrl = utils.makeSyslogUrl(loggingOptions.syslogUrl, optarg);
|
|
@@ -317,6 +317,7 @@ function processCliOptsPhase1(argv, operatingMode)
|
|
|
317
317
|
break;
|
|
318
318
|
}
|
|
319
319
|
/* options below this point are processed in both phase 1 and phase 2 */
|
|
320
|
+
case 'reset':
|
|
320
321
|
case 'claim-earnings':
|
|
321
322
|
case 'register':
|
|
322
323
|
operatingMode.zeroTrust = true;
|
|
@@ -391,7 +392,6 @@ async function processCliOptsPhase2(argv, operatingMode, loggingOptions)
|
|
|
391
392
|
break;
|
|
392
393
|
case 'show':
|
|
393
394
|
operatingMode.show = optarg;
|
|
394
|
-
operatingMode.consoleType = 'stdio';
|
|
395
395
|
break;
|
|
396
396
|
case 'startup-delay':
|
|
397
397
|
await a$sleep(Number(optarg));
|
|
@@ -428,6 +428,7 @@ async function processCliOptsPhase2(argv, operatingMode, loggingOptions)
|
|
|
428
428
|
console.log('Registration failure:', error.message);
|
|
429
429
|
debug('dcp-worker:registration')(error);
|
|
430
430
|
}
|
|
431
|
+
worker.config.unmanaged = true;
|
|
431
432
|
await cleanupThenExit(exitCodes.normal);
|
|
432
433
|
break;
|
|
433
434
|
}
|
|
@@ -467,21 +468,19 @@ async function processCliOptsPhase2(argv, operatingMode, loggingOptions)
|
|
|
467
468
|
case 'management-port':
|
|
468
469
|
httpd.config.port = Number(optarg);
|
|
469
470
|
break;
|
|
470
|
-
case 'management-path':
|
|
471
|
-
httpd.config.path = optarg;
|
|
472
|
-
break;
|
|
473
471
|
case 'disable-httpd':
|
|
474
472
|
operatingMode.startHttpd = false;
|
|
475
473
|
break;
|
|
476
474
|
default:
|
|
477
475
|
throw new Error(`unhandled option '${option}'`);
|
|
478
|
-
|
|
476
|
+
/**** hidden / undocumented options below ****/
|
|
479
477
|
case 'slice-report-interval':
|
|
480
478
|
loggingOptions.sliceReportInterval = Number(optarg);
|
|
481
479
|
break;
|
|
482
480
|
case 'reset': /* can lose unclaimed earnings */
|
|
483
481
|
DistributiveWorker.resetLocalStorage();
|
|
484
482
|
console.info('reset complete');
|
|
483
|
+
worker.config.unmanaged = true;
|
|
485
484
|
await cleanupThenExit(exitCodes.normal);
|
|
486
485
|
break;
|
|
487
486
|
}
|
|
@@ -555,6 +554,8 @@ async function main()
|
|
|
555
554
|
loggingOptions, /* options to tell us where logs go besides the console */
|
|
556
555
|
} = processCliOptsPhase1(process.argv, operatingMode);
|
|
557
556
|
|
|
557
|
+
if (!operatingMode.zeroTrust)
|
|
558
|
+
loggingOptions.loggers.push('p2p');
|
|
558
559
|
if (dcpConfig.worker.logging?.syslog)
|
|
559
560
|
loggingOptions.loggers.push('syslog');
|
|
560
561
|
if (dcpConfig.worker.logging?.logfile)
|
|
@@ -631,6 +632,7 @@ async function main()
|
|
|
631
632
|
|
|
632
633
|
require('../lib/default-ui-events').hook(worker, loggingOptions);
|
|
633
634
|
console.setWorker(worker);
|
|
635
|
+
require('../lib/p2p-shared').init(worker);
|
|
634
636
|
|
|
635
637
|
if (worker.config.jobIds?.length > 0)
|
|
636
638
|
console.info(` * Processing only ${utils.qty(worker.config.jobIds, 'job')} ` + worker.config.jobIds.join(', '));
|
|
@@ -643,29 +645,51 @@ async function main()
|
|
|
643
645
|
console.info(` + Verbosity level: ${loggingOptions.verbose}`);
|
|
644
646
|
|
|
645
647
|
/* Poll the evaluator forever until it accepts a connection */
|
|
646
|
-
let
|
|
648
|
+
let evaluatorInfo = false;
|
|
649
|
+
let p$throbSleep;
|
|
647
650
|
console.throb(` . Waiting for dcp-evaluator on ${utils.shortLoc(sandboxConfig.location)} to start...`);
|
|
648
|
-
|
|
651
|
+
function dieBeforeEvaluatorReady()
|
|
652
|
+
{
|
|
653
|
+
debug('dcp-worker:exit')('Trapped DistributiveWorker<stop> stop event before evaluator ready');
|
|
654
|
+
evaluatorInfo = null;
|
|
655
|
+
p$throbSleep?.intr();
|
|
656
|
+
}
|
|
657
|
+
worker.once('end', dieBeforeEvaluatorReady);
|
|
658
|
+
|
|
659
|
+
for (let i=0; evaluatorInfo === false; i++)
|
|
649
660
|
{
|
|
650
661
|
const ts = performance.now();
|
|
651
|
-
if ((
|
|
662
|
+
if ((evaluatorInfo = await require('../lib/worker-info').getEvaluatorInformation(sandboxConfig)))
|
|
652
663
|
break;
|
|
653
664
|
if (i === 0 && operatingMode.unmanaged !== true)
|
|
654
665
|
utils.noteWorkerStart(worker);
|
|
655
666
|
if (performance.now() - ts > 100)
|
|
656
667
|
console.throb();
|
|
657
|
-
|
|
668
|
+
p$throbSleep = a$sleep(Math.max(i + 1, 10) / 4);
|
|
669
|
+
await p$throbSleep;
|
|
658
670
|
console.throb();
|
|
659
671
|
}
|
|
672
|
+
worker.off('end', dieBeforeEvaluatorReady);
|
|
673
|
+
worker.once('end', () => {
|
|
674
|
+
debug('dcp-worker:exit')('Trapped DistributiveWorker<end>');
|
|
675
|
+
process.nextTick(() => cleanupThenExit(exitCodes.normal));
|
|
676
|
+
/* clean up at the end of tick so that other end event handlers can fire. */
|
|
677
|
+
});
|
|
660
678
|
|
|
661
|
-
if (
|
|
679
|
+
if (!evaluatorInfo)
|
|
680
|
+
{
|
|
681
|
+
console.warn(` . Could not contact dcp-evaluator on ${utils.shortLoc(sandboxConfig.location)} to start...`);
|
|
682
|
+
await cleanupThenExit(exitCodes.normal);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (Object.keys(evaluatorInfo.worktimes).length > 0)
|
|
662
686
|
{
|
|
663
687
|
console.info(' . Worktimes Available:');
|
|
664
|
-
for (const wt of
|
|
688
|
+
for (const wt of evaluatorInfo.worktimes)
|
|
665
689
|
console.info(` - ${wt.name}@${wt.version}`);
|
|
666
690
|
}
|
|
667
691
|
|
|
668
|
-
if (!
|
|
692
|
+
if (!evaluatorInfo.webgpu.enabled)
|
|
669
693
|
{
|
|
670
694
|
console.info(' . WebGPU not enabled');
|
|
671
695
|
systemStateInfo.gpu = false;
|
|
@@ -679,9 +703,9 @@ async function main()
|
|
|
679
703
|
else
|
|
680
704
|
{
|
|
681
705
|
console.info(' . WebGPU available. GPU info:');
|
|
682
|
-
for (let descriptor in
|
|
683
|
-
console.info(` - ${descriptor}: ${
|
|
684
|
-
systemStateInfo.gpu = Object.assign({},
|
|
706
|
+
for (let descriptor in evaluatorInfo.webgpu.info)
|
|
707
|
+
console.info(` - ${descriptor}: ${evaluatorInfo.webgpu.info[descriptor]}`);
|
|
708
|
+
systemStateInfo.gpu = Object.assign({}, evaluatorInfo.webgpu.info);
|
|
685
709
|
|
|
686
710
|
/** @todo move bannedGPU support into DistributiveWorker class /wg jun 2025 */
|
|
687
711
|
const bannedGPUs = Array.isArray(worker.config.bannedGPUs) ? worker.config.bannedGPUs : [ worker.config.bannedGPUs ];
|
|
@@ -689,7 +713,7 @@ async function main()
|
|
|
689
713
|
{
|
|
690
714
|
for (let descriptor in banEntry)
|
|
691
715
|
{
|
|
692
|
-
if (banEntry[descriptor].test(
|
|
716
|
+
if (banEntry[descriptor].test(evaluatorInfo.webgpu.info[descriptor]))
|
|
693
717
|
{
|
|
694
718
|
console.error(' * This GPU is not supported; disabling');
|
|
695
719
|
worker.config.cores = { cpu: worker.config.cores.cpu, gpu: 0 };
|
|
@@ -709,7 +733,7 @@ async function main()
|
|
|
709
733
|
}
|
|
710
734
|
else
|
|
711
735
|
{
|
|
712
|
-
worker.runInfo.gpuType =
|
|
736
|
+
worker.runInfo.gpuType = evaluatorInfo.webgpu.info.device;
|
|
713
737
|
worker.runInfo.gpuCores = 1;
|
|
714
738
|
}
|
|
715
739
|
}
|
|
@@ -738,21 +762,14 @@ async function main()
|
|
|
738
762
|
/* Hold the worker.p$stop promise between the stop and end events, for use by cleanUpThenExit() */
|
|
739
763
|
worker.on('stop', () => {
|
|
740
764
|
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
765
|
worker.on('error', (error) => {
|
|
751
766
|
debug('dcp-worker:exit')('Trapped DistributiveWorker<error> event after stop event');
|
|
752
|
-
|
|
767
|
+
throw error;
|
|
753
768
|
});
|
|
754
769
|
});
|
|
755
770
|
|
|
771
|
+
worker.ref();
|
|
772
|
+
worker.on('start', () => workerStarted = true);
|
|
756
773
|
worker.on('error', function handleWorkerError(error) {
|
|
757
774
|
if (worker.isWorkerError && worker.isWorkerError(error))
|
|
758
775
|
{
|
|
@@ -777,6 +794,7 @@ async function main()
|
|
|
777
794
|
console.error('Unrecoverable', error.code ? `${error.name} (${error.code})` : error.name,
|
|
778
795
|
'- shutting down the worker:',
|
|
779
796
|
process.env.DCP_SUPERVISOR_DEBUG_DISPLAY_MAX_INFO ? error : error.message);
|
|
797
|
+
debug('dcp-worker:error')(error);
|
|
780
798
|
}
|
|
781
799
|
|
|
782
800
|
/* Let the supervisor try to finish slices for up to three minutes after a single unrecoverable error. */
|
|
@@ -799,12 +817,13 @@ async function main()
|
|
|
799
817
|
console.info(` ! not claming earnings in the amount of ${amount} ⊇ (can claim later)`);
|
|
800
818
|
}
|
|
801
819
|
|
|
802
|
-
worker.on('stop', () => { console.log('
|
|
820
|
+
worker.on('stop', () => { console.log(' . Worker is stopping') });
|
|
803
821
|
worker.on('end', () => { console.log(' * Worker has stopped') });
|
|
804
822
|
worker.on('job', job => console.log(` . Job: ${job.name} ${job.id.slice(0,8)} ${job.description || ''} ${job.link || ''}`));
|
|
805
823
|
worker.on('start', () => console.info(' * Ready.\n'));
|
|
806
824
|
|
|
807
|
-
|
|
825
|
+
if (!noStartWorker)
|
|
826
|
+
await worker.start();
|
|
808
827
|
} /* main() */
|
|
809
828
|
|
|
810
829
|
/**
|
|
@@ -843,10 +862,6 @@ function parseResourceOption(option)
|
|
|
843
862
|
*/
|
|
844
863
|
async function handleUnhandled(error)
|
|
845
864
|
{
|
|
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
|
-
|
|
850
865
|
if (error instanceof utils.UserError)
|
|
851
866
|
console.error('Error:', error?.message);
|
|
852
867
|
else if (!error.request || !error.code?.startsWith('EHTTP_'))
|
|
@@ -884,6 +899,8 @@ async function handleUnhandled(error)
|
|
|
884
899
|
*/
|
|
885
900
|
async function cleanupThenExit(exitCode)
|
|
886
901
|
{
|
|
902
|
+
const pr$end = Promise.withResolvers();
|
|
903
|
+
|
|
887
904
|
debug('dcp-worker:exit')('cleanup and exit, code', exitCode);
|
|
888
905
|
if (process.exitCode === exitCodes.lostRef || process.exitCode === exitCodes.normal)
|
|
889
906
|
process.exitCode = exitCode;
|
|
@@ -894,22 +911,33 @@ async function cleanupThenExit(exitCode)
|
|
|
894
911
|
debug('dcp-worker:exit')('exit race, bailing on cleanup for exit code', exitCode);
|
|
895
912
|
return;
|
|
896
913
|
}
|
|
914
|
+
/* Final event loop reference; keeps the process alive while the loggers unhook */
|
|
915
|
+
setTimeout(() => {
|
|
916
|
+
debug('dcp-worker:exit')('cleanup timeout expired');
|
|
917
|
+
console.warn(' *** cleanup timeout expired ***');
|
|
918
|
+
process.exit();
|
|
919
|
+
}, 10 * 1e3).ref();
|
|
897
920
|
|
|
898
|
-
if (worker)
|
|
921
|
+
if (!worker || !workerStarted || worker.inspect.stopped)
|
|
922
|
+
{
|
|
923
|
+
debug('dcp-worker:exit')('cleanup will not wait for DistributiveWorker<end> event');
|
|
924
|
+
pr$end.resolve();
|
|
925
|
+
}
|
|
926
|
+
else
|
|
927
|
+
{
|
|
928
|
+
debug('dcp-worker:exit')('cleanup will wait for DistributiveWorker<end> event');
|
|
929
|
+
worker.once('end', pr$end.resolve);
|
|
930
|
+
worker.once('error', pr$end.reject);
|
|
899
931
|
worker.stop(true); /* toggle internal worker state so it won't surprise us by starting sandboxes */
|
|
932
|
+
}
|
|
900
933
|
|
|
934
|
+
console.info(' ! begin shutdown, exit code =', exitCode);
|
|
901
935
|
const p$workerShutdownTasks = Promise.all([
|
|
936
|
+
require('../lib/p2p-shared').shutdown(),
|
|
902
937
|
utils.noteWorkerShutdown(worker),
|
|
903
938
|
worker?.p$stop,
|
|
904
939
|
]);
|
|
905
940
|
|
|
906
|
-
/* Final event loop reference; keeps the process alive while the loggers unhook */
|
|
907
|
-
setTimeout(() => {
|
|
908
|
-
debug('dcp-worker:exit')('logger cleanup timeout expired');
|
|
909
|
-
console.warn(' *** logger cleanup timeout expired ***');
|
|
910
|
-
process.exit();
|
|
911
|
-
}, 10 * 1e3).ref();
|
|
912
|
-
|
|
913
941
|
try
|
|
914
942
|
{
|
|
915
943
|
debug('dcp-worker:exit')('unhooking process events');
|
|
@@ -924,6 +952,8 @@ async function cleanupThenExit(exitCode)
|
|
|
924
952
|
debug('dcp-worker:exit')('closing console');
|
|
925
953
|
console.close();
|
|
926
954
|
}
|
|
955
|
+
debug('dcp-worker:exit')('waiting for DistributiveWorker<end> event');
|
|
956
|
+
await pr$end.promise;
|
|
927
957
|
debug('dcp-worker:exit')('unhooking loggers');
|
|
928
958
|
await loggers.unhook()
|
|
929
959
|
.then(() => debug('dcp-worker:exit')('loggers unhooked'))
|
|
@@ -969,6 +999,7 @@ async function handleSigDeath(signalName, signal)
|
|
|
969
999
|
});
|
|
970
1000
|
}
|
|
971
1001
|
|
|
1002
|
+
noStartWorker = true; /* sigint before worker.start should not start worker */
|
|
972
1003
|
const warning = signal
|
|
973
1004
|
? `\ntrapped ${signalName}, signal ${signal}`
|
|
974
1005
|
: `\ntrapped ${signalName === 'SIGESC' ? '<esc>' : signalName}`;
|
package/lib/loggers.js
CHANGED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file lib/p2p-shared.js
|
|
3
|
+
* @author Wes Garland, wes@distributive.network
|
|
4
|
+
* @date June 2026
|
|
5
|
+
*
|
|
6
|
+
* Common code for working with the p2p subsystem of the event router in the dcp-worker application.
|
|
7
|
+
* The worker uses its local identity for communication and listens on #worker-${workerId} for control
|
|
8
|
+
* messages. The only authorized sender for the control channel is channel is worker.owner. When an
|
|
9
|
+
* application wants to hear from a given worker, it must add worker.identity to the list of authorized
|
|
10
|
+
* senders on the channel it is listening on.
|
|
11
|
+
*/
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const p2p = require('dcp/p2p');
|
|
15
|
+
const debug = require('debug');
|
|
16
|
+
const assert = require('assert');
|
|
17
|
+
const wallet = require('dcp/wallet');
|
|
18
|
+
|
|
19
|
+
const { a$sleep } = require('dcp/utils');
|
|
20
|
+
const { DcpURL } = require('dcp/dcp-url');
|
|
21
|
+
|
|
22
|
+
const version = '1.0.0';
|
|
23
|
+
var worker; /* singleton instance of DistributiveWorker - held even after stop */
|
|
24
|
+
var workerId; /* worker's opaque id - persisted in case stop() resets it */
|
|
25
|
+
var owner; /* address of worker owner */
|
|
26
|
+
var running; /* 'running' | 'will-pause' | false */
|
|
27
|
+
|
|
28
|
+
const KVIN = new (require('dcp/internal/kvin').KVIN)();
|
|
29
|
+
KVIN.userCtors[wallet.Address.name] = wallet.Address;
|
|
30
|
+
KVIN.userCtors[DcpURL.name] = URL;
|
|
31
|
+
|
|
32
|
+
exports.disabled = false;
|
|
33
|
+
exports.init = async function p2pShared$init(__worker)
|
|
34
|
+
{
|
|
35
|
+
if (__worker.config.unmanaged)
|
|
36
|
+
return;
|
|
37
|
+
owner = await require('./utils').getOwner(__worker);
|
|
38
|
+
if (!owner) /* unregistered worker -- no remote control */
|
|
39
|
+
return;
|
|
40
|
+
|
|
41
|
+
worker = __worker;
|
|
42
|
+
workerId = worker.id;
|
|
43
|
+
worker.on('end', () => {
|
|
44
|
+
p2p.sendMessage(`worker-${workerId}-announce`, { announce: 'stopped', version }, owner);
|
|
45
|
+
running = false;
|
|
46
|
+
});
|
|
47
|
+
worker.on('stop', () => {
|
|
48
|
+
announce('stopping');
|
|
49
|
+
running = false;
|
|
50
|
+
});
|
|
51
|
+
worker.on('start', () => {
|
|
52
|
+
running = 'running';
|
|
53
|
+
});
|
|
54
|
+
/* "make" a connection to the event router whenever possible by copying it from the worker. */
|
|
55
|
+
const __erConnectionFactory = p2p.erConnectionFactory;
|
|
56
|
+
/* Note: disabled July 2026 due to fundamental lifecycle differences between the supervisor's
|
|
57
|
+
* protocol manager and our need to manage/log before and after the worker has stopped. /wg
|
|
58
|
+
*/
|
|
59
|
+
if (false)
|
|
60
|
+
p2p.erConnectionFactory = () => worker?.getConnection('eventRouter') || __erConnectionFactory();
|
|
61
|
+
|
|
62
|
+
/* Listen for control mesages on #worker-ID, and send an unsolicited notification, 'boot', on
|
|
63
|
+
* #worker-ID-announce so that connected entities (eg portal) will notice that the worker has booted
|
|
64
|
+
* and will, for example, know to re-send commands like "log on".
|
|
65
|
+
*/
|
|
66
|
+
p2p.listen([ owner ], `worker-${workerId}`, handleControlMessage);
|
|
67
|
+
p2p.on('listening', ev => {
|
|
68
|
+
announce('hello');
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Start the p2p shutdown sequence. No further p2p should take place after the returned promise resolves.
|
|
74
|
+
*/
|
|
75
|
+
exports.shutdown = async function shutdown()
|
|
76
|
+
{
|
|
77
|
+
if (workerId && owner && !exports.disabled)
|
|
78
|
+
announce('shutdown');
|
|
79
|
+
debug('dcp-worker:p2p')('cleanup for exit');
|
|
80
|
+
await p2p.cleanupForExit();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function announce(announcement, details)
|
|
84
|
+
{
|
|
85
|
+
debug('dcp-worker:p2p')('sending anouncement', announcement);
|
|
86
|
+
if (!workerId)
|
|
87
|
+
throw new Error(`missing worker id for ${announcement} announcement`);
|
|
88
|
+
if (announcement === 'error')
|
|
89
|
+
{
|
|
90
|
+
assert(details instanceof Error);
|
|
91
|
+
const enumerableDetails = {};
|
|
92
|
+
Object.getOwnPropertyNames(details).forEach(prop => enumerableDetails[prop] = details[prop]);
|
|
93
|
+
details = enumerableDetails;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!exports.disabled)
|
|
97
|
+
p2p.sendMessage(`worker-${workerId}-announce`, { announce: announcement, version }, owner);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function enqRespond(message, details)
|
|
101
|
+
{
|
|
102
|
+
assert(message.cmd === 'enq');
|
|
103
|
+
if (exports.disabled)
|
|
104
|
+
return;
|
|
105
|
+
if (!workerId)
|
|
106
|
+
throw new Error(`missing worker id for ${message.enq} enquiry response`);
|
|
107
|
+
if (message.detailsSerializer === 'kvin')
|
|
108
|
+
details = KVIN.marshal(details);
|
|
109
|
+
p2p.sendMessage(`worker-${workerId}-enq`, { enq: message.enq, detailsSerializer: message.detailsSerializer, details, version }, owner);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function handleEnquireMessage(message, channel, sender, originator)
|
|
113
|
+
{
|
|
114
|
+
debug('dcp-worker:p2p')('sending enquiry info for', message.enq);
|
|
115
|
+
assert(message.cmd === 'enq');
|
|
116
|
+
|
|
117
|
+
switch(message.enq)
|
|
118
|
+
{
|
|
119
|
+
default:
|
|
120
|
+
console.error(`Invalid enquire message ${message.enq} from ${originator.party}:`, message);
|
|
121
|
+
break;
|
|
122
|
+
case 'runinfo':
|
|
123
|
+
enqRespond(message, worker.runInfo);
|
|
124
|
+
break;
|
|
125
|
+
case 'ping':
|
|
126
|
+
enqRespond(message, 'pong');
|
|
127
|
+
break;
|
|
128
|
+
case 'running-config':
|
|
129
|
+
enqRespond(message, makeRunningConfig(false, message.detailsSerializer !== 'kvin'));
|
|
130
|
+
break;
|
|
131
|
+
case 'explain-config':
|
|
132
|
+
enqRespond(message, { confSources: worker.confSources });
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Pause the worker. If it takes more than one second to quiesce the worker, log output is printed which estimates the time to quiescence.
|
|
139
|
+
* @returns {Promise} a promise that resolves when the worker is quiescent
|
|
140
|
+
*/
|
|
141
|
+
function pauseWorker()
|
|
142
|
+
{
|
|
143
|
+
let paused = false;
|
|
144
|
+
if (!running)
|
|
145
|
+
{
|
|
146
|
+
console.info(' . Will pause worker on start');
|
|
147
|
+
worker.once('start', pauseWorker);
|
|
148
|
+
running = 'will-pause';
|
|
149
|
+
}
|
|
150
|
+
if (running !== 'running')
|
|
151
|
+
return;
|
|
152
|
+
|
|
153
|
+
console.info(' . Pausing worker');
|
|
154
|
+
announce('paused');
|
|
155
|
+
const p$pause = worker.pause();
|
|
156
|
+
p$pause.then(why => {
|
|
157
|
+
if (why === 'quiescent')
|
|
158
|
+
{
|
|
159
|
+
paused = true;
|
|
160
|
+
console.info(' - Pause: worker quiescent');
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
a$sleep(paused ? 0 : 1).then(() => {
|
|
164
|
+
var remainingMs = null;
|
|
165
|
+
var duration = '';
|
|
166
|
+
const sandboxes = worker.inspect.workingSandboxes;
|
|
167
|
+
if (!worker.inspect.paused)
|
|
168
|
+
return;
|
|
169
|
+
for (let sandbox of sandboxes)
|
|
170
|
+
if (sandbox.remainingMs > remainingMs || remainingMs === null)
|
|
171
|
+
remainingMs = sandbox.remainingMs;
|
|
172
|
+
remainingMs = Number(remainingMs);
|
|
173
|
+
if (sandboxes.length)
|
|
174
|
+
{
|
|
175
|
+
duration = isNaN(remainingMs)
|
|
176
|
+
? ', unknown duration to quiescence'
|
|
177
|
+
: ', estimated duration to quiescence: ' + (Number(remainingMs / 1000).toFixed(1) + 's');
|
|
178
|
+
}
|
|
179
|
+
console.info(` - Pause: ${sandboxes.length} sandboxes running` + duration);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return p$pause;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Handle a control message, eg from the portal. Control messages are sent on channel worker-ID and
|
|
187
|
+
* tell the worker what to do, eg enable logs, reboot, change configuration, etc. These are always
|
|
188
|
+
* trusted, because the p2p.listen operation restricts communication on this channel to the registered
|
|
189
|
+
* owner.
|
|
190
|
+
*/
|
|
191
|
+
function handleControlMessage(message, channel, sender, originator)
|
|
192
|
+
{
|
|
193
|
+
if (!worker || worker.config.unmanaged || exports.disabled)
|
|
194
|
+
return;
|
|
195
|
+
|
|
196
|
+
debug('dcp-worker:p2p')(`p2p party ${sender} sent ${message.cmd} control message`, message);
|
|
197
|
+
|
|
198
|
+
switch (message.cmd)
|
|
199
|
+
{
|
|
200
|
+
default:
|
|
201
|
+
console.error(`p2p party ${sender} sent unknown command '${message.cmd}'`);
|
|
202
|
+
break;
|
|
203
|
+
case 'bonjour':
|
|
204
|
+
announce('hello');
|
|
205
|
+
break;
|
|
206
|
+
case 'refresh-state':
|
|
207
|
+
if (running === 'will-pause')
|
|
208
|
+
announce('will-pause');
|
|
209
|
+
else if (worker.inspect.paused)
|
|
210
|
+
announce('paused');
|
|
211
|
+
if (worker.inspect.stopping)
|
|
212
|
+
announce('stopping');
|
|
213
|
+
if (worker.inspect.stopped)
|
|
214
|
+
announce('stopped');
|
|
215
|
+
break;
|
|
216
|
+
case 'pause':
|
|
217
|
+
pauseWorker();
|
|
218
|
+
break;
|
|
219
|
+
case 'unpause':
|
|
220
|
+
worker.unpause();
|
|
221
|
+
announce('unpaused');
|
|
222
|
+
console.info(' . Unpausing worker');
|
|
223
|
+
break;
|
|
224
|
+
case 'stop':
|
|
225
|
+
worker.stop(message.arguments?.[0]); /* announcement comes from worker<stop> */
|
|
226
|
+
break;
|
|
227
|
+
case 'halt':
|
|
228
|
+
throw new Error(`halt (p2p ${message.sender})`);
|
|
229
|
+
case 'set-cores':
|
|
230
|
+
Object.assign(worker.config.cores, message.arguments[0]);
|
|
231
|
+
worker.dropSandboxes();
|
|
232
|
+
console.info(' . Set cores:', worker.config.cores);
|
|
233
|
+
break;
|
|
234
|
+
case 'set-max-sandboxes':
|
|
235
|
+
worker.config.maxSandboxes = Number(message.arguments[0]);
|
|
236
|
+
console.info(' . Set max sandboxes:', worker.config.maxSandboxes);
|
|
237
|
+
break;
|
|
238
|
+
case 'update-runinfo':
|
|
239
|
+
worker.updateRunInfo();
|
|
240
|
+
break;
|
|
241
|
+
case 'register':
|
|
242
|
+
{
|
|
243
|
+
const [ s$key, label ] = message.arguments;
|
|
244
|
+
const key = new wallet.PrivateKey(s$key);
|
|
245
|
+
console.warn(` * Re-registering worker with key '${label}' (${key})`);
|
|
246
|
+
const errorReporter = function p2p$$registrationErrorReporter(errorMessage) {
|
|
247
|
+
const error = new Error(errorMessage);
|
|
248
|
+
return error;
|
|
249
|
+
};
|
|
250
|
+
worker.config.cores.cpu = 0;
|
|
251
|
+
worker.config.cores.gpu = 0;
|
|
252
|
+
worker.dropSandboxes().then(async () => {
|
|
253
|
+
const result = await require('./utils').registerWorker(worker, key, undefined, errorReporter);
|
|
254
|
+
if (result instanceof(Error))
|
|
255
|
+
{
|
|
256
|
+
console.error(result.message);
|
|
257
|
+
announce('error', result);
|
|
258
|
+
}
|
|
259
|
+
else
|
|
260
|
+
{
|
|
261
|
+
console.log(' . Registration result:', result?.status);
|
|
262
|
+
announce('registered', result);
|
|
263
|
+
}
|
|
264
|
+
}).finally(() => worker.stop(true));
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
case 'enq':
|
|
268
|
+
handleEnquireMessage(message, channel, sender, originator);
|
|
269
|
+
break;
|
|
270
|
+
case 'log':
|
|
271
|
+
require('./worker-loggers/p2p').handleLogControlMessage(message, channel, sender, originator);
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Make a copy of the running config.
|
|
278
|
+
* @param {boolean|undefined} jsonCompat - when true, neuter the output so that it can be serialized with JSON
|
|
279
|
+
* @param {boolean|undefined} sanitize - when true, remove credentials
|
|
280
|
+
* @returns {Object}
|
|
281
|
+
*/
|
|
282
|
+
function makeRunningConfig(sanitize, jsonCompat)
|
|
283
|
+
{
|
|
284
|
+
const schedulerUrl = new URL(dcpConfig.scheduler.location.href);
|
|
285
|
+
var config = Object.assign({}, worker.config);
|
|
286
|
+
config.allowOrigins = {};
|
|
287
|
+
delete config.__allowOriginsCurrentScheduler;
|
|
288
|
+
|
|
289
|
+
/* resolve the schedulerUrl symbols in the allow origins to a real URL */
|
|
290
|
+
for (let allowOriginListName in worker.config.allowOrigins)
|
|
291
|
+
{
|
|
292
|
+
const allowOriginList = worker.config.allowOrigins[allowOriginListName].slice();
|
|
293
|
+
config.allowOrigins[allowOriginListName] = allowOriginList;
|
|
294
|
+
for (let i=0; i < allowOriginList.length; i++)
|
|
295
|
+
{
|
|
296
|
+
if (allowOriginList[i] === worker.config.__allowOriginsCurrentScheduler)
|
|
297
|
+
allowOriginList[i] = schedulerUrl;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (sanitize)
|
|
302
|
+
{
|
|
303
|
+
if (Array.isArray(config.computeGroups))
|
|
304
|
+
config.computeGroups = config.computeGroups.slice();
|
|
305
|
+
else
|
|
306
|
+
config.computeGroups = Object.assign({}, config.computeGroups);
|
|
307
|
+
for (let computeGroupKey in config.computeGroups)
|
|
308
|
+
config.computeGroups[computeGroupKey] = { joinKey: config.computeGroups[computeGroupKey].joinKey };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (jsonCompat)
|
|
312
|
+
config = JSON.parse(JSON.stringify(config));
|
|
313
|
+
return config;
|
|
314
|
+
}
|
package/lib/pidfile.js
CHANGED
|
@@ -80,8 +80,8 @@ exports.write = function pidfile$$write(filename)
|
|
|
80
80
|
}
|
|
81
81
|
catch (error)
|
|
82
82
|
{
|
|
83
|
-
if (fd)
|
|
84
|
-
console.warn(`Warning: Could not remove pidfile
|
|
83
|
+
if (fd && error.code !== 'ENOENT')
|
|
84
|
+
console.warn(`Warning: Could not remove pidfile ${filename} (${error.code})`);
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
}
|
package/lib/utils.js
CHANGED
|
@@ -171,7 +171,14 @@ function makeSyslogUrl(existingUrl, newArg)
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
|
-
* Retrieve the registered owner's address from the database
|
|
174
|
+
* Retrieve the registered owner's address from the database. It is assumed that the ownership will
|
|
175
|
+
* never change while the program is running, so the database lookup is cached for use by subsequent
|
|
176
|
+
* calls.
|
|
177
|
+
*
|
|
178
|
+
* @returns {Promise} a promise which resolves to
|
|
179
|
+
* undefined => worker is running in unmanaged mode
|
|
180
|
+
* false => worker is not registered
|
|
181
|
+
* Address => address of the registered owner
|
|
175
182
|
*/
|
|
176
183
|
async function getOwner(worker)
|
|
177
184
|
{
|
|
@@ -179,14 +186,20 @@ async function getOwner(worker)
|
|
|
179
186
|
|
|
180
187
|
if (worker.config.unmanaged)
|
|
181
188
|
return undefined;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
|
|
190
|
+
if (getOwner.cache === undefined)
|
|
191
|
+
{
|
|
192
|
+
const db = await DistributiveWorker.jnuConnect();
|
|
193
|
+
const cursor = await db.select({
|
|
194
|
+
table: 'w$worker', prototype: {
|
|
195
|
+
opaqueId: worker.id,
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
const row = await cursor.getFirst();
|
|
199
|
+
getOwner.cache = row?.owner || false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return getOwner.cache;
|
|
190
203
|
}
|
|
191
204
|
|
|
192
205
|
/**
|
|
@@ -196,19 +209,26 @@ async function getOwner(worker)
|
|
|
196
209
|
* @param {string} rkey the registration key
|
|
197
210
|
* @param {string|undefined} label optional value for worker.label
|
|
198
211
|
* @param {function} errorReporter function used to report user errors; accepts string
|
|
199
|
-
*
|
|
200
|
-
*
|
|
212
|
+
*
|
|
213
|
+
* @returns {any} the string 'registered' indicates a successful registration. An instance of Error is
|
|
214
|
+
* returned when there is an error at the I/O layer; this is the raw return value of
|
|
215
|
+
* DistributiveWorker::register, which actually comes from submitSignedPost. In the case
|
|
216
|
+
* when I/O is okay but the worker is not registered, and in the case where there is a
|
|
217
|
+
* problem with the registration data itself, this function will pass a string into the
|
|
218
|
+
* errorReporter function and return that function's return value.
|
|
201
219
|
*/
|
|
202
220
|
async function registerWorker(worker, rkey, label, errorReporter)
|
|
203
221
|
{
|
|
204
222
|
const wallet = require('dcp/wallet');
|
|
205
223
|
if (!wallet.isPrivateKey(rkey))
|
|
206
224
|
return errorReporter('invalid registration key');
|
|
225
|
+
if (!(rkey instanceof wallet.PrivateKey))
|
|
226
|
+
rkey = new wallet.PrivateKey(rkey);
|
|
207
227
|
|
|
208
228
|
if (label)
|
|
209
|
-
debug('dcp-worker:registration')('registering worker with key', rkey.
|
|
229
|
+
debug('dcp-worker:registration')('registering worker with key', rkey.toShortString(), 'and label', label);
|
|
210
230
|
else
|
|
211
|
-
debug('dcp-worker:registration')('registering worker with key', rkey.
|
|
231
|
+
debug('dcp-worker:registration')('registering worker with key', rkey.toShortString());
|
|
212
232
|
|
|
213
233
|
const reg = await worker.register(rkey, label);
|
|
214
234
|
if (!(reg instanceof Error))
|
package/lib/web-iface.js
CHANGED
|
@@ -35,17 +35,8 @@ const bootDate = new Date();
|
|
|
35
35
|
var worker;
|
|
36
36
|
var totalEarnings = 0;
|
|
37
37
|
|
|
38
|
-
/** Configuration for
|
|
39
|
-
|
|
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
|
-
};
|
|
38
|
+
/** Configuration for the server, must be mutated before a$init */
|
|
39
|
+
exports.config = { hostname: 'localhost', port: 9080 };
|
|
49
40
|
|
|
50
41
|
/**
|
|
51
42
|
* Web server initialization.
|
|
@@ -61,17 +52,12 @@ exports.a$init = function init(__worker)
|
|
|
61
52
|
try
|
|
62
53
|
{
|
|
63
54
|
worker = __worker;
|
|
64
|
-
|
|
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}`);
|
|
55
|
+
console.info(` . Starting management service on ${exports.config.hostname}:${exports.config.port}`);
|
|
68
56
|
const target = new protocol.Target(
|
|
69
57
|
['socketio'],
|
|
70
58
|
{
|
|
71
59
|
identity: identity.get(),
|
|
72
|
-
listen: exports.config.
|
|
73
|
-
new URL(`unix://${exports.config.path}`)
|
|
74
|
-
: new URL(`http://${exports.config.hostname}:${exports.config.port}/dcp/`),
|
|
60
|
+
listen: new URL(`http://${exports.config.hostname}:${exports.config.port}/dcp/`),
|
|
75
61
|
},
|
|
76
62
|
listeningHandler,
|
|
77
63
|
{ socketio: { module: require('socket.io') } },
|
|
@@ -227,7 +213,7 @@ function setupWebServer(app)
|
|
|
227
213
|
KVIN.userCtors.dcpUrl$$DcpURL = require('dcp/dcp-url').DcpURL;
|
|
228
214
|
|
|
229
215
|
app.use((req, res) => {
|
|
230
|
-
const ipAddress = req.socket.address().address
|
|
216
|
+
const ipAddress = req.socket.address().address;
|
|
231
217
|
const originalUrl = req.originalUrl;
|
|
232
218
|
|
|
233
219
|
let endFired = false;
|
|
@@ -39,6 +39,9 @@ exports.open = function eventLog$$open(options)
|
|
|
39
39
|
*/
|
|
40
40
|
exports.at = function eventLog$$at(level, ...items)
|
|
41
41
|
{
|
|
42
|
+
if (level === 'throb')
|
|
43
|
+
level = 'info';
|
|
44
|
+
|
|
42
45
|
// Use the string log-level to look up the severity number:
|
|
43
46
|
let severity = {
|
|
44
47
|
[logLevels.error]: 'error',
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file worker-loggers/p2p.js
|
|
3
|
+
* @author Wes Garland, wes@distributive.network
|
|
4
|
+
* @date June 2026
|
|
5
|
+
*
|
|
6
|
+
* This logger module emits log lines to a remote listener via the p2p subsystem of the event router.
|
|
7
|
+
* The worker uses its local identity for communication, listens on #worker-${workerId} for control
|
|
8
|
+
* messages, and sends logs on the #worker-${workerId}-log channel. The only authorized sender for
|
|
9
|
+
* the control channel is channel is worker.owner. When an application requests logs, it must add
|
|
10
|
+
* worker.identity to the list of authorized senders.
|
|
11
|
+
*/
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const process = require('process');
|
|
16
|
+
const debug = require('debug');
|
|
17
|
+
const p2p = require('dcp/p2p');
|
|
18
|
+
const p2pShared = require('../p2p-shared');
|
|
19
|
+
|
|
20
|
+
const { RingBuffer } = require('dcp/utils');
|
|
21
|
+
const logDests = new Map(); /* party => set of instances */
|
|
22
|
+
var logIdSeq = 0;
|
|
23
|
+
var logHistory = new RingBuffer(50);
|
|
24
|
+
var disabled = true;
|
|
25
|
+
|
|
26
|
+
/* Static data in log emission */
|
|
27
|
+
const hostname = os.hostname();
|
|
28
|
+
const pid = process.pid;
|
|
29
|
+
const processName = require('path').basename(process.mainModule.filename);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Initialize the p2p worker logger.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} options Options for logger behaviour
|
|
35
|
+
*/
|
|
36
|
+
exports.open = function p2pLogs$$open(options)
|
|
37
|
+
{
|
|
38
|
+
disabled = false;
|
|
39
|
+
debug('dcp-worker:logger.p2p')('initializing p2p logger subsystem');
|
|
40
|
+
p2p.on('mdr', handleMessageDeliveryReport);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Tear down the p2p logging subsystem; used by loggers::unhook(). Invoking this function makes
|
|
45
|
+
* it impossible to send any more messages to syslog, but we try to wait until all messages have been
|
|
46
|
+
* sent before resolving the close promise. This dramatically increases the probability that uncaught
|
|
47
|
+
* exceptions that crash the worker are actually logged some place other than the system console.
|
|
48
|
+
*/
|
|
49
|
+
exports.close = async function p2pLog$$a$close()
|
|
50
|
+
{
|
|
51
|
+
debug('dcp-worker:logger.p2p')('Flushing p2p log buffers');
|
|
52
|
+
const stillMore = await p2p.flushPendingNotifications();
|
|
53
|
+
if (stillMore)
|
|
54
|
+
await p2p.flushPendingNotifications();
|
|
55
|
+
debug('dcp-worker:logger.p2p')('Flushed p2p log buffers, cleared log dests');
|
|
56
|
+
disabled = true;
|
|
57
|
+
logDests.clear();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Emit a message via the p2p subsystem
|
|
62
|
+
*
|
|
63
|
+
* @param {string} level Event level (info, debug, log, warning, error)
|
|
64
|
+
* @param {...any[]} argv Items to log
|
|
65
|
+
*/
|
|
66
|
+
exports.at = function loggers$$p2p$at(level, ...args)
|
|
67
|
+
{
|
|
68
|
+
if (p2pShared.disabled || disabled)
|
|
69
|
+
return;
|
|
70
|
+
|
|
71
|
+
const date = new Date();
|
|
72
|
+
const logIdBase = `${Number(date)}-${hostname}-${processName}-${pid}`;
|
|
73
|
+
const logMessages = args.join(' ').split('\n');
|
|
74
|
+
for (let logMessage of logMessages)
|
|
75
|
+
{
|
|
76
|
+
if (!logMessage && level !== 'throb') /* skip blank lines */
|
|
77
|
+
continue;
|
|
78
|
+
const seq = (++logIdSeq).toString(36).padStart(3,0);
|
|
79
|
+
const logId = logIdBase + `-${seq}`; /* logId must be generated in lexical order including across processes */
|
|
80
|
+
if (logMessage || logHistory.head()?.logMessage) /* avoid storing excess throbber advances in history */
|
|
81
|
+
logHistory.push({ logId, date, level, logMessage });
|
|
82
|
+
|
|
83
|
+
for (let [ destination, instances ] of logDests)
|
|
84
|
+
{
|
|
85
|
+
debug('dcp-worker:logger.p2p.verbose')(`sending log message ${logId} to ${destination} (${instances.size} instances)`);
|
|
86
|
+
if (instances.size > 0)
|
|
87
|
+
p2p.sendMessage(instances.outputChannel, {
|
|
88
|
+
logId,
|
|
89
|
+
processName,
|
|
90
|
+
pid,
|
|
91
|
+
date,
|
|
92
|
+
hostname,
|
|
93
|
+
level,
|
|
94
|
+
logMessage,
|
|
95
|
+
}, destination, { mdr: 'fail' });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {object} message { cmd: 'log', arguments: [] }
|
|
102
|
+
* arguments[0]:
|
|
103
|
+
* 'off' - disable log transmission
|
|
104
|
+
* 'on' - enable log transmission
|
|
105
|
+
* 'history' - (re)transmit whatever is in the history buffer
|
|
106
|
+
* 'on-history' - enable log transmission and send the history buffer,
|
|
107
|
+
* accepts an optional argument, lastLogId. If used, this
|
|
108
|
+
* avoids emitting history <= lastLogId.
|
|
109
|
+
* arguments[1]: optional, name of the output channel, default is worker-ID-log
|
|
110
|
+
* @param {string} channel worker-ID (the command channel)
|
|
111
|
+
* @param {string} sender who sent the message (auth'd)
|
|
112
|
+
* @param {string} originator who sent the message (pre-auth, includes instance)
|
|
113
|
+
*
|
|
114
|
+
*/
|
|
115
|
+
exports.handleLogControlMessage = async function loggers$$p2p$handleLogControlMessage(message, channel, sender, originator)
|
|
116
|
+
{
|
|
117
|
+
const mode = message.arguments?.[0];
|
|
118
|
+
if (message.cmd !== 'log')
|
|
119
|
+
return;
|
|
120
|
+
const outputChannel = `${channel}-log`;
|
|
121
|
+
|
|
122
|
+
switch(mode)
|
|
123
|
+
{
|
|
124
|
+
default:
|
|
125
|
+
console.error(`p2p party ${sender} sent unknown log mode '${mode}'`);
|
|
126
|
+
return;
|
|
127
|
+
case 'off':
|
|
128
|
+
await p2p.flushPendingNotifications();
|
|
129
|
+
if (logDests.has(sender))
|
|
130
|
+
{
|
|
131
|
+
const instances = logDests.get(sender);
|
|
132
|
+
instances.delete(originator.instance);
|
|
133
|
+
if (instances.size === 0)
|
|
134
|
+
logDests.delete(sender);
|
|
135
|
+
}
|
|
136
|
+
debug('dcp-worker:logger.p2p')(`p2p remote logging disabled for ${sender} (${originator.party} ${originator.instance})`);
|
|
137
|
+
break;
|
|
138
|
+
case 'on': case 'on-history':
|
|
139
|
+
{
|
|
140
|
+
debug('dcp-worker:logger.p2p')(`p2p remote logging enabled for ${sender} (${originator.party} ${originator.instance})`);
|
|
141
|
+
if (!logDests.has(sender))
|
|
142
|
+
logDests.set(sender, new Set());
|
|
143
|
+
const instances = logDests.get(sender);
|
|
144
|
+
instances.add(originator.instance);
|
|
145
|
+
instances.outputChannel = outputChannel; /* assumption: outputChannel same for all instances */
|
|
146
|
+
if (mode !== 'on-history')
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
/* fallthrough */
|
|
150
|
+
case 'history':
|
|
151
|
+
{
|
|
152
|
+
/* History replays a few recent log messages, potentially from before remote logging was enabled.
|
|
153
|
+
* Log receivers are responsible for ignoring logs they have already processed. This can be
|
|
154
|
+
* accomplished by either tracking logId, or observing the order of logIds, which are guaranteed
|
|
155
|
+
* to be sortable in emission order. Note that it is possible to receive notifications out of
|
|
156
|
+
* order...
|
|
157
|
+
*/
|
|
158
|
+
const lastLogId = message.arguments[1];
|
|
159
|
+
debug('dcp-worker:logger.p2p')('p2p history request, history length=', logHistory.length);
|
|
160
|
+
debug('dcp-worker:logger.p2p')('p2p history lastLogId=', lastLogId);
|
|
161
|
+
const conn = p2p.getErConnection();
|
|
162
|
+
/* Increase dcp batch size to accomodate the history and related announcements */
|
|
163
|
+
if (conn.options.maxMessagesPerBatch < (logHistory.length + 5))
|
|
164
|
+
conn.options.maxMessagesPerBatch = (logHistory.length + 5);
|
|
165
|
+
logHistory.forEach(entry => {
|
|
166
|
+
if (!lastLogId || entry.logId > lastLogId)
|
|
167
|
+
{
|
|
168
|
+
debug('dcp-worker:logger.p2p.verbose')(`sending historic log message ${entry.logId} to ${sender}`);
|
|
169
|
+
p2p.sendMessage(outputChannel, Object.assign({ processName, pid, hostname }, entry),
|
|
170
|
+
sender, { mdr: 'fail' });
|
|
171
|
+
}
|
|
172
|
+
else
|
|
173
|
+
{
|
|
174
|
+
debug('dcp-worker:logger.p2p.verbose')(`${sender} already has historic log message ${entry.logId}`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function handleMessageDeliveryReport(mdr)
|
|
182
|
+
{
|
|
183
|
+
debug('dcp-worker:logger.p2p.mdr')('received MDR', mdr);
|
|
184
|
+
if (mdr.disposition !== 'fail' || mdr.details.failure !== 'nochan')
|
|
185
|
+
return;
|
|
186
|
+
|
|
187
|
+
/* nochan failure => nobody is listening for our messages */
|
|
188
|
+
for (let [ _destination, instances ] of logDests)
|
|
189
|
+
{
|
|
190
|
+
debug('dcp-worker:logger.p2p.mdr')('output channel is', instances.outputChannel);
|
|
191
|
+
if (instances.outputChannel === mdr.details.channel)
|
|
192
|
+
{
|
|
193
|
+
debug('dcp-worker:logger.p2p.mdr')('clearing instance');
|
|
194
|
+
instances.clear();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -145,6 +145,8 @@ exports.close = function a$syslog$$Close()
|
|
|
145
145
|
function syslogAt(level, ...argv)
|
|
146
146
|
{
|
|
147
147
|
const logPrefix = `${processName}[${process.pid}]: `;
|
|
148
|
+
if (level === 'throb')
|
|
149
|
+
level = 'info';
|
|
148
150
|
|
|
149
151
|
// Use the string log-level to look up the severity number:
|
|
150
152
|
let severity = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dcp-worker",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.2",
|
|
4
4
|
"description": "Node.js Worker for Distributive Compute Platform",
|
|
5
5
|
"main": "bin/dcp-worker",
|
|
6
6
|
"keywords": [
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"blessed": "0.1.81",
|
|
40
40
|
"blessed-contrib": "4.11.0",
|
|
41
41
|
"chalk": "4.1.2",
|
|
42
|
-
"dcp-client": "^5.7.
|
|
42
|
+
"dcp-client": "^5.7.5",
|
|
43
43
|
"kvin": "9.1.0",
|
|
44
44
|
"mux-net": "github:wesgarland/mux-net#e7f81800db4a84ec880026596f7ee68549a6d0f5",
|
|
45
45
|
"posix-getopt": "1.2.1",
|
|
@@ -1,103 +0,0 @@
|
|
|
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);
|