@codesys/mcp-toolkit 1.0.9 → 1.1.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.
@@ -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,181 @@ 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"); // Use spawn directly
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: This function uses spawn WITHOUT shell:true for robustness with Windows paths containing spaces.
75
+ * The approach uses modified environment variables and working directory to ensure proper execution.
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
89
  const tempFilePath = path.join(tempDir, tempFileName);
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
96
  const normalizedScriptContent = scriptContent.replace(/\r\n/g, '\n');
78
97
  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
98
+ process.stderr.write(`INTEROP: Temp script written: ${tempFilePath}\n`);
99
+ // --- Use spawn arguments array ---
100
+ const command = codesysExePath;
101
+ const args = [
102
+ `--profile=${codesysProfileName}`, // Format expected by CODESYS.exe
103
+ // Use = for profile? Or quotes? Check CODESYS docs. Assume '=' for now.
104
+ // If CODESYS needs quotes around the value *itself*: `--profile="${codesysProfileName}"`
105
+ '--noUI',
106
+ `--runscript=${tempFilePath}`
107
+ // If CODESYS needs quotes around the value *itself*: `--runscript="${tempFilePath}"`
108
+ ];
109
+ // --- End spawn arguments ---
110
+ process.stderr.write(`INTEROP: Spawning command: ${command}\n`);
111
+ process.stderr.write(`INTEROP: Spawning args: ${JSON.stringify(args)}\n`);
112
+ process.stderr.write(`INTEROP ENV: CWD before spawn: ${process.cwd()}\n`);
113
+ process.stderr.write(`INTEROP ENV: Forcing CWD for spawn: ${codesysDir}\n`);
114
+ // --- Create modified environment ---
115
+ const spawnEnv = Object.assign({}, process.env);
116
+ const pathSeparator = ';'; // Windows
117
+ const originalPath = spawnEnv.PATH || '';
118
+ spawnEnv.PATH = `${codesysDir}${pathSeparator}${originalPath}`;
119
+ process.stderr.write(`INTEROP ENV: MODIFIED PATH for spawn (prepended): ${spawnEnv.PATH.substring(0, 100)}...\n`);
120
+ // --- End modified environment ---
87
121
  const spawnResult = yield new Promise((resolve) => {
88
122
  let stdoutData = '';
89
123
  let stderrData = '';
90
124
  const controller = new AbortController();
91
125
  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
126
+ const timeoutDuration = 60000; // 60 seconds
127
+ const childProcess = (0, child_process_1.spawn)(command, args, {
128
+ windowsHide: true,
129
+ signal: timeoutSignal,
130
+ cwd: codesysDir, // Force CWD
131
+ env: spawnEnv, // Pass modified environment
132
+ shell: false // <<< Ensure shell is false or omitted (default)
100
133
  });
101
- const timeoutId = setTimeout(() => {
102
- process.stderr.write(`TIMEOUT: CODESYS process timed out after ${timeoutDuration / 1000} seconds\n`);
103
- controller.abort();
104
- }, timeoutDuration);
134
+ const timeoutId = setTimeout(() => { }, timeoutDuration);
135
+ // --- Event Listeners (stdout, stderr, error, close, abort) ---
136
+ // (Keep the event listener logic the same as the previous version)
105
137
  childProcess.stdout.on('data', (data) => {
106
138
  const chunk = data.toString();
107
139
  stdoutData += chunk;
108
- process.stderr.write(`CODESYS stdout chunk: ${chunk.length > 100 ? chunk.substring(0, 100) + '...' : chunk}\n`);
140
+ process.stderr.write(`INTEROP stdout chunk: ${chunk.length > 50 ? chunk.substring(0, 50) + '...' : chunk}\n`);
109
141
  });
110
142
  childProcess.stderr.on('data', (data) => {
111
- // **** THIS IS CRITICAL - CAPTURE STDERR ****
112
143
  const chunk = data.toString();
113
144
  stderrData += chunk;
114
- process.stderr.write(`>>>> CODESYS stderr chunk: ${chunk}\n`); // Make stderr highly visible
145
+ if (chunk.includes('C:\\Program')) {
146
+ process.stderr.write(`>>>> INTEROP STDERR DETECTED 'C:\\Program': ${chunk}\n`);
147
+ }
148
+ else {
149
+ process.stderr.write(`INTEROP stderr chunk: ${chunk}\n`);
150
+ }
115
151
  });
116
- childProcess.on('error', (error) => {
152
+ childProcess.on('error', (spawnError) => {
117
153
  var _a;
118
154
  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}` });
155
+ process.stderr.write(`INTEROP SPAWN ERROR (shell:false): ${spawnError.message}\n`);
156
+ resolve({ code: (_a = spawnError.errno) !== null && _a !== void 0 ? _a : 1, stdout: stdoutData, stderr: stderrData, error: spawnError });
121
157
  });
122
158
  childProcess.on('close', (code) => {
123
159
  clearTimeout(timeoutId);
124
- process.stderr.write(`CODESYS process closed with code: ${code}\n`);
125
- const combinedOutput = stdoutData + stderrData;
126
- resolve({ code: code, output: combinedOutput });
160
+ process.stderr.write(`INTEROP: Process closed code: ${code}\n`);
161
+ resolve({ code: code, stdout: stdoutData, stderr: stderrData });
127
162
  });
128
163
  timeoutSignal.addEventListener('abort', () => {
129
- process.stderr.write('Abort signal received, killing process if still running.\n');
130
- // Kill logic remains the same
164
+ process.stderr.write('INTEROP: Abort signal received, killing process.\n');
131
165
  if (!childProcess.killed)
132
166
  childProcess.kill('SIGTERM');
133
167
  setTimeout(() => { if (!childProcess.killed)
134
168
  childProcess.kill('SIGKILL'); }, 2000);
135
- resolve({ code: null, output: stdoutData + stderrData + "\nTIMEOUT: CODESYS process aborted due to timeout." });
169
+ resolve({ code: null, stdout: stdoutData, stderr: stderrData + "\nTIMEOUT: Process aborted due to timeout." });
136
170
  }, { once: true });
171
+ // --- End Event Listeners ---
137
172
  });
138
- output = spawnResult.output;
173
+ output = spawnResult.stdout;
174
+ stderrOutput = spawnResult.stderr;
139
175
  exitCode = spawnResult.code;
140
- // Success determination logic remains the same...
141
- // [...]
142
- if (output.includes(SCRIPT_SUCCESS_MARKER)) {
143
- success = true;
176
+ // success determination logic
177
+ if (stderrOutput.includes("'C:\\Program' is not recognized")) {
178
+ success = false;
179
+ process.stderr.write("INTEROP: Failure determined by 'C:\\Program' error in stderr.\n");
180
+ if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
181
+ stderrOutput = `SCRIPT_ERROR: ${stderrOutput}`;
144
182
  }
145
- else if (output.includes(SCRIPT_ERROR_MARKER)) {
183
+ else if (spawnResult.error) {
146
184
  success = false;
185
+ process.stderr.write(`INTEROP: Failure determined by spawn error: ${spawnResult.error.message}\n`);
186
+ if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
187
+ stderrOutput = `SCRIPT_ERROR: Spawn failed: ${spawnResult.error.message}\n${stderrOutput}`;
147
188
  }
148
189
  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
190
+ process.stderr.write(`INTEROP: Checking markers/exit code (Code: ${exitCode})...\n`);
191
+ if (output.includes(SCRIPT_SUCCESS_MARKER) || stderrOutput.includes(SCRIPT_SUCCESS_MARKER)) {
192
+ success = true;
193
+ process.stderr.write("INTEROP: Success determined by SUCCESS marker.\n");
152
194
  }
153
- else {
195
+ else if (output.includes(SCRIPT_ERROR_MARKER) || stderrOutput.includes(SCRIPT_ERROR_MARKER)) {
154
196
  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}`;
159
- }
197
+ process.stderr.write("INTEROP: Failure determined by ERROR marker.\n");
198
+ }
199
+ else {
200
+ success = exitCode === 0;
201
+ if (success)
202
+ process.stderr.write(`INTEROP: Success determined by exit code 0 (no markers found).\n`);
160
203
  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.`;
204
+ process.stderr.write(`INTEROP: Failure determined by non-zero exit code ${exitCode} (no markers found).\n`);
205
+ if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
206
+ stderrOutput = `SCRIPT_ERROR: Process failed with exit code ${exitCode} (no markers found).\n${stderrOutput}`;
169
207
  }
170
208
  }
171
209
  }
210
+ // --- End Success Determination ---
172
211
  }
173
212
  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}`;
213
+ process.stderr.write(`INTEROP: Error during setup: ${error.message}\n${error.stack}\n`);
214
+ stderrOutput = `SCRIPT_ERROR: Failed during script execution setup: ${error.message}`;
176
215
  success = false;
177
216
  }
178
217
  finally {
179
- // Cleanup logic remains the same...
218
+ // Cleanup: Attempt to delete the temporary script file
180
219
  try {
181
220
  yield (0, promises_1.unlink)(tempFilePath);
182
- process.stderr.write(`Temporary script deleted: ${tempFilePath}\n`);
221
+ process.stderr.write(`INTEROP: Temp script deleted: ${tempFilePath}\n`);
183
222
  }
184
223
  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
- }
224
+ process.stderr.write(`INTEROP: Failed to delete temp file ${tempFilePath}: ${cleanupError.message}\n`);
225
+ if (success)
226
+ stderrOutput += `\nWARNING: Failed to delete temporary script file ${tempFilePath}. ${cleanupError.message}`;
189
227
  }
190
228
  }
191
- process.stderr.write(`Final script output received (Success: ${success}):\n---\n${output}\n---\n`);
192
- return { success, output };
229
+ // Final output processing
230
+ const finalOutput = success ? output : `${stderrOutput}\n${output}`.trim();
231
+ process.stderr.write(`INTEROP: Final Success: ${success}\n`);
232
+ process.stderr.write(`INTEROP: Final Output Length: ${finalOutput.length}\n---\n`);
233
+ return { success, output: finalOutput };
193
234
  });
194
235
  }