@maiyunnet/kebab 9.21.0 → 9.21.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/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 +83 -2
- 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
package/lib/core.js
CHANGED
|
@@ -830,7 +830,7 @@ export async function sendNpm(path, hosts) {
|
|
|
830
830
|
'time': time,
|
|
831
831
|
'path': path
|
|
832
832
|
}), globalConfig.rpcSecret), {
|
|
833
|
-
'timeout':
|
|
833
|
+
'timeout': 330,
|
|
834
834
|
});
|
|
835
835
|
const content = await res.getContent();
|
|
836
836
|
if (!content) {
|
|
@@ -938,7 +938,7 @@ export async function updateCode(sourcePath, path, hosts, config = true, strict
|
|
|
938
938
|
'action': 'code',
|
|
939
939
|
'time': lTime.stamp()
|
|
940
940
|
}), globalConfig.rpcSecret), fd, {
|
|
941
|
-
'timeout':
|
|
941
|
+
'timeout': 180,
|
|
942
942
|
});
|
|
943
943
|
const content = await res.getContent();
|
|
944
944
|
if (!content) {
|
|
@@ -1182,6 +1182,87 @@ export async function getLog(opt) {
|
|
|
1182
1182
|
'total': res.total,
|
|
1183
1183
|
};
|
|
1184
1184
|
}
|
|
1185
|
+
/**
|
|
1186
|
+
* --- 通过 master RPC 读取已保存的监控数据,不依赖目标 HTTP 子进程 ---
|
|
1187
|
+
* @param action 要读取的监控资源
|
|
1188
|
+
* @param opt 读取参数,host 默认本机
|
|
1189
|
+
* @returns 响应对象;配置或网络错误时返回 false
|
|
1190
|
+
*/
|
|
1191
|
+
async function requestMonitor(action, opt) {
|
|
1192
|
+
if (!Number.isInteger(globalConfig.rpcPort) || typeof globalConfig.rpcSecret !== 'string') {
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
const command = lCrypto.aesEncrypt(lText.stringifyJson({
|
|
1196
|
+
action, 'time': lTime.stamp(), 'path': opt.path, 'offset': opt.offset,
|
|
1197
|
+
'limit': opt.limit, 'preview': opt.preview,
|
|
1198
|
+
}), globalConfig.rpcSecret);
|
|
1199
|
+
const res = await lUndici.get(`http://${opt.host ?? '127.0.0.1'}:${globalConfig.rpcPort}/${command}`, {
|
|
1200
|
+
'timeout': action === 'monitor-file' ? 300 : 5,
|
|
1201
|
+
'follow': 0,
|
|
1202
|
+
});
|
|
1203
|
+
if (res.error || !res.headers) {
|
|
1204
|
+
await res.getRawStream()?.dump({ 'limit': 4096 });
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
return res;
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* --- 获取某天已保存的监控事件,按目录时间倒序分页 ---
|
|
1211
|
+
* @param opt path 为 YYYY/MM/DD,默认目标服务器的当天;offset 默认 0,limit 默认 20、最多 100
|
|
1212
|
+
* @returns 事件列表和总数;无事件时返回空列表,参数、配置或读取错误时返回 false
|
|
1213
|
+
*/
|
|
1214
|
+
export async function getMonitorEvents(opt = {}) {
|
|
1215
|
+
const res = await requestMonitor('monitor-events', opt);
|
|
1216
|
+
if (res === false) {
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
const text = await res.getText();
|
|
1220
|
+
if (text === null) {
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
const result = lText.parseJson(text);
|
|
1224
|
+
if (!result || result.result !== 1 || !Array.isArray(result.list) || !Number.isSafeInteger(result.total)) {
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
return { 'list': result.list, 'total': result.total };
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* --- 读取事件详情和文件清单;只解析事件记录,不解析 Profile 或堆快照 ---
|
|
1231
|
+
* @param opt path 使用 getMonitorEvents 返回的事件路径;host 默认本机
|
|
1232
|
+
* @returns 事件详情;不存在时返回 null,参数、配置或读取错误时返回 false
|
|
1233
|
+
*/
|
|
1234
|
+
export async function getMonitorEvent(opt) {
|
|
1235
|
+
const res = await requestMonitor('monitor-event', opt);
|
|
1236
|
+
if (res === false) {
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1239
|
+
const text = await res.getText();
|
|
1240
|
+
if (text === null) {
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1243
|
+
const result = lText.parseJson(text);
|
|
1244
|
+
if (!result || result.result !== 1 || result.data === undefined) {
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
return result.data;
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* --- 获取监控原始文件的响应,可使用 getStream() 下载;小文件可用 getText()/getJson() 预览 ---
|
|
1251
|
+
* @param opt path 使用事件 files 中的文件路径;preview=true 只允许 1 MiB 内的 JSON/调用栈文本;host 默认本机
|
|
1252
|
+
* @returns 文件响应;不存在时返回 null,参数、预览限制、配置或读取错误时返回 false;流消费或销毁由调用方负责
|
|
1253
|
+
*/
|
|
1254
|
+
export async function getMonitorFile(opt) {
|
|
1255
|
+
const res = await requestMonitor('monitor-file', opt);
|
|
1256
|
+
if (res === false) {
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
if (res.headers?.['http-code'] === 200 && res.headers['x-kebab-monitor-file'] === '1') {
|
|
1260
|
+
return res;
|
|
1261
|
+
}
|
|
1262
|
+
// --- 有界丢弃错误响应;直接 destroy 未消费的 Undici 流会产生未处理的 AbortError ---
|
|
1263
|
+
await res.getRawStream()?.dump({ 'limit': 4096 });
|
|
1264
|
+
return res.headers?.['http-code'] === 404 ? null : false;
|
|
1265
|
+
}
|
|
1185
1266
|
/**
|
|
1186
1267
|
* --- 获取目录内文件/文件夹列表 ---
|
|
1187
1268
|
* @param opt 参数
|
package/package.json
CHANGED
package/sys/master.js
CHANGED
|
@@ -8,9 +8,11 @@ import * as os from 'os';
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as http from 'http';
|
|
10
10
|
import * as path from 'path';
|
|
11
|
+
import * as stream from 'stream/promises';
|
|
11
12
|
// --- 库和定义 ---
|
|
12
13
|
import * as kebab from '#kebab/index.js';
|
|
13
14
|
import * as sRoute from '#kebab/sys/route.js';
|
|
15
|
+
import * as sMonitorFiles from '#kebab/sys/monitor/files.js';
|
|
14
16
|
import * as lCore from '#kebab/lib/core.js';
|
|
15
17
|
import * as lFs from '#kebab/lib/fs.js';
|
|
16
18
|
import * as lText from '#kebab/lib/text.js';
|
|
@@ -918,6 +920,58 @@ function createRpcListener() {
|
|
|
918
920
|
}));
|
|
919
921
|
return;
|
|
920
922
|
}
|
|
923
|
+
case 'monitor-events': {
|
|
924
|
+
const result = await sMonitorFiles.getEvents(path.join(kebab.LOG_CWD, 'monitor'), msg.path ?? lTime.format(null, 'Y/m/d'), msg.offset ?? 0, msg.limit ?? 20);
|
|
925
|
+
res.setHeader('cache-control', 'no-store');
|
|
926
|
+
res.end(lText.stringifyJson(result === false ? { 'result': 0 } : { 'result': 1, ...result }));
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
case 'monitor-event': {
|
|
930
|
+
const data = await sMonitorFiles.getEvent(path.join(kebab.LOG_CWD, 'monitor'), msg.path);
|
|
931
|
+
res.setHeader('cache-control', 'no-store');
|
|
932
|
+
res.end(lText.stringifyJson(data === false ? { 'result': 0 } : { 'result': 1, data }));
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
case 'monitor-file': {
|
|
936
|
+
const file = await sMonitorFiles.getFile(path.join(kebab.LOG_CWD, 'monitor'), msg.path);
|
|
937
|
+
res.setHeader('cache-control', 'no-store');
|
|
938
|
+
if (file === false || msg.preview !== undefined && typeof msg.preview !== 'boolean') {
|
|
939
|
+
res.writeHead(400);
|
|
940
|
+
res.end();
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (file === null) {
|
|
944
|
+
res.writeHead(404);
|
|
945
|
+
res.end();
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (msg.preview && file.info.preview === null) {
|
|
949
|
+
res.writeHead(file.stat.size > sMonitorFiles.PREVIEW_LIMIT ? 413 : 415);
|
|
950
|
+
res.end();
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
let contentType = 'application/octet-stream';
|
|
954
|
+
if (msg.preview) {
|
|
955
|
+
contentType = file.info.preview === 'json' ? 'application/json; charset=utf-8' : 'text/plain; charset=utf-8';
|
|
956
|
+
}
|
|
957
|
+
res.setHeader('content-type', contentType);
|
|
958
|
+
res.setHeader('x-content-type-options', 'nosniff');
|
|
959
|
+
res.setHeader('x-kebab-monitor-file', '1');
|
|
960
|
+
res.setHeader('content-disposition', `attachment; filename="${file.info.name}"`);
|
|
961
|
+
res.setHeader('content-length', file.stat.size);
|
|
962
|
+
if (file.stat.size === 0) {
|
|
963
|
+
res.end();
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
// --- 原始字节直接传输;pipeline 在客户端断开时关闭文件流,不整文件读取或压缩 ---
|
|
967
|
+
try {
|
|
968
|
+
await stream.pipeline(fs.createReadStream(file.path, { 'start': 0, 'end': file.stat.size - 1 }), res);
|
|
969
|
+
}
|
|
970
|
+
catch {
|
|
971
|
+
res.destroy();
|
|
972
|
+
}
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
921
975
|
case 'ls': {
|
|
922
976
|
// --- 获取目录内文件/文件夹列表 ---
|
|
923
977
|
if (typeof msg.path !== 'string') {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Stats } from 'fs';
|
|
2
|
+
import type { IMonitorEvent, IMonitorEventSummary, IMonitorFile } from '#kebab/lib/core.js';
|
|
3
|
+
/** --- 小型 JSON/文本预览上限,Profile 和堆快照不预览 --- */
|
|
4
|
+
export declare const PREVIEW_LIMIT: number;
|
|
5
|
+
/**
|
|
6
|
+
* --- 获取一天的事件摘要,先分页目录再读取记录 ---
|
|
7
|
+
* @param root log/monitor 的绝对路径
|
|
8
|
+
* @param day YYYY/MM/DD
|
|
9
|
+
* @param offset 跳过目录数量
|
|
10
|
+
* @param limit 返回数量,1-100
|
|
11
|
+
* @returns 列表和总数;目录不存在返回空列表,输入或读取错误返回 false
|
|
12
|
+
*/
|
|
13
|
+
export declare function getEvents(root: string, day: unknown, offset?: unknown, limit?: unknown): Promise<{
|
|
14
|
+
'list': IMonitorEventSummary[];
|
|
15
|
+
'total': number;
|
|
16
|
+
} | false>;
|
|
17
|
+
/**
|
|
18
|
+
* --- 获取允许读取的诊断文件及其元数据,文件路径限制在日期/事件目录下 ---
|
|
19
|
+
* @param root log/monitor 的绝对路径
|
|
20
|
+
* @param relative files 中返回的文件相对路径
|
|
21
|
+
* @returns 真实路径、文件状态和元数据;不存在返回 null,非法路径或读取错误返回 false
|
|
22
|
+
*/
|
|
23
|
+
export declare function getFile(root: string, relative: unknown): Promise<{
|
|
24
|
+
'path': string;
|
|
25
|
+
'stat': Stats;
|
|
26
|
+
'info': IMonitorFile;
|
|
27
|
+
} | null | false>;
|
|
28
|
+
/**
|
|
29
|
+
* --- 获取事件原始数据及文件清单,包含 diagnostics 引用的看门狗取证文件 ---
|
|
30
|
+
* @param root log/monitor 的绝对路径
|
|
31
|
+
* @param relative 事件相对路径
|
|
32
|
+
* @returns 事件详情;不存在返回 null,非法路径或读取错误返回 false
|
|
33
|
+
*/
|
|
34
|
+
export declare function getEvent(root: string, relative: unknown): Promise<IMonitorEvent | null | false>;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/** --- master 读取落盘监控数据;只读取有界事件 JSON,Profile 和堆快照仅提供文件元数据 --- */
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as lText from '#kebab/lib/text.js';
|
|
5
|
+
/** --- 单个事件记录读取上限,避免损坏或异常大的文件占用 master 内存 --- */
|
|
6
|
+
const RECORD_LIMIT = 4 * 1024 * 1024;
|
|
7
|
+
/** --- 小型 JSON/文本预览上限,Profile 和堆快照不预览 --- */
|
|
8
|
+
export const PREVIEW_LIMIT = 1024 * 1024;
|
|
9
|
+
const DAY_PATTERN = /^\d{4}\/\d{2}\/\d{2}$/;
|
|
10
|
+
const EVENT_NAME_PATTERN = /^\d{6}-pid-\d+(?:-\d+)?(?:-blocked)?$/;
|
|
11
|
+
const FILE_NAME_PATTERNS = [
|
|
12
|
+
/^(?:event\.json|blocked-event\.json|(?:report|profile)-[\w.-]+\.json)$/,
|
|
13
|
+
/^(?:cpu|blocked-cpu)-[\w.-]+\.cpuprofile$/,
|
|
14
|
+
/^blocked-stack-[\w.-]+\.txt$/,
|
|
15
|
+
/^heap-[\w.-]+\.heapsnapshot$/,
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* --- 仅开放框架生成的监控取证文件名 ---
|
|
19
|
+
* @param name 文件名
|
|
20
|
+
* @returns 是否为诊断文件
|
|
21
|
+
*/
|
|
22
|
+
function isFileName(name) {
|
|
23
|
+
return FILE_NAME_PATTERNS.some(pattern => pattern.test(name));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* --- 解析监控目录内已有路径,阻止目录穿越和符号链接逃逸 ---
|
|
27
|
+
* @param root log/monitor 的绝对路径
|
|
28
|
+
* @param relative 已校验格式的相对路径
|
|
29
|
+
* @returns 真实路径;不存在时返回 null,越界或读取错误时返回 false
|
|
30
|
+
*/
|
|
31
|
+
async function resolveInside(root, relative) {
|
|
32
|
+
try {
|
|
33
|
+
if ((await fs.lstat(root)).isSymbolicLink()) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const rootReal = await fs.realpath(root);
|
|
37
|
+
const target = await fs.realpath(path.join(rootReal, relative));
|
|
38
|
+
const difference = path.relative(rootReal, target);
|
|
39
|
+
if (difference === '..' || difference.startsWith(`..${path.sep}`) || path.isAbsolute(difference)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return target;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return error.code === 'ENOENT' ? null : false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* --- 校验事件相对路径,不接受绝对路径、反斜杠或多余层级 ---
|
|
50
|
+
* @param relative 事件路径
|
|
51
|
+
* @returns 是否为日期下的事件目录
|
|
52
|
+
*/
|
|
53
|
+
function isEventPath(relative) {
|
|
54
|
+
if (typeof relative !== 'string') {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const parts = relative.split('/');
|
|
58
|
+
return parts.length === 4 && DAY_PATTERN.test(parts.slice(0, 3).join('/')) && EVENT_NAME_PATTERN.test(parts[3]);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* --- 有界读取原始事件;没有记录的旧诊断目录仍可列出文件 ---
|
|
62
|
+
* @param directory 事件真实目录
|
|
63
|
+
* @returns 原始数据及读取状态
|
|
64
|
+
*/
|
|
65
|
+
async function readRecord(directory) {
|
|
66
|
+
for (const name of ['event.json', 'blocked-event.json']) {
|
|
67
|
+
let handle = null;
|
|
68
|
+
try {
|
|
69
|
+
// --- 拒绝事件记录本身的符号链接;读取过程中即使文件增长也不会超过上限 ---
|
|
70
|
+
const stat = await fs.lstat(path.join(directory, name));
|
|
71
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
72
|
+
return { 'data': null, 'status': 'unreadable' };
|
|
73
|
+
}
|
|
74
|
+
if (stat.size > RECORD_LIMIT) {
|
|
75
|
+
return { 'data': null, 'status': 'too-large' };
|
|
76
|
+
}
|
|
77
|
+
handle = await fs.open(path.join(directory, name), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
78
|
+
const buffer = Buffer.alloc(stat.size + 1);
|
|
79
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
80
|
+
if (bytesRead > RECORD_LIMIT) {
|
|
81
|
+
return { 'data': null, 'status': 'too-large' };
|
|
82
|
+
}
|
|
83
|
+
const data = lText.parseJson(buffer.subarray(0, bytesRead).toString('utf8'));
|
|
84
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
85
|
+
return { 'data': null, 'status': 'invalid' };
|
|
86
|
+
}
|
|
87
|
+
return { 'data': data, 'status': 'loaded' };
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error.code !== 'ENOENT') {
|
|
91
|
+
return { 'data': null, 'status': 'unreadable' };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await handle?.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { 'data': null, 'status': 'missing' };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* --- 从目录名和事件记录生成简短摘要,不携带大段采样或调用栈 ---
|
|
102
|
+
* @param relative 事件相对路径
|
|
103
|
+
* @param record 原始事件读取结果
|
|
104
|
+
* @returns 事件摘要
|
|
105
|
+
*/
|
|
106
|
+
function summarize(relative, record) {
|
|
107
|
+
const [year, month, day, name] = relative.split('/');
|
|
108
|
+
const parts = name.split('-');
|
|
109
|
+
const time = Number(parts[3]) || new Date(`${year}-${month}-${day}T${parts[0].slice(0, 2)}:${parts[0].slice(2, 4)}:${parts[0].slice(4, 6)}`).getTime();
|
|
110
|
+
const data = record.data;
|
|
111
|
+
const source = name.endsWith('-blocked') ? 'watchdog' : 'main';
|
|
112
|
+
let status = record.status === 'loaded' ? 'active' : record.status;
|
|
113
|
+
if (typeof data?.endReason === 'string') {
|
|
114
|
+
status = data.endReason;
|
|
115
|
+
}
|
|
116
|
+
else if (typeof data?.status === 'string') {
|
|
117
|
+
status = data.status;
|
|
118
|
+
}
|
|
119
|
+
let eventTime = time;
|
|
120
|
+
if (typeof data?.confirmedAt === 'number') {
|
|
121
|
+
eventTime = data.confirmedAt;
|
|
122
|
+
}
|
|
123
|
+
else if (typeof data?.time === 'number') {
|
|
124
|
+
eventTime = data.time;
|
|
125
|
+
}
|
|
126
|
+
let reasons = [];
|
|
127
|
+
if (Array.isArray(data?.reasons)) {
|
|
128
|
+
reasons = data.reasons.filter((reason) => typeof reason === 'string');
|
|
129
|
+
}
|
|
130
|
+
else if (source === 'watchdog') {
|
|
131
|
+
reasons = ['ELOOP_BLOCKED'];
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
'path': relative,
|
|
135
|
+
'pid': typeof data?.pid === 'number' ? data.pid : Number(parts[2]),
|
|
136
|
+
'time': eventTime,
|
|
137
|
+
source, status, reasons,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* --- 获取一天的事件摘要,先分页目录再读取记录 ---
|
|
142
|
+
* @param root log/monitor 的绝对路径
|
|
143
|
+
* @param day YYYY/MM/DD
|
|
144
|
+
* @param offset 跳过目录数量
|
|
145
|
+
* @param limit 返回数量,1-100
|
|
146
|
+
* @returns 列表和总数;目录不存在返回空列表,输入或读取错误返回 false
|
|
147
|
+
*/
|
|
148
|
+
export async function getEvents(root, day, offset = 0, limit = 20) {
|
|
149
|
+
if (typeof day !== 'string' || !DAY_PATTERN.test(day) || !Number.isSafeInteger(offset) ||
|
|
150
|
+
typeof offset !== 'number' || offset < 0 || !Number.isSafeInteger(limit) || typeof limit !== 'number' || limit < 1 || limit > 100) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const directory = await resolveInside(root, day);
|
|
154
|
+
if (directory === false) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
if (directory === null) {
|
|
158
|
+
return { 'list': [], 'total': 0 };
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const entries = await fs.readdir(directory, { 'withFileTypes': true });
|
|
162
|
+
const names = entries.filter(entry => entry.isDirectory() && EVENT_NAME_PATTERN.test(entry.name))
|
|
163
|
+
.map(entry => entry.name).sort().reverse();
|
|
164
|
+
const list = [];
|
|
165
|
+
for (const name of names.slice(offset, offset + limit)) {
|
|
166
|
+
const relative = `${day}/${name}`;
|
|
167
|
+
const record = await readRecord(path.join(directory, name));
|
|
168
|
+
list.push(summarize(relative, record));
|
|
169
|
+
}
|
|
170
|
+
return { list, 'total': names.length };
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* --- 获取允许读取的诊断文件及其元数据,文件路径限制在日期/事件目录下 ---
|
|
178
|
+
* @param root log/monitor 的绝对路径
|
|
179
|
+
* @param relative files 中返回的文件相对路径
|
|
180
|
+
* @returns 真实路径、文件状态和元数据;不存在返回 null,非法路径或读取错误返回 false
|
|
181
|
+
*/
|
|
182
|
+
export async function getFile(root, relative) {
|
|
183
|
+
if (typeof relative !== 'string' || !isEventPath(path.posix.dirname(relative)) || !isFileName(path.posix.basename(relative))) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const file = await resolveInside(root, relative);
|
|
187
|
+
if (file === null || file === false) {
|
|
188
|
+
return file;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const stat = await fs.stat(file);
|
|
192
|
+
if (!stat.isFile()) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
let preview = null;
|
|
196
|
+
if (stat.size <= PREVIEW_LIMIT) {
|
|
197
|
+
if (relative.endsWith('.json')) {
|
|
198
|
+
preview = 'json';
|
|
199
|
+
}
|
|
200
|
+
else if (relative.endsWith('.txt')) {
|
|
201
|
+
preview = 'text';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { 'path': file, stat, 'info': {
|
|
205
|
+
'path': relative, 'name': path.basename(relative), 'size': stat.size, 'mtime': stat.mtimeMs, preview,
|
|
206
|
+
} };
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
return error.code === 'ENOENT' ? null : false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* --- 获取事件原始数据及文件清单,包含 diagnostics 引用的看门狗取证文件 ---
|
|
214
|
+
* @param root log/monitor 的绝对路径
|
|
215
|
+
* @param relative 事件相对路径
|
|
216
|
+
* @returns 事件详情;不存在返回 null,非法路径或读取错误返回 false
|
|
217
|
+
*/
|
|
218
|
+
export async function getEvent(root, relative) {
|
|
219
|
+
if (!isEventPath(relative)) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
const directory = await resolveInside(root, relative);
|
|
223
|
+
if (directory === null || directory === false) {
|
|
224
|
+
return directory;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const record = await readRecord(directory);
|
|
228
|
+
const entries = await fs.readdir(directory, { 'withFileTypes': true });
|
|
229
|
+
const candidates = new Set(entries.filter(entry => entry.isFile() && isFileName(entry.name))
|
|
230
|
+
.map(entry => `${relative}/${entry.name}`));
|
|
231
|
+
if (Array.isArray(record.data?.diagnostics)) {
|
|
232
|
+
// --- 主线程事件会引用另一个阻塞目录;只将监控根目录内的文件加入下载清单 ---
|
|
233
|
+
for (const diagnostic of record.data.diagnostics) {
|
|
234
|
+
if (diagnostic === null || typeof diagnostic !== 'object') {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const files = diagnostic.files;
|
|
238
|
+
if (!Array.isArray(files)) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
for (const file of files) {
|
|
242
|
+
if (typeof file === 'string' && path.isAbsolute(file)) {
|
|
243
|
+
candidates.add(path.relative(root, file).split(path.sep).join('/'));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const files = [];
|
|
249
|
+
for (const candidate of [...candidates].sort()) {
|
|
250
|
+
const file = await getFile(root, candidate);
|
|
251
|
+
if (file !== false && file !== null) {
|
|
252
|
+
files.push(file.info);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { 'summary': summarize(relative, record), 'data': record.data, 'recordStatus': record.status, files };
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|