windows-exe-decompiler-mcp-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/CODEX_INSTALLATION.md +69 -0
  2. package/COPILOT_INSTALLATION.md +77 -0
  3. package/LICENSE +21 -0
  4. package/README.md +314 -0
  5. package/bin/windows-exe-decompiler-mcp-server.js +3 -0
  6. package/dist/analysis-provenance.d.ts +184 -0
  7. package/dist/analysis-provenance.js +74 -0
  8. package/dist/analysis-task-runner.d.ts +31 -0
  9. package/dist/analysis-task-runner.js +160 -0
  10. package/dist/artifact-inventory.d.ts +23 -0
  11. package/dist/artifact-inventory.js +175 -0
  12. package/dist/cache-manager.d.ts +128 -0
  13. package/dist/cache-manager.js +454 -0
  14. package/dist/confidence-semantics.d.ts +66 -0
  15. package/dist/confidence-semantics.js +122 -0
  16. package/dist/config.d.ts +335 -0
  17. package/dist/config.js +193 -0
  18. package/dist/database.d.ts +227 -0
  19. package/dist/database.js +601 -0
  20. package/dist/decompiler-worker.d.ts +441 -0
  21. package/dist/decompiler-worker.js +1962 -0
  22. package/dist/dynamic-trace.d.ts +95 -0
  23. package/dist/dynamic-trace.js +629 -0
  24. package/dist/env-validator.d.ts +15 -0
  25. package/dist/env-validator.js +249 -0
  26. package/dist/error-handler.d.ts +28 -0
  27. package/dist/error-handler.example.d.ts +22 -0
  28. package/dist/error-handler.example.js +141 -0
  29. package/dist/error-handler.js +139 -0
  30. package/dist/ghidra-analysis-status.d.ts +49 -0
  31. package/dist/ghidra-analysis-status.js +178 -0
  32. package/dist/ghidra-config.d.ts +134 -0
  33. package/dist/ghidra-config.js +464 -0
  34. package/dist/index.d.ts +9 -0
  35. package/dist/index.js +200 -0
  36. package/dist/job-queue.d.ts +169 -0
  37. package/dist/job-queue.js +407 -0
  38. package/dist/logger.d.ts +106 -0
  39. package/dist/logger.js +176 -0
  40. package/dist/policy-guard.d.ts +115 -0
  41. package/dist/policy-guard.js +243 -0
  42. package/dist/process-output.d.ts +15 -0
  43. package/dist/process-output.js +90 -0
  44. package/dist/prompts/function-explanation-review.d.ts +5 -0
  45. package/dist/prompts/function-explanation-review.js +64 -0
  46. package/dist/prompts/semantic-name-review.d.ts +5 -0
  47. package/dist/prompts/semantic-name-review.js +63 -0
  48. package/dist/runtime-correlation.d.ts +34 -0
  49. package/dist/runtime-correlation.js +279 -0
  50. package/dist/runtime-paths.d.ts +3 -0
  51. package/dist/runtime-paths.js +11 -0
  52. package/dist/selection-diff.d.ts +667 -0
  53. package/dist/selection-diff.js +53 -0
  54. package/dist/semantic-name-suggestion-artifacts.d.ts +116 -0
  55. package/dist/semantic-name-suggestion-artifacts.js +314 -0
  56. package/dist/server.d.ts +129 -0
  57. package/dist/server.js +578 -0
  58. package/dist/tools/artifact-read.d.ts +235 -0
  59. package/dist/tools/artifact-read.js +317 -0
  60. package/dist/tools/artifacts-diff.d.ts +728 -0
  61. package/dist/tools/artifacts-diff.js +304 -0
  62. package/dist/tools/artifacts-list.d.ts +515 -0
  63. package/dist/tools/artifacts-list.js +389 -0
  64. package/dist/tools/attack-map.d.ts +290 -0
  65. package/dist/tools/attack-map.js +519 -0
  66. package/dist/tools/cache-observability.d.ts +4 -0
  67. package/dist/tools/cache-observability.js +36 -0
  68. package/dist/tools/code-function-cfg.d.ts +50 -0
  69. package/dist/tools/code-function-cfg.js +102 -0
  70. package/dist/tools/code-function-decompile.d.ts +55 -0
  71. package/dist/tools/code-function-decompile.js +103 -0
  72. package/dist/tools/code-function-disassemble.d.ts +43 -0
  73. package/dist/tools/code-function-disassemble.js +185 -0
  74. package/dist/tools/code-function-explain-apply.d.ts +255 -0
  75. package/dist/tools/code-function-explain-apply.js +225 -0
  76. package/dist/tools/code-function-explain-prepare.d.ts +535 -0
  77. package/dist/tools/code-function-explain-prepare.js +276 -0
  78. package/dist/tools/code-function-explain-review.d.ts +397 -0
  79. package/dist/tools/code-function-explain-review.js +589 -0
  80. package/dist/tools/code-function-rename-apply.d.ts +248 -0
  81. package/dist/tools/code-function-rename-apply.js +220 -0
  82. package/dist/tools/code-function-rename-prepare.d.ts +506 -0
  83. package/dist/tools/code-function-rename-prepare.js +279 -0
  84. package/dist/tools/code-function-rename-review.d.ts +574 -0
  85. package/dist/tools/code-function-rename-review.js +761 -0
  86. package/dist/tools/code-functions-list.d.ts +37 -0
  87. package/dist/tools/code-functions-list.js +91 -0
  88. package/dist/tools/code-functions-rank.d.ts +34 -0
  89. package/dist/tools/code-functions-rank.js +90 -0
  90. package/dist/tools/code-functions-reconstruct.d.ts +2725 -0
  91. package/dist/tools/code-functions-reconstruct.js +2807 -0
  92. package/dist/tools/code-functions-search.d.ts +39 -0
  93. package/dist/tools/code-functions-search.js +90 -0
  94. package/dist/tools/code-reconstruct-export.d.ts +1212 -0
  95. package/dist/tools/code-reconstruct-export.js +4002 -0
  96. package/dist/tools/code-reconstruct-plan.d.ts +274 -0
  97. package/dist/tools/code-reconstruct-plan.js +342 -0
  98. package/dist/tools/dotnet-metadata-extract.d.ts +541 -0
  99. package/dist/tools/dotnet-metadata-extract.js +355 -0
  100. package/dist/tools/dotnet-reconstruct-export.d.ts +567 -0
  101. package/dist/tools/dotnet-reconstruct-export.js +1151 -0
  102. package/dist/tools/dotnet-types-list.d.ts +325 -0
  103. package/dist/tools/dotnet-types-list.js +201 -0
  104. package/dist/tools/dynamic-dependencies.d.ts +115 -0
  105. package/dist/tools/dynamic-dependencies.js +213 -0
  106. package/dist/tools/dynamic-memory-import.d.ts +10 -0
  107. package/dist/tools/dynamic-memory-import.js +567 -0
  108. package/dist/tools/dynamic-trace-import.d.ts +10 -0
  109. package/dist/tools/dynamic-trace-import.js +235 -0
  110. package/dist/tools/entrypoint-fallback-disasm.d.ts +30 -0
  111. package/dist/tools/entrypoint-fallback-disasm.js +89 -0
  112. package/dist/tools/ghidra-analyze.d.ts +88 -0
  113. package/dist/tools/ghidra-analyze.js +208 -0
  114. package/dist/tools/ghidra-health.d.ts +37 -0
  115. package/dist/tools/ghidra-health.js +212 -0
  116. package/dist/tools/ioc-export.d.ts +209 -0
  117. package/dist/tools/ioc-export.js +542 -0
  118. package/dist/tools/packer-detect.d.ts +165 -0
  119. package/dist/tools/packer-detect.js +284 -0
  120. package/dist/tools/pe-exports-extract.d.ts +175 -0
  121. package/dist/tools/pe-exports-extract.js +253 -0
  122. package/dist/tools/pe-fingerprint.d.ts +234 -0
  123. package/dist/tools/pe-fingerprint.js +269 -0
  124. package/dist/tools/pe-imports-extract.d.ts +105 -0
  125. package/dist/tools/pe-imports-extract.js +245 -0
  126. package/dist/tools/report-generate.d.ts +157 -0
  127. package/dist/tools/report-generate.js +457 -0
  128. package/dist/tools/report-summarize.d.ts +2131 -0
  129. package/dist/tools/report-summarize.js +596 -0
  130. package/dist/tools/runtime-detect.d.ts +135 -0
  131. package/dist/tools/runtime-detect.js +247 -0
  132. package/dist/tools/sample-ingest.d.ts +94 -0
  133. package/dist/tools/sample-ingest.js +327 -0
  134. package/dist/tools/sample-profile-get.d.ts +183 -0
  135. package/dist/tools/sample-profile-get.js +121 -0
  136. package/dist/tools/sandbox-execute.d.ts +441 -0
  137. package/dist/tools/sandbox-execute.js +392 -0
  138. package/dist/tools/strings-extract.d.ts +375 -0
  139. package/dist/tools/strings-extract.js +314 -0
  140. package/dist/tools/strings-floss-decode.d.ts +143 -0
  141. package/dist/tools/strings-floss-decode.js +259 -0
  142. package/dist/tools/system-health.d.ts +434 -0
  143. package/dist/tools/system-health.js +446 -0
  144. package/dist/tools/task-cancel.d.ts +21 -0
  145. package/dist/tools/task-cancel.js +70 -0
  146. package/dist/tools/task-status.d.ts +27 -0
  147. package/dist/tools/task-status.js +106 -0
  148. package/dist/tools/task-sweep.d.ts +22 -0
  149. package/dist/tools/task-sweep.js +77 -0
  150. package/dist/tools/tool-help.d.ts +340 -0
  151. package/dist/tools/tool-help.js +261 -0
  152. package/dist/tools/yara-scan.d.ts +554 -0
  153. package/dist/tools/yara-scan.js +313 -0
  154. package/dist/types.d.ts +266 -0
  155. package/dist/types.js +41 -0
  156. package/dist/worker-pool.d.ts +204 -0
  157. package/dist/worker-pool.js +650 -0
  158. package/dist/workflows/deep-static.d.ts +104 -0
  159. package/dist/workflows/deep-static.js +276 -0
  160. package/dist/workflows/function-explanation-review.d.ts +655 -0
  161. package/dist/workflows/function-explanation-review.js +440 -0
  162. package/dist/workflows/reconstruct.d.ts +2053 -0
  163. package/dist/workflows/reconstruct.js +666 -0
  164. package/dist/workflows/semantic-name-review.d.ts +2418 -0
  165. package/dist/workflows/semantic-name-review.js +521 -0
  166. package/dist/workflows/triage.d.ts +659 -0
  167. package/dist/workflows/triage.js +1374 -0
  168. package/dist/workspace-manager.d.ts +150 -0
  169. package/dist/workspace-manager.js +411 -0
  170. package/ghidra_scripts/DecompileFunction.java +487 -0
  171. package/ghidra_scripts/DecompileFunction.py +150 -0
  172. package/ghidra_scripts/ExtractCFG.java +256 -0
  173. package/ghidra_scripts/ExtractCFG.py +233 -0
  174. package/ghidra_scripts/ExtractFunctions.java +442 -0
  175. package/ghidra_scripts/ExtractFunctions.py +101 -0
  176. package/ghidra_scripts/README.md +125 -0
  177. package/ghidra_scripts/SearchFunctionReferences.java +380 -0
  178. package/helpers/DotNetMetadataProbe/DotNetMetadataProbe.csproj +9 -0
  179. package/helpers/DotNetMetadataProbe/Program.cs +566 -0
  180. package/install-to-codex.ps1 +178 -0
  181. package/install-to-copilot.ps1 +303 -0
  182. package/package.json +101 -0
  183. package/requirements.txt +9 -0
  184. package/workers/requirements-dynamic.txt +11 -0
  185. package/workers/requirements.txt +8 -0
  186. package/workers/speakeasy_compat.py +175 -0
  187. package/workers/static_worker.py +5183 -0
  188. package/workers/yara_rules/default.yar +33 -0
  189. package/workers/yara_rules/malware_families.yar +93 -0
  190. package/workers/yara_rules/packers.yar +80 -0
