claude-mpm 3.4.2__py3-none-any.whl → 3.4.5__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/agents/INSTRUCTIONS.md +20 -3
- claude_mpm/agents/backups/INSTRUCTIONS.md +119 -5
- claude_mpm/agents/templates/.claude-mpm/memories/README.md +36 -0
- claude_mpm/cli/__init__.py +3 -1
- claude_mpm/cli/commands/__init__.py +3 -1
- claude_mpm/cli/commands/monitor.py +328 -0
- claude_mpm/cli/commands/run.py +9 -17
- claude_mpm/cli/parser.py +69 -1
- claude_mpm/constants.py +9 -0
- claude_mpm/services/memory_router.py +99 -6
- claude_mpm/ticket_wrapper.py +29 -0
- {claude_mpm-3.4.2.dist-info → claude_mpm-3.4.5.dist-info}/METADATA +1 -1
- {claude_mpm-3.4.2.dist-info → claude_mpm-3.4.5.dist-info}/RECORD +17 -24
- claude_mpm/scripts/__init__.py +0 -1
- claude_mpm/scripts/claude-mpm-socketio +0 -32
- claude_mpm/scripts/claude_mpm_monitor.html +0 -567
- claude_mpm/scripts/install_socketio_server.py +0 -407
- claude_mpm/scripts/launch_monitor.py +0 -132
- claude_mpm/scripts/launch_socketio_dashboard.py +0 -261
- claude_mpm/scripts/manage_version.py +0 -479
- claude_mpm/scripts/socketio_daemon.py +0 -221
- claude_mpm/scripts/socketio_server_manager.py +0 -753
- claude_mpm/scripts/ticket.py +0 -269
- {claude_mpm-3.4.2.dist-info → claude_mpm-3.4.5.dist-info}/WHEEL +0 -0
- {claude_mpm-3.4.2.dist-info → claude_mpm-3.4.5.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.4.2.dist-info → claude_mpm-3.4.5.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.4.2.dist-info → claude_mpm-3.4.5.dist-info}/top_level.txt +0 -0
|
@@ -1,753 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Socket.IO Server Manager - Deployment-agnostic server management.
|
|
3
|
-
|
|
4
|
-
This script provides unified management for Socket.IO servers across different deployment scenarios:
|
|
5
|
-
- Local development
|
|
6
|
-
- PyPI installation
|
|
7
|
-
- Docker containers
|
|
8
|
-
- System service installation
|
|
9
|
-
|
|
10
|
-
Features:
|
|
11
|
-
- Start/stop/restart standalone servers
|
|
12
|
-
- Version compatibility checking
|
|
13
|
-
- Health monitoring and diagnostics
|
|
14
|
-
- Multi-instance management
|
|
15
|
-
- Automatic dependency installation
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
import argparse
|
|
19
|
-
import json
|
|
20
|
-
import os
|
|
21
|
-
import signal
|
|
22
|
-
import subprocess
|
|
23
|
-
import sys
|
|
24
|
-
import time
|
|
25
|
-
from pathlib import Path
|
|
26
|
-
from typing import Dict, List, Optional
|
|
27
|
-
|
|
28
|
-
try:
|
|
29
|
-
import requests
|
|
30
|
-
REQUESTS_AVAILABLE = True
|
|
31
|
-
except ImportError:
|
|
32
|
-
REQUESTS_AVAILABLE = False
|
|
33
|
-
|
|
34
|
-
try:
|
|
35
|
-
import psutil
|
|
36
|
-
PSUTIL_AVAILABLE = True
|
|
37
|
-
except ImportError:
|
|
38
|
-
PSUTIL_AVAILABLE = False
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class ServerManager:
|
|
42
|
-
"""Manages Socket.IO server instances across different deployment modes."""
|
|
43
|
-
|
|
44
|
-
def __init__(self):
|
|
45
|
-
self.base_port = 8765
|
|
46
|
-
self.max_instances = 5
|
|
47
|
-
self.script_dir = Path(__file__).parent
|
|
48
|
-
self.project_root = self.script_dir.parent
|
|
49
|
-
# Daemon PID file location (used by socketio_daemon.py)
|
|
50
|
-
self.daemon_pidfile_path = Path.home() / ".claude-mpm" / "socketio-server.pid"
|
|
51
|
-
# Standalone server PID file location pattern
|
|
52
|
-
self.standalone_pidfile_pattern = "/tmp/claude_mpm_socketio_{port}.pid"
|
|
53
|
-
|
|
54
|
-
def get_server_info(self, port: int) -> Optional[Dict]:
|
|
55
|
-
"""Get server information from a running instance with daemon compatibility."""
|
|
56
|
-
if not REQUESTS_AVAILABLE:
|
|
57
|
-
return self._check_daemon_fallback(port)
|
|
58
|
-
|
|
59
|
-
try:
|
|
60
|
-
response = requests.get(f"http://localhost:{port}/health", timeout=2.0)
|
|
61
|
-
if response.status_code == 200:
|
|
62
|
-
data = response.json()
|
|
63
|
-
# Check if this is a daemon-style response (no 'pid' field)
|
|
64
|
-
if 'pid' not in data and 'status' in data:
|
|
65
|
-
# Try to get PID from daemon PID file
|
|
66
|
-
daemon_pid = self._get_daemon_pid()
|
|
67
|
-
if daemon_pid:
|
|
68
|
-
data['pid'] = daemon_pid
|
|
69
|
-
data['management_style'] = 'daemon'
|
|
70
|
-
return data
|
|
71
|
-
except Exception as e:
|
|
72
|
-
# If HTTP fails, try daemon fallback
|
|
73
|
-
return self._check_daemon_fallback(port)
|
|
74
|
-
return None
|
|
75
|
-
|
|
76
|
-
def list_running_servers(self) -> List[Dict]:
|
|
77
|
-
"""List all running Socket.IO servers including daemon-style servers."""
|
|
78
|
-
running_servers = []
|
|
79
|
-
|
|
80
|
-
# Check standard port range
|
|
81
|
-
for port in range(self.base_port, self.base_port + self.max_instances):
|
|
82
|
-
server_info = self.get_server_info(port)
|
|
83
|
-
if server_info:
|
|
84
|
-
server_info['port'] = port
|
|
85
|
-
running_servers.append(server_info)
|
|
86
|
-
|
|
87
|
-
# Also check for daemon-style server specifically
|
|
88
|
-
daemon_info = self._get_daemon_server_info()
|
|
89
|
-
if daemon_info and not any(s['port'] == daemon_info.get('port', self.base_port) for s in running_servers):
|
|
90
|
-
running_servers.append(daemon_info)
|
|
91
|
-
|
|
92
|
-
return running_servers
|
|
93
|
-
|
|
94
|
-
def find_available_port(self, start_port: int = None) -> int:
|
|
95
|
-
"""Find the next available port for a new server."""
|
|
96
|
-
start_port = start_port or self.base_port
|
|
97
|
-
|
|
98
|
-
for port in range(start_port, start_port + self.max_instances):
|
|
99
|
-
if not self.get_server_info(port):
|
|
100
|
-
return port
|
|
101
|
-
|
|
102
|
-
raise RuntimeError(f"No available ports found in range {start_port}-{start_port + self.max_instances}")
|
|
103
|
-
|
|
104
|
-
def start_server(self, port: int = None, server_id: str = None,
|
|
105
|
-
host: str = "localhost") -> bool:
|
|
106
|
-
"""Start a standalone Socket.IO server with conflict detection."""
|
|
107
|
-
|
|
108
|
-
# Find available port if not specified
|
|
109
|
-
if port is None:
|
|
110
|
-
try:
|
|
111
|
-
port = self.find_available_port()
|
|
112
|
-
except RuntimeError as e:
|
|
113
|
-
print(f"Error: {e}")
|
|
114
|
-
return False
|
|
115
|
-
|
|
116
|
-
# Check if server is already running on this port
|
|
117
|
-
existing_server = self.get_server_info(port)
|
|
118
|
-
if existing_server:
|
|
119
|
-
management_style = existing_server.get('management_style', 'http')
|
|
120
|
-
server_id_existing = existing_server.get('server_id', 'unknown')
|
|
121
|
-
|
|
122
|
-
print(f"❌ Server already running on port {port}")
|
|
123
|
-
print(f" Existing server: {server_id_existing} ({management_style}-managed)")
|
|
124
|
-
|
|
125
|
-
if management_style == 'daemon':
|
|
126
|
-
print(f"💡 To stop daemon server: {self.project_root / 'src' / 'claude_mpm' / 'scripts' / 'socketio_daemon.py'} stop")
|
|
127
|
-
else:
|
|
128
|
-
print(f"💡 To stop server: {sys.executable} {__file__} stop --port {port}")
|
|
129
|
-
|
|
130
|
-
return False
|
|
131
|
-
|
|
132
|
-
# Warn if daemon server exists on default port but we're starting on different port
|
|
133
|
-
if port != self.base_port and self._get_daemon_pid():
|
|
134
|
-
print(f"⚠️ Warning: Daemon server is running on port {self.base_port}, you're starting on port {port}")
|
|
135
|
-
print(f" This may cause conflicts. Consider stopping daemon first.")
|
|
136
|
-
|
|
137
|
-
# Try different ways to start the server based on deployment
|
|
138
|
-
success = False
|
|
139
|
-
|
|
140
|
-
# Method 1: Try installed claude-mpm package
|
|
141
|
-
try:
|
|
142
|
-
cmd = [
|
|
143
|
-
sys.executable, "-m", "claude_mpm.services.standalone_socketio_server",
|
|
144
|
-
"--host", host,
|
|
145
|
-
"--port", str(port)
|
|
146
|
-
]
|
|
147
|
-
if server_id:
|
|
148
|
-
cmd.extend(["--server-id", server_id])
|
|
149
|
-
|
|
150
|
-
print(f"Starting server on {host}:{port} using installed package...")
|
|
151
|
-
|
|
152
|
-
# Start in background
|
|
153
|
-
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
154
|
-
|
|
155
|
-
# Wait for server to start
|
|
156
|
-
for _ in range(10): # Wait up to 10 seconds
|
|
157
|
-
time.sleep(1)
|
|
158
|
-
if self.get_server_info(port):
|
|
159
|
-
success = True
|
|
160
|
-
break
|
|
161
|
-
|
|
162
|
-
except Exception as e:
|
|
163
|
-
print(f"Failed to start via installed package: {e}")
|
|
164
|
-
|
|
165
|
-
# Method 2: Try local development mode
|
|
166
|
-
if not success:
|
|
167
|
-
try:
|
|
168
|
-
server_path = self.project_root / "src" / "claude_mpm" / "services" / "standalone_socketio_server.py"
|
|
169
|
-
if server_path.exists():
|
|
170
|
-
cmd = [
|
|
171
|
-
sys.executable, str(server_path),
|
|
172
|
-
"--host", host,
|
|
173
|
-
"--port", str(port)
|
|
174
|
-
]
|
|
175
|
-
if server_id:
|
|
176
|
-
cmd.extend(["--server-id", server_id])
|
|
177
|
-
|
|
178
|
-
print(f"Starting server using local development mode...")
|
|
179
|
-
|
|
180
|
-
# Set PYTHONPATH for local development
|
|
181
|
-
env = os.environ.copy()
|
|
182
|
-
src_path = str(self.project_root / "src")
|
|
183
|
-
if "PYTHONPATH" in env:
|
|
184
|
-
env["PYTHONPATH"] = f"{src_path}:{env['PYTHONPATH']}"
|
|
185
|
-
else:
|
|
186
|
-
env["PYTHONPATH"] = src_path
|
|
187
|
-
|
|
188
|
-
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env)
|
|
189
|
-
|
|
190
|
-
# Wait for server to start
|
|
191
|
-
for _ in range(10):
|
|
192
|
-
time.sleep(1)
|
|
193
|
-
if self.get_server_info(port):
|
|
194
|
-
success = True
|
|
195
|
-
break
|
|
196
|
-
|
|
197
|
-
except Exception as e:
|
|
198
|
-
print(f"Failed to start in development mode: {e}")
|
|
199
|
-
|
|
200
|
-
if success:
|
|
201
|
-
print(f"✅ Server started successfully on {host}:{port}")
|
|
202
|
-
print(f"💡 Management commands:")
|
|
203
|
-
print(f" Status: {sys.executable} {__file__} status")
|
|
204
|
-
print(f" Stop: {sys.executable} {__file__} stop --port {port}")
|
|
205
|
-
return True
|
|
206
|
-
else:
|
|
207
|
-
print(f"❌ Failed to start server on {host}:{port}")
|
|
208
|
-
print(f"💡 Troubleshooting:")
|
|
209
|
-
print(f" • Check if port {port} is already in use: lsof -i :{port}")
|
|
210
|
-
print(f" • Check server status: {sys.executable} {__file__} status")
|
|
211
|
-
print(f" • Try different port: {sys.executable} {__file__} start --port {port + 1}")
|
|
212
|
-
return False
|
|
213
|
-
|
|
214
|
-
def stop_server(self, port: int = None, server_id: str = None) -> bool:
|
|
215
|
-
"""Stop a running Socket.IO server with daemon compatibility."""
|
|
216
|
-
|
|
217
|
-
if port is None and server_id is None:
|
|
218
|
-
print("Must specify either port or server_id")
|
|
219
|
-
return False
|
|
220
|
-
|
|
221
|
-
# Find server by ID if port not specified
|
|
222
|
-
if port is None:
|
|
223
|
-
running_servers = self.list_running_servers()
|
|
224
|
-
for server in running_servers:
|
|
225
|
-
if server.get('server_id') == server_id:
|
|
226
|
-
port = server['port']
|
|
227
|
-
break
|
|
228
|
-
|
|
229
|
-
if port is None:
|
|
230
|
-
print(f"Server with ID '{server_id}' not found")
|
|
231
|
-
return False
|
|
232
|
-
|
|
233
|
-
# Get server info
|
|
234
|
-
server_info = self.get_server_info(port)
|
|
235
|
-
if not server_info:
|
|
236
|
-
# Try daemon-specific stop as fallback
|
|
237
|
-
return self._try_daemon_stop(port)
|
|
238
|
-
|
|
239
|
-
# Determine management style
|
|
240
|
-
management_style = server_info.get('management_style', 'http')
|
|
241
|
-
|
|
242
|
-
# Try HTTP-based stop first
|
|
243
|
-
pid = server_info.get('pid')
|
|
244
|
-
if pid:
|
|
245
|
-
try:
|
|
246
|
-
# Validate PID before attempting to kill
|
|
247
|
-
if self._validate_pid(pid):
|
|
248
|
-
os.kill(pid, signal.SIGTERM)
|
|
249
|
-
print(f"✅ Sent termination signal to server (PID: {pid})")
|
|
250
|
-
|
|
251
|
-
# Wait for server to stop
|
|
252
|
-
for i in range(10):
|
|
253
|
-
time.sleep(1)
|
|
254
|
-
if not self.get_server_info(port):
|
|
255
|
-
print(f"✅ Server stopped successfully")
|
|
256
|
-
return True
|
|
257
|
-
if i == 5: # After 5 seconds, show progress
|
|
258
|
-
print(f"⏳ Waiting for server to stop...")
|
|
259
|
-
|
|
260
|
-
# Force kill if still running
|
|
261
|
-
try:
|
|
262
|
-
if self._validate_pid(pid):
|
|
263
|
-
os.kill(pid, signal.SIGKILL)
|
|
264
|
-
print(f"⚠️ Force killed server (PID: {pid})")
|
|
265
|
-
return True
|
|
266
|
-
except OSError:
|
|
267
|
-
pass
|
|
268
|
-
|
|
269
|
-
else:
|
|
270
|
-
print(f"⚠️ PID {pid} is no longer valid, trying daemon stop...")
|
|
271
|
-
return self._try_daemon_stop(port)
|
|
272
|
-
|
|
273
|
-
except OSError as e:
|
|
274
|
-
print(f"Error stopping server via PID {pid}: {e}")
|
|
275
|
-
if management_style == 'daemon':
|
|
276
|
-
print("🔄 Trying daemon-style stop...")
|
|
277
|
-
return self._try_daemon_stop(port)
|
|
278
|
-
|
|
279
|
-
# If HTTP method failed, try daemon stop
|
|
280
|
-
if management_style == 'daemon' or not pid:
|
|
281
|
-
print("🔄 Attempting daemon-style stop...")
|
|
282
|
-
return self._try_daemon_stop(port)
|
|
283
|
-
|
|
284
|
-
print(f"❌ Failed to stop server on port {port}")
|
|
285
|
-
print(f"💡 Try using the socketio_daemon.py stop command if this is a daemon-managed server")
|
|
286
|
-
return False
|
|
287
|
-
|
|
288
|
-
def restart_server(self, port: int = None, server_id: str = None) -> bool:
|
|
289
|
-
"""Restart a Socket.IO server."""
|
|
290
|
-
|
|
291
|
-
# Stop the server first
|
|
292
|
-
if self.stop_server(port, server_id):
|
|
293
|
-
time.sleep(2) # Give it time to fully stop
|
|
294
|
-
|
|
295
|
-
# Start it again
|
|
296
|
-
if port is None:
|
|
297
|
-
port = self.find_available_port()
|
|
298
|
-
|
|
299
|
-
return self.start_server(port)
|
|
300
|
-
|
|
301
|
-
return False
|
|
302
|
-
|
|
303
|
-
def status(self, verbose: bool = False) -> None:
|
|
304
|
-
"""Show status of all Socket.IO servers with management style info."""
|
|
305
|
-
running_servers = self.list_running_servers()
|
|
306
|
-
|
|
307
|
-
if not running_servers:
|
|
308
|
-
print("No Socket.IO servers currently running")
|
|
309
|
-
print()
|
|
310
|
-
print("💡 Management options:")
|
|
311
|
-
print(f" • Start with manager: {sys.executable} {__file__} start")
|
|
312
|
-
print(f" • Start with daemon: {self.project_root / 'src' / 'claude_mpm' / 'scripts' / 'socketio_daemon.py'} start")
|
|
313
|
-
return
|
|
314
|
-
|
|
315
|
-
print(f"Found {len(running_servers)} running server(s):")
|
|
316
|
-
print()
|
|
317
|
-
|
|
318
|
-
for server in running_servers:
|
|
319
|
-
port = server['port']
|
|
320
|
-
server_id = server.get('server_id', 'unknown')
|
|
321
|
-
version = server.get('server_version', 'unknown')
|
|
322
|
-
uptime = server.get('uptime_seconds', 0)
|
|
323
|
-
clients = server.get('clients_connected', 0)
|
|
324
|
-
management_style = server.get('management_style', 'http')
|
|
325
|
-
|
|
326
|
-
# Different icons based on management style
|
|
327
|
-
icon = "🖥️" if management_style == 'http' else "🔧"
|
|
328
|
-
|
|
329
|
-
print(f"{icon} Server ID: {server_id}")
|
|
330
|
-
print(f" Port: {port}")
|
|
331
|
-
print(f" Version: {version}")
|
|
332
|
-
print(f" Management: {management_style}")
|
|
333
|
-
print(f" Uptime: {self._format_uptime(uptime)}")
|
|
334
|
-
print(f" Clients: {clients}")
|
|
335
|
-
|
|
336
|
-
if verbose:
|
|
337
|
-
print(f" PID: {server.get('pid', 'unknown')}")
|
|
338
|
-
print(f" Host: {server.get('host', 'unknown')}")
|
|
339
|
-
|
|
340
|
-
# Show appropriate stop command
|
|
341
|
-
if management_style == 'daemon':
|
|
342
|
-
print(f" Stop command: {self.project_root / 'src' / 'claude_mpm' / 'scripts' / 'socketio_daemon.py'} stop")
|
|
343
|
-
else:
|
|
344
|
-
print(f" Stop command: {sys.executable} {__file__} stop --port {port}")
|
|
345
|
-
|
|
346
|
-
# Get additional stats (only for HTTP-style servers)
|
|
347
|
-
if management_style == 'http':
|
|
348
|
-
stats = self._get_server_stats(port)
|
|
349
|
-
if stats:
|
|
350
|
-
events_processed = stats.get('events', {}).get('total_processed', 0)
|
|
351
|
-
clients_served = stats.get('connections', {}).get('total_served', 0)
|
|
352
|
-
print(f" Events processed: {events_processed}")
|
|
353
|
-
print(f" Total clients served: {clients_served}")
|
|
354
|
-
|
|
355
|
-
print()
|
|
356
|
-
|
|
357
|
-
def health_check(self, port: int = None) -> bool:
|
|
358
|
-
"""Perform health check on server(s) with management style awareness."""
|
|
359
|
-
|
|
360
|
-
if port:
|
|
361
|
-
# Check specific server
|
|
362
|
-
server_info = self.get_server_info(port)
|
|
363
|
-
if server_info:
|
|
364
|
-
status = server_info.get('status', 'unknown')
|
|
365
|
-
management_style = server_info.get('management_style', 'http')
|
|
366
|
-
server_id = server_info.get('server_id', 'unknown')
|
|
367
|
-
|
|
368
|
-
print(f"Server {server_id} on port {port}: {status} ({management_style}-managed)")
|
|
369
|
-
|
|
370
|
-
# Additional health info for daemon servers
|
|
371
|
-
if management_style == 'daemon':
|
|
372
|
-
pid = server_info.get('pid')
|
|
373
|
-
if pid and self._validate_pid(pid):
|
|
374
|
-
print(f" ✅ Process {pid} is running")
|
|
375
|
-
else:
|
|
376
|
-
print(f" ❌ Process {pid} is not running")
|
|
377
|
-
return False
|
|
378
|
-
|
|
379
|
-
return status in ['healthy', 'running']
|
|
380
|
-
else:
|
|
381
|
-
print(f"No server found on port {port}")
|
|
382
|
-
# Try daemon fallback for default port
|
|
383
|
-
if port == self.base_port:
|
|
384
|
-
daemon_info = self._get_daemon_server_info()
|
|
385
|
-
if daemon_info:
|
|
386
|
-
print(f" Found daemon server: {daemon_info['server_id']}")
|
|
387
|
-
return True
|
|
388
|
-
return False
|
|
389
|
-
else:
|
|
390
|
-
# Check all servers
|
|
391
|
-
running_servers = self.list_running_servers()
|
|
392
|
-
if not running_servers:
|
|
393
|
-
print("No servers running")
|
|
394
|
-
print(f"💡 Start a server with: {sys.executable} {__file__} start")
|
|
395
|
-
return False
|
|
396
|
-
|
|
397
|
-
all_healthy = True
|
|
398
|
-
for server in running_servers:
|
|
399
|
-
port = server['port']
|
|
400
|
-
status = server.get('status', 'unknown')
|
|
401
|
-
server_id = server.get('server_id', 'unknown')
|
|
402
|
-
management_style = server.get('management_style', 'http')
|
|
403
|
-
|
|
404
|
-
health_status = status in ['healthy', 'running']
|
|
405
|
-
icon = "✅" if health_status else "❌"
|
|
406
|
-
|
|
407
|
-
print(f"{icon} Server {server_id} (port {port}): {status} ({management_style}-managed)")
|
|
408
|
-
|
|
409
|
-
if not health_status:
|
|
410
|
-
all_healthy = False
|
|
411
|
-
|
|
412
|
-
return all_healthy
|
|
413
|
-
|
|
414
|
-
def install_dependencies(self) -> bool:
|
|
415
|
-
"""Install required dependencies for Socket.IO server."""
|
|
416
|
-
dependencies = ['python-socketio>=5.11.0', 'aiohttp>=3.9.0', 'requests>=2.25.0']
|
|
417
|
-
|
|
418
|
-
print("Installing Socket.IO server dependencies...")
|
|
419
|
-
|
|
420
|
-
try:
|
|
421
|
-
cmd = [sys.executable, '-m', 'pip', 'install'] + dependencies
|
|
422
|
-
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
423
|
-
|
|
424
|
-
if result.returncode == 0:
|
|
425
|
-
print("✅ Dependencies installed successfully")
|
|
426
|
-
return True
|
|
427
|
-
else:
|
|
428
|
-
print(f"❌ Failed to install dependencies: {result.stderr}")
|
|
429
|
-
return False
|
|
430
|
-
|
|
431
|
-
except Exception as e:
|
|
432
|
-
print(f"❌ Error installing dependencies: {e}")
|
|
433
|
-
return False
|
|
434
|
-
|
|
435
|
-
def _format_uptime(self, seconds: float) -> str:
|
|
436
|
-
"""Format uptime in a human-readable way."""
|
|
437
|
-
if seconds < 60:
|
|
438
|
-
return f"{seconds:.1f}s"
|
|
439
|
-
elif seconds < 3600:
|
|
440
|
-
return f"{seconds/60:.1f}m"
|
|
441
|
-
else:
|
|
442
|
-
return f"{seconds/3600:.1f}h"
|
|
443
|
-
|
|
444
|
-
def _get_server_stats(self, port: int) -> Optional[Dict]:
|
|
445
|
-
"""Get detailed server statistics."""
|
|
446
|
-
if not REQUESTS_AVAILABLE:
|
|
447
|
-
return None
|
|
448
|
-
|
|
449
|
-
try:
|
|
450
|
-
response = requests.get(f"http://localhost:{port}/stats", timeout=2.0)
|
|
451
|
-
if response.status_code == 200:
|
|
452
|
-
return response.json()
|
|
453
|
-
except Exception:
|
|
454
|
-
pass
|
|
455
|
-
return None
|
|
456
|
-
|
|
457
|
-
def _check_daemon_fallback(self, port: int) -> Optional[Dict]:
|
|
458
|
-
"""Check for daemon-style server when HTTP fails."""
|
|
459
|
-
if port == self.base_port: # Only check daemon for default port
|
|
460
|
-
return self._get_daemon_server_info()
|
|
461
|
-
return None
|
|
462
|
-
|
|
463
|
-
def _get_daemon_pid(self) -> Optional[int]:
|
|
464
|
-
"""Get PID from daemon PID file."""
|
|
465
|
-
try:
|
|
466
|
-
if self.daemon_pidfile_path.exists():
|
|
467
|
-
with open(self.daemon_pidfile_path, 'r') as f:
|
|
468
|
-
content = f.read().strip()
|
|
469
|
-
if content.isdigit():
|
|
470
|
-
pid = int(content)
|
|
471
|
-
# Validate the PID exists
|
|
472
|
-
if self._validate_pid(pid):
|
|
473
|
-
return pid
|
|
474
|
-
except Exception:
|
|
475
|
-
pass
|
|
476
|
-
return None
|
|
477
|
-
|
|
478
|
-
def _get_daemon_server_info(self) -> Optional[Dict]:
|
|
479
|
-
"""Get server info for daemon-style server."""
|
|
480
|
-
daemon_pid = self._get_daemon_pid()
|
|
481
|
-
if daemon_pid:
|
|
482
|
-
# Basic server info for daemon
|
|
483
|
-
info = {
|
|
484
|
-
'pid': daemon_pid,
|
|
485
|
-
'server_id': 'daemon-socketio',
|
|
486
|
-
'management_style': 'daemon',
|
|
487
|
-
'status': 'running',
|
|
488
|
-
'port': self.base_port,
|
|
489
|
-
'server_version': 'daemon-managed'
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
# Try to get additional process info if psutil is available
|
|
493
|
-
if PSUTIL_AVAILABLE:
|
|
494
|
-
try:
|
|
495
|
-
process = psutil.Process(daemon_pid)
|
|
496
|
-
info.update({
|
|
497
|
-
'uptime_seconds': time.time() - process.create_time(),
|
|
498
|
-
'host': 'localhost',
|
|
499
|
-
'process_name': process.name()
|
|
500
|
-
})
|
|
501
|
-
except:
|
|
502
|
-
pass
|
|
503
|
-
|
|
504
|
-
return info
|
|
505
|
-
return None
|
|
506
|
-
|
|
507
|
-
def _validate_pid(self, pid: int) -> bool:
|
|
508
|
-
"""Validate that a PID represents a running process."""
|
|
509
|
-
try:
|
|
510
|
-
# Check if process exists
|
|
511
|
-
os.kill(pid, 0)
|
|
512
|
-
return True
|
|
513
|
-
except OSError:
|
|
514
|
-
return False
|
|
515
|
-
|
|
516
|
-
def _try_daemon_stop(self, port: int) -> bool:
|
|
517
|
-
"""Try to stop daemon-style server."""
|
|
518
|
-
if port != self.base_port:
|
|
519
|
-
print(f"⚠️ Daemon management only supports default port {self.base_port}, not {port}")
|
|
520
|
-
return False
|
|
521
|
-
|
|
522
|
-
daemon_pid = self._get_daemon_pid()
|
|
523
|
-
if not daemon_pid:
|
|
524
|
-
print(f"❌ No daemon server found (no PID file at {self.daemon_pidfile_path})")
|
|
525
|
-
return False
|
|
526
|
-
|
|
527
|
-
try:
|
|
528
|
-
print(f"🔄 Stopping daemon server (PID: {daemon_pid})...")
|
|
529
|
-
os.kill(daemon_pid, signal.SIGTERM)
|
|
530
|
-
|
|
531
|
-
# Wait for daemon to stop
|
|
532
|
-
for i in range(10):
|
|
533
|
-
time.sleep(1)
|
|
534
|
-
if not self._validate_pid(daemon_pid):
|
|
535
|
-
print(f"✅ Daemon server stopped successfully")
|
|
536
|
-
# Clean up PID file
|
|
537
|
-
try:
|
|
538
|
-
self.daemon_pidfile_path.unlink(missing_ok=True)
|
|
539
|
-
except:
|
|
540
|
-
pass
|
|
541
|
-
return True
|
|
542
|
-
if i == 5:
|
|
543
|
-
print(f"⏳ Waiting for daemon to stop...")
|
|
544
|
-
|
|
545
|
-
# Force kill if still running
|
|
546
|
-
if self._validate_pid(daemon_pid):
|
|
547
|
-
print(f"⚠️ Force killing daemon server...")
|
|
548
|
-
os.kill(daemon_pid, signal.SIGKILL)
|
|
549
|
-
time.sleep(1)
|
|
550
|
-
if not self._validate_pid(daemon_pid):
|
|
551
|
-
print(f"✅ Daemon server force stopped")
|
|
552
|
-
try:
|
|
553
|
-
self.daemon_pidfile_path.unlink(missing_ok=True)
|
|
554
|
-
except:
|
|
555
|
-
pass
|
|
556
|
-
return True
|
|
557
|
-
|
|
558
|
-
except OSError as e:
|
|
559
|
-
print(f"❌ Error stopping daemon server: {e}")
|
|
560
|
-
return False
|
|
561
|
-
|
|
562
|
-
print(f"❌ Failed to stop daemon server")
|
|
563
|
-
return False
|
|
564
|
-
|
|
565
|
-
def diagnose_conflicts(self, port: int = None) -> None:
|
|
566
|
-
"""Diagnose server management conflicts and suggest resolutions."""
|
|
567
|
-
if port is None:
|
|
568
|
-
port = self.base_port
|
|
569
|
-
|
|
570
|
-
print(f"🔍 Diagnosing Socket.IO server management on port {port}")
|
|
571
|
-
print("=" * 60)
|
|
572
|
-
|
|
573
|
-
# Check HTTP-managed server
|
|
574
|
-
http_server = None
|
|
575
|
-
daemon_server = None
|
|
576
|
-
|
|
577
|
-
try:
|
|
578
|
-
if REQUESTS_AVAILABLE:
|
|
579
|
-
response = requests.get(f"http://localhost:{port}/health", timeout=2.0)
|
|
580
|
-
if response.status_code == 200:
|
|
581
|
-
data = response.json()
|
|
582
|
-
if 'pid' in data:
|
|
583
|
-
http_server = data
|
|
584
|
-
except:
|
|
585
|
-
pass
|
|
586
|
-
|
|
587
|
-
# Check daemon-managed server
|
|
588
|
-
daemon_pid = self._get_daemon_pid()
|
|
589
|
-
if daemon_pid and port == self.base_port:
|
|
590
|
-
daemon_server = self._get_daemon_server_info()
|
|
591
|
-
|
|
592
|
-
# Analysis
|
|
593
|
-
print("📊 Server Analysis:")
|
|
594
|
-
|
|
595
|
-
if http_server and daemon_server:
|
|
596
|
-
print("⚠️ CONFLICT DETECTED: Both HTTP and daemon servers found!")
|
|
597
|
-
print(f" HTTP server: PID {http_server.get('pid')}, ID {http_server.get('server_id')}")
|
|
598
|
-
print(f" Daemon server: PID {daemon_server.get('pid')}, ID {daemon_server.get('server_id')}")
|
|
599
|
-
print()
|
|
600
|
-
print("🔧 Resolution Steps:")
|
|
601
|
-
print(" 1. Choose one management approach:")
|
|
602
|
-
print(f" • Keep HTTP: {sys.executable} {__file__} stop --port {port} (stops daemon)")
|
|
603
|
-
print(f" • Keep daemon: Stop HTTP server first, then use daemon commands")
|
|
604
|
-
print()
|
|
605
|
-
|
|
606
|
-
elif http_server:
|
|
607
|
-
print(f"✅ HTTP-managed server found")
|
|
608
|
-
print(f" Server ID: {http_server.get('server_id')}")
|
|
609
|
-
print(f" PID: {http_server.get('pid')}")
|
|
610
|
-
print(f" Status: {http_server.get('status')}")
|
|
611
|
-
print()
|
|
612
|
-
print("🔧 Management Commands:")
|
|
613
|
-
print(f" • Stop: {sys.executable} {__file__} stop --port {port}")
|
|
614
|
-
print(f" • Status: {sys.executable} {__file__} status")
|
|
615
|
-
print()
|
|
616
|
-
|
|
617
|
-
elif daemon_server:
|
|
618
|
-
print(f"✅ Daemon-managed server found")
|
|
619
|
-
print(f" PID: {daemon_server.get('pid')}")
|
|
620
|
-
print(f" PID file: {self.daemon_pidfile_path}")
|
|
621
|
-
print()
|
|
622
|
-
print("🔧 Management Commands:")
|
|
623
|
-
daemon_script = self.project_root / "src" / "claude_mpm" / "scripts" / "socketio_daemon.py"
|
|
624
|
-
print(f" • Stop: {daemon_script} stop")
|
|
625
|
-
print(f" • Status: {daemon_script} status")
|
|
626
|
-
print(f" • Restart: {daemon_script} restart")
|
|
627
|
-
print()
|
|
628
|
-
|
|
629
|
-
else:
|
|
630
|
-
print("❌ No servers found on specified port")
|
|
631
|
-
print()
|
|
632
|
-
print("🔧 Start a server:")
|
|
633
|
-
print(f" • HTTP-managed: {sys.executable} {__file__} start --port {port}")
|
|
634
|
-
daemon_script = self.project_root / "src" / "claude_mpm" / "scripts" / "socketio_daemon.py"
|
|
635
|
-
if port == self.base_port:
|
|
636
|
-
print(f" • Daemon-managed: {daemon_script} start")
|
|
637
|
-
print()
|
|
638
|
-
|
|
639
|
-
# Port conflict check
|
|
640
|
-
try:
|
|
641
|
-
import socket
|
|
642
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
643
|
-
result = s.connect_ex(('localhost', port))
|
|
644
|
-
if result == 0:
|
|
645
|
-
print("🌐 Port Status: IN USE")
|
|
646
|
-
if not http_server and not daemon_server:
|
|
647
|
-
print(f" ⚠️ Port {port} is occupied by unknown process")
|
|
648
|
-
if PSUTIL_AVAILABLE:
|
|
649
|
-
print(" 🔍 Use 'lsof -i :{port}' or 'netstat -tulpn | grep {port}' to identify")
|
|
650
|
-
else:
|
|
651
|
-
print("🌐 Port Status: AVAILABLE")
|
|
652
|
-
except:
|
|
653
|
-
print("🌐 Port Status: UNKNOWN")
|
|
654
|
-
|
|
655
|
-
print()
|
|
656
|
-
print("📚 Management Style Comparison:")
|
|
657
|
-
print(" HTTP-managed:")
|
|
658
|
-
print(" • Pros: Full API, stats, multi-instance support")
|
|
659
|
-
print(" • Cons: More complex, requires HTTP client")
|
|
660
|
-
print(" Daemon-managed:")
|
|
661
|
-
print(" • Pros: Simple, lightweight, traditional daemon")
|
|
662
|
-
print(" • Cons: Single instance, basic management")
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
def main():
|
|
666
|
-
"""Main CLI entry point."""
|
|
667
|
-
parser = argparse.ArgumentParser(description="Socket.IO Server Manager")
|
|
668
|
-
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
|
669
|
-
|
|
670
|
-
# Start command
|
|
671
|
-
start_parser = subparsers.add_parser('start', help='Start a Socket.IO server')
|
|
672
|
-
start_parser.add_argument('--port', type=int, help='Port to bind to (auto-detect if not specified)')
|
|
673
|
-
start_parser.add_argument('--host', default='localhost', help='Host to bind to')
|
|
674
|
-
start_parser.add_argument('--server-id', help='Custom server ID')
|
|
675
|
-
|
|
676
|
-
# Stop command
|
|
677
|
-
stop_parser = subparsers.add_parser('stop', help='Stop a Socket.IO server')
|
|
678
|
-
stop_parser.add_argument('--port', type=int, help='Port of server to stop')
|
|
679
|
-
stop_parser.add_argument('--server-id', help='Server ID to stop')
|
|
680
|
-
|
|
681
|
-
# Restart command
|
|
682
|
-
restart_parser = subparsers.add_parser('restart', help='Restart a Socket.IO server')
|
|
683
|
-
restart_parser.add_argument('--port', type=int, help='Port of server to restart')
|
|
684
|
-
restart_parser.add_argument('--server-id', help='Server ID to restart')
|
|
685
|
-
|
|
686
|
-
# Status command
|
|
687
|
-
status_parser = subparsers.add_parser('status', help='Show server status')
|
|
688
|
-
status_parser.add_argument('-v', '--verbose', action='store_true', help='Show detailed information')
|
|
689
|
-
|
|
690
|
-
# Health check command
|
|
691
|
-
health_parser = subparsers.add_parser('health', help='Perform health check')
|
|
692
|
-
health_parser.add_argument('--port', type=int, help='Port to check (all servers if not specified)')
|
|
693
|
-
|
|
694
|
-
# Install dependencies command
|
|
695
|
-
subparsers.add_parser('install-deps', help='Install required dependencies')
|
|
696
|
-
|
|
697
|
-
# List command
|
|
698
|
-
subparsers.add_parser('list', help='List running servers')
|
|
699
|
-
|
|
700
|
-
# Diagnose command
|
|
701
|
-
diagnose_parser = subparsers.add_parser('diagnose', help='Diagnose server management conflicts')
|
|
702
|
-
diagnose_parser.add_argument('--port', type=int, default=8765, help='Port to diagnose')
|
|
703
|
-
|
|
704
|
-
args = parser.parse_args()
|
|
705
|
-
|
|
706
|
-
if not args.command:
|
|
707
|
-
parser.print_help()
|
|
708
|
-
return
|
|
709
|
-
|
|
710
|
-
manager = ServerManager()
|
|
711
|
-
|
|
712
|
-
if args.command == 'start':
|
|
713
|
-
success = manager.start_server(
|
|
714
|
-
port=args.port,
|
|
715
|
-
server_id=args.server_id,
|
|
716
|
-
host=args.host
|
|
717
|
-
)
|
|
718
|
-
sys.exit(0 if success else 1)
|
|
719
|
-
|
|
720
|
-
elif args.command == 'stop':
|
|
721
|
-
success = manager.stop_server(
|
|
722
|
-
port=args.port,
|
|
723
|
-
server_id=args.server_id
|
|
724
|
-
)
|
|
725
|
-
sys.exit(0 if success else 1)
|
|
726
|
-
|
|
727
|
-
elif args.command == 'restart':
|
|
728
|
-
success = manager.restart_server(
|
|
729
|
-
port=args.port,
|
|
730
|
-
server_id=args.server_id
|
|
731
|
-
)
|
|
732
|
-
sys.exit(0 if success else 1)
|
|
733
|
-
|
|
734
|
-
elif args.command == 'status':
|
|
735
|
-
manager.status(verbose=args.verbose)
|
|
736
|
-
|
|
737
|
-
elif args.command == 'health':
|
|
738
|
-
healthy = manager.health_check(port=args.port)
|
|
739
|
-
sys.exit(0 if healthy else 1)
|
|
740
|
-
|
|
741
|
-
elif args.command == 'install-deps':
|
|
742
|
-
success = manager.install_dependencies()
|
|
743
|
-
sys.exit(0 if success else 1)
|
|
744
|
-
|
|
745
|
-
elif args.command == 'list':
|
|
746
|
-
manager.status(verbose=False)
|
|
747
|
-
|
|
748
|
-
elif args.command == 'diagnose':
|
|
749
|
-
manager.diagnose_conflicts(port=args.port)
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
if __name__ == "__main__":
|
|
753
|
-
main()
|