@codesys/mcp-toolkit 1.0.8 → 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);
@@ -47,169 +59,177 @@ 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
- * Uses spawn for better handling of paths with spaces.
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
- // Validate arguments passed to the function
65
- if (!codesysExePath) {
66
- throw new Error('CODESYS executable path was not provided to executeCodesysScript.');
67
- }
68
- if (!codesysProfileName) {
69
- throw new Error('CODESYS profile name was not provided to executeCodesysScript.');
70
- }
71
- // Create a temporary file for the script
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 ---
72
87
  const tempDir = os.tmpdir();
73
- // Ensure temp file name is safe for command line (though spawn handles args better)
74
88
  const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).substring(2, 9)}.py`;
75
89
  const tempFilePath = path.join(tempDir, tempFileName);
76
90
  let output = '';
91
+ let stderrOutput = '';
77
92
  let success = false;
78
93
  let exitCode = null;
94
+ const codesysDir = path.dirname(codesysExePath); // Directory containing CODESYS.exe
79
95
  try {
80
- // Normalize line endings to LF (\n)
81
96
  const normalizedScriptContent = scriptContent.replace(/\r\n/g, '\n');
82
- // Write the script content to the temporary file using latin1 encoding
83
97
  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
98
+ process.stderr.write(`INTEROP: Temp script written: ${tempFilePath}\n`);
99
+ // --- Use spawn arguments array ---
100
+ const command = codesysExePath;
87
101
  const args = [
88
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}"`
89
105
  '--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.
106
+ `--runscript=${tempFilePath}`
107
+ // If CODESYS needs quotes around the value *itself*: `--runscript="${tempFilePath}"`
96
108
  ];
97
- process.stderr.write(`Spawning: ${command} with args: ${JSON.stringify(args)}\n`);
98
- const spawnResult = yield new Promise((resolve, reject) => {
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 ---
121
+ const spawnResult = yield new Promise((resolve) => {
99
122
  let stdoutData = '';
100
123
  let stderrData = '';
101
- // Spawn the process. Detached:false is default. Timeout via AbortController recommended for spawn.
102
124
  const controller = new AbortController();
103
125
  const timeoutSignal = controller.signal;
104
- const timeoutDuration = 30000; // 30 seconds
126
+ const timeoutDuration = 60000; // 60 seconds
105
127
  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
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)
108
133
  });
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);
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)
113
137
  childProcess.stdout.on('data', (data) => {
114
138
  const chunk = data.toString();
115
139
  stdoutData += chunk;
116
- 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`);
117
141
  });
118
142
  childProcess.stderr.on('data', (data) => {
119
143
  const chunk = data.toString();
120
144
  stderrData += chunk;
121
- process.stderr.write(`CODESYS stderr chunk: ${chunk}\n`); // Log stderr more verbosely
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
+ }
122
151
  });
123
- childProcess.on('error', (error) => {
152
+ childProcess.on('error', (spawnError) => {
124
153
  var _a;
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}` });
154
+ clearTimeout(timeoutId);
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 });
130
157
  });
131
158
  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 });
159
+ clearTimeout(timeoutId);
160
+ process.stderr.write(`INTEROP: Process closed code: ${code}\n`);
161
+ resolve({ code: code, stdout: stdoutData, stderr: stderrData });
136
162
  });
137
- // Handle the timeout signal specifically
138
163
  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
164
+ process.stderr.write('INTEROP: Abort signal received, killing process.\n');
165
+ if (!childProcess.killed)
166
+ childProcess.kill('SIGTERM');
167
+ setTimeout(() => { if (!childProcess.killed)
168
+ childProcess.kill('SIGKILL'); }, 2000);
169
+ resolve({ code: null, stdout: stdoutData, stderr: stderrData + "\nTIMEOUT: Process aborted due to timeout." });
170
+ }, { once: true });
171
+ // --- End Event Listeners ---
158
172
  });
159
- output = spawnResult.output;
160
- exitCode = spawnResult.code; // Can be null if process killed before exit code retrieved
161
- // Determine success based on exit code and output markers
162
- if (output.includes(SCRIPT_SUCCESS_MARKER)) {
163
- success = true;
173
+ output = spawnResult.stdout;
174
+ stderrOutput = spawnResult.stderr;
175
+ exitCode = spawnResult.code;
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}`;
164
182
  }
165
- else if (output.includes(SCRIPT_ERROR_MARKER)) {
183
+ else if (spawnResult.error) {
166
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}`;
167
188
  }
168
189
  else {
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
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");
173
194
  }
174
- else {
195
+ else if (output.includes(SCRIPT_ERROR_MARKER) || stderrOutput.includes(SCRIPT_ERROR_MARKER)) {
175
196
  success = false;
176
- const exitMsg = exitCode !== null ? `exit code ${exitCode}` : 'signal or unknown reason';
177
- if (!output.includes(SCRIPT_ERROR_MARKER)) {
178
- output += `\nSCRIPT_ERROR: Process exited with ${exitMsg}. Check CODESYS stderr output above for details.`;
179
- }
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`);
180
203
  else {
181
- output += `\nSCRIPT_ERROR: Process exited with ${exitMsg}.`;
182
- }
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.`;
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}`;
190
207
  }
191
208
  }
192
209
  }
210
+ // --- End Success Determination ---
193
211
  }
194
212
  catch (error) {
195
- process.stderr.write(`Error executing CODESYS script: ${error}\n`);
196
- 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}`;
197
215
  success = false;
198
216
  }
199
217
  finally {
200
- // Clean up the temporary file
218
+ // Cleanup: Attempt to delete the temporary script file
201
219
  try {
202
220
  yield (0, promises_1.unlink)(tempFilePath);
203
- process.stderr.write(`Temporary script deleted: ${tempFilePath}\n`);
221
+ process.stderr.write(`INTEROP: Temp script deleted: ${tempFilePath}\n`);
204
222
  }
205
223
  catch (cleanupError) {
206
- process.stderr.write(`Failed to delete temporary script file ${tempFilePath}: ${cleanupError}\n`);
207
- if (success) {
208
- output += `\nWARNING: Failed to delete temporary script file ${tempFilePath}. ${cleanupError.message}`;
209
- }
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}`;
210
227
  }
211
228
  }
212
- process.stderr.write(`Final script output received (Success: ${success}):\n---\n${output}\n---\n`);
213
- 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 };
214
234
  });
215
235
  }