Files
octo-updater/src/config.py
T
2026-09-26 14:11:59 -04:00

121 lines
4.0 KiB
Python

"""Extracted from octo_updater.py. Keep this module focused on its named responsibility."""
import json
import hashlib
import os
import sys
import ssl
import re
import subprocess
import urllib.request
from urllib.parse import urlsplit
import shutil
import stat
import struct
import time
import math
import threading
import queue
import json
from functools import cache
from pathlib import Path
from .platform_ops import user_data_dir, user_config_dir
UPDATER_VERSION = "1.3.1"
SERVER = "https://octowow.st"
UA = f"OctoUpdater/{UPDATER_VERSION}"
DOWNLOAD_RETRY = 5
DOWNLOAD_TIMEOUT = 10 # seconds without any data before a transfer aborts
# Where the app lives: next to the .exe when frozen (PyInstaller), otherwise
# next to this script — never the current working directory, which varies with
# how the app was launched. This anchors the default game folder.
if getattr(sys, "frozen", False):
APP_DIR = os.path.dirname(os.path.abspath(sys.executable)) # Directory where frozen .exe is executed from
#TODO : Compile into binary ? Make into AppImage ?
else:
APP_DIR = os.path.dirname(os.path.abspath(__file__)) # Directory where python3 script is executed from
# Set env vars
APP_DATA_DIR = user_data_dir()
APP_CONFIG_DIR = user_config_dir()
CONFIG_FILE = os.path.join(APP_CONFIG_DIR, "config.json") # TODO: Change this to point to ~/.config/OctoUpdater
# TODO: Create the config directories on a linux host if they don't exist
# First-run default game folder, anchored to the app dir (not the CWD).
DEFAULT_GAME_DIR = os.path.join(APP_DIR, "OctoWoW") # NOTE: May need to change later to point to other dir
def _relocate_legacy_data():
# Old config (<1.3: octo_updater_config.json beside the app) becomes
# config.json in APP_DATA_DIR. Rename + relocate, only when the old name
# exists and the new path doesn't (idempotent). The legacy game-hash cache
# is no longer used and is deleted by _migrate_1_3.
old_name, new = "octo_updater_config.json", CONFIG_FILE
for old in (os.path.join(APP_DIR, old_name),
os.path.join(APP_DATA_DIR, old_name)):
if old == new or not os.path.exists(old) or os.path.exists(new):
continue
try:
shutil.move(old, new)
break
except OSError:
pass
_CONFIG_LOCK = threading.RLock()
def load_config() -> dict:
try:
with open(CONFIG_FILE) as f:
return json.load(f)
except FileNotFoundError:
return {}
except Exception:
return {}
# NOTE: writes the config , using tempfile to prevent data loss
def _atomic_write(path: str, text: str):
"""Write via a temp file + atomic rename so a crash mid-write can never
leave a truncated/corrupt file at `path`."""
tmp = path + ".tmp"
with open(tmp, "w") as f:
f.write(text)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, path)
# NOTE: executes write to config using thread safe lock ?
def save_config(data: dict):
with _CONFIG_LOCK:
try:
_atomic_write(CONFIG_FILE, json.dumps(data, indent=2))
except Exception:
pass
def update_config(mutator):
"""Load the current on-disk config under the lock, apply `mutator(cfg)`,
save atomically, and return the result. Every config change — main thread
or worker — should go through this so no stale in-memory snapshot can
overwrite keys another thread just persisted."""
with _CONFIG_LOCK:
cfg = load_config()
mutator(cfg)
save_config(cfg)
return cfg
# ── logging ─────────────────────────────────────────────────────────────────
# One thread-safe log sink for the whole app. Any function — worker thread or
# main — calls log(); the GUI drains _LOG_Q on the main thread (see
# OctoUpdaterApp._poll) and renders each line. This keeps all Tk access on the