claude-mpm 4.2.24__py3-none-any.whl → 4.2.26__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.
@@ -23,6 +23,7 @@ from pathlib import Path
23
23
  from typing import Optional
24
24
 
25
25
  from ...core.logging_config import get_logger
26
+ from ..hook_installer_service import HookInstallerService
26
27
  from .management.health import HealthMonitor
27
28
  from .management.lifecycle import DaemonLifecycle
28
29
  from .server import UnifiedMonitorServer
@@ -59,9 +60,9 @@ class UnifiedMonitorDaemon:
59
60
 
60
61
  # Daemon management with port for verification
61
62
  self.lifecycle = DaemonLifecycle(
62
- pid_file=pid_file or self._get_default_pid_file(),
63
+ pid_file=pid_file or self._get_default_pid_file(),
63
64
  log_file=log_file,
64
- port=port
65
+ port=port,
65
66
  )
66
67
 
67
68
  # Core server
@@ -70,6 +71,9 @@ class UnifiedMonitorDaemon:
70
71
  # Health monitoring
71
72
  self.health_monitor = HealthMonitor(port=port)
72
73
 
74
+ # Hook installer service
75
+ self.hook_installer = HookInstallerService()
76
+
73
77
  # State
74
78
  self.running = False
75
79
  self.shutdown_event = threading.Event()
@@ -98,9 +102,90 @@ class UnifiedMonitorDaemon:
98
102
  self.logger.error(f"Failed to start unified monitor daemon: {e}")
99
103
  return False
100
104
 
105
+ def _cleanup_port_conflicts(self) -> bool:
106
+ """Try to clean up any processes using our port.
107
+
108
+ Returns:
109
+ True if cleanup was successful, False otherwise
110
+ """
111
+ try:
112
+ # Find process using the port
113
+ import subprocess
114
+ result = subprocess.run(
115
+ ["lsof", "-ti", f":{self.port}"],
116
+ capture_output=True,
117
+ text=True
118
+ )
119
+
120
+ if result.returncode == 0 and result.stdout.strip():
121
+ pids = result.stdout.strip().split('\n')
122
+ for pid_str in pids:
123
+ try:
124
+ pid = int(pid_str.strip())
125
+ self.logger.info(f"Found process {pid} using port {self.port}")
126
+
127
+ # Check if it's a claude-mpm process
128
+ process_info = subprocess.run(
129
+ ["ps", "-p", str(pid), "-o", "comm="],
130
+ capture_output=True,
131
+ text=True
132
+ )
133
+
134
+ if "python" in process_info.stdout.lower() or "claude" in process_info.stdout.lower():
135
+ self.logger.info(f"Killing process {pid} (appears to be Python/Claude related)")
136
+ os.kill(pid, signal.SIGTERM)
137
+ time.sleep(1)
138
+
139
+ # Check if still alive
140
+ try:
141
+ os.kill(pid, 0)
142
+ # Still alive, force kill
143
+ self.logger.warning(f"Process {pid} didn't terminate, force killing")
144
+ os.kill(pid, signal.SIGKILL)
145
+ time.sleep(1)
146
+ except ProcessLookupError:
147
+ pass
148
+ else:
149
+ self.logger.warning(f"Process {pid} is not a Claude MPM process: {process_info.stdout}")
150
+ return False
151
+ except (ValueError, ProcessLookupError) as e:
152
+ self.logger.debug(f"Error handling PID {pid_str}: {e}")
153
+ continue
154
+
155
+ return True
156
+
157
+ except FileNotFoundError:
158
+ # lsof not available, try alternative method
159
+ self.logger.debug("lsof not available, using alternative cleanup")
160
+
161
+ # Check if there's an orphaned service we can identify
162
+ is_ours, pid = self.lifecycle.is_our_service(self.host)
163
+ if is_ours and pid:
164
+ try:
165
+ self.logger.info(f"Killing orphaned Claude MPM service (PID: {pid})")
166
+ os.kill(pid, signal.SIGTERM)
167
+ time.sleep(1)
168
+
169
+ # Check if still alive
170
+ try:
171
+ os.kill(pid, 0)
172
+ os.kill(pid, signal.SIGKILL)
173
+ time.sleep(1)
174
+ except ProcessLookupError:
175
+ pass
176
+
177
+ return True
178
+ except Exception as e:
179
+ self.logger.error(f"Failed to kill process: {e}")
180
+
181
+ except Exception as e:
182
+ self.logger.error(f"Error during port cleanup: {e}")
183
+
184
+ return False
185
+
101
186
  def _start_daemon(self, force_restart: bool = False) -> bool:
102
187
  """Start as background daemon process.
103
-
188
+
104
189
  Args:
105
190
  force_restart: If True, restart existing service if it's ours
106
191
  """
