dcp-worker 4.3.2 → 4.3.4

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.
@@ -0,0 +1,192 @@
1
+ #! /usr/bin/env node
2
+ /**
3
+ * @file collect-debug-info
4
+ * Collect information about the host environment and send it to Distributive or and arbitrary
5
+ * e-mail address for analysis. If no command-line options are specified, logs are sent to a
6
+ * TLS syslog target controlled by Distributive. Emails are sent via DCP (https) through the
7
+ * Distributive scheduler.
8
+ *
9
+ * Usage: collect-debug-info [-f] [mailbox@domain]
10
+ *
11
+ * @author Wes Garland, wes@distributive.network
12
+ * @date Nov 2025
13
+ */
14
+ const os = require('os');
15
+ const process = require('process');
16
+ const readline = require('readline');
17
+
18
+ const { execFileSync } = require('child_process');
19
+
20
+ const argv = process.argv.slice(2);
21
+ var force = false;
22
+ var email;
23
+ var syslogUrl = 'tls://s1565808.eu-nbg-2-vec.betterstackdata.com/?logtail@11993+source_token=kcpVMsbZf9DEvur4MCDd4Fkj';
24
+
25
+ process.stdout.write(`
26
+ *** This program collects information from your computer, and sends it to
27
+ *** Distributive and our analytics partners. This includes your local DCP
28
+ *** configuration, which might include sensitive information such as Compute
29
+ *** Group credentials, or API keys.
30
+
31
+ `);
32
+
33
+ while (argv[0])
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
+ if (argv[0].startsWith('--syslog='))
50
+ {
51
+ syslogUrl = argv[0].slice(0);
52
+ argv.shift();
53
+ continue;
54
+ }
55
+
56
+ break;
57
+ }
58
+
59
+ if (force)
60
+ {
61
+ go();
62
+ email = argv[0] || 'wes+worker-debug-info@distributive.network';
63
+ }
64
+ else
65
+ {
66
+ email = argv[0];
67
+
68
+ const rl = readline.createInterface({
69
+ input: process.stdin,
70
+ output: process.stdout
71
+ });
72
+ rl.question(`Proceed? (y/n) `, answer => {
73
+ rl.close();
74
+ switch(answer.toLowerCase())
75
+ {
76
+ case 'y':
77
+ case 'yes':
78
+ console.log();
79
+ go();
80
+ break;
81
+ default:
82
+ console.error(`Abort (${answer})`);
83
+ process.exit(2);
84
+ break;
85
+ }
86
+ });
87
+ }
88
+
89
+ async function syslogReport(report)
90
+ {
91
+ const syslog = require('../lib/worker-loggers/syslog');
92
+ console.log(' - logging to', syslogUrl);
93
+ syslog.open({ syslogUrl: new URL(syslogUrl) });
94
+ syslog.at('notice', report);
95
+ await syslog.close();
96
+ }
97
+
98
+
99
+ async function emailReport(report)
100
+ {
101
+ const protocol = require('dcp/protocol');
102
+ const worker = require('dcp/worker');
103
+
104
+ const conn = new protocol.Connection(dcpConfig.scheduler.services.mailgw);
105
+ const message = {
106
+ from: `${os.userInfo().username}-${worker.DistributiveWorker.obtainWorkerId()}-worker-${os.hostname()}@distributive.network`,
107
+ fromName: 'collect-debug-info',
108
+ to: email,
109
+ toName: 'DCP Debug Report Receiver',
110
+ subject: `collect-debug-info ${os.userInfo().username}@${os.hostname()}`,
111
+ contentType: 'text/html',
112
+ body: `<html><pre>${report}</pre></html>`
113
+ };
114
+
115
+ const response = await conn.request('sendmail', message);
116
+ conn.close();
117
+ console.log(' - sent report to', message.to);
118
+ if (!response.success)
119
+ console.error(`error ${response.payload?.code} sending email:`, response.payload?.message);
120
+ }
121
+
122
+ async function cmd(prog, args)
123
+ {
124
+ const argv = args.slice();
125
+ console.log(' -', prog, argv.join(' '));
126
+ argv.unshift(prog);
127
+ argv.unshift('/c');
128
+ const s = execFileSync('cmd', argv, { encoding: 'utf8' });
129
+ return `------ [ ${prog} ] ---------------------------------------------------------------\n${s}`;
130
+ }
131
+
132
+ async function sh(prog, args)
133
+ {
134
+ const argv = args.slice();
135
+ console.log(' -', prog, argv.join(' '));
136
+ const s = execFileSync(prog, argv, { encoding: 'utf8', maxBuffer: 1e9 });
137
+ return `------ [ ${prog} ] ---------------------------------------------------------------\n${s}`;
138
+ }
139
+
140
+ async function go()
141
+ {
142
+ const gigs = Math.pow(2, 30);
143
+ await require('dcp-client').init({
144
+ programName: 'dcp-worker',
145
+ dcpConfig: {
146
+ bundle: { location: false },
147
+ worker: { logging: { syslog: {} } },
148
+ }
149
+ });
150
+
151
+ console.log(' - host information');
152
+ var report = `------ [ ${new Date()} ] ---------------------------------------------------------------
153
+ os: ${os.version()}
154
+ platform: node ${process.version} ${os.platform()} ${os.release()} ${os.machine()} ${os.endianness()}
155
+ uptime: ${Math.floor(os.uptime() / 86400)} days ${Number(((os.uptime() / 86400) % 1) * 86400).toFixed(1)} seconds, load average: ${os.loadavg().join(', ')}
156
+ homedir: ${os.homedir()}
157
+ avail //ism: ${os.availableParallelism()}
158
+ memory: ${os.freemem()/gigs}G / ${os.totalmem()/gigs}G (${Number(os.freemem() / os.totalmem()).toFixed(1)}% free)
159
+ userInfo: ${JSON.stringify(os.userInfo(), null, 2)}
160
+ cpus: ${JSON.stringify(os.cpus, null, 2)}
161
+ network: ${JSON.stringify(os.networkInterfaces, null, 2)}
162
+ more versions: ${JSON.stringify(process.versions, null, 2)}
163
+ `;
164
+
165
+ console.log(' - dcp information');
166
+ report +=
167
+ `
168
+ dcpConfig: ${JSON.stringify(dcpConfig, null, 2)}
169
+ `;
170
+
171
+ if (os.platform() === 'win32')
172
+ {
173
+ report += await cmd('dir');
174
+ report += await cmd('tasklist');
175
+ report += await cmd('powershell', [ '"eventlog -LogName Applications -newest 100 | FormatTable -wrap"' ]);
176
+ }
177
+ else
178
+ {
179
+ report += await sh('ls', [ '-l' ]);
180
+ report += await sh('ps', [ '-ef' ]);
181
+ }
182
+
183
+ if (os.platform() === 'linux')
184
+ {
185
+ report += await sh('journalctl', [ '-n', '100', '-u', 'dcp-worker', '-u', 'dcp-evaluator', '-u', 'dcp' ]);
186
+ }
187
+
188
+ if (email)
189
+ await emailReport(report);
190
+ else if (syslogUrl)
191
+ syslogReport(report);
192
+ }
@@ -25,8 +25,11 @@ const path = require('path');
25
25
  const net = require('net');
