remote-claude 1.0.2 → 1.0.4

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.
package/utils/session.py CHANGED
@@ -20,11 +20,6 @@ SOCKET_DIR = Path("/tmp/remote-claude")
20
20
  USER_DATA_DIR = Path.home() / ".remote-claude"
21
21
  TMUX_SESSION_PREFIX = "rc-"
22
22
 
23
- # macOS AF_UNIX sun_path 限制 104 字节
24
- # /tmp/remote-claude/ = 19 字节, .sock = 5 字节
25
- _MAX_SOCKET_PATH = 104
26
- _MAX_FILENAME = _MAX_SOCKET_PATH - len(str(SOCKET_DIR)) - 1 - len(".sock")
27
-
28
23
 
29
24
  def get_env_file() -> Path:
30
25
  """获取 .env 配置文件路径"""
@@ -47,14 +42,15 @@ def ensure_user_data_dir():
47
42
 
48
43
 
49
44
  def _safe_filename(session_name: str) -> str:
50
- """将会话名转为安全文件名(/ 和 . 替换为 _),超长时用完整 MD5"""
51
- name = session_name.replace('/', '_').replace('.', '_')
52
- if len(name) <= _MAX_FILENAME:
53
- return name
54
- # 超长:直接用完整 32 字符 MD5,彻底避免路径超限
45
+ """将会话名转为安全文件名(MD5 hash)"""
55
46
  return hashlib.md5(session_name.encode()).hexdigest()
56
47
 
57
48
 
49
+ def _log_filename(session_name: str) -> str:
50
+ """将会话名转为可读的安全文件名(用于日志文件,不受路径长度限制)"""
51
+ return session_name.replace("/", "_").replace(".", "_").replace(" ", "_")
52
+
53
+
58
54
  def get_socket_path(session_name: str) -> Path:
59
55
  """获取会话的 socket 路径"""
60
56
  return SOCKET_DIR / f"{_safe_filename(session_name)}.sock"
@@ -75,6 +71,11 @@ def get_env_snapshot_path(session_name: str) -> Path:
75
71
  return SOCKET_DIR / f"{_safe_filename(session_name)}_env.json"
76
72
 
77
73
 
74
+ def get_name_file(session_name: str) -> Path:
75
+ """获取会话名映射文件路径(存储原始会话名,供 list 恢复显示名)"""
76
+ return SOCKET_DIR / f"{_safe_filename(session_name)}.name"
77
+
78
+
78
79
  def ensure_socket_dir():
79
80
  """确保 socket 目录存在"""
80
81
  SOCKET_DIR.mkdir(parents=True, exist_ok=True)
@@ -221,13 +222,19 @@ def get_process_cwd(pid: int) -> Optional[str]:
221
222
 
222
223
 
223
224
  def list_active_sessions() -> List[dict]:
224
- """列出所有活跃会话"""
225
+ """列出所有活跃会话
226
+
227
+ 注意:sock 文件的 stem 已经是 MD5 哈希(_safe_filename 的输出),
228
+ 因此直接用 stem 构造 PID/MQ 文件路径,不再经过 get_pid_file 等函数
229
+ (否则会被 _safe_filename 二次哈希)。
230
+ """
225
231
  ensure_socket_dir()
226
232
  sessions = []
227
233
 
228
234
  for sock_file in SOCKET_DIR.glob("*.sock"):
229
- session_name = sock_file.stem
230
- pid_file = get_pid_file(session_name)
235
+ # stem 已经是 _safe_filename() 的输出(MD5 哈希)
236
+ safe_name = sock_file.stem
237
+ pid_file = SOCKET_DIR / f"{safe_name}.pid"
231
238
 
232
239
  # 检查 socket 文件是否有效(进程是否存在)
233
240
  if pid_file.exists():
@@ -237,6 +244,17 @@ def list_active_sessions() -> List[dict]:
237
244
  os.kill(pid, 0)
238
245
  # 获取进程 CWD
239
246
  cwd = get_process_cwd(pid)
247
+
248
+ # 恢复原始会话名(从 .name 文件读取,不存在则回退到哈希值)
249
+ name_file = SOCKET_DIR / f"{safe_name}.name"
250
+ if name_file.exists():
251
+ try:
252
+ display_name = name_file.read_text().strip()
253
+ except OSError:
254
+ display_name = safe_name
255
+ else:
256
+ display_name = safe_name
257
+
240
258
  # 获取启动时间(PID 文件的修改时间,文件可能已被并发清理)
241
259
  import datetime
242
260
  try:
@@ -246,7 +264,7 @@ def list_active_sessions() -> List[dict]:
246
264
  mtime = 0
247
265
  start_time = "?"
248
266
 
249
- # 读取 .mq 文件获取 cli_type(避免循环导入,在函数内导入)
267
+ # 读取 .mq 文件获取 cli_type
250
268
  try:
251
269
  import sys
252
270
  from pathlib import Path
@@ -255,29 +273,38 @@ def list_active_sessions() -> List[dict]:
255
273
  if project_root not in sys.path:
256
274
  sys.path.insert(0, project_root)
