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.
- package/CODEX_INSTALLATION.md +69 -0
- package/COPILOT_INSTALLATION.md +77 -0
- package/LICENSE +21 -0
- package/README.md +314 -0
- package/bin/windows-exe-decompiler-mcp-server.js +3 -0
- package/dist/analysis-provenance.d.ts +184 -0
- package/dist/analysis-provenance.js +74 -0
- package/dist/analysis-task-runner.d.ts +31 -0
- package/dist/analysis-task-runner.js +160 -0
- package/dist/artifact-inventory.d.ts +23 -0
- package/dist/artifact-inventory.js +175 -0
- package/dist/cache-manager.d.ts +128 -0
- package/dist/cache-manager.js +454 -0
- package/dist/confidence-semantics.d.ts +66 -0
- package/dist/confidence-semantics.js +122 -0
- package/dist/config.d.ts +335 -0
- package/dist/config.js +193 -0
- package/dist/database.d.ts +227 -0
- package/dist/database.js +601 -0
- package/dist/decompiler-worker.d.ts +441 -0
- package/dist/decompiler-worker.js +1962 -0
- package/dist/dynamic-trace.d.ts +95 -0
- package/dist/dynamic-trace.js +629 -0
- package/dist/env-validator.d.ts +15 -0
- package/dist/env-validator.js +249 -0
- package/dist/error-handler.d.ts +28 -0
- package/dist/error-handler.example.d.ts +22 -0
- package/dist/error-handler.example.js +141 -0
- package/dist/error-handler.js +139 -0
- package/dist/ghidra-analysis-status.d.ts +49 -0
- package/dist/ghidra-analysis-status.js +178 -0
- package/dist/ghidra-config.d.ts +134 -0
- package/dist/ghidra-config.js +464 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +200 -0
- package/dist/job-queue.d.ts +169 -0
- package/dist/job-queue.js +407 -0
- package/dist/logger.d.ts +106 -0
- package/dist/logger.js +176 -0
- package/dist/policy-guard.d.ts +115 -0
- package/dist/policy-guard.js +243 -0
- package/dist/process-output.d.ts +15 -0
- package/dist/process-output.js +90 -0
- package/dist/prompts/function-explanation-review.d.ts +5 -0
- package/dist/prompts/function-explanation-review.js +64 -0
- package/dist/prompts/semantic-name-review.d.ts +5 -0
- package/dist/prompts/semantic-name-review.js +63 -0
- package/dist/runtime-correlation.d.ts +34 -0
- package/dist/runtime-correlation.js +279 -0
- package/dist/runtime-paths.d.ts +3 -0
- package/dist/runtime-paths.js +11 -0
- package/dist/selection-diff.d.ts +667 -0
- package/dist/selection-diff.js +53 -0
- package/dist/semantic-name-suggestion-artifacts.d.ts +116 -0
- package/dist/semantic-name-suggestion-artifacts.js +314 -0
- package/dist/server.d.ts +129 -0
- package/dist/server.js +578 -0
- package/dist/tools/artifact-read.d.ts +235 -0
- package/dist/tools/artifact-read.js +317 -0
- package/dist/tools/artifacts-diff.d.ts +728 -0
- package/dist/tools/artifacts-diff.js +304 -0
- package/dist/tools/artifacts-list.d.ts +515 -0
- package/dist/tools/artifacts-list.js +389 -0
- package/dist/tools/attack-map.d.ts +290 -0
- package/dist/tools/attack-map.js +519 -0
- package/dist/tools/cache-observability.d.ts +4 -0
- package/dist/tools/cache-observability.js +36 -0
- package/dist/tools/code-function-cfg.d.ts +50 -0
- package/dist/tools/code-function-cfg.js +102 -0
- package/dist/tools/code-function-decompile.d.ts +55 -0
- package/dist/tools/code-function-decompile.js +103 -0
- package/dist/tools/code-function-disassemble.d.ts +43 -0
- package/dist/tools/code-function-disassemble.js +185 -0
- package/dist/tools/code-function-explain-apply.d.ts +255 -0
- package/dist/tools/code-function-explain-apply.js +225 -0
- package/dist/tools/code-function-explain-prepare.d.ts +535 -0
- package/dist/tools/code-function-explain-prepare.js +276 -0
- package/dist/tools/code-function-explain-review.d.ts +397 -0
- package/dist/tools/code-function-explain-review.js +589 -0
- package/dist/tools/code-function-rename-apply.d.ts +248 -0
- package/dist/tools/code-function-rename-apply.js +220 -0
- package/dist/tools/code-function-rename-prepare.d.ts +506 -0
- package/dist/tools/code-function-rename-prepare.js +279 -0
- package/dist/tools/code-function-rename-review.d.ts +574 -0
- package/dist/tools/code-function-rename-review.js +761 -0
- package/dist/tools/code-functions-list.d.ts +37 -0
- package/dist/tools/code-functions-list.js +91 -0
- package/dist/tools/code-functions-rank.d.ts +34 -0
- package/dist/tools/code-functions-rank.js +90 -0
- package/dist/tools/code-functions-reconstruct.d.ts +2725 -0
- package/dist/tools/code-functions-reconstruct.js +2807 -0
- package/dist/tools/code-functions-search.d.ts +39 -0
- package/dist/tools/code-functions-search.js +90 -0
- package/dist/tools/code-reconstruct-export.d.ts +1212 -0
- package/dist/tools/code-reconstruct-export.js +4002 -0
- package/dist/tools/code-reconstruct-plan.d.ts +274 -0
- package/dist/tools/code-reconstruct-plan.js +342 -0
- package/dist/tools/dotnet-metadata-extract.d.ts +541 -0
- package/dist/tools/dotnet-metadata-extract.js +355 -0
- package/dist/tools/dotnet-reconstruct-export.d.ts +567 -0
- package/dist/tools/dotnet-reconstruct-export.js +1151 -0
- package/dist/tools/dotnet-types-list.d.ts +325 -0
- package/dist/tools/dotnet-types-list.js +201 -0
- package/dist/tools/dynamic-dependencies.d.ts +115 -0
- package/dist/tools/dynamic-dependencies.js +213 -0
- package/dist/tools/dynamic-memory-import.d.ts +10 -0
- package/dist/tools/dynamic-memory-import.js +567 -0
- package/dist/tools/dynamic-trace-import.d.ts +10 -0
- package/dist/tools/dynamic-trace-import.js +235 -0
- package/dist/tools/entrypoint-fallback-disasm.d.ts +30 -0
- package/dist/tools/entrypoint-fallback-disasm.js +89 -0
- package/dist/tools/ghidra-analyze.d.ts +88 -0
- package/dist/tools/ghidra-analyze.js +208 -0
- package/dist/tools/ghidra-health.d.ts +37 -0
- package/dist/tools/ghidra-health.js +212 -0
- package/dist/tools/ioc-export.d.ts +209 -0
- package/dist/tools/ioc-export.js +542 -0
- package/dist/tools/packer-detect.d.ts +165 -0
- package/dist/tools/packer-detect.js +284 -0
- package/dist/tools/pe-exports-extract.d.ts +175 -0
- package/dist/tools/pe-exports-extract.js +253 -0
- package/dist/tools/pe-fingerprint.d.ts +234 -0
- package/dist/tools/pe-fingerprint.js +269 -0
- package/dist/tools/pe-imports-extract.d.ts +105 -0
- package/dist/tools/pe-imports-extract.js +245 -0
- package/dist/tools/report-generate.d.ts +157 -0
- package/dist/tools/report-generate.js +457 -0
- package/dist/tools/report-summarize.d.ts +2131 -0
- package/dist/tools/report-summarize.js +596 -0
- package/dist/tools/runtime-detect.d.ts +135 -0
- package/dist/tools/runtime-detect.js +247 -0
- package/dist/tools/sample-ingest.d.ts +94 -0
- package/dist/tools/sample-ingest.js +327 -0
- package/dist/tools/sample-profile-get.d.ts +183 -0
- package/dist/tools/sample-profile-get.js +121 -0
- package/dist/tools/sandbox-execute.d.ts +441 -0
- package/dist/tools/sandbox-execute.js +392 -0
- package/dist/tools/strings-extract.d.ts +375 -0
- package/dist/tools/strings-extract.js +314 -0
- package/dist/tools/strings-floss-decode.d.ts +143 -0
- package/dist/tools/strings-floss-decode.js +259 -0
- package/dist/tools/system-health.d.ts +434 -0
- package/dist/tools/system-health.js +446 -0
- package/dist/tools/task-cancel.d.ts +21 -0
- package/dist/tools/task-cancel.js +70 -0
- package/dist/tools/task-status.d.ts +27 -0
- package/dist/tools/task-status.js +106 -0
- package/dist/tools/task-sweep.d.ts +22 -0
- package/dist/tools/task-sweep.js +77 -0
- package/dist/tools/tool-help.d.ts +340 -0
- package/dist/tools/tool-help.js +261 -0
- package/dist/tools/yara-scan.d.ts +554 -0
- package/dist/tools/yara-scan.js +313 -0
- package/dist/types.d.ts +266 -0
- package/dist/types.js +41 -0
- package/dist/worker-pool.d.ts +204 -0
- package/dist/worker-pool.js +650 -0
- package/dist/workflows/deep-static.d.ts +104 -0
- package/dist/workflows/deep-static.js +276 -0
- package/dist/workflows/function-explanation-review.d.ts +655 -0
- package/dist/workflows/function-explanation-review.js +440 -0
- package/dist/workflows/reconstruct.d.ts +2053 -0
- package/dist/workflows/reconstruct.js +666 -0
- package/dist/workflows/semantic-name-review.d.ts +2418 -0
- package/dist/workflows/semantic-name-review.js +521 -0
- package/dist/workflows/triage.d.ts +659 -0
- package/dist/workflows/triage.js +1374 -0
- package/dist/workspace-manager.d.ts +150 -0
- package/dist/workspace-manager.js +411 -0
- package/ghidra_scripts/DecompileFunction.java +487 -0
- package/ghidra_scripts/DecompileFunction.py +150 -0
- package/ghidra_scripts/ExtractCFG.java +256 -0
- package/ghidra_scripts/ExtractCFG.py +233 -0
- package/ghidra_scripts/ExtractFunctions.java +442 -0
- package/ghidra_scripts/ExtractFunctions.py +101 -0
- package/ghidra_scripts/README.md +125 -0
- package/ghidra_scripts/SearchFunctionReferences.java +380 -0
- package/helpers/DotNetMetadataProbe/DotNetMetadataProbe.csproj +9 -0
- package/helpers/DotNetMetadataProbe/Program.cs +566 -0
- package/install-to-codex.ps1 +178 -0
- package/install-to-copilot.ps1 +303 -0
- package/package.json +101 -0
- package/requirements.txt +9 -0
- package/workers/requirements-dynamic.txt +11 -0
- package/workers/requirements.txt +8 -0
- package/workers/speakeasy_compat.py +175 -0
- package/workers/static_worker.py +5183 -0
- package/workers/yara_rules/default.yar +33 -0
- package/workers/yara_rules/malware_families.yar +93 -0
- 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
|