Files
memind/scripts/monitor-goosed-fds.mjs
T
john b6def0f25b fix(ops): 自愈 goosed 会话 PostgreSQL,防止 H5 卡在创建新对话
103 Colima goosed 依赖宿主机 postgresql@17;库停止时 /agent/start 会 pool timeout 约 60s。新增 ensure 脚本、接入监控与发布前检查,并补充拓扑文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 16:57:38 +08:00

152 lines
5.0 KiB
JavaScript
Executable File

#!/usr/bin/env node
import { execFileSync, spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ENSURE_GOOSE_SESSION_POSTGRES = path.join(__dirname, 'ensure-goose-session-postgres.sh');
const DRY_RUN = process.argv.includes('--dry-run');
const FD_WARN = Number(process.env.GOOSED_FD_WARN ?? 180);
const FD_RESTART = Number(process.env.GOOSED_FD_RESTART ?? 3200);
const SANDBOX_MAX_TOTAL = Number(process.env.SANDBOX_MCP_MAX_TOTAL ?? 80);
const SANDBOX_MAX_PER_ROOT = Number(process.env.SANDBOX_MCP_MAX_PER_ROOT ?? 2);
function sh(command) {
return execFileSync('/bin/zsh', ['-lc', command], { encoding: 'utf8' }).trim();
}
function run(command, args) {
if (DRY_RUN) {
console.log(`[dry-run] ${command} ${args.join(' ')}`);
return { status: 0 };
}
return spawnSync(command, args, { stdio: 'inherit' });
}
function psLines(pattern) {
const output = sh(
`ps -axo pid=,ppid=,etime=,command= | grep ${JSON.stringify(pattern)} | grep -v grep || true`,
);
if (!output) return [];
return output.split('\n').map((line) => {
const match = line.trim().match(/^(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/);
if (!match) return null;
return {
pid: Number(match[1]),
ppid: Number(match[2]),
etimes: etimeToSeconds(match[3]),
command: match[4],
};
}).filter(Boolean);
}
function etimeToSeconds(value) {
const parts = String(value ?? '').split('-');
const dayPart = parts.length === 2 ? Number(parts[0]) : 0;
const timePart = parts.at(-1) ?? '0';
const nums = timePart.split(':').map((item) => Number(item));
if (nums.some((item) => !Number.isFinite(item))) return 0;
const [hours, minutes, seconds] =
nums.length === 3 ? nums : nums.length === 2 ? [0, nums[0], nums[1]] : [0, 0, nums[0]];
return dayPart * 86400 + hours * 3600 + minutes * 60 + seconds;
}
function fdCount(pid) {
const count = sh(`lsof -n -p ${pid} 2>/dev/null | wc -l || true`);
return Number(count.trim() || 0);
}
function killPid(pid, reason) {
console.log(`kill pid=${pid} reason=${reason}`);
run('/bin/kill', ['-TERM', String(pid)]);
}
function rootForSandbox(command) {
const marker = 'mindspace-sandbox-mcp.mjs ';
const idx = command.indexOf(marker);
if (idx < 0) return '';
return command.slice(idx + marker.length).trim().split(/\s+/)[0] ?? '';
}
function cleanupSandboxProcesses() {
const processes = psLines('mindspace-sandbox-mcp.mjs');
const byParentRoot = new Map();
for (const proc of processes) {
const root = rootForSandbox(proc.command);
const key = `${proc.ppid}:${root}`;
if (!byParentRoot.has(key)) byParentRoot.set(key, []);
byParentRoot.get(key).push(proc);
}
const toKill = new Map();
for (const group of byParentRoot.values()) {
group.sort((a, b) => b.etimes - a.etimes);
for (const proc of group.slice(0, Math.max(0, group.length - SANDBOX_MAX_PER_ROOT))) {
toKill.set(proc.pid, { proc, reason: 'duplicate-sandbox-root' });
}
}
for (const proc of processes) {
const parentAlive = proc.ppid > 1 && psLines(`^${proc.ppid} `).length > 0;
if (!parentAlive && proc.etimes > 60) {
toKill.set(proc.pid, { proc, reason: 'orphan-sandbox' });
}
}
if (processes.length > SANDBOX_MAX_TOTAL) {
const oldest = [...processes].sort((a, b) => b.etimes - a.etimes);
for (const proc of oldest.slice(0, processes.length - SANDBOX_MAX_TOTAL)) {
toKill.set(proc.pid, { proc, reason: `over-total-${SANDBOX_MAX_TOTAL}` });
}
}
for (const { proc, reason } of toKill.values()) {
killPid(proc.pid, reason);
}
console.log(`sandbox processes=${processes.length} killed=${toKill.size}`);
}
function goosedLabel(command) {
const port = command.match(/\b(18006|18007)\b/)?.[1];
return port ? `cn.tkmind.goosed-${port}` : null;
}
function monitorGoosed() {
const processes = psLines('goosed agent');
for (const proc of processes) {
const fds = fdCount(proc.pid);
console.log(`goosed pid=${proc.pid} fd=${fds} cmd=${proc.command}`);
if (fds >= FD_RESTART) {
const label = goosedLabel(proc.command);
if (label) {
console.log(`restart ${label} fd=${fds} threshold=${FD_RESTART}`);
run('/bin/launchctl', ['kickstart', '-k', `gui/${process.getuid()}/${label}`]);
}
} else if (fds >= FD_WARN) {
console.warn(`warn goosed pid=${proc.pid} fd=${fds} threshold=${FD_WARN}`);
}
}
}
function ensureGooseSessionPostgres() {
try {
const result = spawnSync('/bin/bash', [ENSURE_GOOSE_SESSION_POSTGRES], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
if (output) console.log(output);
if (result.status !== 0) {
console.warn('goose session postgres ensure failed');
}
} catch (err) {
console.warn(
'goose session postgres ensure skipped:',
err instanceof Error ? err.message : err,
);
}
}
ensureGooseSessionPostgres();
cleanupSandboxProcesses();
monitorGoosed();