@maiyunnet/kebab 9.21.0 → 9.21.1
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/doc/kebab-rag.md +436 -10
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/core.d.ts +59 -0
- package/lib/core.js +81 -0
- package/package.json +1 -1
- package/sys/master.js +54 -0
- package/sys/monitor/files.d.ts +34 -0
- package/sys/monitor/files.js +260 -0
- package/sys/monitor/watchdog.js +284 -215
- package/sys/monitor.d.ts +28 -3
- package/sys/monitor.js +527 -250
- package/www/example/ctr/test.d.ts +21 -0
- package/www/example/ctr/test.js +250 -2
|
@@ -129,6 +129,27 @@ export default class extends sCtr.Ctr {
|
|
|
129
129
|
ai(): Promise<string>;
|
|
130
130
|
aiStream(): string;
|
|
131
131
|
aiStream1(): Promise<any>;
|
|
132
|
+
/**
|
|
133
|
+
* --- 示例只允许本机和框架配置的节点,不能将浏览器输入作为任意 RPC host ---
|
|
134
|
+
* @param host 请求选择的节点
|
|
135
|
+
* @returns 节点地址;本机返回 undefined,未配置的节点返回 false
|
|
136
|
+
*/
|
|
137
|
+
private _monitorHost;
|
|
138
|
+
/**
|
|
139
|
+
* --- 演示通过 master RPC 读取事件列表,并选择配置中的远程节点 ---
|
|
140
|
+
* @returns 事件列表页面或输入/读取错误
|
|
141
|
+
*/
|
|
142
|
+
monitorEvents(): Promise<string | kebab.Json[]>;
|
|
143
|
+
/**
|
|
144
|
+
* --- 演示事件详情及取证文件清单,预览和下载链接均指向本应用 ---
|
|
145
|
+
* @returns 事件详情页面或输入/读取错误
|
|
146
|
+
*/
|
|
147
|
+
monitorEvent(): Promise<string | kebab.Json[]>;
|
|
148
|
+
/**
|
|
149
|
+
* --- 小型 JSON/文本预览或原始文件下载;复杂文件直接流式返回 ---
|
|
150
|
+
* @returns 预览页面或错误;完成流式下载后返回 false
|
|
151
|
+
*/
|
|
152
|
+
monitorFile(): Promise<string | kebab.Json[] | false>;
|
|
132
153
|
/**
|
|
133
154
|
* --- 获取当前性能快照 ---
|
|
134
155
|
*/
|
package/www/example/ctr/test.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as stream from 'stream/promises';
|
|
1
2
|
// --- 库 ---
|
|
2
3
|
import * as kebab from '#kebab/index.js';
|
|
3
4
|
import * as lCore from '#kebab/lib/core.js';
|
|
@@ -259,7 +260,8 @@ export default class extends sCtr.Ctr {
|
|
|
259
260
|
'<br><br><b>Cron:</b>',
|
|
260
261
|
`<br><br><a href="${this._config.const.urlBase}test/cron">View "test/cron"</a>`,
|
|
261
262
|
'<br><br><b>Monitor:</b>',
|
|
262
|
-
`<br><br><a href="${this._config.const.urlBase}test/monitor-
|
|
263
|
+
`<br><br><a href="${this._config.const.urlBase}test/monitor-events">View "test/monitor-events" (saved events, previews and downloads)</a>`,
|
|
264
|
+
`<br><a href="${this._config.const.urlBase}test/monitor-snapshot">View "test/monitor-snapshot"</a>`,
|
|
263
265
|
`<br><a href="${this._config.const.urlBase}test/monitor-spike">View "test/monitor-spike" (non-blocking CPU spike)</a>`,
|
|
264
266
|
`<br><a href="${this._config.const.urlBase}test/monitor-spike?mode=block">View "test/monitor-spike?mode=block" (event loop block)</a>`,
|
|
265
267
|
'<br><br><b>Rate Limit:</b>',
|
|
@@ -4383,6 +4385,252 @@ send.addEventListener('click', async () => {
|
|
|
4383
4385
|
lCore.debug('AI DONE');
|
|
4384
4386
|
}
|
|
4385
4387
|
// --- Monitor ---
|
|
4388
|
+
/**
|
|
4389
|
+
* --- 示例只允许本机和框架配置的节点,不能将浏览器输入作为任意 RPC host ---
|
|
4390
|
+
* @param host 请求选择的节点
|
|
4391
|
+
* @returns 节点地址;本机返回 undefined,未配置的节点返回 false
|
|
4392
|
+
*/
|
|
4393
|
+
_monitorHost(host) {
|
|
4394
|
+
if (host === undefined || host === null || host === '') {
|
|
4395
|
+
return undefined;
|
|
4396
|
+
}
|
|
4397
|
+
if (typeof host !== 'string' || !(lCore.globalConfig.hosts ?? []).includes(host)) {
|
|
4398
|
+
return false;
|
|
4399
|
+
}
|
|
4400
|
+
return host;
|
|
4401
|
+
}
|
|
4402
|
+
/**
|
|
4403
|
+
* --- 演示通过 master RPC 读取事件列表,并选择配置中的远程节点 ---
|
|
4404
|
+
* @returns 事件列表页面或输入/读取错误
|
|
4405
|
+
*/
|
|
4406
|
+
async monitorEvents() {
|
|
4407
|
+
const input = {
|
|
4408
|
+
'path': this._get['path'] ?? lTime.format(null, 'Y/m/d'),
|
|
4409
|
+
'offset': this._get['offset'] ?? '0',
|
|
4410
|
+
'host': this._get['host'],
|
|
4411
|
+
};
|
|
4412
|
+
const rtn = [];
|
|
4413
|
+
if (!this._checkInput(input, {
|
|
4414
|
+
'path': ['require', 'string', /^\d{4}\/\d{2}\/\d{2}$/, [0, 'Invalid date path.']],
|
|
4415
|
+
'offset': ['require', 'string', /^\d+$/, [0, 'Invalid offset.']],
|
|
4416
|
+
'host': ['string', [0, 'Invalid host.']],
|
|
4417
|
+
}, rtn)) {
|
|
4418
|
+
return rtn;
|
|
4419
|
+
}
|
|
4420
|
+
const host = this._monitorHost(input.host);
|
|
4421
|
+
if (host === false) {
|
|
4422
|
+
return [0, 'Host must be a configured node.'];
|
|
4423
|
+
}
|
|
4424
|
+
const offset = Number(input.offset);
|
|
4425
|
+
const events = await lCore.getMonitorEvents({ 'path': input.path, host, offset, 'limit': 20 });
|
|
4426
|
+
if (events === false) {
|
|
4427
|
+
return [0, 'Failed to read monitor events.'];
|
|
4428
|
+
}
|
|
4429
|
+
const echo = [
|
|
4430
|
+
'<b>Saved Monitor Events</b><hr>',
|
|
4431
|
+
`<form method="get">Date (YYYY/MM/DD): <input name="path" value="${lText.htmlescape(input.path)}"> `,
|
|
4432
|
+
'<select name="host"><option value="">Local</option>',
|
|
4433
|
+
];
|
|
4434
|
+
for (const node of lCore.globalConfig.hosts ?? []) {
|
|
4435
|
+
echo.push(`<option value="${lText.htmlescape(node)}"${node === host ? ' selected' : ''}>${lText.htmlescape(node)}</option>`);
|
|
4436
|
+
}
|
|
4437
|
+
echo.push('</select> <button>Read events</button></form>', '<pre>await lCore.getMonitorEvents({ path, host, offset, limit: 20 });</pre>', `Total: ${events.total}<table><tr><th>Time</th><th>PID</th><th>Source</th><th>Status</th><th>Reasons</th><th>Details</th></tr>`);
|
|
4438
|
+
for (const event of events.list) {
|
|
4439
|
+
const url = `${this._config.const.urlBase}test/monitor-event?${lText.queryStringify({ 'path': event.path, host })}`;
|
|
4440
|
+
echo.push(`<tr><td>${lTime.format(null, 'Y-m-d H:i:s', new Date(event.time))}</td><td>${event.pid}</td>` +
|
|
4441
|
+
`<td>${event.source}</td><td>${lText.htmlescape(event.status)}</td><td>${lText.htmlescape(event.reasons.join(', '))}</td>` +
|
|
4442
|
+
`<td><a href="${lText.htmlescape(url)}">View event</a></td></tr>`);
|
|
4443
|
+
}
|
|
4444
|
+
echo.push('</table><div>');
|
|
4445
|
+
if (offset > 0) {
|
|
4446
|
+
const query = lText.queryStringify({ 'path': input.path, host, 'offset': Math.max(0, offset - 20) });
|
|
4447
|
+
echo.push(`<a href="?${lText.htmlescape(query)}">Previous</a> `);
|
|
4448
|
+
}
|
|
4449
|
+
if (offset + events.list.length < events.total) {
|
|
4450
|
+
const query = lText.queryStringify({ 'path': input.path, host, 'offset': offset + 20 });
|
|
4451
|
+
echo.push(`<a href="?${lText.htmlescape(query)}">Next</a>`);
|
|
4452
|
+
}
|
|
4453
|
+
echo.push('</div><br>' + this._getEnd());
|
|
4454
|
+
return echo.join('');
|
|
4455
|
+
}
|
|
4456
|
+
/**
|
|
4457
|
+
* --- 演示事件详情及取证文件清单,预览和下载链接均指向本应用 ---
|
|
4458
|
+
* @returns 事件详情页面或输入/读取错误
|
|
4459
|
+
*/
|
|
4460
|
+
async monitorEvent() {
|
|
4461
|
+
const rtn = [];
|
|
4462
|
+
if (!this._checkInput(this._get, {
|
|
4463
|
+
'path': ['require', 'string', [0, 'Missing event path.']],
|
|
4464
|
+
'host': ['string', [0, 'Invalid host.']],
|
|
4465
|
+
}, rtn)) {
|
|
4466
|
+
return rtn;
|
|
4467
|
+
}
|
|
4468
|
+
const host = this._monitorHost(this._get['host']);
|
|
4469
|
+
if (host === false) {
|
|
4470
|
+
return [0, 'Host must be a configured node.'];
|
|
4471
|
+
}
|
|
4472
|
+
const event = await lCore.getMonitorEvent({ 'path': this._get['path'], host });
|
|
4473
|
+
if (event === false) {
|
|
4474
|
+
return [0, 'Failed to read monitor event.'];
|
|
4475
|
+
}
|
|
4476
|
+
if (event === null) {
|
|
4477
|
+
this._httpCode = 404;
|
|
4478
|
+
return [0, 'Monitor event not found.'];
|
|
4479
|
+
}
|
|
4480
|
+
const echo = [
|
|
4481
|
+
'<b>Monitor Event</b><hr>',
|
|
4482
|
+
`<pre>${lText.htmlescape(`await lCore.getMonitorEvent(${lText.stringifyJson({ 'path': this._get['path'], host }, 4)});`)}</pre>`,
|
|
4483
|
+
`<b>Record:</b> ${event.recordStatus}<pre>${lText.htmlescape(lText.stringifyJson(event.data, 4))}</pre>`,
|
|
4484
|
+
'<table><tr><th>File</th><th>Size</th><th>Actions</th></tr>',
|
|
4485
|
+
];
|
|
4486
|
+
for (const file of event.files) {
|
|
4487
|
+
const query = lText.queryStringify({ 'path': file.path, host });
|
|
4488
|
+
const url = `${this._config.const.urlBase}test/monitor-file?${query}`;
|
|
4489
|
+
echo.push(`<tr><td>${lText.htmlescape(file.name)}</td><td>${lText.sizeFormat(file.size)}</td>` +
|
|
4490
|
+
`<td><a href="${lText.htmlescape(url)}">Download</a>`);
|
|
4491
|
+
if (file.preview) {
|
|
4492
|
+
echo.push(` <a href="${lText.htmlescape(url + '&preview=1')}">Preview</a>`);
|
|
4493
|
+
}
|
|
4494
|
+
echo.push('</td></tr>');
|
|
4495
|
+
}
|
|
4496
|
+
const downloadFile = event.files[0];
|
|
4497
|
+
const previewFile = event.files.find(file => file.preview !== null);
|
|
4498
|
+
const hostCode = `const host = ${host === undefined ? 'undefined' : lText.stringifyJson(host)};`;
|
|
4499
|
+
const pathCode = `// host works like getLog(); undefined means this server.
|
|
4500
|
+
${hostCode}
|
|
4501
|
+
const events = await lCore.getMonitorEvents({
|
|
4502
|
+
'path': ${lText.stringifyJson(event.summary.path.split('/').slice(0, 3).join('/'))},
|
|
4503
|
+
host
|
|
4504
|
+
});
|
|
4505
|
+
if (events === false) {
|
|
4506
|
+
return [0, 'Failed to read monitor events.'];
|
|
4507
|
+
}
|
|
4508
|
+
// Select an event returned by getMonitorEvents().
|
|
4509
|
+
// This page uses the following event; request another page if needed.
|
|
4510
|
+
const summary = events.list.find(item => item.path === ${lText.stringifyJson(event.summary.path)});
|
|
4511
|
+
if (!summary) {
|
|
4512
|
+
return [0, 'Event is not on this page. Adjust offset in getMonitorEvents().'];
|
|
4513
|
+
}
|
|
4514
|
+
const event = await lCore.getMonitorEvent({ 'path': summary.path, host });
|
|
4515
|
+
if (event === false) {
|
|
4516
|
+
return [0, 'Failed to read monitor event.'];
|
|
4517
|
+
}
|
|
4518
|
+
if (event === null) {
|
|
4519
|
+
return [0, 'Monitor event not found.'];
|
|
4520
|
+
}
|
|
4521
|
+
// event.files contains each file's path, name, size and preview metadata.
|
|
4522
|
+
// Pass event.files[].path to getMonitorFile(); do not construct a path from the PID.`;
|
|
4523
|
+
const downloadCode = `import * as stream from 'stream/promises';
|
|
4524
|
+
|
|
4525
|
+
// Continue with the event and host obtained above.
|
|
4526
|
+
const entry = event.files[0];
|
|
4527
|
+
if (!entry) {
|
|
4528
|
+
return [0, 'No saved files are available for download.'];
|
|
4529
|
+
}
|
|
4530
|
+
const path = entry.path;
|
|
4531
|
+
// Example returned path on this page: ${lText.stringifyJson(downloadFile?.path ?? '')}
|
|
4532
|
+
const file = await lCore.getMonitorFile({ path, host });
|
|
4533
|
+
if (file === false) {
|
|
4534
|
+
return [0, 'Failed to read monitor file.'];
|
|
4535
|
+
}
|
|
4536
|
+
if (file === null) {
|
|
4537
|
+
this._httpCode = 404;
|
|
4538
|
+
return [0, 'Monitor file not found.'];
|
|
4539
|
+
}
|
|
4540
|
+
const body = file.getStream();
|
|
4541
|
+
if (!body) {
|
|
4542
|
+
return [0, 'Failed to open download stream.'];
|
|
4543
|
+
}
|
|
4544
|
+
this._res.setHeader('content-type', 'application/octet-stream');
|
|
4545
|
+
this._res.setHeader('content-disposition', file.headers?.['content-disposition'] ?? 'attachment');
|
|
4546
|
+
this._res.setHeader('cache-control', 'no-store');
|
|
4547
|
+
lCore.writeHead(this._res, 200);
|
|
4548
|
+
try {
|
|
4549
|
+
await stream.pipeline(body, this._res);
|
|
4550
|
+
}
|
|
4551
|
+
catch {
|
|
4552
|
+
this._res.destroy();
|
|
4553
|
+
}
|
|
4554
|
+
return false;`;
|
|
4555
|
+
const previewCode = `// Continue with the event and host obtained above.
|
|
4556
|
+
const entry = event.files.find(item => item.preview !== null);
|
|
4557
|
+
if (!entry) {
|
|
4558
|
+
return [0, 'No files in this event support preview.'];
|
|
4559
|
+
}
|
|
4560
|
+
const path = entry.path;
|
|
4561
|
+
// Example returned path on this page: ${lText.stringifyJson(previewFile?.path ?? '')}
|
|
4562
|
+
const file = await lCore.getMonitorFile({ path, host, 'preview': true });
|
|
4563
|
+
if (file === false) {
|
|
4564
|
+
return [0, 'Failed to read file, or preview is unavailable.'];
|
|
4565
|
+
}
|
|
4566
|
+
if (file === null) {
|
|
4567
|
+
this._httpCode = 404;
|
|
4568
|
+
return [0, 'Monitor file not found.'];
|
|
4569
|
+
}
|
|
4570
|
+
const text = await file.getText();
|
|
4571
|
+
if (text === null) {
|
|
4572
|
+
return [0, 'Failed to read preview.'];
|
|
4573
|
+
}
|
|
4574
|
+
return '<pre>' + lText.htmlescape(text) + '</pre>';`;
|
|
4575
|
+
echo.push('</table><br><b>Get file paths (controller):</b>', `<pre style="white-space: pre-wrap; overflow-wrap: anywhere;">${lText.htmlescape(pathCode)}</pre>`, '<b>Download (controller):</b>', `<pre style="white-space: pre-wrap; overflow-wrap: anywhere;">${downloadFile ? lText.htmlescape(downloadCode) : 'No saved files are available for download.'}</pre>`, '<b>Preview (controller):</b>', `<pre style="white-space: pre-wrap; overflow-wrap: anywhere;">${previewFile ? lText.htmlescape(previewCode) : 'No files in this event support preview.'}</pre>`, 'JSON preview can also use file.getJson(). Files over 1 MiB, CPU profiles and heap snapshots are download-only.', '<br><br>' + this._getEnd());
|
|
4576
|
+
return echo.join('');
|
|
4577
|
+
}
|
|
4578
|
+
/**
|
|
4579
|
+
* --- 小型 JSON/文本预览或原始文件下载;复杂文件直接流式返回 ---
|
|
4580
|
+
* @returns 预览页面或错误;完成流式下载后返回 false
|
|
4581
|
+
*/
|
|
4582
|
+
async monitorFile() {
|
|
4583
|
+
const rtn = [];
|
|
4584
|
+
if (!this._checkInput(this._get, {
|
|
4585
|
+
'path': ['require', 'string', [0, 'Missing file path.']],
|
|
4586
|
+
'host': ['string', [0, 'Invalid host.']],
|
|
4587
|
+
'preview': ['string', ['0', '1'], [0, 'Invalid preview option.']],
|
|
4588
|
+
}, rtn)) {
|
|
4589
|
+
return rtn;
|
|
4590
|
+
}
|
|
4591
|
+
const host = this._monitorHost(this._get['host']);
|
|
4592
|
+
if (host === false) {
|
|
4593
|
+
return [0, 'Host must be a configured node.'];
|
|
4594
|
+
}
|
|
4595
|
+
const preview = this._get['preview'] === '1';
|
|
4596
|
+
const file = await lCore.getMonitorFile({ 'path': this._get['path'], host, preview });
|
|
4597
|
+
if (file === false) {
|
|
4598
|
+
return [0, 'Failed to read file, or preview is unavailable.'];
|
|
4599
|
+
}
|
|
4600
|
+
if (file === null) {
|
|
4601
|
+
this._httpCode = 404;
|
|
4602
|
+
return [0, 'Monitor file not found.'];
|
|
4603
|
+
}
|
|
4604
|
+
if (preview) {
|
|
4605
|
+
const text = await file.getText();
|
|
4606
|
+
if (text === null) {
|
|
4607
|
+
return [0, 'Failed to read preview.'];
|
|
4608
|
+
}
|
|
4609
|
+
const code = `const file = await lCore.getMonitorFile(${lText.stringifyJson({ 'path': this._get['path'], host, 'preview': true }, 4)});`;
|
|
4610
|
+
return `<b>Monitor File Preview</b><hr><pre>${lText.htmlescape(code)}</pre>` +
|
|
4611
|
+
`<pre>${lText.htmlescape(text)}</pre><br><br>` + this._getEnd();
|
|
4612
|
+
}
|
|
4613
|
+
const body = file.getStream();
|
|
4614
|
+
if (!body) {
|
|
4615
|
+
return [0, 'Failed to open download stream.'];
|
|
4616
|
+
}
|
|
4617
|
+
this._res.setHeader('content-type', 'application/octet-stream');
|
|
4618
|
+
this._res.setHeader('content-disposition', file.headers?.['content-disposition'] ?? 'attachment');
|
|
4619
|
+
this._res.setHeader('cache-control', 'no-store');
|
|
4620
|
+
const size = file.headers?.['content-length'];
|
|
4621
|
+
if (size !== undefined) {
|
|
4622
|
+
this._res.setHeader('content-length', size);
|
|
4623
|
+
}
|
|
4624
|
+
lCore.writeHead(this._res, 200);
|
|
4625
|
+
// --- 下载响应已经开始;pipeline 使浏览器断开同时关闭远程读取流 ---
|
|
4626
|
+
try {
|
|
4627
|
+
await stream.pipeline(body, this._res);
|
|
4628
|
+
}
|
|
4629
|
+
catch {
|
|
4630
|
+
this._res.destroy();
|
|
4631
|
+
}
|
|
4632
|
+
return false;
|
|
4633
|
+
}
|
|
4386
4634
|
/**
|
|
4387
4635
|
* --- 获取当前性能快照 ---
|
|
4388
4636
|
*/
|
|
@@ -4466,7 +4714,7 @@ send.addEventListener('click', async () => {
|
|
|
4466
4714
|
`RSS ${lText.sizeFormat(after.mem.rss)}, ` +
|
|
4467
4715
|
`Iterations: ${count}`);
|
|
4468
4716
|
echo.push('<br><br>Check <code>log/monitor/YYYY/MM/DD/HHmmss-pid-{pid}/</code> for' +
|
|
4469
|
-
|
|
4717
|
+
` diagnostic files, or <a href="${this._config.const.urlBase}test/monitor-events">view saved events and download files</a>.`);
|
|
4470
4718
|
echo.push('<br><br>' + this._getEnd());
|
|
4471
4719
|
return echo.join('');
|
|
4472
4720
|
}
|