diff --git a/requirements.txt b/requirements.txt index bf326d6..9698fff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,6 @@ idna==3.7 jsonschema==3.2.0 packaging==21.3 pexpect==4.8.0 -psutil==5.8.0 ptyprocess==0.7.0 pyparsing==3.0.9 PyYAML==6.0.1 diff --git a/scale_build/utils/system.py b/scale_build/utils/system.py index 0b4c9a9..b3e047e 100644 --- a/scale_build/utils/system.py +++ b/scale_build/utils/system.py @@ -1,8 +1,12 @@ -import psutil - - -REQUIRED_RAM = 16 # GB +from functools import cache + +REQUIRED_RAM_GB = 16 * (1024 ** 3) + +__all__ = ("has_low_ram",) +@cache def has_low_ram(): - return psutil.virtual_memory().total < REQUIRED_RAM * 1024 * 1024 * 1024 + with open('/proc/meminfo') as f: + for line in filter(lambda x: 'MemTotal' in x, f): + return int(line.split()[1]) * 1024 < REQUIRED_RAM_GB diff --git a/setup.py b/setup.py index 4fd10c2..4a0dd09 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,6 @@ setup( install_requires=[ 'coloredlogs', 'toposort', - 'psutil', 'requests', 'pyyaml' ], diff --git a/truenas_install/__main__.py b/truenas_install/__main__.py index 8a7a35b..dbf501d 100644 --- a/truenas_install/__main__.py +++ b/truenas_install/__main__.py @@ -14,12 +14,11 @@ import subprocess import sys import tempfile -import psutil - from licenselib.license import ContractType, License from .dhs import TRUENAS_DATA_HIERARCHY from .fhs import TRUENAS_DATASETS +from .utils import getmntinfo, get_pids logger = logging.getLogger(__name__) @@ -185,23 +184,20 @@ def precheck(old_root): pass processes = defaultdict(list) - for p in psutil.process_iter(): - processes[p.name()].append(p.pid) + for p in get_pids(): + processes[p.name].append(p) running_services = [] for service, title, process_name, cmdline in services: if process_name in processes: # If we report an enabled service, we don't want to report the same service running. if title not in enabled_services: - for pid in processes[process_name]: + for proc in processes[process_name]: if cmdline is not None: - try: - if cmdline not in psutil.Process(pid).cmdline(): - continue - except psutil.NoSuchProcess: + if cmdline not in proc.cmdline.decode(errors="ignore"): continue try: - with open(f"/proc/{pid}/cgroup") as f: + with open(f"/proc/{proc.pid}/cgroup") as f: cgroups = f.read().strip() except FileNotFoundError: cgroups = "" @@ -276,10 +272,10 @@ def main(): old_root_dataset = None if old_root is not None: - try: - old_root_dataset = next(p for p in psutil.disk_partitions() if p.mountpoint == old_root).device - except StopIteration: - pass + for i in getmntinfo(): + if i.mountpoint == old_root: + old_root_dataset == i.mount_source + break write_progress(0, "Creating dataset") if input.get("dataset_name"): diff --git a/truenas_install/utils.py b/truenas_install/utils.py new file mode 100644 index 0000000..0ad7721 --- /dev/null +++ b/truenas_install/utils.py @@ -0,0 +1,76 @@ +from collections.abc import Generator +from dataclasses import dataclass +from functools import cached_property +from os import makedev, scandir + +__all__ = ("get_pids", "getmntinfo",) + + +@dataclass(frozen=True, kw_only=True) +class PidEntry: + cmdline: bytes + pid: int + + @cached_property + def name(self) -> bytes: + """The name of process as described in man 2 PR_SET_NAME""" + with open(f"/proc/{self.pid}/status", "rb") as f: + # first line in this file is name of process + # and this is in procfs, which is considered + # part of linux's ABI and is stable + return f.readline().split(b"\t", 1)[-1].strip() + + +@dataclass(slots=True, frozen=True, kw_only=True) +class DevIdEntry: + major: int + minor: int + dev_t: int + + +@dataclass(slots=True, frozen=True, kw_only=True) +class MntEntry: + mount_id: int + parent_id: int + device_id: DevIdEntry + root: str + mountpoint: str + mount_opts: list[str] + fs_type: str + mount_source: str + super_opts: list[str] + + +def get_pids() -> Generator[PidEntry] | None: + """Get the currently running processes on the OS""" + with scandir("/proc/") as sdir: + for i in filter(lambda x: x.name.isdigit(), sdir): + try: + with open(f"{i.path}/cmdline", "rb") as f: + cmdline = f.read().replace(b"\x00", b" ") + yield PidEntry(cmdline=cmdline, pid=int(i.name)) + except FileNotFoundError: + # process could have gone away + pass + + +def getmntinfo() -> Generator[MntEntry]: + with open('/proc/self/mountinfo') as f: + for line in f: + mnt_id, parent_id, maj_min, root, mp, opts, extra = line.split(" ", 6) + fstype, mnt_src, super_opts = extra.split(' - ')[1].split() + major, minor = maj_min.split(':') + major, minor = int(major), int(minor) + devid = makedev(major, minor) + deventry = DevIdEntry(major=major, minor=minor, dev_t=devid) + yield MntEntry(**{ + 'mount_id': int(mnt_id), + 'parent_id': int(parent_id), + 'device_id': deventry, + 'root': root.replace('\\040', ' '), + 'mountpoint': mp.replace('\\040', ' '), + 'mount_opts': opts.upper().split(','), + 'fs_type': fstype, + 'mount_source': mnt_src.replace('\\040', ' '), + 'super_opts': super_opts.upper().split(','), + })