blob: 2207d1ff71f1ce1cd72a266053645a7d5f7bb6e3 [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 datetime as dt
import functools
import logging
import os
import shutil
from typing import Optional, Type
from typing_extensions import override
from crossbench import path as pth
from crossbench.plt.base import Platform
from crossbench.plt.signals import WinSignals
from crossbench.plt.version import PlatformVersion
class WinVersion(PlatformVersion):
pass
class WinPlatform(Platform):
# TODO: support remote platforms
SEARCH_PATHS = (
pth.LocalPath("."),
pth.LocalPath(os.path.expandvars("%ProgramFiles%")),
pth.LocalPath(os.path.expandvars("%ProgramFiles(x86)%")),
pth.LocalPath(os.path.expandvars("%APPDATA%")),
pth.LocalPath(os.path.expandvars("%LOCALAPPDATA%")),
)
@property
def signals(self) -> Type[WinSignals]:
return WinSignals
@property
@override
def is_win(self) -> bool:
return True
@property
@override
def name(self) -> str:
return "win"
@property
@override
def model(self) -> str:
# TODO: implement
return ""
def cmd_stdout(self, *args, **kwargs) -> str:
cmd = ["cmd", "/c", *args]
return self.sh_stdout(*cmd, **kwargs)
def powershell_stdout(self, *args, **kwargs) -> str:
cmd = ["powershell", "-c", *args]
return self.sh_stdout(*cmd, **kwargs)
@functools.cached_property
@override
def version_str(self) -> str:
return self.cmd_stdout("ver").strip()
@functools.cached_property
@override
def version(self) -> WinVersion:
return WinVersion.parse(self.version_str)
@functools.cached_property
@override
def cpu(self) -> str:
return self.powershell_stdout(
"Get-CIMInstance -query 'select * from Win32_Processor' | ft Name"
).strip().splitlines()[2].strip()
@functools.lru_cache(maxsize=1)
@override
def _raw_machine_arch(self) -> str:
self.assert_is_local()
# The method in base class doesn't always give the correct answer,
# because it uses py_platform.machine, which give the architecture of
# the Python binary. It is possible to run x64 Python on ARM Windows.
cpu_caption = self.powershell_stdout(
"Get-CIMInstance -query 'select * from Win32_Processor' | ft Caption"
).strip().splitlines()[2].strip().lower()
if cpu_caption.startswith("arm"):
return "arm64" if "64-bit" in cpu_caption else "arm"
return super()._raw_machine_arch()
@override
def uptime(self) -> dt.timedelta:
"""Parse powershell last boot time time-span into a timedelta object.
Example Output:
Days : 14
Hours : 2
Minutes : 19
Seconds : 54
Milliseconds : 978
Ticks : 12179949789862
TotalDays : 14.0971641086366
TotalHours : 338.331938607278
TotalMinutes : 20299.9163164367
TotalSeconds : 1217994.9789862
TotalMilliseconds : 1217994978.9862
"""
uptime_output = self.powershell_stdout(
"(New-TimeSpan -Start ("
"Get-CimInstance Win32_OperatingSystem).LastBootUpTime"
")")
results = {}
for line in uptime_output.splitlines():
line = line.strip()
if not line:
continue
unit, value = line.split(":", maxsplit=1)
unit = unit.strip()
value_f: float = float(value)
results[unit] = value_f
return dt.timedelta(
days=results["Days"],
hours=results["Hours"],
minutes=results["Minutes"],
seconds=results["Seconds"],
milliseconds=results["Milliseconds"])
@override
def search_binary(self, app_or_bin: pth.AnyPathLike) -> Optional[pth.AnyPath]:
self.assert_is_local()
app_or_bin_path: pth.AnyPath = self.path(app_or_bin)
if not app_or_bin_path.parts:
raise ValueError("Got empty path")
if app_or_bin_path.suffix.lower() not in (".exe", ".bat"):
raise ValueError("Expected executable path with '.exe' or '.bat' suffix, "
f"but got: '{app_or_bin_path.name}'")
if result_path := self.which(app_or_bin):
assert self.exists(result_path), f"{result_path} does not exist."
return result_path
for path in self.SEARCH_PATHS:
# Recreate Path object for easier pyfakefs testing
result_path = self.path(path) / app_or_bin
if self.exists(result_path):
return result_path
return None
@override
def app_version(self, app_or_bin: pth.AnyPathLike) -> str:
app_or_bin = self.path(app_or_bin)
if not self.exists(app_or_bin):
raise ValueError(f"Binary {app_or_bin} does not exist.")
if version := self.sh_stdout(
"powershell", "-command",
f"(Get-Item '{app_or_bin}').VersionInfo.ProductVersion").strip():
name = self.sh_stdout(
"powershell", "-command",
f"(Get-Item '{app_or_bin}').VersionInfo.ProductName").strip()
return f"{name} {version}"
try:
# Fall back to command-line tools.
if version := self.sh_stdout(app_or_bin, "--version").strip():
return version
except Exception as e: # noqa: BLE001
logging.debug("Failed to extract binary tool version: %s", e)
raise ValueError(f"Could not extract version for {app_or_bin}")
@override
def symlink_or_copy(self, src: pth.AnyPathLike,
dst: pth.AnyPathLike) -> pth.AnyPath:
"""Windows does not support symlinking without admin support.
Copy files on windows but symlink everywhere else (see base Platform)."""
self.assert_is_local()
dst_path = self.path(dst)
shutil.copy(os.fspath(self.path(src)), os.fspath(dst_path))
return dst_path