mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
523681a254 | ||
|
|
abfca5268f | ||
|
|
4de5c7f55d | ||
|
|
9c52c0434b |
43
CHANGELOG.md
43
CHANGELOG.md
@@ -1,5 +1,48 @@
|
||||
# Jackify Changelog
|
||||
|
||||
## v0.2.0.3 - Engine Bugfix & Settings Cleanup
|
||||
**Release Date:** 2025-12-21
|
||||
|
||||
### Engine Updates
|
||||
- **jackify-engine 0.4.3**: Bugfix release
|
||||
|
||||
### UI Improvements
|
||||
- **Settings Dialog**: Removed GPU disable toggle - GPU usage is now always enabled (the disable option was non-functional)
|
||||
|
||||
---
|
||||
|
||||
## v0.2.0.2 - Emergency Engine Bugfix
|
||||
**Release Date:** 2025-12-18
|
||||
|
||||
### Engine Updates
|
||||
- **jackify-engine 0.4.2**: Fixed OOM issue with jackify-engine 0.4.1 due to array size
|
||||
|
||||
---
|
||||
|
||||
## v0.2.0.1 - Critical Bugfix Release
|
||||
**Release Date:** 2025-12-15
|
||||
|
||||
### Critical Bug Fixes
|
||||
- **Directory Safety Validation**: Fixed data loss bug where directories with only a `downloads/` folder were incorrectly identified as valid modlist directories
|
||||
- **Flatpak Steam Restart**: Fixed Steam restart failures on Ubuntu/PopOS by removing incompatible `-foreground` flag and increasing startup wait
|
||||
|
||||
### Bug Fixes
|
||||
- **External Links**: Fixed Ko-fi, GitHub, and Nexus links not opening on some distros using xdg-open with clean environment
|
||||
- **TTW Console Output**: Filtered standalone "OK"/"DONE" noise messages from TTW installation console
|
||||
- **Activity Window**: Fixed progress display updates in TTW Installer and other workflows
|
||||
- **Wine Component Installation**: Added status feedback during component installation showing component list
|
||||
- **Progress Parser**: Added defensive checks to prevent segfaults from malformed engine output
|
||||
- **Progress Parser Speed Info**: Fixed 'OperationType' object has no attribute 'lower' error by converting enum to string value when extracting speed info from timestamp status patterns
|
||||
|
||||
### Improvements
|
||||
- **Default Wine Components**: Added dxvk to default component list for better graphics compatibility
|
||||
- **TTW Installer UI**: Show version numbers in status displays
|
||||
|
||||
### Engine Updates
|
||||
- **jackify-engine 0.4.1**: Download reliability fixes, BSA case sensitivity handling, external drive I/O limiting, GPU detection caching, and texture processing performance improvements
|
||||
|
||||
---
|
||||
|
||||
## v0.2.0 - Modlist Gallery, OAuth Authentication & Performance Improvements
|
||||
**Release Date:** 2025-12-06
|
||||
|
||||
|
||||
@@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing
|
||||
Wabbajack modlists natively on Linux systems.
|
||||
"""
|
||||
|
||||
__version__ = "0.2.0"
|
||||
__version__ = "0.2.0.3"
|
||||
|
||||
@@ -722,7 +722,7 @@ class ModlistHandler:
|
||||
try:
|
||||
self.logger.info("Installing Wine components using user's preferred method...")
|
||||
self.logger.debug(f"Calling winetricks_handler.install_wine_components with wineprefix={wineprefix}, game_var={self.game_var_full}, components={components}")
|
||||
success = self.winetricks_handler.install_wine_components(wineprefix, self.game_var_full, specific_components=components)
|
||||
success = self.winetricks_handler.install_wine_components(wineprefix, self.game_var_full, specific_components=components, status_callback=status_callback)
|
||||
if success:
|
||||
self.logger.info("Wine component installation completed successfully")
|
||||
if status_callback:
|
||||
|
||||
@@ -224,7 +224,7 @@ class ProgressParser:
|
||||
speed_info = self._parse_speed_from_string(speed_str)
|
||||
if speed_info:
|
||||
operation = self._detect_operation_from_line(status_text)
|
||||
result.speed_info = (operation, speed_info)
|
||||
result.speed_info = (operation.value, speed_info)
|
||||
|
||||
# Calculate overall percentage from step progress
|
||||
if max_steps > 0:
|
||||
@@ -400,6 +400,18 @@ class ProgressParser:
|
||||
|
||||
def _extract_file_progress(self, line: str) -> Optional[FileProgress]:
|
||||
"""Extract file-level progress information."""
|
||||
# CRITICAL: Defensive checks to prevent segfault in regex engine
|
||||
# Segfaults happen in C code before Python exceptions, so we must validate input first
|
||||
if not line or not isinstance(line, str):
|
||||
return None
|
||||
# Limit line length to prevent stack overflow in regex (10KB should be more than enough)
|
||||
if len(line) > 10000:
|
||||
return None
|
||||
# Check for null bytes or other problematic characters that could corrupt regex
|
||||
if '\x00' in line:
|
||||
# Replace null bytes to prevent corruption
|
||||
line = line.replace('\x00', '')
|
||||
|
||||
# PRIORITY: Check for [FILE_PROGRESS] prefix first (new engine format)
|
||||
# Format: [FILE_PROGRESS] Downloading: filename.zip (20.0%) [3.7MB/s]
|
||||
# Updated format: [FILE_PROGRESS] (Downloading|Extracting|Installing|Converting|Completed|etc): filename.zip (20.0%) [3.7MB/s] (current/total)
|
||||
@@ -907,6 +919,13 @@ class ProgressStateManager:
|
||||
self._remove_synthetic_wabbajack()
|
||||
# Mark that we have a real .wabbajack entry to prevent synthetic ones
|
||||
self._has_real_wabbajack = True
|
||||
else:
|
||||
# CRITICAL: If we get a real archive file (not .wabbajack), remove all .wabbajack entries
|
||||
# This ensures .wabbajack entries disappear as soon as archive downloads start
|
||||
from jackify.shared.progress_models import OperationType
|
||||
if parsed.file_progress.operation == OperationType.DOWNLOAD:
|
||||
self._remove_all_wabbajack_entries()
|
||||
self._has_real_wabbajack = True # Prevent re-adding
|
||||
self._augment_file_metrics(parsed.file_progress)
|
||||
# Don't add files that are already at 100% unless they're being updated
|
||||
# This prevents re-adding completed files
|
||||
@@ -934,6 +953,21 @@ class ProgressStateManager:
|
||||
self.state.add_file(parsed.file_progress)
|
||||
updated = True
|
||||
elif parsed.data_info:
|
||||
# CRITICAL: Remove .wabbajack entries as soon as archive download phase starts
|
||||
# Check if we're in "Downloading Mod Archives" phase or have real archive files downloading
|
||||
phase_name_lower = (parsed.phase_name or "").lower()
|
||||
message_lower = (parsed.message or "").lower()
|
||||
is_archive_phase = (
|
||||
'mod archives' in phase_name_lower or
|
||||
'downloading mod archives' in message_lower or
|
||||
(parsed.phase == InstallationPhase.DOWNLOAD and self._has_real_download_activity())
|
||||
)
|
||||
|
||||
if is_archive_phase:
|
||||
# Archive download phase has started - remove all .wabbajack entries immediately
|
||||
self._remove_all_wabbajack_entries()
|
||||
self._has_real_wabbajack = True # Prevent re-adding
|
||||
|
||||
# Only create synthetic .wabbajack entry if we don't already have a real one
|
||||
if not getattr(self, '_has_real_wabbajack', False):
|
||||
if self._maybe_add_wabbajack_progress(parsed):
|
||||
@@ -1164,4 +1198,19 @@ class ProgressStateManager:
|
||||
remaining.append(fp)
|
||||
if removed:
|
||||
self.state.active_files = remaining
|
||||
|
||||
def _remove_all_wabbajack_entries(self):
|
||||
"""Remove ALL .wabbajack entries (synthetic and real) when archive download phase starts."""
|
||||
remaining = []
|
||||
removed = False
|
||||
for fp in self.state.active_files:
|
||||
if fp.filename.lower().endswith('.wabbajack') or 'wabbajack' in fp.filename.lower():
|
||||
removed = True
|
||||
self._file_history.pop(fp.filename, None)
|
||||
continue
|
||||
remaining.append(fp)
|
||||
if removed:
|
||||
self.state.active_files = remaining
|
||||
# Also clear the wabbajack entry name to prevent re-adding
|
||||
self._wabbajack_entry_name = None
|
||||
|
||||
|
||||
@@ -300,7 +300,7 @@ class ValidationHandler:
|
||||
def looks_like_modlist_dir(self, path: Path) -> bool:
|
||||
"""Return True if the directory contains files/folders typical of a modlist install."""
|
||||
expected = [
|
||||
'ModOrganizer.exe', 'profiles', 'mods', 'downloads', '.wabbajack', '.jackify_modlist_marker', 'ModOrganizer.ini'
|
||||
'ModOrganizer.exe', 'profiles', 'mods', '.wabbajack', '.jackify_modlist_marker', 'ModOrganizer.ini'
|
||||
]
|
||||
for item in expected:
|
||||
if (path / item).exists():
|
||||
|
||||
@@ -9,7 +9,7 @@ import os
|
||||
import subprocess
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Callable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -110,10 +110,16 @@ class WinetricksHandler:
|
||||
self.logger.error(f"Error testing winetricks: {e}")
|
||||
return False
|
||||
|
||||
def install_wine_components(self, wineprefix: str, game_var: str, specific_components: Optional[List[str]] = None) -> bool:
|
||||
def install_wine_components(self, wineprefix: str, game_var: str, specific_components: Optional[List[str]] = None, status_callback: Optional[Callable[[str], None]] = None) -> bool:
|
||||
"""
|
||||
Install the specified Wine components into the given prefix using winetricks.
|
||||
If specific_components is None, use the default set (fontsmooth=rgb, xact, xact_x64, vcrun2022).
|
||||
|
||||
Args:
|
||||
wineprefix: Path to Wine prefix
|
||||
game_var: Game name for logging
|
||||
specific_components: Optional list of specific components to install
|
||||
status_callback: Optional callback function(status_message: str) for progress updates
|
||||
"""
|
||||
if not self.is_available():
|
||||
self.logger.error("Winetricks is not available")
|
||||
@@ -268,11 +274,18 @@ class WinetricksHandler:
|
||||
|
||||
if not all_components:
|
||||
self.logger.info("No Wine components to install.")
|
||||
if status_callback:
|
||||
status_callback("No Wine components to install")
|
||||
return True
|
||||
|
||||
# Reorder components for proper installation sequence
|
||||
components_to_install = self._reorder_components_for_installation(all_components)
|
||||
self.logger.info(f"WINEPREFIX: {wineprefix}, Game: {game_var}, Ordered Components: {components_to_install}")
|
||||
|
||||
# Show status with component list
|
||||
if status_callback:
|
||||
components_list = ', '.join(components_to_install)
|
||||
status_callback(f"Installing Wine components: {components_list}")
|
||||
|
||||
# Check user preference for component installation method
|
||||
from ..handlers.config_handler import ConfigHandler
|
||||
@@ -290,7 +303,7 @@ class WinetricksHandler:
|
||||
# Choose installation method based on user preference
|
||||
if method == 'system_protontricks':
|
||||
self.logger.info("Using system protontricks for all components")
|
||||
return self._install_components_protontricks_only(components_to_install, wineprefix, game_var)
|
||||
return self._install_components_protontricks_only(components_to_install, wineprefix, game_var, status_callback)
|
||||
# else: method == 'winetricks' (default behavior continues below)
|
||||
|
||||
# Install all components together with winetricks (faster)
|
||||
@@ -358,6 +371,9 @@ class WinetricksHandler:
|
||||
# Verify components were actually installed
|
||||
if self._verify_components_installed(wineprefix, components_to_install, env):
|
||||
self.logger.info("Component verification successful - all components installed correctly.")
|
||||
components_list = ', '.join(components_to_install)
|
||||
if status_callback:
|
||||
status_callback(f"Wine components installed and verified: {components_list}")
|
||||
# Set Windows 10 mode after component installation (matches legacy script timing)
|
||||
self._set_windows_10_mode(wineprefix, env.get('WINE', ''))
|
||||
return True
|
||||
@@ -461,7 +477,7 @@ class WinetricksHandler:
|
||||
self.logger.warning("=" * 80)
|
||||
|
||||
# Attempt fallback to protontricks
|
||||
fallback_success = self._install_components_protontricks_only(components_to_install, wineprefix, game_var)
|
||||
fallback_success = self._install_components_protontricks_only(components_to_install, wineprefix, game_var, status_callback)
|
||||
|
||||
if fallback_success:
|
||||
self.logger.info("SUCCESS: Protontricks fallback succeeded where winetricks failed")
|
||||
@@ -698,7 +714,7 @@ class WinetricksHandler:
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error setting Windows 10 mode: {e}")
|
||||
|
||||
def _install_components_protontricks_only(self, components: list, wineprefix: str, game_var: str) -> bool:
|
||||
def _install_components_protontricks_only(self, components: list, wineprefix: str, game_var: str, status_callback: Optional[Callable[[str], None]] = None) -> bool:
|
||||
"""
|
||||
Install all components using protontricks only.
|
||||
This matches the behavior of the original bash script.
|
||||
@@ -732,6 +748,9 @@ class WinetricksHandler:
|
||||
return False
|
||||
|
||||
# Install all components using protontricks
|
||||
components_list = ', '.join(components)
|
||||
if status_callback:
|
||||
status_callback(f"Installing Wine components via protontricks: {components_list}")
|
||||
success = protontricks_handler.install_wine_components(appid, game_var, components)
|
||||
|
||||
if success:
|
||||
|
||||
@@ -150,8 +150,14 @@ def is_flatpak_steam() -> bool:
|
||||
stderr=subprocess.DEVNULL, # Suppress stderr to avoid error messages
|
||||
text=True,
|
||||
timeout=5)
|
||||
if result.returncode == 0 and 'com.valvesoftware.Steam' in result.stdout:
|
||||
return True
|
||||
if result.returncode == 0:
|
||||
# Check for exact match - "com.valvesoftware.Steam" as a whole word
|
||||
# This prevents matching "com.valvesoftware.SteamLink" or similar
|
||||
for line in result.stdout.splitlines():
|
||||
parts = line.split()
|
||||
if parts and parts[0] == 'com.valvesoftware.Steam':
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.debug(f"Error detecting Flatpak Steam: {e}")
|
||||
return False
|
||||
@@ -222,7 +228,8 @@ def _start_steam_nak_style(is_steamdeck_flag=False, is_flatpak_flag=False, env_o
|
||||
subprocess.Popen("steam", shell=True, env=env)
|
||||
|
||||
time.sleep(5)
|
||||
check_result = subprocess.run(['pgrep', '-f', 'steam'], capture_output=True, timeout=10, env=env)
|
||||
# Use steamwebhelper for detection (actual Steam process, not steam-powerbuttond)
|
||||
check_result = subprocess.run(['pgrep', '-f', 'steamwebhelper'], capture_output=True, timeout=10, env=env)
|
||||
if check_result.returncode == 0:
|
||||
logger.info("NaK-style restart detected running Steam process.")
|
||||
return True
|
||||
@@ -282,7 +289,8 @@ def start_steam(is_steamdeck_flag=None, is_flatpak_flag=None, env_override=None,
|
||||
subprocess.Popen(["flatpak", "run", "com.valvesoftware.Steam"],
|
||||
env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
time.sleep(7) # Give Flatpak more time to start
|
||||
check_result = subprocess.run(['pgrep', '-f', 'steam'], capture_output=True, timeout=10, env=env)
|
||||
# For Flatpak Steam, check for the flatpak process, not steamwebhelper
|
||||
check_result = subprocess.run(['pgrep', '-f', 'com.valvesoftware.Steam'], capture_output=True, timeout=10, env=env)
|
||||
if check_result.returncode == 0:
|
||||
logger.info("Flatpak Steam started successfully")
|
||||
return True
|
||||
@@ -308,7 +316,8 @@ def start_steam(is_steamdeck_flag=None, is_flatpak_flag=None, env_override=None,
|
||||
if process is not None:
|
||||
logger.info(f"Initiated Steam start with {method_name}.")
|
||||
time.sleep(5) # Wait 5 seconds as in existing logic
|
||||
check_result = subprocess.run(['pgrep', '-f', 'steam'], capture_output=True, timeout=10, env=env)
|
||||
# Use steamwebhelper for detection (actual Steam process, not steam-powerbuttond)
|
||||
check_result = subprocess.run(['pgrep', '-f', 'steamwebhelper'], capture_output=True, timeout=10, env=env)
|
||||
if check_result.returncode == 0:
|
||||
logger.info(f"Steam process detected after using {method_name}. Proceeding to wait phase.")
|
||||
return True
|
||||
@@ -367,7 +376,7 @@ def robust_steam_restart(progress_callback: Optional[Callable[[str], None]] = No
|
||||
try:
|
||||
report("Flatpak Steam detected - stopping via flatpak...")
|
||||
subprocess.run(['flatpak', 'kill', 'com.valvesoftware.Steam'],
|
||||
timeout=15, check=False, capture_output=True, stderr=subprocess.DEVNULL, env=shutdown_env)
|
||||
timeout=15, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=shutdown_env)
|
||||
time.sleep(2)
|
||||
except Exception as e:
|
||||
logger.debug(f"flatpak kill failed: {e}")
|
||||
@@ -427,7 +436,8 @@ def robust_steam_restart(progress_callback: Optional[Callable[[str], None]] = No
|
||||
report("Failed to start Steam.")
|
||||
return False
|
||||
|
||||
# Wait for Steam to fully initialize using existing logic
|
||||
# Wait for Steam to fully initialize
|
||||
# CRITICAL: Use steamwebhelper (actual Steam process), not "steam" (matches steam-powerbuttond, etc.)
|
||||
report("Waiting for Steam to fully start")
|
||||
logger.info("Waiting up to 2 minutes for Steam to fully initialize...")
|
||||
max_startup_wait = 120
|
||||
@@ -436,7 +446,8 @@ def robust_steam_restart(progress_callback: Optional[Callable[[str], None]] = No
|
||||
|
||||
while elapsed_wait < max_startup_wait:
|
||||
try:
|
||||
result = subprocess.run(['pgrep', '-f', 'steam'], capture_output=True, timeout=10, env=start_env)
|
||||
# Use steamwebhelper for detection (matches shutdown logic)
|
||||
result = subprocess.run(['pgrep', '-f', 'steamwebhelper'], capture_output=True, timeout=10, env=start_env)
|
||||
if result.returncode == 0:
|
||||
if not initial_wait_done:
|
||||
logger.info("Steam process detected. Waiting additional time for full initialization...")
|
||||
@@ -444,7 +455,7 @@ def robust_steam_restart(progress_callback: Optional[Callable[[str], None]] = No
|
||||
time.sleep(5)
|
||||
elapsed_wait += 5
|
||||
if initial_wait_done and elapsed_wait >= 15:
|
||||
final_check = subprocess.run(['pgrep', '-f', 'steam'], capture_output=True, timeout=10, env=start_env)
|
||||
final_check = subprocess.run(['pgrep', '-f', 'steamwebhelper'], capture_output=True, timeout=10, env=start_env)
|
||||
if final_check.returncode == 0:
|
||||
report("Steam started successfully.")
|
||||
logger.info("Steam confirmed running after wait.")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user