@codesys/mcp-toolkit 1.0.9 → 1.1.2

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.
@@ -1,4 +1,16 @@
1
1
  "use strict";
2
+ /**
3
+ * CODESYS Interop Module
4
+ * Handles direct interaction with the CODESYS executable via command-line scripts.
5
+ *
6
+ * This module manages:
7
+ * - Creating temporary Python script files
8
+ * - Executing them via CODESYS's scripting engine
9
+ * - Capturing and processing results
10
+ *
11
+ * IMPORTANT: Path handling for Windows is critical - paths with spaces require
12
+ * special handling to avoid the 'C:\Program' not recognized error.
13
+ */
2
14
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
15
  if (k2 === undefined) k2 = k;
4
16
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -43,152 +55,216 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
55
  };
44
56
  Object.defineProperty(exports, "__esModule", { value: true });
45
57
  exports.executeCodesysScript = executeCodesysScript;
46
- const child_process_1 = require("child_process"); // Still using spawn
58
+ const child_process_1 = require("child_process");
47
59
  const promises_1 = require("fs/promises");
48
60
  const path = __importStar(require("path"));
49
61
  const os = __importStar(require("os"));
62
+ const fs = __importStar(require("fs")); // Import fs for existsSync check
50
63
  // Define expected success/error markers from the Python scripts
51
64
  const SCRIPT_SUCCESS_MARKER = 'SCRIPT_SUCCESS';
52
65
  const SCRIPT_ERROR_MARKER = 'SCRIPT_ERROR';
53
66
  /**
54
67
  * Executes a CODESYS Python script using the command line interface.
55
- * Tries using spawn with shell:true and explicit quoting for cmd.exe.
56
68
  *
57
69
  * @param scriptContent The Python script code to execute.
58
- * @param codesysExePath The full path to the CODESYS.exe executable.
70
+ * @param codesysExePath The full path to the CODESYS.exe executable (can contain spaces).
59
71
  * @param codesysProfileName The name of the CODESYS profile to use for scripting.
60
72
  * @returns A promise resolving to an object containing the success status and the script's output.
73
+ *
74
+ * NOTE: Using shell: true with careful command string quoting to handle
75
+ * CODESYS argument parsing quirks when launched non-interactively.
61
76
  */
