eve-watcher/update.py
2026-06-14 07:33:38 +00:00

180 lines
6.8 KiB
Python

#!/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()