#!/usr/bin python3 """ Obtain information about the running system, environment and GPU. """ import json import locale import os import platform import sys from subprocess import PIPE, Popen import psutil from lib.git import git from lib.gpu_stats import GPUStats, GPUInfo from lib.utils import get_backend from setup import CudaCheck class _SysInfo(): """ Obtain information about the System, Python and GPU """ def __init__(self) -> None: self._state_file = _State().state_file self._configs = _Configs().configs self._system = {"platform": platform.platform(), "system": platform.system().lower(), "machine": platform.machine(), "release": platform.release(), "processor": platform.processor(), "cpu_count": os.cpu_count()} self._python = {"implementation": platform.python_implementation(), "version": platform.python_version()} self._gpu = self._get_gpu_info() self._cuda_check = CudaCheck() @property def _encoding(self) -> str: """ str: The system preferred encoding """ return locale.getpreferredencoding() @property def _is_conda(self) -> bool: """ bool: `True` if running in a Conda environment otherwise ``False``. """ return ("conda" in sys.version.lower() or os.path.exists(os.path.join(sys.prefix, 'conda-meta'))) @property def _is_linux(self) -> bool: """ bool: `True` if running on a Linux system otherwise ``False``. """ return self._system["system"] == "linux" @property def _is_macos(self) -> bool: """ bool: `True` if running on a macOS system otherwise ``False``. """ return self._system["system"] == "darwin" @property def _is_windows(self) -> bool: """ bool: `True` if running on a Windows system otherwise ``False``. """ return self._system["system"] == "windows" @property def _is_virtual_env(self) -> bool: """ bool: `True` if running inside a virtual environment otherwise ``False``. """ if not self._is_conda: retval = (hasattr(sys, "real_prefix") or (hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix)) else: prefix = os.path.dirname(sys.prefix) retval = os.path.basename(prefix) == "envs" return retval @property def _ram_free(self) -> int: """ int: The amount of free RAM in bytes. """ return psutil.virtual_memory().free @property def _ram_total(self) -> int: """ int: The amount of total RAM in bytes. """ return psutil.virtual_memory().total @property def _ram_available(self) -> int: """ int: The amount of available RAM in bytes. """ return psutil.virtual_memory().available @property def _ram_used(self) -> int: """ int: The amount of used RAM in bytes. """ return psutil.virtual_memory().used @property def _fs_command(self) -> str: """ str: The command line command used to execute faceswap. """ return " ".join(sys.argv) @property def _installed_pip(self) -> str: """ str: The list of installed pip packages within Faceswap's scope. """ with Popen(f"{sys.executable} -m pip freeze", shell=True, stdout=PIPE) as pip: installed = pip.communicate()[0].decode(self._encoding, errors="replace").splitlines() return "\n".join(installed) @property def _installed_conda(self) -> str: """ str: The list of installed Conda packages within Faceswap's scope. """ if not self._is_conda: return "" with Popen("conda list", shell=True, stdout=PIPE, stderr=PIPE) as conda: stdout, stderr = conda.communicate() if stderr: return "Could not get package list" installed = stdout.decode(self._encoding, errors="replace").splitlines() return "\n".join(installed) @property def _conda_version(self) -> str: """ str: The installed version of Conda, or `N/A` if Conda is not installed. """ if not self._is_conda: return "N/A" with Popen("conda --version", shell=True, stdout=PIPE, stderr=PIPE) as conda: stdout, stderr = conda.communicate() if stderr: return "Conda is used, but version not found" version = stdout.decode(self._encoding, errors="replace").splitlines() return "\n".join(version) @property def _git_commits(self) -> str: """ str: The last 5 git commits for the currently running Faceswap. """ commits = git.get_commits(3) if not commits: return "Not Found" return " | ".join(commits) @property def _cuda_version(self) -> str: """ str: The installed CUDA version. """ # TODO Handle multiple CUDA installs retval = self._cuda_check.cuda_version if not retval: retval = "No global version found" if self._is_conda: retval += ". Check Conda packages for Conda Cuda" return retval @property def _cudnn_version(self) -> str: """ str: The installed cuDNN version. """ retval = self._cuda_check.cudnn_version if not retval: retval = "No global version found" if self._is_conda: retval += ". Check Conda packages for Conda cuDNN" return retval def _get_gpu_info(self) -> GPUInfo: """ Obtain GPU Stats. If an error is raised, swallow the error, and add to GPUInfo output Returns ------- :class:`~lib.gpu_stats.GPUInfo` The information on connected GPUs """ try: retval = GPUStats(log=False).sys_info except Exception as err: # pylint:disable=broad-except err_string = f"{type(err)}: {err}" retval = GPUInfo(vram=[], vram_free=[], driver="N/A", devices=[f"Error obtaining GPU Stats: '{err_string}'"], devices_active=[]) return retval def full_info(self) -> str: """ Obtain extensive system information stats, formatted into a human readable format. Returns ------- str The system information for the currently running system, formatted for output to console or a log file. """ retval = "\n============ System Information ============\n" sys_info = {"backend": get_backend(), "os_platform": self._system["platform"], "os_machine": self._system["machine"], "os_release": self._system["release"], "py_conda_version": self._conda_version, "py_implementation": self._python["implementation"], "py_version": self._python["version"], "py_command": self._fs_command, "py_virtual_env": self._is_virtual_env, "sys_cores": self._system["cpu_count"], "sys_processor": self._system["processor"], "sys_ram": self._format_ram(), "encoding": self._encoding, "git_branch": git.branch, "git_commits": self._git_commits, "gpu_cuda": self._cuda_version, "gpu_cudnn": self._cudnn_version, "gpu_driver": self._gpu.driver, "gpu_devices": ", ".join([f"GPU_{idx}: {device}" for idx, device in enumerate(self._gpu.devices)]), "gpu_vram": ", ".join( f"GPU_{idx}: {int(vram)}MB ({int(vram_free)}MB free)" for idx, (vram, vram_free) in enumerate(zip(self._gpu.vram, self._gpu.vram_free))), "gpu_devices_active": ", ".join([f"GPU_{idx}" for idx in self._gpu.devices_active])} for key in sorted(sys_info.keys()): retval += (f"{key + ':':<20} {sys_info[key]}\n") retval += "\n=============== Pip Packages ===============\n" retval += self._installed_pip if self._is_conda: retval += "\n\n============== Conda Packages ==============\n" retval += self._installed_conda retval += self._state_file retval += "\n\n================= Configs ==================" retval += self._configs return retval def _format_ram(self) -> str: """ Format the RAM stats into Megabytes to make it more readable. Returns ------- str The total, available, used and free RAM displayed in Megabytes """ retval = [] for name in ("total", "available", "used", "free"): value = getattr(self, f"_ram_{name}") value = int(value / (1024 * 1024)) retval.append(f"{name.capitalize()}: {value}MB") return ", ".join(retval) def get_sysinfo() -> str: """ Obtain extensive system information stats, formatted into a human readable format. If an error occurs obtaining the system information, then the error message is returned instead. Returns ------- str The system information for the currently running system, formatted for output to console or a log file. """ try: retval = _SysInfo().full_info() except Exception as err: # pylint:disable=broad-except retval = f"Exception occured trying to retrieve sysinfo: {str(err)}" raise return retval class _Configs(): # pylint:disable=too-few-public-methods """ Parses the config files in /faceswap/config and outputs the information stored within them in a human readable format. """ def __init__(self) -> None: self.config_dir = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "config") self.configs = self._get_configs() def _get_configs(self) -> str: """ Obtain the formatted configurations from the config folder. Returns ------- str The current configuration in the config files formatted in a human readable format """ try: config_files = [os.path.join(self.config_dir, cfile) for cfile in os.listdir(self.config_dir) if os.path.basename(cfile) == ".faceswap" or os.path.splitext(cfile)[1] == ".ini"] return self._parse_configs(config_files) except FileNotFoundError: return "" def _parse_configs(self, config_files: list[str]) -> str: """ Parse the given list of config files into a human readable format. Parameters ---------- config_files: list A list of paths to the faceswap config files Returns ------- str The current configuration in the config files formatted in a human readable format """ formatted = "" for cfile in config_files: fname = os.path.basename(cfile) ext = os.path.splitext(cfile)[1] formatted += f"\n--------- {fname} ---------\n" if ext == ".ini": formatted += self._parse_ini(cfile) elif fname == ".faceswap": formatted += self._parse_json(cfile) return formatted def _parse_ini(self, config_file: str) -> str: """ Parse an ``.ini`` formatted config file into a human readable format. Parameters ---------- config_file: str The path to the config.ini file Returns ------- str The current configuration in the config file formatted in a human readable format """ formatted = "" with open(config_file, "r", encoding="utf-8", errors="replace") as cfile: for line in cfile.readlines(): line = line.strip() if line.startswith("#") or not line: continue item = line.split("=") if len(item) == 1: formatted += f"\n{item[0].strip()}\n" else: formatted += self._format_text(item[0], item[1]) return formatted def _parse_json(self, config_file: str) -> str: """ Parse an ``.json`` formatted config file into a formatted string. Parameters ---------- config_file: str The path to the config.json file Returns ------- dict The current configuration in the config file formatted as a python dictionary """ formatted: str = "" with open(config_file, "r", encoding="utf-8", errors="replace") as cfile: conf_dict = json.load(cfile) for key in sorted(conf_dict.keys()): formatted += self._format_text(key, conf_dict[key]) return formatted @staticmethod def _format_text(key: str, value: str) -> str: """Format a key value pair into a consistently spaced string output for display. Parameters ---------- key: str The label for this display item value: str The value for this display item Returns ------- str The formatted key value pair for display """ return f"{key.strip() + ':':<25} {value.strip()}\n" class _State(): # pylint:disable=too-few-public-methods """ Parses the state file in the current model directory, if the model is training, and formats the content into a human readable format. """ def __init__(self) -> None: self._model_dir = self._get_arg("-m", "--model-dir") self._trainer = self._get_arg("-t", "--trainer") self.state_file = self._get_state_file() @property def _is_training(self) -> bool: """ bool: ``True`` if this function has been called during a training session otherwise ``False``. """ return len(sys.argv) > 1 and sys.argv[1].lower() == "train" @staticmethod def _get_arg(*args: str) -> str | None: """ Obtain the value for a given command line option from sys.argv. Returns ------- str or ``None`` The value of the given command line option, if it exists, otherwise ``None`` """ cmd = sys.argv for opt in args: if opt in cmd: return cmd[cmd.index(opt) + 1] return None def _get_state_file(self) -> str: """ Parses the model's state file and compiles the contents into a human readable string. Returns ------- str The state file formatted into a human readable format """ if not self._is_training or self._model_dir is None or self._trainer is None: return "" fname = os.path.join(self._model_dir, f"{self._trainer}_state.json") if not os.path.isfile(fname): return "" retval = "\n\n=============== State File =================\n" with open(fname, "r", encoding="utf-8", errors="replace") as sfile: retval += sfile.read() return retval sysinfo = get_sysinfo() # pylint:disable=invalid-name