@codesys/mcp-toolkit 1.0.8 → 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"); // Use spawn directly
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,7 +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
+ * Tries using spawn with shell:true and explicit quoting for cmd.exe.
56
56
  *
57
57
  * @param scriptContent The Python script code to execute.
58
58
  * @param codesysExePath The full path to the CODESYS.exe executable.
@@ -61,54 +61,46 @@ const SCRIPT_ERROR_MARKER = 'SCRIPT_ERROR';
61
61
  */
62
62
  function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName) {
63
63
  return __awaiter(this, void 0, void 0, function* () {
64
- // Validate arguments passed to the function
65
64
  if (!codesysExePath) {
66
65
  throw new Error('CODESYS executable path was not provided to executeCodesysScript.');
67
66
  }
68
67
  if (!codesysProfileName) {
69
68
  throw new Error('CODESYS profile name was not provided to executeCodesysScript.');
70
69
  }
71
- // Create a temporary file for the script
72
70
  const tempDir = os.tmpdir();
73
- // Ensure temp file name is safe for command line (though spawn handles args better)
74
71
  const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).substring(2, 9)}.py`;
75
72
  const tempFilePath = path.join(tempDir, tempFileName);
76
73
  let output = '';
77
74
  let success = false;
78
75
  let exitCode = null;
79
76
  try {
80
- // Normalize line endings to LF (\n)
81
77
  const normalizedScriptContent = scriptContent.replace(/\r\n/g, '\n');
82
- // Write the script content to the temporary file using latin1 encoding
83
78
  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) => {
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) => {
99
88
  let stdoutData = '';
100
89
  let stderrData = '';
101
- // Spawn the process. Detached:false is default. Timeout via AbortController recommended for spawn.
102
90
  const controller = new AbortController();
103
91
  const timeoutSignal = controller.signal;
104
92
  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
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
108
100
  });
109
101
  const timeoutId = setTimeout(() => {
110
102
  process.stderr.write(`TIMEOUT: CODESYS process timed out after ${timeoutDuration / 1000} seconds\n`);
111
- controller.abort(); // Abort the process via the signal
103
+ controller.abort();
112
104
  }, timeoutDuration);
113
105
  childProcess.stdout.on('data', (data) => {
114
106
  const chunk = data.toString();
@@ -116,49 +108,37 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
116
108
  process.stderr.write(`CODESYS stdout chunk: ${chunk.length > 100 ? chunk.substring(0, 100) + '...' : chunk}\n`);
117
109
  });
118
110
  childProcess.stderr.on('data', (data) => {
111
+ // **** THIS IS CRITICAL - CAPTURE STDERR ****
119
112
  const chunk = data.toString();
120
113
  stderrData += chunk;
121
- process.stderr.write(`CODESYS stderr chunk: ${chunk}\n`); // Log stderr more verbosely
114
+ process.stderr.write(`>>>> CODESYS stderr chunk: ${chunk}\n`); // Make stderr highly visible
122
115
  });
123
116
  childProcess.on('error', (error) => {
124
117
  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
118
+ clearTimeout(timeoutId);
119
+ process.stderr.write(`CODESYS spawn error (shell mode): ${error.message}\n`);
129
120
  resolve({ code: (_a = error.errno) !== null && _a !== void 0 ? _a : 1, output: stdoutData + stderrData + `\nSpawn Error: ${error.message}` });
130
121
  });
131
122
  childProcess.on('close', (code) => {
132
- clearTimeout(timeoutId); // Clear timeout on successful close
123
+ clearTimeout(timeoutId);
133
124
  process.stderr.write(`CODESYS process closed with code: ${code}\n`);
134
125
  const combinedOutput = stdoutData + stderrData;
135
126
  resolve({ code: code, output: combinedOutput });
136
127
  });
137
- // Handle the timeout signal specifically
138
128
  timeoutSignal.addEventListener('abort', () => {
139
129
  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.
130
+ // Kill logic remains the same
131
+ if (!childProcess.killed)
132
+ childProcess.kill('SIGTERM');
133
+ setTimeout(() => { if (!childProcess.killed)
134
+ childProcess.kill('SIGKILL'); }, 2000);
156
135
  resolve({ code: null, output: stdoutData + stderrData + "\nTIMEOUT: CODESYS process aborted due to timeout." });
157
- }, { once: true }); // Ensure listener is called only once
136
+ }, { once: true });
158
137
  });
159
138
  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
139
+ exitCode = spawnResult.code;
140
+ // Success determination logic remains the same...
141
+ // [...]
162
142
  if (output.includes(SCRIPT_SUCCESS_MARKER)) {
163
143
  success = true;
164
144
  }
@@ -166,7 +146,6 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
166
146
  success = false;
167
147
  }
168
148
  else {
169
- // Fallback logic (handle null exit code)
170
149
  if (exitCode === 0) {
171
150
  console.warn("Warning: CODESYS script finished with exit code 0 but no SCRIPT_SUCCESS marker found.");
172
151
  success = !output.includes(SCRIPT_ERROR_MARKER); // Assume success if exit code 0 and no error marker
@@ -174,37 +153,37 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
174
153
  else {
175
154
  success = false;
176
155
  const exitMsg = exitCode !== null ? `exit code ${exitCode}` : 'signal or unknown reason';
156
+ // Prepend error marker if not already present from script/stderr
177
157
  if (!output.includes(SCRIPT_ERROR_MARKER)) {
178
- output += `\nSCRIPT_ERROR: Process exited with ${exitMsg}. Check CODESYS stderr output above for details.`;
158
+ output = `SCRIPT_ERROR: Process exited with ${exitMsg}. Check CODESYS stderr output above for details.\n${output}`;
179
159
  }
180
160
  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
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
+ }
186
165
  }
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
166
+ // Handle timeout message addition if necessary (should be added by abort handler)
167
+ if (exitCode === null && !output.includes("TIMEOUT:") && !output.includes("Spawn Error:")) {
189
168
  output += `\nProcess may have been killed externally or timed out without proper exit code reporting.`;
190
169
  }
191
170
  }
192
171
  }
193
172
  }
194
173
  catch (error) {
195
- process.stderr.write(`Error executing CODESYS script: ${error}\n`);
174
+ process.stderr.write(`Error during script execution setup: ${error}\n`); // Catch errors *before* spawn
196
175
  output += `\nSCRIPT_ERROR: Failed to set up script execution process. ${error.message}`;
197
176
  success = false;
198
177
  }
199
178
  finally {
200
- // Clean up the temporary file
179
+ // Cleanup logic remains the same...
201
180
  try {
202
181
  yield (0, promises_1.unlink)(tempFilePath);
203
182
  process.stderr.write(`Temporary script deleted: ${tempFilePath}\n`);
204
183
  }
205
184
  catch (cleanupError) {
206
185
  process.stderr.write(`Failed to delete temporary script file ${tempFilePath}: ${cleanupError}\n`);
207
- if (success) {
186
+ if (success) { // Only add warning if main process didn't already fail
208
187
  output += `\nWARNING: Failed to delete temporary script file ${tempFilePath}. ${cleanupError.message}`;
209
188
  }
210
189
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codesys/mcp-toolkit",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "main": "dist/server.js",
5
5
  "bin": "./dist/bin.js",
6
6
  "scripts": {