@codesys/mcp-toolkit 1.0.7 → 1.0.9

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"); // Still using spawn
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
+ * Tries using spawn with shell:true and explicit quoting for cmd.exe.
55
56
  *
56
57
  * @param scriptContent The Python script code to execute.
57
58
  * @param codesysExePath The full path to the CODESYS.exe executable.
@@ -60,69 +61,84 @@ const SCRIPT_ERROR_MARKER = 'SCRIPT_ERROR';
60
61
  */
61
62
  function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName) {
62
63
  return __awaiter(this, void 0, void 0, function* () {
63
- // Validate arguments passed to the function
64
64
  if (!codesysExePath) {
65
65
  throw new Error('CODESYS executable path was not provided to executeCodesysScript.');
66
66
  }
67
67
  if (!codesysProfileName) {
68
68
  throw new Error('CODESYS profile name was not provided to executeCodesysScript.');
69
69
  }
70
- // Create a temporary file for the script
71
70
  const tempDir = os.tmpdir();
72
- const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).substring(7)}.py`;
71
+ const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).substring(2, 9)}.py`;
73
72
  const tempFilePath = path.join(tempDir, tempFileName);
74
73
  let output = '';
75
74
  let success = false;
75
+ let exitCode = null;
76
76
  try {
77
- // Normalize line endings to LF (\n)
78
77
  const normalizedScriptContent = scriptContent.replace(/\r\n/g, '\n');
79
- // 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) => {
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
87
+ const spawnResult = yield new Promise((resolve) => {
88
+ let stdoutData = '';
89
+ let stderrData = '';
90
+ const controller = new AbortController();
91
+ 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
100
+ });
101
+ const timeoutId = setTimeout(() => {
102
+ process.stderr.write(`TIMEOUT: CODESYS process timed out after ${timeoutDuration / 1000} seconds\n`);
103
+ controller.abort();
104
+ }, timeoutDuration);
105
+ childProcess.stdout.on('data', (data) => {
106
+ const chunk = data.toString();
107
+ stdoutData += chunk;
108
+ process.stderr.write(`CODESYS stdout chunk: ${chunk.length > 100 ? chunk.substring(0, 100) + '...' : chunk}\n`);
109
+ });
110
+ childProcess.stderr.on('data', (data) => {
111
+ // **** THIS IS CRITICAL - CAPTURE STDERR ****
112
+ const chunk = data.toString();
113
+ stderrData += chunk;
114
+ process.stderr.write(`>>>> CODESYS stderr chunk: ${chunk}\n`); // Make stderr highly visible
115
+ });
116
+ childProcess.on('error', (error) => {
93
117
  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
- }
118
+ 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}` });
111
121
  });
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" });
122
+ childProcess.on('close', (code) => {
123
+ clearTimeout(timeoutId);
124
+ process.stderr.write(`CODESYS process closed with code: ${code}\n`);
125
+ const combinedOutput = stdoutData + stderrData;
126
+ resolve({ code: code, output: combinedOutput });
117
127
  });
128
+ 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." });
136
+ }, { once: true });
118
137
  });
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
123
- // 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.
138
+ output = spawnResult.output;
139
+ exitCode = spawnResult.code;
140
+ // Success determination logic remains the same...
141
+ // [...]
126
142
  if (output.includes(SCRIPT_SUCCESS_MARKER)) {
127
143
  success = true;
128
144
  }
@@ -130,48 +146,49 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
130
146
  success = false;
131
147
  }
132
148
  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
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
152
+ }
153
+ 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
137
157
  if (!output.includes(SCRIPT_ERROR_MARKER)) {
138
- output += `\nSCRIPT_ERROR: Process exited with non-zero code ${exitCode}. Check CODESYS stderr output above for details.`;
158
+ output = `SCRIPT_ERROR: Process exited with ${exitMsg}. Check CODESYS stderr output above for details.\n${output}`;
139
159
  }
140
160
  else {
141
- output += `\nSCRIPT_ERROR: Process exited with non-zero code ${exitCode}.`;
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
+ }
142
165
  }
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;
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.`;
151
169
  }
152
170
  }
153
171
  }
154
172
  }
155
173
  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}`;
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}`;
158
176
  success = false;
159
177
  }
160
178
  finally {
161
- // Clean up the temporary file
179
+ // Cleanup logic remains the same...
162
180
  try {
163
181
  yield (0, promises_1.unlink)(tempFilePath);
164
- process.stderr.write(`Temporary script deleted: ${tempFilePath}\n`); // Use process.stderr
182
+ process.stderr.write(`Temporary script deleted: ${tempFilePath}\n`);
165
183
  }
166
184
  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
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
170
187
  output += `\nWARNING: Failed to delete temporary script file ${tempFilePath}. ${cleanupError.message}`;
171
188
  }
172
189
  }
173
190
  }
174
- process.stderr.write(`Final script output received:\n---\n${output}\n---\n`); // Use process.stderr
175
- return { success, output }; // Revert return object
191
+ process.stderr.write(`Final script output received (Success: ${success}):\n---\n${output}\n---\n`);
192
+ return { success, output };
176
193
  });
177
194
  }
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.9",
4
4
  "main": "dist/server.js",
5
5
  "bin": "./dist/bin.js",
6
6
  "scripts": {