Files
license-manager-sdk-python/license_manager/hardware.py
T
2026-02-27 14:29:48 +08:00

153 lines
5.2 KiB
Python

import hashlib
import json
import logging
import os
import platform
import socket
import subprocess
import uuid
from typing import List, Tuple, Dict, Optional
class HardwareProvider:
"""Abstracts hardware fingerprint collection."""
def fingerprint(self) -> Tuple[str, Dict[str, str]]:
raise NotImplementedError
class DefaultHardwareProvider(HardwareProvider):
"""
Collects host-agnostic identifiers.
Supported field names (case-insensitive):
- mac: first non-loopback MAC address (default)
- hostname: OS host name (default)
- cpu: CPU model / brand string (best-effort)
- memory: total physical memory capacity (best-effort)
"""
def __init__(self, fields: Optional[List[str]] = None):
if not fields:
self.fields = ["mac", "hostname"]
else:
self.fields = [f.strip().lower() for f in fields if f.strip()]
if not self.fields:
self.fields = ["mac", "hostname"]
def fingerprint(self) -> Tuple[str, Dict[str, str]]:
data: Dict[str, str] = {}
for field in self.fields:
if field == "hostname":
data["hostname"] = self._get_hostname()
elif field == "mac":
mac = self._get_mac()
if mac:
data["mac"] = mac
elif field == "cpu":
cpu = self._get_cpu()
if cpu:
data["cpu"] = cpu
elif field == "memory":
mem = self._get_memory()
if mem:
data["memory"] = mem
# Deterministic serialization
keys = sorted(data.keys())
builder = []
for k in keys:
val = str(data[k]).lower()
builder.append(f"{k}={val};")
full_string = "".join(builder)
hash_sum = hashlib.sha256(full_string.encode('utf-8')).hexdigest()
return hash_sum, data
def _get_hostname(self) -> str:
try:
return socket.gethostname()
except Exception:
return "unknown"
def _get_mac(self) -> str:
try:
# uuid.getnode() returns MAC of the first active interface
# or a random 48-bit number if not found (has multicast bit set)
node = uuid.getnode()
if (node >> 40) % 2 == 0: # Not a random MAC
mac = ':'.join(('%012x' % node)[i:i+2] for i in range(0, 12, 2))
return mac.lower()
return ""
except Exception:
return ""
def _get_cpu(self) -> str:
system = platform.system().lower()
try:
if system == "linux":
with open("/proc/cpuinfo", "r") as f:
for line in f:
if line.lower().startswith("model name"):
return line.split(":")[1].strip()
elif system == "darwin":
return self._cmd_output(["sysctl", "-n", "machdep.cpu.brand_string"])
elif system == "windows":
wmic_out = self._cmd_output(["wmic", "cpu", "get", "Name"])
if wmic_out and "name" not in wmic_out.lower():
return wmic_out
# Fallback
env_proc = os.getenv("PROCESSOR_IDENTIFIER")
if env_proc:
return env_proc
except Exception:
pass
return platform.processor() or platform.machine()
def _get_memory(self) -> str:
system = platform.system().lower()
try:
if system == "linux":
with open("/proc/meminfo", "r") as f:
for line in f:
if line.lower().startswith("memtotal"):
parts = line.split()
if len(parts) >= 2:
val_kb = int(parts[1])
return self._format_bytes(val_kb * 1024)
elif system == "darwin":
out = self._cmd_output(["sysctl", "-n", "hw.memsize"])
if out:
return self._format_bytes(int(out))
elif system == "windows":
out = self._cmd_output(["wmic", "computersystem", "get", "TotalPhysicalMemory"])
if out:
return self._format_bytes(int(out))
except Exception:
pass
return ""
def _cmd_output(self, cmd: List[str]) -> str:
try:
out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL, timeout=2).decode('utf-8')
lines = [line.strip() for line in out.split('\n') if line.strip()]
for line in lines:
lower_line = line.lower()
if "name" in lower_line or "totalphysicalmemory" in lower_line:
continue
return line
return ""
except Exception:
return ""
def _format_bytes(self, b: int) -> str:
kb = 1024
mb = 1024 * kb
gb = 1024 * mb
if b >= gb:
return f"{b // gb}GB"
elif b >= mb:
return f"{b // mb}MB"
elif b >= kb:
return f"{b // kb}KB"
else:
return f"{b}B"