@@ -109,14 +194,18 @@ class UnifiedMonitorDaemon:
109
194
  # Check if already running
110
195
  if self.lifecycle.is_running():
111
196
  existing_pid = self.lifecycle.get_pid()
112
-
197
+
113
198
  if force_restart:
114
199
  # Check if it's our service
115
- self.logger.debug(f"Checking if existing daemon (PID: {existing_pid}) is our service...")
200
+ self.logger.debug(
201
+ f"Checking if existing daemon (PID: {existing_pid}) is our service..."
202
+ )
116
203
  is_ours, detected_pid = self.lifecycle.is_our_service(self.host)
117
-
204
+
118
205
  if is_ours:
119
- self.logger.info(f"Force restarting our existing claude-mpm monitor daemon (PID: {detected_pid or existing_pid})")
206
+ self.logger.info(
207
+ f"Force restarting our existing claude-mpm monitor daemon (PID: {detected_pid or existing_pid})"
208
+ )
120
209
  # Stop the existing daemon
121
210
  if self.lifecycle.stop_daemon():
122
211
  # Wait a moment for port to be released
@@ -125,19 +214,27 @@ class UnifiedMonitorDaemon:
125
214
  self.logger.error("Failed to stop existing daemon for restart")
126
215
  return False
127
216
  else:
128
- self.logger.warning(f"Port {self.port} is in use by another service (PID: {existing_pid}). Cannot force restart.")
129
- self.logger.info("To restart the claude-mpm monitor, first stop the other service or use a different port.")
217
+ self.logger.warning(
218
+ f"Port {self.port} is in use by another service (PID: {existing_pid}). Cannot force restart."
219
+ )
220
+ self.logger.info(
221
+ "To restart the claude-mpm monitor, first stop the other service or use a different port."
222
+ )
130
223
  return False
131
224
  else:
132
225
  self.logger.warning(f"Daemon already running with PID {existing_pid}")
133
226
  return False
134
-
227
+
135
228
  # Check for orphaned processes (service running but no PID file)
136
229
  elif force_restart:
137
- self.logger.debug("No PID file found, checking for orphaned claude-mpm service...")
230
+ self.logger.debug(
231
+ "No PID file found, checking for orphaned claude-mpm service..."
232
+ )
138
233
  is_ours, pid = self.lifecycle.is_our_service(self.host)
139
234
  if is_ours and pid:
140
- self.logger.info(f"Found orphaned claude-mpm monitor service (PID: {pid}), force restarting")
235
+ self.logger.info(
236
+ f"Found orphaned claude-mpm monitor service (PID: {pid}), force restarting"
237
+ )
141
238
  # Try to kill the orphaned process
142
239
  try:
143
240
  os.kill(pid, signal.SIGTERM)
@@ -155,13 +252,27 @@ class UnifiedMonitorDaemon:
155
252
  except Exception as e:
156
253
  self.logger.error(f"Failed to kill orphaned process: {e}")
157
254
  return False
158
-
159
- # Verify port is available before forking
255
+
256
+ # Check port availability and clean up if needed
160
257
  port_available, error_msg = self.lifecycle.verify_port_available(self.host)
161
258
  if not port_available:
162
- self.logger.error(error_msg)
163
- print(f"Error: {error_msg}", file=sys.stderr)
164
- return False
259
+ self.logger.warning(f"Port {self.port} is not available: {error_msg}")
260
+
261
+ # Try to identify and kill any process using the port
262
+ self.logger.info("Attempting to clean up processes on port...")
263
+ cleaned = self._cleanup_port_conflicts()
264
+
265
+ if cleaned:
266
+ # Wait a moment for port to be released
267
+ time.sleep(2)
268
+ # Check again
269
+ port_available, error_msg = self.lifecycle.verify_port_available(self.host)
270
+
271
+ if not port_available:
272
+ self.logger.error(f"Port {self.port} is still not available after cleanup: {error_msg}")
273
+ print(f"Error: {error_msg}", file=sys.stderr)
274
+ print(f"Try 'claude-mpm monitor stop' or use --force flag", file=sys.stderr)
275
+ return False
165
276
 
166
277
  # Wait for any pre-warming threads to complete before forking
167
278
  self._wait_for_prewarm_completion()
@@ -186,7 +297,7 @@ class UnifiedMonitorDaemon:
186
297
 
187
298
  def _start_foreground(self, force_restart: bool = False) -> bool:
188
299
  """Start in foreground mode.
189
-
300
+
190
301
  Args:
191
302
  force_restart: If True, restart existing service if it's ours
192
303
  """
@@ -195,14 +306,18 @@ class UnifiedMonitorDaemon:
195
306
  # Check if already running (check PID file even in foreground mode)
