cheatengine 5.8.13 → 5.8.15
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/bin/cheatengine +30 -40
- package/ce_mcp_server.js +367 -0
- package/package.json +9 -7
- package/src/base.js +235 -0
- package/src/pipe-client.js +316 -0
- package/src/tool-registry.js +887 -0
- package/ce_mcp_server.py +0 -2509
package/src/base.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// -*- coding: utf-8 -*-
|
|
3
|
+
/**
|
|
4
|
+
* Cheat Engine MCP Server - Named pipe bridge to Cheat Engine
|
|
5
|
+
* JavaScript/Node.js version
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const net = require('net');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { spawn, execSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
// ============ Configuration ============
|
|
14
|
+
const Config = {
|
|
15
|
+
PIPE_NAME: `\\\\.\\pipe\\${process.env.CE_MCP_PIPE_NAME || 'ce_mcp_bridge'}`,
|
|
16
|
+
AUTH_TOKEN: process.env.CE_MCP_AUTH_TOKEN || null,
|
|
17
|
+
MAX_RETRIES: 3,
|
|
18
|
+
CHUNK_SIZE: 1024 * 1024,
|
|
19
|
+
MAX_RESPONSE_SIZE: 10 * 1024 * 1024,
|
|
20
|
+
DEBUG: process.env.DEBUG === '1' || false,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// ============ Timeout Error ============
|
|
24
|
+
class TimeoutError extends Error {
|
|
25
|
+
constructor(toolName, params, elapsedTime, timeout) {
|
|
26
|
+
super(`Tool '${toolName}' timed out after ${elapsedTime.toFixed(2)}s (timeout: ${timeout}s)`);
|
|
27
|
+
this.toolName = toolName;
|
|
28
|
+
this.params = params;
|
|
29
|
+
this.elapsedTime = elapsedTime;
|
|
30
|
+
this.timeout = timeout;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============ Logger ============
|
|
35
|
+
class Logger {
|
|
36
|
+
static log(msg, level = 'INFO') {
|
|
37
|
+
if (level === 'DEBUG' && !Config.DEBUG) return;
|
|
38
|
+
process.stderr.write(`[CheatEngine-MCP-${level}] ${msg}\n`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static info(msg) {
|
|
42
|
+
Logger.log(msg, 'INFO');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static debug(msg) {
|
|
46
|
+
Logger.log(msg, 'DEBUG');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static error(msg) {
|
|
50
|
+
Logger.log(msg, 'ERROR');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const log = Logger;
|
|
55
|
+
|
|
56
|
+
// ============ Connection Health Monitoring ============
|
|
57
|
+
class ConnectionHealth {
|
|
58
|
+
constructor() {
|
|
59
|
+
this.lastSuccessTime = 0.0;
|
|
60
|
+
this.connectionAttempts = 0;
|
|
61
|
+
this.consecutiveErrors = 0;
|
|
62
|
+
this.totalErrors = 0;
|
|
63
|
+
this.lastError = null;
|
|
64
|
+
this.isConnected = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class HealthMonitor {
|
|
69
|
+
constructor(errorThreshold = 5) {
|
|
70
|
+
this.health = new ConnectionHealth();
|
|
71
|
+
this.errorThreshold = errorThreshold;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
recordSuccess() {
|
|
75
|
+
this.health.lastSuccessTime = Date.now() / 1000;
|
|
76
|
+
this.health.consecutiveErrors = 0;
|
|
77
|
+
this.health.isConnected = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
recordError(error) {
|
|
81
|
+
this.health.consecutiveErrors++;
|
|
82
|
+
this.health.totalErrors++;
|
|
83
|
+
this.health.lastError = error;
|
|
84
|
+
this.health.isConnected = false;
|
|
85
|
+
return this.health.consecutiveErrors >= this.errorThreshold;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
recordConnectionAttempt() {
|
|
89
|
+
this.health.connectionAttempts++;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getMetrics() {
|
|
93
|
+
return {
|
|
94
|
+
last_success_time: this.health.lastSuccessTime,
|
|
95
|
+
connection_attempts: this.health.connectionAttempts,
|
|
96
|
+
consecutive_errors: this.health.consecutiveErrors,
|
|
97
|
+
total_errors: this.health.totalErrors,
|
|
98
|
+
last_error: this.health.lastError,
|
|
99
|
+
is_connected: this.health.isConnected,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
reset() {
|
|
104
|
+
this.health = new ConnectionHealth();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============ Timeout Manager ============
|
|
109
|
+
class TimeoutManager {
|
|
110
|
+
static DEFAULT_TIMEOUT = 30;
|
|
111
|
+
|
|
112
|
+
static TOOL_TIMEOUTS = {
|
|
113
|
+
ce_find_pointer_path: 60,
|
|
114
|
+
ce_break_and_trace: 60,
|
|
115
|
+
ce_find_what_accesses: 30,
|
|
116
|
+
ce_find_what_writes: 30,
|
|
117
|
+
ce_aob_scan: 30,
|
|
118
|
+
ce_value_scan: 30,
|
|
119
|
+
ce_scan_new: 30,
|
|
120
|
+
ce_scan_next: 30,
|
|
121
|
+
ce_build_cfg: 45,
|
|
122
|
+
ce_symbolic_trace: 45,
|
|
123
|
+
ce_call_function: 30,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
constructor(defaultTimeout = null, toolTimeouts = null) {
|
|
127
|
+
this.defaultTimeout = defaultTimeout !== null ? defaultTimeout : TimeoutManager.DEFAULT_TIMEOUT;
|
|
128
|
+
this.toolTimeouts = { ...TimeoutManager.TOOL_TIMEOUTS };
|
|
129
|
+
if (toolTimeouts) {
|
|
130
|
+
Object.assign(this.toolTimeouts, toolTimeouts);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getTimeout(toolName) {
|
|
135
|
+
return this.toolTimeouts[toolName] || this.defaultTimeout;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setTimeout(toolName, timeout) {
|
|
139
|
+
this.toolTimeouts[toolName] = timeout;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getAllTimeouts() {
|
|
143
|
+
return {
|
|
144
|
+
default_timeout: this.defaultTimeout,
|
|
145
|
+
tool_timeouts: { ...this.toolTimeouts },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ============ Metrics Collector ============
|
|
151
|
+
class ToolMetrics {
|
|
152
|
+
constructor() {
|
|
153
|
+
this.callCount = 0;
|
|
154
|
+
this.totalTime = 0.0;
|
|
155
|
+
this.errorCount = 0;
|
|
156
|
+
this.lastCallTime = 0.0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
class MetricsCollector {
|
|
161
|
+
constructor() {
|
|
162
|
+
this.toolMetrics = new Map();
|
|
163
|
+
this.startTime = Date.now() / 1000;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
recordCall(toolName, duration, isError = false) {
|
|
167
|
+
if (!this.toolMetrics.has(toolName)) {
|
|
168
|
+
this.toolMetrics.set(toolName, new ToolMetrics());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const metrics = this.toolMetrics.get(toolName);
|
|
172
|
+
metrics.callCount++;
|
|
173
|
+
metrics.totalTime += duration;
|
|
174
|
+
metrics.lastCallTime = Date.now() / 1000;
|
|
175
|
+
if (isError) {
|
|
176
|
+
metrics.errorCount++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getToolMetrics(toolName) {
|
|
181
|
+
const metrics = this.toolMetrics.get(toolName);
|
|
182
|
+
if (!metrics) return null;
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
calls: metrics.callCount,
|
|
186
|
+
total_time: metrics.totalTime,
|
|
187
|
+
avg_time: metrics.callCount > 0 ? metrics.totalTime / metrics.callCount : 0,
|
|
188
|
+
errors: metrics.errorCount,
|
|
189
|
+
error_rate: metrics.callCount > 0 ? metrics.errorCount / metrics.callCount : 0,
|
|
190
|
+
last_call_time: metrics.lastCallTime,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getSummary() {
|
|
195
|
+
let totalCalls = 0;
|
|
196
|
+
let totalErrors = 0;
|
|
197
|
+
const tools = {};
|
|
198
|
+
|
|
199
|
+
for (const [name, metrics] of this.toolMetrics) {
|
|
200
|
+
totalCalls += metrics.callCount;
|
|
201
|
+
totalErrors += metrics.errorCount;
|
|
202
|
+
tools[name] = {
|
|
203
|
+
calls: metrics.callCount,
|
|
204
|
+
avg_time: metrics.callCount > 0 ? metrics.totalTime / metrics.callCount : 0,
|
|
205
|
+
errors: metrics.errorCount,
|
|
206
|
+
error_rate: metrics.callCount > 0 ? metrics.errorCount / metrics.callCount : 0,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
uptime: Date.now() / 1000 - this.startTime,
|
|
212
|
+
total_calls: totalCalls,
|
|
213
|
+
total_errors: totalErrors,
|
|
214
|
+
error_rate: totalCalls > 0 ? totalErrors / totalCalls : 0,
|
|
215
|
+
tools,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
reset() {
|
|
220
|
+
this.toolMetrics.clear();
|
|
221
|
+
this.startTime = Date.now() / 1000;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
Config,
|
|
227
|
+
TimeoutError,
|
|
228
|
+
ConnectionHealth,
|
|
229
|
+
HealthMonitor,
|
|
230
|
+
TimeoutManager,
|
|
231
|
+
ToolMetrics,
|
|
232
|
+
MetricsCollector,
|
|
233
|
+
Logger,
|
|
234
|
+
log,
|
|
235
|
+
};
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows Named Pipe Client for Cheat Engine MCP Bridge
|
|
3
|
+
* Uses Node.js net module to connect to Windows named pipes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const net = require('net');
|
|
7
|
+
const { EventEmitter } = require('events');
|
|
8
|
+
const { Config, HealthMonitor, Logger, log } = require('./base');
|
|
9
|
+
|
|
10
|
+
// Windows error codes
|
|
11
|
+
const ERROR_FILE_NOT_FOUND = 2;
|
|
12
|
+
const ERROR_PIPE_BUSY = 231;
|
|
13
|
+
const ERROR_ACCESS_DENIED = 5;
|
|
14
|
+
const ERROR_BROKEN_PIPE = 109;
|
|
15
|
+
const ERROR_PIPE_NOT_CONNECTED = 233;
|
|
16
|
+
|
|
17
|
+
class PipeClient extends EventEmitter {
|
|
18
|
+
constructor(pipeName = Config.PIPE_NAME, errorThreshold = 5) {
|
|
19
|
+
super();
|
|
20
|
+
this.pipeName = pipeName;
|
|
21
|
+
this.socket = null;
|
|
22
|
+
this.healthMonitor = new HealthMonitor(errorThreshold);
|
|
23
|
+
this.reconnectTimer = null;
|
|
24
|
+
this.stopReconnectFlag = false;
|
|
25
|
+
this.connected = false;
|
|
26
|
+
this.connectionAttempts = 0;
|
|
27
|
+
this.lastError = null;
|
|
28
|
+
this.lastConnectErrorCode = null;
|
|
29
|
+
this.lastSuccessTime = 0;
|
|
30
|
+
this.responseBuffer = Buffer.alloc(0);
|
|
31
|
+
this.pendingResponse = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
isValid() {
|
|
35
|
+
return this.socket !== null && this.connected && !this.socket.destroyed;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async _tryConnectOnce() {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
try {
|
|
41
|
+
const socket = net.createConnection(this.pipeName, () => {
|
|
42
|
+
this.socket = socket;
|
|
43
|
+
this.connected = true;
|
|
44
|
+
this.lastError = null;
|
|
45
|
+
this.lastConnectErrorCode = null;
|
|
46
|
+
this.lastSuccessTime = Date.now() / 1000;
|
|
47
|
+
log.info('Connected to Cheat Engine Pipe');
|
|
48
|
+
this.healthMonitor.recordSuccess();
|
|
49
|
+
resolve(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
socket.on('error', (err) => {
|
|
53
|
+
this.lastConnectErrorCode = err.code;
|
|
54
|
+
this.lastError = err.message;
|
|
55
|
+
|
|
56
|
+
// Handle specific error codes
|
|
57
|
+
if (err.code === 'ENOENT') {
|
|
58
|
+
this.lastConnectErrorCode = ERROR_FILE_NOT_FOUND;
|
|
59
|
+
} else if (err.code === 'EACCES') {
|
|
60
|
+
this.lastConnectErrorCode = ERROR_ACCESS_DENIED;
|
|
61
|
+
} else if (err.message && err.message.includes('EBUSY')) {
|
|
62
|
+
this.lastConnectErrorCode = ERROR_PIPE_BUSY;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
resolve(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
socket.on('close', () => {
|
|
69
|
+
this.connected = false;
|
|
70
|
+
if (this.socket) {
|
|
71
|
+
this.socket = null;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
socket.on('data', (data) => {
|
|
76
|
+
this._handleData(data);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
} catch (err) {
|
|
80
|
+
this.lastError = err.message;
|
|
81
|
+
resolve(false);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_handleData(data) {
|
|
87
|
+
this.responseBuffer = Buffer.concat([this.responseBuffer, data]);
|
|
88
|
+
|
|
89
|
+
// Try to parse response
|
|
90
|
+
if (this.pendingResponse && this.responseBuffer.length >= 4) {
|
|
91
|
+
const respLen = this.responseBuffer.readUInt32LE(0);
|
|
92
|
+
|
|
93
|
+
if (respLen === 0 || respLen > Config.MAX_RESPONSE_SIZE) {
|
|
94
|
+
this.pendingResponse.reject(new Error(`Invalid response size: ${respLen}`));
|
|
95
|
+
this.pendingResponse = null;
|
|
96
|
+
this.responseBuffer = Buffer.alloc(0);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (this.responseBuffer.length >= 4 + respLen) {
|
|
101
|
+
const respData = this.responseBuffer.slice(4, 4 + respLen);
|
|
102
|
+
this.responseBuffer = this.responseBuffer.slice(4 + respLen);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const decoded = this._decodeResponse(respData);
|
|
106
|
+
const result = JSON.parse(decoded);
|
|
107
|
+
this.pendingResponse.resolve(result);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
this.pendingResponse.reject(err);
|
|
110
|
+
}
|
|
111
|
+
this.pendingResponse = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_decodeResponse(data) {
|
|
117
|
+
// Try multiple encodings
|
|
118
|
+
const encodings = ['utf-8', 'latin1'];
|
|
119
|
+
for (const encoding of encodings) {
|
|
120
|
+
try {
|
|
121
|
+
return data.toString(encoding);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return data.toString('utf-8');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async _reconnectLoop() {
|
|
130
|
+
let backoff = 0.5;
|
|
131
|
+
const maxBackoff = 10.0;
|
|
132
|
+
|
|
133
|
+
while (!this.stopReconnectFlag) {
|
|
134
|
+
if (!this.isValid()) {
|
|
135
|
+
this.connectionAttempts++;
|
|
136
|
+
this.healthMonitor.recordConnectionAttempt();
|
|
137
|
+
|
|
138
|
+
if (await this._tryConnectOnce()) {
|
|
139
|
+
backoff = 0.5;
|
|
140
|
+
} else {
|
|
141
|
+
backoff = Math.min(backoff * 1.5, maxBackoff);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
backoff = 0.5;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await this._sleep(backoff * 1000);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
_sleep(ms) {
|
|
152
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
startBackgroundReconnect() {
|
|
156
|
+
if (this.reconnectTimer) return;
|
|
157
|
+
this.stopReconnectFlag = false;
|
|
158
|
+
this._reconnectLoop();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async connect(force = false, timeout = 3000) {
|
|
162
|
+
if (!force && this.isValid()) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Try immediate connection
|
|
167
|
+
this.connectionAttempts++;
|
|
168
|
+
this.healthMonitor.recordConnectionAttempt();
|
|
169
|
+
if (await this._tryConnectOnce()) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Start background reconnect and wait
|
|
174
|
+
this.startBackgroundReconnect();
|
|
175
|
+
|
|
176
|
+
// Wait for connection with timeout
|
|
177
|
+
const startTime = Date.now();
|
|
178
|
+
while (Date.now() - startTime < timeout) {
|
|
179
|
+
if (this.isValid()) return true;
|
|
180
|
+
await this._sleep(100);
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_close() {
|
|
186
|
+
this.connected = false;
|
|
187
|
+
if (this.socket) {
|
|
188
|
+
try {
|
|
189
|
+
this.socket.destroy();
|
|
190
|
+
} catch (err) {
|
|
191
|
+
// Ignore
|
|
192
|
+
}
|
|
193
|
+
this.socket = null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
_getConnectionErrorMessage(winerrorCode, attempt) {
|
|
198
|
+
const messages = {
|
|
199
|
+
[ERROR_FILE_NOT_FOUND]: (
|
|
200
|
+
"Cheat Engine MCP Bridge not running. " +
|
|
201
|
+
"Load 'ce_mcp_bridge.lua' in CE (Table -> Show Cheat Table Lua Script -> Execute)"
|
|
202
|
+
),
|
|
203
|
+
[ERROR_PIPE_BUSY]: "Pipe busy - another client may be connected",
|
|
204
|
+
[ERROR_ACCESS_DENIED]: "Access denied - try running as administrator",
|
|
205
|
+
[ERROR_BROKEN_PIPE]: "Connection lost - restart bridge script in CE",
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const baseMsg = messages[winerrorCode] || `Connection failed (error ${winerrorCode})`;
|
|
209
|
+
return `${baseMsg} [attempt ${attempt}/${Config.MAX_RETRIES}]`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async _doSendReceive(data) {
|
|
213
|
+
// Add authentication token if configured
|
|
214
|
+
if (Config.AUTH_TOKEN) {
|
|
215
|
+
data = { ...data, auth_token: Config.AUTH_TOKEN };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (let retry = 0; retry < Config.MAX_RETRIES; retry++) {
|
|
219
|
+
const connected = await this.connect(retry > 0);
|
|
220
|
+
if (!connected) {
|
|
221
|
+
if (this.lastConnectErrorCode) {
|
|
222
|
+
return {
|
|
223
|
+
error: this._getConnectionErrorMessage(this.lastConnectErrorCode, retry + 1)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
error: `Cannot connect to CE (attempt ${retry + 1}/${Config.MAX_RETRIES})`
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const jsonStr = JSON.stringify(data);
|
|
233
|
+
const jsonBytes = Buffer.from(jsonStr, 'utf-8');
|
|
234
|
+
const lenBuffer = Buffer.alloc(4);
|
|
235
|
+
lenBuffer.writeUInt32LE(jsonBytes.length, 0);
|
|
236
|
+
|
|
237
|
+
// Send request
|
|
238
|
+
this.socket.write(lenBuffer);
|
|
239
|
+
this.socket.write(jsonBytes);
|
|
240
|
+
|
|
241
|
+
// Wait for response
|
|
242
|
+
const result = await this._waitForResponse();
|
|
243
|
+
this.lastSuccessTime = Date.now() / 1000;
|
|
244
|
+
this.healthMonitor.recordSuccess();
|
|
245
|
+
return result;
|
|
246
|
+
|
|
247
|
+
} catch (err) {
|
|
248
|
+
log.debug(`Pipe error (attempt ${retry + 1}): ${err.message}`);
|
|
249
|
+
this.healthMonitor.recordError(err.message);
|
|
250
|
+
this._close();
|
|
251
|
+
await this._sleep(100 * (retry + 1));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { error: this.lastError || "Connection failed after retries" };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
_waitForResponse() {
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
this.pendingResponse = { resolve, reject };
|
|
261
|
+
this.responseBuffer = Buffer.alloc(0);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async sendReceive(data, timeout = 30000, toolName = null) {
|
|
266
|
+
const timeoutSeconds = timeout / 1000.0;
|
|
267
|
+
const startTime = Date.now();
|
|
268
|
+
|
|
269
|
+
// Create timeout promise
|
|
270
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
271
|
+
setTimeout(() => {
|
|
272
|
+
const elapsedTime = (Date.now() - startTime) / 1000;
|
|
273
|
+
const errorMsg = `Tool execution timed out after ${elapsedTime.toFixed(2)}s (timeout: ${timeoutSeconds}s)`;
|
|
274
|
+
log.error(`Timeout: tool=${toolName || 'unknown'}, params=${JSON.stringify(data.params || {})}, elapsed=${elapsedTime.toFixed(2)}s`);
|
|
275
|
+
this.healthMonitor.recordError(errorMsg);
|
|
276
|
+
reject(new TimeoutError(toolName || data.command || 'unknown', data.params || {}, elapsedTime, timeoutSeconds));
|
|
277
|
+
}, timeout);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const result = await Promise.race([
|
|
282
|
+
this._doSendReceive(data),
|
|
283
|
+
timeoutPromise
|
|
284
|
+
]);
|
|
285
|
+
return result;
|
|
286
|
+
} catch (err) {
|
|
287
|
+
if (err instanceof TimeoutError) {
|
|
288
|
+
return {
|
|
289
|
+
error: err.message,
|
|
290
|
+
timeout_info: {
|
|
291
|
+
tool_name: toolName || data.command || 'unknown',
|
|
292
|
+
params: data.params || {},
|
|
293
|
+
elapsed_time: err.elapsedTime,
|
|
294
|
+
timeout_seconds: err.timeout,
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return { error: err.message };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
getHealthMetrics() {
|
|
303
|
+
return this.healthMonitor.getMetrics();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
stop() {
|
|
307
|
+
this.stopReconnectFlag = true;
|
|
308
|
+
if (this.reconnectTimer) {
|
|
309
|
+
clearTimeout(this.reconnectTimer);
|
|
310
|
+
this.reconnectTimer = null;
|
|
311
|
+
}
|
|
312
|
+
this._close();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
module.exports = { PipeClient };
|