Added more folders and restructured classes inside them.
This commit is contained in:
267
src/ptlinux/telemetry/environment.py
Normal file
267
src/ptlinux/telemetry/environment.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""Static host, display, filesystem, and process probes for telemetry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import re
|
||||
import resource
|
||||
import sys
|
||||
|
||||
from ..paths import AppPaths
|
||||
from ..system_identity import (
|
||||
SystemIdentity,
|
||||
detect_system_identity,
|
||||
read_os_release as read_system_os_release,
|
||||
)
|
||||
|
||||
|
||||
NETWORK_FILESYSTEMS = frozenset(
|
||||
{"9p", "afs", "ceph", "cifs", "nfs", "nfs4", "smb3", "sshfs"}
|
||||
)
|
||||
MOUNT_ESCAPE_PATTERN = re.compile(r"\\([0-7]{3})")
|
||||
|
||||
|
||||
def operating_system_info(
|
||||
identity: SystemIdentity | None = None,
|
||||
*,
|
||||
detect_identity: Callable[[], SystemIdentity] = detect_system_identity,
|
||||
system_name: Callable[[], str] = platform.system,
|
||||
kernel_release: Callable[[], str] = platform.release,
|
||||
machine_name: Callable[[], str] = platform.machine,
|
||||
libc_version: Callable[[], tuple[str, str]] = platform.libc_ver,
|
||||
) -> dict[str, object]:
|
||||
identity = detect_identity() if identity is None else identity
|
||||
release = identity.operating_system_release
|
||||
return {
|
||||
"name": release.get("NAME", system_name() or "unknown"),
|
||||
"id": release.get("ID", "unknown"),
|
||||
"version": release.get("PRETTY_NAME", release.get("VERSION", "unknown")),
|
||||
"version_id": release.get("VERSION_ID", "unknown"),
|
||||
"kernel": kernel_release(),
|
||||
"architecture": machine_name() or "unknown",
|
||||
"machine_architecture": machine_name() or "unknown",
|
||||
"glibc_version": libc_version()[1] or None,
|
||||
"identity_source": (
|
||||
"container_host_os_release"
|
||||
if identity.host_release is not None
|
||||
else "execution_os_release"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def read_os_release(
|
||||
path: Path = Path("/etc/os-release"),
|
||||
*,
|
||||
reader: Callable[[Path], dict[str, str]] = read_system_os_release,
|
||||
) -> dict[str, str]:
|
||||
return reader(path)
|
||||
|
||||
|
||||
def execution_environment_info(
|
||||
identity: SystemIdentity,
|
||||
*,
|
||||
libc_version: Callable[[], tuple[str, str]] = platform.libc_ver,
|
||||
) -> dict[str, object] | None:
|
||||
if not identity.running_inside_steam_runtime:
|
||||
return None
|
||||
release = identity.execution_release
|
||||
return {
|
||||
"containerized": True,
|
||||
"kind": "steam_runtime",
|
||||
"id": release.get("ID", "steamrt"),
|
||||
"name": release.get("NAME", "Steam Runtime"),
|
||||
"version": release.get(
|
||||
"PRETTY_NAME",
|
||||
release.get("VERSION", "unknown"),
|
||||
),
|
||||
"version_id": release.get("VERSION_ID", "unknown"),
|
||||
"host_os_release_available": identity.host_release is not None,
|
||||
"launcher_inside_game_runtime": True,
|
||||
"glibc_version": libc_version()[1] or None,
|
||||
}
|
||||
|
||||
|
||||
def display_info(environment: Mapping[str, str]) -> dict[str, object]:
|
||||
session_type = environment.get("XDG_SESSION_TYPE", "").casefold()
|
||||
if session_type not in {"wayland", "x11"}:
|
||||
if environment.get("WAYLAND_DISPLAY"):
|
||||
session_type = "wayland"
|
||||
elif environment.get("DISPLAY"):
|
||||
session_type = "x11"
|
||||
else:
|
||||
session_type = "unknown"
|
||||
desktop = environment.get("XDG_CURRENT_DESKTOP", "")
|
||||
desktop = desktop.split(":", 1)[0].strip().casefold() or "unknown"
|
||||
session = environment.get("DESKTOP_SESSION", "").strip().casefold() or "unknown"
|
||||
return {
|
||||
"server": session_type,
|
||||
"desktop": desktop[:64],
|
||||
"session": session[:64],
|
||||
}
|
||||
|
||||
|
||||
Mount = tuple[Path, str, set[str]]
|
||||
|
||||
|
||||
def filesystem_info(
|
||||
target: Path,
|
||||
*,
|
||||
read_mounts: Callable[[], list[Mount]] | None = None,
|
||||
access: Callable[[Path, int], bool] = os.access,
|
||||
writable_mode: int = os.W_OK,
|
||||
network_filesystems: frozenset[str] = NETWORK_FILESYSTEMS,
|
||||
) -> dict[str, object]:
|
||||
existing = target
|
||||
while not existing.exists() and existing != existing.parent:
|
||||
existing = existing.parent
|
||||
filesystem_type = "unknown"
|
||||
readonly = False
|
||||
noexec = False
|
||||
try:
|
||||
mounts = read_mountinfo() if read_mounts is None else read_mounts()
|
||||
resolved = existing.resolve(strict=True)
|
||||
matches = [
|
||||
mount
|
||||
for mount in mounts
|
||||
if resolved == mount[0] or resolved.is_relative_to(mount[0])
|
||||
]
|
||||
if matches:
|
||||
_mountpoint, filesystem_type, options = max(
|
||||
matches,
|
||||
key=lambda item: len(item[0].parts),
|
||||
)
|
||||
readonly = "ro" in options
|
||||
noexec = "noexec" in options
|
||||
except (OSError, RuntimeError, ValueError):
|
||||
pass
|
||||
try:
|
||||
writable = access(existing, writable_mode) and not readonly
|
||||
except OSError:
|
||||
writable = False
|
||||
return {
|
||||
"type": filesystem_type,
|
||||
"writable": writable,
|
||||
"readonly": readonly,
|
||||
"noexec": noexec,
|
||||
"network": filesystem_type.casefold() in network_filesystems,
|
||||
}
|
||||
|
||||
|
||||
def read_mountinfo(
|
||||
path: Path = Path("/proc/self/mountinfo"),
|
||||
*,
|
||||
unescape: Callable[[str], str] | None = None,
|
||||
) -> list[Mount]:
|
||||
decode = unescape_mount_field if unescape is None else unescape
|
||||
mounts: list[Mount] = []
|
||||
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
fields = line.split()
|
||||
try:
|
||||
separator = fields.index("-")
|
||||
except ValueError:
|
||||
continue
|
||||
if separator < 6 or len(fields) <= separator + 3:
|
||||
continue
|
||||
mountpoint = Path(decode(fields[4]))
|
||||
filesystem_type = fields[separator + 1]
|
||||
options = set(fields[5].split(",")) | set(fields[separator + 3].split(","))
|
||||
mounts.append((mountpoint, filesystem_type, options))
|
||||
return mounts
|
||||
|
||||
|
||||
def unescape_mount_field(value: str) -> str:
|
||||
return MOUNT_ESCAPE_PATTERN.sub(
|
||||
lambda match: chr(int(match.group(1), 8)),
|
||||
value,
|
||||
)
|
||||
|
||||
|
||||
def install_is_external(paths: AppPaths, environment: Mapping[str, str]) -> bool:
|
||||
home_value = environment.get("HOME")
|
||||
if not home_value:
|
||||
return False
|
||||
try:
|
||||
home = Path(home_value).expanduser().resolve()
|
||||
data = paths.data.resolve()
|
||||
return data != home and not data.is_relative_to(home)
|
||||
except (OSError, RuntimeError):
|
||||
return False
|
||||
|
||||
|
||||
def current_game_version(
|
||||
path: Path,
|
||||
*,
|
||||
json_loader: Callable[[str], object] = json.loads,
|
||||
) -> str | None:
|
||||
try:
|
||||
value = json_loader(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return None
|
||||
if not isinstance(value, dict):
|
||||
return None
|
||||
version = value.get("version")
|
||||
return version[:32] if isinstance(version, str) and version else None
|
||||
|
||||
|
||||
def settings_schema_version() -> int:
|
||||
try:
|
||||
from ..settings import SETTINGS_SCHEMA
|
||||
|
||||
return SETTINGS_SCHEMA
|
||||
except Exception:
|
||||
return 1
|
||||
|
||||
|
||||
def resource_usage(
|
||||
*,
|
||||
get_usage: Callable[[int], object] = resource.getrusage,
|
||||
usage_target: int = resource.RUSAGE_SELF,
|
||||
platform_name: str = sys.platform,
|
||||
is_finite: Callable[[float], bool] = math.isfinite,
|
||||
) -> dict[str, object]:
|
||||
"""Read one bounded process-usage snapshot on the Linux launcher host."""
|
||||
|
||||
try:
|
||||
usage = get_usage(usage_target)
|
||||
peak_value = float(getattr(usage, "ru_maxrss"))
|
||||
peak_divisor = 1024 * 1024 if platform_name == "darwin" else 1024
|
||||
peak_memory_mb = peak_value / peak_divisor
|
||||
cpu_time_seconds = float(getattr(usage, "ru_utime")) + float(
|
||||
getattr(usage, "ru_stime")
|
||||
)
|
||||
except (OSError, TypeError, ValueError, OverflowError):
|
||||
return {}
|
||||
if (
|
||||
not is_finite(peak_memory_mb)
|
||||
or not is_finite(cpu_time_seconds)
|
||||
or peak_memory_mb < 0
|
||||
or cpu_time_seconds < 0
|
||||
):
|
||||
return {}
|
||||
return {
|
||||
"peak_memory_mb": round(peak_memory_mb, 2),
|
||||
"cpu_time_seconds": round(cpu_time_seconds, 3),
|
||||
}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"MOUNT_ESCAPE_PATTERN",
|
||||
"Mount",
|
||||
"NETWORK_FILESYSTEMS",
|
||||
"current_game_version",
|
||||
"display_info",
|
||||
"execution_environment_info",
|
||||
"filesystem_info",
|
||||
"install_is_external",
|
||||
"operating_system_info",
|
||||
"read_mountinfo",
|
||||
"read_os_release",
|
||||
"resource_usage",
|
||||
"settings_schema_version",
|
||||
"unescape_mount_field",
|
||||
]
|
||||
Reference in New Issue
Block a user