blob: 7debe66d6a449c82afd6d3aa2f33e237d814bf73 [file] [log] [blame]
# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from __future__ import annotations
import abc
import functools
import logging
import re
from typing import TYPE_CHECKING, Any, Dict, Iterator, Optional
from crossbench import path as pth
from crossbench.plt.base import Environ, ListCmdArgs, Platform, SubprocessError
from crossbench.plt.remote import RemotePlatformMixin
if TYPE_CHECKING:
from crossbench.types import JsonDict
class PosixPlatform(Platform, metaclass=abc.ABCMeta):
# pylint: disable=locally-disabled, redefined-builtin
def __init__(self) -> None:
super().__init__()
self._default_tmp_dir = pth.RemotePath("")
@functools.cached_property
def version(self) -> str: #pylint: disable=invalid-overridden-method
return self.sh_stdout("uname", "-r").strip()
def _raw_machine_arch(self):
if self.is_local:
return super()._raw_machine_arch()
return self.sh_stdout("uname", "-m").strip()
def _get_cpu_cores_info(self) -> str:
try:
max_cores_file = self.path("/sys/devices/system/cpu/possible")
_, max_core = self.cat(max_cores_file).strip().split("-", maxsplit=1)
cores = int(max_core) + 1
return f"{cores} cores"
except Exception as e: # pylint: disable=broad-except
logging.debug("Failed to get detailed CPU stats: %s", e)
return ""
_GET_CPONF_PROC_RE: re.Pattern = re.compile(
r".*PROCESSORS_CONF[^0-9]+(?P<cores>[0-9]+)")
def cpu_details(self) -> Dict[str, Any]:
if self.is_local:
return super().cpu_details()
cores = -1
if self.which("nproc"):
cores = int(self.sh_stdout("nproc"))
elif self.which("getconf"):
result = self._GET_CPONF_PROC_RE.search(self.sh_stdout("getconf", "-a"))
if result:
cores = int(result["cores"])
return {
"physical cores": cores,
"info": self.cpu,
}
def os_details(self) -> JsonDict:
if self.is_local:
return super().os_details()
return {
"system": self.sh_stdout("uname").strip(),
"release": self.sh_stdout("uname", "-r").strip(),
"version": self.sh_stdout("uname", "-v").strip(),
"platform": self.sh_stdout("uname", "-a").strip(),
}
_PY_VERSION: str = "import sys; print(64 if sys.maxsize > 2**32 else 32)"
def python_details(self) -> JsonDict:
if self.is_local:
return super().python_details()
if not self.which("python3"):
return {"version": "unknown", "bits": 64}
return {
"version": self.sh_stdout("python3", "--version").strip(),
"bits": int(self.sh_stdout("python3", "-c", self._PY_VERSION).strip())
}
def app_version(self, app_or_bin: pth.RemotePathLike) -> str:
app_or_bin = self.path(app_or_bin)
assert self.exists(app_or_bin), f"Binary {app_or_bin} does not exist."
return self.sh_stdout(app_or_bin, "--version")
@property
def default_tmp_dir(self) -> pth.RemotePath:
if self._default_tmp_dir.parts:
return self._default_tmp_dir
if self.is_local:
self._default_tmp_dir = self.path(super().default_tmp_dir)
return self._default_tmp_dir
env = self.environ
for tmp_var in ("TMPDIR", "TEMP", "TMP"):
if tmp_var not in env:
continue
tmp_path = self.path(env[tmp_var])
if self.is_dir(tmp_path):
self._default_tmp_dir = tmp_path
return tmp_path
self._default_tmp_dir = self.path("/tmp")
assert self.is_dir(self._default_tmp_dir), (
f"Fallback tmp dir does not exist: {self._default_tmp_dir}")
return self._default_tmp_dir
def path(self, path: pth.RemotePathLike) -> pth.RemotePath:
if self.is_local:
return pth.LocalPosixPath(path)
return pth.RemotePosixPath(path)
def which(self, binary_name: pth.RemotePathLike) -> Optional[pth.RemotePath]:
if self.is_local:
return super().which(binary_name)
if not binary_name:
raise ValueError("Got empty path")
if override := self.lookup_binary_override(str(binary_name)):
return override
try:
if maybe_path := self.sh_stdout("which", self.path(binary_name)).strip():
maybe_bin = self.path(maybe_path)
if self.exists(maybe_bin):
return maybe_bin
except SubprocessError:
pass
return None
def cat(self, file: pth.RemotePathLike, encoding: str = "utf-8") -> str:
if self.is_local:
return super().cat(file, encoding)
return self.sh_stdout("cat", self.path(file), encoding=encoding)
def rm(self,
path: pth.RemotePathLike,
dir: bool = False,
missing_ok: bool = False) -> None:
if self.is_local:
super().rm(path, dir, missing_ok)
return
if missing_ok and not self.exists(path):
return
if dir:
self.sh("rm", "-rf", self.path(path))
else:
self.sh("rm", self.path(path))
def rename(self, src_path: pth.RemotePathLike,
dst_path: pth.RemotePathLike) -> pth.RemotePath:
if self.is_local:
return super().rename(src_path, dst_path)
dst_path = self.path(dst_path)
self.sh("mv", self.path(src_path), dst_path)
return dst_path
def home(self) -> pth.RemotePath:
if self.is_local:
return super().home()
return self.path(self.sh_stdout("printenv", "HOME").strip())
def touch(self, path: pth.RemotePathLike) -> None:
if self.is_local:
super().touch(path)
else:
self.sh("touch", self.path(path))
def mkdir(self,
path: pth.RemotePathLike,
parents: bool = True,
exist_ok: bool = True) -> None:
if self.is_local:
super().mkdir(path, parents, exist_ok)
elif parents or exist_ok:
self.sh("mkdir", "-p", self.path(path))
else:
self.sh("mkdir", "-p", self.path(path))
def mkdtemp(self,
prefix: Optional[str] = None,
dir: Optional[pth.RemotePathLike] = None) -> pth.RemotePath:
if self.is_local:
return super().mkdtemp(prefix, dir)
return self._mktemp_sh(is_dir=True, prefix=prefix, dir=dir)
def mktemp(self,
prefix: Optional[str] = None,
dir: Optional[pth.RemotePathLike] = None) -> pth.RemotePath:
if self.is_local:
return super().mktemp(prefix, dir)
return self._mktemp_sh(is_dir=False, prefix=prefix, dir=dir)
def _mktemp_sh(self, is_dir: bool, prefix: Optional[str],
dir: Optional[pth.RemotePathLike]) -> pth.RemotePath:
if not dir:
dir = self.default_tmp_dir
template = self.path(dir) / f"{prefix}.XXXXXXXXXXX"
args: ListCmdArgs = ["mktemp"]
if is_dir:
args.append("-d")
args.append(str(template))
result = self.sh_stdout(*args)
return self.path(result.strip())
def exists(self, path: pth.RemotePathLike) -> bool:
if self.is_local:
return super().exists(path)
return self.sh("[", "-e", self.path(path), "]", check=False).returncode == 0
def is_file(self, path: pth.RemotePathLike) -> bool:
if self.is_local:
return super().is_file(path)
return self.sh("[", "-f", self.path(path), "]", check=False).returncode == 0
def is_dir(self, path: pth.RemotePathLike) -> bool:
if self.is_local:
return super().is_dir(path)
return self.sh("[", "-d", self.path(path), "]", check=False).returncode == 0
def terminate(self, proc_pid: int) -> None:
self.sh("kill", "-s", "TERM", str(proc_pid))
def process_info(self, pid: int) -> Optional[Dict[str, Any]]:
if self.is_local:
return super().process_info(pid)
try:
lines = self.sh_stdout("ps", "-o", "comm", "-p", str(pid)).splitlines()
if len(lines) <= 1:
return None
assert len(lines) == 2, lines
tokens = lines[1].split()
assert len(tokens) == 1
return {"comm": tokens[0]}
except SubprocessError:
return None
@property
def environ(self) -> Environ:
if self.is_local:
return super().environ
return RemotePosixEnviron(self)
class RemotePosixEnviron(Environ):
def __init__(self, platform: PosixPlatform) -> None:
self._platform = platform
self._environ = {}
for line in self._platform.sh_stdout("env").splitlines():
parts = line.split("=", maxsplit=1)
if len(parts) == 2:
key, value = parts
self._environ[key] = value
else:
assert len(parts) == 1
key = parts[0]
self._environ[key] = ""
def __getitem__(self, key: str) -> str:
return self._environ.__getitem__(key)
def __setitem__(self, key: str, item: str) -> None:
raise NotImplementedError("Unsupported")
def __delitem__(self, key: str) -> None:
raise NotImplementedError("Unsupported")
def __iter__(self) -> Iterator[str]:
return self._environ.__iter__()
def __len__(self) -> int:
return self._environ.__len__()
class RemotePosixPlatform(RemotePlatformMixin, PosixPlatform):
pass