add support for XLM pre-launch script integration and non-interactive configuration modes

This commit is contained in:
CPTN Cosmo 2026-04-18 15:23:24 +02:00
parent 72c301be78
commit 6f7d8c9d7c
4 changed files with 188 additions and 66 deletions

View file

@ -13,6 +13,7 @@ import hashlib
import os
import shutil
import configparser
import argparse
try:
import keyring
HAS_KEYRING = True
@ -142,7 +143,11 @@ def check_and_update_launcher_ini():
print(f"\nConfiguration Check: '{ini_path}' not found.")
print("Please open XIVLauncher, go to Settings, and enable 'Start internal OTP server'.")
print("This is required for the wrapper to function.")
input("Press Enter to continue once you have done this (or to try anyway)...")
if sys.stdin.isatty():
try:
input("Press Enter to continue once you have done this (or to try anyway)...")
except EOFError:
pass
return
config = configparser.ConfigParser()
@ -164,7 +169,16 @@ def check_and_update_launcher_ini():
print(f"\nConfiguration Check: '{ini_path}'")
print("XIVLauncher requires 'IsOtpServer' to be enabled to accept OTP codes from this wrapper.")
choice = input("Enable 'IsOtpServer' now? [Y/n]: ").strip().lower()
choice = 'n'
if sys.stdin.isatty():
try:
choice = input("Enable 'IsOtpServer' now? [Y/n]: ").strip().lower()
except EOFError:
choice = 'y'
else:
print("Non-interactive mode detected. Auto-enabling 'IsOtpServer'.")
choice = 'y'
if choice in ('', 'y', 'yes'):
config.set('Main', 'IsOtpServer', 'true')
try:
@ -257,6 +271,10 @@ def get_secret(config, config_path):
if not secret_input:
print("GUI prompt unavailable or cancelled. Falling back to CLI.")
if not sys.stdin.isatty():
print("Error: No interactive terminal available and GUI prompt failed. Cannot prompt for secret.")
return None
print("Please enter your TOTP Secret (base32) or otpauth:// URL.")
while True:
try:
@ -268,6 +286,9 @@ def get_secret(config, config_path):
except KeyboardInterrupt:
print("\nOperation cancelled.")
sys.exit(1)
except EOFError:
print("\nError: EOF reading secret.")
return None
# Basic validation/parsing check
parsed = parse_secret(secret_input)
@ -298,6 +319,16 @@ def get_secret(config, config_path):
return parsed
def main():
parser = argparse.ArgumentParser(description="XIVLauncher Wrapper")
parser.add_argument('--prelaunch', action='store_true', help="Run as a prelaunch script (spawns injector and exits)")
parser.add_argument('--inject-only', action='store_true', help=argparse.SUPPRESS)
args, unknown_args = parser.parse_known_args()
# Auto-detect if running from a prelaunch.d directory
script_path = os.path.abspath(sys.argv[0])
if 'prelaunch.d' in script_path.split(os.sep):
args.prelaunch = True
# Determine config path (XDG standard)
xdg_config_home = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
config_dir = os.path.join(xdg_config_home, 'xivlauncher-wrapper')
@ -332,52 +363,62 @@ def main():
check_and_update_launcher_ini()
cmd = config.get("launcher_cmd")
config_dirty = False
cmd_args = None
# 2. Auto-detect command if missing
if not cmd:
print("\n'launcher_cmd' not set in config. detecting installed XIVLauncher versions...")
available = detect_launch_command()
if not available:
print("Error: No XIVLauncher installation found (Flatpak or Native).")
print("Please install XIVLauncher or manually set 'launcher_cmd' in config.json.")
sys.exit(1)
elif len(available) >= 1:
name, command = available[0]
if len(available) > 1:
print(f"Multiple XIVLauncher installations found. Automatically selecting first available: {name}")
else:
print(f"Detected {name}: {command}")
cmd = command
config['launcher_cmd'] = cmd
config_dirty = True
# 2.5 Auto-detect gamemode/game-performance
if "use_gamemode" not in config:
gamemode_cmd = None
if shutil.which("game-performance"):
gamemode_cmd = "game-performance"
elif shutil.which("gamemoderun"):
gamemode_cmd = "gamemoderun"
if gamemode_cmd:
print(f"\n'{gamemode_cmd}' was detected on your system.")
print("Would you like to enable it for XIVLauncher? This can improve game performance.")
try:
choice = input(f"Enable {gamemode_cmd}? [Y/n]: ").strip().lower()
except (EOFError, KeyboardInterrupt):
choice = 'n'
print("\nPrompt interrupted, defaulting to No.")
# Determine command to launch (if not prelaunch/inject-only)
if not args.prelaunch and not args.inject_only:
if unknown_args:
# Command passed via args (Steam %command% mode)
cmd_args = unknown_args
cmd = " ".join(shlex.quote(a) for a in cmd_args)
else:
# 2. Auto-detect command if missing
if not cmd:
print("\n'launcher_cmd' not set in config. detecting installed XIVLauncher versions...")
available = detect_launch_command()
if choice in ('', 'y', 'yes'):
config["use_gamemode"] = True
config["gamemode_cmd"] = gamemode_cmd
else:
config["use_gamemode"] = False
config_dirty = True
if not available:
print("Error: No XIVLauncher installation found (Flatpak or Native).")
print("Please install XIVLauncher or manually set 'launcher_cmd' in config.json.")
sys.exit(1)
elif len(available) >= 1:
name, command = available[0]
if len(available) > 1:
print(f"Multiple XIVLauncher installations found. Automatically selecting first available: {name}")
else:
print(f"Detected {name}: {command}")
cmd = command
config['launcher_cmd'] = cmd
config_dirty = True
# 2.5 Auto-detect gamemode/game-performance
if "use_gamemode" not in config:
gamemode_cmd = None
if shutil.which("game-performance"):
gamemode_cmd = "game-performance"
elif shutil.which("gamemoderun"):
gamemode_cmd = "gamemoderun"
if gamemode_cmd:
print(f"\n'{gamemode_cmd}' was detected on your system.")
print("Would you like to enable it for XIVLauncher? This can improve game performance.")
try:
if sys.stdin.isatty():
choice = input(f"Enable {gamemode_cmd}? [Y/n]: ").strip().lower()
else:
choice = 'y' # auto-enable if not interactive
except (EOFError, KeyboardInterrupt):
choice = 'n'
print("\nPrompt interrupted, defaulting to No.")
if choice in ('', 'y', 'yes'):
config["use_gamemode"] = True
config["gamemode_cmd"] = gamemode_cmd
else:
config["use_gamemode"] = False
config_dirty = True
# Save config if updated (e.g. launcher_cmd detected)
if config_dirty:
@ -395,18 +436,25 @@ def main():
print("Error: No secret available.")
sys.exit(1)
if args.inject_only:
# Only run the OTP injection loop and exit
send_otp(secret)
sys.exit(0)
if args.prelaunch:
print("Running in prelaunch mode. Spawning OTP injector in background...")
# Spawn ourselves in background with --inject-only
subprocess.Popen([sys.executable, os.path.realpath(__file__), '--inject-only'],
start_new_session=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
sys.exit(0)
print(f"Launch Command: {cmd}")
# 3. Launch Logic
try:
# Launch the process
# We use shell=True if the command contains arguments like 'flatpak run ...'
# But split() and direct arg list is safer and cleaner if possible.
# However, 'flatpak run ...' is multiple args.
# If the user put a complex command string in config, we should respect it.
# Simple split logic:
import shlex
args = shlex.split(cmd)
if not cmd_args:
cmd_args = shlex.split(cmd)
# Prepend gamemode wrapper if enabled
if config.get("use_gamemode"):
@ -417,16 +465,17 @@ def main():
elif shutil.which("gamemoderun"):
g_cmd = "gamemoderun"
if g_cmd and shutil.which(g_cmd):
args.insert(0, g_cmd)
cmd_args.insert(0, g_cmd)
print(f"Using game performance wrapper: {g_cmd}")
try:
# Inject XL_SECRET_PROVIDER=file environment variable
env = os.environ.copy()
env['XL_SECRET_PROVIDER'] = 'file'
proc = subprocess.Popen(args, env=env)
proc = subprocess.Popen(cmd_args, env=env)
except FileNotFoundError:
print(f"Error: Command executable not found: {cmd}")
print(f"Error: Command executable not found: {cmd_args[0] if cmd_args else cmd}")
sys.exit(1)
except Exception as e:
print(f"Error launching command '{cmd}': {e}")