@codesys/mcp-toolkit 1.2.1 → 1.2.4
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/CHANGELOG.md +16 -0
- package/dist/codesys_interop.js +143 -2
- package/dist/scriptLoader.js +14 -2
- package/dist/scripts/create_project.py +51 -5
- package/dist/scripts/session_status.py +168 -0
- package/dist/scripts/start_session.py +240 -0
- package/dist/scripts/stop_session.py +94 -0
- package/dist/sessionManager.js +399 -117
- package/docs/session_management.md +6 -1
- package/package.json +1 -1
package/dist/sessionManager.js
CHANGED
|
@@ -58,9 +58,19 @@ const os = __importStar(require("os"));
|
|
|
58
58
|
const fs = __importStar(require("fs"));
|
|
59
59
|
const events_1 = require("events");
|
|
60
60
|
const uuid_1 = require("uuid");
|
|
61
|
+
const scriptLoader_1 = require("./scriptLoader");
|
|
61
62
|
// Default timeout values
|
|
62
63
|
const DEFAULT_SCRIPT_TIMEOUT = 60000; // 60 seconds
|
|
63
64
|
const DEFAULT_SESSION_IDLE_TIMEOUT = 300000; // 5 minutes
|
|
65
|
+
const DEFAULT_SESSION_POLL_INTERVAL = 1000; // 1 second
|
|
66
|
+
// Session directory and pipe names
|
|
67
|
+
const SESSION_DIR = path.join(os.tmpdir(), 'codesys_session');
|
|
68
|
+
const COMMAND_PIPE = 'command_pipe';
|
|
69
|
+
const RESULT_PIPE = 'result_pipe';
|
|
70
|
+
const STATE_FILE = 'session_state.json';
|
|
71
|
+
// Define expected success/error markers from the Python scripts
|
|
72
|
+
const SCRIPT_SUCCESS_MARKER = 'SCRIPT_SUCCESS';
|
|
73
|
+
const SCRIPT_ERROR_MARKER = 'SCRIPT_ERROR';
|
|
64
74
|
// Event names
|
|
65
75
|
exports.SESSION_EVENTS = {
|
|
66
76
|
STARTED: 'session:started',
|
|
@@ -109,6 +119,12 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
109
119
|
this.codesysExePath = codesysExePath;
|
|
110
120
|
this.codesysProfileName = codesysProfileName;
|
|
111
121
|
this.idleTimeoutMs = idleTimeoutMs;
|
|
122
|
+
this.sessionId = (0, uuid_1.v4)();
|
|
123
|
+
// Set up session directory and pipes
|
|
124
|
+
this.sessionDir = path.join(SESSION_DIR, this.sessionId);
|
|
125
|
+
this.commandPipePath = path.join(this.sessionDir, COMMAND_PIPE);
|
|
126
|
+
this.resultPipePath = path.join(this.sessionDir, RESULT_PIPE);
|
|
127
|
+
this.stateFilePath = path.join(this.sessionDir, STATE_FILE);
|
|
112
128
|
// Log status changes through event emitter
|
|
113
129
|
this.on(exports.SESSION_EVENTS.STARTED, () => {
|
|
114
130
|
console.error(`SESSION_MANAGER: Session started successfully`);
|
|
@@ -134,6 +150,70 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
134
150
|
getState() {
|
|
135
151
|
return this.sessionState;
|
|
136
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Get the session directory
|
|
155
|
+
*/
|
|
156
|
+
getSessionDir() {
|
|
157
|
+
return this.sessionDir;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get the session ID
|
|
161
|
+
*/
|
|
162
|
+
getSessionId() {
|
|
163
|
+
return this.sessionId;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Check if the session is active
|
|
167
|
+
* @returns Promise that resolves to true if the session is active
|
|
168
|
+
*/
|
|
169
|
+
isSessionActive() {
|
|
170
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
171
|
+
try {
|
|
172
|
+
// If our internal state is not running, return false immediately
|
|
173
|
+
if (this.sessionState !== exports.SessionState.RUNNING && this.sessionState !== exports.SessionState.BUSY) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
// Check if session directory exists
|
|
177
|
+
if (!fs.existsSync(this.sessionDir)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
// Check if state file exists
|
|
181
|
+
if (!fs.existsSync(this.stateFilePath)) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
// Request status from the running session
|
|
185
|
+
const statusResult = yield this.executeScript('print("SCRIPT_SUCCESS: Session is active")', 5000);
|
|
186
|
+
return statusResult.success;
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
console.error(`SESSION_MANAGER: Error checking session status: ${error}`);
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get detailed session information
|
|
196
|
+
* @returns Promise that resolves to session information
|
|
197
|
+
*/
|
|
198
|
+
getSessionInfo() {
|
|
199
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
200
|
+
try {
|
|
201
|
+
// Check if session is active
|
|
202
|
+
const isActive = yield this.isSessionActive();
|
|
203
|
+
if (!isActive) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
// Read the state file
|
|
207
|
+
const stateContent = yield (0, promises_1.readFile)(this.stateFilePath, 'utf8');
|
|
208
|
+
const stateData = JSON.parse(stateContent);
|
|
209
|
+
return stateData;
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
console.error(`SESSION_MANAGER: Error getting session info: ${error}`);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
137
217
|
/**
|
|
138
218
|
* Start a CODESYS session
|
|
139
219
|
* @returns Promise that resolves when the session is started
|
|
@@ -145,55 +225,122 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
145
225
|
return Promise.resolve(); // Session already running
|
|
146
226
|
}
|
|
147
227
|
this.sessionState = exports.SessionState.STARTING;
|
|
148
|
-
|
|
228
|
+
try {
|
|
229
|
+
console.error(`SESSION_MANAGER: Starting CODESYS session with profile: ${this.codesysProfileName}`);
|
|
230
|
+
// Create session directory if it doesn't exist
|
|
231
|
+
if (!fs.existsSync(SESSION_DIR)) {
|
|
232
|
+
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
233
|
+
}
|
|
234
|
+
// Create our specific session directory
|
|
235
|
+
if (!fs.existsSync(this.sessionDir)) {
|
|
236
|
+
fs.mkdirSync(this.sessionDir, { recursive: true });
|
|
237
|
+
}
|
|
238
|
+
// Create initial state file
|
|
239
|
+
const initialState = {
|
|
240
|
+
sessionId: this.sessionId,
|
|
241
|
+
status: exports.SessionState.STARTING,
|
|
242
|
+
startTime: new Date().toISOString()
|
|
243
|
+
};
|
|
244
|
+
yield (0, promises_1.writeFile)(this.stateFilePath, JSON.stringify(initialState, null, 2), 'utf8');
|
|
245
|
+
// Load the start_session script template
|
|
246
|
+
const scriptTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('start_session');
|
|
247
|
+
// Interpolate parameters
|
|
248
|
+
const scriptContent = (0, scriptLoader_1.interpolateScript)(scriptTemplate, {
|
|
249
|
+
SESSION_DIR: this.sessionDir,
|
|
250
|
+
COMMAND_PIPE: COMMAND_PIPE,
|
|
251
|
+
RESULT_PIPE: RESULT_PIPE,
|
|
252
|
+
STATE_FILE: STATE_FILE,
|
|
253
|
+
SESSION_ID: this.sessionId,
|
|
254
|
+
});
|
|
255
|
+
// Create a temp file for the script
|
|
256
|
+
const tempDir = os.tmpdir();
|
|
257
|
+
const tempFileName = `codesys_start_session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}.py`;
|
|
258
|
+
const tempFilePath = path.join(tempDir, tempFileName);
|
|
259
|
+
// Write the script to the temp file
|
|
260
|
+
yield (0, promises_1.writeFile)(tempFilePath, scriptContent, 'latin1');
|
|
261
|
+
// Prepare CODESYS process
|
|
262
|
+
const codesysDir = path.dirname(this.codesysExePath);
|
|
263
|
+
const quotedExePath = `"${this.codesysExePath}"`;
|
|
264
|
+
const profileArg = `--profile="${this.codesysProfileName}"`;
|
|
265
|
+
// Start CODESYS with the session script - in "visible" mode so we can see it's running
|
|
266
|
+
const fullCommandString = `${quotedExePath} ${profileArg} --runscript="${tempFilePath}"`;
|
|
267
|
+
// Create a modified environment with CODESYS bin dir in PATH
|
|
268
|
+
const spawnEnv = Object.assign({}, process.env);
|
|
269
|
+
const pathSeparator = os.platform() === 'win32' ? ';' : ':';
|
|
270
|
+
const originalPath = spawnEnv.PATH || '';
|
|
271
|
+
spawnEnv.PATH = `${codesysDir}${pathSeparator}${originalPath}`;
|
|
272
|
+
// Spawn the process
|
|
273
|
+
this.codesysProcess = (0, child_process_1.spawn)(fullCommandString, [], {
|
|
274
|
+
windowsHide: false, // Make visible to user
|
|
275
|
+
cwd: codesysDir,
|
|
276
|
+
env: spawnEnv,
|
|
277
|
+
shell: true
|
|
278
|
+
});
|
|
279
|
+
// Handle process events
|
|
280
|
+
this.codesysProcess.on('error', (error) => {
|
|
281
|
+
console.error(`SESSION_MANAGER: Error starting CODESYS: ${error.message}`);
|
|
282
|
+
this.emit(exports.SESSION_EVENTS.FAILED, error);
|
|
283
|
+
this.sessionState = exports.SessionState.ERROR;
|
|
284
|
+
});
|
|
285
|
+
this.codesysProcess.on('exit', (code) => {
|
|
286
|
+
console.error(`SESSION_MANAGER: CODESYS process exited with code ${code}`);
|
|
287
|
+
// Only change state if we're not already stopping
|
|
288
|
+
if (this.sessionState !== exports.SessionState.STOPPING && this.sessionState !== exports.SessionState.STOPPED) {
|
|
289
|
+
this.sessionState = exports.SessionState.ERROR;
|
|
290
|
+
this.emit(exports.SESSION_EVENTS.FAILED, new Error(`CODESYS process exited unexpectedly with code ${code}`));
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
// Wait for session to initialize by checking for the existence of pipes
|
|
294
|
+
const success = yield this.waitForSessionInitialization();
|
|
295
|
+
if (success) {
|
|
296
|
+
this.sessionState = exports.SessionState.RUNNING;
|
|
297
|
+
this.emit(exports.SESSION_EVENTS.STARTED);
|
|
298
|
+
this.resetIdleTimeout();
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
throw new Error('Session failed to initialize within timeout');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
console.error(`SESSION_MANAGER: Error during session start: ${error.message}`);
|
|
306
|
+
this.sessionState = exports.SessionState.ERROR;
|
|
307
|
+
this.emit(exports.SESSION_EVENTS.FAILED, error);
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Wait for session initialization by checking for the existence of the pipes
|
|
314
|
+
* @param timeoutMs Timeout in milliseconds
|
|
315
|
+
* @returns Promise that resolves to true if session initialized successfully
|
|
316
|
+
*/
|
|
317
|
+
waitForSessionInitialization() {
|
|
318
|
+
return __awaiter(this, arguments, void 0, function* (timeoutMs = 30000) {
|
|
319
|
+
const startTime = Date.now();
|
|
320
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
149
321
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const spawnEnv = Object.assign({}, process.env);
|
|
161
|
-
const pathSeparator = os.platform() === 'win32' ? ';' : ':';
|
|
162
|
-
const originalPath = spawnEnv.PATH || '';
|
|
163
|
-
spawnEnv.PATH = `${codesysDir}${pathSeparator}${originalPath}`;
|
|
164
|
-
// Spawn the process
|
|
165
|
-
this.codesysProcess = (0, child_process_1.spawn)(fullCommandString, [], {
|
|
166
|
-
windowsHide: false, // Make visible to user
|
|
167
|
-
cwd: codesysDir,
|
|
168
|
-
env: spawnEnv,
|
|
169
|
-
shell: true
|
|
170
|
-
});
|
|
171
|
-
// Handle process events
|
|
172
|
-
this.codesysProcess.on('error', (error) => {
|
|
173
|
-
console.error(`SESSION_MANAGER: Error starting CODESYS: ${error.message}`);
|
|
174
|
-
this.emit(exports.SESSION_EVENTS.FAILED, error);
|
|
175
|
-
reject(error);
|
|
176
|
-
});
|
|
177
|
-
// Give it a small delay to start properly
|
|
178
|
-
setTimeout(() => {
|
|
179
|
-
if (this.codesysProcess && !this.codesysProcess.killed) {
|
|
180
|
-
this.emit(exports.SESSION_EVENTS.STARTED);
|
|
181
|
-
this.resetIdleTimeout();
|
|
182
|
-
resolve();
|
|
322
|
+
// Check if pipes exist
|
|
323
|
+
const commandPipeExists = fs.existsSync(this.commandPipePath);
|
|
324
|
+
const resultPipeExists = fs.existsSync(this.resultPipePath);
|
|
325
|
+
if (commandPipeExists && resultPipeExists) {
|
|
326
|
+
// Read state file to check if session is ready
|
|
327
|
+
const stateContent = yield (0, promises_1.readFile)(this.stateFilePath, 'utf8');
|
|
328
|
+
const stateData = JSON.parse(stateContent);
|
|
329
|
+
if (stateData.status === exports.SessionState.RUNNING) {
|
|
330
|
+
console.error(`SESSION_MANAGER: Session initialized successfully`);
|
|
331
|
+
return true;
|
|
183
332
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
reject(error);
|
|
188
|
-
}
|
|
189
|
-
}, 2000); // 2 second delay to let CODESYS initialize
|
|
333
|
+
}
|
|
334
|
+
// Wait a bit before checking again
|
|
335
|
+
yield new Promise(resolve => setTimeout(resolve, DEFAULT_SESSION_POLL_INTERVAL));
|
|
190
336
|
}
|
|
191
337
|
catch (error) {
|
|
192
|
-
console.error(`SESSION_MANAGER: Error
|
|
193
|
-
|
|
194
|
-
reject(error);
|
|
338
|
+
console.error(`SESSION_MANAGER: Error checking session initialization: ${error}`);
|
|
339
|
+
// Continue waiting
|
|
195
340
|
}
|
|
196
|
-
}
|
|
341
|
+
}
|
|
342
|
+
console.error(`SESSION_MANAGER: Session initialization timed out after ${timeoutMs}ms`);
|
|
343
|
+
return false;
|
|
197
344
|
});
|
|
198
345
|
}
|
|
199
346
|
/**
|
|
@@ -217,29 +364,123 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
217
364
|
pending.reject(new Error('Session was stopped while script was pending'));
|
|
218
365
|
this.pendingScripts.delete(id);
|
|
219
366
|
}
|
|
220
|
-
|
|
367
|
+
try {
|
|
368
|
+
// Check if we have a running session
|
|
369
|
+
if (fs.existsSync(this.sessionDir) && fs.existsSync(this.commandPipePath)) {
|
|
370
|
+
// Load the stop_session script template
|
|
371
|
+
const scriptTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('stop_session');
|
|
372
|
+
// Interpolate parameters
|
|
373
|
+
const scriptContent = (0, scriptLoader_1.interpolateScript)(scriptTemplate, {
|
|
374
|
+
SESSION_DIR: this.sessionDir,
|
|
375
|
+
COMMAND_PIPE: COMMAND_PIPE,
|
|
376
|
+
RESULT_PIPE: RESULT_PIPE,
|
|
377
|
+
STATE_FILE: STATE_FILE,
|
|
378
|
+
SESSION_ID: this.sessionId,
|
|
379
|
+
});
|
|
380
|
+
try {
|
|
381
|
+
// Execute the stop script
|
|
382
|
+
const tempDir = os.tmpdir();
|
|
383
|
+
const tempFileName = `codesys_stop_session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}.py`;
|
|
384
|
+
const tempFilePath = path.join(tempDir, tempFileName);
|
|
385
|
+
// Write the script to the temp file
|
|
386
|
+
yield (0, promises_1.writeFile)(tempFilePath, scriptContent, 'latin1');
|
|
387
|
+
// Prepare CODESYS process
|
|
388
|
+
const codesysDir = path.dirname(this.codesysExePath);
|
|
389
|
+
const quotedExePath = `"${this.codesysExePath}"`;
|
|
390
|
+
const profileArg = `--profile="${this.codesysProfileName}"`;
|
|
391
|
+
// Execute the stop script
|
|
392
|
+
const fullCommandString = `${quotedExePath} ${profileArg} --noUI --runscript="${tempFilePath}"`;
|
|
393
|
+
// Create a modified environment with CODESYS bin dir in PATH
|
|
394
|
+
const spawnEnv = Object.assign({}, process.env);
|
|
395
|
+
const pathSeparator = os.platform() === 'win32' ? ';' : ':';
|
|
396
|
+
const originalPath = spawnEnv.PATH || '';
|
|
397
|
+
spawnEnv.PATH = `${codesysDir}${pathSeparator}${originalPath}`;
|
|
398
|
+
// Execute the script
|
|
399
|
+
console.error('SESSION_MANAGER: Executing stop session script');
|
|
400
|
+
const stopProcess = (0, child_process_1.spawn)(fullCommandString, [], {
|
|
401
|
+
windowsHide: true,
|
|
402
|
+
cwd: codesysDir,
|
|
403
|
+
env: spawnEnv,
|
|
404
|
+
shell: true
|
|
405
|
+
});
|
|
406
|
+
yield new Promise((resolve) => {
|
|
407
|
+
stopProcess.on('exit', () => {
|
|
408
|
+
resolve();
|
|
409
|
+
});
|
|
410
|
+
// Set a timeout in case the stop script hangs
|
|
411
|
+
setTimeout(() => {
|
|
412
|
+
resolve();
|
|
413
|
+
}, 5000);
|
|
414
|
+
});
|
|
415
|
+
// Clean up the temp file
|
|
416
|
+
try {
|
|
417
|
+
yield (0, promises_1.unlink)(tempFilePath);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
console.error(`SESSION_MANAGER: Error deleting temp file ${tempFilePath}: ${error}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
console.error(`SESSION_MANAGER: Error executing stop script: ${error}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// If we have a process, kill it
|
|
221
428
|
if (this.codesysProcess && !this.codesysProcess.killed) {
|
|
222
429
|
console.error('SESSION_MANAGER: Stopping CODESYS process');
|
|
223
430
|
// First try SIGTERM
|
|
224
431
|
this.codesysProcess.kill('SIGTERM');
|
|
225
432
|
// Set a timeout to force kill if needed
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
433
|
+
yield new Promise((resolve) => {
|
|
434
|
+
setTimeout(() => {
|
|
435
|
+
if (this.codesysProcess && !this.codesysProcess.killed) {
|
|
436
|
+
console.error('SESSION_MANAGER: SIGTERM failed, using SIGKILL');
|
|
437
|
+
this.codesysProcess.kill('SIGKILL');
|
|
438
|
+
}
|
|
439
|
+
resolve();
|
|
440
|
+
}, 2000); // Give it 2 seconds to terminate gracefully
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
// Clean up session directory
|
|
444
|
+
try {
|
|
445
|
+
if (fs.existsSync(this.sessionDir)) {
|
|
446
|
+
console.error(`SESSION_MANAGER: Cleaning up session directory: ${this.sessionDir}`);
|
|
447
|
+
// Use recursive: true once we're on Node.js 14+
|
|
448
|
+
this.recursiveRemoveDir(this.sessionDir);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
console.error(`SESSION_MANAGER: Error cleaning up session directory: ${error}`);
|
|
453
|
+
}
|
|
454
|
+
this.codesysProcess = null;
|
|
455
|
+
this.sessionState = exports.SessionState.STOPPED;
|
|
456
|
+
this.emit(exports.SESSION_EVENTS.STOPPED);
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
console.error(`SESSION_MANAGER: Error during session stop: ${error}`);
|
|
460
|
+
this.sessionState = exports.SessionState.ERROR;
|
|
461
|
+
this.emit(exports.SESSION_EVENTS.FAILED, error);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Recursively remove a directory
|
|
467
|
+
* @param dirPath Directory path to remove
|
|
468
|
+
*/
|
|
469
|
+
recursiveRemoveDir(dirPath) {
|
|
470
|
+
if (fs.existsSync(dirPath)) {
|
|
471
|
+
fs.readdirSync(dirPath).forEach((file) => {
|
|
472
|
+
const curPath = path.join(dirPath, file);
|
|
473
|
+
if (fs.lstatSync(curPath).isDirectory()) {
|
|
474
|
+
// Recursive call
|
|
475
|
+
this.recursiveRemoveDir(curPath);
|
|
235
476
|
}
|
|
236
477
|
else {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
resolve();
|
|
478
|
+
// Delete file
|
|
479
|
+
fs.unlinkSync(curPath);
|
|
240
480
|
}
|
|
241
481
|
});
|
|
242
|
-
|
|
482
|
+
fs.rmdirSync(dirPath);
|
|
483
|
+
}
|
|
243
484
|
}
|
|
244
485
|
/**
|
|
245
486
|
* Execute a script in the persistent CODESYS session
|
|
@@ -283,31 +524,35 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
283
524
|
this.resetIdleTimeout();
|
|
284
525
|
// Mark the session as busy
|
|
285
526
|
this.sessionState = exports.SessionState.BUSY;
|
|
527
|
+
// Check if the command pipe exists
|
|
528
|
+
if (!fs.existsSync(this.commandPipePath)) {
|
|
529
|
+
this.sessionState = exports.SessionState.ERROR;
|
|
530
|
+
throw new Error(`Command pipe not found at ${this.commandPipePath}`);
|
|
531
|
+
}
|
|
532
|
+
// Check if the result pipe exists
|
|
533
|
+
if (!fs.existsSync(this.resultPipePath)) {
|
|
534
|
+
this.sessionState = exports.SessionState.ERROR;
|
|
535
|
+
throw new Error(`Result pipe not found at ${this.resultPipePath}`);
|
|
536
|
+
}
|
|
286
537
|
// Return a promise that will be resolved when script execution completes
|
|
287
538
|
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
|
|
288
539
|
try {
|
|
289
|
-
|
|
290
|
-
const tempDir = os.tmpdir();
|
|
291
|
-
const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).substring(2, 9)}.py`;
|
|
292
|
-
const tempFilePath = path.join(tempDir, tempFileName);
|
|
293
|
-
// Write the script to the temp file
|
|
294
|
-
yield (0, promises_1.writeFile)(tempFilePath, scriptContent, 'latin1');
|
|
295
|
-
console.error(`SESSION_MANAGER: Script ${scriptId} created at ${tempFilePath}`);
|
|
540
|
+
console.error(`SESSION_MANAGER: Executing script ${scriptId}`);
|
|
296
541
|
// Set up a timeout for this script execution
|
|
297
542
|
const scriptTimeout = setTimeout(() => {
|
|
298
543
|
if (this.pendingScripts.has(scriptId)) {
|
|
299
544
|
const pending = this.pendingScripts.get(scriptId);
|
|
300
545
|
this.pendingScripts.delete(scriptId);
|
|
301
546
|
pending.reject(new Error(`Script execution timed out after ${timeoutMs}ms`));
|
|
547
|
+
// Check if we have more pending scripts
|
|
548
|
+
if (this.pendingScripts.size === 0) {
|
|
549
|
+
this.sessionState = exports.SessionState.RUNNING;
|
|
550
|
+
}
|
|
302
551
|
}
|
|
303
552
|
}, timeoutMs);
|
|
304
553
|
// Add to pending scripts
|
|
305
554
|
this.pendingScripts.set(scriptId, {
|
|
306
555
|
resolve: (result) => {
|
|
307
|
-
// Clean up the temp file
|
|
308
|
-
(0, promises_1.unlink)(tempFilePath).catch(err => {
|
|
309
|
-
console.error(`SESSION_MANAGER: Error deleting temp file ${tempFilePath}: ${err.message}`);
|
|
310
|
-
});
|
|
311
556
|
resolve(result);
|
|
312
557
|
// Check if we have more pending scripts
|
|
313
558
|
if (this.pendingScripts.size === 0) {
|
|
@@ -315,10 +560,6 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
315
560
|
}
|
|
316
561
|
},
|
|
317
562
|
reject: (error) => {
|
|
318
|
-
// Clean up the temp file
|
|
319
|
-
(0, promises_1.unlink)(tempFilePath).catch(err => {
|
|
320
|
-
console.error(`SESSION_MANAGER: Error deleting temp file ${tempFilePath}: ${err.message}`);
|
|
321
|
-
});
|
|
322
563
|
reject(error);
|
|
323
564
|
// Check if we have more pending scripts
|
|
324
565
|
if (this.pendingScripts.size === 0) {
|
|
@@ -327,33 +568,100 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
327
568
|
},
|
|
328
569
|
timeout: scriptTimeout
|
|
329
570
|
});
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
};
|
|
343
|
-
pending.resolve(result);
|
|
344
|
-
}
|
|
345
|
-
}, 1000); // 1 second delay for mock result
|
|
571
|
+
// Create the command to send
|
|
572
|
+
const command = {
|
|
573
|
+
id: scriptId,
|
|
574
|
+
script: scriptContent,
|
|
575
|
+
timestamp: Date.now()
|
|
576
|
+
};
|
|
577
|
+
// Send the command
|
|
578
|
+
const commandString = JSON.stringify(command) + '\n';
|
|
579
|
+
fs.writeFileSync(this.commandPipePath, commandString);
|
|
580
|
+
console.error(`SESSION_MANAGER: Command sent to pipe`);
|
|
581
|
+
// Start reading the result pipe
|
|
582
|
+
this.readResultPipe(scriptId);
|
|
346
583
|
}
|
|
347
584
|
catch (error) {
|
|
348
|
-
|
|
349
|
-
//
|
|
585
|
+
console.error(`SESSION_MANAGER: Error executing script: ${error.message}`);
|
|
586
|
+
// Remove from pending scripts
|
|
587
|
+
if (this.pendingScripts.has(scriptId)) {
|
|
588
|
+
const pending = this.pendingScripts.get(scriptId);
|
|
589
|
+
clearTimeout(pending.timeout);
|
|
590
|
+
this.pendingScripts.delete(scriptId);
|
|
591
|
+
}
|
|
592
|
+
// Update session state if no more pending scripts
|
|
350
593
|
if (this.pendingScripts.size === 0) {
|
|
351
594
|
this.sessionState = exports.SessionState.RUNNING;
|
|
352
595
|
}
|
|
596
|
+
reject(error);
|
|
353
597
|
}
|
|
354
598
|
}));
|
|
355
599
|
});
|
|
356
600
|
}
|
|
601
|
+
/**
|
|
602
|
+
* Read the result pipe for a specific script execution
|
|
603
|
+
* @param scriptId The script ID to read results for
|
|
604
|
+
*/
|
|
605
|
+
readResultPipe(scriptId) {
|
|
606
|
+
console.error(`SESSION_MANAGER: Starting to read result pipe for script ${scriptId}`);
|
|
607
|
+
// We'll use a polling approach to read from the pipe
|
|
608
|
+
const pollInterval = setInterval(() => __awaiter(this, void 0, void 0, function* () {
|
|
609
|
+
try {
|
|
610
|
+
if (!this.pendingScripts.has(scriptId)) {
|
|
611
|
+
console.error(`SESSION_MANAGER: Script ${scriptId} is no longer pending, stopping poll`);
|
|
612
|
+
clearInterval(pollInterval);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
// Check if the result pipe exists
|
|
616
|
+
if (!fs.existsSync(this.resultPipePath)) {
|
|
617
|
+
console.error(`SESSION_MANAGER: Result pipe not found at ${this.resultPipePath}`);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
// Read the result pipe
|
|
621
|
+
const resultContent = yield (0, promises_1.readFile)(this.resultPipePath, 'utf8');
|
|
622
|
+
// Process all lines
|
|
623
|
+
const lines = resultContent.split('\n');
|
|
624
|
+
for (const line of lines) {
|
|
625
|
+
if (!line.trim())
|
|
626
|
+
continue;
|
|
627
|
+
try {
|
|
628
|
+
const result = JSON.parse(line);
|
|
629
|
+
// Check if this result is for our script
|
|
630
|
+
if (result.id === scriptId) {
|
|
631
|
+
console.error(`SESSION_MANAGER: Found result for script ${scriptId}`);
|
|
632
|
+
// Remove from pending scripts
|
|
633
|
+
if (this.pendingScripts.has(scriptId)) {
|
|
634
|
+
const pending = this.pendingScripts.get(scriptId);
|
|
635
|
+
clearTimeout(pending.timeout);
|
|
636
|
+
this.pendingScripts.delete(scriptId);
|
|
637
|
+
// Resolve with the result
|
|
638
|
+
const scriptResult = {
|
|
639
|
+
id: scriptId,
|
|
640
|
+
success: result.success,
|
|
641
|
+
output: result.output
|
|
642
|
+
};
|
|
643
|
+
pending.resolve(scriptResult);
|
|
644
|
+
}
|
|
645
|
+
// Stop polling
|
|
646
|
+
clearInterval(pollInterval);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
catch (parseError) {
|
|
651
|
+
console.error(`SESSION_MANAGER: Error parsing result line: ${parseError}`);
|
|
652
|
+
// Continue with the next line
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
console.error(`SESSION_MANAGER: Error reading result pipe: ${error}`);
|
|
658
|
+
}
|
|
659
|
+
}), 100); // Poll every 100ms
|
|
660
|
+
// Set a timeout to stop polling after the script timeout
|
|
661
|
+
setTimeout(() => {
|
|
662
|
+
clearInterval(pollInterval);
|
|
663
|
+
}, 60000); // 1 minute max polling time
|
|
664
|
+
}
|
|
357
665
|
/**
|
|
358
666
|
* Reset the idle timeout
|
|
359
667
|
*/
|
|
@@ -371,31 +679,5 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
371
679
|
}, this.idleTimeoutMs);
|
|
372
680
|
}
|
|
373
681
|
}
|
|
374
|
-
/**
|
|
375
|
-
* Create a keep-alive script that keeps CODESYS running
|
|
376
|
-
* This is a placeholder for now - we'll need to implement
|
|
377
|
-
* a proper mechanism to keep CODESYS running while allowing
|
|
378
|
-
* additional scripts to be executed
|
|
379
|
-
*/
|
|
380
|
-
createKeepAliveScript() {
|
|
381
|
-
// This is a placeholder - we need a real implementation
|
|
382
|
-
// that keeps CODESYS running and allows for executing other scripts
|
|
383
|
-
const tempScript = `
|
|
384
|
-
import time
|
|
385
|
-
|
|
386
|
-
# This is a placeholder keep-alive script
|
|
387
|
-
# In a real implementation, we would:
|
|
388
|
-
# 1. Set up a communication channel (named pipe, socket, etc.)
|
|
389
|
-
# 2. Listen for script execution requests
|
|
390
|
-
# 3. Execute them and return results
|
|
391
|
-
|
|
392
|
-
print("Keep-alive script running...")
|
|
393
|
-
|
|
394
|
-
# Just keep running for now
|
|
395
|
-
while True:
|
|
396
|
-
time.sleep(1)
|
|
397
|
-
`;
|
|
398
|
-
return tempScript;
|
|
399
|
-
}
|
|
400
682
|
}
|
|
401
683
|
exports.CodesysSessionManager = CodesysSessionManager;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
# CODESYS MCP Session Management
|
|
1
|
+
# CODESYS MCP Session Management (Not Yet Implemented)
|
|
2
|
+
|
|
3
|
+
> **IMPORTANT:** Session management is currently disabled in version 1.2.4.
|
|
4
|
+
> The `start_session` and `stop_session` commands exist but are non-functional.
|
|
5
|
+
> All operations will run in standard mode (starting and stopping CODESYS for each operation).
|
|
6
|
+
> This feature will be fully implemented in a future release.
|
|
2
7
|
|
|
3
8
|
## Overview
|
|
4
9
|
|