mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 17:47:23 +01:00
Real fixes: split over-long functions (lock-decision chain, manual sync, validation, phone verification, status), hoist a dead lazy import, context-manage Xvfb and temp dirs, add encodings, share the silent-failure argv loop. lock_grab_handoff read lock._recovery, which gatelock's LockWindow does not have (its public property is recovery); a contract test now checks every attribute the handoff reads against the real class. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
105 lines
3.6 KiB
Python
105 lines
3.6 KiB
Python
"""Re-run a GUI check script on a throwaway X display.
|
|
|
|
Split out of ``verify_lock_popup_safety.py`` and ``verify_screen_fits.py``
|
|
to keep every file under the 250-line cap. Both had their own near-identical
|
|
copy; they differ only in the sentinel env var and how the inner process is
|
|
addressed, so both are parameters here.
|
|
|
|
Each script runs itself twice: the outer run starts ``Xvfb`` and re-execs,
|
|
the inner run (marked by the sentinel) does the real work against that
|
|
display. Without the sentinel the inner process would start another Xvfb
|
|
forever.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterator
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
# Comfortably bigger than every size under test, so each surface is sized by
|
|
# the geometry the harness sets rather than clamped by the display.
|
|
_SCREEN_ARG = ("-screen", "0", "1600x1200x24")
|
|
|
|
|
|
def reexec_under_xvfb(
|
|
*,
|
|
inner_env: str,
|
|
module: str | None = None,
|
|
script: Path | None = None,
|
|
) -> int:
|
|
"""Re-run this check on a throwaway X display, returning its exit code.
|
|
|
|
Exactly one of *module* (run as ``python -m module`` from the repo root)
|
|
or *script* (run as an absolute path) identifies the inner process; that
|
|
difference is why the two callers cannot share a single hardcoded form.
|
|
|
|
Args:
|
|
inner_env: Sentinel env var set for the inner run, so it knows not to
|
|
start another Xvfb.
|
|
module: Dotted module path to run with ``-m``.
|
|
script: Absolute path of the script file to run.
|
|
|
|
Returns:
|
|
The inner process's exit code, or 1 if ``Xvfb`` is unavailable or
|
|
never became ready.
|
|
"""
|
|
if (module is None) == (script is None):
|
|
msg = "pass exactly one of module= or script="
|
|
raise ValueError(msg)
|
|
|
|
xvfb = shutil.which("Xvfb")
|
|
if xvfb is None:
|
|
_logger.error(
|
|
"Xvfb not found. Install it with: sudo pacman -S --needed xorg-server-xvfb"
|
|
)
|
|
return 1
|
|
|
|
repo_root = Path(__file__).resolve().parent.parent
|
|
# Absolute argv, no shell.
|
|
target = ["-m", module] if module is not None else [str(script)]
|
|
with _xvfb_display(xvfb) as display:
|
|
if display is None:
|
|
_logger.error("Xvfb exited before it was ready to accept clients")
|
|
return 1
|
|
env = dict(os.environ, DISPLAY=display, **{inner_env: "1"})
|
|
env.pop("XAUTHORITY", None) # a throwaway server has no cookie to check
|
|
return subprocess.run(
|
|
[sys.executable, *target], check=False, cwd=repo_root, env=env
|
|
).returncode
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def _xvfb_display(xvfb: str) -> Iterator[str | None]:
|
|
"""Run Xvfb on a free display; yield ``:N`` once ready (``None`` if it died).
|
|
|
|
``-displayfd`` makes Xvfb pick a free display and write its number only
|
|
once it accepts clients. ``xvfb-run`` instead sleeps a fixed 3 s after
|
|
starting it, which was most of this pre-commit hook's runtime.
|
|
"""
|
|
read_fd, write_fd = os.pipe()
|
|
with subprocess.Popen(
|
|
[xvfb, "-displayfd", str(write_fd), "-nolisten", "tcp", *_SCREEN_ARG],
|
|
pass_fds=(write_fd,),
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
) as server:
|
|
os.close(write_fd)
|
|
try:
|
|
with os.fdopen(read_fd) as ready:
|
|
number = ready.readline().strip()
|
|
yield f":{number}" if number else None
|
|
finally:
|
|
server.terminate()
|
|
server.wait(timeout=5)
|