process_utils.py 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  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) -> bool:
  57. """Kill process after validation. Returns True if killed."""
  58. if not validate_pid_file(pid_file, cmd_file):
  59. pid_file.unlink(missing_ok=True) # Clean stale file
  60. return False
  61. try:
  62. pid = int(pid_file.read_text().strip())
  63. os.kill(pid, signal_num)
  64. return True
  65. except (OSError, ValueError, ProcessLookupError):
  66. return False