mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 00:37:45 +02:00
Sync from development - prepare for v0.2.0.1
This commit is contained in:
@@ -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.")
|
||||
|
||||
Reference in New Issue
Block a user