Lab056: compare 20 and 500 ksymbol/s in controlled RF session

This commit is contained in:
LittleSam129
2026-09-10 13:58:11 +03:00
parent 5add95ecb8
commit 92477a4632
27 changed files with 6518 additions and 0 deletions

View File

@@ -0,0 +1,178 @@
"""Проверка конфликтующих SDR-процессов без обращения к оборудованию."""
from __future__ import annotations
import csv
from dataclasses import asdict, dataclass
from enum import Enum
import importlib
import io
import json
from pathlib import PureWindowsPath
import subprocess
from typing import Callable, Iterable
class ProcessState(str, Enum):
CLEAR = "CLEAR"
BUSY = "BUSY"
UNKNOWN = "UNKNOWN"
@dataclass(frozen=True)
class ProcessIdentity:
pid: int
name: str
@dataclass(frozen=True)
class ProcessScan:
state: ProcessState
mechanism: str
matches: tuple[ProcessIdentity, ...] = ()
inspected: int = 0
errors: tuple[str, ...] = ()
@dataclass(frozen=True)
class ProcessCheck:
state: ProcessState
attempts: tuple[ProcessScan, ...]
def to_dict(self) -> dict:
return asdict(self)
# Эти программы способны открыть SDR. Их наличие консервативно даёт BUSY,
# даже если конкретный экземпляр пока не открыл устройство. Это проверка имён,
# а не доказательство отсутствия произвольного переименованного USB-клиента.
SDR_PROCESS_NAMES = frozenset({
"gqrx", "sdrangel", "sdrsharp", "sdrpp", "sdr++", "cubicsdr", "hdsdr",
"sdrconnect", "gnuradio-companion", "osmocom_fft", "osmocom_siggen",
"soapysdrserver", "soapyremote", "soapy_sdr_server", "urh",
})
def is_conflicting(name: str) -> bool:
normalized = PureWindowsPath(name).name.casefold()
if normalized.endswith(".exe"):
normalized = normalized[:-4]
return normalized.startswith("hackrf_") or normalized in SDR_PROCESS_NAMES
def classify_snapshot(mechanism: str, rows: Iterable[ProcessIdentity],
errors: Iterable[str] = ()) -> ProcessScan:
"""Пустое, неполное или неразобранное перечисление не означает CLEAR."""
failures = list(errors)
matches = []
count = 0
try:
for row in rows:
if (not isinstance(row, ProcessIdentity) or type(row.pid) is not int or row.pid < 0
or not isinstance(row.name, str) or not row.name.strip()):
failures.append("Некорректная запись процесса")
continue
count += 1
if is_conflicting(row.name):
matches.append(row)
except Exception as error:
failures.append(f"{type(error).__name__}: {error}")
if count == 0:
failures.append("Не получено ни одного процесса")
state = ProcessState.BUSY if matches else (ProcessState.UNKNOWN if failures else ProcessState.CLEAR)
return ProcessScan(state, mechanism, tuple(matches), count, tuple(failures))
def scan_psutil() -> ProcessScan:
"""NoSuchProcess — обычное завершение; отказ доступа сохраняет UNKNOWN."""
rows, errors = [], []
try:
psutil = importlib.import_module("psutil")
clear_cache = getattr(psutil.process_iter, "cache_clear", None)
if clear_cache is not None:
clear_cache()
for process in psutil.process_iter():
try:
rows.append(ProcessIdentity(process.pid, process.name()))
except psutil.NoSuchProcess:
continue
except Exception as error:
errors.append(f"pid={process.pid}: {type(error).__name__}: {error}")
except Exception as error:
errors.append(f"{type(error).__name__}: {error}")
return classify_snapshot("psutil", rows, errors)
def run_command(command: list[str]) -> subprocess.CompletedProcess:
return subprocess.run(command, capture_output=True, text=True, encoding="utf-8",
errors="strict", timeout=10,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
def scan_powershell(runner: Callable = run_command) -> ProcessScan:
"""Get-Process завершает перечисление целиком или возвращает ошибку."""
script = ("$ErrorActionPreference='Stop'; "
"[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new(); "
"try { $items=@(Get-Process -ErrorAction Stop | "
"Select-Object Id,ProcessName); "
"ConvertTo-Json -InputObject $items -Compress } "
"catch { [Console]::Error.WriteLine($_.Exception.Message); exit 1 }")
rows, errors = [], []
try:
result = runner(["powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script])
if result.returncode != 0 or result.stderr.strip():
raise RuntimeError(f"exit={result.returncode}: {result.stderr.strip()}")
data = json.loads(result.stdout)
if not isinstance(data, list):
raise ValueError("Ожидался полный JSON-массив процессов")
for row in data:
try:
rows.append(ProcessIdentity(row["Id"], row["ProcessName"]))
except Exception as error:
errors.append(f"{type(error).__name__}: {error}")
except Exception as error:
errors.append(f"{type(error).__name__}: {error}")
return classify_snapshot("powershell_get_process", rows, errors)
def scan_tasklist(runner: Callable = run_command) -> ProcessScan:
"""Резервный полный CSV; текст ошибки или пустой вывод не равен CLEAR."""
rows, errors = [], []
try:
result = runner(["tasklist.exe", "/FO", "CSV", "/NH"])
if result.returncode != 0 or result.stderr.strip():
raise RuntimeError(f"exit={result.returncode}: {result.stderr.strip()}")
for row in csv.reader(io.StringIO(result.stdout), strict=True):
try:
if len(row) != 5:
raise ValueError("Некорректная строка tasklist CSV")
rows.append(ProcessIdentity(int(row[1]), row[0]))
except Exception as error:
errors.append(f"{type(error).__name__}: {error}")
except Exception as error:
errors.append(f"{type(error).__name__}: {error}")
return classify_snapshot("tasklist", rows, errors)
def check_processes(providers: Iterable[Callable[[], ProcessScan]] | None = None) -> ProcessCheck:
"""UNKNOWN разрешает другой источник; только полноценный CLEAR открывает gate."""
attempts = []
sources = (scan_psutil, scan_powershell, scan_tasklist) if providers is None else providers
for provider in sources:
try:
scan = provider()
if not isinstance(scan, ProcessScan) or not isinstance(scan.state, ProcessState):
raise TypeError("Некорректный ответ проверки процессов")
except Exception as error:
scan = ProcessScan(ProcessState.UNKNOWN, getattr(provider, "__name__", "provider"),
errors=(f"{type(error).__name__}: {error}",))
attempts.append(scan)
if scan.state in (ProcessState.CLEAR, ProcessState.BUSY):
return ProcessCheck(scan.state, tuple(attempts))
return ProcessCheck(ProcessState.UNKNOWN, tuple(attempts))
def require_clear(result: ProcessCheck) -> None:
if not isinstance(result, ProcessCheck) or result.state is not ProcessState.CLEAR:
detail = result.to_dict() if isinstance(result, ProcessCheck) else repr(result)
raise RuntimeError(f"Проверка процессов не дала CLEAR: {detail}")