tg-ws-proxy/utils/tray_lock.py

182 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Один экземпляр tray-приложения: lock-файлы с PID и метаданными."""
from __future__ import annotations
import json
import logging
import os
from pathlib import Path
from typing import Callable, Optional
import psutil
import sys
SameProcessFn = Callable[[dict, psutil.Process], bool]
def make_same_process_checker(
*,
script_marker: Optional[str],
frozen_match: Callable[[psutil.Process], bool],
) -> SameProcessFn:
"""Проверка «наш ли процесс» для lock-файла (cmdline + frozen)."""
def check(lock_meta: dict, proc: psutil.Process) -> bool:
try:
lock_ct = float(lock_meta.get("create_time", 0.0))
proc_ct = float(proc.create_time())
if lock_ct > 0 and abs(lock_ct - proc_ct) > 1.0:
return False
except Exception:
return False
frozen = bool(getattr(sys, "frozen", False))
if script_marker is not None:
sm_lower = script_marker.lower()
try:
for arg in proc.cmdline():
if sm_lower in arg.lower():
return True
except Exception:
pass
# Тот же python.exe, маркер в строке cmdline (регистр пути на Windows)
if not frozen:
try:
if proc.exe() and sys.executable:
if os.path.normcase(os.path.abspath(proc.exe())) == os.path.normcase(
os.path.abspath(sys.executable)
):
try:
joined = " ".join(proc.cmdline())
except Exception:
joined = ""
if sm_lower in joined.lower():
return True
except Exception:
pass
if frozen:
return frozen_match(proc)
return False
return check
def frozen_match_executable_basename(proc: psutil.Process) -> bool:
import os as _os
return _os.path.basename(sys.executable).lower() == proc.name().lower()
def frozen_match_app_name_contains(app_name: str) -> Callable[[psutil.Process], bool]:
needle = app_name.lower()
def _m(proc: psutil.Process) -> bool:
return needle in proc.name().lower()
return _m
class SingleInstanceLock:
"""RAII-совместимый lock каталога приложения (*.lock с PID)."""
def __init__(
self,
app_dir: Path,
same_process: SameProcessFn,
*,
log: Optional[logging.Logger] = None,
) -> None:
self._app_dir = app_dir
self._same_process = same_process
self._log = log
self._lock_file: Optional[Path] = None
@property
def lock_file(self) -> Optional[Path]:
return self._lock_file
def acquire(self) -> bool:
self._app_dir.mkdir(parents=True, exist_ok=True)
for f in self._app_dir.glob("*.lock"):
pid = None
meta: dict = {}
try:
pid = int(f.stem)
except Exception:
f.unlink(missing_ok=True)
continue
try:
raw = f.read_text(encoding="utf-8").strip()
if raw:
meta = json.loads(raw)
except Exception:
meta = {}
if not psutil.pid_exists(pid):
f.unlink(missing_ok=True)
continue
try:
proc = psutil.Process(pid)
except psutil.NoSuchProcess:
f.unlink(missing_ok=True)
continue
except Exception:
if self._log:
self._log.debug("Lock %s: cannot open pid %s", f, pid, exc_info=True)
if psutil.pid_exists(pid):
return False
f.unlink(missing_ok=True)
continue
try:
if self._same_process(meta, proc):
return False
except Exception:
if self._log:
self._log.debug("same_process failed for pid %s", pid, exc_info=True)
# PID живой, но не распознали как наш экземпляр — не удаляем lock
# (раньше удаляли и второй процесс занимал порт → WinError 10013 и два трея).
try:
if proc.is_running():
if self._log:
self._log.warning(
"Уже запущен другой процесс с lock %s (pid=%s); "
"второй экземпляр не запускается.",
f.name,
pid,
)
return False
except Exception:
return False
f.unlink(missing_ok=True)
lock_file = self._app_dir / f"{os.getpid()}.lock"
try:
proc = psutil.Process(os.getpid())
payload = {"create_time": proc.create_time()}
lock_file.write_text(
json.dumps(payload, ensure_ascii=False),
encoding="utf-8",
)
except Exception:
lock_file.touch()
self._lock_file = lock_file
return True
def release(self) -> None:
if not self._lock_file:
return
try:
self._lock_file.unlink(missing_ok=True)
except Exception:
if self._log:
self._log.debug("Lock release failed", exc_info=True)
self._lock_file = None