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/.gitlab-ci.yml +1 -0
- package/bin/collect-debug-info +73 -30
- package/bin/dcp-evaluator-manager +46 -15
- package/bin/dcp-task-probe +2 -5
- package/bin/dcp-worker +105 -44
- package/lib/loggers.js +1 -1
- package/lib/p2p-shared.js +314 -0
- package/lib/pidfile.js +2 -2
- package/lib/utils.js +34 -14
- package/lib/worker-consoles/dashboard-console.js +9 -2
- 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 +9 -4
- package/tests/worker-with-options.simple +5 -1
- package/www/admin/register-worker.html +1 -0
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,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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
*
|
|
201
|
-
*
|
|
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.
|
|
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.
|
|
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,
|
|
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',
|
|
@@ -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 = {
|