Skip to content

Commit 32b35c9

Browse files
committed
Mirror shutdown improvements: signal handlers, stdin/parent monitor, guarded exit timers, and os._exit force-exit in UnityMcpServer~ entry points
1 parent dfd1238 commit 32b35c9

File tree

2 files changed

+198
-2
lines changed

2 files changed

+198
-2
lines changed

MCPForUnity/UnityMcpServer~/src/server.py

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
from logging.handlers import RotatingFileHandler
55
import os
66
from contextlib import asynccontextmanager
7+
import signal
8+
import sys
9+
import threading
710
from typing import AsyncIterator, Dict, Any
811
from config import config
912
from tools import register_all_tools
@@ -64,6 +67,10 @@
6467
# Global connection state
6568
_unity_connection: UnityConnection = None
6669

70+
# Global shutdown coordination
71+
_shutdown_flag = threading.Event()
72+
_exit_timer_scheduled = threading.Event()
73+
6774

6875
@asynccontextmanager
6976
async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
@@ -186,9 +193,98 @@ def _emit_startup():
186193
register_all_resources(mcp)
187194

188195

196+
def _force_exit(code: int = 0):
197+
"""Force process exit, bypassing any background threads that might linger."""
198+
os._exit(code)
199+
200+
201+
def _signal_handler(signum, frame):
202+
logger.info(f"Received signal {signum}, initiating shutdown...")
203+
_shutdown_flag.set()
204+
if not _exit_timer_scheduled.is_set():
205+
_exit_timer_scheduled.set()
206+
threading.Timer(1.0, _force_exit, args=(0,)).start()
207+
208+
209+
def _monitor_stdin():
210+
"""Background thread to detect stdio detach (stdin EOF) or parent exit."""
211+
try:
212+
parent_pid = os.getppid() if hasattr(os, "getppid") else None
213+
while not _shutdown_flag.is_set():
214+
if _shutdown_flag.wait(0.5):
215+
break
216+
217+
if parent_pid is not None:
218+
try:
219+
os.kill(parent_pid, 0)
220+
except ValueError:
221+
# Signal 0 unsupported on this platform (e.g., Windows); disable parent probing
222+
parent_pid = None
223+
except (ProcessLookupError, OSError):
224+
logger.info(f"Parent process {parent_pid} no longer exists; shutting down")
225+
break
226+
227+
try:
228+
if sys.stdin.closed:
229+
logger.info("stdin.closed is True; client disconnected")
230+
break
231+
fd = sys.stdin.fileno()
232+
if fd < 0:
233+
logger.info("stdin fd invalid; client disconnected")
234+
break
235+
except (ValueError, OSError, AttributeError):
236+
# Closed pipe or unavailable stdin
237+
break
238+
except Exception:
239+
# Ignore transient errors
240+
logger.debug("Transient error checking stdin", exc_info=True)
241+
242+
if not _shutdown_flag.is_set():
243+
logger.info("Client disconnected (stdin or parent), initiating shutdown...")
244+
_shutdown_flag.set()
245+
if not _exit_timer_scheduled.is_set():
246+
_exit_timer_scheduled.set()
247+
threading.Timer(0.5, _force_exit, args=(0,)).start()
248+
except Exception:
249+
# Never let monitor thread crash the process
250+
logger.debug("Monitor thread error", exc_info=True)
251+
252+
189253
def main():
190254
"""Entry point for uvx and console scripts."""
191-
mcp.run(transport='stdio')
255+
try:
256+
signal.signal(signal.SIGTERM, _signal_handler)
257+
signal.signal(signal.SIGINT, _signal_handler)
258+
if hasattr(signal, "SIGPIPE"):
259+
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
260+
if hasattr(signal, "SIGBREAK"):
261+
signal.signal(signal.SIGBREAK, _signal_handler)
262+
except Exception:
263+
# Signals can fail in some environments
264+
pass
265+
266+
t = threading.Thread(target=_monitor_stdin, daemon=True)
267+
t.start()
268+
269+
try:
270+
mcp.run(transport='stdio')
271+
logger.info("FastMCP run() returned (stdin EOF or disconnect)")
272+
except (KeyboardInterrupt, SystemExit):
273+
logger.info("Server interrupted; shutting down")
274+
_shutdown_flag.set()
275+
except BrokenPipeError:
276+
logger.info("Broken pipe; shutting down")
277+
_shutdown_flag.set()
278+
except Exception as e:
279+
logger.error(f"Server error: {e}", exc_info=True)
280+
_shutdown_flag.set()
281+
_force_exit(1)
282+
finally:
283+
_shutdown_flag.set()
284+
logger.info("Server main loop exited")
285+
if not _exit_timer_scheduled.is_set():
286+
_exit_timer_scheduled.set()
287+
threading.Timer(0.5, _force_exit, args=(0,)).start()
192288

