210 lines
6.5 KiB
JavaScript
210 lines
6.5 KiB
JavaScript
// 안티그래비티 Orca 상태 및 EST 5175 헬스 백그라운드 감시 스크립트
|
|
// 상태 변경 시에만 기록하며, 안티그래비티 완료/대기 또는 서버 장애 시 Codex Orchestration Run으로 알림 전송.
|
|
// 자동 재시작이나 코드 수정 지시는 일절 포함하지 않음.
|
|
const http = require("node:http");
|
|
const { execFile } = require("node:child_process");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const TARGET_RUN = "run_cdb1558b9280";
|
|
const logFile = path.join(__dirname, "monitor.log");
|
|
const pidFile = path.join(__dirname, "monitor.pid");
|
|
const MAX_LOG_BYTES = 512 * 1024; // 512KB 순환
|
|
|
|
// 중복 실행 방지
|
|
if (fs.existsSync(pidFile)) {
|
|
try {
|
|
const existingPid = Number(fs.readFileSync(pidFile, "utf8").trim());
|
|
process.kill(existingPid, 0);
|
|
console.log(`[monitor] 이미 감시 프로세스가 실행 중입니다 (PID: ${existingPid}). 종료합니다.`);
|
|
process.exit(0);
|
|
} catch {}
|
|
}
|
|
|
|
fs.writeFileSync(pidFile, String(process.pid), "utf8");
|
|
|
|
function cleanup() {
|
|
try {
|
|
if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
|
|
} catch {}
|
|
process.exit(0);
|
|
}
|
|
|
|
process.on("SIGINT", cleanup);
|
|
process.on("SIGTERM", cleanup);
|
|
process.on("exit", () => {
|
|
try {
|
|
if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
|
|
} catch {}
|
|
});
|
|
|
|
function log(message) {
|
|
const line = `[${new Date().toISOString()}] ${message}\n`;
|
|
try {
|
|
if (fs.existsSync(logFile) && fs.statSync(logFile).size > MAX_LOG_BYTES) {
|
|
const old = fs.readFileSync(logFile, "utf8");
|
|
fs.writeFileSync(logFile, old.slice(-MAX_LOG_BYTES / 2), "utf8");
|
|
}
|
|
fs.appendFileSync(logFile, line, "utf8");
|
|
} catch {}
|
|
}
|
|
|
|
log(`감시 시작 (PID: ${process.pid})`);
|
|
|
|
let lastAntigravityState = null;
|
|
let lastHealthStatus = null;
|
|
let notifiedDone = false;
|
|
let notifiedFailure = false;
|
|
|
|
const ORCA_BIN = "C:\\Users\\umsan\\AppData\\Local\\Programs\\orca\\resources\\bin\\orca.exe";
|
|
|
|
function sendOrchestrationNotification(subject, body, type = "status") {
|
|
// 민감한 환경변수 노출 방지 및 타임아웃 5초 설정 (shell: false로 공백/인자 안정성 확보)
|
|
execFile(
|
|
ORCA_BIN,
|
|
[
|
|
"orchestration",
|
|
"send",
|
|
"--to",
|
|
`run:${TARGET_RUN}`,
|
|
"--type",
|
|
type,
|
|
"--subject",
|
|
subject,
|
|
"--body",
|
|
body,
|
|
"--json",
|
|
],
|
|
{ timeout: 5000, shell: false },
|
|
(error, stdout, stderr) => {
|
|
if (error) {
|
|
log(`Orchestration 알림 실패: ${error.message}`);
|
|
return;
|
|
}
|
|
try {
|
|
const res = JSON.parse(stdout);
|
|
if (res.ok) {
|
|
log(`Orchestration 알림 성공: messageId=${res.result?.messageId}`);
|
|
} else {
|
|
log(`Orchestration 알림 거부: ${JSON.stringify(res.error || res)}`);
|
|
}
|
|
} catch (e) {
|
|
log(`Orchestration 응답 파싱 오류: ${stdout}`);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
function checkOrcaAntigravity() {
|
|
execFile(
|
|
ORCA_BIN,
|
|
["worktree", "ps", "--json"],
|
|
{ timeout: 4000, maxBuffer: 1024 * 1024, shell: false },
|
|
(error, stdout) => {
|
|
if (error) {
|
|
if (lastAntigravityState !== `error:${error.message}`) {
|
|
lastAntigravityState = `error:${error.message}`;
|
|
log(`Orca 확인 실패: ${error.message}`);
|
|
}
|
|
return;
|
|
}
|
|
try {
|
|
const data = JSON.parse(stdout);
|
|
const codexWorktree = data.result?.worktrees?.find((w) =>
|
|
/D:\/aislo-wt\/CODEX/i.test(w.path || ""),
|
|
);
|
|
const antigravityAgent = codexWorktree?.agents?.find((a) => a.agentType === "antigravity");
|
|
const currentState = antigravityAgent?.state || codexWorktree?.status || "unknown";
|
|
|
|
if (currentState !== lastAntigravityState) {
|
|
const prevState = lastAntigravityState;
|
|
lastAntigravityState = currentState;
|
|
log(`안티그래비티 작업 상태 변경: ${prevState} -> ${currentState}`);
|
|
|
|
// 안티그래비티 완료/대기 전환 시 단발성 자동 알림
|
|
if ((currentState === "done" || currentState === "idle") && !notifiedDone) {
|
|
notifiedDone = true;
|
|
sendOrchestrationNotification(
|
|
"안티그래비티 작업 완료 보고",
|
|
`안티그래비티 상태가 ${currentState}로 전이되었습니다. 검토를 재개할 수 있습니다.`,
|
|
"status",
|
|
);
|
|
} else if (currentState === "working") {
|
|
notifiedDone = false; // 다시 작업 중이 되면 플래그 리셋
|
|
}
|
|
}
|
|
} catch (parseErr) {
|
|
if (lastAntigravityState !== `parse-error:${parseErr.message}`) {
|
|
lastAntigravityState = `parse-error:${parseErr.message}`;
|
|
log(`Orca JSON 파싱 오류: ${parseErr.message}`);
|
|
}
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
function checkServerHealth() {
|
|
const req = http.get("http://100.65.141.15:5175/api/health", { timeout: 3000 }, (res) => {
|
|
let data = "";
|
|
res.on("data", (chunk) => (data += chunk));
|
|
res.on("end", () => {
|
|
const statusStr = `HTTP ${res.statusCode}`;
|
|
if (statusStr !== lastHealthStatus) {
|
|
lastHealthStatus = statusStr;
|
|
log(`EST 5175 서버 상태 변경: ${statusStr} (${data.trim()})`);
|
|
}
|
|
if (res.statusCode === 200) {
|
|
notifiedFailure = false;
|
|
} else if (!notifiedFailure) {
|
|
notifiedFailure = true;
|
|
sendOrchestrationNotification(
|
|
"EST 5175 서버 장애 경고",
|
|
`EST 독립 서버 응답이 비정상입니다 (${statusStr}). 확인이 필요합니다.`,
|
|
"status",
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
req.on("error", (err) => {
|
|
const errStr = `error:${err.message}`;
|
|
if (errStr !== lastHealthStatus) {
|
|
lastHealthStatus = errStr;
|
|
log(`EST 5175 접속 실패: ${err.message}`);
|
|
}
|
|
if (!notifiedFailure) {
|
|
notifiedFailure = true;
|
|
sendOrchestrationNotification(
|
|
"EST 5175 서버 접속 불가",
|
|
`EST 독립 서버 연결에 실패했습니다 (${err.message}).`,
|
|
"status",
|
|
);
|
|
}
|
|
});
|
|
|
|
req.on("timeout", () => {
|
|
req.destroy();
|
|
const timeoutStr = "timeout";
|
|
if (timeoutStr !== lastHealthStatus) {
|
|
lastHealthStatus = timeoutStr;
|
|
log("EST 5175 응답 시간 초과");
|
|
}
|
|
if (!notifiedFailure) {
|
|
notifiedFailure = true;
|
|
sendOrchestrationNotification(
|
|
"EST 5175 서버 응답 시간 초과",
|
|
"EST 독립 서버 헬스 체크 응답 시간이 초과되었습니다.",
|
|
"status",
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
function poll() {
|
|
checkOrcaAntigravity();
|
|
checkServerHealth();
|
|
}
|
|
|
|
poll();
|
|
setInterval(poll, 15000);
|