262 lines
9.9 KiB
Python
262 lines
9.9 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
|
|
|
|
|
|
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()
|