process_utils.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. """
  2. Process validation using psutil and filesystem mtime.
  3. Uses mtime as a "password": PID files are timestamped with process start time.
  4. Since filesystem mtimes can be set arbitrarily but process start times cannot,
  5. comparing them detects PID reuse.
  6. """
  7. __package__ = 'archivebox.misc'
  8. import os
  9. import time
  10. from pathlib import Path
  11. from typing import Optional
  12. try:
  13. import psutil
  14. PSUTIL_AVAILABLE = True
  15. except ImportError:
  16. PSUTIL_AVAILABLE = False
  17. def validate_pid_file(pid_file: Path, cmd_file: Optional[Path] = None, tolerance: float = 5.0) -> bool:
  18. """Validate PID using mtime and optional cmd.sh. Returns True if process is ours."""
  19. if not PSUTIL_AVAILABLE or not pid_file.exists():
  20. return False
  21. try:
  22. pid = int(pid_file.read_text().strip())
  23. proc = psutil.Process(pid)
  24. # Check mtime matches process start time
  25. if abs(pid_file.stat().st_mtime - proc.create_time()) > tolerance:
  26. return False # PID reused
  27. # Validate command if provided
  28. if cmd_file and cmd_file.exists():
  29. cmd = cmd_file.read_text()
  30. cmdline = ' '.join(proc.cmdline())
  31. if '--remote-debugging-port' in cmd and '--remote-debugging-port' not in cmdline:
  32. return False
  33. if ('chrome' in cmd.lower() or 'chromium' in cmd.lower()):
  34. if 'chrome' not in proc.name().lower() and 'chromium' not in proc.name().lower():
  35. return False
  36. return True
  37. except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess, ValueError, OSError):
  38. return False
  39. def write_pid_file_with_mtime(pid_file: Path, pid: int, start_time: float):
  40. """Write PID file and set mtime to process start time."""
  41. pid_file.write_text(str(pid))
  42. try:
  43. os.utime(pid_file, (start_time, start_time))
  44. except OSError:
  45. pass # mtime optional, validation degrades gracefully
  46. def write_cmd_file(cmd_file: Path, cmd: list[str]):
  47. """Write shell command script."""
  48. def escape(arg: str) -> str:
  49. return f'"{arg.replace(chr(34), chr(92)+chr(34))}"' if any(c in arg for c in ' "$') else arg
  50. script = '#!/bin/bash\n' + ' '.join(escape(arg) for arg in cmd) + '\n'
  51. cmd_file.write_text(script)
  52. try:
  53. cmd_file.chmod(0o755)
  54. except OSError:
  55. pass
  56. def safe_kill_process(pid_file: Path, cmd_file: Optional[Path] = None, signal_num: int = 15, timeout: float = 3.0) -> bool:
  57. """
  58. Kill process after validation, with graceful wait and SIGKILL escalation.
  59. Returns True only if process is confirmed dead (either already dead or killed successfully).
  60. """
  61. import time
  62. import signal
  63. if not validate_pid_file(pid_file, cmd_file):
  64. pid_file.unlink(missing_ok=True) # Clean stale file
  65. return True # Process already dead, consider it killed
  66. try:
  67. pid = int(pid_file.read_text().strip())
  68. # Send initial signal (SIGTERM by default)
  69. try:
  70. os.kill(pid, signal_num)
  71. except ProcessLookupError:
  72. # Process already dead
  73. return True
  74. # Wait for process to terminate gracefully
  75. start_time = time.time()
  76. while time.time() - start_time < timeout:
  77. try:
  78. os.kill(pid, 0) # Check if process still exists
  79. time.sleep(0.1)
  80. except ProcessLookupError:
  81. # Process terminated
  82. return True
  83. # Process didn't terminate, escalate to SIGKILL
  84. try:
  85. os.kill(pid, signal.SIGKILL)
  86. time.sleep(0.5) # Brief wait after SIGKILL
  87. # Verify it's dead
  88. try:
  89. os.kill(pid, 0)
  90. # Process still alive after SIGKILL - this is unusual
  91. return False
  92. except ProcessLookupError:
  93. # Process finally dead
  94. return True
  95. except ProcessLookupError:
  96. # Process died between timeout and SIGKILL
  97. return True
  98. except (OSError, ValueError):
  99. return False