mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Sync from development - prepare for v0.1.7.1
This commit is contained in:
@@ -100,7 +100,8 @@ class ConfigHandler:
|
||||
libraryfolders_vdf_paths = [
|
||||
os.path.expanduser("~/.steam/steam/config/libraryfolders.vdf"),
|
||||
os.path.expanduser("~/.local/share/Steam/config/libraryfolders.vdf"),
|
||||
os.path.expanduser("~/.steam/root/config/libraryfolders.vdf")
|
||||
os.path.expanduser("~/.steam/root/config/libraryfolders.vdf"),
|
||||
os.path.expanduser("~/.var/app/com.valvesoftware.Steam/.local/share/Steam/config/libraryfolders.vdf") # Flatpak
|
||||
]
|
||||
|
||||
for vdf_path in libraryfolders_vdf_paths:
|
||||
|
||||
@@ -784,7 +784,8 @@ class FileSystemHandler:
|
||||
possible_vdf_paths = [
|
||||
Path.home() / ".steam/steam/config/libraryfolders.vdf",
|
||||
Path.home() / ".local/share/Steam/config/libraryfolders.vdf",
|
||||
Path.home() / ".steam/root/config/libraryfolders.vdf"
|
||||
Path.home() / ".steam/root/config/libraryfolders.vdf",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/config/libraryfolders.vdf" # Flatpak
|
||||
]
|
||||
|
||||
libraryfolders_vdf_path: Optional[Path] = None
|
||||
|
||||
@@ -650,6 +650,10 @@ class ModlistMenuHandler:
|
||||
print("Modlist Install and Configuration complete!")
|
||||
print(f"• You should now be able to Launch '{context.get('name')}' through Steam")
|
||||
print("• Congratulations and enjoy the game!")
|
||||
print("")
|
||||
print("NOTE: If you experience ENB issues, consider using GE-Proton 10-14 instead of")
|
||||
print(" Valve's Proton 10 (known ENB compatibility issues in Valve's Proton 10).")
|
||||
print("")
|
||||
print("Detailed log available at: ~/Jackify/logs/Configure_New_Modlist_workflow.log")
|
||||
# Only wait for input in CLI mode, not GUI mode
|
||||
if not gui_mode:
|
||||
|
||||
@@ -329,22 +329,18 @@ class ModlistHandler:
|
||||
# On non-Steam Deck systems, /media mounts should use Z: drive, not D: drive
|
||||
is_on_sdcard_path = str(self.modlist_dir).startswith("/run/media") or str(self.modlist_dir).startswith("/media")
|
||||
|
||||
# DEBUG: Log SD card detection logic
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] SD card detection for instance id={id(self)}:")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] modlist_dir: {self.modlist_dir}")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] is_on_sdcard_path: {is_on_sdcard_path}")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] self.steamdeck: {self.steamdeck}")
|
||||
# Log SD card detection for debugging
|
||||
self.logger.debug(f"SD card detection: modlist_dir={self.modlist_dir}, is_sdcard_path={is_on_sdcard_path}, steamdeck={self.steamdeck}")
|
||||
|
||||
if is_on_sdcard_path and self.steamdeck:
|
||||
self.modlist_sdcard = True
|
||||
self.logger.info("Modlist appears to be on an SD card (Steam Deck).")
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] Set modlist_sdcard=True")
|
||||
self.logger.debug(f"Set modlist_sdcard=True")
|
||||
else:
|
||||
self.modlist_sdcard = False
|
||||
self.logger.debug(f"[SD_CARD_DEBUG] Set modlist_sdcard=False because: is_on_sdcard_path={is_on_sdcard_path} AND steamdeck={self.steamdeck}")
|
||||
self.logger.debug(f"Set modlist_sdcard=False (is_on_sdcard_path={is_on_sdcard_path}, steamdeck={self.steamdeck})")
|
||||
if is_on_sdcard_path and not self.steamdeck:
|
||||
self.logger.info("Modlist on /media mount detected on non-Steam Deck system - using Z: drive mapping.")
|
||||
self.logger.debug("[SD_CARD_DEBUG] This is the ROOT CAUSE - SD card path but steamdeck=False!")
|
||||
|
||||
# Find and set compatdata path now that we have appid
|
||||
# Ensure PathHandler is available (should be initialized in __init__)
|
||||
@@ -722,6 +718,8 @@ class ModlistHandler:
|
||||
success = self.winetricks_handler.install_wine_components(wineprefix, self.game_var_full, specific_components=components)
|
||||
if success:
|
||||
self.logger.info("Wine component installation completed successfully")
|
||||
if status_callback:
|
||||
status_callback(f"{self._get_progress_timestamp()} Wine components verified and installed successfully")
|
||||
else:
|
||||
self.logger.error("Wine component installation failed")
|
||||
print("Error: Failed to install necessary Wine components.")
|
||||
@@ -752,6 +750,19 @@ class ModlistHandler:
|
||||
self.logger.error("=" * 80)
|
||||
# Continue but user should be aware of potential issues
|
||||
|
||||
# Step 4.6: Enable dotfiles visibility for Wine prefix
|
||||
if status_callback:
|
||||
status_callback(f"{self._get_progress_timestamp()} Enabling dotfiles visibility")
|
||||
self.logger.info("Step 4.6: Enabling dotfiles visibility in Wine prefix...")
|
||||
try:
|
||||
if self.protontricks_handler.enable_dotfiles(self.appid):
|
||||
self.logger.info("Dotfiles visibility enabled successfully")
|
||||
else:
|
||||
self.logger.warning("Failed to enable dotfiles visibility (non-critical, continuing)")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error enabling dotfiles visibility: {e} (non-critical, continuing)")
|
||||
self.logger.info("Step 4.6: Enabling dotfiles visibility... Done")
|
||||
|
||||
# Step 5: Ensure permissions of Modlist directory
|
||||
if status_callback:
|
||||
status_callback(f"{self._get_progress_timestamp()} Setting ownership and permissions for modlist directory")
|
||||
|
||||
@@ -390,7 +390,7 @@ class PathHandler:
|
||||
libraryfolders_vdf_paths = [
|
||||
os.path.expanduser("~/.steam/steam/config/libraryfolders.vdf"),
|
||||
os.path.expanduser("~/.local/share/Steam/config/libraryfolders.vdf"),
|
||||
# Add other potential standard locations if necessary
|
||||
os.path.expanduser("~/.var/app/com.valvesoftware.Steam/.local/share/Steam/config/libraryfolders.vdf"), # Flatpak
|
||||
]
|
||||
|
||||
# Simple backup mechanism (optional but good practice)
|
||||
@@ -622,7 +622,9 @@ class PathHandler:
|
||||
m = re.search(r'"path"\s*"([^"]+)"', line)
|
||||
if m:
|
||||
lib_path = Path(m.group(1))
|
||||
library_paths.add(lib_path)
|
||||
# Resolve symlinks for consistency (mmcblk0p1 -> deck/UUID)
|
||||
resolved_path = lib_path.resolve()
|
||||
library_paths.add(resolved_path)
|
||||
except Exception as e:
|
||||
logger.error(f"[DEBUG] Failed to parse {vdf_path}: {e}")
|
||||
logger.info(f"[DEBUG] All detected Steam libraries: {library_paths}")
|
||||
@@ -871,10 +873,9 @@ class PathHandler:
|
||||
else:
|
||||
found_stock = None
|
||||
for folder in STOCK_GAME_FOLDERS:
|
||||
folder_pattern = f"/{folder.replace(' ', '')}".lower()
|
||||
value_part_lower = value_part.replace(' ', '').lower()
|
||||
if folder_pattern in value_part_lower:
|
||||
idx = value_part_lower.index(folder_pattern)
|
||||
folder_pattern = f"/{folder}"
|
||||
if folder_pattern in value_part:
|
||||
idx = value_part.index(folder_pattern)
|
||||
rel_path = value_part[idx:].lstrip('/')
|
||||
found_stock = folder
|
||||
break
|
||||
|
||||
@@ -117,23 +117,21 @@ class ProtontricksHandler:
|
||||
try:
|
||||
# PyInstaller fix: Comprehensive environment cleaning for subprocess calls
|
||||
env = self._get_clean_subprocess_env()
|
||||
|
||||
|
||||
result = subprocess.run(
|
||||
["flatpak", "list"],
|
||||
capture_output=True,
|
||||
["flatpak", "list"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, # Suppress stderr to avoid error messages when flatpak not installed
|
||||
text=True,
|
||||
check=True,
|
||||
env=env # Use comprehensively cleaned environment
|
||||
)
|
||||
if "com.github.Matoking.protontricks" in result.stdout:
|
||||
if result.returncode == 0 and "com.github.Matoking.protontricks" in result.stdout:
|
||||
logger.info("Flatpak Protontricks is installed")
|
||||
self.which_protontricks = 'flatpak'
|
||||
flatpak_installed = True
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
logger.warning("'flatpak' command not found. Cannot check for Flatpak Protontricks.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning(f"Error checking flatpak list: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error checking flatpak: {e}")
|
||||
|
||||
@@ -707,8 +705,15 @@ class ProtontricksHandler:
|
||||
result = self.run_protontricks("--no-bwrap", appid, "-q", *components_to_install, env=env, timeout=600)
|
||||
self.logger.debug(f"Protontricks output: {result.stdout if result else ''}")
|
||||
if result and result.returncode == 0:
|
||||
self.logger.info("Wine Component installation command completed successfully.")
|
||||
return True
|
||||
self.logger.info("Wine Component installation command completed.")
|
||||
|
||||
# Verify components were actually installed
|
||||
if self._verify_components_installed(appid, components_to_install):
|
||||
self.logger.info("Component verification successful - all components installed correctly.")
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"Component verification failed (Attempt {attempt}/{max_attempts})")
|
||||
# Continue to retry
|
||||
else:
|
||||
self.logger.error(f"Protontricks command failed (Attempt {attempt}/{max_attempts}). Return Code: {result.returncode if result else 'N/A'}")
|
||||
self.logger.error(f"Stdout: {result.stdout.strip() if result else ''}")
|
||||
@@ -718,14 +723,73 @@ class ProtontricksHandler:
|
||||
self.logger.error(f"Failed to install Wine components after {max_attempts} attempts.")
|
||||
return False
|
||||
|
||||
def _verify_components_installed(self, appid: str, components: List[str]) -> bool:
|
||||
"""
|
||||
Verify that Wine components were actually installed by querying protontricks.
|
||||
|
||||
Args:
|
||||
appid: Steam AppID
|
||||
components: List of components that should be installed
|
||||
|
||||
Returns:
|
||||
bool: True if all critical components are verified, False otherwise
|
||||
"""
|
||||
try:
|
||||
self.logger.info("Verifying installed components...")
|
||||
|
||||
# Run protontricks list-installed to get actual installed components
|
||||
result = self.run_protontricks("--no-bwrap", appid, "list-installed", timeout=30)
|
||||
|
||||
if not result or result.returncode != 0:
|
||||
self.logger.error("Failed to query installed components")
|
||||
self.logger.debug(f"list-installed stderr: {result.stderr if result else 'N/A'}")
|
||||
return False
|
||||
|
||||
installed_output = result.stdout.lower()
|
||||
self.logger.debug(f"Installed components output: {installed_output}")
|
||||
|
||||
# Define critical components that MUST be installed
|
||||
# These are the core components that determine success
|
||||
critical_components = ["vcrun2022", "xact"]
|
||||
|
||||
# Check for critical components
|
||||
missing_critical = []
|
||||
for component in critical_components:
|
||||
if component.lower() not in installed_output:
|
||||
missing_critical.append(component)
|
||||
|
||||
if missing_critical:
|
||||
self.logger.error(f"CRITICAL: Missing essential components: {missing_critical}")
|
||||
self.logger.error("Installation reported success but components are NOT installed")
|
||||
return False
|
||||
|
||||
# Check for requested components (warn but don't fail)
|
||||
missing_requested = []
|
||||
for component in components:
|
||||
# Handle settings like fontsmooth=rgb (just check the base component name)
|
||||
base_component = component.split('=')[0].lower()
|
||||
if base_component not in installed_output and component.lower() not in installed_output:
|
||||
missing_requested.append(component)
|
||||
|
||||
if missing_requested:
|
||||
self.logger.warning(f"Some requested components may not be installed: {missing_requested}")
|
||||
self.logger.warning("This may cause issues, but critical components are present")
|
||||
|
||||
self.logger.info(f"Verification passed - critical components confirmed: {critical_components}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error verifying components: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
def _cleanup_wine_processes(self):
|
||||
"""
|
||||
Internal method to clean up wine processes during component installation
|
||||
"""
|
||||
try:
|
||||
subprocess.run("pgrep -f 'win7|win10|ShowDotFiles|protontricks' | xargs -r kill -9",
|
||||
subprocess.run("pgrep -f 'win7|win10|ShowDotFiles|protontricks' | xargs -r kill -9",
|
||||
shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
subprocess.run("pkill -9 winetricks",
|
||||
subprocess.run("pkill -9 winetricks",
|
||||
shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up wine processes: {e}")
|
||||
|
||||
@@ -200,40 +200,55 @@ class WineUtils:
|
||||
@staticmethod
|
||||
def _get_sd_card_mounts():
|
||||
"""
|
||||
Dynamically detect all current SD card mount points
|
||||
Returns list of mount point paths
|
||||
Detect SD card mount points using df.
|
||||
Returns list of actual mount paths from /run/media (e.g., /run/media/deck/MicroSD).
|
||||
"""
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(['df', '-h'], capture_output=True, text=True, timeout=5)
|
||||
sd_mounts = []
|
||||
for line in result.stdout.split('\n'):
|
||||
# Look for common SD card mount patterns
|
||||
if '/run/media' in line or ('/mnt' in line and 'sdcard' in line.lower()):
|
||||
parts = line.split()
|
||||
if len(parts) >= 6: # df output has 6+ columns
|
||||
mount_point = parts[-1] # Last column is mount point
|
||||
if mount_point.startswith(('/run/media', '/mnt')):
|
||||
sd_mounts.append(mount_point)
|
||||
return sd_mounts
|
||||
except Exception:
|
||||
# Fallback to common patterns if df fails
|
||||
return ['/run/media/mmcblk0p1', '/run/media/deck']
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
result = subprocess.run(['df', '-h'], capture_output=True, text=True, timeout=5)
|
||||
sd_mounts = []
|
||||
|
||||
for line in result.stdout.split('\n'):
|
||||
if '/run/media' in line:
|
||||
parts = line.split()
|
||||
if len(parts) >= 6:
|
||||
mount_point = parts[-1] # Last column is the mount point
|
||||
if mount_point.startswith('/run/media/'):
|
||||
sd_mounts.append(mount_point)
|
||||
|
||||
# Sort by length (longest first) to match most specific paths first
|
||||
sd_mounts.sort(key=len, reverse=True)
|
||||
logger.debug(f"Detected SD card mounts from df: {sd_mounts}")
|
||||
return sd_mounts
|
||||
|
||||
@staticmethod
|
||||
def _strip_sdcard_path(path):
|
||||
"""
|
||||
Strip any detected SD card mount prefix from paths
|
||||
Handles both /run/media/mmcblk0p1 and /run/media/deck/UUID patterns
|
||||
Strip SD card mount prefix from path.
|
||||
Handles both /run/media/mmcblk0p1 and /run/media/deck/UUID patterns.
|
||||
Pattern: /run/media/deck/UUID/Games/... becomes /Games/...
|
||||
Pattern: /run/media/mmcblk0p1/Games/... becomes /Games/...
|
||||
"""
|
||||
sd_mounts = WineUtils._get_sd_card_mounts()
|
||||
import re
|
||||
|
||||
for mount in sd_mounts:
|
||||
if path.startswith(mount):
|
||||
# Strip the mount prefix and ensure proper leading slash
|
||||
relative_path = path[len(mount):].lstrip('/')
|
||||
return "/" + relative_path if relative_path else "/"
|
||||
# Pattern 1: /run/media/deck/UUID/... strip everything up to and including UUID
|
||||
# This matches the bash: "${path#*/run/media/deck/*/*}"
|
||||
deck_pattern = r'^/run/media/deck/[^/]+(/.*)?$'
|
||||
match = re.match(deck_pattern, path)
|
||||
if match:
|
||||
stripped = match.group(1) if match.group(1) else "/"
|
||||
logger.debug(f"Stripped SD card path (deck pattern): {path} -> {stripped}")
|
||||
return stripped
|
||||
|
||||
# Pattern 2: /run/media/mmcblk0p1/... strip /run/media/mmcblk0p1
|
||||
# This matches the bash: "${path#*mmcblk0p1}"
|
||||
if path.startswith('/run/media/mmcblk0p1/'):
|
||||
stripped = path.replace('/run/media/mmcblk0p1', '', 1)
|
||||
logger.debug(f"Stripped SD card path (mmcblk pattern): {path} -> {stripped}")
|
||||
return stripped
|
||||
|
||||
# No SD card pattern matched
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -349,10 +349,17 @@ class WinetricksHandler:
|
||||
|
||||
self.logger.debug(f"Winetricks output: {result.stdout}")
|
||||
if result.returncode == 0:
|
||||
self.logger.info("Wine Component installation command completed successfully.")
|
||||
# Set Windows 10 mode after component installation (matches legacy script timing)
|
||||
self._set_windows_10_mode(wineprefix, env.get('WINE', ''))
|
||||
return True
|
||||
self.logger.info("Wine Component installation command completed.")
|
||||
|
||||
# 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.")
|
||||
# Set Windows 10 mode after component installation (matches legacy script timing)
|
||||
self._set_windows_10_mode(wineprefix, env.get('WINE', ''))
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"Component verification failed (Attempt {attempt}/{max_attempts})")
|
||||
# Continue to retry
|
||||
else:
|
||||
# Special handling for dotnet40 verification issue (mimics protontricks behavior)
|
||||
if "dotnet40" in components_to_install and "ngen.exe not found" in result.stderr:
|
||||
@@ -430,11 +437,36 @@ class WinetricksHandler:
|
||||
if winetricks_failed:
|
||||
self.logger.error(f"Winetricks failed after {max_attempts} attempts.")
|
||||
|
||||
# Check if protontricks is available for fallback
|
||||
# Network diagnostics before fallback (non-fatal)
|
||||
self.logger.warning("=" * 80)
|
||||
self.logger.warning("NETWORK DIAGNOSTICS: Testing connectivity to component download sources...")
|
||||
try:
|
||||
protontricks_check = subprocess.run(['which', 'protontricks'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if protontricks_check.returncode == 0:
|
||||
# Check if curl is available
|
||||
curl_check = subprocess.run(['which', 'curl'], capture_output=True, timeout=5)
|
||||
if curl_check.returncode == 0:
|
||||
# Test Microsoft download servers (used by winetricks for .NET, VC runtimes, DirectX)
|
||||
test_result = subprocess.run(['curl', '-I', '--max-time', '10', 'https://download.microsoft.com'],
|
||||
capture_output=True, text=True, timeout=15)
|
||||
if test_result.returncode == 0:
|
||||
self.logger.warning("Can reach download.microsoft.com")
|
||||
else:
|
||||
self.logger.error("Cannot reach download.microsoft.com - network/DNS issue likely")
|
||||
self.logger.error(f" Curl exit code: {test_result.returncode}")
|
||||
if test_result.stderr:
|
||||
self.logger.error(f" Curl error: {test_result.stderr.strip()}")
|
||||
else:
|
||||
self.logger.warning("curl not available, skipping network diagnostic test")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Network diagnostic test skipped: {e}")
|
||||
self.logger.warning("=" * 80)
|
||||
|
||||
# Check if protontricks is available for fallback using centralized handler
|
||||
try:
|
||||
from .protontricks_handler import ProtontricksHandler
|
||||
protontricks_handler = ProtontricksHandler()
|
||||
protontricks_available = protontricks_handler.is_available()
|
||||
|
||||
if protontricks_available:
|
||||
self.logger.warning("=" * 80)
|
||||
self.logger.warning("AUTOMATIC FALLBACK: Winetricks failed, attempting protontricks fallback...")
|
||||
self.logger.warning(f"Last winetricks error: {last_error_details}")
|
||||
@@ -721,13 +753,6 @@ class WinetricksHandler:
|
||||
|
||||
if success:
|
||||
self.logger.info(f"Legacy .NET components {legacy_components} installed successfully with protontricks")
|
||||
|
||||
# Enable dotfiles and symlinks for the prefix
|
||||
if protontricks_handler.enable_dotfiles(appid):
|
||||
self.logger.info("Enabled dotfiles and symlinks support")
|
||||
else:
|
||||
self.logger.warning("Failed to enable dotfiles/symlinks (non-critical)")
|
||||
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"Legacy .NET components {legacy_components} installation failed with protontricks")
|
||||
@@ -844,11 +869,18 @@ class WinetricksHandler:
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
self.logger.info(f"Winetricks components installed successfully: {components}")
|
||||
# Set Windows 10 mode after component installation (matches legacy script timing)
|
||||
wine_binary = env.get('WINE', '')
|
||||
self._set_windows_10_mode(env.get('WINEPREFIX', ''), wine_binary)
|
||||
return True
|
||||
self.logger.info(f"Winetricks components installation command completed.")
|
||||
|
||||
# Verify components were actually installed
|
||||
if self._verify_components_installed(wineprefix, components, env):
|
||||
self.logger.info("Component verification successful - all components installed correctly.")
|
||||
# Set Windows 10 mode after component installation (matches legacy script timing)
|
||||
wine_binary = env.get('WINE', '')
|
||||
self._set_windows_10_mode(env.get('WINEPREFIX', ''), wine_binary)
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"Component verification failed (attempt {attempt})")
|
||||
# Continue to retry
|
||||
else:
|
||||
self.logger.error(f"Winetricks failed (attempt {attempt}): {result.stderr.strip()}")
|
||||
|
||||
@@ -992,6 +1024,70 @@ class WinetricksHandler:
|
||||
self.logger.error(f"Error getting wine binary for prefix: {e}")
|
||||
return ""
|
||||
|
||||
def _verify_components_installed(self, wineprefix: str, components: List[str], env: dict) -> bool:
|
||||
"""
|
||||
Verify that Wine components were actually installed by checking winetricks.log.
|
||||
|
||||
Args:
|
||||
wineprefix: Wine prefix path
|
||||
components: List of components that should be installed
|
||||
env: Environment variables (includes WINE path)
|
||||
|
||||
Returns:
|
||||
bool: True if all critical components are verified, False otherwise
|
||||
"""
|
||||
try:
|
||||
self.logger.info("Verifying installed components...")
|
||||
|
||||
# Check winetricks.log file for installed components
|
||||
winetricks_log = os.path.join(wineprefix, 'winetricks.log')
|
||||
|
||||
if not os.path.exists(winetricks_log):
|
||||
self.logger.error(f"winetricks.log not found at {winetricks_log}")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(winetricks_log, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
log_content = f.read().lower()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to read winetricks.log: {e}")
|
||||
return False
|
||||
|
||||
self.logger.debug(f"winetricks.log length: {len(log_content)} bytes")
|
||||
|
||||
# Define critical components that MUST be installed
|
||||
critical_components = ["vcrun2022", "xact"]
|
||||
|
||||
# Check for critical components
|
||||
missing_critical = []
|
||||
for component in critical_components:
|
||||
if component.lower() not in log_content:
|
||||
missing_critical.append(component)
|
||||
|
||||
if missing_critical:
|
||||
self.logger.error(f"CRITICAL: Missing essential components: {missing_critical}")
|
||||
self.logger.error("Installation reported success but components are NOT in winetricks.log")
|
||||
return False
|
||||
|
||||
# Check for requested components (warn but don't fail)
|
||||
missing_requested = []
|
||||
for component in components:
|
||||
# Handle settings like fontsmooth=rgb (just check the base component name)
|
||||
base_component = component.split('=')[0].lower()
|
||||
if base_component not in log_content and component.lower() not in log_content:
|
||||
missing_requested.append(component)
|
||||
|
||||
if missing_requested:
|
||||
self.logger.warning(f"Some requested components may not be installed: {missing_requested}")
|
||||
self.logger.warning("This may cause issues, but critical components are present")
|
||||
|
||||
self.logger.info(f"Verification passed - critical components confirmed: {critical_components}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error verifying components: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
def _cleanup_wine_processes(self):
|
||||
"""
|
||||
Internal method to clean up wine processes during component installation
|
||||
|
||||
@@ -2904,10 +2904,21 @@ echo Prefix creation complete.
|
||||
"""Find a Steam game installation path by AppID and common names"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Get Steam libraries from libraryfolders.vdf
|
||||
steam_config_path = Path.home() / ".steam/steam/config/libraryfolders.vdf"
|
||||
if not steam_config_path.exists():
|
||||
|
||||
# Get Steam libraries from libraryfolders.vdf - check multiple possible locations
|
||||
possible_config_paths = [
|
||||
Path.home() / ".steam/steam/config/libraryfolders.vdf",
|
||||
Path.home() / ".local/share/Steam/config/libraryfolders.vdf",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/config/libraryfolders.vdf" # Flatpak
|
||||
]
|
||||
|
||||
steam_config_path = None
|
||||
for path in possible_config_paths:
|
||||
if path.exists():
|
||||
steam_config_path = path
|
||||
break
|
||||
|
||||
if not steam_config_path:
|
||||
return None
|
||||
|
||||
steam_libraries = []
|
||||
|
||||
@@ -135,6 +135,9 @@ class NativeSteamOperationsService:
|
||||
steam_locations = [
|
||||
Path.home() / ".steam/steam",
|
||||
Path.home() / ".local/share/Steam",
|
||||
# Flatpak Steam - direct data directory
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam",
|
||||
# Flatpak Steam - symlinked home paths
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/home/.steam/steam",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/home/.local/share/Steam"
|
||||
]
|
||||
@@ -161,6 +164,9 @@ class NativeSteamOperationsService:
|
||||
standard_locations = [
|
||||
Path.home() / ".steam/steam/steamapps/compatdata",
|
||||
Path.home() / ".local/share/Steam/steamapps/compatdata",
|
||||
# Flatpak Steam - direct data directory
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/compatdata",
|
||||
# Flatpak Steam - symlinked home paths
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/home/.steam/steam/steamapps/compatdata",
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/home/.local/share/Steam/steamapps/compatdata"
|
||||
]
|
||||
|
||||
@@ -127,20 +127,18 @@ class ProtontricksDetectionService:
|
||||
try:
|
||||
env = handler._get_clean_subprocess_env()
|
||||
result = subprocess.run(
|
||||
["flatpak", "list"],
|
||||
capture_output=True,
|
||||
["flatpak", "list"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, # Suppress stderr to avoid error messages
|
||||
text=True,
|
||||
check=True,
|
||||
env=env
|
||||
)
|
||||
if "com.github.Matoking.protontricks" in result.stdout:
|
||||
if result.returncode == 0 and "com.github.Matoking.protontricks" in result.stdout:
|
||||
logger.info("Flatpak Protontricks is installed")
|
||||
handler.which_protontricks = 'flatpak'
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
logger.warning("'flatpak' command not found. Cannot check for Flatpak Protontricks.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning(f"Error checking flatpak list: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error checking flatpak: {e}")
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import signal
|
||||
import psutil
|
||||
import logging
|
||||
import sys
|
||||
import shutil
|
||||
from typing import Callable, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -86,6 +87,25 @@ def is_steam_deck() -> bool:
|
||||
logger.debug(f"Error detecting Steam Deck: {e}")
|
||||
return False
|
||||
|
||||
def is_flatpak_steam() -> bool:
|
||||
"""Detect if Steam is installed as a Flatpak."""
|
||||
try:
|
||||
# First check if flatpak command exists
|
||||
if not shutil.which('flatpak'):
|
||||
return False
|
||||
|
||||
# Verify the app is actually installed (not just directory exists)
|
||||
result = subprocess.run(['flatpak', 'list', '--app'],
|
||||
stdout=subprocess.PIPE,
|
||||
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
|
||||
except Exception as e:
|
||||
logger.debug(f"Error detecting Flatpak Steam: {e}")
|
||||
return False
|
||||
|
||||
def get_steam_processes() -> list:
|
||||
"""Return a list of psutil.Process objects for running Steam processes."""
|
||||
steam_procs = []
|
||||
@@ -122,16 +142,37 @@ def start_steam() -> bool:
|
||||
"""Attempt to start Steam using the exact methods from existing working logic."""
|
||||
env = _get_clean_subprocess_env()
|
||||
try:
|
||||
# Try systemd user service (Steam Deck)
|
||||
# Try systemd user service (Steam Deck) - HIGHEST PRIORITY
|
||||
if is_steam_deck():
|
||||
subprocess.Popen(["systemctl", "--user", "restart", "app-steam@autostart.service"], env=env)
|
||||
return True
|
||||
|
||||
|
||||
# Check if Flatpak Steam (only if not Steam Deck)
|
||||
if is_flatpak_steam():
|
||||
logger.info("Flatpak Steam detected - using flatpak run command")
|
||||
try:
|
||||
# Redirect flatpak's stderr to suppress "app not installed" errors on systems without flatpak Steam
|
||||
# Steam's own stdout/stderr will still go through (flatpak forwards them)
|
||||
subprocess.Popen(["flatpak", "run", "com.valvesoftware.Steam", "-silent"],
|
||||
env=env, stderr=subprocess.DEVNULL)
|
||||
time.sleep(5)
|
||||
check_result = subprocess.run(['pgrep', '-f', 'steam'], capture_output=True, timeout=10, env=env)
|
||||
if check_result.returncode == 0:
|
||||
logger.info("Flatpak Steam process detected after start.")
|
||||
return True
|
||||
else:
|
||||
logger.warning("Flatpak Steam process not detected after start attempt.")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting Flatpak Steam: {e}")
|
||||
return False
|
||||
|
||||
# Use startup methods with only -silent flag (no -minimized or -no-browser)
|
||||
# Don't redirect stdout/stderr or use start_new_session to allow Steam to connect to display/tray
|
||||
start_methods = [
|
||||
{"name": "Popen", "cmd": ["steam", "-silent"], "kwargs": {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL, "stdin": subprocess.DEVNULL, "start_new_session": True, "env": env}},
|
||||
{"name": "setsid", "cmd": ["setsid", "steam", "-silent"], "kwargs": {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL, "stdin": subprocess.DEVNULL, "env": env}},
|
||||
{"name": "nohup", "cmd": ["nohup", "steam", "-silent"], "kwargs": {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL, "stdin": subprocess.DEVNULL, "start_new_session": True, "preexec_fn": os.setpgrp, "env": env}}
|
||||
{"name": "Popen", "cmd": ["steam", "-silent"], "kwargs": {"env": env}},
|
||||
{"name": "setsid", "cmd": ["setsid", "steam", "-silent"], "kwargs": {"env": env}},
|
||||
{"name": "nohup", "cmd": ["nohup", "steam", "-silent"], "kwargs": {"preexec_fn": os.setpgrp, "env": env}}
|
||||
]
|
||||
|
||||
for method in start_methods:
|
||||
@@ -174,17 +215,26 @@ def robust_steam_restart(progress_callback: Optional[Callable[[str], None]] = No
|
||||
progress_callback(msg)
|
||||
|
||||
report("Shutting down Steam...")
|
||||
|
||||
# Steam Deck: Use systemctl for shutdown (special handling)
|
||||
|
||||
# Steam Deck: Use systemctl for shutdown (special handling) - HIGHEST PRIORITY
|
||||
if is_steam_deck():
|
||||
try:
|
||||
report("Steam Deck detected - using systemctl shutdown...")
|
||||
subprocess.run(['systemctl', '--user', 'stop', 'app-steam@autostart.service'],
|
||||
subprocess.run(['systemctl', '--user', 'stop', 'app-steam@autostart.service'],
|
||||
timeout=15, check=False, capture_output=True, env=env)
|
||||
time.sleep(2)
|
||||
except Exception as e:
|
||||
logger.debug(f"systemctl stop failed on Steam Deck: {e}")
|
||||
|
||||
# Flatpak Steam: Use flatpak kill command (only if not Steam Deck)
|
||||
elif is_flatpak_steam():
|
||||
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=env)
|
||||
time.sleep(2)
|
||||
except Exception as e:
|
||||
logger.debug(f"flatpak kill failed: {e}")
|
||||
|
||||
# All systems: Use pkill approach (proven 15/16 test success rate)
|
||||
try:
|
||||
# Skip unreliable steam -shutdown, go straight to pkill
|
||||
|
||||
Reference in New Issue
Block a user