mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 01:47:45 +02:00
464 lines
27 KiB
Python
464 lines
27 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Winetricks Handler Module
|
|
Handles wine component installation using bundled winetricks.
|
|
Discovery, installation strategy, and verification live in mixins.
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Optional, List, Callable
|
|
|
|
from .winetricks_discovery import WinetricksDiscoveryMixin
|
|
from .winetricks_env import WinetricksEnvMixin
|
|
from .winetricks_installation import WinetricksInstallationMixin
|
|
from .winetricks_verification import WinetricksVerificationMixin
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class WinetricksHandler(
|
|
WinetricksDiscoveryMixin,
|
|
WinetricksEnvMixin,
|
|
WinetricksInstallationMixin,
|
|
WinetricksVerificationMixin,
|
|
):
|
|
"""Handles wine component installation. Discovery, installation, verification in mixins."""
|
|
|
|
def __init__(self, logger=None):
|
|
self.logger = logger or logging.getLogger(__name__)
|
|
self.winetricks_path = self._get_bundled_winetricks_path()
|
|
|
|
def install_wine_components(self, wineprefix: str, game_var: str, specific_components: Optional[List[str]] = None, status_callback: Optional[Callable[[str], None]] = None, appid: Optional[str] = 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
|
|
appid: Optional Steam App ID (for fallback or logging)
|
|
"""
|
|
if not self.is_available():
|
|
self.logger.error("Winetricks is not available")
|
|
return False
|
|
|
|
env, components_to_install = self._build_winetricks_env(wineprefix, status_callback, specific_components)
|
|
if env is None:
|
|
return False
|
|
if not components_to_install:
|
|
return True
|
|
|
|
# Flatpak Steam: use protontricks only; bundled winetricks is unreliable (e.g. from AppImage)
|
|
flatpak_steam = False
|
|
try:
|
|
from ..services.steam_restart_service import is_flatpak_steam
|
|
flatpak_steam = is_flatpak_steam()
|
|
except Exception as e:
|
|
self.logger.debug("Could not check Flatpak Steam via CLI: %s", e)
|
|
if not flatpak_steam and self._is_flatpak_steam_prefix(wineprefix):
|
|
flatpak_steam = True
|
|
self.logger.info("Flatpak Steam prefix detected (path): using protontricks only")
|
|
if flatpak_steam:
|
|
self.logger.info("Flatpak Steam detected: using protontricks only for component installation")
|
|
return self._install_components_protontricks_only(
|
|
components_to_install, wineprefix, game_var, status_callback, appid=appid
|
|
)
|
|
|
|
# Check user preference for component installation method
|
|
from ..handlers.config_handler import ConfigHandler
|
|
config_handler = ConfigHandler()
|
|
|
|
# Get component installation method with migration
|
|
method = config_handler.get('component_installation_method', 'winetricks')
|
|
|
|
# Migrate bundled_protontricks to system_protontricks (no longer supported)
|
|
if method == 'bundled_protontricks':
|
|
self.logger.warning("Bundled protontricks no longer supported, migrating to system_protontricks")
|
|
method = 'system_protontricks'
|
|
config_handler.set('component_installation_method', 'system_protontricks')
|
|
|
|
# Choose installation method based on user preference
|
|
if method == 'system_protontricks':
|
|
self.logger.info("=" * 80)
|
|
self.logger.info("USING PROTONTRICKS")
|
|
self.logger.info("=" * 80)
|
|
self.logger.info("Using system protontricks for all components")
|
|
return self._install_components_protontricks_only(components_to_install, wineprefix, game_var, status_callback)
|
|
|
|
# Install all components together with winetricks (faster)
|
|
self.logger.info("=" * 80)
|
|
self.logger.info("USING WINETRICKS")
|
|
self.logger.info("=" * 80)
|
|
max_attempts = 3
|
|
winetricks_failed = False
|
|
last_error_details = None
|
|
|
|
for attempt in range(1, max_attempts + 1):
|
|
if attempt > 1:
|
|
self.logger.warning(f"Retrying component installation (attempt {attempt}/{max_attempts})...")
|
|
self._cleanup_wine_processes()
|
|
elif attempt == 1:
|
|
self._kill_wineserver_for_prefix(env)
|
|
|
|
try:
|
|
cmd = [self.winetricks_path, '--unattended'] + components_to_install
|
|
run_env = env
|
|
|
|
# Log full command for advanced users to reproduce manually (debug mode only)
|
|
cmd_str = ' '.join(cmd)
|
|
self.logger.debug("=" * 80)
|
|
self.logger.debug("WINETRICKS COMMAND (for manual reproduction):")
|
|
self.logger.debug(f" {cmd_str}")
|
|
self.logger.debug("")
|
|
self.logger.debug("Environment variables required:")
|
|
self.logger.debug(f" WINEPREFIX={env.get('WINEPREFIX', 'NOT SET')}")
|
|
self.logger.debug(f" WINE={env.get('WINE', 'NOT SET')}")
|
|
self.logger.debug(f" WINESERVER={env.get('WINESERVER', 'NOT SET')}")
|
|
self.logger.debug("=" * 80)
|
|
|
|
# Enhanced diagnostics for bundled winetricks
|
|
self.logger.debug("=== Winetricks Environment Diagnostics ===")
|
|
self.logger.debug(f"Bundled winetricks path: {self.winetricks_path}")
|
|
self.logger.debug(f"Winetricks exists: {os.path.exists(self.winetricks_path)}")
|
|
self.logger.debug(f"Winetricks executable: {os.access(self.winetricks_path, os.X_OK)}")
|
|
if os.path.exists(self.winetricks_path):
|
|
try:
|
|
winetricks_stat = os.stat(self.winetricks_path)
|
|
self.logger.debug(f"Winetricks permissions: {oct(winetricks_stat.st_mode)}")
|
|
self.logger.debug(f"Winetricks size: {winetricks_stat.st_size} bytes")
|
|
except Exception as stat_err:
|
|
self.logger.debug(f"Could not stat winetricks: {stat_err}")
|
|
|
|
self.logger.debug(f"WINE binary: {env.get('WINE', 'NOT SET')}")
|
|
wine_binary = env.get('WINE', '')
|
|
if wine_binary and os.path.exists(wine_binary):
|
|
self.logger.debug(f"WINE binary exists: True")
|
|
else:
|
|
self.logger.debug(f"WINE binary exists: False")
|
|
|
|
self.logger.debug(f"WINEPREFIX: {env.get('WINEPREFIX', 'NOT SET')}")
|
|
wineprefix = env.get('WINEPREFIX', '')
|
|
if wineprefix and os.path.exists(wineprefix):
|
|
self.logger.debug(f"WINEPREFIX exists: True")
|
|
self.logger.debug(f"WINEPREFIX/pfx exists: {os.path.exists(os.path.join(wineprefix, 'pfx'))}")
|
|
else:
|
|
self.logger.debug(f"WINEPREFIX exists: False")
|
|
|
|
self.logger.debug(f"DISPLAY: {env.get('DISPLAY', 'NOT SET')}")
|
|
self.logger.debug(f"WINETRICKS_CACHE: {env.get('WINETRICKS_CACHE', 'NOT SET')}")
|
|
self.logger.debug(f"Components to install: {components_to_install}")
|
|
self.logger.debug("==========================================")
|
|
|
|
result = subprocess.run(
|
|
cmd,
|
|
env=run_env,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
timeout=600
|
|
)
|
|
|
|
self.logger.debug(f"Winetricks output: {result.stdout}")
|
|
if result.returncode == 0:
|
|
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.")
|
|
components_list = ', '.join(components_to_install)
|
|
if status_callback:
|
|
status_callback(f"Wine components installed and verified: {components_list}")
|
|
self._set_windows_10_mode_after_install(wineprefix, env)
|
|
return True
|
|
else:
|
|
self.logger.error(f"Component verification failed (Attempt {attempt}/{max_attempts})")
|
|
winetricks_failed = True
|
|
elif result.returncode == 1 and "returned status 120" in result.stderr and "aborting" in result.stderr.lower():
|
|
# VC redist / some installers return 120 under Wine (ERROR_CALL_NOT_IMPLEMENTED); install may still have succeeded
|
|
self.logger.info("Winetricks returned 1 with status 120 (installer quirk under Wine); verifying components...")
|
|
if self._verify_components_installed(wineprefix, components_to_install, env):
|
|
self.logger.info("Component verification passed after status 120 - accepting as success.")
|
|
if status_callback:
|
|
status_callback(f"Wine components installed and verified: {', '.join(components_to_install)}")
|
|
self._set_windows_10_mode_after_install(wineprefix, env)
|
|
return True
|
|
last_error_details = {'returncode': result.returncode, 'stdout': result.stdout.strip(), 'stderr': result.stderr.strip(), 'attempt': attempt}
|
|
winetricks_failed = True
|
|
else:
|
|
# Store detailed error information for fallback diagnostics
|
|
last_error_details = {
|
|
'returncode': result.returncode,
|
|
'stdout': result.stdout.strip(),
|
|
'stderr': result.stderr.strip(),
|
|
'attempt': attempt
|
|
}
|
|
|
|
# Log full error details to help diagnose failures
|
|
self.logger.error("=" * 80)
|
|
self.logger.error(f"WINETRICKS FAILED (Attempt {attempt}/{max_attempts})")
|
|
self.logger.error(f"Return Code: {result.returncode}")
|
|
self.logger.error("")
|
|
self.logger.error("STDOUT:")
|
|
if result.stdout.strip():
|
|
for line in result.stdout.strip().split('\n'):
|
|
self.logger.error(f" {line}")
|
|
else:
|
|
self.logger.error(" (empty)")
|
|
self.logger.error("")
|
|
self.logger.error("STDERR:")
|
|
if result.stderr.strip():
|
|
# Filter out verbose winetricks "Executing..." messages - these are informational, not errors
|
|
error_lines = []
|
|
verbose_lines = []
|
|
for line in result.stderr.strip().split('\n'):
|
|
line_lower = line.lower().strip()
|
|
# Skip verbose informational messages
|
|
if (line_lower.startswith('executing ') or
|
|
(line_lower.startswith('grep: warning:') and 'stray' in line_lower) or
|
|
('warning; possible' in line_lower and 'extra bytes' in line_lower)):
|
|
# These are verbose info messages, log at debug level instead
|
|
verbose_lines.append(line)
|
|
else:
|
|
# Actual error/warning messages (including "returned status", "aborting", dbus errors, etc.)
|
|
error_lines.append(line)
|
|
|
|
if error_lines:
|
|
self.logger.error(" Actual errors/warnings:")
|
|
for line in error_lines:
|
|
self.logger.error(f" {line}")
|
|
if verbose_lines:
|
|
self.logger.debug(f" ({len(verbose_lines)} verbose 'Executing...' lines suppressed - see debug log for details)")
|
|
else:
|
|
self.logger.error(" (only verbose output, no actual errors)")
|
|
if verbose_lines:
|
|
self.logger.debug(f" ({len(verbose_lines)} verbose lines suppressed)")
|
|
else:
|
|
self.logger.error(" (empty)")
|
|
self.logger.error("=" * 80)
|
|
|
|
# Enhanced error diagnostics with actionable information
|
|
stderr_lower = result.stderr.lower()
|
|
stdout_lower = result.stdout.lower()
|
|
|
|
# Log which diagnostic category matches
|
|
diagnostic_found = False
|
|
|
|
if "command not found" in stderr_lower or "no such file" in stderr_lower:
|
|
self.logger.error("DIAGNOSTIC: Winetricks or dependency binary not found")
|
|
self.logger.error(" - Bundled winetricks may be missing dependencies")
|
|
self.logger.error(" - Check dependency check output above for missing tools")
|
|
self.logger.error(" - Will attempt protontricks fallback if all attempts fail")
|
|
diagnostic_found = True
|
|
elif ("returned status" in stderr_lower and "aborting" in stderr_lower) or "connection reset by peer" in stderr_lower:
|
|
self.logger.error("DIAGNOSTIC: Wine/Proton command failed (regedit, VC redist, etc.)")
|
|
self.logger.error(" - Wine subprocess returned non-zero or wineserver connection reset")
|
|
self.logger.error(" - Common when running winetricks from AppImage against Proton prefix; protontricks fallback uses same prefix from inside Steam env")
|
|
diagnostic_found = True
|
|
elif "w_metadata" in stderr_lower and ("unix path" in stderr_lower or "windows path" in stderr_lower):
|
|
self.logger.error("DIAGNOSTIC: Winetricks metadata bug (e.g. jet40 installed_file path)")
|
|
self.logger.error(" - Known winetricks bug: component metadata uses Unix path instead of Windows path")
|
|
self.logger.error(" - Upstream fix in newer winetricks; protontricks fallback will be used")
|
|
diagnostic_found = True
|
|
elif "permission denied" in stderr_lower:
|
|
self.logger.error("DIAGNOSTIC: Permission issue detected")
|
|
self.logger.error(f" - Check permissions on: {self.winetricks_path}")
|
|
self.logger.error(f" - Check permissions on WINEPREFIX: {env.get('WINEPREFIX', 'N/A')}")
|
|
diagnostic_found = True
|
|
elif "timeout" in stderr_lower:
|
|
self.logger.error("DIAGNOSTIC: Timeout issue detected during component download/install")
|
|
self.logger.error(" - Network may be slow or unstable")
|
|
self.logger.error(" - Component download may be taking too long")
|
|
diagnostic_found = True
|
|
elif "sha256sum mismatch" in stderr_lower or ("checksum" in stderr_lower and ("fail" in stderr_lower or "mismatch" in stderr_lower)):
|
|
self.logger.error("DIAGNOSTIC: Checksum verification failed")
|
|
self.logger.error(" - Component download may be corrupted")
|
|
self.logger.error(" - Network issue or upstream file change")
|
|
diagnostic_found = True
|
|
elif ("please install" in stderr_lower or "please install" in stdout_lower) and ("wget" in stderr_lower or "aria2c" in stderr_lower or "curl" in stderr_lower or "wget" in stdout_lower or "aria2c" in stdout_lower or "curl" in stdout_lower):
|
|
# Winetricks explicitly says to install a downloader
|
|
self._handle_missing_downloader_error()
|
|
diagnostic_found = True
|
|
elif "curl" in stderr_lower or "wget" in stderr_lower or "aria2c" in stderr_lower:
|
|
self.logger.error("DIAGNOSTIC: Download tool (curl/wget/aria2c) issue")
|
|
self.logger.error(" - Network connectivity problem or missing download tool")
|
|
self.logger.error(" - Check dependency check output above")
|
|
diagnostic_found = True
|
|
elif "cabextract" in stderr_lower and ("not found" in stderr_lower or "failed" in stderr_lower or "command not found" in stderr_lower or "no such file" in stderr_lower):
|
|
self.logger.error("DIAGNOSTIC: cabextract missing or failed")
|
|
self.logger.error(" - Required for extracting Windows cabinet files")
|
|
self.logger.error(" - Bundled cabextract should be available, check PATH")
|
|
diagnostic_found = True
|
|
elif ("unzip" in stderr_lower or "7z" in stderr_lower) and ("not found" in stderr_lower or "failed" in stderr_lower or "error" in stderr_lower):
|
|
self.logger.error("DIAGNOSTIC: Archive extraction tool (unzip/7z) missing or failed")
|
|
self.logger.error(" - Required for extracting zip/7z archives")
|
|
self.logger.error(" - Check dependency check output above")
|
|
diagnostic_found = True
|
|
|
|
if not diagnostic_found:
|
|
self.logger.error("DIAGNOSTIC: Unknown winetricks failure pattern")
|
|
self.logger.error(" - Error details logged above (STDOUT/STDERR)")
|
|
self.logger.error(" - Check dependency check output above for missing tools")
|
|
self.logger.error(" - Will attempt protontricks fallback if all attempts fail")
|
|
|
|
winetricks_failed = True
|
|
|
|
except subprocess.TimeoutExpired as e:
|
|
self.logger.error(f"Winetricks timed out (Attempt {attempt}/{max_attempts}): {e}")
|
|
last_error_details = {'error': 'timeout', 'attempt': attempt}
|
|
winetricks_failed = True
|
|
except Exception as e:
|
|
self.logger.error(f"Error during winetricks run (Attempt {attempt}/{max_attempts}): {e}", exc_info=True)
|
|
last_error_details = {'error': str(e), 'attempt': attempt}
|
|
winetricks_failed = True
|
|
|
|
# All winetricks attempts failed - try automatic fallback to protontricks
|
|
if winetricks_failed:
|
|
self.logger.error("=" * 80)
|
|
self.logger.error(f"WINETRICKS FAILED AFTER {max_attempts} ATTEMPTS")
|
|
self.logger.error("")
|
|
if last_error_details:
|
|
self.logger.error("Last error details:")
|
|
if 'returncode' in last_error_details:
|
|
self.logger.error(f" Return code: {last_error_details['returncode']}")
|
|
if 'stderr' in last_error_details and last_error_details['stderr']:
|
|
self.logger.error(f" Last stderr (first 500 chars): {last_error_details['stderr'][:500]}")
|
|
if 'stdout' in last_error_details and last_error_details['stdout']:
|
|
self.logger.error(f" Last stdout (first 500 chars): {last_error_details['stdout'][:500]}")
|
|
self.logger.error("")
|
|
self.logger.error("Attempting automatic fallback to protontricks...")
|
|
self.logger.error("=" * 80)
|
|
|
|
# Network diagnostics before fallback (non-fatal)
|
|
self.logger.warning("=" * 80)
|
|
self.logger.warning("NETWORK DIAGNOSTICS: Testing connectivity to component download sources...")
|
|
try:
|
|
# 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
|
|
steamdeck = os.path.exists('/home/deck')
|
|
protontricks_handler = ProtontricksHandler(steamdeck)
|
|
protontricks_available = protontricks_handler.detect_protontricks()
|
|
|
|
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}")
|
|
self.logger.warning("=" * 80)
|
|
self.logger.info("=" * 80)
|
|
self.logger.info("USING PROTONTRICKS")
|
|
self.logger.info("=" * 80)
|
|
|
|
# Attempt fallback to protontricks
|
|
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")
|
|
return True
|
|
else:
|
|
self.logger.error("FAILURE: Both winetricks and protontricks fallback failed")
|
|
return False
|
|
else:
|
|
self.logger.error("Protontricks not available for fallback")
|
|
self.logger.error(f"Final winetricks error details: {last_error_details}")
|
|
return False
|
|
except Exception as e:
|
|
self.logger.error(f"Could not check for protontricks fallback: {e}")
|
|
return False
|
|
|
|
return False
|
|
|
|
def _handle_missing_downloader_error(self):
|
|
"""Handle winetricks error indicating missing downloader - provide platform-specific instructions"""
|
|
from ..services.platform_detection_service import PlatformDetectionService
|
|
|
|
platform = PlatformDetectionService.get_instance()
|
|
is_steamos = platform.is_steamdeck
|
|
|
|
self.logger.error("=" * 80)
|
|
self.logger.error("CRITICAL: Winetricks cannot find a downloader (curl, wget, or aria2c)")
|
|
self.logger.error("")
|
|
|
|
if is_steamos:
|
|
self.logger.error("STEAMOS/STEAM DECK DETECTED")
|
|
self.logger.error("")
|
|
self.logger.error("SteamOS has a read-only filesystem. To install packages:")
|
|
self.logger.error("")
|
|
self.logger.error("1. Disable read-only mode (required for package installation):")
|
|
self.logger.error(" sudo steamos-readonly disable")
|
|
self.logger.error("")
|
|
self.logger.error("2. Install curl (recommended - most reliable):")
|
|
self.logger.error(" sudo pacman -S curl")
|
|
self.logger.error("")
|
|
self.logger.error("3. (Optional) Re-enable read-only mode after installation:")
|
|
self.logger.error(" sudo steamos-readonly enable")
|
|
self.logger.error("")
|
|
self.logger.error("Note: curl is usually pre-installed on SteamOS. If missing,")
|
|
self.logger.error(" the above steps will install it.")
|
|
else:
|
|
self.logger.error("SOLUTION: Install one of the following downloaders:")
|
|
self.logger.error("")
|
|
self.logger.error(" For Debian/Ubuntu/PopOS:")
|
|
self.logger.error(" sudo apt install curl # or: sudo apt install wget")
|
|
self.logger.error("")
|
|
self.logger.error(" For Fedora/RHEL/CentOS:")
|
|
self.logger.error(" sudo dnf install curl # or: sudo dnf install wget")
|
|
self.logger.error("")
|
|
self.logger.error(" For Arch/Manjaro:")
|
|
self.logger.error(" sudo pacman -S curl # or: sudo pacman -S wget")
|
|
self.logger.error("")
|
|
self.logger.error(" For openSUSE:")
|
|
self.logger.error(" sudo zypper install curl # or: sudo zypper install wget")
|
|
self.logger.error("")
|
|
self.logger.error("Note: Most Linux distributions include curl by default.")
|
|
self.logger.error(" If curl is missing, install it using your package manager.")
|
|
|
|
self.logger.error("=" * 80)
|
|
|
|
def _kill_wineserver_for_prefix(self, env: dict) -> None:
|
|
"""Kill wineserver for the current WINEPREFIX so the next wine invocation starts a fresh one (avoids connection reset by peer)."""
|
|
wineserver = env.get('WINESERVER')
|
|
if not wineserver or not os.path.exists(wineserver):
|
|
return
|
|
try:
|
|
subprocess.run(
|
|
[wineserver, '-k'],
|
|
env=env,
|
|
timeout=10,
|
|
capture_output=True,
|
|
)
|
|
self.logger.debug("Killed wineserver for prefix so winetricks can start a fresh one")
|
|
except Exception as e:
|
|
self.logger.debug("Wineserver -k failed (non-fatal): %s", e)
|
|
|
|
def _cleanup_wine_processes(self):
|
|
"""Clean up winetricks processes only during component installation."""
|
|
try:
|
|
subprocess.run("pkill -f winetricks", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
self.logger.debug("Cleaned up winetricks processes only")
|
|
except Exception as e:
|
|
self.logger.error(f"Error cleaning up winetricks processes: {e}") |