257
275
  from server.shared_state import SharedStateReader
258
- reader = SharedStateReader(session_name)
276
+ # _BypassHashReader 直接传入 safe_name 避免二次哈希
277
+ mq_path = SOCKET_DIR / f"{safe_name}.mq"
278
+ reader = SharedStateReader.__new__(SharedStateReader)
279
+ reader._path = mq_path
259
280
  snapshot = reader.read()
260
281
  cli_type = snapshot.get("cli_type", "claude")
261
282
  except Exception as e:
262
- # 添加详细日志记录,便于诊断问题
263
283
  import logging
264
284
  logger = logging.getLogger('Session')
265
- logger.warning(f"读取共享内存 cli_type 失败: session={session_name}, error={e}")
266
- cli_type = "claude" # 读取失败时使用默认值
285
+ logger.warning(f"读取共享内存 cli_type 失败: session={display_name}, error={e}")
286
+ cli_type = "claude"
287
+
288
+ # tmux 会话名也用 safe_name 直接构造
289
+ tmux_name = f"{TMUX_SESSION_PREFIX}{safe_name}"
290
+ tmux_exists = subprocess.run(
291
+ ["tmux", "has-session", "-t", tmux_name],
292
+ capture_output=True
293
+ ).returncode == 0
267
294
 
268
295
  sessions.append({
269
- "name": session_name,
296
+ "name": display_name,
270
297
  "socket": str(sock_file),
271
298
  "pid": pid,
272
299
  "cwd": cwd or "",
273
300
  "start_time": start_time,
274
301
  "mtime": mtime,
275
- "tmux": tmux_session_exists(session_name),
302
+ "tmux": tmux_exists,
276
303
  "cli_type": cli_type
277
304
  })
278
305
  except (ProcessLookupError, ValueError, OSError):
279
306
  # 进程不存在或文件被并发清理,清理残留文件
280
- cleanup_session(session_name)
307
+ _cleanup_by_safe_name(safe_name)
281
308
  else:
282
309
  # 没有 PID 文件,清理 socket
283
310
  sock_file.unlink(missing_ok=True)
@@ -287,14 +314,32 @@ def list_active_sessions() -> List[dict]:
287
314
 
288
315
 
289
316
  def cleanup_session(session_name: str):
290
- """清理会话残留文件"""
291
- sock_path = get_socket_path(session_name)
292
- pid_file = get_pid_file(session_name)
317
+ """清理会话残留文件(传入原始会话名,经过 _safe_filename 转换)"""
318
+ safe = _safe_filename(session_name)
319
+ _cleanup_by_safe_name(safe, session_name)
293
320
 
294
- sock_path.unlink(missing_ok=True)
295
- pid_file.unlink(missing_ok=True)
296
- get_mq_path(session_name).unlink(missing_ok=True)
297
- get_env_snapshot_path(session_name).unlink(missing_ok=True)
321
+
322
+ def _cleanup_by_safe_name(safe_name: str, session_name: Optional[str] = None):
323
+ """清理会话残留文件(传入已经是 MD5 哈希的 safe_name,不再二次哈希)"""
324
+ # 尝试从 .name 文件读取原始会话名(用于清理可读日志文件)
325
+ if session_name is None:
326
+ name_file = SOCKET_DIR / f"{safe_name}.name"
327
+ if name_file.exists():
328
+ try:
329
+ session_name = name_file.read_text().strip()
330
+ except OSError:
331
+ pass
332
+ for suffix in [".sock", ".pid", ".mq", ".name", "_env.json"]:
333
+ (SOCKET_DIR / f"{safe_name}{suffix}").unlink(missing_ok=True)
334
+ # 清理带后缀的日志文件(使用可读文件名)
335
+ log_suffixes = ["_messages.log", "_screen.log", "_server.log", "_debug.log", "_pty_raw.log"]
336
+ if session_name:
337
+ log_name = _log_filename(session_name)
338
+ for suffix in log_suffixes:
339
+ (SOCKET_DIR / f"{log_name}{suffix}").unlink(missing_ok=True)
340
+ # 兼容旧的 MD5 日志文件
341
+ for suffix in log_suffixes:
342
+ (SOCKET_DIR / f"{safe_name}{suffix}").unlink(missing_ok=True)
298
343
 
299
344
 
300
345
  def is_session_active(session_name: str) -> bool:
@@ -346,8 +391,14 @@ def is_lark_running() -> bool:
346
391
  try:
347
392
  pid = int(pid_file.read_text().strip())
348
393
  os.kill(pid, 0)
349
- return True
350
- except (ProcessLookupError, ValueError, OSError):
394
+ # 额外验证:确认进程确实是我们的 lark_client(防止 PID 复用误判)
395
+ import subprocess
396
+ result = subprocess.run(
397
+ ["ps", "-p", str(pid), "-o", "command="],
398
+ capture_output=True, text=True, timeout=2
399
+ )
400
+ return "lark_client" in result.stdout
401
+ except (ProcessLookupError, ValueError, OSError, Exception):
351
402
  return False
352
403
 
353
404