196
307
  if self.lifecycle.is_running():
197
308
  existing_pid = self.lifecycle.get_pid()
198
-
309
+
199
310
  if force_restart:
200
311
  # Check if it's our service
201
- self.logger.debug(f"Checking if existing daemon (PID: {existing_pid}) is our service...")
312
+ self.logger.debug(
313
+ f"Checking if existing daemon (PID: {existing_pid}) is our service..."
314
+ )
202
315
  is_ours, detected_pid = self.lifecycle.is_our_service(self.host)
203
-
316
+
204
317
  if is_ours:
205
- self.logger.info(f"Force restarting our existing claude-mpm monitor daemon (PID: {detected_pid or existing_pid})")
318
+ self.logger.info(
319
+ f"Force restarting our existing claude-mpm monitor daemon (PID: {detected_pid or existing_pid})"
320
+ )
206
321
  # Stop the existing daemon
207
322
  if self.lifecycle.stop_daemon():
208
323
  # Wait a moment for port to be released
@@ -211,21 +326,29 @@ class UnifiedMonitorDaemon:
211
326
  self.logger.error("Failed to stop existing daemon for restart")
212
327
  return False
213
328
  else:
214
- self.logger.warning(f"Port {self.port} is in use by another service (PID: {existing_pid}). Cannot force restart.")
215
- self.logger.info("To restart the claude-mpm monitor, first stop the other service or use a different port.")
329
+ self.logger.warning(
330
+ f"Port {self.port} is in use by another service (PID: {existing_pid}). Cannot force restart."
331
+ )
332
+ self.logger.info(
333
+ "To restart the claude-mpm monitor, first stop the other service or use a different port."
334
+ )
216
335
  return False
217
336
  else:
218
337
  self.logger.warning(
219
338
  f"Monitor daemon already running with PID {existing_pid}"
220
339
  )
221
340
  return False
222
-
341
+
223
342
  # Check for orphaned processes (service running but no PID file)
224
343
  elif force_restart:
225
- self.logger.debug("No PID file found, checking for orphaned claude-mpm service...")
344
+ self.logger.debug(
345
+ "No PID file found, checking for orphaned claude-mpm service..."
346
+ )
226
347
  is_ours, pid = self.lifecycle.is_our_service(self.host)
227
348
  if is_ours and pid:
228
- self.logger.info(f"Found orphaned claude-mpm monitor service (PID: {pid}), force restarting")
349
+ self.logger.info(
350
+ f"Found orphaned claude-mpm monitor service (PID: {pid}), force restarting"
351
+ )
229
352
  # Try to kill the orphaned process
230
353
  try:
231
354
  os.kill(pid, signal.SIGTERM)
@@ -271,6 +394,26 @@ class UnifiedMonitorDaemon:
271
394
  self.lifecycle._report_startup_error(error_msg)
272
395
  return False
273
396
 
397
+ # Check and install hooks if needed
398
+ try:
399
+ if not self.hook_installer.is_hooks_configured():
400
+ self.logger.info("Claude Code hooks not configured, installing...")
401
+ if self.hook_installer.install_hooks():
402
+ self.logger.info("Claude Code hooks installed successfully")
403
+ else:
404
+ # Don't fail startup if hook installation fails
405
+ # The monitor can still function without hooks
406
+ self.logger.warning(
407
+ "Failed to install Claude Code hooks. Monitor will run without hook integration."
408
+ )
409
+ else:
410
+ self.logger.info("Claude Code hooks are already configured")
411
+ except Exception as e:
412
+ # Don't fail startup if hook checking fails
413
+ self.logger.warning(
414
+ f"Error checking/installing hooks: {e}. Monitor will run without hook integration."
415
+ )
416
+
274
417
  # Start health monitoring
275
418
  self.health_monitor.start()
276
419
 
@@ -285,7 +428,7 @@ class UnifiedMonitorDaemon:
285
428
 
286
429
  self.running = True
287
430
  self.logger.info("Unified monitor daemon started successfully")
288
-
431
+
289
432
  # Report successful startup to parent (for daemon mode)
290
433
  if self.daemon_mode:
291
434
  self.lifecycle._report_startup_success()
@@ -326,7 +469,7 @@ class UnifiedMonitorDaemon:
326
469
  pid = self.lifecycle.get_pid()
327
470
  if pid and pid != os.getpid():
328
471
  # We're not the daemon process, so stop it via signal
329
- self.logger.info(f"Stopping daemon process with PID {pid}")
472
+ # Don't log here - lifecycle.stop_daemon will log
330
473
  success = self.lifecycle.stop_daemon()
331
474
  if success:
332
475
  # Clean up our local state