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:
parent
e0f873ce5d
commit
71ef81ca28
186
addon.py
186
addon.py
@ -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)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in server loop: {str(e)}")
|
||||||
|
if not self.running:
|
||||||
|
break
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
# Process existing connection
|
print("Server thread stopped")
|
||||||
if self.client:
|
|
||||||
|
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)}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user