claude-mpm 4.2.9__py3-none-any.whl → 4.2.12__py3-none-any.whl
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.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/dashboard.py +59 -126
- claude_mpm/cli/commands/monitor.py +82 -212
- claude_mpm/cli/commands/run.py +33 -33
- claude_mpm/cli/parsers/monitor_parser.py +12 -2
- claude_mpm/dashboard/static/css/code-tree.css +8 -16
- claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
- claude_mpm/dashboard/static/dist/components/file-viewer.js +2 -0
- claude_mpm/dashboard/static/dist/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/unified-data-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/code-tree.js +692 -114
- claude_mpm/dashboard/static/js/components/file-viewer.js +538 -0
- claude_mpm/dashboard/static/js/components/module-viewer.js +26 -0
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +166 -14
- claude_mpm/dashboard/static/js/dashboard.js +108 -91
- claude_mpm/dashboard/static/js/socket-client.js +9 -7
- claude_mpm/dashboard/templates/index.html +2 -7
- claude_mpm/hooks/claude_hooks/hook_handler.py +1 -11
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +54 -59
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +112 -72
- claude_mpm/services/agents/deployment/agent_template_builder.py +0 -1
- claude_mpm/services/cli/unified_dashboard_manager.py +354 -0
- claude_mpm/services/monitor/__init__.py +20 -0
- claude_mpm/services/monitor/daemon.py +378 -0
- claude_mpm/services/monitor/event_emitter.py +342 -0
- claude_mpm/services/monitor/handlers/__init__.py +20 -0
- claude_mpm/services/monitor/handlers/code_analysis.py +334 -0
- claude_mpm/services/monitor/handlers/dashboard.py +298 -0
- claude_mpm/services/monitor/handlers/hooks.py +491 -0
- claude_mpm/services/monitor/management/__init__.py +18 -0
- claude_mpm/services/monitor/management/health.py +124 -0
- claude_mpm/services/monitor/management/lifecycle.py +338 -0
- claude_mpm/services/monitor/server.py +596 -0
- claude_mpm/tools/code_tree_analyzer.py +33 -17
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/RECORD +42 -37
- claude_mpm/cli/commands/socketio_monitor.py +0 -233
- claude_mpm/scripts/socketio_daemon.py +0 -571
- claude_mpm/scripts/socketio_daemon_hardened.py +0 -937
- claude_mpm/scripts/socketio_daemon_wrapper.py +0 -78
- claude_mpm/scripts/socketio_server_manager.py +0 -349
- claude_mpm/services/cli/dashboard_launcher.py +0 -423
- claude_mpm/services/cli/socketio_manager.py +0 -595
- claude_mpm/services/dashboard/stable_server.py +0 -1020
- claude_mpm/services/socketio/monitor_server.py +0 -505
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.9.dist-info → claude_mpm-4.2.12.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified Monitor Server for Claude MPM
|
|
3
|
+
====================================
|
|
4
|
+
|
|
5
|
+
WHY: This server combines HTTP dashboard serving and Socket.IO event handling
|
|
6
|
+
into a single, stable process. It uses real AST analysis instead of mock data
|
|
7
|
+
and provides all monitoring functionality on a single port.
|
|
8
|
+
|
|
9
|
+
DESIGN DECISIONS:
|
|
10
|
+
- Combines aiohttp HTTP server with Socket.IO server
|
|
11
|
+
- Uses real CodeTreeAnalyzer for AST analysis
|
|
12
|
+
- Single port (8765) for all functionality
|
|
13
|
+
- Event-driven architecture with proper handler registration
|
|
14
|
+
- Built for stability and daemon operation
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import threading
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Dict
|
|
21
|
+
|
|
22
|
+
import socketio
|
|
23
|
+
from aiohttp import web
|
|
24
|
+
|
|
25
|
+
from ...core.logging_config import get_logger
|
|
26
|
+
from ...dashboard.api.simple_directory import list_directory
|
|
27
|
+
from .event_emitter import get_event_emitter
|
|
28
|
+
from .handlers.code_analysis import CodeAnalysisHandler
|
|
29
|
+
from .handlers.dashboard import DashboardHandler
|
|
30
|
+
from .handlers.hooks import HookHandler
|
|
31
|
+
|
|
32
|
+
# EventBus integration
|
|
33
|
+
try:
|
|
34
|
+
from ...services.event_bus import EventBus
|
|
35
|
+
|
|
36
|
+
EVENTBUS_AVAILABLE = True
|
|
37
|
+
except ImportError:
|
|
38
|
+
EventBus = None
|
|
39
|
+
EVENTBUS_AVAILABLE = False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class UnifiedMonitorServer:
|
|
43
|
+
"""Unified server that combines HTTP dashboard and Socket.IO functionality.
|
|
44
|
+
|
|
45
|
+
WHY: Provides a single server process that handles all monitoring needs.
|
|
46
|
+
Replaces multiple competing server implementations with one stable solution.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, host: str = "localhost", port: int = 8765):
|
|
50
|
+
"""Initialize the unified monitor server.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
host: Host to bind to
|
|
54
|
+
port: Port to bind to
|
|
55
|
+
"""
|
|
56
|
+
self.host = host
|
|
57
|
+
self.port = port
|
|
58
|
+
self.logger = get_logger(__name__)
|
|
59
|
+
|
|
60
|
+
# Core components
|
|
61
|
+
self.app = None
|
|
62
|
+
self.sio = None
|
|
63
|
+
self.runner = None
|
|
64
|
+
self.site = None
|
|
65
|
+
|
|
66
|
+
# Event handlers
|
|
67
|
+
self.code_analysis_handler = None
|
|
68
|
+
self.dashboard_handler = None
|
|
69
|
+
self.hook_handler = None
|
|
70
|
+
|
|
71
|
+
# High-performance event emitter
|
|
72
|
+
self.event_emitter = None
|
|
73
|
+
|
|
74
|
+
# State
|
|
75
|
+
self.running = False
|
|
76
|
+
self.loop = None
|
|
77
|
+
self.server_thread = None
|
|
78
|
+
|
|
79
|
+
def start(self) -> bool:
|
|
80
|
+
"""Start the unified monitor server.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if started successfully, False otherwise
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
self.logger.info(
|
|
87
|
+
f"Starting unified monitor server on {self.host}:{self.port}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Start in a separate thread to avoid blocking
|
|
91
|
+
self.server_thread = threading.Thread(target=self._run_server, daemon=True)
|
|
92
|
+
self.server_thread.start()
|
|
93
|
+
|
|
94
|
+
# Wait for server to start
|
|
95
|
+
import time
|
|
96
|
+
|
|
97
|
+
for _ in range(50): # Wait up to 5 seconds
|
|
98
|
+
if self.running:
|
|
99
|
+
break
|
|
100
|
+
time.sleep(0.1)
|
|
101
|
+
|
|
102
|
+
if not self.running:
|
|
103
|
+
self.logger.error("Server failed to start within timeout")
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
self.logger.info("Unified monitor server started successfully")
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
self.logger.error(f"Failed to start unified monitor server: {e}")
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def _run_server(self):
|
|
114
|
+
"""Run the server in its own event loop."""
|
|
115
|
+
loop = None
|
|
116
|
+
try:
|
|
117
|
+
# Create new event loop for this thread
|
|
118
|
+
loop = asyncio.new_event_loop()
|
|
119
|
+
asyncio.set_event_loop(loop)
|
|
120
|
+
self.loop = loop
|
|
121
|
+
|
|
122
|
+
# Run the async server
|
|
123
|
+
loop.run_until_complete(self._start_async_server())
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
self.logger.error(f"Error in server thread: {e}")
|
|
127
|
+
finally:
|
|
128
|
+
# Always ensure loop cleanup happens
|
|
129
|
+
if loop is not None:
|
|
130
|
+
try:
|
|
131
|
+
# Cancel all pending tasks first
|
|
132
|
+
self._cancel_all_tasks(loop)
|
|
133
|
+
|
|
134
|
+
# Give tasks a moment to cancel gracefully
|
|
135
|
+
if not loop.is_closed():
|
|
136
|
+
try:
|
|
137
|
+
loop.run_until_complete(asyncio.sleep(0.1))
|
|
138
|
+
except RuntimeError:
|
|
139
|
+
# Loop might be stopped already, that's ok
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
self.logger.debug(f"Error during task cancellation: {e}")
|
|
144
|
+
finally:
|
|
145
|
+
try:
|
|
146
|
+
# Clear the loop reference from the instance first
|
|
147
|
+
self.loop = None
|
|
148
|
+
|
|
149
|
+
# Stop the loop if it's still running
|
|
150
|
+
if loop.is_running():
|
|
151
|
+
loop.stop()
|
|
152
|
+
|
|
153
|
+
# CRITICAL: Wait a moment for the loop to stop
|
|
154
|
+
import time
|
|
155
|
+
|
|
156
|
+
time.sleep(0.1)
|
|
157
|
+
|
|
158
|
+
# Clear the event loop from the thread BEFORE closing
|
|
159
|
+
# This prevents other code from accidentally using it
|
|
160
|
+
asyncio.set_event_loop(None)
|
|
161
|
+
|
|
162
|
+
# Now close the loop - this is critical to prevent the kqueue error
|
|
163
|
+
if not loop.is_closed():
|
|
164
|
+
loop.close()
|
|
165
|
+
# Wait for the close to complete
|
|
166
|
+
time.sleep(0.05)
|
|
167
|
+
|
|
168
|
+
except Exception as e:
|
|
169
|
+
self.logger.debug(f"Error during event loop cleanup: {e}")
|
|
170
|
+
|
|
171
|
+
async def _start_async_server(self):
|
|
172
|
+
"""Start the async server components."""
|
|
173
|
+
try:
|
|
174
|
+
# Create Socket.IO server
|
|
175
|
+
self.sio = socketio.AsyncServer(
|
|
176
|
+
cors_allowed_origins="*", logger=False, engineio_logger=False
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Create aiohttp application
|
|
180
|
+
self.app = web.Application()
|
|
181
|
+
|
|
182
|
+
# Attach Socket.IO to the app
|
|
183
|
+
self.sio.attach(self.app)
|
|
184
|
+
|
|
185
|
+
# Setup event handlers
|
|
186
|
+
self._setup_event_handlers()
|
|
187
|
+
|
|
188
|
+
# Setup high-performance event emitter
|
|
189
|
+
await self._setup_event_emitter()
|
|
190
|
+
|
|
191
|
+
self.logger.info(
|
|
192
|
+
"Using high-performance async event architecture with direct calls"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Setup HTTP routes
|
|
196
|
+
self._setup_http_routes()
|
|
197
|
+
|
|
198
|
+
# Create and start the server
|
|
199
|
+
self.runner = web.AppRunner(self.app)
|
|
200
|
+
await self.runner.setup()
|
|
201
|
+
|
|
202
|
+
self.site = web.TCPSite(self.runner, self.host, self.port)
|
|
203
|
+
await self.site.start()
|
|
204
|
+
|
|
205
|
+
self.running = True
|
|
206
|
+
self.logger.info(f"Server running on http://{self.host}:{self.port}")
|
|
207
|
+
|
|
208
|
+
# Keep the server running
|
|
209
|
+
while self.running:
|
|
210
|
+
await asyncio.sleep(1)
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
self.logger.error(f"Error starting async server: {e}")
|
|
214
|
+
raise
|
|
215
|
+
finally:
|
|
216
|
+
await self._cleanup_async()
|
|
217
|
+
|
|
218
|
+
def _setup_event_handlers(self):
|
|
219
|
+
"""Setup Socket.IO event handlers."""
|
|
220
|
+
try:
|
|
221
|
+
# Create event handlers
|
|
222
|
+
self.code_analysis_handler = CodeAnalysisHandler(self.sio)
|
|
223
|
+
self.dashboard_handler = DashboardHandler(self.sio)
|
|
224
|
+
self.hook_handler = HookHandler(self.sio)
|
|
225
|
+
|
|
226
|
+
# Register handlers
|
|
227
|
+
self.code_analysis_handler.register()
|
|
228
|
+
self.dashboard_handler.register()
|
|
229
|
+
self.hook_handler.register()
|
|
230
|
+
|
|
231
|
+
self.logger.info("Event handlers registered successfully")
|
|
232
|
+
|
|
233
|
+
except Exception as e:
|
|
234
|
+
self.logger.error(f"Error setting up event handlers: {e}")
|
|
235
|
+
raise
|
|
236
|
+
|
|
237
|
+
async def _setup_event_emitter(self):
|
|
238
|
+
"""Setup high-performance event emitter."""
|
|
239
|
+
try:
|
|
240
|
+
# Get the global event emitter instance
|
|
241
|
+
self.event_emitter = await get_event_emitter()
|
|
242
|
+
|
|
243
|
+
# Register this Socket.IO server for direct event emission
|
|
244
|
+
self.event_emitter.register_socketio_server(self.sio)
|
|
245
|
+
|
|
246
|
+
self.logger.info("Event emitter setup complete - direct calls enabled")
|
|
247
|
+
|
|
248
|
+
except Exception as e:
|
|
249
|
+
self.logger.error(f"Error setting up event emitter: {e}")
|
|
250
|
+
raise
|
|
251
|
+
|
|
252
|
+
def _setup_http_routes(self):
|
|
253
|
+
"""Setup HTTP routes for the dashboard."""
|
|
254
|
+
try:
|
|
255
|
+
# Dashboard static files
|
|
256
|
+
dashboard_dir = Path(__file__).parent.parent.parent / "dashboard"
|
|
257
|
+
|
|
258
|
+
# Main dashboard route
|
|
259
|
+
async def dashboard_index(request):
|
|
260
|
+
template_path = dashboard_dir / "templates" / "index.html"
|
|
261
|
+
if template_path.exists():
|
|
262
|
+
with open(template_path) as f:
|
|
263
|
+
content = f.read()
|
|
264
|
+
return web.Response(text=content, content_type="text/html")
|
|
265
|
+
return web.Response(text="Dashboard not found", status=404)
|
|
266
|
+
|
|
267
|
+
# Health check
|
|
268
|
+
async def health_check(request):
|
|
269
|
+
return web.json_response(
|
|
270
|
+
{
|
|
271
|
+
"status": "healthy",
|
|
272
|
+
"service": "unified-monitor",
|
|
273
|
+
"version": "1.0.0",
|
|
274
|
+
"port": self.port,
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Event ingestion endpoint for hook handlers
|
|
279
|
+
async def api_events_handler(request):
|
|
280
|
+
"""Handle HTTP POST events from hook handlers."""
|
|
281
|
+
try:
|
|
282
|
+
data = await request.json()
|
|
283
|
+
|
|
284
|
+
# Extract event data
|
|
285
|
+
namespace = data.get("namespace", "hook")
|
|
286
|
+
event = data.get("event", "claude_event")
|
|
287
|
+
event_data = data.get("data", {})
|
|
288
|
+
|
|
289
|
+
# Emit to Socket.IO clients via the appropriate event
|
|
290
|
+
if self.sio:
|
|
291
|
+
await self.sio.emit(event, event_data)
|
|
292
|
+
self.logger.debug(f"HTTP event forwarded to Socket.IO: {event}")
|
|
293
|
+
|
|
294
|
+
return web.Response(status=204) # No content response
|
|
295
|
+
|
|
296
|
+
except Exception as e:
|
|
297
|
+
self.logger.error(f"Error handling HTTP event: {e}")
|
|
298
|
+
return web.Response(text=f"Error: {e!s}", status=500)
|
|
299
|
+
|
|
300
|
+
# File content endpoint for file viewer
|
|
301
|
+
async def api_file_handler(request):
|
|
302
|
+
"""Handle file content requests."""
|
|
303
|
+
import json
|
|
304
|
+
import os
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
data = await request.json()
|
|
308
|
+
file_path = data.get("path", "")
|
|
309
|
+
|
|
310
|
+
# Security check: ensure path is absolute and exists
|
|
311
|
+
if not file_path or not os.path.isabs(file_path):
|
|
312
|
+
return web.json_response(
|
|
313
|
+
{"success": False, "error": "Invalid file path"}, status=400
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Check if file exists and is readable
|
|
317
|
+
if not os.path.exists(file_path):
|
|
318
|
+
return web.json_response(
|
|
319
|
+
{"success": False, "error": "File not found"}, status=404
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if not os.path.isfile(file_path):
|
|
323
|
+
return web.json_response(
|
|
324
|
+
{"success": False, "error": "Path is not a file"},
|
|
325
|
+
status=400,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Read file content (with size limit for safety)
|
|
329
|
+
max_size = 10 * 1024 * 1024 # 10MB limit
|
|
330
|
+
file_size = os.path.getsize(file_path)
|
|
331
|
+
|
|
332
|
+
if file_size > max_size:
|
|
333
|
+
return web.json_response(
|
|
334
|
+
{
|
|
335
|
+
"success": False,
|
|
336
|
+
"error": f"File too large (>{max_size} bytes)",
|
|
337
|
+
},
|
|
338
|
+
status=413,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
with open(file_path, encoding="utf-8") as f:
|
|
343
|
+
content = f.read()
|
|
344
|
+
lines = content.count("\n") + 1
|
|
345
|
+
except UnicodeDecodeError:
|
|
346
|
+
# Try reading as binary if UTF-8 fails
|
|
347
|
+
return web.json_response(
|
|
348
|
+
{"success": False, "error": "File is not a text file"},
|
|
349
|
+
status=415,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Get file extension for type detection
|
|
353
|
+
file_ext = os.path.splitext(file_path)[1].lstrip(".")
|
|
354
|
+
|
|
355
|
+
return web.json_response(
|
|
356
|
+
{
|
|
357
|
+
"success": True,
|
|
358
|
+
"content": content,
|
|
359
|
+
"lines": lines,
|
|
360
|
+
"size": file_size,
|
|
361
|
+
"type": file_ext or "text",
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
except json.JSONDecodeError:
|
|
366
|
+
return web.json_response(
|
|
367
|
+
{"success": False, "error": "Invalid JSON in request"},
|
|
368
|
+
status=400,
|
|
369
|
+
)
|
|
370
|
+
except Exception as e:
|
|
371
|
+
self.logger.error(f"Error reading file: {e}")
|
|
372
|
+
return web.json_response(
|
|
373
|
+
{"success": False, "error": str(e)}, status=500
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Version endpoint for dashboard build tracker
|
|
377
|
+
async def version_handler(request):
|
|
378
|
+
"""Serve version information for dashboard build tracker."""
|
|
379
|
+
try:
|
|
380
|
+
# Try to get version from version service
|
|
381
|
+
from claude_mpm.services.version_service import VersionService
|
|
382
|
+
|
|
383
|
+
version_service = VersionService()
|
|
384
|
+
version_info = version_service.get_version_info()
|
|
385
|
+
|
|
386
|
+
return web.json_response(
|
|
387
|
+
{
|
|
388
|
+
"version": version_info.get("base_version", "1.0.0"),
|
|
389
|
+
"build": version_info.get("build_number", 1),
|
|
390
|
+
"formatted_build": f"{version_info.get('build_number', 1):04d}",
|
|
391
|
+
"full_version": version_info.get("version", "v1.0.0-0001"),
|
|
392
|
+
"service": "unified-monitor",
|
|
393
|
+
}
|
|
394
|
+
)
|
|
395
|
+
except Exception as e:
|
|
396
|
+
self.logger.warning(f"Error getting version info: {e}")
|
|
397
|
+
# Return default version info if service fails
|
|
398
|
+
return web.json_response(
|
|
399
|
+
{
|
|
400
|
+
"version": "1.0.0",
|
|
401
|
+
"build": 1,
|
|
402
|
+
"formatted_build": "0001",
|
|
403
|
+
"full_version": "v1.0.0-0001",
|
|
404
|
+
"service": "unified-monitor",
|
|
405
|
+
}
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# Register routes
|
|
409
|
+
self.app.router.add_get("/", dashboard_index)
|
|
410
|
+
self.app.router.add_get("/health", health_check)
|
|
411
|
+
self.app.router.add_get("/version.json", version_handler)
|
|
412
|
+
self.app.router.add_get("/api/directory", list_directory)
|
|
413
|
+
self.app.router.add_post("/api/events", api_events_handler)
|
|
414
|
+
self.app.router.add_post("/api/file", api_file_handler)
|
|
415
|
+
|
|
416
|
+
# Static files
|
|
417
|
+
static_dir = dashboard_dir / "static"
|
|
418
|
+
if static_dir.exists():
|
|
419
|
+
self.app.router.add_static("/static/", static_dir)
|
|
420
|
+
|
|
421
|
+
# Templates
|
|
422
|
+
templates_dir = dashboard_dir / "templates"
|
|
423
|
+
if templates_dir.exists():
|
|
424
|
+
self.app.router.add_static("/templates/", templates_dir)
|
|
425
|
+
|
|
426
|
+
self.logger.info("HTTP routes registered successfully")
|
|
427
|
+
|
|
428
|
+
except Exception as e:
|
|
429
|
+
self.logger.error(f"Error setting up HTTP routes: {e}")
|
|
430
|
+
raise
|
|
431
|
+
|
|
432
|
+
def stop(self):
|
|
433
|
+
"""Stop the unified monitor server."""
|
|
434
|
+
try:
|
|
435
|
+
self.logger.info("Stopping unified monitor server")
|
|
436
|
+
|
|
437
|
+
# Signal shutdown first
|
|
438
|
+
self.running = False
|
|
439
|
+
|
|
440
|
+
# If we have a loop, schedule the cleanup
|
|
441
|
+
if self.loop and not self.loop.is_closed():
|
|
442
|
+
try:
|
|
443
|
+
# Use call_soon_threadsafe to schedule cleanup from another thread
|
|
444
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
445
|
+
self._graceful_shutdown(), self.loop
|
|
446
|
+
)
|
|
447
|
+
# Wait for cleanup to complete (with timeout)
|
|
448
|
+
future.result(timeout=3)
|
|
449
|
+
except Exception as e:
|
|
450
|
+
self.logger.debug(f"Error during graceful shutdown: {e}")
|
|
451
|
+
|
|
452
|
+
# Wait for server thread to finish with a reasonable timeout
|
|
453
|
+
if self.server_thread and self.server_thread.is_alive():
|
|
454
|
+
self.server_thread.join(timeout=5)
|
|
455
|
+
|
|
456
|
+
# If thread is still alive after timeout, log a warning
|
|
457
|
+
if self.server_thread.is_alive():
|
|
458
|
+
self.logger.warning("Server thread did not stop within timeout")
|
|
459
|
+
|
|
460
|
+
# Clear all references to help with cleanup
|
|
461
|
+
self.server_thread = None
|
|
462
|
+
self.app = None
|
|
463
|
+
self.sio = None
|
|
464
|
+
self.runner = None
|
|
465
|
+
self.site = None
|
|
466
|
+
self.event_emitter = None
|
|
467
|
+
|
|
468
|
+
# Give the system a moment to cleanup resources
|
|
469
|
+
import time
|
|
470
|
+
|
|
471
|
+
time.sleep(0.2)
|
|
472
|
+
|
|
473
|
+
self.logger.info("Unified monitor server stopped")
|
|
474
|
+
|
|
475
|
+
except Exception as e:
|
|
476
|
+
self.logger.error(f"Error stopping unified monitor server: {e}")
|
|
477
|
+
|
|
478
|
+
async def _cleanup_async(self):
|
|
479
|
+
"""Cleanup async resources."""
|
|
480
|
+
try:
|
|
481
|
+
# Close the Socket.IO server first to stop accepting new connections
|
|
482
|
+
if self.sio:
|
|
483
|
+
try:
|
|
484
|
+
await self.sio.shutdown()
|
|
485
|
+
self.logger.debug("Socket.IO shutdown complete")
|
|
486
|
+
except Exception as e:
|
|
487
|
+
self.logger.debug(f"Error shutting down Socket.IO: {e}")
|
|
488
|
+
finally:
|
|
489
|
+
self.sio = None
|
|
490
|
+
|
|
491
|
+
# Cleanup event emitter
|
|
492
|
+
if self.event_emitter:
|
|
493
|
+
try:
|
|
494
|
+
if self.sio:
|
|
495
|
+
self.event_emitter.unregister_socketio_server(self.sio)
|
|
496
|
+
|
|
497
|
+
# Use the global cleanup function to ensure proper cleanup
|
|
498
|
+
from .event_emitter import cleanup_event_emitter
|
|
499
|
+
|
|
500
|
+
await cleanup_event_emitter()
|
|
501
|
+
|
|
502
|
+
self.logger.info("Event emitter cleaned up")
|
|
503
|
+
except Exception as e:
|
|
504
|
+
self.logger.warning(f"Error cleaning up event emitter: {e}")
|
|
505
|
+
finally:
|
|
506
|
+
self.event_emitter = None
|
|
507
|
+
|
|
508
|
+
# Stop the site (must be done before runner cleanup)
|
|
509
|
+
if self.site:
|
|
510
|
+
try:
|
|
511
|
+
await self.site.stop()
|
|
512
|
+
self.logger.debug("Site stopped")
|
|
513
|
+
except Exception as e:
|
|
514
|
+
self.logger.debug(f"Error stopping site: {e}")
|
|
515
|
+
finally:
|
|
516
|
+
self.site = None
|
|
517
|
+
|
|
518
|
+
# Cleanup the runner (after site is stopped)
|
|
519
|
+
if self.runner:
|
|
520
|
+
try:
|
|
521
|
+
await self.runner.cleanup()
|
|
522
|
+
self.logger.debug("Runner cleaned up")
|
|
523
|
+
except Exception as e:
|
|
524
|
+
self.logger.debug(f"Error cleaning up runner: {e}")
|
|
525
|
+
finally:
|
|
526
|
+
self.runner = None
|
|
527
|
+
|
|
528
|
+
# Clear app reference
|
|
529
|
+
self.app = None
|
|
530
|
+
|
|
531
|
+
except Exception as e:
|
|
532
|
+
self.logger.error(f"Error during async cleanup: {e}")
|
|
533
|
+
|
|
534
|
+
def get_status(self) -> Dict:
|
|
535
|
+
"""Get server status information.
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
Dictionary with server status
|
|
539
|
+
"""
|
|
540
|
+
return {
|
|
541
|
+
"server_running": self.running,
|
|
542
|
+
"host": self.host,
|
|
543
|
+
"port": self.port,
|
|
544
|
+
"handlers": {
|
|
545
|
+
"code_analysis": self.code_analysis_handler is not None,
|
|
546
|
+
"dashboard": self.dashboard_handler is not None,
|
|
547
|
+
"hooks": self.hook_handler is not None,
|
|
548
|
+
},
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
def _cancel_all_tasks(self, loop=None):
|
|
552
|
+
"""Cancel all pending tasks in the event loop."""
|
|
553
|
+
if loop is None:
|
|
554
|
+
loop = self.loop
|
|
555
|
+
|
|
556
|
+
if not loop or loop.is_closed():
|
|
557
|
+
return
|
|
558
|
+
|
|
559
|
+
try:
|
|
560
|
+
# Get all tasks in the loop
|
|
561
|
+
pending = asyncio.all_tasks(loop)
|
|
562
|
+
|
|
563
|
+
# Count tasks to cancel
|
|
564
|
+
tasks_to_cancel = [task for task in pending if not task.done()]
|
|
565
|
+
|
|
566
|
+
if tasks_to_cancel:
|
|
567
|
+
# Cancel each task
|
|
568
|
+
for task in tasks_to_cancel:
|
|
569
|
+
task.cancel()
|
|
570
|
+
|
|
571
|
+
# Wait for all tasks to complete cancellation
|
|
572
|
+
gather = asyncio.gather(*tasks_to_cancel, return_exceptions=True)
|
|
573
|
+
try:
|
|
574
|
+
loop.run_until_complete(gather)
|
|
575
|
+
except Exception:
|
|
576
|
+
# Some tasks might fail to cancel, that's ok
|
|
577
|
+
pass
|
|
578
|
+
|
|
579
|
+
self.logger.debug(f"Cancelled {len(tasks_to_cancel)} pending tasks")
|
|
580
|
+
except Exception as e:
|
|
581
|
+
self.logger.debug(f"Error cancelling tasks: {e}")
|
|
582
|
+
|
|
583
|
+
async def _graceful_shutdown(self):
|
|
584
|
+
"""Perform graceful shutdown of async resources."""
|
|
585
|
+
try:
|
|
586
|
+
# Stop accepting new connections
|
|
587
|
+
self.running = False
|
|
588
|
+
|
|
589
|
+
# Give ongoing operations a moment to complete
|
|
590
|
+
await asyncio.sleep(0.5)
|
|
591
|
+
|
|
592
|
+
# Then cleanup resources
|
|
593
|
+
await self._cleanup_async()
|
|
594
|
+
|
|
595
|
+
except Exception as e:
|
|
596
|
+
self.logger.debug(f"Error in graceful shutdown: {e}")
|
|
@@ -1727,27 +1727,43 @@ class CodeTreeAnalyzer:
|
|
|
1727
1727
|
|
|
1728
1728
|
def _is_internal_node(self, node: CodeNode) -> bool:
|
|
1729
1729
|
"""Check if node is an internal function that should be filtered."""
|
|
1730
|
-
#
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
"__", # Python magic methods
|
|
1738
|
-
]
|
|
1730
|
+
# Don't filter classes - always show them
|
|
1731
|
+
if node.node_type == "class":
|
|
1732
|
+
return False
|
|
1733
|
+
|
|
1734
|
+
# Don't filter variables or imports - they're useful for tree view
|
|
1735
|
+
if node.node_type in ["variable", "import"]:
|
|
1736
|
+
return False
|
|
1739
1737
|
|
|
1740
1738
|
name_lower = node.name.lower()
|
|
1741
1739
|
|
|
1742
|
-
#
|
|
1743
|
-
|
|
1744
|
-
|
|
1740
|
+
# Filter only very specific internal patterns
|
|
1741
|
+
# Be more conservative - only filter obvious internal handlers
|
|
1742
|
+
if name_lower.startswith("handle_") or name_lower.startswith("on_"):
|
|
1743
|
+
return True
|
|
1744
|
+
|
|
1745
|
+
# Filter Python magic methods except important ones
|
|
1746
|
+
if name_lower.startswith("__") and name_lower.endswith("__"):
|
|
1747
|
+
# Keep important magic methods
|
|
1748
|
+
important_magic = [
|
|
1749
|
+
"__init__",
|
|
1750
|
+
"__call__",
|
|
1751
|
+
"__enter__",
|
|
1752
|
+
"__exit__",
|
|
1753
|
+
"__str__",
|
|
1754
|
+
"__repr__",
|
|
1755
|
+
]
|
|
1756
|
+
return node.name not in important_magic
|
|
1745
1757
|
|
|
1746
|
-
#
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1758
|
+
# Filter very generic getters/setters only if they're trivial
|
|
1759
|
+
if (name_lower.startswith("get_") or name_lower.startswith("set_")) and len(
|
|
1760
|
+
node.name
|
|
1761
|
+
) <= 8:
|
|
1762
|
+
return True
|
|
1763
|
+
|
|
1764
|
+
# Don't filter single underscore functions - they're often important
|
|
1765
|
+
# (like _setup_logging, _validate_input, etc.)
|
|
1766
|
+
return False
|
|
1751
1767
|
|
|
1752
1768
|
return False
|
|
1753
1769
|
|