Added umu download and repair for missing components on install, update and manual repair.
This commit is contained in:
208
ptlinux/cli.py
208
ptlinux/cli.py
@@ -4,10 +4,11 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from contextlib import AbstractContextManager, nullcontext
|
||||
from dataclasses import asdict
|
||||
from dataclasses import asdict, replace
|
||||
from datetime import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from typing import Callable, Sequence
|
||||
@@ -20,7 +21,7 @@ from ptdiag.logs import (
|
||||
)
|
||||
|
||||
from . import __version__
|
||||
from .command import CommandError, CommandRunner
|
||||
from .command import CommandError, CommandOutcome, CommandRunner
|
||||
from .depot import (
|
||||
SyncProgress,
|
||||
read_cached_depot_launch_catalog,
|
||||
@@ -44,6 +45,7 @@ from .login_trace import DEFAULT_TRACE_TIMEOUT_SECONDS, LOGIN_TRACE_DISABLED_REA
|
||||
from .overlay import cache_compatibility_binaries, prepare_runtime_view
|
||||
from .paths import AppPaths
|
||||
from .runtime import (
|
||||
RuntimeErrorPT,
|
||||
game_runtime_ready,
|
||||
prepare_game_runtime,
|
||||
run_game,
|
||||
@@ -57,6 +59,12 @@ from .self_update import (
|
||||
update_check_to_dict,
|
||||
verify_launcher_update_channel,
|
||||
)
|
||||
from .umu import (
|
||||
UmuBootstrapResult,
|
||||
find_umu_launcher,
|
||||
provision_umu,
|
||||
provision_umu_for_startup,
|
||||
)
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
@@ -74,6 +82,10 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
subparsers.add_parser("gui", help="open the native Linux dashboard")
|
||||
subparsers.add_parser("doctor", help="check cross-distribution dependencies")
|
||||
subparsers.add_parser(
|
||||
"runtime-bootstrap",
|
||||
help="download and verify the Steam Runtime launcher helper",
|
||||
)
|
||||
login_diagnostics = subparsers.add_parser(
|
||||
"login-diagnostics",
|
||||
help="check the client, runtime, and publisher login route without credentials",
|
||||
@@ -236,12 +248,15 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
paths = AppPaths.from_environment()
|
||||
try:
|
||||
if command == "gui":
|
||||
provision_umu_for_startup(paths)
|
||||
from .ui import run_dashboard
|
||||
|
||||
run_dashboard(paths)
|
||||
return 0
|
||||
if command == "doctor":
|
||||
return _doctor(arguments.json)
|
||||
if command == "runtime-bootstrap":
|
||||
return _runtime_bootstrap(paths, arguments.json)
|
||||
if command == "login-diagnostics":
|
||||
return _login_diagnostics(
|
||||
paths,
|
||||
@@ -368,6 +383,25 @@ def _doctor(as_json: bool) -> int:
|
||||
return 0 if report.ok else 1
|
||||
|
||||
|
||||
def _runtime_bootstrap(paths: AppPaths, as_json: bool) -> int:
|
||||
_print_operation_status(as_json, 1, 1, "Preparing the Steam Runtime helper…")
|
||||
result = provision_umu(paths)
|
||||
executable = find_umu_launcher(paths)
|
||||
if executable is None:
|
||||
raise LauncherError("the Steam Runtime helper is still unavailable")
|
||||
_emit(
|
||||
{
|
||||
"ok": True,
|
||||
"runtime_bootstrap": {
|
||||
"executable": executable,
|
||||
"managed_download": asdict(result) if result else None,
|
||||
},
|
||||
},
|
||||
as_json,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _login_diagnostics(
|
||||
paths: AppPaths,
|
||||
timeout_seconds: float,
|
||||
@@ -781,8 +815,13 @@ def _play_locked(
|
||||
)
|
||||
elif not dry_run:
|
||||
overlay = prepare_runtime_view(paths, source)
|
||||
_print_operation_status(as_json, 2, 4, "Selecting the configured game runtime…")
|
||||
selection = select_runtime(settings)
|
||||
_print_operation_status(as_json, 2, 4, "Ensuring the compatibility runtime is available…")
|
||||
umu_bootstrap = (
|
||||
None
|
||||
if reuse_prepared
|
||||
else _ensure_portable_umu_for_play(paths, settings, dry_run=dry_run)
|
||||
)
|
||||
selection = select_runtime(settings, paths=paths)
|
||||
runner = _runner(paths, "play", dry_run)
|
||||
setup_results = []
|
||||
if reuse_prepared:
|
||||
@@ -820,6 +859,7 @@ def _play_locked(
|
||||
start_option=start_option,
|
||||
use_gamebox=not no_box,
|
||||
concurrent_prefix_launch=reuse_prepared,
|
||||
check=False,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
current_game_log = _current_game_log(
|
||||
@@ -841,23 +881,63 @@ def _play_locked(
|
||||
and runtime_issues["host_futex_fatal_count"]
|
||||
):
|
||||
runtime_issues["recommended_one_variable_comparison"] = "proton7"
|
||||
_emit(
|
||||
{
|
||||
"ok": outcome.returncode == 0,
|
||||
"dry_run": dry_run,
|
||||
"runtime": asdict(selection),
|
||||
"realm": asdict(start_option),
|
||||
"runtime_view": asdict(overlay) if overlay else None,
|
||||
"setup_commands": [asdict(result) for result in setup_results],
|
||||
"game": asdict(outcome),
|
||||
"current_game_log": current_game_log,
|
||||
"runtime_issues": runtime_issues,
|
||||
},
|
||||
as_json,
|
||||
)
|
||||
elif selection.kind == "proton7" and runtime_issues["gstreamer_missing_libraries"]:
|
||||
runtime_issues["recommended_one_variable_comparison"] = "proton7-container"
|
||||
elif runtime_issues["dbghelp_symdeferred_assertion_count"]:
|
||||
runtime_issues["recommended_one_variable_comparison"] = "umu"
|
||||
report = {
|
||||
"ok": outcome.returncode == 0,
|
||||
"dry_run": dry_run,
|
||||
"runtime": asdict(selection),
|
||||
"umu_bootstrap": asdict(umu_bootstrap) if umu_bootstrap else None,
|
||||
"realm": asdict(start_option),
|
||||
"runtime_view": asdict(overlay) if overlay else None,
|
||||
"setup_commands": [asdict(result) for result in setup_results],
|
||||
"game": asdict(outcome),
|
||||
"current_game_log": current_game_log,
|
||||
"runtime_issues": runtime_issues,
|
||||
}
|
||||
if as_json or dry_run:
|
||||
_emit(report, as_json)
|
||||
else:
|
||||
_print_game_outcome(outcome, current_game_log, runtime_issues)
|
||||
return 0 if outcome.returncode == 0 else 1
|
||||
|
||||
|
||||
def _ensure_portable_umu_for_play(
|
||||
paths: AppPaths,
|
||||
settings: Settings,
|
||||
*,
|
||||
dry_run: bool,
|
||||
) -> UmuBootstrapResult | None:
|
||||
"""Provide a pinned UMU when the selected policy may use a container."""
|
||||
|
||||
if settings.runtime == "proton7":
|
||||
return None
|
||||
if dry_run:
|
||||
return None
|
||||
try:
|
||||
return provision_umu(paths)
|
||||
except (OSError, RuntimeError) as exc:
|
||||
if settings.runtime != "auto":
|
||||
raise LauncherError(
|
||||
f"the verified portable UMU runtime helper could not be installed: {exc}"
|
||||
) from exc
|
||||
try:
|
||||
select_runtime(replace(settings, runtime="proton7"), paths=paths)
|
||||
except RuntimeErrorPT:
|
||||
raise LauncherError(
|
||||
"no usable game runtime is available and the verified portable UMU "
|
||||
f"runtime helper could not be installed: {exc}"
|
||||
) from exc
|
||||
print(
|
||||
"Warning: the Steam Runtime helper could not be installed; falling back "
|
||||
f"to direct Proton 7 for this launch ({exc}).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _current_game_log(
|
||||
directory: Path,
|
||||
baseline: dict[str, LogFileState],
|
||||
@@ -910,15 +990,76 @@ def _runtime_issue_summary(stderr: str) -> dict[str, object]:
|
||||
folded = stderr.casefold()
|
||||
futex_message = "the futex facility returned an unexpected error code"
|
||||
wrong_elf_message = "wrong elf class: elfclass64"
|
||||
dbghelp_assertion = "assertion failed: module->module.symtype != symdeferred"
|
||||
missing_libraries = sorted(
|
||||
{
|
||||
library
|
||||
for line in folded.splitlines()
|
||||
if "gstreamer-warning" in line and "failed to load plugin" in line
|
||||
for library in re.findall(
|
||||
r"([a-z0-9_.+-]+\.so(?:\.\d+)*): cannot open shared object file",
|
||||
line,
|
||||
)
|
||||
}
|
||||
)
|
||||
return {
|
||||
"host_futex_fatal_count": folded.count(futex_message),
|
||||
"gstreamer_wrong_elf_class_count": folded.count(wrong_elf_message),
|
||||
"gstreamer_missing_library_count": len(missing_libraries),
|
||||
"gstreamer_missing_libraries": missing_libraries,
|
||||
"gstreamer_stream_error": "winegstreamer error" in folded,
|
||||
"mesa_egl_initialization_failed": "egl: failed to create dri2 screen" in folded,
|
||||
"steam_api_app_id_missing": "steamapi_init() failed; no appid found" in folded,
|
||||
"dbghelp_symdeferred_assertion_count": folded.count(dbghelp_assertion),
|
||||
}
|
||||
|
||||
|
||||
def _print_game_outcome(
|
||||
outcome: CommandOutcome,
|
||||
current_game_log: dict[str, object] | None,
|
||||
runtime_issues: dict[str, object],
|
||||
) -> None:
|
||||
# Keep the human-facing stream concise. CommandRunner already relayed every
|
||||
# process line live; replaying its capture here made each Proton failure appear
|
||||
# twice in the dashboard.
|
||||
if outcome.timed_out:
|
||||
print("Game client timed out and was stopped.", file=sys.stderr)
|
||||
elif outcome.returncode == 0:
|
||||
print("Game client exited normally.")
|
||||
else:
|
||||
print(f"Game client exited with status {outcome.returncode}.", file=sys.stderr)
|
||||
if current_game_log is not None:
|
||||
_print_interrupted_game_log(current_game_log)
|
||||
wrong_elf_count = runtime_issues["gstreamer_wrong_elf_class_count"]
|
||||
if isinstance(wrong_elf_count, int) and wrong_elf_count:
|
||||
print(
|
||||
"The repeated GStreamer wrong-ELF-class messages are plugin-scan noise; "
|
||||
"they do not mean that Proton itself failed to start.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
missing_libraries = runtime_issues["gstreamer_missing_libraries"]
|
||||
if isinstance(missing_libraries, list) and missing_libraries:
|
||||
print(
|
||||
"Media plugins could not load these shared libraries: "
|
||||
+ ", ".join(str(value) for value in missing_libraries)
|
||||
+ ".",
|
||||
file=sys.stderr,
|
||||
)
|
||||
assertion_count = runtime_issues["dbghelp_symdeferred_assertion_count"]
|
||||
if isinstance(assertion_count, int) and assertion_count:
|
||||
print(
|
||||
"Proton 7's older Wine build hit its dbghelp SymDeferred assertion; this "
|
||||
"can mask the client failure that occurred immediately before it.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
recommendation = runtime_issues.get("recommended_one_variable_comparison")
|
||||
if isinstance(recommendation, str):
|
||||
print(
|
||||
f"Recommended single-variable runtime comparison: {recommendation}.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def _print_interrupted_game_log(report: dict[str, object]) -> None:
|
||||
print(f"\nCurrent game log: {report['path']}", file=sys.stderr)
|
||||
if report["module_code_sync_error"]:
|
||||
@@ -1020,17 +1161,44 @@ def _launcher_update(
|
||||
as_json,
|
||||
)
|
||||
return 0
|
||||
_print_operation_status(as_json, 1, 2, "Verifying the signed launcher release…")
|
||||
_print_operation_status(as_json, 1, 3, "Verifying the signed launcher release…")
|
||||
result = install_launcher_update(
|
||||
paths,
|
||||
manifest_url=manifest_url,
|
||||
public_key_path=public_key,
|
||||
)
|
||||
_print_operation_status(as_json, 2, 2, "Launcher update installed safely.")
|
||||
_print_operation_status(
|
||||
as_json,
|
||||
2,
|
||||
3,
|
||||
"Launcher update installed; preparing the Steam Runtime helper…",
|
||||
)
|
||||
umu_result: UmuBootstrapResult | None = None
|
||||
umu_executable: str | None = None
|
||||
umu_warning: str | None = None
|
||||
try:
|
||||
umu_result = provision_umu(paths)
|
||||
umu_executable = find_umu_launcher(paths)
|
||||
except (OSError, RuntimeError) as exc:
|
||||
# Activation is already durable. Keep the update successful so the GUI can
|
||||
# restart into the new release, whose startup hook and Play path both retry.
|
||||
warning_message = (
|
||||
"the launcher update was installed, but the Steam Runtime helper could "
|
||||
f"not be prepared yet: {exc}"
|
||||
)
|
||||
umu_warning = warning_message
|
||||
if not as_json:
|
||||
print(f"Warning: {warning_message}", file=sys.stderr)
|
||||
_print_operation_status(as_json, 3, 3, "Launcher update and runtime check complete.")
|
||||
_emit(
|
||||
{
|
||||
"ok": True,
|
||||
"launcher_update": asdict(result),
|
||||
"runtime_bootstrap": {
|
||||
"executable": umu_executable,
|
||||
"managed_download": asdict(umu_result) if umu_result else None,
|
||||
"warning": umu_warning,
|
||||
},
|
||||
},
|
||||
as_json,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user