fixed create_schematics timeout

This commit is contained in:
fariouche
2025-12-02 21:26:13 +01:00
parent c91cd45006
commit 1e557d5d84
4 changed files with 105 additions and 81 deletions

View File

@@ -1,5 +1,8 @@
from skip import Schematic from skip import Schematic
import os import os
import logging
logger = logging.getLogger('kicad_interface')
class SchematicManager: class SchematicManager:
"""Core schematic operations using kicad-skip""" """Core schematic operations using kicad-skip"""
@@ -33,21 +36,21 @@ class SchematicManager:
# For now, we'll just create the schematic. # For now, we'll just create the schematic.
pass # Placeholder for potential metadata handling pass # Placeholder for potential metadata handling
print(f"Created new schematic: {name}") logger.info(f"Created new schematic: {name}")
return sch return sch
@staticmethod @staticmethod
def load_schematic(file_path): def load_schematic(file_path):
"""Load an existing schematic""" """Load an existing schematic"""
if not os.path.exists(file_path): if not os.path.exists(file_path):
print(f"Error: Schematic file not found at {file_path}") logger.error(f"Schematic file not found at {file_path}")
return None return None
try: try:
sch = Schematic(file_path) sch = Schematic(file_path)
print(f"Loaded schematic from: {file_path}") logger.info(f"Loaded schematic from: {file_path}")
return sch return sch
except Exception as e: except Exception as e:
print(f"Error loading schematic from {file_path}: {e}") logger.error(f"Error loading schematic from {file_path}: {e}")
return None return None
@staticmethod @staticmethod
@@ -56,10 +59,10 @@ class SchematicManager:
try: try:
# kicad-skip uses write method, not save # kicad-skip uses write method, not save
schematic.write(file_path) schematic.write(file_path)
print(f"Saved schematic to: {file_path}") logger.info(f"Saved schematic to: {file_path}")
return True return True
except Exception as e: except Exception as e:
print(f"Error saving schematic to {file_path}: {e}") logger.error(f"Error saving schematic to {file_path}: {e}")
return False return False
@staticmethod @staticmethod
@@ -72,7 +75,7 @@ class SchematicManager:
"generator": schematic.generator, "generator": schematic.generator,
# Add other relevant properties if needed # Add other relevant properties if needed
} }
print("Extracted schematic metadata") logger.debug("Extracted schematic metadata")
return metadata return metadata
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -71,7 +71,7 @@ function setupGracefulShutdown(server: KiCADMcpServer) {
// Handle termination signals // Handle termination signals
process.on('SIGINT', async () => { process.on('SIGINT', async () => {
logger.info('Received SIGINT signal. Shutting down...'); logger.info('Received SIGINT signal. Shutting down...');
await shutdownServer(server); //await shutdownServer(server);
}); });
process.on('SIGTERM', async () => { process.on('SIGTERM', async () => {

View File

@@ -308,10 +308,10 @@ class KiCADServer {
console.error(`Using Python executable: ${pythonExe}`); console.error(`Using Python executable: ${pythonExe}`);
this.pythonProcess = spawn(pythonExe, [this.kicadScriptPath], { this.pythonProcess = spawn(pythonExe, [this.kicadScriptPath], {
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
env: { /*env: {
...process.env, ...process.env,
PYTHONPATH: 'C:/Program Files/KiCad/9.0/lib/python3/dist-packages' PYTHONPATH: 'C:/Program Files/KiCad/9.0/lib/python3/dist-packages'
} }*/
}); });
// Listen for process exit // Listen for process exit

View File

@@ -82,6 +82,8 @@ export class KiCADMcpServer {
private stdioTransport!: StdioServerTransport; private stdioTransport!: StdioServerTransport;
private requestQueue: Array<{ request: any, resolve: Function, reject: Function }> = []; private requestQueue: Array<{ request: any, resolve: Function, reject: Function }> = [];
private processingRequest = false; private processingRequest = false;
private responseBuffer: string = '';
private currentRequestHandler: { resolve: Function, reject: Function, timeoutHandle: NodeJS.Timeout } | null = null;
/** /**
* Constructor for the KiCAD MCP Server * Constructor for the KiCAD MCP Server
@@ -267,10 +269,10 @@ export class KiCADMcpServer {
} }
this.pythonProcess = spawn(pythonExe, [this.kicadScriptPath], { this.pythonProcess = spawn(pythonExe, [this.kicadScriptPath], {
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
env: { /*env: {
...process.env, ...process.env,
PYTHONPATH: process.env.PYTHONPATH || 'C:/Program Files/KiCad/9.0/lib/python3/dist-packages' PYTHONPATH: process.env.PYTHONPATH || 'C:/Program Files/KiCad/9.0/lib/python3/dist-packages'
} }*/
}); });
// Listen for process exit // Listen for process exit
@@ -290,7 +292,14 @@ export class KiCADMcpServer {
logger.error(`Python stderr: ${data.toString()}`); logger.error(`Python stderr: ${data.toString()}`);
}); });
} }
// Set up persistent stdout handler (instead of adding/removing per request)
if (this.pythonProcess.stdout) {
this.pythonProcess.stdout.on('data', (data: Buffer) => {
this.handlePythonResponse(data);
});
}
// Connect server to STDIO transport // Connect server to STDIO transport
logger.info('Connecting MCP server to STDIO transport...'); logger.info('Connecting MCP server to STDIO transport...');
try { try {
@@ -365,6 +374,64 @@ export class KiCADMcpServer {
}); });
} }
/**
* Handle incoming data from Python process stdout
* This is a persistent handler that processes all responses
*/
private handlePythonResponse(data: Buffer): void {
const chunk = data.toString();
logger.debug(`Received data chunk: ${chunk.length} bytes`);
this.responseBuffer += chunk;
// Try to parse complete JSON responses (may have multiple or partial)
this.tryParseResponse();
}
/**
* Try to parse a complete JSON response from the buffer
*/
private tryParseResponse(): void {
if (!this.currentRequestHandler) {
// No pending request, clear buffer if it has data (shouldn't happen)
if (this.responseBuffer.trim()) {
logger.warn(`Received data with no pending request: ${this.responseBuffer.substring(0, 100)}...`);
this.responseBuffer = '';
}
return;
}
try {
// Try to parse the response as JSON
const result = JSON.parse(this.responseBuffer);
// If we get here, we have a valid JSON response
logger.debug(`Completed KiCAD command with result: ${result.success ? 'success' : 'failure'}`);
// Clear the timeout since we got a response
if (this.currentRequestHandler.timeoutHandle) {
clearTimeout(this.currentRequestHandler.timeoutHandle);
}
// Get the handler before clearing
const handler = this.currentRequestHandler;
// Clear state
this.responseBuffer = '';
this.currentRequestHandler = null;
this.processingRequest = false;
// Resolve the promise with the result
handler.resolve(result);
// Process next request if any
setTimeout(() => this.processNextRequest(), 0);
} catch (e) {
// Not a complete JSON yet, keep collecting data
// This is normal for large responses that come in chunks
}
}
/** /**
* Process the next request in the queue * Process the next request in the queue
*/ */
@@ -373,102 +440,56 @@ export class KiCADMcpServer {
if (this.requestQueue.length === 0 || this.processingRequest) { if (this.requestQueue.length === 0 || this.processingRequest) {
return; return;
} }
// Set processing flag // Set processing flag
this.processingRequest = true; this.processingRequest = true;
// Get the next request // Get the next request
const { request, resolve, reject } = this.requestQueue.shift()!; const { request, resolve, reject } = this.requestQueue.shift()!;
try { try {
logger.debug(`Processing KiCAD command: ${request.command}`); logger.debug(`Processing KiCAD command: ${request.command}`);
// Format the command and parameters as JSON // Format the command and parameters as JSON
const requestStr = JSON.stringify(request); const requestStr = JSON.stringify(request);
// Set up response handling
let responseData = '';
let timeoutHandle: NodeJS.Timeout | null = null;
// Clear any previous listeners // Clear response buffer for new request
if (this.pythonProcess?.stdout) { this.responseBuffer = '';
this.pythonProcess.stdout.removeAllListeners('data');
this.pythonProcess.stdout.removeAllListeners('end');
}
// Set up new listeners
if (this.pythonProcess?.stdout) {
this.pythonProcess.stdout.on('data', (data: Buffer) => {
const chunk = data.toString();
logger.debug(`Received data chunk: ${chunk.length} bytes`);
responseData += chunk;
// Check if we have a complete response
try {
// Try to parse the response as JSON
const result = JSON.parse(responseData);
// If we get here, we have a valid JSON response
logger.debug(`Completed KiCAD command: ${request.command} with result: ${result.success ? 'success' : 'failure'}`);
// Clear the timeout since we got a response
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
// Reset processing flag
this.processingRequest = false;
// Process next request if any
setTimeout(() => this.processNextRequest(), 0);
// Clear listeners
if (this.pythonProcess?.stdout) {
this.pythonProcess.stdout.removeAllListeners('data');
this.pythonProcess.stdout.removeAllListeners('end');
}
// Resolve the promise with the result
resolve(result);
} catch (e) {
// Not a complete JSON yet, keep collecting data
}
});
}
// Set a timeout (use command-specific timeout or default) // Set a timeout (use command-specific timeout or default)
const timeoutDuration = request.timeout || 30000; const timeoutDuration = request.timeout || 30000;
timeoutHandle = setTimeout(() => { const timeoutHandle = setTimeout(() => {
logger.error(`Command timeout after ${timeoutDuration/1000}s: ${request.command}`); logger.error(`Command timeout after ${timeoutDuration/1000}s: ${request.command}`);
logger.error(`Buffer contents: ${this.responseBuffer.substring(0, 200)}...`);
// Clear listeners // Clear state
if (this.pythonProcess?.stdout) { this.responseBuffer = '';
this.pythonProcess.stdout.removeAllListeners('data'); this.currentRequestHandler = null;
this.pythonProcess.stdout.removeAllListeners('end');
}
// Reset processing flag
this.processingRequest = false; this.processingRequest = false;
// Process next request
setTimeout(() => this.processNextRequest(), 0);
// Reject the promise // Reject the promise
reject(new Error(`Command timeout after ${timeoutDuration/1000}s: ${request.command}`)); reject(new Error(`Command timeout after ${timeoutDuration/1000}s: ${request.command}`));
// Process next request
setTimeout(() => this.processNextRequest(), 0);
}, timeoutDuration); }, timeoutDuration);
// Store the current request handler
this.currentRequestHandler = { resolve, reject, timeoutHandle };
// Write the request to the Python process // Write the request to the Python process
logger.debug(`Sending request: ${requestStr}`); logger.debug(`Sending request: ${requestStr}`);
this.pythonProcess?.stdin?.write(requestStr + '\n'); this.pythonProcess?.stdin?.write(requestStr + '\n');
} catch (error) { } catch (error) {
logger.error(`Error processing request: ${error}`); logger.error(`Error processing request: ${error}`);
// Reset processing flag // Reset processing flag
this.processingRequest = false; this.processingRequest = false;
this.currentRequestHandler = null;
// Process next request // Process next request
setTimeout(() => this.processNextRequest(), 0); setTimeout(() => this.processNextRequest(), 0);
// Reject the promise // Reject the promise
reject(error); reject(error);
} }