forked from Ikalus1988/MisakaNet
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfatal-error-hooks-rawless.ts
More file actions
117 lines (102 loc) · 4.05 KB
/
Copy pathfatal-error-hooks-rawless.ts
File metadata and controls
117 lines (102 loc) · 4.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
* Updated src/infra/fatal-error-hooks.ts — 移除 RAW=1 (方案A)
*
* Changes from current PR version:
* - 删除 OPENCLAW_ERROR_HANDLER_RAW env var 读取
* - 删除 includeRaw / error / isError 变量
* - 删除 if (includeRaw && isError) payload 扩展块
* - JSDoc 简化,去掉 RAW 相关说明
* - payload 固定 4 字段: schemaVersion, reason, timestamp, pid
*/
import { spawn } from "node:child_process";
/** Context passed to fatal-error hooks before the process exits. */
export type FatalErrorHookContext = {
reason: string;
error?: unknown;
};
/** Hook that can return one extra diagnostic line for fatal error output. */
export type FatalErrorHook = (context: FatalErrorHookContext) => string | undefined | void;
const hooks = new Set<FatalErrorHook>();
function formatHookFailure(error: unknown): string {
const name = error instanceof Error && error.name ? error.name : "unknown";
return `fatal-error hook failed: ${name}`;
}
/** Registers a fatal-error hook and returns an unsubscribe callback. */
export function registerFatalErrorHook(hook: FatalErrorHook): () => void {
hooks.add(hook);
return () => {
hooks.delete(hook);
};
}
/**
* If OPENCLAW_ERROR_HANDLER is set, spawns the executable with a
* structured payload as the first argv entry. The handler must be a
* path to an executable (not a shell command) — shell expansion is
* deliberately disabled.
*
* Security: `shell: false` prevents command injection via the env var.
*
* Privacy: the payload is intentionally limited to non-sensitive fields
* (schemaVersion, reason, timestamp, pid) to avoid leaking diagnostic
* details through argv, which is visible to process listings, audit
* logs, and platform telemetry.
*
* Lifetime: the handler runs detached and is `unref()`'d; OpenClaw does
* not wait for its completion. The payload is delivered atomically as
* argv[1] during execve, which avoids the parent `process.exit()` <->
* child stdin drain race that a `stdio: "pipe"` approach would
* introduce.
*/
function runExternalErrorHandler(context: FatalErrorHookContext): void {
const handler = process.env.OPENCLAW_ERROR_HANDLER?.trim();
if (!handler) return;
try {
const payload: Record<string, unknown> = {
schemaVersion: 1,
reason: context.reason,
timestamp: new Date().toISOString(),
pid: process.pid,
};
const child = spawn(handler, [JSON.stringify(payload)], {
stdio: ["ignore", "inherit", "inherit"],
detached: true,
shell: false,
});
// Async 'error' fires when spawn fails asynchronously (ENOENT, EACCES,
// etc.). The synchronous try/catch above does not catch it, and a
// detached + unref()'d child would otherwise let the event bubble to
// the top-level uncaughtException handler. Swallow it here so a
// misconfigured handler cannot corrupt OpenClaw's exit pathway.
child.on("error", () => {
// Intentionally silent: the primary fatal-error output has already
// been written, and surfacing the handler's failure here would
// just look like OpenClaw itself crashed.
});
child.unref();
} catch (err) {
console.error("[fatal-error-hooks] OPENCLAW_ERROR_HANDLER failed:", String(err));
}
}
/** Runs registered fatal-error hooks and returns non-empty diagnostic lines. */
export function runFatalErrorHooks(context: FatalErrorHookContext): string[] {
const messages: string[] = [];
for (const hook of hooks) {
try {
const message = hook(context);
if (typeof message === "string" && message.trim()) {
messages.push(message);
}
} catch (err) {
// Fatal output must keep progressing even if a diagnostic hook itself throws.
messages.push(formatHookFailure(err));
}
}
// External handler is best-effort and never contributes to the in-process
// diagnostic stream: its job is to ship a structured payload elsewhere.
runExternalErrorHandler(context);
return messages;
}
/** Clears registered fatal-error hooks; test-only helper. */
export function resetFatalErrorHooksForTest(): void {
hooks.clear();
}