@@ -0,0 +1,650 @@
1
+ /**
2
+ * Worker Pool - Manages worker processes for job execution
3
+ *
4
+ * Implements requirements 21.3, 27.2, and operational constraint 4:
5
+ * - Worker process management
6
+ * - Task allocation and status updates
7
+ * - Concurrency control (max 4 concurrent Ghidra analyses)
8
+ */
9
+ import { EventEmitter } from 'events';
10
+ import { logger } from './logger.js';
11
+ /**
12
+ * Default configuration
13
+ */
14
+ const DEFAULT_CONFIG = {
15
+ maxStaticWorkers: 8,
16
+ maxDecompileWorkers: 4, // Operational constraint 4: limit Ghidra concurrency to 4
17
+ maxDotNetWorkers: 4,
18
+ maxSandboxWorkers: 2,
19
+ heartbeatIntervalMs: 5000,
20
+ workerTimeoutMs: 30000
21
+ };
22
+ /**
23
+ * Worker pool that manages worker processes and allocates jobs
24
+ *
25
+ * Features:
26
+ * - Per-worker-type concurrency limits
27
+ * - Automatic worker allocation
28
+ * - Job status tracking
29
+ * - Worker health monitoring
30
+ */
31
+ export class WorkerPool extends EventEmitter {
32
+ workers = new Map();
33
+ config;
34
+ jobQueue;
35
+ heartbeatTimer;
36
+ allocationTimer;
37
+ isRunning = false;
38
+ constructor(jobQueue, config = {}) {
39
+ super();
40
+ this.jobQueue = jobQueue;
41
+ this.config = { ...DEFAULT_CONFIG, ...config };
42
+ }
43
+ /**
44
+ * Start the worker pool
45
+ *
46
+ * Begins monitoring the job queue and allocating jobs to workers
47
+ */
48
+ start() {
49
+ if (this.isRunning) {
50
+ logger.warn('Worker pool already running');
51
+ return;
52
+ }
53
+ this.isRunning = true;
54
+ logger.info({
55
+ maxStaticWorkers: this.config.maxStaticWorkers,
56
+ maxDecompileWorkers: this.config.maxDecompileWorkers,
57
+ maxDotNetWorkers: this.config.maxDotNetWorkers,
58
+ maxSandboxWorkers: this.config.maxSandboxWorkers
59
+ }, 'Starting worker pool');
60
+ // Start job allocation loop
61
+ this.allocationTimer = setInterval(() => {
62
+ this.allocateJobs();
63
+ }, 1000);
64
+ // Start heartbeat monitoring
65
+ this.heartbeatTimer = setInterval(() => {
66
+ this.checkWorkerHealth();
67
+ }, this.config.heartbeatIntervalMs);
68
+ // Listen for job queue events
69
+ this.jobQueue.on('job:enqueued', () => {
70
+ this.allocateJobs();
71
+ });
72
+ this.emit('pool:started');
73
+ }
74
+ /**
75
+ * Stop the worker pool
76
+ *
77
+ * Stops allocating new jobs and waits for running jobs to complete
78
+ */
79
+ async stop() {
80
+ if (!this.isRunning) {
81
+ return;
82
+ }
83
+ this.isRunning = false;
84
+ logger.info('Stopping worker pool');
85
+ // Clear timers
86
+ if (this.allocationTimer) {
87
+ clearInterval(this.allocationTimer);
88
+ this.allocationTimer = undefined;
89
+ }
90
+ if (this.heartbeatTimer) {
91
+ clearInterval(this.heartbeatTimer);
92
+ this.heartbeatTimer = undefined;
93
+ }
94
+ // Clear all timeout timers
95
+ for (const worker of this.workers.values()) {
96
+ if (worker.timeoutTimer) {
97
+ clearTimeout(worker.timeoutTimer);
98
+ worker.timeoutTimer = undefined;
99
+ }
100
+ }
101
+ // Wait for all workers to finish
102
+ await this.waitForWorkers();
103
+ this.emit('pool:stopped');
104
+ }
105
+ /**
106
+ * Allocate jobs from the queue to available workers
107
+ *
108
+ * Requirements: 21.3 - Task allocation
109
+ */
110
+ allocateJobs() {
111
+ if (!this.isRunning) {
112
+ return;
113
+ }
114
+ // Try to allocate jobs while we have capacity
115
+ let allocated = 0;
116
+ while (this.hasCapacity()) {
117
+ const job = this.jobQueue.dequeue();
118
+ if (!job) {
119
+ break; // No more jobs in queue
120
+ }
121
+ const worker = this.allocateWorker(job);
122
+ if (worker) {
123
+ this.executeJob(worker, job);
124
+ allocated++;
125
+ }
126
+ else {
127
+ // No worker available, put job back in queue
128
+ // This shouldn't happen if hasCapacity() is correct
129
+ logger.warn({ jobId: job.id, jobType: job.type }, 'Failed to allocate worker for job');
130
+ break;
131
+ }
132
+ }
133
+ if (allocated > 0) {
134
+ logger.debug(`Allocated ${allocated} jobs to workers`);
135
+ }
136
+ }
137
+ /**
138
+ * Check if we have capacity to run more jobs
139
+ */
140
+ hasCapacity() {
141
+ // Check if any worker type has capacity
142
+ const staticBusy = this.getWorkerCountByType('static', 'busy');
143
+ const decompileBusy = this.getWorkerCountByType('decompile', 'busy');
144
+ const dotnetBusy = this.getWorkerCountByType('dotnet', 'busy');
145
+ const sandboxBusy = this.getWorkerCountByType('sandbox', 'busy');
146
+ if (staticBusy < this.config.maxStaticWorkers)
147
+ return true;
148
+ if (decompileBusy < this.config.maxDecompileWorkers)
149
+ return true;
150
+ if (dotnetBusy < this.config.maxDotNetWorkers)
151
+ return true;
152
+ if (sandboxBusy < this.config.maxSandboxWorkers)
153
+ return true;
154
+ return false;
155
+ }
156
+ /**
157
+ * Allocate a worker for a job
158
+ *
159
+ * @param job - Job to allocate worker for
160
+ * @returns Worker state or undefined if no capacity
161
+ */
162
+ allocateWorker(job) {
163
+ const maxWorkers = this.getMaxWorkers(job.type);
164
+ // Check if we have capacity for this job type
165
+ const busyCount = this.getWorkerCountByType(job.type, 'busy');
166
+ if (busyCount >= maxWorkers) {
167
+ return undefined;
168
+ }
169
+ // Try to find an idle worker of the same type
170
+ for (const worker of this.workers.values()) {
171
+ if (worker.type === job.type && worker.status === 'idle') {
172
+ return worker;
173
+ }
174
+ }
175
+ // Create a new worker if we haven't reached the limit
176
+ const totalCount = this.getWorkerCountByType(job.type, 'idle') +
177
+ this.getWorkerCountByType(job.type, 'busy');
178
+ if (totalCount < maxWorkers) {
179
+ return this.createWorker(job.type);
180
+ }
181
+ return undefined;
182
+ }
183
+ /**
184
+ * Create a new worker
185
+ *
186
+ * @param type - Worker type
187
+ * @returns New worker state
188
+ */
189
+ createWorker(type) {
190
+ const workerId = `${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
191
+ const worker = {
192
+ id: workerId,
193
+ type,
194
+ status: 'idle',
195
+ lastHeartbeat: new Date().toISOString()
196
+ };
197
+ this.workers.set(workerId, worker);
198
+ logger.debug({ workerId, type }, 'Created new worker');
199
+ this.emit('worker:created', workerId, type);
200
+ return worker;
201
+ }
202
+ /**
203
+ * Execute a job on a worker
204
+ *
205
+ * Requirements: 21.3 - Task allocation and status updates
206
+ * Requirements: 21.5, 28.2 - Failure retry mechanism with exponential backoff
207
+ * Requirements: 21.6, 操作约束 5 - Timeout control and worker termination
208
+ *
209
+ * @param worker - Worker to execute job on
210
+ * @param job - Job to execute
211
+ */
212
+ async executeJob(worker, job) {
213
+ worker.status = 'busy';
214
+ worker.currentJob = job;
215
+ worker.startedAt = new Date().toISOString();
216
+ worker.lastHeartbeat = new Date().toISOString();
217
+ logger.info({
218
+ workerId: worker.id,
219
+ jobId: job.id,
220
+ jobType: job.type,
221
+ tool: job.tool,
222
+ attempt: job.attempts + 1,
223
+ timeout: job.timeout
224
+ }, 'Executing job on worker');
225
+ // Set up timeout timer for this job
226
+ // Requirements: 21.6 - Task timeout detection
227
+ worker.timeoutTimer = setTimeout(() => {
228
+ this.handleJobTimeout(worker, job);
229
+ }, job.timeout);
230
+ this.emit('worker:job:started', worker.id, job.id);
231
+ try {
232
+ // Execute the job based on type
233
+ const result = await this.executeJobByType(job);
234
+ // Clear timeout timer if job completed before timeout
235
+ if (worker.timeoutTimer) {
236
+ clearTimeout(worker.timeoutTimer);
237
+ worker.timeoutTimer = undefined;
238
+ }
239
+ // Update job status
240
+ this.jobQueue.complete(job.id, result);
241
+ // Update worker state
242
+ worker.status = 'idle';
243
+ worker.currentJob = undefined;
244
+ worker.startedAt = undefined;
245
+ worker.lastHeartbeat = new Date().toISOString();
246
+ logger.info({
247
+ workerId: worker.id,
248
+ jobId: job.id,
249
+ elapsedMs: result.metrics.elapsedMs
250
+ }, 'Job completed successfully');
251
+ this.emit('worker:job:completed', worker.id, job.id, result);
252
+ // Try to allocate more jobs
253
+ this.allocateJobs();
254
+ }
255
+ catch (error) {
256
+ const errorMessage = error instanceof Error ? error.message : String(error);
257
+ // Clear timeout timer if it exists
258
+ if (worker.timeoutTimer) {
259
+ clearTimeout(worker.timeoutTimer);
260
+ worker.timeoutTimer = undefined;
261
+ }
262
+ logger.error({
263
+ workerId: worker.id,
264
+ jobId: job.id,
265
+ error: errorMessage,
266
+ attempt: job.attempts + 1
267
+ }, 'Job execution failed');
268
+ // Create failure result
269
+ const result = {
270
+ jobId: job.id,
271
+ ok: false,
272
+ errors: [errorMessage],
273
+ warnings: [],
274
+ artifacts: [],
275
+ metrics: {
276
+ elapsedMs: Date.now() - new Date(worker.startedAt).getTime(),
277
+ peakRssMb: 0
278
+ }
279
+ };
280
+ // Update worker state
281
+ worker.status = 'idle';
282
+ worker.currentJob = undefined;
283
+ worker.startedAt = undefined;
284
+ worker.lastHeartbeat = new Date().toISOString();
285
+ // Check if job should be retried
286
+ const shouldRetry = this.shouldRetryJob(job, errorMessage);
287
+ if (shouldRetry) {
288
+ // Retry the job with exponential backoff
289
+ await this.retryJob(job);
290
+ // Emit event with the NEW attempt number (after increment)
291
+ this.emit('worker:job:retrying', worker.id, job.id, job.attempts);
292
+ }
293
+ else {
294
+ // Job failed permanently
295
+ this.jobQueue.complete(job.id, result);
296
+ this.emit('worker:job:failed', worker.id, job.id, error);
297
+ }
298
+ // Try to allocate more jobs
299
+ this.allocateJobs();
300
+ }
301
+ }
302
+ /**
303
+ * Handle job timeout
304
+ *
305
+ * Requirements: 21.6 - Terminate worker process and mark task as failed when timeout occurs
306
+ * Requirements: 操作约束 5 - Enforce configurable timeout limits
307
+ *
308
+ * @param worker - Worker that timed out
309
+ * @param job - Job that timed out
310
+ */
311
+ handleJobTimeout(worker, job) {
312
+ if (!worker.currentJob || worker.currentJob.id !== job.id) {
313
+ // Job already completed or worker is processing a different job
314
+ return;
315
+ }
316
+ const elapsedMs = Date.now() - new Date(worker.startedAt).getTime();
317
+ logger.error({
318
+ workerId: worker.id,
319
+ jobId: job.id,
320
+ jobType: job.type,
321
+ tool: job.tool,
322
+ timeout: job.timeout,
323
+ elapsedMs,
324
+ attempt: job.attempts + 1
325
+ }, 'Job timeout detected');
326
+ // Create timeout error result
327
+ const result = {
328
+ jobId: job.id,
329
+ ok: false,
330
+ errors: [`E_TIMEOUT: Job exceeded timeout of ${job.timeout}ms (elapsed: ${elapsedMs}ms)`],
331
+ warnings: [],
332
+ artifacts: [],
333
+ metrics: {
334
+ elapsedMs,
335
+ peakRssMb: 0
336
+ }
337
+ };
338
+ // Terminate the worker process
339
+ // Requirements: 21.6 - Worker process termination
340
+ this.terminateWorker(worker);
341
+ // Update worker state
342
+ worker.status = 'idle';
343
+ worker.currentJob = undefined;
344
+ worker.startedAt = undefined;
345
+ worker.lastHeartbeat = new Date().toISOString();
346
+ worker.timeoutTimer = undefined;
347
+ // Emit timeout event BEFORE checking retry
348
+ this.emit('worker:job:timeout', worker.id, job.id, elapsedMs);
349
+ // Check if job should be retried (timeout is a retryable error)
350
+ const shouldRetry = this.shouldRetryJob(job, 'E_TIMEOUT');
351
+ if (shouldRetry) {
352
+ // Retry the job with exponential backoff
353
+ this.retryJob(job).then(() => {
354
+ this.emit('worker:job:retrying', worker.id, job.id, job.attempts);
355
+ });
356
+ }
357
+ else {
358
+ // Job failed permanently due to timeout
359
+ this.jobQueue.complete(job.id, result);
360
+ }
361
+ // Try to allocate more jobs
362
+ this.allocateJobs();
363
+ }
364
+ /**
365
+ * Terminate a worker process
366
+ *
367
+ * Requirements: 21.6 - Worker process termination on timeout
368
+ *
369
+ * This method terminates the worker process to free up resources.
370
+ * In a real implementation, this would kill the actual subprocess.
371
+ *
372
+ * @param worker - Worker to terminate
373
+ */
374
+ terminateWorker(worker) {
375
+ logger.warn({
376
+ workerId: worker.id,
377
+ type: worker.type,
378
+ currentJob: worker.currentJob?.id
379
+ }, 'Terminating worker process');
380
+ // In a real implementation, this would:
381
+ // 1. Send SIGTERM to the worker process
382
+ // 2. Wait for graceful shutdown (with timeout)
383
+ // 3. Send SIGKILL if process doesn't terminate
384
+ // 4. Clean up any resources (temp files, sockets, etc.)
385
+ // For now, we just emit an event
386
+ this.emit('worker:terminated', worker.id, worker.type);
387
+ // Clear the timeout timer if it exists
388
+ if (worker.timeoutTimer) {
389
+ clearTimeout(worker.timeoutTimer);
390
+ worker.timeoutTimer = undefined;
391
+ }
392
+ }
393
+ /**
394
+ * Determine if a job should be retried
395
+ *
396
+ * Requirements: 21.5, 28.2 - Retry policy with max 3 attempts
397
+ *
398
+ * @param job - Job that failed
399
+ * @param errorMessage - Error message from failure
400
+ * @returns True if job should be retried
401
+ */
402
+ shouldRetryJob(job, errorMessage) {
403
+ // Check if we've exceeded max retries
404
+ if (job.attempts >= job.retryPolicy.maxRetries) {
405
+ logger.info({
406
+ jobId: job.id,
407
+ attempts: job.attempts,
408
+ maxRetries: job.retryPolicy.maxRetries
409
+ }, 'Job exceeded max retries');
410
+ return false;
411
+ }
412
+ // Check if error is retryable
413
+ const isRetryable = job.retryPolicy.retryableErrors.some(retryableError => errorMessage.includes(retryableError));
414
+ if (!isRetryable) {
415
+ logger.info({
416
+ jobId: job.id,
417
+ error: errorMessage,
418
+ retryableErrors: job.retryPolicy.retryableErrors
419
+ }, 'Job error is not retryable');
420
+ return false;
421
+ }
422
+ return true;
423
+ }
424
+ /**
425
+ * Retry a failed job with exponential backoff
426
+ *
427
+ * Requirements: 21.5, 28.2 - Exponential backoff retry
428
+ *
429
+ * @param job - Job to retry
430
+ */
431
+ async retryJob(job) {
432
+ // Increment attempt counter
433
+ job.attempts++;
434
+ // Calculate exponential backoff delay
435
+ // Formula: backoffMs * (2 ^ (attempts - 1))
436
+ const backoffDelay = job.retryPolicy.backoffMs * Math.pow(2, job.attempts - 1);
437
+ logger.info({
438
+ jobId: job.id,
439
+ attempt: job.attempts,
440
+ backoffMs: backoffDelay,
441
+ maxRetries: job.retryPolicy.maxRetries
442
+ }, 'Retrying job with exponential backoff');
443
+ // Wait for backoff period
444
+ await new Promise(resolve => setTimeout(resolve, backoffDelay));
445
+ // Re-enqueue the job with updated attempt count
446
+ this.jobQueue.requeue(job);
447
+ logger.debug({
448
+ jobId: job.id,
449
+ attempt: job.attempts
450
+ }, 'Job re-enqueued after backoff');
451
+ }
452
+ /**
453
+ * Execute a job based on its type
454
+ *
455
+ * This is a placeholder that will be implemented by specific worker implementations
456
+ *
457
+ * @param job - Job to execute
458
+ * @returns Job result
459
+ */
460
+ async executeJobByType(job) {
461
+ // Use test executor if provided (for testing)
462
+ if (this.config.testExecutor) {
463
+ return this.config.testExecutor(job);
464
+ }
465
+ const startTime = Date.now();
466
+ // TODO: Implement actual worker execution
467
+ // For now, this is a placeholder that simulates work
468
+ logger.debug({
469
+ jobId: job.id,
470
+ type: job.type,
471
+ tool: job.tool
472
+ }, 'Executing job (placeholder)');
473
+ // Simulate work - use longer time for decompile jobs to test concurrency
474
+ const simulationTime = job.type === 'decompile' ? 500 : 100;
475
+ await new Promise(resolve => setTimeout(resolve, simulationTime));
476
+ return {
477
+ jobId: job.id,
478
+ ok: true,
479
+ data: { placeholder: true },
480
+ errors: [],
481
+ warnings: [],
482
+ artifacts: [],
483
+ metrics: {
484
+ elapsedMs: Date.now() - startTime,
485
+ peakRssMb: 0
486
+ }
487
+ };
488
+ }
489
+ /**
490
+ * Check worker health and handle timeouts
491
+ *
492
+ * Requirements: 27.2 - Worker process management
493
+ */
494
+ checkWorkerHealth() {
495
+ const now = Date.now();
496
+ const timeoutMs = this.config.workerTimeoutMs;
497
+ for (const [workerId, worker] of this.workers.entries()) {
498
+ if (worker.status === 'busy' && worker.lastHeartbeat) {
499
+ const lastHeartbeat = new Date(worker.lastHeartbeat).getTime();
500
+ const elapsed = now - lastHeartbeat;
501
+ if (elapsed > timeoutMs) {
502
+ logger.warn({
503
+ workerId,
504
+ jobId: worker.currentJob?.id,
505
+ elapsedMs: elapsed
506
+ }, 'Worker timeout detected');
507
+ // Mark worker as failed
508
+ worker.status = 'failed';
509
+ // Fail the current job
510
+ if (worker.currentJob) {
511
+ const result = {
512
+ jobId: worker.currentJob.id,
513
+ ok: false,
514
+ errors: ['Worker timeout'],
515
+ warnings: [],
516
+ artifacts: [],
517
+ metrics: {
518
+ elapsedMs: elapsed,
519
+ peakRssMb: 0
520
+ }
521
+ };
522
+ this.jobQueue.complete(worker.currentJob.id, result);
523
+ this.emit('worker:timeout', workerId, worker.currentJob.id);
524
+ }
525
+ // Remove failed worker
526
+ this.workers.delete(workerId);
527
+ this.emit('worker:removed', workerId);
528
+ }
529
+ }
530
+ }
531
+ }
532
+ /**
533
+ * Wait for all workers to finish their current jobs
534
+ */
535
+ async waitForWorkers() {
536
+ const busyWorkers = Array.from(this.workers.values()).filter(w => w.status === 'busy');
537
+ if (busyWorkers.length === 0) {
538
+ return;
539
+ }
540
+ logger.info(`Waiting for ${busyWorkers.length} workers to finish`);
541
+ // Wait for all busy workers to become idle
542
+ await Promise.all(busyWorkers.map(worker => new Promise(resolve => {
543
+ const checkInterval = setInterval(() => {
544
+ const currentWorker = this.workers.get(worker.id);
545
+ if (!currentWorker || currentWorker.status !== 'busy') {
546
+ clearInterval(checkInterval);
547
+ resolve();
548
+ }
549
+ }, 100);
550
+ })));
551
+ }
552
+ /**
553
+ * Get worker counts by type and status
554
+ */
555
+ getWorkerCounts() {
556
+ const counts = {
557
+ static: { idle: 0, busy: 0, failed: 0 },
558
+ decompile: { idle: 0, busy: 0, failed: 0 },
559
+ dotnet: { idle: 0, busy: 0, failed: 0 },
560
+ sandbox: { idle: 0, busy: 0, failed: 0 }
561
+ };
562
+ for (const worker of this.workers.values()) {
563
+ counts[worker.type][worker.status]++;
564
+ }
565
+ return counts;
566
+ }
567
+ /**
568
+ * Get count of workers by type and status
569
+ */
570
+ getWorkerCountByType(type, status) {
571
+ let count = 0;
572
+ for (const worker of this.workers.values()) {
573
+ if (worker.type === type && worker.status === status) {
574
+ count++;
575
+ }
576
+ }
577
+ return count;
578
+ }
579
+ /**
580
+ * Get maximum workers for a job type
581
+ */
582
+ getMaxWorkers(type) {
583
+ switch (type) {
584
+ case 'static':
585
+ return this.config.maxStaticWorkers;
586
+ case 'decompile':
587
+ return this.config.maxDecompileWorkers;
588
+ case 'dotnet':
589
+ return this.config.maxDotNetWorkers;
590
+ case 'sandbox':
591
+ return this.config.maxSandboxWorkers;
592
+ default:
593
+ return 1;
594
+ }
595
+ }
596
+ /**
597
+ * Get pool statistics
598
+ */
599
+ getStats() {
600
+ const counts = this.getWorkerCounts();
601
+ return {
602
+ isRunning: this.isRunning,
603
+ queueLength: this.jobQueue.getQueueLength(),
604
+ workers: {
605
+ static: {
606
+ total: counts.static.idle + counts.static.busy + counts.static.failed,
607
+ idle: counts.static.idle,
608
+ busy: counts.static.busy,
609
+ failed: counts.static.failed,
610
+ max: this.config.maxStaticWorkers
611
+ },
612
+ decompile: {
613
+ total: counts.decompile.idle + counts.decompile.busy + counts.decompile.failed,
614
+ idle: counts.decompile.idle,
615
+ busy: counts.decompile.busy,
616
+ failed: counts.decompile.failed,
617
+ max: this.config.maxDecompileWorkers
618
+ },
619
+ dotnet: {
620
+ total: counts.dotnet.idle + counts.dotnet.busy + counts.dotnet.failed,
621
+ idle: counts.dotnet.idle,
622
+ busy: counts.dotnet.busy,
623
+ failed: counts.dotnet.failed,
624
+ max: this.config.maxDotNetWorkers
625
+ },
626
+ sandbox: {
627
+ total: counts.sandbox.idle + counts.sandbox.busy + counts.sandbox.failed,
628
+ idle: counts.sandbox.idle,
629
+ busy: counts.sandbox.busy,
630
+ failed: counts.sandbox.failed,
631
+ max: this.config.maxSandboxWorkers
632
+ }
633
+ }
634
+ };
635
+ }
636
+ /**
637
+ * Update worker heartbeat
638
+ *
639
+ * Should be called by worker implementations to indicate they're still alive
640
+ *
641
+ * @param workerId - Worker identifier
642
+ */
643
+ updateHeartbeat(workerId) {
644
+ const worker = this.workers.get(workerId);
645
+ if (worker) {
646
+ worker.lastHeartbeat = new Date().toISOString();
647
+ }
648
+ }
649
+ }
650
+ //# sourceMappingURL=worker-pool.js.map