mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 02:37:44 +02:00
Initial public release v0.1.0 - Linux Wabbajack Modlist Application
Jackify provides native Linux support for Wabbajack modlist installation and management with automated Steam integration and Proton configuration. Key Features: - Almost Native Linux implementation (texconv.exe run via proton) - Automated Steam shortcut creation and Proton prefix management - Both CLI and GUI interfaces, with Steam Deck optimization Supported Games: - Skyrim Special Edition - Fallout 4 - Fallout New Vegas - Oblivion, Starfield, Enderal, and diverse other games Technical Architecture: - Clean separation between frontend and backend services - Powered by jackify-engine 0.3.x for Wabbajack-matching modlist installation
This commit is contained in:
274
jackify/backend/services/steam_restart_service.py
Normal file
274
jackify/backend/services/steam_restart_service.py
Normal file
@@ -0,0 +1,274 @@
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import signal
|
||||
import psutil
|
||||
import logging
|
||||
import sys
|
||||
from typing import Callable, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def _get_clean_subprocess_env():
|
||||
"""
|
||||
Create a clean environment for subprocess calls by removing PyInstaller-specific
|
||||
environment variables that can interfere with Steam execution.
|
||||
|
||||
Returns:
|
||||
dict: Cleaned environment dictionary
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
pyinstaller_vars_removed = []
|
||||
|
||||
# Remove PyInstaller-specific environment variables
|
||||
if env.pop('_MEIPASS', None):
|
||||
pyinstaller_vars_removed.append('_MEIPASS')
|
||||
if env.pop('_MEIPASS2', None):
|
||||
pyinstaller_vars_removed.append('_MEIPASS2')
|
||||
|
||||
# Clean library path variables that PyInstaller modifies (Linux/Unix)
|
||||
if 'LD_LIBRARY_PATH_ORIG' in env:
|
||||
# Restore original LD_LIBRARY_PATH if it was backed up by PyInstaller
|
||||
env['LD_LIBRARY_PATH'] = env['LD_LIBRARY_PATH_ORIG']
|
||||
pyinstaller_vars_removed.append('LD_LIBRARY_PATH (restored from _ORIG)')
|
||||
else:
|
||||
# Remove PyInstaller-modified LD_LIBRARY_PATH
|
||||
if env.pop('LD_LIBRARY_PATH', None):
|
||||
pyinstaller_vars_removed.append('LD_LIBRARY_PATH (removed)')
|
||||
|
||||
# Clean PATH of PyInstaller-specific entries
|
||||
if 'PATH' in env and hasattr(sys, '_MEIPASS'):
|
||||
path_entries = env['PATH'].split(os.pathsep)
|
||||
original_count = len(path_entries)
|
||||
# Remove any PATH entries that point to PyInstaller temp directory
|
||||
cleaned_path = [p for p in path_entries if not p.startswith(sys._MEIPASS)]
|
||||
env['PATH'] = os.pathsep.join(cleaned_path)
|
||||
if len(cleaned_path) < original_count:
|
||||
pyinstaller_vars_removed.append(f'PATH (removed {original_count - len(cleaned_path)} PyInstaller entries)')
|
||||
|
||||
# Clean macOS library path (if present)
|
||||
if 'DYLD_LIBRARY_PATH' in env and hasattr(sys, '_MEIPASS'):
|
||||
dyld_entries = env['DYLD_LIBRARY_PATH'].split(os.pathsep)
|
||||
cleaned_dyld = [p for p in dyld_entries if not p.startswith(sys._MEIPASS)]
|
||||
if cleaned_dyld:
|
||||
env['DYLD_LIBRARY_PATH'] = os.pathsep.join(cleaned_dyld)
|
||||
pyinstaller_vars_removed.append('DYLD_LIBRARY_PATH (cleaned)')
|
||||
else:
|
||||
env.pop('DYLD_LIBRARY_PATH', None)
|
||||
pyinstaller_vars_removed.append('DYLD_LIBRARY_PATH (removed)')
|
||||
|
||||
# Log what was cleaned for debugging
|
||||
if pyinstaller_vars_removed:
|
||||
logger.debug(f"Steam restart: Cleaned PyInstaller environment variables: {', '.join(pyinstaller_vars_removed)}")
|
||||
else:
|
||||
logger.debug("Steam restart: No PyInstaller environment variables detected (likely DEV mode)")
|
||||
|
||||
return env
|
||||
|
||||
class SteamRestartError(Exception):
|
||||
pass
|
||||
|
||||
def is_steam_deck() -> bool:
|
||||
"""Detect if running on Steam Deck/SteamOS."""
|
||||
try:
|
||||
if os.path.exists('/etc/os-release'):
|
||||
with open('/etc/os-release', 'r') as f:
|
||||
content = f.read().lower()
|
||||
if 'steamos' in content or 'steam deck' in content:
|
||||
return True
|
||||
if os.path.exists('/sys/devices/virtual/dmi/id/product_name'):
|
||||
with open('/sys/devices/virtual/dmi/id/product_name', 'r') as f:
|
||||
if 'steam deck' in f.read().lower():
|
||||
return True
|
||||
if os.environ.get('STEAM_RUNTIME') and os.path.exists('/home/deck'):
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Error detecting Steam Deck: {e}")
|
||||
return False
|
||||
|
||||
def get_steam_processes() -> list:
|
||||
"""Return a list of psutil.Process objects for running Steam processes."""
|
||||
steam_procs = []
|
||||
for proc in psutil.process_iter(['pid', 'name', 'exe', 'cmdline']):
|
||||
try:
|
||||
name = proc.info['name']
|
||||
exe = proc.info['exe']
|
||||
cmdline = proc.info['cmdline']
|
||||
if name and 'steam' in name.lower():
|
||||
steam_procs.append(proc)
|
||||
elif exe and 'steam' in exe.lower():
|
||||
steam_procs.append(proc)
|
||||
elif cmdline and any('steam' in str(arg).lower() for arg in cmdline):
|
||||
steam_procs.append(proc)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
continue
|
||||
return steam_procs
|
||||
|
||||
def wait_for_steam_exit(timeout: int = 60, check_interval: float = 0.5) -> bool:
|
||||
"""Wait for all Steam processes to exit using pgrep (matching existing logic)."""
|
||||
start = time.time()
|
||||
env = _get_clean_subprocess_env()
|
||||
while time.time() - start < timeout:
|
||||
try:
|
||||
result = subprocess.run(['pgrep', '-f', 'steamwebhelper'], capture_output=True, timeout=10, env=env)
|
||||
if result.returncode != 0:
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking Steam processes: {e}")
|
||||
time.sleep(check_interval)
|
||||
return False
|
||||
|
||||
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)
|
||||
if is_steam_deck():
|
||||
subprocess.Popen(["systemctl", "--user", "restart", "app-steam@autostart.service"], env=env)
|
||||
return True
|
||||
|
||||
# Use startup methods with only -silent flag (no -minimized or -no-browser)
|
||||
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}}
|
||||
]
|
||||
|
||||
for method in start_methods:
|
||||
method_name = method["name"]
|
||||
logger.info(f"Attempting to start Steam using method: {method_name}")
|
||||
try:
|
||||
process = subprocess.Popen(method["cmd"], **method["kwargs"])
|
||||
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)
|
||||
if check_result.returncode == 0:
|
||||
logger.info(f"Steam process detected after using {method_name}. Proceeding to wait phase.")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"Steam process not detected after initiating with {method_name}. Trying next method.")
|
||||
else:
|
||||
logger.warning(f"Failed to start process with {method_name}. Trying next method.")
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Command not found for method {method_name} (e.g., setsid, nohup). Trying next method.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting Steam with {method_name}: {e}. Trying next method.")
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting Steam: {e}")
|
||||
return False
|
||||
|
||||
def robust_steam_restart(progress_callback: Optional[Callable[[str], None]] = None, timeout: int = 60) -> bool:
|
||||
"""
|
||||
Robustly restart Steam across all distros. Returns True on success, False on failure.
|
||||
Optionally accepts a progress_callback(message: str) for UI feedback.
|
||||
Uses aggressive pkill approach for maximum reliability.
|
||||
"""
|
||||
env = _get_clean_subprocess_env()
|
||||
|
||||
def report(msg):
|
||||
logger.info(msg)
|
||||
if progress_callback:
|
||||
progress_callback(msg)
|
||||
|
||||
report("Shutting down Steam...")
|
||||
|
||||
# Steam Deck: Use systemctl for shutdown (special handling)
|
||||
if is_steam_deck():
|
||||
try:
|
||||
report("Steam Deck detected - using systemctl shutdown...")
|
||||
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}")
|
||||
|
||||
# All systems: Use pkill approach (proven 15/16 test success rate)
|
||||
try:
|
||||
# Skip unreliable steam -shutdown, go straight to pkill
|
||||
pkill_result = subprocess.run(['pkill', 'steam'], timeout=15, check=False, capture_output=True, env=env)
|
||||
logger.debug(f"pkill steam result: {pkill_result.returncode}")
|
||||
time.sleep(2)
|
||||
|
||||
# Check if Steam is still running
|
||||
check_result = subprocess.run(['pgrep', '-f', 'steamwebhelper'], capture_output=True, timeout=10, env=env)
|
||||
if check_result.returncode == 0:
|
||||
# Force kill if still running
|
||||
report("Steam still running - force terminating...")
|
||||
force_result = subprocess.run(['pkill', '-9', 'steam'], timeout=15, check=False, capture_output=True, env=env)
|
||||
logger.debug(f"pkill -9 steam result: {force_result.returncode}")
|
||||
time.sleep(2)
|
||||
|
||||
# Final check
|
||||
final_check = subprocess.run(['pgrep', '-f', 'steamwebhelper'], capture_output=True, timeout=10, env=env)
|
||||
if final_check.returncode != 0:
|
||||
logger.info("Steam processes successfully force terminated.")
|
||||
else:
|
||||
report("Failed to terminate Steam processes.")
|
||||
return False
|
||||
else:
|
||||
logger.info("Steam processes successfully terminated.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error during Steam shutdown: {e}")
|
||||
report("Failed to shut down Steam.")
|
||||
return False
|
||||
|
||||
report("Steam closed successfully.")
|
||||
|
||||
# Start Steam using platform-specific logic
|
||||
report("Starting Steam...")
|
||||
|
||||
# Steam Deck: Use systemctl restart (keep existing working approach)
|
||||
if is_steam_deck():
|
||||
try:
|
||||
subprocess.Popen(["systemctl", "--user", "restart", "app-steam@autostart.service"], env=env)
|
||||
logger.info("Steam Deck: Initiated systemctl restart")
|
||||
except Exception as e:
|
||||
logger.error(f"Steam Deck systemctl restart failed: {e}")
|
||||
report("Failed to restart Steam on Steam Deck.")
|
||||
return False
|
||||
else:
|
||||
# All other distros: Use proven steam -silent method
|
||||
if not start_steam():
|
||||
report("Failed to start Steam.")
|
||||
return False
|
||||
|
||||
# Wait for Steam to fully initialize using existing logic
|
||||
report("Waiting for Steam to fully start")
|
||||
logger.info("Waiting up to 2 minutes for Steam to fully initialize...")
|
||||
max_startup_wait = 120
|
||||
elapsed_wait = 0
|
||||
initial_wait_done = False
|
||||
|
||||
while elapsed_wait < max_startup_wait:
|
||||
try:
|
||||
result = subprocess.run(['pgrep', '-f', 'steam'], capture_output=True, timeout=10, env=env)
|
||||
if result.returncode == 0:
|
||||
if not initial_wait_done:
|
||||
logger.info("Steam process detected. Waiting additional time for full initialization...")
|
||||
initial_wait_done = True
|
||||
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=env)
|
||||
if final_check.returncode == 0:
|
||||
report("Steam started successfully.")
|
||||
logger.info("Steam confirmed running after wait.")
|
||||
return True
|
||||
else:
|
||||
logger.warning("Steam process disappeared during final initialization wait.")
|
||||
break
|
||||
else:
|
||||
logger.debug(f"Steam process not yet detected. Waiting... ({elapsed_wait + 5}s)")
|
||||
time.sleep(5)
|
||||
elapsed_wait += 5
|
||||
except Exception as e:
|
||||
logger.warning(f"Error during Steam startup wait: {e}")
|
||||
time.sleep(5)
|
||||
elapsed_wait += 5
|
||||
|
||||
report("Steam did not start within timeout.")
|
||||
logger.error("Steam failed to start/initialize within the allowed time.")
|
||||
return False
|
||||
Reference in New Issue
Block a user