dcp-worker 4.5.0 → 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/lib/loggers.js CHANGED
@@ -183,7 +183,7 @@ exports.hook = function loggers$$hook(console, options)
183
183
  */
184
184
  globalThis.console.throb = (...args) => {
185
185
  console.throb(...args);
186
- multiplex('info', ...args);
186
+ multiplex('throb', ...args);
187
187
  };
188
188
  }
189
189
 
@@ -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 at ${filename} (${error.code})`);
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,23 +171,35 @@ 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
  {
178
185
  const { DistributiveWorker } = require('dcp/worker');
179
- const identity = require('dcp/identity');
180
186
 
181
187
  if (worker.config.unmanaged)
182
188
  return undefined;
183
- const db = await DistributiveWorker.jnuConnect();
184
- const cursor = await db.select({
185
- table: 'w$worker', prototype: {
186
- opaqueId: worker.id,
187
- }
188
- });
189
- const row = await cursor.getFirst();
190
- return row?.owner;
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;
191
203
  }
192
204
 
193
205
  /**
@@ -197,19 +209,26 @@ async function getOwner(worker)
197
209
  * @param {string} rkey the registration key
198
210
  * @param {string|undefined} label optional value for worker.label
199
211
  * @param {function} errorReporter function used to report user errors; accepts string
200
- * @returns string if successful, instanceof Error if there was an error on the server. In either case,
201
- * this is the return value from DistributiveWorker::register.
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.
202
219
  */
203
220
  async function registerWorker(worker, rkey, label, errorReporter)
204
221
  {
205
222
  const wallet = require('dcp/wallet');
206
223
  if (!wallet.isPrivateKey(rkey))
207
224
  return errorReporter('invalid registration key');
225
+ if (!(rkey instanceof wallet.PrivateKey))
226
+ rkey = new wallet.PrivateKey(rkey);
208
227
 
209
228
  if (label)
210
- debug('dcp-worker:registration')('registering worker with key', rkey.slice(0,8), 'and label', label);
229
+ debug('dcp-worker:registration')('registering worker with key', rkey.toShortString(), 'and label', label);
211
230
  else
212
- debug('dcp-worker:registration')('registering worker with key', rkey.slice(0,8));
231
+ debug('dcp-worker:registration')('registering worker with key', rkey.toShortString());
213
232
 
214
233
  const reg = await worker.register(rkey, label);
215
234
  if (!(reg instanceof Error))
@@ -385,6 +404,7 @@ exports.noteWorkerStart = async function noteWorkerStart(worker)
385
404
  if (worker.config?.unmanaged)
386
405
  return;
387
406
 
407
+ debug('dcp-worker:start')('noting worker start');
388
408
  return worker.updateRunInfo({
389
409
  lastStartTime: new Date(),
390
410
  status: 'running',
@@ -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;
@@ -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',
@@ -88,6 +88,8 @@ function createLogFile(options, overwrite)
88
88
  function logToFile(level, ...items)
89
89
  {
90
90
  const logPrefix = `${(new Date()).toISOString()} ${level} `;
91
+ if (level === 'throb')
92
+ level = 'info';
91
93
 
92
94
  try
93
95
  {
@@ -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 = {