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