#!/usr/bin/env python3 """Self-installing / self-updating entry point — `python update.py`. Run it once (the install one-liner does this for you). From then on a Windows Scheduled Task runs it automatically **at logon and once a day**, so the watchers stay up to date and running with zero further thought. Each run it: 1. makes config.ini on first run (webhook prefilled; gitignored so your edits and OCR snip survive every update), 2. `git pull`s the latest watcher code, 3. installs/upgrades deps only when something changed, 4. (re)starts the watchers ONLY if the code changed or they aren't running — so the daily run never interrupts an active session for nothing, 5. ensures the daily+logon Scheduled Task exists (idempotent). First time on a new PC (or just paste the install one-liner): git clone https://git.armoredarmadillo.com/brockdarnold/eve-watcher.git cd eve-watcher python update.py python eve_orehold_watcher.py --snip # one-time GUI step for OCR hold alerts """ import configparser import os import shutil import subprocess import sys HERE = os.path.dirname(os.path.abspath(__file__)) WIN = os.name == "nt" TASK = "EveWatcher" WATCHER_SCRIPTS = ("eve_combat_watcher.py", "eve_chat_watcher.py", "eve_orehold_watcher.py") def run(cmd, **kw): print(f" $ {' '.join(cmd)}") return subprocess.run(cmd, cwd=HERE, **kw) def ps(script): """Run a PowerShell snippet, return (rc, stdout+stderr).""" r = subprocess.run(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], cwd=HERE, capture_output=True, text=True) return r.returncode, ((r.stdout or "") + (r.stderr or "")).strip() def pythonw(): p = os.path.join(os.path.dirname(sys.executable), "pythonw.exe") return p if os.path.exists(p) else "pythonw.exe" def ensure_config(): cfg = os.path.join(HERE, "config.ini") first = not os.path.exists(cfg) if first: shutil.copyfile(os.path.join(HERE, "config.ini.example"), cfg) print("• created config.ini from example (webhook prefilled).") else: print("• config.ini exists — your settings + OCR snip left untouched.") # heal a UTF-8 BOM left by the old PowerShell installer (breaks configparser) with open(cfg, "rb") as fh: raw = fh.read() if raw.startswith(b"\xef\xbb\xbf"): with open(cfg, "wb") as fh: fh.write(raw[3:]) print("• stripped a UTF-8 BOM from config.ini (old-installer leftover).") return cfg, first ZIP_URL = "https://git.armoredarmadillo.com/brockdarnold/eve-watcher/archive/main.zip" def _git(*a): return subprocess.run(["git", "-C", HERE, *a], capture_output=True, text=True) def _have_git(): import shutil as _sh return os.path.isdir(os.path.join(HERE, ".git")) and _sh.which("git") is not None def update_code(): """Pull the latest watcher code. Uses git when this is a clone; otherwise downloads the repo zip and overwrites files (config.ini is never touched). Returns True if anything changed.""" if _have_git(): before = _git("rev-parse", "HEAD").stdout.strip() if _git("fetch", "-q", "origin").returncode != 0: print("! git fetch failed (offline?). Keeping current version.") return False _git("reset", "--hard", "-q", "origin/main") after = _git("rev-parse", "HEAD").stdout.strip() if before and after and before != after: print(f" updated: {before[:7]} -> {after[:7]}") return True print(" already on latest code.") return False return _zip_update() def _zip_update(): """git-less update: download the repo zip and write any changed files.""" import io import urllib.request import zipfile try: req = urllib.request.Request(ZIP_URL, headers={"User-Agent": "eve-watcher-updater"}) data = urllib.request.urlopen(req, timeout=60).read() z = zipfile.ZipFile(io.BytesIO(data)) except Exception as e: print(f"• update check failed (offline?): {e}") return False changed = 0 for n in z.namelist(): if n.endswith("/"): continue rel = n.split("/", 1)[1] if "/" in n else n # strip top 'eve-watcher/' if not rel or rel == "config.ini": # never clobber local config continue dest = os.path.join(HERE, rel.replace("/", os.sep)) new = z.read(n) old = open(dest, "rb").read() if os.path.exists(dest) else None if old != new: os.makedirs(os.path.dirname(dest) or HERE, exist_ok=True) with open(dest, "wb") as f: f.write(new) changed += 1 print(f" code updated (zip): {changed} file(s)." if changed else " already on latest code (zip).") return changed > 0 def deps(): req = os.path.join(HERE, "requirements.txt") if os.path.exists(req): run([sys.executable, "-m", "pip", "install", "--quiet", "-r", req]) def ensure_tesseract(cfg_path): """OCR needs the Tesseract binary. Detect it; if missing, try a silent winget install (best-effort — may raise a one-time UAC prompt). Writes tesseract_cmd into config.ini when found. Non-fatal: timer/combat/chat work without it.""" if not WIN: return import shutil as _sh std = r"C:\Program Files\Tesseract-OCR\tesseract.exe" path = _sh.which("tesseract") or (std if os.path.exists(std) else None) if not path: print("• Tesseract not found — installing (one-time, may prompt UAC)...") try: subprocess.run(["winget", "install", "--id", "UB-Mannheim.TesseractOCR", "-e", "--silent", "--accept-package-agreements", "--accept-source-agreements"], capture_output=True, text=True, timeout=600) except Exception as ex: print(f" (winget install skipped: {ex})") path = _sh.which("tesseract") or (std if os.path.exists(std) else None) if not path: print("• Tesseract still missing — live OCR % off until it's installed " "(combat/chat/phone alerts still work).") return # record the path so pytesseract finds it even if not on PATH cp = configparser.ConfigParser() cp.read(cfg_path, encoding="utf-8-sig") if not cp.has_section("ocr"): cp.add_section("ocr") if cp.get("ocr", "tesseract_cmd", fallback="").strip() != path: cp["ocr"]["tesseract_cmd"] = path with open(cfg_path, "w") as f: cp.write(f) print(f"• Tesseract ready: {path}") def watchers_running(): if not WIN: return 0 _, out = ps("(Get-CimInstance Win32_Process -Filter \"Name='pythonw.exe'\" " "| Where-Object { $_.CommandLine -like '*watcher*' }).Count") try: return int(out or "0") except ValueError: return 0 def stop_watchers(): ps("Get-CimInstance Win32_Process -Filter \"Name='pythonw.exe'\" | " "Where-Object { $_.CommandLine -like '*watcher*' } | " "ForEach-Object { Stop-Process -Id $_.ProcessId -Force }") def start_watchers(cfg_path): if not WIN: print("• non-Windows — start watchers manually (this is a Windows tool).") return stop_watchers() run(["powershell", "-ExecutionPolicy", "Bypass", "-File", os.path.join(HERE, "start-all.ps1")]) cp = configparser.ConfigParser() cp.read(cfg_path, encoding="utf-8-sig") if cp.get("watcher", "mode", fallback="ocr") == "ocr" and \ not cp.get("ocr", "region", fallback="").strip(): print("\n >> The live hold % reader auto-calibrates: just have your in-game\n" " Ore Hold window OPEN and it finds the readout itself (no snip).") def ensure_logon_autostart(): """Drop a launcher in the Startup folder so this runs at every logon. No admin needed — most reliable auto-start mechanism on Windows.""" if not WIN: return False rc, startup = ps("[Environment]::GetFolderPath('Startup')") startup = startup.strip() if rc != 0 or not startup or not os.path.isdir(startup): print("! couldn't find Startup folder for logon auto-start") return False launcher = os.path.join(startup, "EveWatcher.cmd") body = ('@echo off\r\n' f'start "" "{pythonw()}" "{os.path.join(HERE, "update.py")}"\r\n') with open(launcher, "w", newline="") as f: f.write(body) print(f"• logon auto-start installed: {launcher}") return True def ensure_daily_task(): """Best-effort daily Scheduled Task (covers PCs left on for days). Uses schtasks (works for the current user without admin). Idempotent.""" if not WIN: return q = subprocess.run(["schtasks", "/query", "/tn", TASK], capture_output=True, text=True) if q.returncode == 0: print(f"• daily task '{TASK}' already set.") return tr = f'"{pythonw()}" "{os.path.join(HERE, "update.py")}"' c = subprocess.run(["schtasks", "/create", "/tn", TASK, "/tr", tr, "/sc", "DAILY", "/st", "05:00", "/f"], capture_output=True, text=True) if c.returncode == 0: print(f"• daily auto-update task '{TASK}' created (05:00).") else: print(f"• daily task not created ({(c.stdout + c.stderr).strip() or 'unknown'});" " logon auto-start still covers updates.") def main(): print("=== Eve watcher setup / auto-update ===") cfg, first = ensure_config() changed = update_code() if changed or first: deps() ensure_tesseract(cfg) if changed or first or watchers_running() == 0: start_watchers(cfg) else: print("• up to date and watchers already running — nothing to restart.") ensure_logon_autostart() ensure_daily_task() print("\nDone. Hands-off from here: it updates + keeps itself running automatically.") if __name__ == "__main__": main()