A run died with "'charmap' codec can't encode character '▶'" at step 1. Python picks the console code page for stdout, and under PM2 - where stdout is a pipe rather than a console - that is cp1252 on Windows. cp1252 handles the em dashes in these files but not the run markers, so the first one raised UnicodeEncodeError from inside run_steps and the runner reported it as a step failure rather than an output problem. Reconfigure stdout and stderr to UTF-8 at the top of each entry point, with errors="replace" as a backstop for streams that cannot be reconfigured. Fixing the encoding once beats stripping the glyphs from ~30 call sites, and covers manual runs in a plain console too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
142 lines
4.7 KiB
Python
142 lines
4.7 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 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 != "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())
|