Files
pristontale-eu-linux/src/ptlinux/telemetry/environment.py

268 lines
7.2 KiB
Python

"""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 cumulative CPU time and peak resident memory for the selected process."""
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",
]