dcp-worker 4.3.7 → 4.4.0
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-evaluator-manager +15 -1
- package/bin/dcp-evaluator-start +9 -2
- package/bin/dcp-worker +197 -101
- package/lib/default-ui-events.js +3 -3
- package/lib/pidfile.js +7 -3
- package/lib/show.js +7 -16
- package/lib/telnetd.js +1 -1
- package/lib/utils.js +219 -9
- package/lib/web-iface.js +495 -91
- package/lib/worker-consoles/dashboard-console.js +8 -1
- package/lib/worker-consoles/stdio-console.js +9 -2
- package/package.json +6 -5
- package/www/admin/index.html +22 -0
- package/www/admin/manage-worker.html +19 -0
- package/www/admin/register-worker.html +334 -0
- package/www/hud/dark.css +10 -0
- package/www/hud/hud-common.css +19 -0
- package/www/hud/hud-common.mjs +69 -0
- package/www/hud/index.html +46 -0
- package/www/hud/small-dark.html +346 -0
- package/lib/web-console.js +0 -178
package/lib/web-iface.js
CHANGED
|
@@ -1,128 +1,532 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* @file web-iface.js - Web Interface for dcp-worker
|
|
3
|
+
* @author Wes Garland, wes@distributive.network
|
|
4
|
+
* @date July 2025
|
|
5
|
+
*
|
|
6
|
+
* @description
|
|
7
|
+
* This file sets up a daemon listening on localhost:9080 by default, which functions as a DCP Target.
|
|
8
|
+
*
|
|
9
|
+
* The security model is:
|
|
10
|
+
* 1. if the worker is unregistered, only the register command is supported, and anybody on the same
|
|
11
|
+
* local network as the worker can invoke it.
|
|
12
|
+
* 2. if the worker is registered, only commands sent by the registered owner are supported. This
|
|
13
|
+
* include the register command. "Same owner" is determined by the following algorithm:
|
|
14
|
+
* - Target receives DCP connection from Initiator (eg portal dcp-client on same host)
|
|
15
|
+
* - Initiator makes Request: signed message X that has messageId containing dcpsid
|
|
16
|
+
* - worker ensures X.messageId starts with dcpsid
|
|
17
|
+
* - worker sends message M to althea owner-check operation
|
|
18
|
+
* - M includes { innerMessage: X, operationKey: entity.worker, related: workerId }
|
|
19
|
+
* - althea compares peerAddress{entity.worker, r=workerId} and X.owner{entity.worker, r=workerId}
|
|
20
|
+
* - Althea sends Response: ok (or close connection when compare does not match)
|
|
21
|
+
* - Target memoizes connection as "good"
|
|
22
|
+
* - Initiator makes Request: tell me all your secrets
|
|
23
|
+
* - Target sends Response: connection "good" ? response with secrets : close connection
|
|
24
|
+
*
|
|
25
|
+
* Note: abc{ok,r=xyz} means "the deproxied address of abc for operation key ok" with pxa related=xyz
|
|
3
26
|
*/
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
27
|
+
'use strict';
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const process = require('process');
|
|
31
|
+
const querystring = require('querystring');
|
|
32
|
+
const debug = require('debug');
|
|
9
33
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const listenHost = listenUrl.hostname === 'localhost' ? '::' : listenUrl.hostname;
|
|
34
|
+
const bootDate = new Date();
|
|
35
|
+
var worker;
|
|
36
|
+
var totalEarnings = 0;
|
|
14
37
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
/** Configuration for the server, must be mutated before a$init */
|
|
39
|
+
exports.config = { hostname: 'localhost', port: 9080 };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Web server initialization.
|
|
43
|
+
* @param {DistributiveWorker} __worker
|
|
44
|
+
* @returns {Promise} that is resolved when the server is listening
|
|
45
|
+
*/
|
|
46
|
+
exports.a$init = function init(__worker)
|
|
47
|
+
{
|
|
48
|
+
const protocol = require('dcp/protocol');
|
|
49
|
+
const identity = require('dcp/identity');
|
|
50
|
+
var p$target, reject;
|
|
51
|
+
|
|
52
|
+
try
|
|
53
|
+
{
|
|
54
|
+
worker = __worker;
|
|
55
|
+
console.info(` . Starting management service on ${exports.config.hostname}:${exports.config.port}`);
|
|
56
|
+
const target = new protocol.Target(
|
|
57
|
+
['socketio'],
|
|
58
|
+
{
|
|
59
|
+
identity: identity.get(),
|
|
60
|
+
listen: new URL(`http://${exports.config.hostname}:${exports.config.port}/dcp/`),
|
|
61
|
+
},
|
|
62
|
+
listeningHandler,
|
|
63
|
+
{ socketio: { module: require('socket.io') } },
|
|
64
|
+
);
|
|
65
|
+
target.constructor.setBufferCtor(Buffer);
|
|
66
|
+
target.on('error', (error) => {
|
|
67
|
+
console.error('Management service error:', error.message);
|
|
68
|
+
reject(error);
|
|
69
|
+
});
|
|
70
|
+
target.on('end', (conn) => debug('dcp-worker:dcp')('connection closed'));
|
|
71
|
+
target.on('connection', (conn) => debug('dcp-worker:dcp')('new connection'));
|
|
72
|
+
target.on('session', (conn) => debug('dcp-worker:dcp')('new session'));
|
|
73
|
+
target.on('session', (conn) => newSessionHandler(conn, worker));
|
|
74
|
+
target.on('error', (error) => console.log('Management service error:', error));
|
|
75
|
+
|
|
76
|
+
p$target = new Promise((resolve, __reject) => {
|
|
77
|
+
reject = __reject;
|
|
78
|
+
target.on('listening', () => resolve(target));
|
|
79
|
+
setupWebServer(target);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch(error)
|
|
83
|
+
{
|
|
84
|
+
throw new Error(`Unable to start management service: ${error.message}`);
|
|
85
|
+
}
|
|
27
86
|
|
|
28
|
-
|
|
87
|
+
worker.on('payment', function trackTotalEarnings(ev) {
|
|
88
|
+
const payment = parseFloat(ev);
|
|
89
|
+
if (!isNaN(payment))
|
|
90
|
+
totalEarnings += payment;
|
|
91
|
+
});
|
|
29
92
|
|
|
30
|
-
|
|
93
|
+
return p$target;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function listeningHandler()
|
|
31
97
|
{
|
|
32
|
-
|
|
33
|
-
scheduler: {
|
|
34
|
-
location: new URL('${dcpConfig.scheduler.location.href}'),
|
|
35
|
-
},
|
|
36
|
-
const listenUrl = dcpConfig.webConsole.listen;
|
|
37
|
-
worker: (${JSON.stringify(dcpConfig.worker)})
|
|
38
|
-
}`;
|
|
98
|
+
debug('dcp-worker:dcp')('web interface listening');
|
|
39
99
|
}
|
|
40
100
|
|
|
41
101
|
/**
|
|
42
|
-
*
|
|
102
|
+
* Handle new session on connection conn from application connected over dcp to management web server.
|
|
103
|
+
* Implements an event relay for events of worker.
|
|
43
104
|
*/
|
|
44
|
-
|
|
45
|
-
function handleHttpRequest(request, response)
|
|
105
|
+
function newSessionHandler(conn, _worker)
|
|
46
106
|
{
|
|
47
|
-
|
|
107
|
+
const KVIN = new (require('dcp/internal/kvin').KVIN)();
|
|
108
|
+
const capturedEvents = {};
|
|
109
|
+
|
|
110
|
+
conn.on('request', request => requestHandler(request, worker));
|
|
111
|
+
/**
|
|
112
|
+
* Handle a request; these operations are supported:
|
|
113
|
+
* - event-on: capture events for event in request.payload.data, returning eventHandlerId
|
|
114
|
+
* - event-off: stop capturing events corresponding to eventHandlerId
|
|
115
|
+
* - task-info: get the current task info object
|
|
116
|
+
* - run-info: get the current run info object
|
|
117
|
+
* Captured events are automatically uncaptured when the connection closes.
|
|
118
|
+
*/
|
|
119
|
+
function requestHandler(request, _worker)
|
|
48
120
|
{
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
121
|
+
try
|
|
122
|
+
{
|
|
123
|
+
switch(request.payload.operation)
|
|
124
|
+
{
|
|
125
|
+
case 'event-on':
|
|
126
|
+
{
|
|
127
|
+
const eventName = request.payload.data;
|
|
128
|
+
const eventHandler = function workerEventCapture(...args) {
|
|
129
|
+
const data = {
|
|
130
|
+
type: 'event',
|
|
131
|
+
name: eventName,
|
|
132
|
+
workerId: worker.id,
|
|
133
|
+
id: workerEventCapture.id,
|
|
134
|
+
kvinArgs: KVIN.stringify(args),
|
|
135
|
+
};
|
|
136
|
+
request.connection.notify(data).catch(error => conn.close());
|
|
137
|
+
};
|
|
138
|
+
eventHandler.id = request.id.match('[A-Za-z0-9]*$')[0]; /* unique id - used for event-off */
|
|
139
|
+
capturedEvents[eventHandler.id] = { eventName, eventHandler };
|
|
140
|
+
worker.setMaxListeners(worker.getMaxListeners() + 1);
|
|
141
|
+
worker.on(eventName, eventHandler);
|
|
142
|
+
request.respond({ eventHandlerId: eventHandler.id });
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case 'event-off':
|
|
146
|
+
{
|
|
147
|
+
const eventHandlerId = request.payload.data.eventHandlerId;
|
|
148
|
+
const { eventName, eventHandler } = capturedEvents[eventHandlerId];
|
|
149
|
+
delete capturedEvents[eventHandlerId];
|
|
150
|
+
worker.setMaxListeners(worker.getMaxListeners() - 1);
|
|
151
|
+
if (!eventHandler)
|
|
152
|
+
console.error('missing event handler for captured event', eventName, eventHandlerId); /* impossible? */
|
|
153
|
+
else
|
|
154
|
+
worker.off(eventName, eventHandler);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case 'task-info':
|
|
158
|
+
request.respond(worker.__stiCache || { jobs: [], tasks: [] });
|
|
159
|
+
break;
|
|
160
|
+
case 'run-info':
|
|
161
|
+
request.respond(runInfoObjectFactory());
|
|
162
|
+
break;
|
|
163
|
+
case 'worker-id':
|
|
164
|
+
request.response(worker.id);
|
|
165
|
+
break;
|
|
166
|
+
default:
|
|
167
|
+
throw new Error(`invalid operation '${request.payload.operation}'`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch(error)
|
|
171
|
+
{
|
|
172
|
+
request.respond(error);
|
|
173
|
+
}
|
|
52
174
|
}
|
|
53
175
|
|
|
54
|
-
|
|
176
|
+
/* Remove all our worker event handlers when client connection closes */
|
|
177
|
+
conn.on('end', () => {
|
|
178
|
+
for (let eventName of Object.keys(capturedEvents))
|
|
179
|
+
{
|
|
180
|
+
for (let i=0; i < capturedEvents[eventName].length; i++)
|
|
181
|
+
{
|
|
182
|
+
const { eventHandler } = capturedEvents[eventName][i];
|
|
183
|
+
worker.off(eventName, eventHandler);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const magic = {
|
|
190
|
+
html: 'text/html',
|
|
191
|
+
shtml: 'text/html',
|
|
192
|
+
txt: 'text/plain',
|
|
193
|
+
js: 'application/javascript',
|
|
194
|
+
mjs: 'application/javascript',
|
|
195
|
+
css: 'text/css',
|
|
196
|
+
gif: 'image/gif',
|
|
197
|
+
jpeg: 'image/jpeg',
|
|
198
|
+
jpg: 'image/jpeg',
|
|
199
|
+
png: 'image/png',
|
|
200
|
+
svg: 'image/svg+xml',
|
|
201
|
+
ico: 'image/vnd.microsoft.icon',
|
|
202
|
+
json: 'application/json',
|
|
203
|
+
kvin: 'application/x-kvin',
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
function setupWebServer(app)
|
|
207
|
+
{
|
|
208
|
+
const identity = require('dcp/identity');
|
|
209
|
+
const wallet = require('dcp/wallet');
|
|
210
|
+
const KVIN = new (require('dcp/internal/kvin').KVIN)();
|
|
211
|
+
|
|
212
|
+
KVIN.userCtors.dcpEth$$Address = wallet.Address;
|
|
213
|
+
KVIN.userCtors.dcpUrl$$DcpURL = require('dcp/dcp-url').DcpURL;
|
|
214
|
+
|
|
215
|
+
app.use((req, res) => {
|
|
216
|
+
const ipAddress = req.socket.address().address;
|
|
217
|
+
const originalUrl = req.originalUrl;
|
|
218
|
+
|
|
219
|
+
let endFired = false;
|
|
220
|
+
res.on('finish', () => {
|
|
221
|
+
req = req.httpRequest;
|
|
222
|
+
res = res.httpResponse;
|
|
223
|
+
endFired = true;
|
|
224
|
+
console.log(`${res.statusCode} ${req.method.toUpperCase()} ${req.headers.host} ${originalUrl} ` +
|
|
225
|
+
`${ipAddress} ${res._hasBody ? res._contentLength : '-'}`);
|
|
226
|
+
});
|
|
227
|
+
res.on('close', () => {
|
|
228
|
+
if (!endFired)
|
|
229
|
+
console.log(`${res.statusCode} ${req.method.toUpperCase()} ${req.headers.host} ${originalUrl} ` +
|
|
230
|
+
`${ipAddress} [abort]`);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
app.get('/favicon.ico', (req, res) => {
|
|
235
|
+
res.redirect(302, '/dcp-client/assets/favicon.ico');
|
|
236
|
+
});
|
|
237
|
+
app.get(/^(\/|\/admin\/?)$/, (req, res) => {
|
|
238
|
+
res.redirect(302, '/admin/index.html');
|
|
239
|
+
});
|
|
240
|
+
app.get(/^\/admin\/.+/, (req, res) => {
|
|
241
|
+
serveStaticContent(req, res, 'www');
|
|
242
|
+
});
|
|
243
|
+
app.get(/^(\/|\/hud\/?)$/, (req, res) => {
|
|
244
|
+
res.redirect(302, '/hud/index.html');
|
|
245
|
+
});
|
|
246
|
+
app.get(/^\/hud\/.+\.shtml/, (req, res) => {
|
|
247
|
+
serveStaticContent(req, res, 'www', { ssi: true });
|
|
248
|
+
});
|
|
249
|
+
app.get(/^\/hud\/.+/, (req, res) => {
|
|
250
|
+
serveStaticContent(req, res, 'www');
|
|
251
|
+
});
|
|
252
|
+
app.get(/^\/dcp-client\/.+/, (req, res) => {
|
|
253
|
+
serveStaticContent(req, res, path.dirname(require.resolve('dcp-client')), {
|
|
254
|
+
strip: '/dcp-client/', absoluteDocroot: true });
|
|
255
|
+
});
|
|
256
|
+
app.get('/etc/dcp-config.js', async (req, res) => {
|
|
257
|
+
res.set('Content-Type', magic.js);
|
|
258
|
+
res.send('window.dcpConfig = ' + await makeSkeletonConfig());
|
|
259
|
+
});
|
|
260
|
+
app.get(/\/worker\/?$/, (req, res) => {
|
|
261
|
+
res.redirect(302, '/worker/index.html');
|
|
262
|
+
});
|
|
263
|
+
app.get('/worker/id', (req, res) => {
|
|
264
|
+
res.set('Content-Type', 'text/plain');
|
|
265
|
+
res.send(worker.id);
|
|
266
|
+
});
|
|
267
|
+
app.get('/worker/pid', (req, res) => {
|
|
268
|
+
res.set('Content-Type', 'text/plain');
|
|
269
|
+
res.send(String(process.pid));
|
|
270
|
+
});
|
|
271
|
+
app.get('/worker/owner', (req, res) => {
|
|
272
|
+
res.set('Content-Type', 'text/plain');
|
|
273
|
+
res.send(String(identity.get().address));
|
|
274
|
+
});
|
|
275
|
+
app.get('/worker/task-info', (req, res) => {
|
|
276
|
+
res.set('Content-Type', 'application/javascript');
|
|
277
|
+
res.send('window.taskInfo=' + JSON.stringify(worker.__stiCache || { jobs: [], tasks: [] }));
|
|
278
|
+
});
|
|
279
|
+
app.get('/worker/run-info', (req, res) => {
|
|
280
|
+
res.set('Content-Type', 'application/javascript');
|
|
281
|
+
res.send('window.runInfo=' + JSON.stringify(runInfoObjectFactory()));
|
|
282
|
+
});
|
|
283
|
+
app.post('/worker/register', (req, res) => handlePostMessage(req, res, workerRegisterPostHandler));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Run handler function, then prepare a JSON response to a POST message. All responses have a 'success'
|
|
288
|
+
* property, which is true if the handler ran without rejecting. If the handler rejects, then the
|
|
289
|
+
* success property is false and the rejection argument (eg Error object) is copied to the response.
|
|
290
|
+
*
|
|
291
|
+
* The resolved value of the handler is added to the result object, unless it is an instance of Error. In
|
|
292
|
+
* that case, it is carefully copied into the result as a property named 'error'; this error property has
|
|
293
|
+
* all of the enumerable properties of the exception, plus message, name, stack, and code (if defined).
|
|
294
|
+
*
|
|
295
|
+
* @param {PseudoExpressRequest} request
|
|
296
|
+
* @param {PseudoExpressResponse} response
|
|
297
|
+
* @param {function} handler - receives query object
|
|
298
|
+
*/
|
|
299
|
+
async function handlePostMessage(request, response, handler)
|
|
300
|
+
{
|
|
301
|
+
const responseBody = { success: false };
|
|
302
|
+
|
|
303
|
+
response.set('access-control-allow-origin', '*');
|
|
304
|
+
response.set('access-control-allow-headers', '*');
|
|
305
|
+
|
|
306
|
+
/* dest, src */
|
|
307
|
+
function copyError(obj, error)
|
|
55
308
|
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
309
|
+
if (!obj)
|
|
310
|
+
obj = {};
|
|
311
|
+
Object.assign(obj, error);
|
|
312
|
+
obj.message = error.message;
|
|
313
|
+
obj.name = error.name;
|
|
314
|
+
obj.stack = error.stack;
|
|
315
|
+
if (error.code)
|
|
316
|
+
obj.code = error.code;
|
|
59
317
|
}
|
|
60
318
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
319
|
+
try
|
|
320
|
+
{
|
|
321
|
+
if (request.headers['content-type'] !== 'application/x-www-form-urlencoded')
|
|
322
|
+
throw new Error(`invalid request content-type '${request.headers['content-type']}'`);
|
|
64
323
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
324
|
+
const query = querystring.parse(request.body.toString('utf-8'));
|
|
325
|
+
const retval = await handler(query, request, response);
|
|
326
|
+
responseBody.success = true;
|
|
327
|
+
if (retval instanceof Error) /* error reported by handler */
|
|
328
|
+
copyError((responseBody.error = {}), retval);
|
|
329
|
+
else
|
|
330
|
+
Object.assign(responseBody, retval);
|
|
331
|
+
}
|
|
332
|
+
catch(error)
|
|
68
333
|
{
|
|
69
|
-
|
|
70
|
-
|
|
334
|
+
responseBody.success = false;
|
|
335
|
+
copyError(responseBody, error);
|
|
71
336
|
}
|
|
72
337
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!filename.startsWith(contentDir) || filename[0] !== '/')
|
|
76
|
-
errorStatus = 401;
|
|
77
|
-
else if (!fs.existsSync(filename))
|
|
78
|
-
errorStatus = 404;
|
|
338
|
+
response.json(responseBody);
|
|
339
|
+
}
|
|
79
340
|
|
|
80
|
-
|
|
341
|
+
/**
|
|
342
|
+
* When we receive the registration post message, we respond with a JSON string that is designed for
|
|
343
|
+
* submitPost. The string deserializes to an object which has properties
|
|
344
|
+
* - status: true if successful, false if this function threw (see handlePostMessage)
|
|
345
|
+
* - error: if not successful because an error was thrown, this is an Error-like object like it
|
|
346
|
+
* - failExpn: if not successful because of another reason, this message explains why
|
|
347
|
+
* - pid: if successful, this is the process id of the worker that accepted the request.
|
|
348
|
+
* - owner: if successful, this the string value of the wallet.Address of the registered owner
|
|
349
|
+
*
|
|
350
|
+
* Once successful, the client polls /worker/pid to know that it has rebooted.
|
|
351
|
+
*/
|
|
352
|
+
async function workerRegisterPostHandler(query, req, res)
|
|
353
|
+
{
|
|
354
|
+
var failExpn;
|
|
355
|
+
const utils = require('./utils');
|
|
356
|
+
const reg = await utils.registerWorker(worker, query.rkey, msg => failExpn = msg);
|
|
357
|
+
reg.failExpn = failExpn;
|
|
358
|
+
reg.pid = process.pid;
|
|
359
|
+
|
|
360
|
+
/* Restart the worker as soon as the HTTP response with the pid has been sent to the management UI */
|
|
361
|
+
function restartSelf()
|
|
81
362
|
{
|
|
82
|
-
|
|
83
|
-
response.statusCode = errorStatus;
|
|
84
|
-
response.end(`${errorStatus} accessing ${filename}`);
|
|
85
|
-
return;
|
|
363
|
+
require('../lib/pidfile').signal(worker.config.pidfile, 'SIGQUIT', 30, 'stopping worker on pid %i');
|
|
86
364
|
}
|
|
365
|
+
res.on('finish', restartSelf);
|
|
366
|
+
setTimeout(restartSelf, 30e3).unref();
|
|
87
367
|
|
|
88
|
-
|
|
89
|
-
response.end(fs.readFileSync(filename));
|
|
368
|
+
return reg; /* object that came back from the scheduler when we posted the registration request */
|
|
90
369
|
}
|
|
91
370
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
371
|
+
function filenameFromPathname(docroot, pathname, strip)
|
|
372
|
+
{
|
|
373
|
+
if (strip)
|
|
374
|
+
{
|
|
375
|
+
if (!pathname.startsWith(strip))
|
|
376
|
+
throw new Error(`invalid pathname, does not start with '${strip}'`);
|
|
377
|
+
pathname = pathname.slice(strip.length);
|
|
378
|
+
}
|
|
379
|
+
const filename = path.join(docroot, pathname);
|
|
380
|
+
if (!filename.startsWith(docroot) || filename[docroot.length] !== '/')
|
|
381
|
+
throw new Error(`invalid pathname ${pathname} ${filename} ${docroot}`);
|
|
382
|
+
return filename;
|
|
383
|
+
}
|
|
95
384
|
|
|
96
385
|
/**
|
|
97
|
-
*
|
|
386
|
+
* relative docroot means relative to dcp-worker package dir
|
|
387
|
+
* @param {object} options options object with properties:
|
|
388
|
+
* - strip: chars to strip from pathname before joining to docroot
|
|
389
|
+
* - ssi: enable server-side-includes
|
|
98
390
|
*/
|
|
99
|
-
|
|
100
|
-
const wsServer = new WebSocketServer({
|
|
101
|
-
httpServer: server,
|
|
102
|
-
autoAcceptConnections: false
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
wsServer.on('request', handleWssRequest);
|
|
106
|
-
function handleWssRequest(request)
|
|
391
|
+
function serveStaticContent(req, res, docroot, options)
|
|
107
392
|
{
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
393
|
+
const hash = require('dcp/utils').hash;
|
|
394
|
+
const pathname = req.url.match('^[^?#]*')[0];
|
|
395
|
+
// @TODO: security issue: we are allowing reaching above the dcp-worker folder for serving webcontent.
|
|
396
|
+
// Repro for why this is required: try creating a new folder, and installing dcp-worker. npm will install
|
|
397
|
+
// dcp-worker and dcp-client in the node_modules folder, NOT having dcp-worker/node_modules/dcp-client.
|
|
398
|
+
// Thus, to serve this dcp-client, we need to reach above the dcp-worker root.
|
|
399
|
+
if (!options?.absoluteDocroot)
|
|
400
|
+
docroot = path.resolve(path.join(path.dirname(require.main.filename), '..', docroot));
|
|
401
|
+
|
|
402
|
+
const filename = filenameFromPathname(docroot, pathname, options?.strip);
|
|
403
|
+
const contentType = magic[path.extname(filename).slice(1)] || 'application/octet-stream';
|
|
404
|
+
|
|
405
|
+
try
|
|
120
406
|
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
407
|
+
let fileDate;
|
|
408
|
+
const cacheDate = !options?.ssi && (req.headers['if-modified-since'] && new Date(req.headers['if-modified-since']));
|
|
409
|
+
const nowDate = new Date();
|
|
410
|
+
|
|
411
|
+
if (cacheDate)
|
|
412
|
+
{
|
|
413
|
+
fileDate = Math.max(bootDate, fs.statSync(filename).mtime);
|
|
414
|
+
if (!options?.ssi && cacheDate < nowDate && cacheDate > fileDate && cacheDate > bootDate)
|
|
415
|
+
{
|
|
416
|
+
/* fast path to 304 without reading file in non-SSI content */
|
|
417
|
+
res.status(304).send('Not Modified');
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** @todo add content log hook to PseudoExpress */
|
|
423
|
+
let contents = fs.readFileSync(filename);
|
|
424
|
+
if (options?.ssi)
|
|
425
|
+
{
|
|
426
|
+
contents = contents.toString('utf-8');
|
|
427
|
+
const directives = contents.match(/<!--#include virtual="(.[^"]+)" -->/g);
|
|
428
|
+
for (let directive of directives)
|
|
429
|
+
{
|
|
430
|
+
try
|
|
431
|
+
{
|
|
432
|
+
let inclPathname = directive.match(/"[^"]+"/)[0].slice(1, -1);
|
|
433
|
+
if (inclPathname[0] === '/')
|
|
434
|
+
inclPathname = inclPathname.slice(1);
|
|
435
|
+
else
|
|
436
|
+
inclPathname = path.resolve(path.dirname(pathname), inclPathname);
|
|
437
|
+
const insert = fs.readFileSync(filenameFromPathname(docroot, inclPathname));
|
|
438
|
+
contents = contents.replace(directive, insert);
|
|
439
|
+
}
|
|
440
|
+
catch(error)
|
|
441
|
+
{
|
|
442
|
+
throw new Error(`error '${error.message}' in ssi directive '${directive.slice(2,-2)}'`);
|
|
443
|
+
}
|
|
124
444
|
|
|
125
|
-
|
|
445
|
+
if (cacheDate)
|
|
446
|
+
fileDate = Math.max(bootDate, fileDate, fs.statSync(filename).mtime);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (cacheDate && !options?.ssi && cacheDate < nowDate && cacheDate > fileDate && cacheDate > bootDate)
|
|
451
|
+
{
|
|
452
|
+
res.status(304).send('Not Modified');
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const etag = `"${hash.calculate(hash.eh0, contents)}"`;
|
|
457
|
+
if (req.headers['if-none-match'] && req.headers['if-none-match'].split('/, ?/').includes(etag))
|
|
458
|
+
res.status(304).send('Not Modified');
|
|
459
|
+
else
|
|
460
|
+
{
|
|
461
|
+
res.set('etag', etag);
|
|
462
|
+
res.set('Content-Type', contentType);
|
|
463
|
+
res.send(contents);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch(error)
|
|
126
467
|
{
|
|
468
|
+
res.set('Content-Type', 'text/plain');
|
|
469
|
+
switch(error.code)
|
|
470
|
+
{
|
|
471
|
+
case 'ENOENT':
|
|
472
|
+
res.status(404).send('Not found: ' + pathname);
|
|
473
|
+
break;
|
|
474
|
+
default:
|
|
475
|
+
res.status(500).send('Internal Server Error ' + (error.code || '') + '\n' + error.stack);
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
res.end();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function runInfoObjectFactory()
|
|
483
|
+
{
|
|
484
|
+
return {
|
|
485
|
+
workerId: worker.id,
|
|
486
|
+
startTime: Date.now() - performance.now(),
|
|
487
|
+
totalEarnings,
|
|
488
|
+
earningsAccount: worker.config.paymentAddress,
|
|
489
|
+
maxSandboxes: worker.config.maxSandboxes,
|
|
490
|
+
minimumWage: worker.config.minimumWage,
|
|
491
|
+
cores: Object.assign({}, worker.config.cores,
|
|
492
|
+
{ gpu: worker.runInfo.gpuCores, gpuType: worker.runInfo.gpuType }),
|
|
493
|
+
gpuType: worker.runInfo.gpuType || '',
|
|
127
494
|
}
|
|
128
495
|
}
|
|
496
|
+
|
|
497
|
+
async function makeSkeletonConfig()
|
|
498
|
+
{
|
|
499
|
+
const owner = await require('./utils').getOwner(worker);
|
|
500
|
+
const ownerStr = owner ? `'${String(owner)}'` : 'undefined';
|
|
501
|
+
|
|
502
|
+
return `window.dcpConfig = {
|
|
503
|
+
scheduler: {
|
|
504
|
+
location: new URL('${dcpConfig.scheduler.location.href}'),
|
|
505
|
+
},
|
|
506
|
+
bank: {
|
|
507
|
+
location: new URL('${dcpConfig.bank.location.href}'),
|
|
508
|
+
},
|
|
509
|
+
portal: {
|
|
510
|
+
location: new URL('${dcpConfig.portal.location.href}'),
|
|
511
|
+
},
|
|
512
|
+
cdn: {
|
|
513
|
+
location: new URL('${dcpConfig.cdn.location.href}'),
|
|
514
|
+
},
|
|
515
|
+
pxAuth: {
|
|
516
|
+
location: new URL('${dcpConfig.pxAuth.location.href}'),
|
|
517
|
+
},
|
|
518
|
+
oAuth: {
|
|
519
|
+
identityIntegration: false,
|
|
520
|
+
walletIntegration: false,
|
|
521
|
+
},
|
|
522
|
+
worker: {
|
|
523
|
+
id: '${worker.id}',
|
|
524
|
+
_owner: ${ownerStr},
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
window.addEventListener('dcpclientready', () => {
|
|
529
|
+
dcpConfig.worker.owner = dcpConfig.worker._owner && new dcp.wallet.Address(dcpConfig.worker._owner);
|
|
530
|
+
});
|
|
531
|
+
`;
|
|
532
|
+
}
|
|
@@ -346,12 +346,19 @@ exports.init = function dashboardConsole$$Init()
|
|
|
346
346
|
|
|
347
347
|
/**
|
|
348
348
|
* throb API: calls with arguments set facility and message. First call without argument emits the
|
|
349
|
-
* message. All calls without arguments advance the throbber.
|
|
349
|
+
* message. All calls without arguments advance the throbber. Null cancels the throbber.
|
|
350
350
|
*/
|
|
351
351
|
console.throb = function dashboardConsole$$throb(...args)
|
|
352
352
|
{
|
|
353
353
|
const throb = dashboardConsole$$throb;
|
|
354
354
|
var throbPos = throb.pos || 0;
|
|
355
|
+
if (args[0] === null && args.length === 1)
|
|
356
|
+
{
|
|
357
|
+
if (typeof throb.pos === 'number')
|
|
358
|
+
logPane.advanceThrob(' ');
|
|
359
|
+
throb.pos = undefined;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
355
362
|
if (args.length)
|
|
356
363
|
logPane.log(...args);
|
|
357
364
|
const throbChars = '/-\\|';
|
|
@@ -45,12 +45,19 @@ exports.init = function stdioConsole$$init(options)
|
|
|
45
45
|
const throbChars = '/-\\|';
|
|
46
46
|
/**
|
|
47
47
|
* throb API: Writes message to stdout with a newline. Throb char is appended to message. If no message,
|
|
48
|
-
* advance the previous throb char.
|
|
48
|
+
* advance the previous throb char. Null cancels the throbber.
|
|
49
49
|
*/
|
|
50
50
|
console.throb = function throb(...args) {
|
|
51
|
-
if (args.length && lastWasThrobber)
|
|
51
|
+
if ((args.length && lastWasThrobber) || args[0] === null)
|
|
52
52
|
stdout.write(' \n');
|
|
53
53
|
|
|
54
|
+
if (args[0] === null && args.length === 1)
|
|
55
|
+
{
|
|
56
|
+
lastWasThrobber = false;
|
|
57
|
+
throbIdx = 0;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
for (let i=0; i < args.length; i++)
|
|
55
62
|
{
|
|
56
63
|
if (i)
|