26
26
  const dns = require('dns');
27
27
  const debug = require('debug');
28
- const getopt = require('posix-getopt');
29
28
  const pidfile = require('../lib/pidfile');
29
+ const loggers = require('../lib/loggers');
30
+ const utils = require('../lib/utils');
31
+
32
+ const { logLevels } = require('../lib/consts');
30
33
 
31
34
  var userActivity = { /* no props true => okay to launch evaluator */
32
35
  screensaver: true,
@@ -59,6 +62,15 @@ const daemonConfig = {
59
62
  },
60
63
  };
61
64
 
65
+ async function processExit(code)
66
+ {
67
+ if (typeof code === 'undefined')
68
+ code = process.exitCode;
69
+ console.debug('Exit, code', code);
70
+ await loggers.unhook();
71
+ process.exit(code);
72
+ }
73
+
62
74
  function usage()
63
75
  {
64
76
  console.log(
@@ -123,7 +135,7 @@ async function listenForConnections(config)
123
135
  server.on('error', (error) => {
124
136
  delete error.stack;
125
137
  console.error(error);
126
- process.exit(2);
138
+ processExit(2);
127
139
  });
128
140
 
129
141
  server.listen({ host: hostaddr, port: config.net.port }, function daemonReady() {
@@ -410,7 +422,7 @@ async function dbusScreenSaverMonitor()
410
422
  {
411
423
  const prettyName = chosen.split('.')[1];
412
424
  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}`);
413
- process.exit(3);
425
+ processExit(3);
414
426
  }
415
427
  }
416
428
 
@@ -538,15 +550,20 @@ function odRequire(moduleId)
538
550
 
539
551
  console.error(`Screensaver operating mode requires optional dependency ${moduleId}.`);
540
552
  console.error(`Please \`npm i ${moduleId}\` and try again.`);
541
- process.exit(2);
553
+ processExit(2);
542
554
  }
543
555
 
544
556
  /* Main program entry point */
545
557
  async function main()
546
558
  {
547
- const parser = new getopt.BasicParser('h(help)P:(prefix)H:p:d:(disable-monitor)l:(max-load)r:(rate)'
548
- + 'Li:as:(signal)T:(loadavg-type)f:(pidfile)n(no-pidfile)', process.argv);
549
- var option;
559
+ const loggingOptions = {
560
+ minLevel: { all: undefined }, /* minimum logging levels for console and per logger */
561
+ loggers: [], /* names of loggers (places logs go besides console) */
562
+ syslogUrl: new URL(dcpConfig.worker.logging?.syslog?.url || 'udp://localhost:514/local7'),
563
+ logfile: path.resolve(path.dirname(require.main.filename),
564
+ (dcpConfig.worker.logging?.['evaluator-start-logfile']
565
+ || '../log/dcp-evaluator-start.log')),
566
+ };
550
567
 
551
568
  if (dcpConfig.evaluator.listen)
552
569
  {
@@ -554,32 +571,36 @@ async function main()
554
571
  daemonConfig.net.port = dcpConfig.evaluator.listen.port;
555
572
  }
556
573
 
557
- while ((option = parser.getopt()) !== undefined)
574
+ for (let { option, optarg, argvSegment } of
575
+ utils.getoptParse(process.argv.slice(2), ''
576
+ + 'h(help)P:(prefix)H:p:d:(disable-monitor)l:(max-load)r:(rate)'
577
+ + 'Li:as:(signal)T:(loadavg-type)f:(pidfile)n(no-pidfile)'
578
+ + '*:(logfile)*(overwrite-logfile)*:(syslog)*:(min-level)'))
558
579
  {
559
- switch (option.option)
580
+ switch (option)
560
581
  {
561
582
  case 'h':
562
583
  usage();
563
584
  break;
564
585
 
565
586
  default:
566
- throw new Error(`defined but unspecified option: -${option.option}` + (option.optarg ? `=${option.optarg}` : ''));
587
+ throw new Error(`defined but unspecified option: -${option}` + (optarg ? `=${optarg}` : ''));
567
588
 
568
589
  case '?':
569
- process.exit(1);
590
+ processExit(1);
570
591
  break;
571
592
 
572
593
  case 'P':
573
594
  {
574
595
  const re = new RegExp('^(--prefix=).*$');
575
596
  for (let i=0; i < daemonConfig.argv.length; i++)
576
- daemonConfig.argv[i] = daemonConfig.argv[i].replace(re, '$1' + path.resolve(option.optarg + '/'));
597
+ daemonConfig.argv[i] = daemonConfig.argv[i].replace(re, '$1' + path.resolve(optarg + '/'));
577
598
  break;
578
599
  }
579
600
 
580
601
  case 'd':
581
602
  {
582
- let what = option.optarg;
603
+ let what = optarg;
583
604
 
584
605
  if (what[0] === '=')
585
606
  what = what.slice(1);
@@ -590,7 +611,7 @@ async function main()
590
611
 
591
612
  case 'i':
592
613
  {
593
- daemonConfig.session.idleTimeout = Number(option.optarg).toFixed(1);
614
+ daemonConfig.session.idleTimeout = Number(optarg).toFixed(1);
594
615
  break;
595
616
  }
596
617
 
@@ -619,31 +640,31 @@ async function main()
619
640
 
620
641
  case 'p':
621
642
  {
622
- daemonConfig.net.port = Number(option.optarg);
643
+ daemonConfig.net.port = Number(optarg);
623
644
  break;
624
645
  }
625
646
 
626
647
  case 'H':
627
648
  {
628
- daemonConfig.net.hostname = option.optarg;
649
+ daemonConfig.net.hostname = optarg;
629
650
  break;
630
651
  }
631
652
 
632
653
  case 'l':
633
654
  {
634
- daemonConfig.limits.maxLoad = Number(option.optarg);
655
+ daemonConfig.limits.maxLoad = Number(optarg);
635
656
  break;
636
657
  }
637
658
 
638
659
  case 'r':
639
660
  {
640
- daemonConfig.limits.rateMs = 1000 * Number(option.optarg);
661
+ daemonConfig.limits.rateMs = 1000 * Number(optarg);
641
662
  break;
642
663
  }
643
664
 
644
665
  case 's':
645
666
  {
646
- const opts = option.optarg.split(',');
667
+ const opts = optarg.split(',');
647
668
  daemonConfig.signal.which = opts[0] || daemonConfig.signal.which;
648
669
  daemonConfig.signal.mode = opts[1] || daemonConfig.signal.mode;
649
670
  break;
@@ -651,13 +672,13 @@ async function main()
651
672
 
652
673
  case 'T':
653
674
  {
654
- daemonConfig.limits.loadavgType = option.optarg;
675
+ daemonConfig.limits.loadavgType = optarg;
655
676
  break;
656
677
  }
657
678
 
658
679
  case 'f':
659
680
  {
660
- daemonConfig.pidfile = option.optarg;
681
+ daemonConfig.pidfile = optarg;
661
682
  break;
662
683
  }
663
684
 
@@ -666,10 +687,43 @@ async function main()
666
687
  daemonConfig.pidfile = false;
667
688
  break;
668
689
  }
690
+ case 'syslog':
691
+ loggingOptions.loggers.push('syslog');
692
+ loggingOptions.syslogUrl = utils.makeSyslogUrl(loggingOptions.syslogUrl, optarg);
693
+ break;
694
+ case 'logfile':
695
+ loggingOptions.logfile = optarg;
696
+ loggingOptions.loggers.push('logfile');
697
+ break;
698
+ case 'overwrite-logfile':
699
+ loggingOptions.overwriteLogfile = true;
700
+ break;
701
+ case 'min-level':
702
+ {
703
+ if (optarg.indexOf(',') === -1)
704
+ optarg=`all,${optarg}`;
705
+ const [ which, level ] = optarg.split(',');
706
+ if (which !== 'all' && which !== 'console' && !loggers.exists(which))
707
+ console.error(`--min-level: ${which} is neither 'console' nor the name of a logger`);
708
+ if (!logLevels.lookup(level))
709
+ console.error(`--min-level: ${level} is not a valid log level (${Object.keys(logLevels).join(', ')})`);
710
+ loggingOptions.minLevel[which] = level;
711
+ break;
712
+ }
713
+ case '--':
714
+ daemonConfig.argv = daemonConfig.argv.concat(argvSegment); /* All options after -- pass to dcp-evaluator-start */
715
+ break;
669
716
  }
670
717
  }
671
718
 
672
- daemonConfig.argv = daemonConfig.argv.concat(process.argv.slice(parser.optind())); /* All options after -- pass to dcp-evaluator-start */
719
+ if (dcpConfig.worker.logging?.syslog) loggingOptions.loggers.push('syslog');
720
+ if (dcpConfig.worker.logging?.logfile) loggingOptions.loggers.push('logfile');
721
+ if (dcpConfig.worker.eventLog) loggingOptions.loggers.push('event-log');
722
+ loggingOptions.loggers = utils.uniq(loggingOptions.loggers);
723
+
724
+ const newConsole = require('../lib/worker-consoles').setConsoleType('stdio');
725
+ loggers.hook(newConsole, loggingOptions);
726
+
673
727
  if (daemonConfig.pidfile)
674
728
  pidfile.write(daemonConfig.pidfile);
675
729
 
@@ -696,12 +750,18 @@ async function main()
696
750
 
697
751
  /* Initialize dcp-client to use only local resources before launching the main function */
698
752
  try {
753
+ process.on('SIGHUP', () => processExit( 1 - 128));
754
+ process.on('SIGINT', () => processExit( 2 - 128));
755
+ process.on('SIGTERM', () => processExit(15 - 128));
756
+ process.on('EXIT', processExit);
757
+
699
758
  require('dcp-client').init({
700
759
  programName: 'dcp-worker',
701
760
  parseArgv: false,
702
761
  dcpConfig: {
703
762
  scheduler: { configLocation: false },
704
763
  bundle: { location: false },
764
+ worker: { logging: { syslog: {} } },
705
765
  }
706
766
  }).then(main);
707
767
  }
@@ -13,28 +13,43 @@
13
13
  */
14
14
  'use strict';
15
15
 
16
+ const loggers = require('../lib/loggers');
17
+ const utils = require('../lib/utils');
18
+ const path = require('path');
19
+
20
+ const { logLevels } = require('../lib/consts');
21
+
22
+ async function processExit(code)
23
+ {
24
+ if (typeof code === 'undefined')
25
+ code = process.exitCode;
26
+ console.debug('Exit, code', code);
27
+ await loggers.unhook();
28
+ process.exit(code);
29
+ }
30
+
16
31
  function panic(...argv)
17
32
  {
18
33
  console.error(...argv);
19
- process.exit(1);
34
+ loggers.unhook();
35
+ processExit(1);
20
36
  }
21
37
 
22
38
  function main()
23
39
  {
40
+ const dcpConfig = require('dcp/dcp-config');
41
+ const loggingOptions = {
42
+ minLevel: { all: undefined }, /* minimum logging levels for console and per logger */
43
+ loggers: [], /* names of loggers (places logs go besides console) */
44
+ syslogUrl: new URL(dcpConfig.worker.logging?.syslog?.url || 'udp://localhost:514/local7'),
45
+ logfile: path.resolve(path.dirname(require.main.filename),
46
+ (dcpConfig.worker.logging?.['evaluator-start-logfile']
47
+ || '../log/dcp-evaluator-start.log')),
48
+ };
24
49
  const { clearLine, cursorTo } = require('readline');
25
- const { BasicParser } = require('posix-getopt');
26
50
  const { expandPath } = require('dcp/utils');
27
- const path = require('path');
28
51
  const pidfile = require('../lib/pidfile')
29
- const dcpConfig = require('dcp/dcp-config');
30
52
  const dcpClientDir = path.dirname(require.resolve('dcp-client'));
31
- const parser = new BasicParser(
32
- 'h(help)P:(prefix)p:(port)s(stdio)e:(evaluator)r:(rEvaluator)\u1000:(sandbox-libexec-dir)' +
33
- '\u1001:(sandbox-definitions)t:(sandbox-type)D(dump)T(dump-types)F(dump-files)J(json)v(verbose)' +
34
- 'f:(pidfile)d:(logLevel)',
35
- process.argv
36
- );
37
-
38
53
  const options = {
39
54
  prefix: path.resolve(path.dirname(process.argv[1]), '..'),
40
55
  port: Number(dcpConfig.evaluator.listen.port),
@@ -42,7 +57,7 @@ function main()
42
57
  sandboxDefinitions: path.resolve(dcpClientDir, 'generated', 'sandbox-definitions.json'),
43
58
  sandboxType: 'native',
44
59
  evaluator: path.join('bin', 'dcp-evaluator'), /* resolved against prefix later */
45
- verbose: 0,
60
+ verbose: 0,
46
61
  };
47
62
 
48
63
  function help() {
@@ -75,66 +90,72 @@ Options:
75
90
  -v, --verbose Generate verbose output
76
91
  -f, --pidfile= create a .pid file; default is
77
92
  ${pidfile.getDefaultPidFileName(dcpConfig.worker.pidfile)}
78
- -d, --log-level= Select minimum log level for evaluator output
79
- (debug, info, notice, warn, error, crit, alert, emerg)
93
+ -d, --log-level= Select minimum log level for evaluator's output
94
+ (debug, info, notice, warn, error, crit, alert, emerg).
95
+ Affects only the evaluator itself, not this script.
96
+ --logfile=filename Change filename, implies --logger=logfile
97
+ --overwrite-logfile Overwrite previous log files instead of appending
98
+ --syslog=url Change syslog url, implies --logger=syslog. Syslog
99
+ facility (eg. local7) is the pathname, structured
100
+ data is encoded in search params; id+name=value.
101
+ --syslog=facility Change syslog facility, implies --logger=syslog.
102
+ --min-level=[logger,]level Set the lowest level of log sent to a given logger
103
+ --min-level=[console,]level Set the lowest level of log sent to the console
80
104
  `);
81
105
  }
82
106
 
83
- for (let opthnd; (opthnd = parser.getopt()) !== undefined; /* while loop */)
107
+ for (let { option, optarg, argvSegment } of
108
+ utils.getoptParse(process.argv.slice(2), ''
109
+ + 'h(help)P:(prefix)p:(port)s(stdio)e:(evaluator)r:(rEvaluator)'
110
+ + '*:(sandbox-libexec-dir)*:(sandbox-definitions)t:(sandbox-type)'
111
+ + 'D(dump)T(dump-types)F(dump-files)J(json)v(verbose)f:(pidfile)d:(logLevel)'
112
+ + '*:(logfile)*(overwrite-logfile)*:(syslog)*:(min-level)'))
84
113
  {
85
- let option;
86
-
87
- /* To fake long-only-opts, we use short opts >= \u1000), and present only the long opt below */
88
- if (opthnd.option < '\u1000')
89
- option = opthnd.option;
90
- else
91
- option = (Object.entries(parser.gop_aliases).filter(longShort => longShort[1] === opthnd.option)[0] || ['?'])[0];
92
-
93
114
  switch (option)
94
115
  {
95
116
  case '?':
96
- process.exit(1);
117
+ processExit(1);
97
118
  break;
98
119
  case 'h':
99
120
  help();
100
- process.exit(0);
121
+ processExit(0);
101
122
  break;
102
123
  case 'P':
103
- options.prefix = opthnd.optarg;
124
+ options.prefix = optarg;
104
125
  break;
105
126
  case 'p':
106
- options.port = opthnd.optarg;
127
+ options.port = optarg;
107
128
  break;
108
129
  case 's':
109
130
  options.stdio = true;
110
131
  options.port = false;
111
132
  break;
112
133
  case 'e':
113
- options.evaluator = expandPath(opthnd.optarg);
134
+ options.evaluator = expandPath(optarg);
114
135
  break;
115
136
  case 'm':
116
- options.evaluator = require.resolve(opthnd.optarg);
137
+ options.evaluator = require.resolve(optarg);
117
138
  break;
118
139
  case 't':
119
- options.sandboxType = opthnd.optarg;
140
+ options.sandboxType = optarg;
120
141
  break;
121
142
  case 'l':
122
- options.sandboxLibexecDir = expandPath(opthnd.optarg);
143
+ options.sandboxLibexecDir = expandPath(optarg);
123
144
  break;
124
145
  case 'J':
125
146
  options.json = true;
126
147
  break;
127
148
  case 'f':
128
- pidfile.write(expandPath(opthnd.optarg));
149
+ pidfile.write(expandPath(optarg));
129
150
  break;
130
151
  case 'sandbox-definitions':
131
- options.sandboxDefinitions = expandPath(opthnd.optarg);
152
+ options.sandboxDefinitions = expandPath(optarg);
132
153
  break;
133
154
  case 'T':
134
155
  {
135
156
  const dump = Object.keys(require(options.sandboxDefinitions));
136
157
  console.log(options.json ? JSON.stringify(dump) : dump);
137
- process.exit(0);
158
+ processExit(0);
138
159
  break;
139
160
  }
140
161
  case 'D':
@@ -146,13 +167,44 @@ Options:
146
167
  case 'v':
147
168
  options.verbose++;
148
169
  break;
170
+ case 'syslog':
171
+ loggingOptions.loggers.push('syslog');
172
+ loggingOptions.syslogUrl = utils.makeSyslogUrl(loggingOptions.syslogUrl, optarg);
173
+ break;
174
+ case 'logfile':
175
+ loggingOptions.logfile = optarg;
176
+ loggingOptions.loggers.push('logfile');
177
+ break;
178
+ case 'overwrite-logfile':
179
+ loggingOptions.overwriteLogfile = true;
180
+ break;
181
+ case 'min-level':
182
+ {
183
+ if (optarg.indexOf(',') === -1)
184
+ optarg=`all,${optarg}`;
185
+ const [ which, level ] = optarg.split(',');
186
+ if (which !== 'all' && which !== 'console' && !loggers.exists(which))
187
+ console.error(`--min-level: ${which} is neither 'console' nor the name of a logger`);
188
+ if (!logLevels.lookup(level))
189
+ console.error(`--min-level: ${level} is not a valid log level (${Object.keys(logLevels).join(', ')})`);
190
+ loggingOptions.minLevel[which] = level;
191
+ break;
192
+ }
193
+ case '--':
194
+ options.evaluatorOptions = argvSegment; /* after --, pass to evaluator as-is */
195
+ break;
149
196
  default:
150
197
  throw new Error(`unhandled option '${option}'`);
151
198
  }
152
199
  }
153
200
 
154
- options.evaluatorOptions = process.argv.slice(parser.optind()); /* after --, pass to evaluator as-is */
201
+ if (dcpConfig.worker.logging?.syslog) loggingOptions.loggers.push('syslog');
202
+ if (dcpConfig.worker.logging?.logfile) loggingOptions.loggers.push('logfile');
203
+ if (dcpConfig.worker.eventLog) loggingOptions.loggers.push('event-log');
204
+ loggingOptions.loggers = utils.uniq(loggingOptions.loggers);
155
205
 
206
+ const newConsole = require('../lib/worker-consoles').setConsoleType('stdio');
207
+ loggers.hook(newConsole, loggingOptions);
156
208
  const sandboxSetupDefs = require(options.sandboxDefinitions)[options.sandboxType];
157
209
  if (!sandboxSetupDefs)
158
210
  panic(`Invalid sandbox type: ${options.sandboxType}`);
@@ -208,7 +260,7 @@ Options:
208
260
  }
209
261
 
210
262
  /* Pass options after -- as evaluator-specific options */
211
- if (options.evaluatorOptions.length > 0)
263
+ if (options.evaluatorOptions?.length > 0)
212
264
  args = args.concat(options.evaluatorOptions);
213
265
 
214
266
  /* Definitions in the JSON file are either in the sandboxLibexecDir, or pathed
@@ -229,7 +281,7 @@ Options:
229
281
  console.log('Evaluator command:', [path.resolve(options.prefix, options.evaluator)].concat(files).concat(args).join(' '));
230
282
  console.log('Options:', options);
231
283
  }
232
- process.exit(0);
284
+ processExit(0);
233
285
  }
234
286
 
235
287
  if (options.dumpFiles)
@@ -239,7 +291,7 @@ Options:
239
291
  if (options.json)
240
292
  dump = JSON.stringify(dump);
241
293
  console.log(dump);
242
- process.exit(0);
294
+ processExit(0);
243
295
  }
244
296
 
245
297
  if (!options.stdio && options.sandboxType === 'node') {
@@ -295,7 +347,7 @@ Options:
295
347
  console.log('Evaluator server process exited, status', code);
296
348
  }
297
349
 
298
- process.exit(code);
350
+ processExit(code);
299
351
  });
300
352
 
301
353
  process.on('SIGINT', () => process.kill(child.pid, 'SIGINT'));
@@ -308,5 +360,6 @@ require('dcp-client').init({
308
360
  dcpConfig: {
309
361
  scheduler: { configLocation: false },
310
362
  bundle: { location: false },
363
+ worker: { logging: { syslog: {} } },
311
364
  }
312
- }).then(main);
365
+ }).then(main).finally(loggers.unhook);
package/bin/dcp-worker CHANGED
@@ -602,7 +602,7 @@ async function main()
602
602
  {
603
603
  console.info(' . Worktimes Available:');
604
604
  for (const wt of workerInfo.worktimes)
605
- console.info(` - ${wt.name}@${wt.versions.join(';')}`);
605
+ console.info(` - ${wt.name}@${wt.version}`);
606
606
  }
607
607
 
608
608
  if (!workerInfo.webgpu.enabled)
package/lib/utils.js CHANGED
@@ -16,6 +16,7 @@ exports.shortLoc = shortLoc;
16
16
  exports.qty = qty;
17
17
  exports.uniq = uniq;
18
18
  exports.makeSyslogUrl = makeSyslogUrl;
19
+ exports.getoptParse = getoptParse;
19
20
 
20
21
  /**
21
22
  * Figure out #slices fetched from the different forms of the 'fetch' event.
@@ -118,9 +119,10 @@ function uniq(array)
118
119
  *
119
120
  * existingUrl newArg result
120
121
  * ------------------------------- -------------------------------- ----------------------------------
121
- * udp://localhost:513/local0 tls://provider.com:123/local1 tls://provider.com:123/local1
122
- * udp://localhost:513/local0 local1 udp://localhost:513/local1
123
- * udp://localhost:513/local0 tls://provider.com:123/ tls://provider.com:123/local0
122
+ * udp://localhost:514/local0 tls://provider.com:123/local1 tls://provider.com:123/local1
123
+ * udp://localhost:514/local0 local1 udp://localhost:514/local1
124
+ * udp://localhost:514/local0 tls://provider.com:123/ tls://provider.com:123/local0
125
+ * udp://localhost:514/local0 tls://provider.com/ tls://provider.com:6514/local0
124
126
  *
125
127
  * @param {URL} existingUrl
126
128
  * @param {string|URL} newArg
@@ -145,5 +147,83 @@ function makeSyslogUrl(existingUrl, newArg)
145
147
  if (!newUrl.pathname)
146
148
  newUrl.pathname = 'local7';
147
149
 
150
+ if (!newUrl.port)
151
+ {
152
+ switch(newUrl.protocol)
153
+ {
154
+ case 'udp:':
155
+ case 'tcp:':
156
+ newUrl.port = '514';
157
+ break;
158
+ case 'tls:':
159
+ newUrl.port = '6514';
160
+ break;
161
+ }
162
+ }
163
+
148
164
  return newUrl;
149
165
  }
166
+
167
+ /**
168
+ * A posix getopt parser with a nice UX and API.
169
+ * - accepts optionsDefn string like posix-getopt module, but understands that the character * means
170
+ * long option only.
171
+ * - is a generator that yields for each option
172
+ * - option: character or long option when character is *.
173
+ * Magic option -- means that argvSegment is remainder of argv.
174
+ * - optarg: argument to option
175
+ * - optind: position of option character in argv
176
+ * - argvSegment: elements of argv used to make up option and optarg
177
+ *
178
+ * @param {Array} argv array of options to parse
179
+ * @param {string} optionsDefn
180
+ */
181
+ function *getoptParse(argv, optionsDefn)
182
+ {
183
+ const { BasicParser } = require('posix-getopt');
184
+ var longOptCount = 0;
185
+
186
+ optionsDefn = optionsDefn /* patchup long opt * to > \u1000 chars */
187
+ .match(/[A-Za-z*]:?(\([^)]*\))?/g)
188
+ .map(s => s[0] === '*' ? String.fromCharCode(++longOptCount + 0x1000) + s.slice(1) : s).join('');
189
+ const optionChars = optionsDefn.match(/[A-Za-z\u1000-\u1100/](:?\([^)]*\))?/g).map(s=>s[0]);
190
+ const uniqueOptionChars = uniq(optionChars);
191
+ if (optionChars.length !== uniqueOptionChars.length)
192
+ {
193
+ uniqueOptionChars.forEach(ch => {
194
+ if (optionChars.lastIndexOf(ch) !== optionChars.indexOf(ch))
195
+ throw new Error(`getopt - option character '${ch}' used more than once`);
196
+ });
197
+ throw new Error('unreachable code');
198
+ }
199
+
200
+ argv = process.argv.slice(0,2).concat(argv);
201
+ const parser = new BasicParser(optionsDefn, argv);
202
+ const psw = process.stderr.write;
203
+ process.stderr.write = function patchupGetoptErrorOutput(s, ...args) {
204
+ /* posix-getopt library has useless chr for long opts when stderr.write('option requires an argument -- ' + chr + '\n'); */
205
+ if (s.startsWith('option requires an argument -- '))
206
+ s = `option ${argv[parser.optind() - 1]} requires an argument\n`;
207
+ psw.call(process.stderr, s, ...args);
208
+ };
209
+
210
+ for (let opthnd, lastOpt=2; (opthnd = parser.getopt()); lastOpt = parser.optind())
211
+ {
212
+ let option;
213
+
214
+ /* To fake long-only-opts, we use short opts >= \u1000), and present only the long opt below */
215
+ if (opthnd.option < '\u1000')
216
+ option = opthnd.option;
217
+ else
218
+ option = (Object.entries(parser.gop_aliases).filter(longShort => longShort[1] === opthnd.option)[0] || ['?'])[0];
219
+
220
+ yield { option, optarg: opthnd.optarg, optind: parser.optind() - 2, argvSegment: argv.slice(lastOpt, parser.optind()) };
221
+ if (opthnd.error)
222
+ break;
223
+ }
224
+
225
+ if (argv.length !== parser.optind())
226
+ yield { option: '--', optind: parser.optind(), argvSegment: argv.slice(parser.optind()) };
227
+
228
+ process.stderr.write = psw; // eslint-disable-line require-atomic-updates
229
+ }
@@ -165,7 +165,7 @@ function setWorker(worker)
165
165
  const identity = require('dcp/identity');
166
166
 
167
167
  console.log(' . Starting dashboard');
168
- setInterval(() => screen.render(), 2000).unref(); /* ensure we didn't forget to render an important update */
168
+ setInterval(() => screen && screen.render(), 2000).unref(); /* ensure we didn't forget to render an important update */
169
169
  updateWorkerInfo();
170
170
 
171
171
  setInterval(updateWorkerInfo, 1000).unref();
@@ -57,7 +57,7 @@ async function eval$$getInfo()
57
57
  info.webgpu = { enabled: false };
58
58
  }
59
59
 
60
- info.worktimes = globalThis.worktimes;
60
+ info.worktimes = globalThis.getWorktimeList();
61
61
 
62
62
  return info;
63
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dcp-worker",
3
- "version": "4.3.2",
3
+ "version": "4.3.4",
4
4
  "description": "Node.js Worker for Distributive Compute Platform",
5
5
  "main": "bin/dcp-worker",
6
6
  "keywords": [
@@ -38,23 +38,23 @@
38
38
  "dependencies": {
39
39
  "blessed": "0.1.81",
40
40
  "blessed-contrib": "4.11.0",
41
- "chalk": "^4.1.0",
42
- "dcp-client": "^5.2.0",
41
+ "chalk": "4.1.2",
42
+ "dcp-client": "^5.4.0",
43
43
  "kvin": "^9.0.0",
44
- "posix-getopt": "^1.2.1",
45
- "semver": "^7.3.8",
44
+ "posix-getopt": "1.2.1",
45
+ "semver": "7.7.3",
46
46
  "syslog-client": "1.1.1",
47
47
  "syslog-client-tls": "github:wesgarland/node-syslog-client#0d734f33767bc6a8275552d3daa51712cc2d306d",
48
48
  "websocket": "1.0.34"
49
49
  },
50
50
  "optionalDependencies": {
51
- "dbus-next": "^0.10.2",
51
+ "dbus-next": "0.9.2",
52
52
  "telnet-console": "^1.0.6"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@distributive/eslint-config": "2.1.4",
56
56
  "@trunkio/launcher": "1.2.7",
57
- "eslint": "8.56.0",
57
+ "eslint": "9.39.1",
58
58
  "peter": "2.4.7"
59
59
  },
60
60
  "peerDependencies": {
@@ -67,14 +67,14 @@
67
67
  },
68
68
  "engines": {
69
69
  "node": ">=18",
70
- "npm": ">=7"
70
+ "npm": ">=8"
71
71
  },
72
72
  "overrides": {
73
73
  "blessed-contrib": {
74
- "xml2js": "^0.6.0"
74
+ "xml2js": "0.6.2"
75
75
  },
76
76
  "dbus-next": {
77
- "xml2js": "^0.6.0",
77
+ "xml2js": "0.6.2",
78
78
  "usocket": {
79
79
  "node-gyp": "10.0.1"
80
80
  }