@codesys/mcp-toolkit 1.0.7 → 1.0.8

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.
@@ -43,7 +43,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.executeCodesysScript = executeCodesysScript;
46
- const child_process_1 = require("child_process");
46
+ const child_process_1 = require("child_process"); // Use spawn directly
47
47
  const promises_1 = require("fs/promises");
48
48
  const path = __importStar(require("path"));
49
49
  const os = __importStar(require("os"));
@@ -52,6 +52,7 @@ const SCRIPT_SUCCESS_MARKER = 'SCRIPT_SUCCESS';
52
52
  const SCRIPT_ERROR_MARKER = 'SCRIPT_ERROR';
53
53
  /**
54
54
  * Executes a CODESYS Python script using the command line interface.
55
+ * Uses spawn for better handling of paths with spaces.
55
56
  *
56
57
  * @param scriptContent The Python script code to execute.
57
58
  * @param codesysExePath The full path to the CODESYS.exe executable.
@@ -69,60 +70,95 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
69
70
  }
70
71
  // Create a temporary file for the script
71
72
  const tempDir = os.tmpdir();
72
- const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).substring(7)}.py`;
73
+ // Ensure temp file name is safe for command line (though spawn handles args better)
74
+ const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).substring(2, 9)}.py`;
73
75
  const tempFilePath = path.join(tempDir, tempFileName);
74
76
  let output = '';
75
77
  let success = false;
78
+ let exitCode = null;
76
79
  try {
77
80
  // Normalize line endings to LF (\n)
78
81
  const normalizedScriptContent = scriptContent.replace(/\r\n/g, '\n');
79
82
  // Write the script content to the temporary file using latin1 encoding
80
- yield (0, promises_1.writeFile)(tempFilePath, normalizedScriptContent, 'latin1'); // Explicitly use latin1
81
- process.stderr.write(`Temporary script written to: ${tempFilePath} with latin1 encoding and LF line endings\n`); // Use process.stderr
82
- // Add --additionalfolder argument
83
- const additionalFolderPath = "C:\\Program Files\\CODESYS 3.5.21.0\\CODESYS\\AdditionalFolders\\Default"; // Assuming Default instance
84
- // Fix: Use shell:true but ensure proper quoting for paths with spaces
85
- const commandWithArgs = `"${codesysExePath}" --profile="${codesysProfileName}" --noUI --runscript="${tempFilePath}"`;
86
- process.stderr.write(`Executing: ${commandWithArgs}\n`); // Log the command
87
- // CONFIRMED WORKING APPROACH: Use simple exec() with complete quoting
88
- // This approach is tested and works with the CODESYS executable
89
- const execResult = yield new Promise((resolve) => {
90
- process.stderr.write(`EXECUTING COMMAND: ${commandWithArgs}\n`);
91
- // Execute command using exec which handles Windows command paths better
92
- const childProcess = (0, child_process_1.exec)(commandWithArgs, { timeout: 30000 }, (error, stdout, stderr) => {
83
+ yield (0, promises_1.writeFile)(tempFilePath, normalizedScriptContent, 'latin1');
84
+ process.stderr.write(`Temporary script written to: ${tempFilePath} with latin1 encoding and LF line endings\n`);
85
+ // --- Use spawn ---
86
+ const command = codesysExePath; // The executable itself
87
+ const args = [
88
+ `--profile=${codesysProfileName}`, // Format expected by CODESYS.exe
89
+ '--noUI',
90
+ `--runscript=${tempFilePath}` // Format expected by CODESYS.exe
91
+ // If CODESYS requires quotes *around* the profile/script path *value*, adjust like:
92
+ // `--profile="${codesysProfileName}"`,
93
+ // `--runscript="${tempFilePath}"`,
94
+ // But usually, spawn handles the argument separation correctly without needing inner quotes unless the *program itself* requires them.
95
+ // Test without inner quotes first.
96
+ ];
97
+ process.stderr.write(`Spawning: ${command} with args: ${JSON.stringify(args)}\n`);
98
+ const spawnResult = yield new Promise((resolve, reject) => {
99
+ let stdoutData = '';
100
+ let stderrData = '';
101
+ // Spawn the process. Detached:false is default. Timeout via AbortController recommended for spawn.
102
+ const controller = new AbortController();
103
+ const timeoutSignal = controller.signal;
104
+ const timeoutDuration = 30000; // 30 seconds
105
+ const childProcess = (0, child_process_1.spawn)(command, args, {
106
+ windowsHide: true, // Hides the console window on Windows
107
+ signal: timeoutSignal // Use AbortSignal for timeout
108
+ });
109
+ const timeoutId = setTimeout(() => {
110
+ process.stderr.write(`TIMEOUT: CODESYS process timed out after ${timeoutDuration / 1000} seconds\n`);
111
+ controller.abort(); // Abort the process via the signal
112
+ }, timeoutDuration);
113
+ childProcess.stdout.on('data', (data) => {
114
+ const chunk = data.toString();
115
+ stdoutData += chunk;
116
+ process.stderr.write(`CODESYS stdout chunk: ${chunk.length > 100 ? chunk.substring(0, 100) + '...' : chunk}\n`);
117
+ });
118
+ childProcess.stderr.on('data', (data) => {
119
+ const chunk = data.toString();
120
+ stderrData += chunk;
121
+ process.stderr.write(`CODESYS stderr chunk: ${chunk}\n`); // Log stderr more verbosely
122
+ });
123
+ childProcess.on('error', (error) => {
93
124
  var _a;
94
- let combinedOutput = "";
95
- if (stdout)
96
- combinedOutput += stdout;
97
- if (stderr)
98
- combinedOutput += stderr;
99
- // Log results for debugging
100
- if (stdout)
101
- process.stderr.write(`CODESYS stdout: ${stdout.length > 100 ? stdout.substring(0, 100) + '...' : stdout}\n`);
102
- if (stderr)
103
- process.stderr.write(`CODESYS stderr: ${stderr}\n`);
104
- if (error) {
105
- process.stderr.write(`CODESYS exec error: ${error.message}\n`);
106
- resolve({ code: (_a = error.code) !== null && _a !== void 0 ? _a : 1, output: combinedOutput });
107
- }
108
- else {
109
- resolve({ code: 0, output: combinedOutput });
110
- }
125
+ clearTimeout(timeoutId); // Clear timeout on explicit error
126
+ // This usually means the process couldn't be spawned (e.g., EACCES, ENOENT)
127
+ process.stderr.write(`CODESYS spawn error: ${error.message}\n`);
128
+ // Don't reject here, resolve with error info like exec did
129
+ resolve({ code: (_a = error.errno) !== null && _a !== void 0 ? _a : 1, output: stdoutData + stderrData + `\nSpawn Error: ${error.message}` });
111
130
  });
112
- // Safety cleanup for timeout case
113
- childProcess.on('timeout', () => {
114
- process.stderr.write(`TIMEOUT: CODESYS process timed out after 30 seconds\n`);
115
- childProcess.kill();
116
- resolve({ code: 0, output: output + "\nTIMEOUT: CODESYS process timed out" });
131
+ childProcess.on('close', (code) => {
132
+ clearTimeout(timeoutId); // Clear timeout on successful close
133
+ process.stderr.write(`CODESYS process closed with code: ${code}\n`);
134
+ const combinedOutput = stdoutData + stderrData;
135
+ resolve({ code: code, output: combinedOutput });
117
136
  });
137
+ // Handle the timeout signal specifically
138
+ timeoutSignal.addEventListener('abort', () => {
139
+ process.stderr.write('Abort signal received, killing process if still running.\n');
140
+ if (!childProcess.killed) {
141
+ childProcess.kill('SIGTERM'); // Attempt graceful kill first
142
+ // Optionally force kill after a short delay if SIGTERM doesn't work
143
+ setTimeout(() => {
144
+ if (!childProcess.killed) {
145
+ process.stderr.write('SIGTERM ineffective, sending SIGKILL.\n');
146
+ childProcess.kill('SIGKILL');
147
+ }
148
+ }, 2000); // 2 second grace period
149
+ }
150
+ // Resolve with timeout information. The 'close' event might still fire later with a specific code.
151
+ // We already logged the timeout, let the 'close' handler report the final code.
152
+ // If 'close' doesn't fire after kill, it might indicate a deeper issue.
153
+ // We could resolve here with a timeout indicator, but letting 'close' handle it is cleaner if it works.
154
+ // If 'close' never fires after abort/kill, the promise might hang. Let's add a failsafe resolve in the listener.
155
+ // Revised: Resolve directly on abort to ensure promise completion.
156
+ resolve({ code: null, output: stdoutData + stderrData + "\nTIMEOUT: CODESYS process aborted due to timeout." });
157
+ }, { once: true }); // Ensure listener is called only once
118
158
  });
119
- // Add exec results to our output and get the exit code
120
- output += execResult.output;
121
- const exitCode = execResult.code;
122
- process.stderr.write(`CODESYS process exited with code: ${exitCode}\n`); // Use process.stderr
159
+ output = spawnResult.output;
160
+ exitCode = spawnResult.code; // Can be null if process killed before exit code retrieved
123
161
  // Determine success based on exit code and output markers
124
- // Note: CODESYS scripting might return exit code 0 even if the script had logical errors.
125
- // Relying on specific output markers is more robust.
126
162
  if (output.includes(SCRIPT_SUCCESS_MARKER)) {
127
163
  success = true;
128
164
  }
@@ -130,48 +166,50 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
130
166
  success = false;
131
167
  }
132
168
  else {
133
- // If no markers found, fall back to exit code (less reliable)
134
- success = exitCode === 0;
135
- if (!success) {
136
- // Append the specific CODESYS error if available and no other marker found
169
+ // Fallback logic (handle null exit code)
170
+ if (exitCode === 0) {
171
+ console.warn("Warning: CODESYS script finished with exit code 0 but no SCRIPT_SUCCESS marker found.");
172
+ success = !output.includes(SCRIPT_ERROR_MARKER); // Assume success if exit code 0 and no error marker
173
+ }
174
+ else {
175
+ success = false;
176
+ const exitMsg = exitCode !== null ? `exit code ${exitCode}` : 'signal or unknown reason';
137
177
  if (!output.includes(SCRIPT_ERROR_MARKER)) {
138
- output += `\nSCRIPT_ERROR: Process exited with non-zero code ${exitCode}. Check CODESYS stderr output above for details.`;
178
+ output += `\nSCRIPT_ERROR: Process exited with ${exitMsg}. Check CODESYS stderr output above for details.`;
139
179
  }
140
180
  else {
141
- output += `\nSCRIPT_ERROR: Process exited with non-zero code ${exitCode}.`;
181
+ output += `\nSCRIPT_ERROR: Process exited with ${exitMsg}.`;
142
182
  }
143
- }
144
- else {
145
- // If exit code is 0 but no success marker, treat as potential issue or simple script
146
- console.error("Warning: CODESYS script finished with exit code 0 but no SCRIPT_SUCCESS marker found."); // Log warning to stderr
147
- // Optionally, you could treat this as success or failure depending on requirements
148
- // For now, let's assume success if exit code is 0 and no error marker.
149
- if (!output.includes(SCRIPT_ERROR_MARKER)) {
150
- success = true;
183
+ // Add timeout message specifically if that was the cause
184
+ if (output.includes("TIMEOUT: CODESYS process aborted")) {
185
+ // Message already added by the abort handler
186
+ }
187
+ else if (exitCode === null && !output.includes("Spawn Error:")) {
188
+ // If exit code is null but not due to spawn error or handled timeout, add generic killed message
189
+ output += `\nProcess may have been killed externally or timed out without proper exit code reporting.`;
151
190
  }
152
191
  }
153
192
  }
154
193
  }
155
194
  catch (error) {
156
- process.stderr.write(`Error executing CODESYS script: ${error}\n`); // Use process.stderr
157
- output += `\nSCRIPT_ERROR: Failed to execute script process. ${error.message}`;
195
+ process.stderr.write(`Error executing CODESYS script: ${error}\n`);
196
+ output += `\nSCRIPT_ERROR: Failed to set up script execution process. ${error.message}`;
158
197
  success = false;
159
198
  }
160
199
  finally {
161
200
  // Clean up the temporary file
162
201
  try {
163
202
  yield (0, promises_1.unlink)(tempFilePath);
164
- process.stderr.write(`Temporary script deleted: ${tempFilePath}\n`); // Use process.stderr
203
+ process.stderr.write(`Temporary script deleted: ${tempFilePath}\n`);
165
204
  }
166
205
  catch (cleanupError) {
167
- process.stderr.write(`Failed to delete temporary script file ${tempFilePath}: ${cleanupError}\n`); // Use process.stderr
168
- // Don't overwrite the original error if cleanup fails
169
- if (success) { // Only add cleanup error if the main process didn't already fail
206
+ process.stderr.write(`Failed to delete temporary script file ${tempFilePath}: ${cleanupError}\n`);
207
+ if (success) {
170
208
  output += `\nWARNING: Failed to delete temporary script file ${tempFilePath}. ${cleanupError.message}`;
171
209
  }
172
210
  }
173
211
  }
174
- process.stderr.write(`Final script output received:\n---\n${output}\n---\n`); // Use process.stderr
175
- return { success, output }; // Revert return object
212
+ process.stderr.write(`Final script output received (Success: ${success}):\n---\n${output}\n---\n`);
213
+ return { success, output };
176
214
  });
177
215
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codesys/mcp-toolkit",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "main": "dist/server.js",
5
5
  "bin": "./dist/bin.js",
6
6
  "scripts": {