#!/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 def git_pull(): """Return True if new commits were pulled.""" if not os.path.isdir(os.path.join(HERE, ".git")): print("• not a git clone — can't auto-update. Reinstall via:\n" " git clone https://git.armoredarmadillo.com/brockdarnold/eve-watcher.git") return False r = subprocess.run(["git", "pull", "--ff-only"], cwd=HERE, capture_output=True, text=True) out = (r.stdout + r.stderr).strip() print(f" git: {out.splitlines()[-1] if out else '(no output)'}") if r.returncode != 0: print("! git pull failed (local edits to tracked files?). config.ini is safe.") return False return "Already up to date" not in out and "Already up-to-date" not in out def deps(): req = os.path.join(HERE, "requirements.txt") if os.path.exists(req): run([sys.executable, "-m", "pip", "install", "--quiet", "-r", req]) 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 >> ONE-TIME (per PC) for hold/compress/stall alerts, run once:\n" " python eve_orehold_watcher.py --snip\n" " (drag a box around the Ore Hold fill bar; saved forever after.)") 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 = git_pull() if changed or first: deps() 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()