host-agent.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import os from 'os';
  2. import { execFile } from 'child_process';
  3. import { promisify } from 'util';
  4. const execFileAsync = promisify(execFile);
  5. type AgentJob = {
  6. runId: string;
  7. traceId: string;
  8. targetId: string;
  9. targetName: string;
  10. serviceKey: string;
  11. type: string;
  12. endpoint: string;
  13. config: Record<string, unknown>;
  14. };
  15. type HeartbeatResponse = {
  16. ok: boolean;
  17. control: {
  18. enabled: boolean;
  19. monitoringEnabled: boolean;
  20. logIngestEnabled: boolean;
  21. proactivePushEnabled: boolean;
  22. heartbeatIntervalMs: number;
  23. };
  24. };
  25. const API_BASE =
  26. (process.env.MONITORING_API_BASE_URL ?? 'http://localhost:3001').replace(
  27. /\/$/,
  28. '',
  29. );
  30. const AGENT_KEY = process.env.MONITORING_AGENT_KEY ?? 'local-windows-agent';
  31. const AGENT_TOKEN =
  32. process.env.MONITORING_AGENT_TOKEN ?? 'sentai-agent-token';
  33. const POLL_INTERVAL_MS = Number(process.env.MONITORING_AGENT_POLL_MS ?? 5000);
  34. const RUN_ONCE = process.argv.includes('--once');
  35. async function request<T>(path: string, init?: RequestInit) {
  36. const headers = new Headers(init?.headers);
  37. headers.set('Content-Type', 'application/json');
  38. headers.set('x-monitoring-agent-token', AGENT_TOKEN);
  39. const response = await fetch(`${API_BASE}${path}`, {
  40. ...init,
  41. headers,
  42. });
  43. if (!response.ok) {
  44. const text = await response.text();
  45. throw new Error(`HTTP_${response.status}: ${text || response.statusText}`);
  46. }
  47. return (await response.json()) as T;
  48. }
  49. async function powershellJson<T>(script: string) {
  50. const { stdout } = await execFileAsync('powershell', [
  51. '-NoProfile',
  52. '-Command',
  53. script,
  54. ]);
  55. const raw = stdout.trim();
  56. if (!raw) {
  57. return [] as T[];
  58. }
  59. const parsed = JSON.parse(raw) as T | T[];
  60. return Array.isArray(parsed) ? parsed : [parsed];
  61. }
  62. async function findJavaProcess(config: Record<string, unknown>) {
  63. const matchMode = String(config.processMatchMode ?? 'command');
  64. if (matchMode === 'pid') {
  65. const pid = Number(config.processPid ?? config.processMatchValue ?? 0);
  66. if (!pid) {
  67. throw new Error('host-agent 监测要求填写 processPid 或 processMatchValue');
  68. }
  69. const [process] = await powershellJson<{
  70. ProcessId: number;
  71. Name: string;
  72. CommandLine: string;
  73. }>(
  74. `Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}" | Select-Object ProcessId,Name,CommandLine | ConvertTo-Json -Compress`,
  75. );
  76. if (!process) {
  77. throw new Error(`未找到 PID=${pid} 的 Java 进程`);
  78. }
  79. return process;
  80. }
  81. const keyword = String(config.processMatchValue ?? '').trim();
  82. if (!keyword) {
  83. throw new Error('host-agent 监测要求填写 processMatchValue');
  84. }
  85. const escaped = keyword.replace(/'/g, "''");
  86. const [process] = await powershellJson<{
  87. ProcessId: number;
  88. Name: string;
  89. CommandLine: string;
  90. }>(
  91. `$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`,
  92. );
  93. if (!process) {
  94. throw new Error(`未找到命令行包含 ${keyword} 的 Java 进程`);
  95. }
  96. return process;
  97. }
  98. async function tailLog(config: Record<string, unknown>) {
  99. const logFilePath = String(config.logFilePath ?? '').trim();
  100. if (!logFilePath) {
  101. return null;
  102. }
  103. const tailLines = Number(config.logTailLines ?? 80);
  104. const keyword = String(config.logKeyword ?? 'ERROR|WARN|Exception|Timeout');
  105. 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 } }`;
  106. const { stdout } = await execFileAsync('powershell', ['-NoProfile', '-Command', script]);
  107. const output = stdout.trim();
  108. return output || null;
  109. }
  110. async function runJcmd(pid: number, command: string, jcmdPath: string) {
  111. const parts = command.split(' ').filter(Boolean);
  112. const { stdout, stderr } = await execFileAsync(jcmdPath, [
  113. String(pid),
  114. ...parts,
  115. ]);
  116. return (stdout || stderr).trim();
  117. }
  118. async function runJstat(pid: number, options: string[], jstatPath: string) {
  119. const { stdout, stderr } = await execFileAsync(jstatPath, [
  120. ...options,
  121. String(pid),
  122. ]);
  123. return (stdout || stderr).trim();
  124. }
  125. async function executeJavaJob(job: AgentJob) {
  126. const config = job.config;
  127. const process = await findJavaProcess(config);
  128. const jcmdPath = String(
  129. config.jcmdPath ?? 'D:\\Antigravity\\soft\\Java\\bin\\jcmd.exe',
  130. );
  131. const jstatPath = String(
  132. config.jstatPath ?? 'D:\\Antigravity\\soft\\Java\\bin\\jstat.exe',
  133. );
  134. const jcmdCommands = Array.isArray(config.jcmdCommands)
  135. ? (config.jcmdCommands as string[])
  136. : ['VM.command_line', 'GC.heap_info'];
  137. const jstatOptions = Array.isArray(config.jstatOptions)
  138. ? (config.jstatOptions as string[])
  139. : ['-gcutil'];
  140. const jcmdResults: Record<string, string> = {};
  141. for (const command of jcmdCommands) {
  142. jcmdResults[command] = await runJcmd(process.ProcessId, command, jcmdPath);
  143. }
  144. let jstatResult = '';
  145. try {
  146. jstatResult = await runJstat(process.ProcessId, jstatOptions, jstatPath);
  147. } catch (error) {
  148. jstatResult = error instanceof Error ? error.message : 'jstat failed';
  149. }
  150. const logExcerpt = await tailLog(config);
  151. return {
  152. status: 'success' as const,
  153. errorTag: null,
  154. summary: '宿主机 Agent 监测完成',
  155. failureReason: null,
  156. metrics: {
  157. pid: process.ProcessId,
  158. host: os.hostname(),
  159. processCommand: process.CommandLine,
  160. jstatResult,
  161. },
  162. details: {
  163. process,
  164. jcmdResults,
  165. agentKey: AGENT_KEY,
  166. nodeKey: `${job.serviceKey}-node-1`,
  167. },
  168. logExcerpt,
  169. };
  170. }
  171. async function processNextJob() {
  172. const claim = await request<{ job: AgentJob | null }>(
  173. '/api/monitoring/agent/jobs/claim',
  174. {
  175. method: 'POST',
  176. body: JSON.stringify({ agentKey: AGENT_KEY }),
  177. },
  178. );
  179. if (!claim.job) {
  180. return false;
  181. }
  182. try {
  183. const result =
  184. claim.job.type === 'java-app'
  185. ? await executeJavaJob(claim.job)
  186. : {
  187. status: 'failed' as const,
  188. errorTag: 'UNSUPPORTED_AGENT_TARGET',
  189. summary: '当前 Agent 暂未支持该目标类型',
  190. failureReason: `type=${claim.job.type} 暂未实现`,
  191. metrics: {},
  192. details: { agentKey: AGENT_KEY },
  193. logExcerpt: null,
  194. };
  195. await request(`/api/monitoring/agent/jobs/${claim.job.runId}/result`, {
  196. method: 'POST',
  197. body: JSON.stringify(result),
  198. });
  199. } catch (error) {
  200. await request(`/api/monitoring/agent/jobs/${claim.job.runId}/result`, {
  201. method: 'POST',
  202. body: JSON.stringify({
  203. status: 'failed',
  204. errorTag: 'AGENT_EXECUTION_ERROR',
  205. summary: '宿主机 Agent 执行失败',
  206. failureReason: error instanceof Error ? error.message : 'unknown error',
  207. metrics: {},
  208. details: { agentKey: AGENT_KEY },
  209. logExcerpt: null,
  210. }),
  211. });
  212. }
  213. return true;
  214. }
  215. async function main() {
  216. let heartbeatIntervalMs = POLL_INTERVAL_MS;
  217. do {
  218. const heartbeat = await request<HeartbeatResponse>('/api/monitoring/agent/heartbeat', {
  219. method: 'POST',
  220. body: JSON.stringify({
  221. agentKey: AGENT_KEY,
  222. host: os.hostname(),
  223. platform: process.platform,
  224. version: 'host-agent-tsx',
  225. }),
  226. });
  227. heartbeatIntervalMs = heartbeat.control.heartbeatIntervalMs ?? POLL_INTERVAL_MS;
  228. if (!heartbeat.control.enabled || !heartbeat.control.monitoringEnabled) {
  229. if (RUN_ONCE) {
  230. break;
  231. }
  232. await new Promise((resolve) => setTimeout(resolve, heartbeatIntervalMs));
  233. continue;
  234. }
  235. const handled = await processNextJob();
  236. if (RUN_ONCE) {
  237. break;
  238. }
  239. if (!handled) {
  240. await new Promise((resolve) => setTimeout(resolve, heartbeatIntervalMs));
  241. }
  242. } while (true);
  243. }
  244. void main().catch((error) => {
  245. console.error(error);
  246. process.exit(1);
  247. });