268 lines
7.2 KiB
Python
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",
|
|
]
|