Fix server stability with thread-based networking

Replace timer-based networking with more reliable thread-based implementation to prevent server freezes and timeouts. This fix addresses issues where the server would appear running but not process requests.
This commit is contained in:
embogomolov 2025-03-17 19:44:38 +03:00 committed by GitHub
parent e0f873ce5d
commit 71ef81ca28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

192
addon.py
View File

@ -4,20 +4,20 @@ import json
import threading import threading
import socket import socket
import time import time
import requests # Add this import for HTTP requests import requests
import tempfile # Add this import for temporary directories import tempfile
from bpy.props import StringProperty, IntProperty
import traceback import traceback
import os import os
import shutil import shutil
from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty
bl_info = { bl_info = {
"name": "Blender MCP", "name": "Blender MCP Fixed",
"author": "BlenderMCP", "author": "BlenderMCP",
"version": (0, 1), "version": (0, 2),
"blender": (3, 0, 0), "blender": (3, 0, 0),
"location": "View3D > Sidebar > BlenderMCP", "location": "View3D > Sidebar > BlenderMCP",
"description": "Connect Blender to Claude via MCP", "description": "Connect Blender to Claude via MCP (Fixed Version)",
"category": "Interface", "category": "Interface",
} }
@ -27,21 +27,27 @@ class BlenderMCPServer:
self.port = port self.port = port
self.running = False self.running = False
self.socket = None self.socket = None
self.client = None self.server_thread = None
self.command_queue = []
self.buffer = b'' # Add buffer for incomplete data
def start(self): def start(self):
if self.running:
print("Server is already running")
return
self.running = True self.running = True
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try: try:
# Create socket
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind((self.host, self.port)) self.socket.bind((self.host, self.port))
self.socket.listen(1) self.socket.listen(1)
self.socket.setblocking(False)
# Register the timer # Start server thread
bpy.app.timers.register(self._process_server, persistent=True) self.server_thread = threading.Thread(target=self._server_loop)
self.server_thread.daemon = True
self.server_thread.start()
print(f"BlenderMCP server started on {self.host}:{self.port}") print(f"BlenderMCP server started on {self.host}:{self.port}")
except Exception as e: except Exception as e:
print(f"Failed to start server: {str(e)}") print(f"Failed to start server: {str(e)}")
@ -49,79 +55,118 @@ class BlenderMCPServer:
def stop(self): def stop(self):
self.running = False self.running = False
if hasattr(bpy.app.timers, "unregister"):
if bpy.app.timers.is_registered(self._process_server): # Close socket
bpy.app.timers.unregister(self._process_server)
if self.socket: if self.socket:
self.socket.close() try:
if self.client: self.socket.close()
self.client.close() except:
self.socket = None pass
self.client = None self.socket = None
# Wait for thread to finish
if self.server_thread:
try:
if self.server_thread.is_alive():
self.server_thread.join(timeout=1.0)
except:
pass
self.server_thread = None
print("BlenderMCP server stopped") print("BlenderMCP server stopped")
def _process_server(self): def _server_loop(self):
"""Timer callback to process server operations""" """Main server loop in a separate thread"""
if not self.running: print("Server thread started")
return None # Unregister timer self.socket.settimeout(1.0) # Timeout to allow for stopping
try: while self.running:
# Accept new connections try:
if not self.client and self.socket: # Accept new connection
try: try:
self.client, address = self.socket.accept() client, address = self.socket.accept()
self.client.setblocking(False)
print(f"Connected to client: {address}") print(f"Connected to client: {address}")
except BlockingIOError:
pass # No connection waiting # Handle client in a separate thread
client_thread = threading.Thread(
target=self._handle_client,
args=(client,)
)
client_thread.daemon = True
client_thread.start()
except socket.timeout:
# Just check running condition
continue
except Exception as e: except Exception as e:
print(f"Error accepting connection: {str(e)}") print(f"Error accepting connection: {str(e)}")
time.sleep(0.5)
# Process existing connection except Exception as e:
if self.client: print(f"Error in server loop: {str(e)}")
if not self.running:
break
time.sleep(0.5)
print("Server thread stopped")
def _handle_client(self, client):
"""Handle connected client"""
print("Client handler started")
client.settimeout(None) # No timeout
buffer = b''
try:
while self.running:
# Receive data
try: try:
# Try to receive data data = client.recv(8192)
if not data:
print("Client disconnected")
break
buffer += data
try: try:
data = self.client.recv(8192) # Try to parse command
if data: command = json.loads(buffer.decode('utf-8'))
self.buffer += data buffer = b''
# Try to process complete messages
# Execute command in Blender's main thread
def execute_wrapper():
try: try:
# Attempt to parse the buffer as JSON
command = json.loads(self.buffer.decode('utf-8'))
# If successful, clear the buffer and process command
self.buffer = b''
response = self.execute_command(command) response = self.execute_command(command)
response_json = json.dumps(response) response_json = json.dumps(response)
self.client.sendall(response_json.encode('utf-8')) try:
except json.JSONDecodeError: client.sendall(response_json.encode('utf-8'))
# Incomplete data, keep in buffer except:
pass print("Failed to send response - client disconnected")
else: except Exception as e:
# Connection closed by client print(f"Error executing command: {str(e)}")
print("Client disconnected") traceback.print_exc()
self.client.close() try:
self.client = None error_response = {
self.buffer = b'' "status": "error",
except BlockingIOError: "message": str(e)
pass # No data available }
except Exception as e: client.sendall(json.dumps(error_response).encode('utf-8'))
print(f"Error receiving data: {str(e)}") except:
self.client.close() pass
self.client = None return None
self.buffer = b''
# Schedule execution in main thread
bpy.app.timers.register(execute_wrapper, first_interval=0.0)
except json.JSONDecodeError:
# Incomplete data, wait for more
pass
except Exception as e: except Exception as e:
print(f"Error with client: {str(e)}") print(f"Error receiving data: {str(e)}")
if self.client: break
self.client.close()
self.client = None
self.buffer = b''
except Exception as e: except Exception as e:
print(f"Server error: {str(e)}") print(f"Error in client handler: {str(e)}")
finally:
return 0.1 # Continue timer with 0.1 second interval try:
client.close()
except:
pass
print("Client handler stopped")
def execute_command(self, command): def execute_command(self, command):
"""Execute a command in the main Blender thread""" """Execute a command in the main Blender thread"""
@ -140,7 +185,6 @@ class BlenderMCPServer:
except Exception as e: except Exception as e:
print(f"Error executing command: {str(e)}") print(f"Error executing command: {str(e)}")
traceback.print_exc() traceback.print_exc()
return {"status": "error", "message": str(e)} return {"status": "error", "message": str(e)}