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.
|
**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`)
|
### GUI localization (`gui/locales.py`)
|
||||||
|
|
||||||
All user-facing strings are centralised in `gui/locales.py`. Two languages are supported: English (`"en"`) and Vietnamese (`"vi"`).
|
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
|
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.
|
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 base_url: Root URL of the Caddy file server (trailing slash optional).
|
||||||
:param auth: ``(username, password)`` tuple for HTTP Basic Auth.
|
: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:
|
:returns: Dict with keys:
|
||||||
|
|
||||||
- ``by_steam_id`` — ``{steam_id: folder_url}``
|
- ``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("/") + "/"
|
root = base_url.rstrip("/") + "/"
|
||||||
items = _list_dir(root, session)
|
items = _list_dir(root, session)
|
||||||
folders = [it for it in items if it.get("is_dir")]
|
folders = [it for it in items if it.get("is_dir")]
|
||||||
|
total = len(folders)
|
||||||
|
|
||||||
by_steam_id: dict[str, str] = {}
|
by_steam_id: dict[str, str] = {}
|
||||||
by_name: dict[str, str] = {}
|
by_name: dict[str, str] = {}
|
||||||
|
|
||||||
for folder in folders:
|
for i, folder in enumerate(folders, 1):
|
||||||
name = folder["name"].strip("/")
|
name = folder["name"].strip("/")
|
||||||
url = _folder_url(root, name)
|
url = _folder_url(root, name)
|
||||||
by_name[_normalize_name(name)] = url
|
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:
|
except requests.RequestException:
|
||||||
pass # meta.cpp missing or unreachable — name-based fallback still works
|
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}
|
return {"by_steam_id": by_steam_id, "by_name": by_name, "folders": folders}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
11
gui/app.py
11
gui/app.py
@@ -143,11 +143,22 @@ class ArmaModManagerApp(ctk.CTk):
|
|||||||
self._pipeline_running = True
|
self._pipeline_running = True
|
||||||
self._get_dashboard().set_pipeline_ui(running=True)
|
self._get_dashboard().set_pipeline_ui(running=True)
|
||||||
self.navigate_to("Logs")
|
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:
|
def worker() -> None:
|
||||||
# run.py calls fix_console_encoding() at import time, which needs
|
# run.py calls fix_console_encoding() at import time, which needs
|
||||||
# the real sys.stdout.buffer. Import it before we redirect stdout.
|
# the real sys.stdout.buffer. Import it before we redirect stdout.
|
||||||
|
try:
|
||||||
from run import step_fetch, step_link
|
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()
|
self._redirect_output()
|
||||||
try:
|
try:
|
||||||
from arma_modlist_tools.parser import parse_modlist_html
|
from arma_modlist_tools.parser import parse_modlist_html
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ _EN: dict[str, str] = {
|
|||||||
"nav.settings": "Settings",
|
"nav.settings": "Settings",
|
||||||
|
|
||||||
# ── Pipeline step headers (printed to log) ───────────────────────────────
|
# ── Pipeline step headers (printed to log) ───────────────────────────────
|
||||||
|
"pipeline.starting": "Pipeline started",
|
||||||
"pipeline.step1_name": "Parse presets",
|
"pipeline.step1_name": "Parse presets",
|
||||||
"pipeline.step2_name": "Compare presets",
|
"pipeline.step2_name": "Compare presets",
|
||||||
"pipeline.step3_name": "Download mods",
|
"pipeline.step3_name": "Download mods",
|
||||||
@@ -266,6 +267,7 @@ _VI: dict[str, str] = {
|
|||||||
"nav.settings": "Cài đặt",
|
"nav.settings": "Cài đặt",
|
||||||
|
|
||||||
# ── Pipeline step headers ────────────────────────────────────────────────
|
# ── Pipeline step headers ────────────────────────────────────────────────
|
||||||
|
"pipeline.starting": "Pipeline đã bắt đầu",
|
||||||
"pipeline.step1_name": "Phân tích preset",
|
"pipeline.step1_name": "Phân tích preset",
|
||||||
"pipeline.step2_name": "So sánh preset",
|
"pipeline.step2_name": "So sánh preset",
|
||||||
"pipeline.step3_name": "Tải mod",
|
"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"]:
|
for mod in data["mods"]:
|
||||||
queue.append((mod, preset_name))
|
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...")
|
print(f" Building server index...")
|
||||||
index = build_server_index(cfg.server_url, cfg.server_auth)
|
index = build_server_index(cfg.server_url, cfg.server_auth, progress_fn=_index_progress)
|
||||||
print(f" Indexed {len(index['by_steam_id'])} mods\n")
|
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)
|
session = make_session(cfg.server_auth)
|
||||||
resolved = []
|
resolved = []
|
||||||
|
|||||||
Reference in New Issue
Block a user