193289

194290
# Run the server

UnityMcpBridge/UnityMcpServer~/src/server.py

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
from logging.handlers import RotatingFileHandler
55
import os
66
from contextlib import asynccontextmanager
7+
import signal
8+
import sys
9+
import threading
710
from typing import AsyncIterator, Dict, Any
811
from config import config
912
from tools import register_all_tools
@@ -63,6 +66,10 @@
6366
# Global connection state
6467
_unity_connection: UnityConnection = None
6568

69+
# Global shutdown coordination
70+
_shutdown_flag = threading.Event()
71+
_exit_timer_scheduled = threading.Event()
72+
6673

6774
@asynccontextmanager
6875
async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
@@ -189,6 +196,99 @@ def asset_creation_strategy() -> str:
189196
)
190197

191198

199+
def _force_exit(code: int = 0):
200+
"""Force process exit, bypassing any background threads that might linger."""
201+
os._exit(code)
202+
203+
204+
def _signal_handler(signum, frame):
205+
logger.info(f"Received signal {signum}, initiating shutdown...")
206+
_shutdown_flag.set()
207+
if not _exit_timer_scheduled.is_set():
208+
_exit_timer_scheduled.set()
209+
threading.Timer(1.0, _force_exit, args=(0,)).start()
210+
211+
212+
def _monitor_stdin():
213+
"""Background thread to detect stdio detach (stdin EOF) or parent exit."""
214+
try:
215+
parent_pid = os.getppid() if hasattr(os, "getppid") else None
216+
while not _shutdown_flag.is_set():
217+
if _shutdown_flag.wait(0.5):
218+
break
219+
220+
if parent_pid is not None:
221+
try:
222+
os.kill(parent_pid, 0)
223+
except ValueError:
224+
# Signal 0 unsupported on this platform (e.g., Windows); disable parent probing
225+
parent_pid = None
226+
except (ProcessLookupError, OSError):
227+
logger.info(f"Parent process {parent_pid} no longer exists; shutting down")
228+
break
229+
230+
try:
231+
if sys.stdin.closed:
232+
logger.info("stdin.closed is True; client disconnected")
233+
break
234+
fd = sys.stdin.fileno()
235+
if fd < 0:
236+
logger.info("stdin fd invalid; client disconnected")
237+
break
238+
except (ValueError, OSError, AttributeError):
239+
# Closed pipe or unavailable stdin
240+
break
241+
except Exception:
242+
# Ignore transient errors
243+
logger.debug("Transient error checking stdin", exc_info=True)
244+
245+
if not _shutdown_flag.is_set():
246+
logger.info("Client disconnected (stdin or parent), initiating shutdown...")
247+
_shutdown_flag.set()
248+
if not _exit_timer_scheduled.is_set():
249+
_exit_timer_scheduled.set()
250+
threading.Timer(0.5, _force_exit, args=(0,)).start()
251+
except Exception:
252+
# Never let monitor thread crash the process
253+
logger.debug("Monitor thread error", exc_info=True)
254+
255+
256+
def main():
257+
try:
258+
signal.signal(signal.SIGTERM, _signal_handler)
259+
signal.signal(signal.SIGINT, _signal_handler)
260+
if hasattr(signal, "SIGPIPE"):
261+
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
262+
if hasattr(signal, "SIGBREAK"):
263+
signal.signal(signal.SIGBREAK, _signal_handler)
264+
except Exception:
265+
# Signals can fail in some environments
266+
pass
267+
268+
t = threading.Thread(target=_monitor_stdin, daemon=True)
269+
t.start()
270+
271+
try:
272+
mcp.run(transport='stdio')
273+
logger.info("FastMCP run() returned (stdin EOF or disconnect)")
274+
except (KeyboardInterrupt, SystemExit):
275+
logger.info("Server interrupted; shutting down")
276+
_shutdown_flag.set()
277+
except BrokenPipeError:
278+
logger.info("Broken pipe; shutting down")
279+
_shutdown_flag.set()
280+
except Exception as e:
281+
logger.error(f"Server error: {e}", exc_info=True)
282+
_shutdown_flag.set()
283+
_force_exit(1)
284+
finally:
285+
_shutdown_flag.set()
286+
logger.info("Server main loop exited")
287+
if not _exit_timer_scheduled.is_set():
288+
_exit_timer_scheduled.set()
289+
threading.Timer(0.5, _force_exit, args=(0,)).start()
290+
291+
192292
# Run the server
193293
if __name__ == "__main__":
194-
mcp.run(transport='stdio')
294+
main()

0 commit comments

Comments
 (0)