b6def0f25b
103 Colima goosed 依赖宿主机 postgresql@17;库停止时 /agent/start 会 pool timeout 约 60s。新增 ensure 脚本、接入监控与发布前检查,并补充拓扑文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
152 lines
5.0 KiB
JavaScript
Executable File
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();
|