chatccc 0.2.39 → 0.2.40
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/package.json +1 -1
- package/src/__tests__/crash-logging.test.ts +62 -6
- package/src/shared.ts +63 -40
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, it, expect, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { buildCrashLoggingHandlers, installCrashLogging, setupFileLogging } from "../shared.ts";
|
|
4
7
|
|
|
5
8
|
describe("buildCrashLoggingHandlers", () => {
|
|
6
9
|
it("uncaughtException: 写诊断、刷新日志、调用 onFatal", () => {
|
|
@@ -141,7 +144,7 @@ describe("buildCrashLoggingHandlers", () => {
|
|
|
141
144
|
});
|
|
142
145
|
});
|
|
143
146
|
|
|
144
|
-
describe("installCrashLogging", () => {
|
|
147
|
+
describe("installCrashLogging", () => {
|
|
145
148
|
it("注册所有相关事件监听器", () => {
|
|
146
149
|
const before = {
|
|
147
150
|
uncaught: process.listenerCount("uncaughtException"),
|
|
@@ -206,5 +209,58 @@ describe("installCrashLogging", () => {
|
|
|
206
209
|
} finally {
|
|
207
210
|
cleanup();
|
|
208
211
|
}
|
|
209
|
-
});
|
|
210
|
-
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("setupFileLogging", () => {
|
|
216
|
+
it("flush 后继续写日志不会触发 write after end,且日志已落盘", async () => {
|
|
217
|
+
const originalLog = console.log;
|
|
218
|
+
const originalError = console.error;
|
|
219
|
+
console.log = vi.fn() as never;
|
|
220
|
+
console.error = vi.fn() as never;
|
|
221
|
+
const dir = await mkdtemp(join(tmpdir(), "chatccc-log-"));
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const fileLog = setupFileLogging(dir, "index");
|
|
225
|
+
|
|
226
|
+
console.log("before flush");
|
|
227
|
+
fileLog.flush();
|
|
228
|
+
|
|
229
|
+
expect(() => console.error("after flush")).not.toThrow();
|
|
230
|
+
fileLog.flush();
|
|
231
|
+
|
|
232
|
+
const content = await readFile(fileLog.logPath, "utf8");
|
|
233
|
+
expect(content).toContain("[LOG] before flush");
|
|
234
|
+
expect(content).toContain("[ERR] after flush");
|
|
235
|
+
} finally {
|
|
236
|
+
console.log = originalLog;
|
|
237
|
+
console.error = originalError;
|
|
238
|
+
await rm(dir, { recursive: true, force: true });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("日志参数无法 JSON 序列化时也不会让 console 调用抛错", async () => {
|
|
243
|
+
const originalLog = console.log;
|
|
244
|
+
const originalError = console.error;
|
|
245
|
+
console.log = vi.fn() as never;
|
|
246
|
+
console.error = vi.fn() as never;
|
|
247
|
+
const dir = await mkdtemp(join(tmpdir(), "chatccc-log-"));
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const fileLog = setupFileLogging(dir, "index");
|
|
251
|
+
const circular: Record<string, unknown> = { name: "root" };
|
|
252
|
+
circular.self = circular;
|
|
253
|
+
|
|
254
|
+
expect(() => console.log("circular", circular)).not.toThrow();
|
|
255
|
+
fileLog.flush();
|
|
256
|
+
|
|
257
|
+
const content = await readFile(fileLog.logPath, "utf8");
|
|
258
|
+
expect(content).toContain("[LOG] circular");
|
|
259
|
+
expect(content).toContain("self");
|
|
260
|
+
} finally {
|
|
261
|
+
console.log = originalLog;
|
|
262
|
+
console.error = originalError;
|
|
263
|
+
await rm(dir, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
});
|
package/src/shared.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
appendFileSync,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "node:
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { WebSocketServer, WebSocket } from "ws";
|
|
2
|
+
import {
|
|
3
|
+
appendFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { createServer } from "node:http";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { inspect } from "node:util";
|
|
14
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
15
15
|
|
|
16
16
|
import { printServiceDidNotStart } from "./exit-banner.ts";
|
|
17
17
|
|
|
@@ -355,33 +355,56 @@ export function installCrashLogging(opts: CrashHandlersOptions = {}): InstallCra
|
|
|
355
355
|
// 文件日志:同时输出到控制台和日志文件
|
|
356
356
|
// ---------------------------------------------------------------------------
|
|
357
357
|
|
|
358
|
-
export function setupFileLogging(logDir: string, prefix: string): { logPath: string; flush: () => void } {
|
|
359
|
-
mkdirSync(logDir, { recursive: true });
|
|
360
|
-
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
361
|
-
const logPath = join(logDir, `${prefix}-${ts}.log`);
|
|
362
|
-
|
|
363
|
-
const origConsoleLog = console.log.bind(console);
|
|
364
|
-
const origConsoleError = console.error.bind(console);
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
358
|
+
export function setupFileLogging(logDir: string, prefix: string): { logPath: string; flush: () => void } {
|
|
359
|
+
mkdirSync(logDir, { recursive: true });
|
|
360
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
361
|
+
const logPath = join(logDir, `${prefix}-${ts}.log`);
|
|
362
|
+
writeFileSync(logPath, "", { flag: "a", encoding: "utf8" });
|
|
363
|
+
const origConsoleLog = console.log.bind(console);
|
|
364
|
+
const origConsoleError = console.error.bind(console);
|
|
365
|
+
const formatArg = (arg: unknown): string => {
|
|
366
|
+
if (typeof arg === "string") return arg;
|
|
367
|
+
if (arg instanceof Error) return arg.stack ?? arg.message;
|
|
368
|
+
return inspect(arg, { depth: 6, breakLength: Infinity, maxArrayLength: 200 });
|
|
369
|
+
};
|
|
370
|
+
const writeLine = (level: string, args: unknown[]) => {
|
|
371
|
+
try {
|
|
372
|
+
const line = args.map(formatArg).join(" ");
|
|
373
|
+
appendFileSync(logPath, `[${new Date().toISOString()}] [${level}] ${line}\n`, "utf8");
|
|
374
|
+
} catch (err) {
|
|
375
|
+
try {
|
|
376
|
+
appendStartupTrace("fileLog write failed", { level, error: err instanceof Error ? err.message : String(err) });
|
|
377
|
+
} catch {
|
|
378
|
+
// 日志系统自身不能影响主流程
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
console.log = (...args: unknown[]) => {
|
|
383
|
+
writeLine("LOG", args);
|
|
384
|
+
try {
|
|
385
|
+
origConsoleLog(...args);
|
|
386
|
+
} catch {
|
|
387
|
+
// 控制台输出失败也不能拖垮服务
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
console.error = (...args: unknown[]) => {
|
|
391
|
+
writeLine("ERR", args);
|
|
392
|
+
try {
|
|
393
|
+
origConsoleError(...args);
|
|
394
|
+
} catch {
|
|
395
|
+
// 控制台输出失败也不能拖垮服务
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const flush = () => {
|
|
399
|
+
try {
|
|
400
|
+
appendFileSync(logPath, "", "utf8");
|
|
401
|
+
} catch (err) {
|
|
402
|
+
appendStartupTrace("fileLog flush failed", { error: err instanceof Error ? err.message : String(err) });
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
origConsoleLog(`Log file: ${logPath}`);
|
|
406
|
+
return { logPath, flush };
|
|
407
|
+
}
|
|
385
408
|
|
|
386
409
|
// ---------------------------------------------------------------------------
|
|
387
410
|
// 本地 WebSocket 中继服务器(同一端口、多客户端广播)
|