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