Updated the update system to support multi channels (stable / pre-release).
This commit is contained in:
122
ptlinux/cli.py
122
ptlinux/cli.py
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user