mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 00:07:45 +02:00
Sync from development - prepare for v0.4.0
This commit is contained in:
142
jackify/backend/utils/engine_error_parser.py
Normal file
142
jackify/backend/utils/engine_error_parser.py
Normal file
@@ -0,0 +1,142 @@
|
||||
import json
|
||||
from typing import Optional
|
||||
from jackify.shared.errors import (
|
||||
JackifyError, InstallError, OAuthError,
|
||||
oauth_expired, wabbajack_install_failed, format_technical_context,
|
||||
)
|
||||
|
||||
|
||||
def _ctx_detail(ctx: dict) -> Optional[str]:
|
||||
if not ctx:
|
||||
return None
|
||||
return format_technical_context(context=ctx)
|
||||
|
||||
|
||||
_TYPE_MAP = {
|
||||
"auth_failed": lambda msg, ctx: oauth_expired(),
|
||||
"premium_required": lambda msg, ctx: InstallError(
|
||||
"Nexus Premium Required",
|
||||
msg,
|
||||
suggestion="Jackify requires a Nexus Premium account for automated installs.",
|
||||
solutions=[
|
||||
"Log in to Nexus Mods with a Premium account.",
|
||||
"Non-premium support is planned for a future release.",
|
||||
],
|
||||
),
|
||||
"network_error": lambda msg, ctx: InstallError(
|
||||
"Network or Download Failure",
|
||||
msg,
|
||||
suggestion="Check your internet connection and retry.",
|
||||
solutions=[
|
||||
"Verify your internet connection.",
|
||||
"Re-run the install — Wabbajack resumes from where it stopped.",
|
||||
"Check if Nexus Mods is reachable at nexusmods.com.",
|
||||
"Disable VPN or proxy if active.",
|
||||
],
|
||||
technical=_ctx_detail(ctx),
|
||||
),
|
||||
"disk_full": lambda msg, ctx: InstallError(
|
||||
"Disk Full",
|
||||
msg,
|
||||
suggestion="Free space on the target drive and retry.",
|
||||
solutions=[
|
||||
"Run: df -h to see available space.",
|
||||
"Delete old modlist downloads or backups.",
|
||||
"Move the install to a larger drive.",
|
||||
],
|
||||
technical=_ctx_detail(ctx),
|
||||
),
|
||||
"permission_denied": lambda msg, ctx: InstallError(
|
||||
"Permission Denied",
|
||||
msg,
|
||||
suggestion="Check write permissions on the target path.",
|
||||
solutions=[
|
||||
"Ensure Jackify and Steam are run as the same user.",
|
||||
"Avoid install paths under /usr, /var, or /opt.",
|
||||
f"Check permissions: ls -la {ctx.get('path', '<path>')}",
|
||||
],
|
||||
technical=_ctx_detail(ctx),
|
||||
),
|
||||
"archive_corrupt": lambda msg, ctx: InstallError(
|
||||
"Corrupted Archive",
|
||||
msg,
|
||||
suggestion="Re-run the install — Wabbajack will re-download and re-verify the file.",
|
||||
solutions=[
|
||||
"Re-run the install.",
|
||||
"Check available disk space (partial downloads appear corrupt).",
|
||||
"Check Modlist_Install_workflow.log for the specific filename.",
|
||||
],
|
||||
technical=_ctx_detail(ctx),
|
||||
),
|
||||
"file_not_found": lambda msg, ctx: InstallError(
|
||||
"File Not Found",
|
||||
msg,
|
||||
suggestion="Check the modlist URL and your game installation paths.",
|
||||
solutions=[
|
||||
"Verify the modlist name is correct.",
|
||||
"Ensure the target game is installed.",
|
||||
"Re-run — the modlist index may have been temporarily unavailable.",
|
||||
],
|
||||
technical=_ctx_detail(ctx),
|
||||
),
|
||||
"validation_failed": lambda msg, ctx: InstallError(
|
||||
"Validation Failed",
|
||||
msg,
|
||||
suggestion="Re-run the install to re-download any failed files.",
|
||||
solutions=[
|
||||
"Re-run the install — Wabbajack resumes and re-validates.",
|
||||
"Check available disk space.",
|
||||
"Check Modlist_Install_workflow.log for specific failures.",
|
||||
],
|
||||
technical=_ctx_detail(ctx),
|
||||
),
|
||||
"download_stalled": lambda msg, ctx: InstallError(
|
||||
"Downloads Stalled",
|
||||
msg,
|
||||
suggestion="Check your connection and OAuth status, then retry.",
|
||||
solutions=[
|
||||
"Check your internet connection.",
|
||||
"In Settings, confirm Nexus OAuth is active.",
|
||||
"Re-run the install.",
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
_EXIT_CODE_MAP = {
|
||||
2: lambda d, c: _TYPE_MAP["auth_failed"](d, c or {}),
|
||||
3: lambda d, c: _TYPE_MAP["network_error"](d, c or {}),
|
||||
4: lambda d, c: _TYPE_MAP["disk_full"](d, c or {}),
|
||||
5: lambda d, c: _TYPE_MAP["validation_failed"](d, c or {}),
|
||||
6: lambda d, c: wabbajack_install_failed(format_technical_context(detail=d, context=c) or d),
|
||||
}
|
||||
|
||||
|
||||
def parse_engine_error_line(line: str) -> Optional[JackifyError]:
|
||||
"""Parse one stderr line. Returns JackifyError or None."""
|
||||
line = line.strip()
|
||||
if not line:
|
||||
return None
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return None
|
||||
if obj.get("je") != "1":
|
||||
return None
|
||||
if obj.get("level") == "warning":
|
||||
return None
|
||||
error_type = obj.get("type", "engine_error")
|
||||
message = obj.get("message", "An unknown engine error occurred.")
|
||||
context = obj.get("context") or {}
|
||||
factory = _TYPE_MAP.get(error_type)
|
||||
if factory:
|
||||
return factory(message, context)
|
||||
return wabbajack_install_failed(f"[{error_type}] {message}")
|
||||
|
||||
|
||||
def error_from_exit_code(exit_code: int, detail: str = "", context: Optional[dict] = None) -> Optional[JackifyError]:
|
||||
"""Return a JackifyError based on exit code alone (fallback when no stderr line received)."""
|
||||
factory = _EXIT_CODE_MAP.get(exit_code)
|
||||
if factory:
|
||||
detail_message = detail or f"Engine exited with code {exit_code}."
|
||||
return factory(detail_message, context or {})
|
||||
return None
|
||||
87
jackify/backend/utils/modlist_meta.py
Normal file
87
jackify/backend/utils/modlist_meta.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
JACKIFY_META_FILE = "jackify_meta.json"
|
||||
|
||||
_BYTEARRAY_RE = re.compile(r"@ByteArray\((.+)\)", re.DOTALL)
|
||||
|
||||
|
||||
def write_modlist_meta(
|
||||
install_dir: str,
|
||||
modlist_name: str,
|
||||
game_type: Optional[str],
|
||||
install_mode: str = "online",
|
||||
modlist_version: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""Write jackify_meta.json into install_dir. Returns True on success."""
|
||||
from jackify import __version__ as jackify_version
|
||||
import datetime
|
||||
|
||||
try:
|
||||
meta = {
|
||||
"modlist_name": modlist_name,
|
||||
"game_type": game_type or "",
|
||||
"install_mode": install_mode,
|
||||
"install_date": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||
"jackify_version": jackify_version,
|
||||
}
|
||||
if modlist_version:
|
||||
meta["modlist_version"] = modlist_version
|
||||
|
||||
out = Path(install_dir) / JACKIFY_META_FILE
|
||||
out.write_text(json.dumps(meta, indent=2), encoding="utf-8")
|
||||
logger.debug(f"Wrote modlist meta to {out}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to write modlist meta: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def read_modlist_meta(install_dir: str) -> Optional[dict]:
|
||||
"""Read jackify_meta.json from install_dir. Returns dict or None."""
|
||||
try:
|
||||
meta_path = Path(install_dir) / JACKIFY_META_FILE
|
||||
if not meta_path.exists():
|
||||
return None
|
||||
return json.loads(meta_path.read_text(encoding="utf-8"))
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to read modlist meta from {install_dir}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _read_selected_profile(install_dir: str) -> Optional[str]:
|
||||
"""Read selected_profile from ModOrganizer.ini, stripping @ByteArray() wrapper."""
|
||||
try:
|
||||
mo2_ini = Path(install_dir) / "ModOrganizer.ini"
|
||||
if not mo2_ini.exists():
|
||||
return None
|
||||
for line in mo2_ini.read_text(encoding="utf-8", errors="ignore").splitlines():
|
||||
if not line.startswith("selected_profile"):
|
||||
continue
|
||||
_, _, value = line.partition("=")
|
||||
value = value.strip()
|
||||
m = _BYTEARRAY_RE.match(value)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
return value or None
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to read selected_profile from {install_dir}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_modlist_name(install_dir: str) -> Optional[str]:
|
||||
"""Return the best available modlist name for install_dir.
|
||||
|
||||
Priority:
|
||||
1. jackify_meta.json (written by Jackify at install time)
|
||||
2. selected_profile from ModOrganizer.ini (set by modlist author)
|
||||
"""
|
||||
meta = read_modlist_meta(install_dir)
|
||||
if meta and meta.get("modlist_name"):
|
||||
return meta["modlist_name"]
|
||||
return _read_selected_profile(install_dir)
|
||||
Reference in New Issue
Block a user