Updated the update system to support multi channels (stable / pre-release).

This commit is contained in:
2026-09-09 20:52:00 +02:00
parent 2a53a59cef
commit 5770969629
12 changed files with 3329 additions and 254 deletions

View File

@@ -14,6 +14,7 @@ import sys
import threading
import time
from typing import Callable, Sequence
from uuid import uuid4
from ptdiag.logs import (
LogFileState,
@@ -78,6 +79,7 @@ from .self_update import (
update_check_to_dict,
verify_launcher_update_channel,
)
from .update_resolver import STABLE_CHANNEL
from .telemetry import (
TelemetryService,
classify_network_failure,
@@ -92,6 +94,11 @@ from .umu import (
provision_umu,
provision_umu_for_startup,
)
from .window_diagnostics import (
WINDOW_DIAGNOSTICS_ENV,
WINDOW_DIAGNOSTICS_SESSION_ENV,
parse_window_diagnostic,
)
def build_parser() -> argparse.ArgumentParser:
@@ -262,8 +269,13 @@ def build_parser() -> argparse.ArgumentParser:
)
launcher_action.add_argument(
"--rollback",
action="store_true",
help="switch back to the previously installed launcher release",
nargs="?",
const="",
metavar="VERSION",
help=(
"switch to VERSION from signed release history; without VERSION, "
"switch to the locally retained previous release"
),
)
launcher_update.add_argument(
"--manifest-url",
@@ -274,6 +286,10 @@ def build_parser() -> argparse.ArgumentParser:
type=Path,
help="override the pinned launcher-update public key",
)
launcher_update.add_argument(
"--history-url",
help="override the signed launcher release-history URL",
)
return parser
@@ -496,6 +512,7 @@ def _dispatch_command(
arguments.manifest_url,
arguments.public_key,
arguments.json,
history_url=arguments.history_url,
)
parser.error(f"unknown command: {command}")
@@ -516,7 +533,7 @@ def _command_telemetry_events(
if command == "launcher-update" and not (
bool(getattr(arguments, "check", False))
or bool(getattr(arguments, "verify", False))
or bool(getattr(arguments, "rollback", False))
or getattr(arguments, "rollback", None) is not None
):
return (
"launcher_update",
@@ -1282,9 +1299,21 @@ def _play_locked(
) -> int:
telemetry = current_telemetry()
launch_requested_at = time.monotonic()
window_diagnostics_active = (
telemetry is not None
and not dry_run
and bool(getattr(telemetry, "enabled", True))
)
launch_session_id = uuid4().hex if window_diagnostics_active else None
launch_session_metadata = (
{"launch_session_id": launch_session_id}
if launch_session_id is not None
else {}
)
if telemetry is not None and not dry_run:
telemetry.send_event(
"game_launch_requested",
metadata=launch_session_metadata,
context={"operation": "launch_game", "stage": "preflight"},
)
if game_dir is None and game_import_requires_verification(paths):
@@ -1387,6 +1416,7 @@ def _play_locked(
telemetry.send_event(
"game_launch_started",
metadata={
**launch_session_metadata,
"launch_duration_ms": round(
(game_started_at - launch_requested_at) * 1000
),
@@ -1404,10 +1434,31 @@ def _play_locked(
else None
)
def observe_game_output(line: str) -> str:
def observe_game_output(line: str) -> str | None:
"""Timestamp launch success when the supervisor first confirms readiness."""
nonlocal launch_success_reported
window_diagnostic = parse_window_diagnostic(line)
if window_diagnostic is not None:
diagnostic_session_id = window_diagnostic.get("session_id")
if launch_session_id is None or diagnostic_session_id != launch_session_id:
return line
if telemetry is not None and window_diagnostics_active:
with launch_success_lock:
telemetry.send_event(
"game_window_diagnostic",
severity=_window_diagnostic_severity(window_diagnostic),
metadata={
"runtime": selection.kind,
"realm": start_option.name,
**window_diagnostic,
},
context={
"operation": "launch_game",
"stage": "window_diagnostics",
},
)
return None
live_readiness_signal = _game_readiness_signal(line)
if telemetry is None or dry_run or live_readiness_signal is None:
return line
@@ -1418,6 +1469,7 @@ def _play_locked(
launch_success_reported = telemetry.send_event(
"game_launch_success",
metadata={
**launch_session_metadata,
"launch_duration_ms": round(
(ready_at - launch_requested_at) * 1000
),
@@ -1444,6 +1496,14 @@ def _play_locked(
concurrent_prefix_launch=reuse_prepared,
check=False,
output_line_filter=observe_game_output,
extra_environment=(
{
WINDOW_DIAGNOSTICS_ENV: "1",
WINDOW_DIAGNOSTICS_SESSION_ENV: launch_session_id,
}
if launch_session_id is not None
else None
),
pass_fds=game_lease_fds,
)
except KeyboardInterrupt:
@@ -1453,6 +1513,7 @@ def _play_locked(
severity="warning",
message="Game launch was interrupted",
metadata={
**launch_session_metadata,
"runtime_seconds": round(time.monotonic() - game_started_at),
"clean_exit": False,
"session_duration_class": _session_duration_class(
@@ -1524,6 +1585,7 @@ def _play_locked(
game_runtime_seconds = max(0.0, time.monotonic() - game_started_at)
if telemetry is not None and not dry_run:
common_game_metadata: dict[str, object] = {
**launch_session_metadata,
"runtime_seconds": round(game_runtime_seconds),
"clean_exit": clean_exit,
"timed_out": outcome.timed_out,
@@ -1547,6 +1609,7 @@ def _play_locked(
telemetry.send_event(
"game_launch_success",
metadata={
**launch_session_metadata,
"detection_duration_ms": round(game_runtime_seconds * 1000),
"detected_after_exit": True,
"game_window_ready": game_window_ready,
@@ -1858,6 +1921,33 @@ def _game_readiness_signal(output: str) -> str | None:
return None
def _window_diagnostic_severity(diagnostic: dict[str, object]) -> str:
name = diagnostic.get("diagnostic")
details = diagnostic.get("details")
if isinstance(name, str) and (
name.endswith(("_failed", "_unavailable"))
or name in {
"host_hidden_state_cleared",
"host_inactive_remap",
"host_minimize_latched",
}
):
return "warning"
if isinstance(details, dict):
if name == "host_scaling_configured" and not all(
details.get(key) is True
for key in (
"resize_increments_applied",
"scaling_enabled",
"compositing_preferred",
)
):
return "warning"
if name == "scaling_configured" and details.get("enabled") is not True:
return "warning"
return "debug"
def _print_game_outcome(
outcome: CommandOutcome,
current_game_log: dict[str, object] | None,
@@ -2015,13 +2105,28 @@ def _launcher_update(
paths: AppPaths,
check_only: bool,
verify: bool,
rollback: bool,
rollback: str | bool | None,
manifest_url: str | None,
public_key: Path | None,
as_json: bool,
*,
history_url: str | None = None,
) -> int:
if rollback:
result = rollback_launcher_update(paths)
try:
selected_channel = load_settings(paths).update_channel
except (OSError, TypeError, ValueError):
# Keep update recovery usable when settings are absent or damaged.
selected_channel = STABLE_CHANNEL
rollback_requested = rollback is not None and rollback is not False
if rollback_requested:
target_version = rollback if isinstance(rollback, str) and rollback else None
result = rollback_launcher_update(
paths,
target_version=target_version,
selected_channel=selected_channel,
history_url=history_url,
public_key_path=public_key,
)
_emit(
{
"ok": True,
@@ -2034,6 +2139,7 @@ def _launcher_update(
check = check_for_launcher_update(
manifest_url=manifest_url,
public_key_path=public_key,
channel=selected_channel,
)
_emit(
{
@@ -2048,6 +2154,7 @@ def _launcher_update(
result = verify_launcher_update_channel(
manifest_url=manifest_url,
public_key_path=public_key,
channel=selected_channel,
)
_print_operation_status(as_json, 2, 2, "Published launcher release verified.")
_emit(
@@ -2063,6 +2170,7 @@ def _launcher_update(
paths,
manifest_url=manifest_url,
public_key_path=public_key,
channel=selected_channel,
)
_print_operation_status(
as_json,