| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- import os from 'os';
- import { execFile } from 'child_process';
- import { promisify } from 'util';
- const execFileAsync = promisify(execFile);
- type AgentJob = {
- runId: string;
- traceId: string;
- targetId: string;
- targetName: string;
- serviceKey: string;
- type: string;
- endpoint: string;
- config: Record<string, unknown>;
- };
- type HeartbeatResponse = {
- ok: boolean;
- control: {
- enabled: boolean;
- monitoringEnabled: boolean;
- logIngestEnabled: boolean;
- proactivePushEnabled: boolean;
- heartbeatIntervalMs: number;
- };
- };
- const API_BASE =
- (process.env.MONITORING_API_BASE_URL ?? 'http://localhost:3001').replace(
- /\/$/,
- '',
- );
- const AGENT_KEY = process.env.MONITORING_AGENT_KEY ?? 'local-windows-agent';
- const AGENT_TOKEN =
- process.env.MONITORING_AGENT_TOKEN ?? 'sentai-agent-token';
- const POLL_INTERVAL_MS = Number(process.env.MONITORING_AGENT_POLL_MS ?? 5000);
- const RUN_ONCE = process.argv.includes('--once');
- async function request<T>(path: string, init?: RequestInit) {
- const headers = new Headers(init?.headers);
- headers.set('Content-Type', 'application/json');
- headers.set('x-monitoring-agent-token', AGENT_TOKEN);
- const response = await fetch(`${API_BASE}${path}`, {
- ...init,
- headers,
- });
- if (!response.ok) {
- const text = await response.text();
- throw new Error(`HTTP_${response.status}: ${text || response.statusText}`);
- }
- return (await response.json()) as T;
- }
- async function powershellJson<T>(script: string) {
- const { stdout } = await execFileAsync('powershell', [
- '-NoProfile',
- '-Command',
- script,
- ]);
- const raw = stdout.trim();
- if (!raw) {
- return [] as T[];
- }
- const parsed = JSON.parse(raw) as T | T[];
- return Array.isArray(parsed) ? parsed : [parsed];
- }
- async function findJavaProcess(config: Record<string, unknown>) {
- const matchMode = String(config.processMatchMode ?? 'command');
- if (matchMode === 'pid') {
- const pid = Number(config.processPid ?? config.processMatchValue ?? 0);
- if (!pid) {
- throw new Error('host-agent 监测要求填写 processPid 或 processMatchValue');
- }
- const [process] = await powershellJson<{
- ProcessId: number;
- Name: string;
- CommandLine: string;
- }>(
- `Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}" | Select-Object ProcessId,Name,CommandLine | ConvertTo-Json -Compress`,
- );
- if (!process) {
- throw new Error(`未找到 PID=${pid} 的 Java 进程`);
- }
- return process;
- }
- const keyword = String(config.processMatchValue ?? '').trim();
- if (!keyword) {
- throw new Error('host-agent 监测要求填写 processMatchValue');
- }
- const escaped = keyword.replace(/'/g, "''");
- const [process] = await powershellJson<{
- ProcessId: number;
- Name: string;
- CommandLine: string;
- }>(
- `$keyword='${escaped}'; Get-CimInstance Win32_Process | Where-Object { $_.Name -match '^java' -and $_.CommandLine -like "*$keyword*" } | Select-Object -First 1 ProcessId,Name,CommandLine | ConvertTo-Json -Compress`,
- );
- if (!process) {
- throw new Error(`未找到命令行包含 ${keyword} 的 Java 进程`);
- }
- return process;
- }
- async function tailLog(config: Record<string, unknown>) {
- const logFilePath = String(config.logFilePath ?? '').trim();
- if (!logFilePath) {
- return null;
- }
- const tailLines = Number(config.logTailLines ?? 80);
- const keyword = String(config.logKeyword ?? 'ERROR|WARN|Exception|Timeout');
- const script = `$path='${logFilePath.replace(/'/g, "''")}'; if (Test-Path $path) { Get-Content $path -Tail ${tailLines} | Select-String -Pattern '${keyword.replace(/'/g, "''")}' | Select-Object -Last 20 | ForEach-Object { $_.Line } }`;
- const { stdout } = await execFileAsync('powershell', ['-NoProfile', '-Command', script]);
- const output = stdout.trim();
- return output || null;
- }
- async function runJcmd(pid: number, command: string, jcmdPath: string) {
- const parts = command.split(' ').filter(Boolean);
- const { stdout, stderr } = await execFileAsync(jcmdPath, [
- String(pid),
- ...parts,
- ]);
- return (stdout || stderr).trim();
- }
- async function runJstat(pid: number, options: string[], jstatPath: string) {
- const { stdout, stderr } = await execFileAsync(jstatPath, [
- ...options,
- String(pid),
- ]);
- return (stdout || stderr).trim();
- }
- async function executeJavaJob(job: AgentJob) {
- const config = job.config;
- const process = await findJavaProcess(config);
- const jcmdPath = String(
- config.jcmdPath ?? 'D:\\Antigravity\\soft\\Java\\bin\\jcmd.exe',
- );
- const jstatPath = String(
- config.jstatPath ?? 'D:\\Antigravity\\soft\\Java\\bin\\jstat.exe',
- );
- const jcmdCommands = Array.isArray(config.jcmdCommands)
- ? (config.jcmdCommands as string[])
- : ['VM.command_line', 'GC.heap_info'];
- const jstatOptions = Array.isArray(config.jstatOptions)
- ? (config.jstatOptions as string[])
- : ['-gcutil'];
- const jcmdResults: Record<string, string> = {};
- for (const command of jcmdCommands) {
- jcmdResults[command] = await runJcmd(process.ProcessId, command, jcmdPath);
- }
- let jstatResult = '';
- try {
- jstatResult = await runJstat(process.ProcessId, jstatOptions, jstatPath);
- } catch (error) {
- jstatResult = error instanceof Error ? error.message : 'jstat failed';
- }
- const logExcerpt = await tailLog(config);
- return {
- status: 'success' as const,
- errorTag: null,
- summary: '宿主机 Agent 监测完成',
- failureReason: null,
- metrics: {
- pid: process.ProcessId,
- host: os.hostname(),
- processCommand: process.CommandLine,
- jstatResult,
- },
- details: {
- process,
- jcmdResults,
- agentKey: AGENT_KEY,
- nodeKey: `${job.serviceKey}-node-1`,
- },
- logExcerpt,
- };
- }
- async function processNextJob() {
- const claim = await request<{ job: AgentJob | null }>(
- '/api/monitoring/agent/jobs/claim',
- {
- method: 'POST',
- body: JSON.stringify({ agentKey: AGENT_KEY }),
- },
- );
- if (!claim.job) {
- return false;
- }
- try {
- const result =
- claim.job.type === 'java-app'
- ? await executeJavaJob(claim.job)
- : {
- status: 'failed' as const,
- errorTag: 'UNSUPPORTED_AGENT_TARGET',
- summary: '当前 Agent 暂未支持该目标类型',
- failureReason: `type=${claim.job.type} 暂未实现`,
- metrics: {},
- details: { agentKey: AGENT_KEY },
- logExcerpt: null,
- };
- await request(`/api/monitoring/agent/jobs/${claim.job.runId}/result`, {
- method: 'POST',
- body: JSON.stringify(result),
- });
- } catch (error) {
- await request(`/api/monitoring/agent/jobs/${claim.job.runId}/result`, {
- method: 'POST',
- body: JSON.stringify({
- status: 'failed',
- errorTag: 'AGENT_EXECUTION_ERROR',
- summary: '宿主机 Agent 执行失败',
- failureReason: error instanceof Error ? error.message : 'unknown error',
- metrics: {},
- details: { agentKey: AGENT_KEY },
- logExcerpt: null,
- }),
- });
- }
- return true;
- }
- async function main() {
- let heartbeatIntervalMs = POLL_INTERVAL_MS;
- do {
- const heartbeat = await request<HeartbeatResponse>('/api/monitoring/agent/heartbeat', {
- method: 'POST',
- body: JSON.stringify({
- agentKey: AGENT_KEY,
- host: os.hostname(),
- platform: process.platform,
- version: 'host-agent-tsx',
- }),
- });
- heartbeatIntervalMs = heartbeat.control.heartbeatIntervalMs ?? POLL_INTERVAL_MS;
- if (!heartbeat.control.enabled || !heartbeat.control.monitoringEnabled) {
- if (RUN_ONCE) {
- break;
- }
- await new Promise((resolve) => setTimeout(resolve, heartbeatIntervalMs));
- continue;
- }
- const handled = await processNextJob();
- if (RUN_ONCE) {
- break;
- }
- if (!handled) {
- await new Promise((resolve) => setTimeout(resolve, heartbeatIntervalMs));
- }
- } while (true);
- }
- void main().catch((error) => {
- console.error(error);
- process.exit(1);
- });
|