react-native-debug-toolkit 3.0.0 → 3.1.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/README.md +115 -97
- package/README.zh-CN.md +113 -95
- package/lib/commonjs/core/initialize.js +5 -0
- package/lib/commonjs/core/initialize.js.map +1 -1
- package/lib/commonjs/index.js +23 -26
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js +24 -58
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +1 -1
- package/lib/commonjs/utils/DaemonClient.js +721 -0
- package/lib/commonjs/utils/DaemonClient.js.map +1 -0
- package/lib/commonjs/utils/{sessionReport.js → deviceReport.js} +3 -3
- package/lib/commonjs/utils/deviceReport.js.map +1 -0
- package/lib/module/core/initialize.js +6 -0
- package/lib/module/core/initialize.js.map +1 -1
- package/lib/module/index.js +3 -5
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/panel/StreamingSettingsModal.js +21 -55
- package/lib/module/ui/panel/StreamingSettingsModal.js.map +1 -1
- package/lib/module/utils/DaemonClient.js +703 -0
- package/lib/module/utils/DaemonClient.js.map +1 -0
- package/lib/module/utils/{sessionReport.js → deviceReport.js} +2 -2
- package/lib/module/utils/deviceReport.js.map +1 -0
- package/lib/typescript/src/core/initialize.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -10
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +1 -1
- package/lib/typescript/src/utils/DaemonClient.d.ts +141 -0
- package/lib/typescript/src/utils/DaemonClient.d.ts.map +1 -0
- package/lib/typescript/src/utils/{sessionReport.d.ts → deviceReport.d.ts} +4 -4
- package/lib/typescript/src/utils/deviceReport.d.ts.map +1 -0
- package/node/daemon/src/cli.js +9 -2
- package/node/daemon/src/console/console.html +914 -188
- package/node/daemon/src/constants.js +6 -0
- package/node/daemon/src/server.js +205 -123
- package/node/daemon/src/store.js +122 -45
- package/node/mcp/src/daemonClient.js +6 -6
- package/node/mcp/src/index.js +2 -2
- package/node/mcp/src/logs.js +5 -4
- package/node/mcp/src/tools.js +16 -16
- package/package.json +2 -2
- package/src/core/initialize.ts +8 -0
- package/src/index.ts +18 -10
- package/src/ui/panel/StreamingSettingsModal.tsx +25 -63
- package/src/utils/DaemonClient.ts +887 -0
- package/src/utils/{sessionReport.ts → deviceReport.ts} +6 -6
- package/lib/commonjs/utils/autoDetectDaemon.js +0 -141
- package/lib/commonjs/utils/autoDetectDaemon.js.map +0 -1
- package/lib/commonjs/utils/daemonConnection.js +0 -81
- package/lib/commonjs/utils/daemonConnection.js.map +0 -1
- package/lib/commonjs/utils/daemonSettings.js +0 -110
- package/lib/commonjs/utils/daemonSettings.js.map +0 -1
- package/lib/commonjs/utils/reportToDaemon.js +0 -112
- package/lib/commonjs/utils/reportToDaemon.js.map +0 -1
- package/lib/commonjs/utils/sessionReport.js.map +0 -1
- package/lib/commonjs/utils/streamToDaemon.js +0 -334
- package/lib/commonjs/utils/streamToDaemon.js.map +0 -1
- package/lib/module/utils/autoDetectDaemon.js +0 -136
- package/lib/module/utils/autoDetectDaemon.js.map +0 -1
- package/lib/module/utils/daemonConnection.js +0 -77
- package/lib/module/utils/daemonConnection.js.map +0 -1
- package/lib/module/utils/daemonSettings.js +0 -102
- package/lib/module/utils/daemonSettings.js.map +0 -1
- package/lib/module/utils/reportToDaemon.js +0 -105
- package/lib/module/utils/reportToDaemon.js.map +0 -1
- package/lib/module/utils/sessionReport.js.map +0 -1
- package/lib/module/utils/streamToDaemon.js +0 -328
- package/lib/module/utils/streamToDaemon.js.map +0 -1
- package/lib/typescript/src/utils/autoDetectDaemon.d.ts +0 -15
- package/lib/typescript/src/utils/autoDetectDaemon.d.ts.map +0 -1
- package/lib/typescript/src/utils/daemonConnection.d.ts +0 -18
- package/lib/typescript/src/utils/daemonConnection.d.ts.map +0 -1
- package/lib/typescript/src/utils/daemonSettings.d.ts +0 -19
- package/lib/typescript/src/utils/daemonSettings.d.ts.map +0 -1
- package/lib/typescript/src/utils/reportToDaemon.d.ts +0 -34
- package/lib/typescript/src/utils/reportToDaemon.d.ts.map +0 -1
- package/lib/typescript/src/utils/sessionReport.d.ts.map +0 -1
- package/lib/typescript/src/utils/streamToDaemon.d.ts +0 -23
- package/lib/typescript/src/utils/streamToDaemon.d.ts.map +0 -1
- package/src/utils/autoDetectDaemon.ts +0 -175
- package/src/utils/daemonConnection.ts +0 -133
- package/src/utils/daemonSettings.ts +0 -134
- package/src/utils/reportToDaemon.ts +0 -172
- package/src/utils/streamToDaemon.ts +0 -419
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
4
5
|
|
|
5
6
|
const DEFAULT_HOST = '0.0.0.0';
|
|
6
7
|
const DEFAULT_PORT = 3799;
|
|
@@ -22,11 +23,16 @@ function getLanIPs() {
|
|
|
22
23
|
return ips;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
function getDefaultDeviceStorePath() {
|
|
27
|
+
return path.join(os.homedir(), '.react-native-debug-toolkit', 'daemon-devices.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
module.exports = {
|
|
26
31
|
DAEMON_NAME,
|
|
27
32
|
DAEMON_VERSION,
|
|
28
33
|
DEFAULT_HOST,
|
|
29
34
|
DEFAULT_PORT,
|
|
30
35
|
REPORT_PROTOCOL_VERSION,
|
|
36
|
+
getDefaultDeviceStorePath,
|
|
31
37
|
getLanIPs,
|
|
32
38
|
};
|
|
@@ -94,21 +94,54 @@ function authorizeRequest(req, res, url, token) {
|
|
|
94
94
|
return false;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function
|
|
98
|
-
if (!
|
|
97
|
+
function normalizeIpAddress(value) {
|
|
98
|
+
if (!value || typeof value !== 'string') {
|
|
99
99
|
return null;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
if (value.startsWith('::ffff:')) {
|
|
103
|
+
return value.slice(7);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getRequestSource(req) {
|
|
110
|
+
const forwarded = req.headers['x-forwarded-for'];
|
|
111
|
+
const forwardedIp = Array.isArray(forwarded)
|
|
112
|
+
? forwarded[0]
|
|
113
|
+
: typeof forwarded === 'string'
|
|
114
|
+
? forwarded.split(',')[0]
|
|
115
|
+
: null;
|
|
116
|
+
const ip = normalizeIpAddress((forwardedIp || '').trim()) ||
|
|
117
|
+
normalizeIpAddress(req.socket && req.socket.remoteAddress);
|
|
118
|
+
const userAgent = req.headers['user-agent'];
|
|
119
|
+
|
|
102
120
|
return {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
logCount: session.logCount,
|
|
106
|
-
report: session.report,
|
|
121
|
+
ip: ip || 'unknown',
|
|
122
|
+
userAgent: typeof userAgent === 'string' ? userAgent : '',
|
|
107
123
|
};
|
|
108
124
|
}
|
|
109
125
|
|
|
110
|
-
function
|
|
111
|
-
if (!
|
|
126
|
+
function toDevicePayload(deviceLog) {
|
|
127
|
+
if (!deviceLog) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
deviceId: deviceLog.deviceId,
|
|
133
|
+
firstSeenAt: deviceLog.firstSeenAt,
|
|
134
|
+
lastSeenAt: deviceLog.lastSeenAt,
|
|
135
|
+
receivedAt: deviceLog.receivedAt,
|
|
136
|
+
device: deviceLog.device || null,
|
|
137
|
+
source: deviceLog.source || null,
|
|
138
|
+
logCount: deviceLog.logCount,
|
|
139
|
+
report: deviceLog.report,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function selectLogs(deviceLog, searchParams) {
|
|
144
|
+
if (!deviceLog) {
|
|
112
145
|
return [];
|
|
113
146
|
}
|
|
114
147
|
|
|
@@ -118,7 +151,7 @@ function selectLogs(session, searchParams) {
|
|
|
118
151
|
? Math.min(Math.floor(limitParam), 500)
|
|
119
152
|
: 50;
|
|
120
153
|
const failedOnly = searchParams.get('failedOnly') === 'true';
|
|
121
|
-
const logs =
|
|
154
|
+
const logs = deviceLog.report.logs || {};
|
|
122
155
|
|
|
123
156
|
let entries = [];
|
|
124
157
|
if (type) {
|
|
@@ -143,12 +176,18 @@ function selectLogs(session, searchParams) {
|
|
|
143
176
|
return entries.slice(-limit);
|
|
144
177
|
}
|
|
145
178
|
|
|
146
|
-
function broadcastSSE(clients, eventType,
|
|
179
|
+
function broadcastSSE(clients, eventType, deviceLog, delta) {
|
|
147
180
|
if (clients.size === 0) return;
|
|
148
181
|
|
|
149
182
|
const payload = delta
|
|
150
|
-
? { type: 'delta',
|
|
151
|
-
: {
|
|
183
|
+
? { type: 'delta', deviceId: deviceLog.deviceId, delta, logCount: deviceLog.logCount }
|
|
184
|
+
: {
|
|
185
|
+
type: 'full',
|
|
186
|
+
deviceId: deviceLog.deviceId,
|
|
187
|
+
logCount: deviceLog.logCount,
|
|
188
|
+
device: deviceLog.device || null,
|
|
189
|
+
source: deviceLog.source || null,
|
|
190
|
+
};
|
|
152
191
|
const data = JSON.stringify(payload);
|
|
153
192
|
|
|
154
193
|
clients.forEach((client) => {
|
|
@@ -160,14 +199,136 @@ function broadcastSSE(clients, eventType, session, delta) {
|
|
|
160
199
|
});
|
|
161
200
|
}
|
|
162
201
|
|
|
202
|
+
// --- Route Handlers ---
|
|
203
|
+
|
|
204
|
+
function handleHealth(req, res, ctx) {
|
|
205
|
+
sendJson(res, 200, {
|
|
206
|
+
ok: true,
|
|
207
|
+
name: DAEMON_NAME,
|
|
208
|
+
version: DAEMON_VERSION,
|
|
209
|
+
protocolVersion: REPORT_PROTOCOL_VERSION,
|
|
210
|
+
ips: getLanIPs(),
|
|
211
|
+
deviceStore: ctx.options.deviceStorePath || null,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function handleReport(req, res, ctx) {
|
|
216
|
+
if (!authorizeRequest(req, res, req.url, ctx.token)) return;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const report = await readJsonBody(req);
|
|
220
|
+
if (!isValidReport(report)) {
|
|
221
|
+
sendJson(res, 400, {
|
|
222
|
+
ok: false,
|
|
223
|
+
error: `Report must include version ${REPORT_PROTOCOL_VERSION} and logs object`,
|
|
224
|
+
});
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const deviceLog = ctx.store.saveReport(report, { source: getRequestSource(req) });
|
|
229
|
+
sendJson(res, 200, {
|
|
230
|
+
ok: true,
|
|
231
|
+
deviceId: deviceLog.deviceId,
|
|
232
|
+
receivedAt: deviceLog.receivedAt,
|
|
233
|
+
logCount: deviceLog.logCount,
|
|
234
|
+
});
|
|
235
|
+
} catch (error) {
|
|
236
|
+
sendJson(res, error.statusCode || 500, {
|
|
237
|
+
ok: false,
|
|
238
|
+
error: error.message || 'Failed to read report',
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function handleIngest(req, res, ctx) {
|
|
244
|
+
if (!authorizeRequest(req, res, req.url, ctx.token)) return;
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const body = await readJsonBody(req);
|
|
248
|
+
if (!body || typeof body.deviceId !== 'string' || !body.delta) {
|
|
249
|
+
sendJson(res, 400, { ok: false, error: 'Must include deviceId and delta' });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const deviceLog = ctx.store.appendLogs(body.deviceId, body.delta);
|
|
254
|
+
if (!deviceLog) {
|
|
255
|
+
sendJson(res, 404, { ok: false, error: 'Device not found' });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
sendJson(res, 200, { ok: true, deviceId: deviceLog.deviceId, logCount: deviceLog.logCount });
|
|
260
|
+
} catch (error) {
|
|
261
|
+
sendJson(res, error.statusCode || 500, {
|
|
262
|
+
ok: false,
|
|
263
|
+
error: error.message || 'Failed to ingest',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function handleLatestDevice(req, res, ctx) {
|
|
269
|
+
if (!authorizeRequest(req, res, req.url, ctx.token)) return;
|
|
270
|
+
|
|
271
|
+
const deviceLog = ctx.store.getLatestDevice();
|
|
272
|
+
if (!deviceLog) {
|
|
273
|
+
sendJson(res, 404, { ok: false, error: 'No device logs have been received' });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
sendJson(res, 200, {
|
|
278
|
+
ok: true,
|
|
279
|
+
...toDevicePayload(deviceLog),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function handleDevicesList(req, res, ctx) {
|
|
284
|
+
if (!authorizeRequest(req, res, req.url, ctx.token)) return;
|
|
285
|
+
|
|
286
|
+
sendJson(res, 200, { ok: true, devices: ctx.store.listDevices() });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function handleDevicesClear(req, res, ctx) {
|
|
290
|
+
if (!authorizeRequest(req, res, req.url, ctx.token)) return;
|
|
291
|
+
|
|
292
|
+
ctx.store.clear();
|
|
293
|
+
sendJson(res, 200, { ok: true });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function handleDeviceMatch(req, res, ctx, url, deviceMatch) {
|
|
297
|
+
if (!authorizeRequest(req, res, url, ctx.token)) return;
|
|
298
|
+
|
|
299
|
+
const deviceLog = ctx.store.getDevice(decodeURIComponent(deviceMatch[1]));
|
|
300
|
+
if (!deviceLog) {
|
|
301
|
+
sendJson(res, 404, { ok: false, error: 'Device not found' });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (url.pathname.endsWith('/logs')) {
|
|
306
|
+
sendJson(res, 200, {
|
|
307
|
+
ok: true,
|
|
308
|
+
deviceId: deviceLog.deviceId,
|
|
309
|
+
receivedAt: deviceLog.receivedAt,
|
|
310
|
+
logs: selectLogs(deviceLog, url.searchParams),
|
|
311
|
+
});
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
sendJson(res, 200, {
|
|
316
|
+
ok: true,
|
|
317
|
+
...toDevicePayload(deviceLog),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// --- Server Factory ---
|
|
322
|
+
|
|
163
323
|
function createDaemonServer(options = {}) {
|
|
164
324
|
const sseClients = new Set();
|
|
165
325
|
const maxSseClients = options.maxSseClients || MAX_SSE_CLIENTS;
|
|
166
326
|
let keepaliveTimer = null;
|
|
167
327
|
|
|
168
328
|
const store = options.store || createMemoryStore({
|
|
169
|
-
|
|
170
|
-
|
|
329
|
+
storagePath: options.deviceStorePath || null,
|
|
330
|
+
onUpdate(deviceLog, type, delta) {
|
|
331
|
+
broadcastSSE(sseClients, 'logs', deviceLog, type === 'delta' ? delta : null);
|
|
171
332
|
},
|
|
172
333
|
});
|
|
173
334
|
const token = options.token || null;
|
|
@@ -175,9 +336,12 @@ function createDaemonServer(options = {}) {
|
|
|
175
336
|
authorize: (req, res, url) => authorizeRequest(req, res, url, token),
|
|
176
337
|
});
|
|
177
338
|
|
|
339
|
+
const ctx = { store, token, options };
|
|
340
|
+
|
|
178
341
|
const server = http.createServer(async (req, res) => {
|
|
179
342
|
const url = new URL(req.url || '/', 'http://127.0.0.1');
|
|
180
343
|
const method = req.method || 'GET';
|
|
344
|
+
req.url = url;
|
|
181
345
|
|
|
182
346
|
if (handleConsole(req, res, url, method)) return;
|
|
183
347
|
|
|
@@ -197,9 +361,15 @@ function createDaemonServer(options = {}) {
|
|
|
197
361
|
sseClients.add(res);
|
|
198
362
|
req.on('close', () => { sseClients.delete(res); });
|
|
199
363
|
|
|
200
|
-
const latest = store.
|
|
364
|
+
const latest = store.getLatestDevice();
|
|
201
365
|
if (latest) {
|
|
202
|
-
const data = JSON.stringify({
|
|
366
|
+
const data = JSON.stringify({
|
|
367
|
+
type: 'full',
|
|
368
|
+
deviceId: latest.deviceId,
|
|
369
|
+
logCount: latest.logCount,
|
|
370
|
+
device: latest.device || null,
|
|
371
|
+
source: latest.source || null,
|
|
372
|
+
});
|
|
203
373
|
res.write(`event: logs\ndata: ${data}\n\n`);
|
|
204
374
|
}
|
|
205
375
|
|
|
@@ -219,127 +389,32 @@ function createDaemonServer(options = {}) {
|
|
|
219
389
|
}
|
|
220
390
|
|
|
221
391
|
if (method === 'GET' && url.pathname === '/health') {
|
|
222
|
-
|
|
223
|
-
ok: true,
|
|
224
|
-
name: DAEMON_NAME,
|
|
225
|
-
version: DAEMON_VERSION,
|
|
226
|
-
protocolVersion: REPORT_PROTOCOL_VERSION,
|
|
227
|
-
ips: getLanIPs(),
|
|
228
|
-
});
|
|
229
|
-
return;
|
|
392
|
+
return handleHealth(req, res, ctx);
|
|
230
393
|
}
|
|
231
394
|
|
|
232
395
|
if (method === 'POST' && url.pathname === '/report') {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
const report = await readJsonBody(req);
|
|
237
|
-
if (!isValidReport(report)) {
|
|
238
|
-
sendJson(res, 400, {
|
|
239
|
-
ok: false,
|
|
240
|
-
error: `Report must include version ${REPORT_PROTOCOL_VERSION} and logs object`,
|
|
241
|
-
});
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const session = store.saveReport(report);
|
|
246
|
-
sendJson(res, 200, {
|
|
247
|
-
ok: true,
|
|
248
|
-
sessionId: session.sessionId,
|
|
249
|
-
receivedAt: session.receivedAt,
|
|
250
|
-
logCount: session.logCount,
|
|
251
|
-
});
|
|
252
|
-
} catch (error) {
|
|
253
|
-
sendJson(res, error.statusCode || 500, {
|
|
254
|
-
ok: false,
|
|
255
|
-
error: error.message || 'Failed to read report',
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
return;
|
|
396
|
+
return await handleReport(req, res, ctx);
|
|
259
397
|
}
|
|
260
398
|
|
|
261
399
|
if (method === 'POST' && url.pathname === '/ingest') {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
const body = await readJsonBody(req);
|
|
266
|
-
if (!body || typeof body.sessionId !== 'string' || !body.delta) {
|
|
267
|
-
sendJson(res, 400, { ok: false, error: 'Must include sessionId and delta' });
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const session = store.appendLogs(body.sessionId, body.delta);
|
|
272
|
-
if (!session) {
|
|
273
|
-
sendJson(res, 404, { ok: false, error: 'Session not found' });
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
sendJson(res, 200, { ok: true, sessionId: session.sessionId, logCount: session.logCount });
|
|
278
|
-
} catch (error) {
|
|
279
|
-
sendJson(res, error.statusCode || 500, {
|
|
280
|
-
ok: false,
|
|
281
|
-
error: error.message || 'Failed to ingest',
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
return;
|
|
400
|
+
return await handleIngest(req, res, ctx);
|
|
285
401
|
}
|
|
286
402
|
|
|
287
|
-
if (method === 'GET' &&
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const session = store.getLatestSession();
|
|
291
|
-
if (!session) {
|
|
292
|
-
sendJson(res, 404, { ok: false, error: 'No debug session report has been received' });
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
sendJson(res, 200, {
|
|
297
|
-
ok: true,
|
|
298
|
-
...toSessionPayload(session),
|
|
299
|
-
});
|
|
300
|
-
return;
|
|
403
|
+
if (method === 'GET' && url.pathname === '/devices/latest') {
|
|
404
|
+
return handleLatestDevice(req, res, ctx);
|
|
301
405
|
}
|
|
302
406
|
|
|
303
|
-
if (method === 'GET' && url.pathname === '/
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
sendJson(res, 200, { ok: true, sessions: store.listSessions() });
|
|
307
|
-
return;
|
|
407
|
+
if (method === 'GET' && url.pathname === '/devices') {
|
|
408
|
+
return handleDevicesList(req, res, ctx);
|
|
308
409
|
}
|
|
309
410
|
|
|
310
|
-
if (method === 'DELETE' && url.pathname === '/
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
store.clear();
|
|
314
|
-
sendJson(res, 200, { ok: true });
|
|
315
|
-
return;
|
|
411
|
+
if (method === 'DELETE' && url.pathname === '/devices') {
|
|
412
|
+
return handleDevicesClear(req, res, ctx);
|
|
316
413
|
}
|
|
317
414
|
|
|
318
|
-
const
|
|
319
|
-
if (method === 'GET' &&
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const session = store.getSession(decodeURIComponent(sessionMatch[1]));
|
|
323
|
-
if (!session) {
|
|
324
|
-
sendJson(res, 404, { ok: false, error: 'Session not found' });
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (url.pathname.endsWith('/logs')) {
|
|
329
|
-
sendJson(res, 200, {
|
|
330
|
-
ok: true,
|
|
331
|
-
sessionId: session.sessionId,
|
|
332
|
-
receivedAt: session.receivedAt,
|
|
333
|
-
logs: selectLogs(session, url.searchParams),
|
|
334
|
-
});
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
sendJson(res, 200, {
|
|
339
|
-
ok: true,
|
|
340
|
-
...toSessionPayload(session),
|
|
341
|
-
});
|
|
342
|
-
return;
|
|
415
|
+
const deviceMatch = url.pathname.match(/^\/devices\/([^/]+)(?:\/logs)?$/);
|
|
416
|
+
if (method === 'GET' && deviceMatch) {
|
|
417
|
+
return handleDeviceMatch(req, res, ctx, url, deviceMatch);
|
|
343
418
|
}
|
|
344
419
|
|
|
345
420
|
sendJson(res, 404, { ok: false, error: 'Not found' });
|
|
@@ -361,5 +436,12 @@ function createDaemonServer(options = {}) {
|
|
|
361
436
|
|
|
362
437
|
module.exports = {
|
|
363
438
|
createDaemonServer,
|
|
439
|
+
handleHealth,
|
|
440
|
+
handleReport,
|
|
441
|
+
handleIngest,
|
|
442
|
+
handleLatestDevice,
|
|
443
|
+
handleDevicesList,
|
|
444
|
+
handleDevicesClear,
|
|
445
|
+
handleDeviceMatch,
|
|
364
446
|
isValidReport,
|
|
365
447
|
};
|
package/node/daemon/src/store.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MAX_DEVICES = 20;
|
|
4
7
|
|
|
5
8
|
function createLogCount(report) {
|
|
6
9
|
const logs = report && typeof report === 'object' ? report.logs : undefined;
|
|
@@ -16,95 +19,169 @@ function createLogCount(report) {
|
|
|
16
19
|
}, {});
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
function
|
|
20
|
-
return
|
|
22
|
+
function slugPart(value) {
|
|
23
|
+
return String(value || 'unknown')
|
|
24
|
+
.trim()
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
27
|
+
.replace(/^-+|-+$/g, '')
|
|
28
|
+
.slice(0, 80) || 'unknown';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createDeviceId(report, source) {
|
|
32
|
+
const device = report && typeof report === 'object' && report.device && typeof report.device === 'object'
|
|
33
|
+
? report.device
|
|
34
|
+
: {};
|
|
35
|
+
return [
|
|
36
|
+
slugPart(device.platform),
|
|
37
|
+
slugPart(device.model),
|
|
38
|
+
slugPart(source && source.ip),
|
|
39
|
+
].join('_');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readPersistedDevices(storagePath, maxDevices) {
|
|
43
|
+
if (!storagePath) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const raw = fs.readFileSync(storagePath, 'utf8');
|
|
49
|
+
const parsed = JSON.parse(raw);
|
|
50
|
+
if (!Array.isArray(parsed)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
return parsed
|
|
54
|
+
.filter((deviceLog) => (
|
|
55
|
+
deviceLog &&
|
|
56
|
+
typeof deviceLog === 'object' &&
|
|
57
|
+
typeof deviceLog.deviceId === 'string' &&
|
|
58
|
+
deviceLog.report &&
|
|
59
|
+
typeof deviceLog.report === 'object'
|
|
60
|
+
))
|
|
61
|
+
.slice(-maxDevices);
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
21
65
|
}
|
|
22
66
|
|
|
23
67
|
function createMemoryStore(options = {}) {
|
|
24
|
-
const
|
|
68
|
+
const maxDevices = options.maxDevices || DEFAULT_MAX_DEVICES;
|
|
25
69
|
const onUpdate = options.onUpdate || null;
|
|
26
|
-
const
|
|
27
|
-
|
|
70
|
+
const storagePath = options.storagePath || null;
|
|
71
|
+
const devices = readPersistedDevices(storagePath, maxDevices);
|
|
28
72
|
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
73
|
+
function persist() {
|
|
74
|
+
if (!storagePath) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
fs.mkdirSync(path.dirname(storagePath), { recursive: true });
|
|
80
|
+
const tmpPath = `${storagePath}.tmp`;
|
|
81
|
+
fs.writeFileSync(tmpPath, JSON.stringify(devices, null, 2));
|
|
82
|
+
fs.renameSync(tmpPath, storagePath);
|
|
83
|
+
} catch {
|
|
84
|
+
// Persistence is best-effort; daemon HTTP API should keep working.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
32
87
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
88
|
+
function saveReport(report, metadata = {}) {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const receivedAt = new Date(now).toISOString();
|
|
91
|
+
const source = metadata.source || null;
|
|
92
|
+
const deviceId = createDeviceId(report, source);
|
|
93
|
+
const existingIndex = devices.findIndex((item) => item.deviceId === deviceId);
|
|
94
|
+
const existing = existingIndex >= 0 ? devices[existingIndex] : null;
|
|
95
|
+
const deviceLog = {
|
|
96
|
+
deviceId,
|
|
97
|
+
firstSeenAt: existing ? existing.firstSeenAt : receivedAt,
|
|
98
|
+
lastSeenAt: receivedAt,
|
|
99
|
+
receivedAt,
|
|
100
|
+
source,
|
|
101
|
+
device: report.device || null,
|
|
38
102
|
report,
|
|
39
103
|
logCount: createLogCount(report),
|
|
40
104
|
};
|
|
41
105
|
|
|
42
|
-
const existingIndex = sessions.findIndex((item) => item.sessionId === session.sessionId);
|
|
43
106
|
if (existingIndex >= 0) {
|
|
44
|
-
|
|
107
|
+
devices.splice(existingIndex, 1);
|
|
45
108
|
}
|
|
46
109
|
|
|
47
|
-
|
|
48
|
-
while (
|
|
49
|
-
|
|
110
|
+
devices.push(deviceLog);
|
|
111
|
+
while (devices.length > maxDevices) {
|
|
112
|
+
devices.shift();
|
|
50
113
|
}
|
|
51
114
|
|
|
52
|
-
|
|
53
|
-
|
|
115
|
+
persist();
|
|
116
|
+
if (onUpdate) onUpdate(deviceLog, 'full');
|
|
117
|
+
return deviceLog;
|
|
54
118
|
}
|
|
55
119
|
|
|
56
|
-
function appendLogs(
|
|
57
|
-
const
|
|
58
|
-
if (!
|
|
120
|
+
function appendLogs(deviceId, delta) {
|
|
121
|
+
const deviceLog = devices.find((item) => item.deviceId === deviceId);
|
|
122
|
+
if (!deviceLog) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
59
125
|
|
|
60
126
|
const deltaLogs = (delta && delta.logs) || {};
|
|
61
127
|
Object.entries(deltaLogs).forEach(([type, entries]) => {
|
|
62
|
-
if (!Array.isArray(entries))
|
|
63
|
-
|
|
64
|
-
|
|
128
|
+
if (!Array.isArray(entries)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!deviceLog.report.logs[type]) {
|
|
132
|
+
deviceLog.report.logs[type] = [];
|
|
65
133
|
}
|
|
66
|
-
|
|
134
|
+
deviceLog.report.logs[type].push(...entries);
|
|
67
135
|
});
|
|
68
136
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
137
|
+
deviceLog.lastSeenAt = new Date(Date.now()).toISOString();
|
|
138
|
+
deviceLog.receivedAt = deviceLog.lastSeenAt;
|
|
139
|
+
deviceLog.logCount = createLogCount(deviceLog.report);
|
|
140
|
+
persist();
|
|
141
|
+
if (onUpdate) onUpdate(deviceLog, 'delta', delta);
|
|
142
|
+
return deviceLog;
|
|
72
143
|
}
|
|
73
144
|
|
|
74
|
-
function
|
|
75
|
-
return
|
|
145
|
+
function listDevices() {
|
|
146
|
+
return devices
|
|
76
147
|
.slice()
|
|
77
148
|
.reverse()
|
|
78
|
-
.map((
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
149
|
+
.map((deviceLog) => ({
|
|
150
|
+
deviceId: deviceLog.deviceId,
|
|
151
|
+
firstSeenAt: deviceLog.firstSeenAt,
|
|
152
|
+
lastSeenAt: deviceLog.lastSeenAt,
|
|
153
|
+
receivedAt: deviceLog.receivedAt,
|
|
154
|
+
device: deviceLog.device || null,
|
|
155
|
+
source: deviceLog.source || null,
|
|
156
|
+
logCount: deviceLog.logCount,
|
|
82
157
|
}));
|
|
83
158
|
}
|
|
84
159
|
|
|
85
|
-
function
|
|
86
|
-
return
|
|
160
|
+
function getLatestDevice() {
|
|
161
|
+
return devices[devices.length - 1] || null;
|
|
87
162
|
}
|
|
88
163
|
|
|
89
|
-
function
|
|
90
|
-
return
|
|
164
|
+
function getDevice(deviceId) {
|
|
165
|
+
return devices.find((deviceLog) => deviceLog.deviceId === deviceId) || null;
|
|
91
166
|
}
|
|
92
167
|
|
|
93
168
|
function clear() {
|
|
94
|
-
|
|
169
|
+
devices.splice(0, devices.length);
|
|
170
|
+
persist();
|
|
95
171
|
}
|
|
96
172
|
|
|
97
173
|
return {
|
|
98
174
|
appendLogs,
|
|
99
175
|
clear,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
176
|
+
getDevice,
|
|
177
|
+
getLatestDevice,
|
|
178
|
+
listDevices,
|
|
103
179
|
saveReport,
|
|
104
180
|
};
|
|
105
181
|
}
|
|
106
182
|
|
|
107
183
|
module.exports = {
|
|
184
|
+
createDeviceId,
|
|
108
185
|
createLogCount,
|
|
109
186
|
createMemoryStore,
|
|
110
187
|
};
|
|
@@ -105,8 +105,8 @@ async function ensureDaemon(options = {}) {
|
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
async function
|
|
109
|
-
const requestPath =
|
|
108
|
+
async function readDevice(origin, deviceId) {
|
|
109
|
+
const requestPath = deviceId ? `/devices/${encodeURIComponent(deviceId)}` : '/devices/latest';
|
|
110
110
|
const response = await requestJson(origin, requestPath, { timeoutMs: 3000 });
|
|
111
111
|
if (response.status !== 200 || !response.body?.ok) {
|
|
112
112
|
throw new Error(response.body?.error || `Daemon request failed with status ${response.status}`);
|
|
@@ -114,8 +114,8 @@ async function readSession(origin, sessionId) {
|
|
|
114
114
|
return response.body;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
async function
|
|
118
|
-
const response = await requestJson(origin, '/
|
|
117
|
+
async function readDevices(origin) {
|
|
118
|
+
const response = await requestJson(origin, '/devices', { timeoutMs: 3000 });
|
|
119
119
|
if (response.status !== 200 || !response.body?.ok) {
|
|
120
120
|
throw new Error(response.body?.error || `Daemon request failed with status ${response.status}`);
|
|
121
121
|
}
|
|
@@ -127,6 +127,6 @@ module.exports = {
|
|
|
127
127
|
findDaemonBin,
|
|
128
128
|
getDaemonOrigin,
|
|
129
129
|
readHealth,
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
readDevice,
|
|
131
|
+
readDevices,
|
|
132
132
|
};
|