The clicker had no Linux support at all: pygetwindow has no X11 backend, so _other_activate returned "window management is unsupported" and every step failed before it could click. focus.py now has a third backend that raises the browser with xdotool and reads the focused window's WM_CLASS to verify it came forward - the same activate-then-confirm shape as the macOS and Windows paths. setup-linux.sh mirrors setup-windows.bat with apt instead of winget. Three things are specific to this platform rather than incidental: - Node comes from NodeSource. Pi OS ships one too old for Next 16, and the LTS line is also what better-sqlite3 publishes prebuilt arm64 binaries for. - Python dependencies go in a virtualenv. Pi OS Bookworm enforces PEP 668, so pip into the system interpreter fails with externally-managed-environment. - Autostart uses an XDG ~/.config/autostart entry, the direct analogue of the Windows Startup folder: no sudo, and it runs inside the graphical session, which the clicker needs for DISPLAY. Wayland is called out in four places - the session guard, diagnose.py, the installer and both READMEs - because Pi OS on a Pi 5 defaults to it and the clicker simply cannot work there. Wayland does not let one client synthesise input into another, so this is a switch-to-X11 situation, not a bug to fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
157 lines
5.5 KiB
Python
157 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Check whether this machine can actually be driven.
|
|
|
|
Run it on the box that will do the clicking, before trusting a run:
|
|
|
|
python diagnose.py
|
|
|
|
It answers the question a remote desktop makes hard — is the mouse really moving,
|
|
or is the viewer just not drawing it? The cursor position is read back from the
|
|
OS after each move, so the answer doesn't depend on anything being rendered.
|
|
|
|
Nothing is clicked. The cursor is moved and put back where it started.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import time
|
|
|
|
from clicker import force_utf8_output
|
|
|
|
|
|
def line(label: str, value: str) -> None:
|
|
print(f" {label:<22} {value}")
|
|
|
|
|
|
def check_platform() -> None:
|
|
print("\nPlatform")
|
|
line("os", sys.platform)
|
|
line("python", sys.version.split()[0])
|
|
|
|
if sys.platform.startswith("linux"):
|
|
# The two things that decide whether the clicker can work at all here.
|
|
session = os.environ.get("XDG_SESSION_TYPE", "unknown")
|
|
display = os.environ.get("DISPLAY") or "(unset)"
|
|
line("session type", session)
|
|
line("DISPLAY", display)
|
|
line("xdotool", shutil.which("xdotool") or "NOT INSTALLED — sudo apt install -y xdotool")
|
|
if session == "wayland" or (os.environ.get("WAYLAND_DISPLAY") and not os.environ.get("DISPLAY")):
|
|
print(" Wayland cannot be automated: xdotool and pyautogui both speak X11,")
|
|
print(" and Wayland does not let one client drive another. On Raspberry Pi OS:")
|
|
print(" sudo raspi-config -> Advanced Options -> Wayland -> X11, then reboot.")
|
|
return
|
|
|
|
if sys.platform != "win32":
|
|
return
|
|
|
|
import ctypes
|
|
|
|
# SM_REMOTESESSION: non-zero when this process is running inside an RDP
|
|
# session rather than at the physical console.
|
|
remote = ctypes.windll.user32.GetSystemMetrics(0x1000)
|
|
line("remote session", "yes — RDP/terminal services" if remote else "no — physical console")
|
|
if remote:
|
|
print(" Input injection still works over RDP, but the session's desktop")
|
|
print(" is locked when you disconnect, and clicks go nowhere until you")
|
|
print(" reconnect. Keep the window open for the duration of a run.")
|
|
|
|
|
|
def check_dpi() -> None:
|
|
print("\nDisplay")
|
|
try:
|
|
import focus
|
|
except ImportError:
|
|
line("dpi awareness", "focus.py not importable — run this from clicker/")
|
|
return
|
|
|
|
result = focus.enable_dpi_awareness()
|
|
line("dpi awareness", result or "n/a (not Windows)")
|
|
|
|
try:
|
|
import pyautogui
|
|
except ImportError:
|
|
line("screen size", "pyautogui not installed")
|
|
return
|
|
size = pyautogui.size()
|
|
line("screen size", f"{size.width}x{size.height} (as the OS reports it)")
|
|
print(" Compare against what `clicker.py locate` reports for screenSize.")
|
|
print(" A mismatch that isn't a clean scaling factor means clicks land off.")
|
|
|
|
|
|
def check_mouse() -> bool:
|
|
"""Move the cursor and read it back. Returns True if the OS agreed."""
|
|
print("\nMouse")
|
|
try:
|
|
import pyautogui
|
|
except ImportError:
|
|
line("result", "pyautogui not installed — run: pip install -r requirements.txt")
|
|
return False
|
|
|
|
pyautogui.FAILSAFE = False # a deliberate corner move would abort us
|
|
start = pyautogui.position()
|
|
line("start position", f"{start[0]},{start[1]}")
|
|
|
|
width, height = pyautogui.size()
|
|
targets = [(width // 4, height // 4), (width // 2, height // 2)]
|
|
|
|
agreed = True
|
|
for x, y in targets:
|
|
pyautogui.moveTo(x, y, duration=0.3)
|
|
time.sleep(0.1)
|
|
got = pyautogui.position()
|
|
ok = abs(got[0] - x) <= 2 and abs(got[1] - y) <= 2
|
|
agreed &= ok
|
|
line("moved to", f"{x},{y} -> OS reports {got[0]},{got[1]} {'OK' if ok else 'MISMATCH'}")
|
|
|
|
pyautogui.moveTo(start[0], start[1], duration=0.2)
|
|
|
|
print()
|
|
if agreed:
|
|
print(" The OS moved the cursor to every requested point.")
|
|
print(" If you saw nothing move, that is your viewer not drawing it —")
|
|
print(" the clicks are landing where they should.")
|
|
else:
|
|
print(" The cursor did NOT land where it was asked to.")
|
|
print(" On Windows this is usually display scaling: the process is being")
|
|
print(" fed virtualised coordinates. Check the dpi awareness line above,")
|
|
print(" and pass --scale to clicker.py to compensate.")
|
|
return agreed
|
|
|
|
|
|
def check_foreground() -> None:
|
|
print("\nForeground window")
|
|
try:
|
|
import focus
|
|
except ImportError:
|
|
line("frontmost", "focus.py not importable")
|
|
return
|
|
|
|
front = focus.frontmost()
|
|
line("frontmost", front or "could not determine on this platform")
|
|
line("browsers known", ", ".join(focus.browser_ids()) or "none for this platform")
|
|
|
|
result = focus.ensure_frontmost()
|
|
line("raise browser", f"{'OK' if result.ok else 'FAILED'} — {result.detail}")
|
|
print(" With no browser named, any known one counts — that is this check")
|
|
print(" only. A real run raises the browser the extension reported, so if")
|
|
print(" this raised one you do not automate, that is not a fault.")
|
|
if not result.ok:
|
|
print(" Every click is refused while this fails: a click on an")
|
|
print(" unfocused window is consumed activating it.")
|
|
|
|
|
|
def main() -> int:
|
|
force_utf8_output()
|
|
print("AutoFirmer clicker diagnostics")
|
|
check_platform()
|
|
check_dpi()
|
|
ok = check_mouse()
|
|
check_foreground()
|
|
print()
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|