Added more folders and restructured classes inside them.
This commit is contained in:
245
src/ptlinux/application/common.py
Normal file
245
src/ptlinux/application/common.py
Normal file
@@ -0,0 +1,245 @@
|
||||
"""Shared CLI telemetry, result reporting, and output helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import signal
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from ..command import CommandError
|
||||
from ..paths import AppPaths
|
||||
from ..telemetry import TelemetryService
|
||||
|
||||
|
||||
CommandTelemetryEvents = tuple[str, str, str, str]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CommandTelemetryLifecycle:
|
||||
telemetry: TelemetryService | None
|
||||
events: CommandTelemetryEvents | None
|
||||
started_at: float
|
||||
owner: bool
|
||||
|
||||
|
||||
def start_command_telemetry(
|
||||
api: Any,
|
||||
paths: AppPaths,
|
||||
command: str,
|
||||
arguments: argparse.Namespace,
|
||||
manage_telemetry: bool,
|
||||
) -> CommandTelemetryLifecycle:
|
||||
telemetry: TelemetryService | None = None
|
||||
telemetry_owner = False
|
||||
if manage_telemetry:
|
||||
interface = "gui" if command == "gui" else "cli"
|
||||
telemetry, telemetry_owner = api.initialize_shared_telemetry(
|
||||
paths,
|
||||
session_metadata={"interface": interface, "command": command},
|
||||
report_launcher_lifecycle=command == "gui",
|
||||
)
|
||||
events = api._command_telemetry_events(command, arguments, paths)
|
||||
started_at = time.monotonic()
|
||||
if telemetry is not None and events is not None:
|
||||
operation, started_event, _success_event, _failed_event = events
|
||||
if operation == "repair":
|
||||
telemetry.send_event(
|
||||
"repair_requested",
|
||||
metadata={"command": command, "reason": "user_requested"},
|
||||
context={"operation": operation, "stage": "requested"},
|
||||
)
|
||||
telemetry.send_event(
|
||||
started_event,
|
||||
metadata={"command": command},
|
||||
context={"operation": operation, "stage": "started"},
|
||||
)
|
||||
return CommandTelemetryLifecycle(
|
||||
telemetry=telemetry,
|
||||
events=events,
|
||||
started_at=started_at,
|
||||
owner=telemetry_owner,
|
||||
)
|
||||
|
||||
|
||||
def shutdown_command_telemetry(
|
||||
api: Any,
|
||||
lifecycle: CommandTelemetryLifecycle,
|
||||
) -> None:
|
||||
if lifecycle.telemetry is not None and lifecycle.owner:
|
||||
api.shutdown_shared_telemetry(lifecycle.telemetry)
|
||||
|
||||
|
||||
def command_telemetry_events(
|
||||
command: str,
|
||||
arguments: argparse.Namespace,
|
||||
paths: AppPaths,
|
||||
) -> CommandTelemetryEvents | None:
|
||||
if bool(getattr(arguments, "dry_run", False)):
|
||||
return None
|
||||
if command in {"update", "sync", "wine-update"}:
|
||||
if bool(getattr(arguments, "repair", False)):
|
||||
return ("repair", "repair_started", "repair_success", "repair_failed")
|
||||
if not paths.source_game.exists():
|
||||
return ("install", "install_started", "install_success", "install_failed")
|
||||
return ("update", "update_started", "update_success", "update_failed")
|
||||
if command == "launcher-update" and not (
|
||||
bool(getattr(arguments, "check", False))
|
||||
or bool(getattr(arguments, "verify", False))
|
||||
or getattr(arguments, "rollback", None) is not None
|
||||
):
|
||||
return (
|
||||
"launcher_update",
|
||||
"update_started",
|
||||
"update_success",
|
||||
"update_failed",
|
||||
)
|
||||
if command in {"setup", "runtime-bootstrap"}:
|
||||
return (
|
||||
"runtime_install",
|
||||
"runtime_install_started",
|
||||
"runtime_install_success",
|
||||
"runtime_install_failed",
|
||||
)
|
||||
if command == "doctor":
|
||||
return (
|
||||
"compatibility_check",
|
||||
"compatibility_check_started",
|
||||
"compatibility_check_completed",
|
||||
"compatibility_check_failed",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def report_command_result(
|
||||
telemetry: TelemetryService | None,
|
||||
events: CommandTelemetryEvents | None,
|
||||
command: str,
|
||||
started_at: float,
|
||||
returncode: int,
|
||||
) -> None:
|
||||
if telemetry is None or events is None:
|
||||
return
|
||||
operation, _started_event, success_event, failed_event = events
|
||||
duration_ms = round((time.monotonic() - started_at) * 1000)
|
||||
telemetry.send_event(
|
||||
success_event if returncode == 0 else failed_event,
|
||||
severity="info" if returncode == 0 else "error",
|
||||
message=None if returncode == 0 else f"Command exited with status {returncode}",
|
||||
metadata={
|
||||
"command": command,
|
||||
"duration_ms": duration_ms,
|
||||
"exit_code": returncode,
|
||||
},
|
||||
context={
|
||||
"operation": operation,
|
||||
"stage": "completed" if returncode == 0 else "command_exit",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def report_command_failure(
|
||||
telemetry: TelemetryService | None,
|
||||
events: CommandTelemetryEvents | None,
|
||||
command: str,
|
||||
started_at: float,
|
||||
message: str,
|
||||
*,
|
||||
stage: str = "failed",
|
||||
) -> None:
|
||||
if telemetry is None:
|
||||
return
|
||||
if command == "play":
|
||||
telemetry.send_event(
|
||||
"game_launch_failed",
|
||||
severity="error",
|
||||
message=message,
|
||||
metadata={
|
||||
"duration_ms": round((time.monotonic() - started_at) * 1000),
|
||||
},
|
||||
context={"operation": "launch_game", "stage": stage},
|
||||
)
|
||||
return
|
||||
if events is None:
|
||||
return
|
||||
operation, _started_event, _success_event, failed_event = events
|
||||
telemetry.send_event(
|
||||
failed_event,
|
||||
severity="error",
|
||||
message=message,
|
||||
metadata={
|
||||
"command": command,
|
||||
"duration_ms": round((time.monotonic() - started_at) * 1000),
|
||||
},
|
||||
context={"operation": operation, "stage": stage},
|
||||
)
|
||||
|
||||
|
||||
def report_game_launch_cancelled(
|
||||
telemetry: TelemetryService | None,
|
||||
started_at: float,
|
||||
*,
|
||||
stage: str,
|
||||
metadata: dict[str, object] | None = None,
|
||||
) -> None:
|
||||
if telemetry is None:
|
||||
return
|
||||
event_metadata: dict[str, object] = {
|
||||
"duration_ms": round((time.monotonic() - started_at) * 1000),
|
||||
"interrupted": True,
|
||||
"launcher_exit_code": 130,
|
||||
"signal": int(signal.SIGINT),
|
||||
"signal_name": signal.SIGINT.name,
|
||||
}
|
||||
if metadata is not None:
|
||||
event_metadata.update(metadata)
|
||||
telemetry.send_event(
|
||||
"game_launch_cancelled",
|
||||
severity="info",
|
||||
message="Game launch was cancelled",
|
||||
metadata=event_metadata,
|
||||
context={"operation": "launch_game", "stage": stage},
|
||||
)
|
||||
|
||||
|
||||
def failure_stage(command: str, exception: BaseException) -> str:
|
||||
if isinstance(exception, ValueError):
|
||||
return "configuration"
|
||||
if isinstance(exception, CommandError):
|
||||
return "subprocess"
|
||||
message = str(exception).casefold()
|
||||
if any(value in message for value in ("manifest", "cdn", "request failed")):
|
||||
return "manifest"
|
||||
if any(value in message for value in ("download", "incomplete response")):
|
||||
return "download"
|
||||
if any(value in message for value in ("sha-1", "checksum", "hash mismatch")):
|
||||
return "validation"
|
||||
if any(
|
||||
value in message
|
||||
for value in ("permission", "read-only", "cannot write", "writable")
|
||||
):
|
||||
return "filesystem"
|
||||
if any(value in message for value in ("prefix", "wine", "proton", "umu")):
|
||||
return "runtime_check"
|
||||
return f"{command}_operation"
|
||||
|
||||
|
||||
def emit(
|
||||
value: object,
|
||||
as_json: bool,
|
||||
*,
|
||||
json_printer: Callable[[object], None],
|
||||
) -> None:
|
||||
if as_json:
|
||||
json_printer(value)
|
||||
else:
|
||||
if isinstance(value, dict) and value.get("ok") is True:
|
||||
print("Done.")
|
||||
print(json.dumps(value, indent=2, sort_keys=True))
|
||||
|
||||
|
||||
def print_json(value: object) -> None:
|
||||
print(json.dumps(value, indent=2, sort_keys=True))
|
||||
Reference in New Issue
Block a user