py-mcpdock-cli 1.0.13__py3-none-any.whl → 1.0.18__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.
@@ -0,0 +1,656 @@
1
+ """
2
+ 进程管理工具模块
3
+
4
+ 该模块提供了与进程管理相关的工具函数,包括:
5
+ - 查找进程ID
6
+ - 保存PID文件
7
+ - 读取PID文件
8
+ """
9
+ import json
10
+ import os
11
+ import time
12
+ import socket
13
+ import psutil
14
+ import sys
15
+ import tempfile
16
+ from filelock import FileLock
17
+ from typing import List, Dict, Any, Optional, Tuple
18
+ from logging import error
19
+ from ..utils.logger import verbose
20
+
21
+
22
+ def find_server_process_by_command(command: str, args: List[str]) -> Optional[int]:
23
+ """
24
+ 根据命令和参数查找匹配的进程并返回其PID
25
+ 针对Python CLI启动的MCP服务器进程进行了特殊处理
26
+
27
+ Args:
28
+ command: 命令名称 (如 'python' 或 'npx')
29
+ args: 命令参数 (如 ['-m', 'server_name'] 或 ['-y', '@package/name'])
30
+
31
+ Returns:
32
+ 匹配进程的PID,如果没找到则返回None
33
+ """
34
+ try:
35
+ command_str = ' '.join([command] + args)
36
+ verbose(f"[Runner] 查找进程,命令: {command_str}")
37
+
38
+ import sys
39
+
40
+ # 检测是否为Windows系统
41
+ is_windows = sys.platform == "win32"
42
+
43
+ # 提取关键标识符 - 对于MCP服务器尤其重要
44
+ # 通常是最后一个参数或包含@的参数
45
+ key_identifiers = []
46
+ server_name = None
47
+
48
+ # 查找包含 @ 的参数,通常是包名或服务器标识
49
+ for arg in args:
50
+ if '@' in arg:
51
+ key_identifiers.append(arg)
52
+ # 保存可能的服务器名称
53
+ if '/' in arg:
54
+ # 对于 @scope/package 格式,提取 package 部分
55
+ server_name = arg.split('/', 1)[1] if arg.startswith('@') else arg
56
+
57
+ # 如果没有找到包含 @ 的参数,使用最后一个参数
58
+ if not key_identifiers and args:
59
+ key_identifiers.append(args[-1])
60
+ server_name = args[-1]
61
+
62
+ # 加入命令本身作为标识符
63
+ if command not in ['python', 'python3', 'node', 'npx', 'uvx', 'npm']:
64
+ key_identifiers.append(command)
65
+
66
+ verbose(f"[Runner] 使用关键标识符: {key_identifiers}")
67
+
68
+ # 1. 首先尝试直接匹配完整命令行
69
+ for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
70
+ try:
71
+ if proc.info['cmdline']:
72
+ proc_cmd = ' '.join(proc.info['cmdline'])
73
+ # 检查命令是否完全匹配
74
+ if command_str in proc_cmd:
75
+ verbose(f"[Runner] 找到完全匹配进程 PID: {proc.info['pid']}, 命令行: {proc_cmd}")
76
+ return proc.info['pid']
77
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
78
+ pass
79
+
80
+ # 2. 处理NPX和Node特殊情况
81
+ if command.lower() == 'npx':
82
+ # 通常npx会启动node进程
83
+ node_patterns = ["node", "node.exe"]
84
+ if len(args) >= 2 and args[0] == '-y':
85
+ # 如果是npx -y package的格式,实际执行的可能是node命令
86
+ package_name = args[1]
87
+ verbose(f"[Runner] NPX命令检测:寻找包含 '{package_name}' 的Node进程")
88
+
89
+ for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
90
+ try:
91
+ if proc.info['cmdline']:
92
+ cmdline = proc.info['cmdline']
93
+ cmd_str = ' '.join(cmdline)
94
+
95
+ # 检查是否是node进程并且命令行包含包名
96
+ proc_name = os.path.basename(cmdline[0].lower())
97
+ if (any(pattern in proc_name for pattern in node_patterns) and
98
+ package_name in cmd_str):
99
+ verbose(f"[Runner] 找到匹配的NPX启动的进程 PID: {proc.info['pid']}, 命令行: {cmd_str}")
100
+ return proc.info['pid']
101
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
102
+ pass
103
+
104
+ # 3. 查找Python CLI启动的子进程
105
+ parent_processes = []
106
+ child_processes = []
107
+
108
+ # 特别检查可能是Python/Node启动的进程
109
+ potential_parent_commands = ['python', 'python3', 'node', 'node.exe']
110
+
111
+ # 查找所有潜在的父进程和子进程
112
+ for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
113
+ try:
114
+ if proc.info['cmdline']:
115
+ cmdline = proc.info['cmdline']
116
+ proc_cmd = ' '.join(cmdline)
117
+
118
+ # 检查是否是可能的父进程
119
+ proc_name = os.path.basename(cmdline[0].lower())
120
+ if any(parent_cmd in proc_name for parent_cmd in potential_parent_commands):
121
+ # 如果命令行包含任何关键标识符,这可能是我们要找的进程
122
+ if any(identifier in proc_cmd for identifier in key_identifiers):
123
+ parent_processes.append((proc.info['pid'], proc_cmd))
124
+
125
+ # 记录所有可能的子进程
126
+ if server_name and server_name.lower() in proc_cmd.lower():
127
+ child_processes.append((proc.info['pid'], proc_cmd))
128
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
129
+ pass
130
+
131
+ # 4. 如果找到了可能的父进程,返回最匹配的一个
132
+ if parent_processes:
133
+ # 按照命令行长度排序,更长的命令行通常包含更多信息,匹配度更高
134
+ parent_processes.sort(key=lambda x: len(x[1]), reverse=True)
135
+ verbose(f"[Runner] 找到最匹配的父进程 PID: {parent_processes[0][0]}, 命令行: {parent_processes[0][1]}")
136
+ return parent_processes[0][0]
137
+
138
+ # 5. 如果没有找到父进程,但找到了可能的子进程,返回第一个
139
+ if child_processes:
140
+ verbose(f"[Runner] 找到可能的服务器进程 PID: {child_processes[0][0]}, 命令行: {child_processes[0][1]}")
141
+ return child_processes[0][0]
142
+
143
+ verbose("[Runner] 未找到匹配的服务器进程")
144
+ return None
145
+ except Exception as e:
146
+ error(f"[Runner] 查找进程异常: {str(e)}")
147
+ return None
148
+
149
+
150
+ def save_pid_to_file(server_pid: int, server_name: str, client_name: str, command: str, args: List[str]) -> Optional[str]:
151
+ """
152
+ 将进程ID保存到PID文件,记录MCP服务器的使用情况,包括客户端名称。
153
+ 同时查找并保存与该MCP服务器相关的进程ID(包括父进程和子进程)。
154
+
155
+ Args:
156
+ server_pid: 服务器进程ID
157
+ server_name: MCP服务器名称
158
+ client_name: 客户端名称
159
+ command: 启动命令
160
+ args: 命令参数
161
+
162
+ Returns:
163
+ 成功时返回PID文件路径,失败时返回None
164
+ """
165
+ # 获取PID文件路径
166
+ current_dir = os.path.dirname(os.path.abspath(__file__))
167
+ base_dir = os.path.dirname(os.path.dirname(current_dir))
168
+ pid_file = os.path.join(base_dir, "pids", "run-mcp-srv.pid")
169
+ lock_file = f"{pid_file}.lock"
170
+
171
+ try:
172
+ # 获取当前CLI进程的PID
173
+ cli_pid = os.getpid()
174
+ verbose(f"[Runner] 当前CLI进程PID: {cli_pid}")
175
+
176
+ # 获取客户端进程ID(可能是第三级父进程)
177
+ client_pid = None
178
+ try:
179
+ # 尝试找到三级父进程(如果存在)
180
+ try:
181
+ process = psutil.Process(server_pid)
182
+ parent = process.parent()
183
+ if parent:
184
+ grandparent = parent.parent()
185
+ if grandparent:
186
+ # 找到三级父进程
187
+ client_pid = grandparent.pid
188
+ if client_pid:
189
+ verbose(f"[Runner] 找到MCP服务器的三级父进程 PID: {client_pid}")
190
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
191
+ verbose(f"[Runner] 无法获取进程 {server_pid} 的父进程")
192
+
193
+ verbose(f"[Runner] 为MCP服务器 {server_name} 找到客户端进程ID: {client_pid}")
194
+ except Exception as e:
195
+ verbose(f"[Runner] 查找客户端进程时出错: {str(e)}")
196
+
197
+ with FileLock(lock_file, timeout=5):
198
+ pid_data = {}
199
+
200
+ # 如果文件存在,读取现有数据
201
+ if os.path.exists(pid_file):
202
+ with open(pid_file, 'r') as f:
203
+ content = f.read().strip()
204
+ if not content:
205
+ verbose(f"[Runner] PID文件为空: {pid_file}")
206
+ content = '{}'
207
+ pid_data = json.loads(content)
208
+
209
+ # 如果当前服务器名称不存在,初始化为数组
210
+ if server_name not in pid_data:
211
+ pid_data[server_name] = []
212
+
213
+ # 客户端信息
214
+ client_info = {
215
+ "client_name": client_name,
216
+ "server_id": server_pid, # 服务器进程ID
217
+ "client_pid": client_pid, # 客户端进程ID(第三级父进程)
218
+ "cli_pid": cli_pid, # 当前CLI进程的PID
219
+ "command": command,
220
+ "args": args,
221
+ "timestamp": int(time.time()),
222
+ "hostname": os.uname().nodename if hasattr(os, 'uname') else socket.gethostname()
223
+ }
224
+
225
+ # 检查是否已存在相同客户端,如果存在则更新
226
+ found = False
227
+ for i, entry in enumerate(pid_data[server_name]):
228
+ if entry.get("client_name") == client_name:
229
+ pid_data[server_name][i] = client_info
230
+ found = True
231
+ break
232
+
233
+ # 如果不存在,则添加到列表
234
+ if not found:
235
+ pid_data[server_name].append(client_info)
236
+
237
+ # 写回文件
238
+ with open(pid_file, 'w') as f:
239
+
240
+ json.dump(pid_data, f, indent=2)
241
+
242
+ verbose(f"[Runner] PID信息已保存到文件: {pid_file}")
243
+ return pid_file
244
+
245
+ except Exception as e:
246
+ error(f"[Runner] 保存PID文件失败: {str(e)}")
247
+ return None
248
+
249
+
250
+ def read_pid_file() -> Optional[Dict[str, Any]]:
251
+ """
252
+ 读取PID文件,按优先级尝试查找并读取PID文件
253
+ 使用文件锁防止与并发写入操作的冲突
254
+
255
+ Returns:
256
+ 成功时返回PID信息字典,失败时返回None
257
+ """
258
+ # 按照写入文件时的优先级顺序尝试查找
259
+ # 1. 项目内部日志目录
260
+ try:
261
+ current_dir = os.path.dirname(os.path.abspath(__file__))
262
+ base_dir = os.path.dirname(os.path.dirname(current_dir))
263
+ pid_file = os.path.join(base_dir, "logs", "run-mcp-srv.pid")
264
+ lock_file = f"{pid_file}.lock"
265
+
266
+ if os.path.exists(pid_file):
267
+ # 使用文件锁防止与并发写入操作冲突
268
+ with FileLock(lock_file, timeout=2):
269
+ with open(pid_file, 'r') as f:
270
+ pid_info = json.load(f)
271
+ verbose(f"[Runner] 从项目日志目录读取到PID信息: {pid_info}")
272
+ return pid_info
273
+ except Exception as e:
274
+ verbose(f"[Runner] 读取项目日志目录PID文件失败: {str(e)}")
275
+
276
+ # 2. 用户主目录
277
+ try:
278
+ home_dir = os.path.expanduser("~")
279
+ pid_file = os.path.join(home_dir, ".mcpdock", "run-mcp-srv.pid")
280
+ lock_file = f"{pid_file}.lock"
281
+
282
+ if os.path.exists(pid_file):
283
+ # 使用文件锁防止与并发写入操作冲突
284
+ with FileLock(lock_file, timeout=2):
285
+ with open(pid_file, 'r') as f:
286
+ pid_info = json.load(f)
287
+ verbose(f"[Runner] 从用户主目录读取到PID信息: {pid_info}")
288
+ return pid_info
289
+ except Exception as e:
290
+ verbose(f"[Runner] 读取用户主目录PID文件失败: {str(e)}")
291
+
292
+ # 3. 系统临时目录
293
+ try:
294
+ temp_dir = tempfile.gettempdir()
295
+ pid_file = os.path.join(temp_dir, "mcpdock-srv.pid")
296
+ lock_file = f"{pid_file}.lock"
297
+
298
+ if os.path.exists(pid_file):
299
+ # 使用文件锁防止与并发写入操作冲突
300
+ with FileLock(lock_file, timeout=2):
301
+ with open(pid_file, 'r') as f:
302
+ pid_info = json.load(f)
303
+ verbose(f"[Runner] 从临时目录读取到PID信息: {pid_info}")
304
+ return pid_info
305
+ except Exception as e:
306
+ verbose(f"[Runner] 读取临时目录PID文件失败: {str(e)}")
307
+
308
+ verbose("[Runner] 在所有位置均未找到有效的PID文件")
309
+ return None
310
+
311
+
312
+ def is_process_running(pid: int) -> bool:
313
+ """
314
+ 检查指定PID的进程是否正在运行
315
+
316
+ Args:
317
+ pid: 进程ID
318
+
319
+ Returns:
320
+ 进程是否存在且正在运行
321
+ """
322
+ try:
323
+ # 检查进程是否存在
324
+ process = psutil.Process(pid)
325
+ # 进一步检查进程状态
326
+ return process.is_running() and process.status() != psutil.STATUS_ZOMBIE
327
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
328
+ return False
329
+ except Exception as e:
330
+ error(f"[Runner] 检查进程状态异常: {str(e)}")
331
+ return False
332
+
333
+
334
+ def check_server_process() -> Tuple[bool, Optional[Dict[str, Any]]]:
335
+ """
336
+ 检查MCP服务器进程是否正在运行
337
+
338
+ Returns:
339
+ (是否正在运行, PID信息)
340
+ """
341
+ # 尝试读取PID文件
342
+ pid_info = read_pid_file()
343
+ if not pid_info:
344
+ return False, None
345
+
346
+ # 获取PID - 兼容新旧格式:先尝试读取server_id,如果不存在则使用pid字段
347
+ server_id = pid_info.get("server_id")
348
+ if not server_id:
349
+ server_id = pid_info.get("pid") # 兼容旧格式
350
+
351
+ if not server_id:
352
+ return False, pid_info
353
+
354
+ # 检查进程是否在运行
355
+ is_running = is_process_running(server_id)
356
+ return is_running, pid_info
357
+
358
+
359
+ def check_server_by_package_name(package_name: str) -> Tuple[bool, Optional[int]]:
360
+ """
361
+ 通过包名检查MCP服务器是否正在运行
362
+
363
+ 这个方法不依赖PID文件,而是直接搜索运行中的进程,查找包含指定包名的进程
364
+
365
+ Args:
366
+ package_name: 服务器包名,如 '@suekou/mcp-notion-server'
367
+
368
+ Returns:
369
+ (是否正在运行, 进程ID如果找到)
370
+ """
371
+ try:
372
+ verbose(f"[Runner] 通过包名搜索服务器进程: {package_name}")
373
+
374
+ # 处理包名格式
375
+ server_id = package_name
376
+ server_name = None
377
+
378
+ # 处理 @scope/package 格式
379
+ if '/' in package_name and package_name.startswith('@'):
380
+ # 提取包名的主要部分(不含scope)
381
+ server_name = package_name.split('/', 1)[1]
382
+ else:
383
+ server_name = package_name
384
+
385
+ # 去除特殊字符,便于匹配
386
+ clean_name = server_name.replace('-', '').lower() if server_name else ''
387
+
388
+ # 潜在进程类型
389
+ potential_cmd_patterns = ['node', 'python', 'python3']
390
+ matching_processes = []
391
+
392
+ # 搜索所有匹配的进程
393
+ for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
394
+ try:
395
+ if proc.info['cmdline']:
396
+ cmdline = proc.info['cmdline']
397
+ cmd_str = ' '.join(cmdline).lower()
398
+
399
+ # 检查命令行是否包含包名
400
+ if package_name.lower() in cmd_str:
401
+ matching_processes.append((proc.info['pid'], cmd_str, 3)) # 完全匹配权重最高
402
+ elif server_name and server_name.lower() in cmd_str:
403
+ matching_processes.append((proc.info['pid'], cmd_str, 2)) # 不含scope的包名匹配
404
+ elif clean_name and clean_name in cmd_str.replace('-', ''):
405
+ matching_processes.append((proc.info['pid'], cmd_str, 1)) # 去除横线后的匹配
406
+
407
+ # 特殊检测: 看命令行是否有可能是MCP服务器
408
+ proc_base = os.path.basename(cmdline[0]).lower()
409
+ if any(pattern in proc_base for pattern in potential_cmd_patterns):
410
+ if 'mcp' in cmd_str and (server_name in cmd_str or clean_name in cmd_str.replace('-', '')):
411
+ matching_processes.append((proc.info['pid'], cmd_str, 2)) # MCP相关进程
412
+
413
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
414
+ pass
415
+
416
+ # 按照匹配权重和命令行长度排序
417
+ if matching_processes:
418
+ # 先按权重排序,权重相同时按命令行长度排序
419
+ matching_processes.sort(key=lambda x: (x[2], len(x[1])), reverse=True)
420
+ best_match = matching_processes[0]
421
+ verbose(f"[Runner] 找到匹配的服务器进程 PID: {best_match[0]}, 命令行: {best_match[1]}")
422
+ return True, best_match[0]
423
+
424
+ verbose(f"[Runner] 未找到包含 '{package_name}' 的服务器进程")
425
+ return False, None
426
+ except Exception as e:
427
+ error(f"[Runner] 通过包名查找进程异常: {str(e)}")
428
+ return False, None
429
+
430
+
431
+ def find_child_processes(parent_pid: int, server_id: Optional[str] = None) -> List[Tuple[int, str]]:
432
+ """
433
+ 查找指定父进程的所有子进程
434
+
435
+ Args:
436
+ parent_pid: 父进程ID
437
+ server_id: 可选的服务器标识符,用于过滤子进程
438
+
439
+ Returns:
440
+ 匹配的子进程列表,每项包含(pid, 命令行)
441
+ """
442
+ try:
443
+ matching_children = []
444
+
445
+ # 尝试获取父进程
446
+ try:
447
+ parent = psutil.Process(parent_pid)
448
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
449
+ verbose(f"[Runner] 无法访问父进程 PID: {parent_pid}")
450
+ return matching_children
451
+
452
+ # 获取所有子进程
453
+ children = parent.children(recursive=True)
454
+
455
+ # 如果没有指定服务器ID,返回所有子进程
456
+ if not server_id:
457
+ return [(child.pid, ' '.join(child.cmdline())) for child in children if child.is_running()]
458
+
459
+ # 处理服务器标识符以便匹配
460
+ server_name = None
461
+ clean_name = None
462
+
463
+ # 处理 @scope/package 格式
464
+ if server_id and '/' in server_id and server_id.startswith('@'):
465
+ server_name = server_id.split('/', 1)[1]
466
+ else:
467
+ server_name = server_id
468
+
469
+ # 去除特殊字符以便更灵活匹配
470
+ if server_name:
471
+ clean_name = server_name.replace('-', '').lower()
472
+
473
+ # 过滤匹配的子进程
474
+ for child in children:
475
+ try:
476
+ if not child.is_running():
477
+ continue
478
+
479
+ cmd_str = ' '.join(child.cmdline()).lower()
480
+
481
+ # 使用多种匹配策略
482
+ if server_id and server_id.lower() in cmd_str:
483
+ matching_children.append((child.pid, cmd_str))
484
+ elif server_name and server_name.lower() in cmd_str:
485
+ matching_children.append((child.pid, cmd_str))
486
+ elif clean_name and clean_name in cmd_str.replace('-', ''):
487
+ matching_children.append((child.pid, cmd_str))
488
+ elif 'mcp' in cmd_str and (server_name in cmd_str or
489
+ (clean_name and clean_name in cmd_str.replace('-', ''))):
490
+ matching_children.append((child.pid, cmd_str))
491
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
492
+ # 子进程可能已经结束
493
+ continue
494
+
495
+ # 按命令行长度排序,通常更长的命令行包含更多信息
496
+ matching_children.sort(key=lambda x: len(x[1]), reverse=True)
497
+
498
+ if matching_children:
499
+ verbose(f"[Runner] 找到 {len(matching_children)} 个匹配的子进程,第一个: "
500
+ f"PID: {matching_children[0][0]}, 命令行: {matching_children[0][1]}")
501
+ else:
502
+ verbose(f"[Runner] 未找到父进程 {parent_pid} 下匹配的子进程")
503
+
504
+ return matching_children
505
+ except Exception as e:
506
+ error(f"[Runner] 查找子进程异常: {str(e)}")
507
+ return []
508
+
509
+
510
+ def find_server_process_from_current() -> Tuple[bool, Optional[int], Optional[str]]:
511
+ """
512
+ 从当前进程开始,查找MCP服务器进程
513
+
514
+ 利用进程的父子关系,查找当前CLI工具启动的MCP服务器进程
515
+
516
+ Returns:
517
+ (是否找到, 进程ID, 命令行)
518
+ """
519
+ try:
520
+ # 获取当前进程ID
521
+ current_pid = os.getpid()
522
+ verbose(f"[Runner] 当前进程ID: {current_pid}")
523
+
524
+ # 查找当前进程启动的所有子进程
525
+ child_processes = find_child_processes(current_pid)
526
+
527
+ if not child_processes:
528
+ verbose("[Runner] 当前进程没有子进程")
529
+ return False, None, None
530
+
531
+ # 过滤可能是MCP服务器的子进程
532
+ mcp_candidates = []
533
+
534
+ for pid, cmd_str in child_processes:
535
+ cmd_lower = cmd_str.lower()
536
+ # 查找命令行中包含MCP相关关键词的进程
537
+ if any(kw in cmd_lower for kw in ['mcp', 'notion-server', 'node', 'python']):
538
+ mcp_candidates.append((pid, cmd_str))
539
+
540
+ if mcp_candidates:
541
+ # 按命令行长度排序
542
+ mcp_candidates.sort(key=lambda x: len(x[1]), reverse=True)
543
+ best_match = mcp_candidates[0]
544
+ verbose(f"[Runner] 找到最可能的MCP服务器进程: PID: {best_match[0]}, 命令行: {best_match[1]}")
545
+ return True, best_match[0], best_match[1]
546
+
547
+ verbose("[Runner] 未找到可能的MCP服务器子进程")
548
+ return False, None, None
549
+ except Exception as e:
550
+ error(f"[Runner] 从当前进程查找服务器异常: {str(e)}")
551
+ return False, None, None
552
+
553
+
554
+ def remove_pid_by_server_name(server_name: str, client_name: Optional[str] = None, target_pid: Optional[int] = None) -> None:
555
+ """
556
+ 根据服务器名称或目标进程ID从PID文件中移除对应的PID信息。
557
+ 支持通过当前CLI进程ID匹配删除记录。
558
+
559
+ Args:
560
+ server_name: 服务器名称
561
+ client_name: 客户端名称(如果提供,只移除该客户端的记录;否则移除服务器的所有记录)
562
+ target_pid: 目标进程ID(如果提供,会匹配server_id、client_pid和cli_pid)
563
+
564
+ Returns:
565
+ None
566
+ """
567
+ try:
568
+ # 获取当前CLI进程ID
569
+ current_cli_pid = os.getpid()
570
+ verbose(f"[Runner] 当前CLI进程PID: {current_cli_pid}")
571
+
572
+ # 获取PID文件路径
573
+ current_dir = os.path.dirname(os.path.abspath(__file__))
574
+ base_dir = os.path.dirname(os.path.dirname(current_dir))
575
+ pid_file = os.path.join(base_dir, "pids", "run-mcp-srv.pid")
576
+ lock_file = f"{pid_file}.lock"
577
+
578
+ # 使用文件锁防止并发写入
579
+ with FileLock(lock_file, timeout=5):
580
+ if not os.path.exists(pid_file):
581
+ verbose(f"[Runner] PID文件不存在: {pid_file}")
582
+ return
583
+
584
+ with open(pid_file, 'r') as f:
585
+ content = f.read().strip()
586
+ if not content:
587
+ verbose(f"[Runner] PID文件为空: {pid_file}")
588
+ return
589
+ pid_data = json.loads(content)
590
+
591
+ # 检查服务器名称是否存在
592
+ if server_name not in pid_data:
593
+ verbose(f"[Runner] 服务器名称 {server_name} 不存在于PID文件中")
594
+ return
595
+
596
+ entries_to_remove = []
597
+
598
+ # 优先使用当前CLI进程ID匹配记录
599
+ for i, entry in enumerate(pid_data[server_name]):
600
+ cli_pid = entry.get("cli_pid")
601
+
602
+ # 如果当前CLI进程ID与记录中的CLI PID匹配,标记为删除
603
+ if cli_pid == current_cli_pid:
604
+ entries_to_remove.append(i)
605
+ verbose(f"[Runner] 通过当前CLI进程ID {current_cli_pid} 找到匹配记录,客户端: {entry.get('client_name')}")
606
+
607
+ # 如果没有通过CLI PID找到记录且提供了目标进程ID,查找所有包含该PID的记录
608
+ if not entries_to_remove and target_pid:
609
+ for i, entry in enumerate(pid_data[server_name]):
610
+ server_id = entry.get("server_id")
611
+ client_pid = entry.get("client_pid")
612
+ cli_pid = entry.get("cli_pid")
613
+
614
+ # 检查目标PID是否匹配服务器ID、客户端进程ID或CLI进程ID
615
+ if server_id == target_pid or client_pid == target_pid or cli_pid == target_pid:
616
+ entries_to_remove.append(i)
617
+ verbose(f"[Runner] 已找到匹配目标PID {target_pid} 的记录,客户端: {entry.get('client_name')}")
618
+
619
+ # 如果通过PID未找到记录且提供了客户端名称,只移除该客户端的记录
620
+ elif not entries_to_remove and client_name:
621
+ for i, entry in enumerate(pid_data[server_name]):
622
+ if entry.get("client_name") == client_name:
623
+ entries_to_remove.append(i)
624
+ verbose(f"[Runner] 已找到客户端 {client_name} 的记录")
625
+ break
626
+
627
+ # 如果既没有通过PID找到记录也没有指定客户端名称,移除服务器的所有记录
628
+ elif not entries_to_remove and not target_pid and not client_name:
629
+ verbose(f"[Runner] 移除服务器 {server_name} 的所有记录")
630
+ del pid_data[server_name]
631
+
632
+ # 按索引从高到低删除,避免索引变化导致的问题
633
+ if entries_to_remove:
634
+ entries_to_remove.sort(reverse=True)
635
+ for i in entries_to_remove:
636
+ removed_entry = pid_data[server_name].pop(i)
637
+ verbose(f"[Runner] 已从文件中移除记录,客户端: {removed_entry.get('client_name')}, "
638
+ f"服务器ID: {removed_entry.get('server_id')}, 客户端进程ID: {removed_entry.get('client_pid')}, "
639
+ f"CLI进程ID: {removed_entry.get('cli_pid')}")
640
+
641
+ # 如果该服务器没有客户端了,移除服务器记录
642
+ if not pid_data[server_name]:
643
+ del pid_data[server_name]
644
+ verbose(f"[Runner] 服务器 {server_name} 没有客户端记录,已移除")
645
+
646
+ # 如果文件中没有服务器了,删除文件
647
+ if not pid_data:
648
+ os.remove(pid_file)
649
+ verbose(f"[Runner] PID文件已被删除: {pid_file}")
650
+ else:
651
+ # 否则更新文件内容
652
+ with open(pid_file, 'w') as f:
653
+ json.dump(pid_data, f, indent=2)
654
+ verbose(f"[Runner] PID文件已更新")
655
+ except Exception as e:
656
+ error(f"[Runner] 移除PID记录失败: {str(e)}")