fix: silent pipeline log and server indexing progress
Three issues caused the Logs view to appear blank during a real pipeline run: 1. `from run import step_fetch, step_link` was outside the worker's try/except/finally. An import failure silently killed the thread, leaving _pipeline_done uncalled and the Run button stuck disabled forever. Now wrapped in its own try/except that posts the error to the log and resets the UI. 2. `build_server_index` makes N sequential HTTP requests (one per mod folder's meta.cpp) with no output during the scan. Added an optional `progress_fn(current, total, name)` callback; step_fetch wires it to print progress every 25 folders so the log never goes silent. 3. No immediate feedback after clicking Start — the log was blank until the worker thread started printing. Now posts a "Pipeline started" banner from the main thread before the worker launches.
This commit is contained in:
@@ -112,6 +112,10 @@ Pass 2 builds `ok_disk_names` — the set of disk names that already match the s
|
||||
|
||||
**GUI threading model:** Every network or long-running operation runs in a `threading.Thread(daemon=True)` so the Tkinter event loop is never blocked. The only safe way to update widgets from a background thread is `self.after(0, callback)` — never touch widgets directly from a worker thread. `_poll_log` drains the entire log queue in one `after(80, ...)` tick and does a single batched `CTkTextbox.insert()` call rather than one per log entry, keeping the UI smooth even when `tqdm` emits many rapid updates during downloads. The wizard's "Test Connection" button follows the same pattern: `requests.get` runs in a daemon thread; the result is posted back via `self.after(0, ...)` with widget references captured *before* the thread starts, so stale references cannot update the wrong widgets if the user navigates away mid-request.
|
||||
|
||||
**`run_pipeline` worker — import guard:** `from run import step_fetch, step_link` is performed inside its own `try/except` *before* stdout is redirected. If this import fails for any reason the exception is posted to the log via `self.after(0, ...)` and `_pipeline_done` is called so the UI resets cleanly. Previously an import failure would silently kill the worker thread and leave the pipeline button disabled forever.
|
||||
|
||||
**`build_server_index` progress callback:** Accepts an optional `progress_fn(current, total, name)` callback. `step_fetch` in `run.py` uses this to print `Indexing N/M: @FolderName` every 25 folders so the log never goes silent during the server scan phase. The library itself never calls `print` — the caller owns the I/O.
|
||||
|
||||
### GUI localization (`gui/locales.py`)
|
||||
|
||||
All user-facing strings are centralised in `gui/locales.py`. Two languages are supported: English (`"en"`) and Vietnamese (`"vi"`).
|
||||
|
||||
@@ -81,7 +81,11 @@ def make_session(auth: tuple[str, str]) -> requests.Session:
|
||||
return s
|
||||
|
||||
|
||||
def build_server_index(base_url: str, auth: tuple[str, str]) -> dict:
|
||||
def build_server_index(
|
||||
base_url: str,
|
||||
auth: tuple[str, str],
|
||||
progress_fn: "Callable[[int, int, str], None] | None" = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Scan the root of the file server and build mod lookup maps.
|
||||
|
||||
@@ -90,6 +94,9 @@ def build_server_index(base_url: str, auth: tuple[str, str]) -> dict:
|
||||
|
||||
:param base_url: Root URL of the Caddy file server (trailing slash optional).
|
||||
:param auth: ``(username, password)`` tuple for HTTP Basic Auth.
|
||||
:param progress_fn: Optional callback called as ``progress_fn(current, total, name)``
|
||||
after each folder is processed. Use it to report progress without
|
||||
coupling the library to ``print`` or any specific I/O sink.
|
||||
:returns: Dict with keys:
|
||||
|
||||
- ``by_steam_id`` — ``{steam_id: folder_url}``
|
||||
@@ -100,11 +107,12 @@ def build_server_index(base_url: str, auth: tuple[str, str]) -> dict:
|
||||
root = base_url.rstrip("/") + "/"
|
||||
items = _list_dir(root, session)
|
||||
folders = [it for it in items if it.get("is_dir")]
|
||||
total = len(folders)
|
||||
|
||||
by_steam_id: dict[str, str] = {}
|
||||
by_name: dict[str, str] = {}
|
||||
|
||||
for folder in folders:
|
||||
for i, folder in enumerate(folders, 1):
|
||||
name = folder["name"].strip("/")
|
||||
url = _folder_url(root, name)
|
||||
by_name[_normalize_name(name)] = url
|
||||
@@ -118,6 +126,9 @@ def build_server_index(base_url: str, auth: tuple[str, str]) -> dict:
|
||||
except requests.RequestException:
|
||||
pass # meta.cpp missing or unreachable — name-based fallback still works
|
||||
|
||||
if progress_fn is not None:
|
||||
progress_fn(i, total, name)
|
||||
|
||||
return {"by_steam_id": by_steam_id, "by_name": by_name, "folders": folders}
|
||||
|
||||
|
||||
|
||||
13
gui/app.py
13
gui/app.py
@@ -143,11 +143,22 @@ class ArmaModManagerApp(ctk.CTk):
|
||||
self._pipeline_running = True
|
||||
self._get_dashboard().set_pipeline_ui(running=True)
|
||||
self.navigate_to("Logs")
|
||||
# Post an immediate banner so the log is never blank after clicking Start.
|
||||
_sep = "=" * 50
|
||||
self.post_log(f"\n{_sep}\n {t('pipeline.starting')}\n{_sep}\n\n")
|
||||
|
||||
def worker() -> None:
|
||||
# run.py calls fix_console_encoding() at import time, which needs
|
||||
# the real sys.stdout.buffer. Import it before we redirect stdout.
|
||||
from run import step_fetch, step_link
|
||||
try:
|
||||
from run import step_fetch, step_link
|
||||
except Exception as _import_err:
|
||||
self.after(0, lambda: self.post_log(
|
||||
f"\n✗ Failed to load pipeline: {_import_err}\n"
|
||||
))
|
||||
self.after(0, self._pipeline_done)
|
||||
return
|
||||
|
||||
self._redirect_output()
|
||||
try:
|
||||
from arma_modlist_tools.parser import parse_modlist_html
|
||||
|
||||
@@ -26,6 +26,7 @@ _EN: dict[str, str] = {
|
||||
"nav.settings": "Settings",
|
||||
|
||||
# ── Pipeline step headers (printed to log) ───────────────────────────────
|
||||
"pipeline.starting": "Pipeline started",
|
||||
"pipeline.step1_name": "Parse presets",
|
||||
"pipeline.step2_name": "Compare presets",
|
||||
"pipeline.step3_name": "Download mods",
|
||||
@@ -266,6 +267,7 @@ _VI: dict[str, str] = {
|
||||
"nav.settings": "Cài đặt",
|
||||
|
||||
# ── Pipeline step headers ────────────────────────────────────────────────
|
||||
"pipeline.starting": "Pipeline đã bắt đầu",
|
||||
"pipeline.step1_name": "Phân tích preset",
|
||||
"pipeline.step2_name": "So sánh preset",
|
||||
"pipeline.step3_name": "Tải mod",
|
||||
|
||||
9
run.py
9
run.py
@@ -91,9 +91,14 @@ def step_fetch(cfg) -> None:
|
||||
for mod in data["mods"]:
|
||||
queue.append((mod, preset_name))
|
||||
|
||||
def _index_progress(current: int, total: int, name: str) -> None:
|
||||
if current == 1 or current % 25 == 0 or current == total:
|
||||
print(f" Indexing {current}/{total}: {name}")
|
||||
|
||||
print(f" Building server index...")
|
||||
index = build_server_index(cfg.server_url, cfg.server_auth)
|
||||
print(f" Indexed {len(index['by_steam_id'])} mods\n")
|
||||
index = build_server_index(cfg.server_url, cfg.server_auth, progress_fn=_index_progress)
|
||||
print(f" Indexed {len(index['by_steam_id'])} mods by steam_id, "
|
||||
f"{len(index['by_name'])} by name\n")
|
||||
|
||||
session = make_session(cfg.server_auth)
|
||||
resolved = []
|
||||
|
||||
Reference in New Issue
Block a user