62
77
  function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName) {
63
78
  return __awaiter(this, void 0, void 0, function* () {
64
- if (!codesysExePath) {
65
- throw new Error('CODESYS executable path was not provided to executeCodesysScript.');
66
- }
67
- if (!codesysProfileName) {
68
- throw new Error('CODESYS profile name was not provided to executeCodesysScript.');
69
- }
79
+ // --- Pre-checks ---
80
+ if (!codesysExePath)
81
+ throw new Error('CODESYS executable path was not provided.');
82
+ if (!codesysProfileName)
83
+ throw new Error('CODESYS profile name was not provided.');
84
+ if (!fs.existsSync(codesysExePath))
85
+ throw new Error(`CODESYS executable not found at provided path: ${codesysExePath}`);
86
+ // --- End Pre-checks ---
70
87
  const tempDir = os.tmpdir();
71
88
  const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).substring(2, 9)}.py`;
72
- const tempFilePath = path.join(tempDir, tempFileName);
89
+ const tempFilePath = path.join(tempDir, tempFileName); // Path module uses OS-specific separators ('\' on Windows)
73
90
  let output = '';
91
+ let stderrOutput = '';
74
92
  let success = false;
75
93
  let exitCode = null;
94
+ const codesysDir = path.dirname(codesysExePath); // Directory containing CODESYS.exe
76
95
  try {
77
- const normalizedScriptContent = scriptContent.replace(/\r\n/g, '\n');
78
- yield (0, promises_1.writeFile)(tempFilePath, normalizedScriptContent, 'latin1');
79
- process.stderr.write(`Temporary script written to: ${tempFilePath}\n`);
80
- // --- Force shell execution with explicit quoting for cmd.exe ---
81
- // Meticulously construct the command string exactly as cmd.exe needs it.
82
- // Rule 1: The executable path with spaces MUST be quoted.
83
- // Rule 2: Arguments containing spaces MUST be quoted (or potentially escaped, but quotes are safer).
84
- // Assuming profile name might have spaces, tempFilePath might too (less likely but possible).
85
- const commandStringForShell = `"${codesysExePath}" --profile="${codesysProfileName}" --noUI --runscript="${tempFilePath}"`;
86
- process.stderr.write(`Attempting to execute via shell: ${commandStringForShell}\n`); // Log the EXACT command
96
+ const normalizedScriptContent = scriptContent.replace(/\r\n/g, '\n'); // Normalize line endings
97
+ // <<< --- ADDED SCRIPT CONTENT LOGGING --- >>>
98
+ process.stderr.write(`INTEROP: Script content to be written (first 500 chars):\n`);
99
+ process.stderr.write(`------ START SCRIPT (TEMP FILE) -----\n`);
100
+ process.stderr.write(`${normalizedScriptContent.substring(0, 500)}\n`); // Log the first 500 chars
101
+ process.stderr.write(`------ END SCRIPT SNIPPET (TEMP FILE) -----\n`);
102
+ // <<< --- END SCRIPT CONTENT LOGGING --- >>>
103
+ yield (0, promises_1.writeFile)(tempFilePath, normalizedScriptContent, 'latin1'); // Write the normalized content
104
+ process.stderr.write(`INTEROP: Temp script written: ${tempFilePath}\n`);
105
+ // --- Construct command string for shell: true ---
106
+ // Quote the executable path itself
107
+ const quotedExePath = `"${codesysExePath}"`;
108
+ // Format arguments exactly as CODESYS seems to want: --option="Value With Spaces"
109
+ // The outer quotes are for the shell parser.
110
+ const profileArg = `--profile="${codesysProfileName}"`;
111
+ const scriptArg = `--runscript="${tempFilePath}"`; // tempFilePath from path.join has correct backslashes for Win
112
+ // Combine into a single string for the shell
113
+ const fullCommandString = `${quotedExePath} ${profileArg} --noUI ${scriptArg}`;
114
+ // Example result: "\"C:\\Program Files\\...\\CODESYS.exe\" --profile=\"CODESYS V3.5 SP21\" --noUI --runscript=\"C:\\Users\\...\\script.py\""
115
+ // --- End command string construction ---
116
+ process.stderr.write(`INTEROP: Spawning command (shell:true): ${fullCommandString}\n`);
117
+ process.stderr.write(`INTEROP ENV: CWD before spawn: ${process.cwd()}\n`);
118
+ process.stderr.write(`INTEROP ENV: Forcing CWD for spawn: ${codesysDir}\n`); // Re-enabled CWD change
119
+ // --- Create modified environment (Re-enabled) ---
120
+ const spawnEnv = Object.assign({}, process.env);
121
+ const pathSeparator = ';'; // Windows
122
+ const originalPath = spawnEnv.PATH || '';
123
+ spawnEnv.PATH = `${codesysDir}${pathSeparator}${originalPath}`; // Prepend CODESYS dir to PATH
124
+ process.stderr.write(`INTEROP ENV: MODIFIED PATH for spawn (prepended): ${spawnEnv.PATH.substring(0, 100)}...\n`); // Re-enabled ENV change
125
+ // --- End modified environment ---
87
126
  const spawnResult = yield new Promise((resolve) => {
88
127
  let stdoutData = '';
89
128
  let stderrData = '';
90
129
  const controller = new AbortController();
91
130
  const timeoutSignal = controller.signal;
92
- const timeoutDuration = 30000; // 30 seconds
93
- // Use shell: true. Pass the command string directly.
94
- // On Windows, this uses cmd.exe.
95
- // Pass the command string as the first argument when shell is true and args is empty/undefined.
96
- const childProcess = (0, child_process_1.spawn)(commandStringForShell, [], {
97
- shell: true, // FORCE shell usage
98
- windowsHide: false,
99
- signal: timeoutSignal
131
+ const timeoutDuration = 60000; // 60 seconds
132
+ // Pass the single command string, empty args array, and shell: true
133
+ const childProcess = (0, child_process_1.spawn)(fullCommandString, [], {
134
+ windowsHide: true,
135
+ signal: timeoutSignal,
136
+ cwd: codesysDir, // Re-enabled CWD change
137
+ env: spawnEnv, // Re-enabled ENV change
138
+ shell: true // USE shell: true
100
139
  });
101
140
  const timeoutId = setTimeout(() => {
102
- process.stderr.write(`TIMEOUT: CODESYS process timed out after ${timeoutDuration / 1000} seconds\n`);
141
+ process.stderr.write('INTEROP: Process timeout reached.\n');
103
142
  controller.abort();
104
143
  }, timeoutDuration);
144
+ // --- Event Listeners (stdout, stderr, error, close, abort) ---
105
145
  childProcess.stdout.on('data', (data) => {
106
146
  const chunk = data.toString();
107
147
  stdoutData += chunk;
108
- process.stderr.write(`CODESYS stdout chunk: ${chunk.length > 100 ? chunk.substring(0, 100) + '...' : chunk}\n`);
148
+ process.stderr.write(`INTEROP stdout chunk: ${chunk.length > 50 ? chunk.substring(0, 50) + '...' : chunk}\n`);
109
149
  });
110
150
  childProcess.stderr.on('data', (data) => {
111
- // **** THIS IS CRITICAL - CAPTURE STDERR ****
112
151
  const chunk = data.toString();
113
152
  stderrData += chunk;
114
- process.stderr.write(`>>>> CODESYS stderr chunk: ${chunk}\n`); // Make stderr highly visible
153
+ // Check for specific error patterns
154
+ if (chunk.includes('--profile="profile name"')) {
155
+ process.stderr.write(`>>>> INTEROP STDERR DETECTED Profile Error Message: ${chunk}\n`);
156
+ }
157
+ else if (chunk.includes('is not recognized')) {
158
+ process.stderr.write(`>>>> INTEROP STDERR DETECTED 'not recognized' (shell issue?): ${chunk}\n`);
159
+ }
160
+ else if (chunk.includes('SyntaxErrorException')) {
161
+ process.stderr.write(`>>>> INTEROP STDERR DETECTED Syntax Error: ${chunk}\n`);
162
+ } // More specific check
163
+ else {
164
+ process.stderr.write(`INTEROP stderr chunk: ${chunk}\n`);
165
+ }
115
166
  });
116
- childProcess.on('error', (error) => {
167
+ childProcess.on('error', (spawnError) => {
117
168
  var _a;
118
169
  clearTimeout(timeoutId);
119
- process.stderr.write(`CODESYS spawn error (shell mode): ${error.message}\n`);
120
- resolve({ code: (_a = error.errno) !== null && _a !== void 0 ? _a : 1, output: stdoutData + stderrData + `\nSpawn Error: ${error.message}` });
170
+ process.stderr.write(`INTEROP SPAWN ERROR (shell:true): ${spawnError.message}\n`);
171
+ resolve({ code: (_a = spawnError.errno) !== null && _a !== void 0 ? _a : 1, stdout: stdoutData, stderr: stderrData, error: spawnError });
121
172
  });
122
173
  childProcess.on('close', (code) => {
123
174
  clearTimeout(timeoutId);
124
- process.stderr.write(`CODESYS process closed with code: ${code}\n`);
125
- const combinedOutput = stdoutData + stderrData;
126
- resolve({ code: code, output: combinedOutput });
175
+ process.stderr.write(`INTEROP: Process closed code: ${code}\n`);
176
+ resolve({ code: code, stdout: stdoutData, stderr: stderrData });
127
177
  });
128
178
  timeoutSignal.addEventListener('abort', () => {
129
- process.stderr.write('Abort signal received, killing process if still running.\n');
130
- // Kill logic remains the same
131
- if (!childProcess.killed)
132
- childProcess.kill('SIGTERM');
133
- setTimeout(() => { if (!childProcess.killed)
134
- childProcess.kill('SIGKILL'); }, 2000);
135
- resolve({ code: null, output: stdoutData + stderrData + "\nTIMEOUT: CODESYS process aborted due to timeout." });
179
+ process.stderr.write('INTEROP: Abort signal received, attempting to kill process.\n');
180
+ if (!childProcess.killed) {
181
+ if (!childProcess.kill('SIGTERM')) { // Try graceful termination first
182
+ process.stderr.write('INTEROP: SIGTERM failed, attempting SIGKILL in 2s.\n');
183
+ setTimeout(() => { if (!childProcess.killed)
184
+ childProcess.kill('SIGKILL'); }, 2000);
185
+ }
186
+ else {
187
+ process.stderr.write('INTEROP: SIGTERM sent.\n');
188
+ }
189
+ }
190
+ resolve({ code: null, stdout: stdoutData, stderr: stderrData + "\nTIMEOUT: Process aborted due to timeout." });
136
191
  }, { once: true });
192
+ // --- End Event Listeners ---
137
193
  });
138
- output = spawnResult.output;
194
+ output = spawnResult.stdout;
195
+ stderrOutput = spawnResult.stderr;
139
196
  exitCode = spawnResult.code;
140
- // Success determination logic remains the same...
141
- // [...]
142
- if (output.includes(SCRIPT_SUCCESS_MARKER)) {
143
- success = true;
197
+ // --- Success Determination Logic ---
198
+ success = false; // Assume failure unless proven otherwise
199
+ if (spawnResult.error) {
200
+ process.stderr.write(`INTEROP: Failure determined by spawn error: ${spawnResult.error.message}\n`);
201
+ if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
202
+ stderrOutput = `SCRIPT_ERROR: Spawn failed: ${spawnResult.error.message}\n${stderrOutput}`;
203
+ }
204
+ else if (stderrOutput.includes('is not recognized as an internal or external command')) {
205
+ process.stderr.write("INTEROP: Failure determined by 'not recognized' error in stderr (shell:true quoting issue likely).\n");
206
+ if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
207
+ stderrOutput = `SCRIPT_ERROR: Shell execution failed: ${stderrOutput}`;
144
208
  }
145
- else if (output.includes(SCRIPT_ERROR_MARKER)) {
146
- success = false;
209
+ else if (stderrOutput.includes('--profile="profile name"')) {
210
+ process.stderr.write("INTEROP: Failure determined by CODESYS profile error message in stderr.\n");
211
+ if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
212
+ stderrOutput = `SCRIPT_ERROR: ${stderrOutput}`;
213
+ }
214
+ else if (stderrOutput.includes('SyntaxErrorException')) { // Check for syntax error specifically
215
+ process.stderr.write("INTEROP: Failure determined by CODESYS Script Syntax Error.\n");
216
+ if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
217
+ stderrOutput = `SCRIPT_ERROR: ${stderrOutput}`; // Include the syntax error details
147
218
  }
148
219
  else {
149
- if (exitCode === 0) {
150
- console.warn("Warning: CODESYS script finished with exit code 0 but no SCRIPT_SUCCESS marker found.");
151
- success = !output.includes(SCRIPT_ERROR_MARKER); // Assume success if exit code 0 and no error marker
220
+ // No spawn error, no shell error, no profile error, no syntax error -> check markers/exit code
221
+ process.stderr.write(`INTEROP: Checking markers/exit code (Code: ${exitCode})...\n`);
222
+ if (output.includes(SCRIPT_SUCCESS_MARKER) || stderrOutput.includes(SCRIPT_SUCCESS_MARKER)) {
223
+ success = true;
224
+ process.stderr.write("INTEROP: Success determined by SUCCESS marker.\n");
225
+ }
226
+ else if (output.includes(SCRIPT_ERROR_MARKER) || stderrOutput.includes(SCRIPT_ERROR_MARKER)) {
227
+ success = false; // Explicit error marker found
228
+ process.stderr.write("INTEROP: Failure determined by ERROR marker.\n");
152
229
  }
153
230
  else {
154
- success = false;
155
- const exitMsg = exitCode !== null ? `exit code ${exitCode}` : 'signal or unknown reason';
156
- // Prepend error marker if not already present from script/stderr
157
- if (!output.includes(SCRIPT_ERROR_MARKER)) {
158
- output = `SCRIPT_ERROR: Process exited with ${exitMsg}. Check CODESYS stderr output above for details.\n${output}`;
231
+ // No markers found, rely solely on exit code
232
+ success = exitCode === 0;
233
+ if (success) {
234
+ process.stderr.write(`INTEROP: Success determined by exit code 0 (no markers found).\n`);
159
235
  }
160
236
  else {
161
- // Potentially add exit code info if not already part of the error marker message
162
- if (!output.includes(`exited with ${exitMsg}`)) {
163
- output += `\n(Process exited with ${exitMsg})`;
164
- }
165
- }
166
- // Handle timeout message addition if necessary (should be added by abort handler)
167
- if (exitCode === null && !output.includes("TIMEOUT:") && !output.includes("Spawn Error:")) {
168
- output += `\nProcess may have been killed externally or timed out without proper exit code reporting.`;
237
+ process.stderr.write(`INTEROP: Failure determined by non-zero exit code ${exitCode} (no markers found).\n`);
238
+ // Add generic failure message if stderr doesn't already contain SCRIPT_ERROR
239
+ if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
240
+ stderrOutput = `SCRIPT_ERROR: Process failed with exit code ${exitCode} (no markers found).\n${stderrOutput}`;
169
241
  }
170
242
  }
171
243
  }
244
+ // --- End Success Determination ---
172
245
  }
173
246
  catch (error) {
174
- process.stderr.write(`Error during script execution setup: ${error}\n`); // Catch errors *before* spawn
175
- output += `\nSCRIPT_ERROR: Failed to set up script execution process. ${error.message}`;
247
+ process.stderr.write(`INTEROP: Error during setup: ${error.message}\n${error.stack}\n`);
248
+ stderrOutput = `SCRIPT_ERROR: Failed during script execution setup: ${error.message}`;
176
249
  success = false;
177
250
  }
178
251
  finally {
179
- // Cleanup logic remains the same...
252
+ // Cleanup: Attempt to delete the temporary script file
180
253
  try {
181
254
  yield (0, promises_1.unlink)(tempFilePath);
182
- process.stderr.write(`Temporary script deleted: ${tempFilePath}\n`);
255
+ process.stderr.write(`INTEROP: Temp script deleted: ${tempFilePath}\n`);
183
256
  }
184
257
  catch (cleanupError) {
185
- process.stderr.write(`Failed to delete temporary script file ${tempFilePath}: ${cleanupError}\n`);
186
- if (success) { // Only add warning if main process didn't already fail
187
- output += `\nWARNING: Failed to delete temporary script file ${tempFilePath}. ${cleanupError.message}`;
188
- }
258
+ process.stderr.write(`INTEROP: Failed to delete temp file ${tempFilePath}: ${cleanupError.message}\n`);
259
+ if (success)
260
+ stderrOutput += `\nWARNING: Failed to delete temporary script file ${tempFilePath}. ${cleanupError.message}`;
189
261
  }
190
262
  }
191
- process.stderr.write(`Final script output received (Success: ${success}):\n---\n${output}\n---\n`);
192
- return { success, output };
263
+ // Final output processing
264
+ // Combine stderr and stdout only on failure to preserve clean success output
265
+ const finalOutput = success ? output : `${stderrOutput}\n${output}`.trim();
266
+ process.stderr.write(`INTEROP: Final Success: ${success}\n`);
267
+ process.stderr.write(`INTEROP: Final Output Length: ${finalOutput.length}\n---\n`);
268
+ return { success, output: finalOutput };
193
269
  });
194
270
  }