fix(openclaw): harden reply listener process lifecycle
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
appendFileSync,
|
||||||
|
chmodSync,
|
||||||
|
existsSync,
|
||||||
|
renameSync,
|
||||||
|
statSync,
|
||||||
|
unlinkSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from "fs"
|
||||||
|
import {
|
||||||
|
ensureReplyListenerStateDir,
|
||||||
|
REPLY_LISTENER_SECURE_FILE_MODE,
|
||||||
|
getReplyListenerLogFilePath,
|
||||||
|
} from "./reply-listener-paths"
|
||||||
|
|
||||||
|
const MAX_REPLY_LISTENER_LOG_SIZE_BYTES = 1024 * 1024
|
||||||
|
|
||||||
|
export function writeSecureReplyListenerFile(filePath: string, content: string): void {
|
||||||
|
ensureReplyListenerStateDir()
|
||||||
|
writeFileSync(filePath, content, { mode: REPLY_LISTENER_SECURE_FILE_MODE })
|
||||||
|
|
||||||
|
try {
|
||||||
|
chmodSync(filePath, REPLY_LISTENER_SECURE_FILE_MODE)
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotateReplyListenerLogIfNeeded(logPath: string): void {
|
||||||
|
try {
|
||||||
|
if (!existsSync(logPath)) return
|
||||||
|
|
||||||
|
const stats = statSync(logPath)
|
||||||
|
if (stats.size <= MAX_REPLY_LISTENER_LOG_SIZE_BYTES) return
|
||||||
|
|
||||||
|
const backupPath = `${logPath}.old`
|
||||||
|
if (existsSync(backupPath)) {
|
||||||
|
unlinkSync(backupPath)
|
||||||
|
}
|
||||||
|
renameSync(logPath, backupPath)
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logReplyListenerMessage(message: string): void {
|
||||||
|
try {
|
||||||
|
ensureReplyListenerStateDir()
|
||||||
|
const logFilePath = getReplyListenerLogFilePath()
|
||||||
|
rotateReplyListenerLogIfNeeded(logFilePath)
|
||||||
|
const timestamp = new Date().toISOString()
|
||||||
|
appendFileSync(logFilePath, `[${timestamp}] ${message}\n`, {
|
||||||
|
mode: REPLY_LISTENER_SECURE_FILE_MODE,
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { readFileSync } from "fs"
|
||||||
|
import { spawn } from "bun"
|
||||||
|
|
||||||
|
export const REPLY_LISTENER_DAEMON_IDENTITY_MARKER = "--openclaw-reply-listener-daemon"
|
||||||
|
|
||||||
|
const REPLY_LISTENER_DAEMON_ENV_ALLOWLIST = [
|
||||||
|
"PATH",
|
||||||
|
"HOME",
|
||||||
|
"USERPROFILE",
|
||||||
|
"USER",
|
||||||
|
"USERNAME",
|
||||||
|
"LOGNAME",
|
||||||
|
"LANG",
|
||||||
|
"LC_ALL",
|
||||||
|
"LC_CTYPE",
|
||||||
|
"TERM",
|
||||||
|
"TMUX",
|
||||||
|
"TMUX_PANE",
|
||||||
|
"TMPDIR",
|
||||||
|
"TMP",
|
||||||
|
"TEMP",
|
||||||
|
"XDG_RUNTIME_DIR",
|
||||||
|
"XDG_DATA_HOME",
|
||||||
|
"XDG_CONFIG_HOME",
|
||||||
|
"SHELL",
|
||||||
|
"NODE_ENV",
|
||||||
|
"HTTP_PROXY",
|
||||||
|
"HTTPS_PROXY",
|
||||||
|
"http_proxy",
|
||||||
|
"https_proxy",
|
||||||
|
"NO_PROXY",
|
||||||
|
"no_proxy",
|
||||||
|
"SystemRoot",
|
||||||
|
"SYSTEMROOT",
|
||||||
|
"windir",
|
||||||
|
"COMSPEC",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export function createReplyListenerDaemonEnv(extraEnv: Record<string, string>): Record<string, string> {
|
||||||
|
const env: Record<string, string> = {}
|
||||||
|
|
||||||
|
for (const key of REPLY_LISTENER_DAEMON_ENV_ALLOWLIST) {
|
||||||
|
const value = process.env[key]
|
||||||
|
if (value !== undefined) {
|
||||||
|
env[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...env, ...extraEnv }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isReplyListenerProcessRunning(pid: number): boolean {
|
||||||
|
try {
|
||||||
|
process.kill(pid, 0)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isReplyListenerDaemonProcess(pid: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (process.platform === "linux") {
|
||||||
|
const cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf-8")
|
||||||
|
return cmdline.includes(REPLY_LISTENER_DAEMON_IDENTITY_MARKER)
|
||||||
|
}
|
||||||
|
|
||||||
|
const processInfo = spawn(["ps", "-p", String(pid), "-o", "args="], {
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "ignore",
|
||||||
|
})
|
||||||
|
const stdout = await new Response(processInfo.stdout).text()
|
||||||
|
if (processInfo.exitCode !== 0) return false
|
||||||
|
return stdout.includes(REPLY_LISTENER_DAEMON_IDENTITY_MARKER)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { spawn } from "bun"
|
||||||
|
import {
|
||||||
|
createReplyListenerDaemonEnv,
|
||||||
|
REPLY_LISTENER_DAEMON_IDENTITY_MARKER,
|
||||||
|
} from "./reply-listener-process"
|
||||||
|
import { REPLY_LISTENER_STARTUP_TOKEN_ENV } from "./reply-listener-state"
|
||||||
|
|
||||||
|
export interface ReplyListenerSpawnProcess {
|
||||||
|
pid: number | undefined
|
||||||
|
unref(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spawnReplyListenerDaemon(
|
||||||
|
daemonScript: string,
|
||||||
|
startupToken: string,
|
||||||
|
): ReplyListenerSpawnProcess {
|
||||||
|
return spawn(["bun", "run", daemonScript, REPLY_LISTENER_DAEMON_IDENTITY_MARKER], {
|
||||||
|
detached: true,
|
||||||
|
stdio: ["ignore", "ignore", "ignore"],
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: createReplyListenerDaemonEnv({
|
||||||
|
[REPLY_LISTENER_STARTUP_TOKEN_ENV]: startupToken,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user