Files
scheduler/backend/scanner.py
T
2026-06-16 15:07:59 -04:00

263 lines
7.5 KiB
Python

import os
import re
import threading
try:
import smbclient
except ModuleNotFoundError:
smbclient = None
from db import DEVICE_DUT, DEVICE_REF, mark_tests_completed
_SMB_SESSIONS = set()
_SCAN_STATE_LOCK = threading.Lock()
_ACTIVE_SCAN_COUNT = 0
_RESULT_TEST_ID_PATTERN = re.compile(r"(?:COE|P2P|P3P)(?:RX|TX)?[A-Z]{2}\d{3}", re.IGNORECASE)
def _extract_test_id_from_result_dir_name(dir_name):
if not dir_name:
return None
match = _RESULT_TEST_ID_PATTERN.search(str(dir_name).upper())
if match:
return match.group(0)
return None
def _build_match_tokens(parsed):
tokens = set()
def _add(value):
if value in (None, ""):
return
tokens.add(str(value).upper())
_add(parsed.get("interference"))
_add(parsed.get("device"))
_add(parsed.get("rotation"))
_add(parsed.get("test_point"))
_add(parsed.get("rssi"))
_add(parsed.get("station"))
_add(parsed.get("band"))
_add(parsed.get("channel"))
_add(parsed.get("bandwidth"))
_add(parsed.get("direction"))
_add(parsed.get("throttled"))
_add(parsed.get("test_id"))
for bw in parsed.get("extra_bandwidths") or []:
_add(bw)
# Add numeric alias for devices, e.g. CGW453 -> 453.
device = (parsed.get("device") or "").upper()
device_digits = re.sub(r"\D", "", device)
if device_digits:
tokens.add(device_digits)
# SP can appear as flag or specific token variant (e.g., SP40).
if parsed.get("sp"):
tokens.add("SP")
tokens.add(str(parsed.get("sp")).upper())
return tokens
def _rule_matches_target(rule, parsed_tokens):
# Rule parts are AND-ed: CGW453_P3P_ROT2 => device AND interference AND rotation.
# Support _, -, or spaces as condition separators.
parts = [p for p in re.split(r"[_\-\s]+", rule) if p]
if not parts:
return False
def _part_matches(part):
if part in parsed_tokens:
return True
# Support tag variants (e.g., BW80 should match BW80M/BW80+80 in source names).
# Keep this conservative for very short parts to avoid overmatching.
if len(part) >= 3:
for token in parsed_tokens:
if token.startswith(part) or part in token:
return True
return False
return all(_part_matches(part) for part in parts)
def _should_exclude_target(parsed, exclusions):
parsed_tokens = _build_match_tokens(parsed)
# Any matching rule excludes the testcase.
return any(_rule_matches_target(rule, parsed_tokens) for rule in exclusions)
def _scan_started():
global _ACTIVE_SCAN_COUNT
with _SCAN_STATE_LOCK:
_ACTIVE_SCAN_COUNT += 1
def _scan_finished():
global _ACTIVE_SCAN_COUNT
with _SCAN_STATE_LOCK:
_ACTIVE_SCAN_COUNT = max(0, _ACTIVE_SCAN_COUNT - 1)
def is_scan_in_progress():
with _SCAN_STATE_LOCK:
return _ACTIVE_SCAN_COUNT > 0
def resolve_runtime_path(path_value):
if not path_value:
return path_value
raw_path = str(path_value).strip()
if not raw_path:
return raw_path
# If the path is already valid in the current runtime, keep it.
if os.path.exists(raw_path):
return raw_path
# UNC/network paths are handled separately via smbclient.
if raw_path.startswith("\\\\") or raw_path.startswith("//"):
return raw_path
# Map host paths (Windows or Linux) to the container mount point when running in a container.
if os.name != "nt":
mount_root = os.getenv("HOST_MOUNT_ROOT", "/host").strip() or "/host"
host_root = os.getenv("HOST_BROWSE_ROOT", "").strip()
raw_norm = raw_path.replace("\\", "/")
if host_root:
host_norm = host_root.replace("\\", "/").rstrip("/")
if raw_norm.lower() == host_norm.lower() or raw_norm.lower().startswith(host_norm.lower() + "/"):
relative = raw_norm[len(host_norm):].lstrip("/")
if relative:
return os.path.join(mount_root, *relative.split("/"))
return mount_root
return raw_path
def _normalize_input_path(path_value):
if not path_value:
return path_value
path = resolve_runtime_path(path_value)
path = str(path).strip()
# Accept //server/share style and normalize to UNC for smbclient.
if path.startswith("//"):
return "\\\\" + path.lstrip("/").replace("/", "\\")
# Accept /<ipv4>/<share>/... and normalize to UNC for Linux-hosted inputs.
if re.match(r"^/\d{1,3}(?:\.\d{1,3}){3}/[^/]+", path):
return "\\\\" + path.lstrip("/").replace("/", "\\")
return path
def _is_unc_path(path):
return isinstance(path, str) and path.startswith("\\\\")
def _extract_unc_server(path):
if not _is_unc_path(path):
return None
rest = path[2:]
return rest.split("\\", 1)[0] if rest else None
def _register_smb_session_if_needed(path):
if not _is_unc_path(path):
return
if smbclient is None:
raise ModuleNotFoundError("smbclient is required to scan UNC result paths")
server = _extract_unc_server(path)
if not server or server in _SMB_SESSIONS:
return
username = os.getenv("SMB_USERNAME", "").strip()
password = os.getenv("SMB_PASSWORD", "")
domain = os.getenv("SMB_DOMAIN", "").strip()
if username and domain and "\\" not in username and "@" not in username:
username = f"{domain}\\{username}"
if username:
smbclient.register_session(server, username=username, password=password)
else:
smbclient.register_session(server)
_SMB_SESSIONS.add(server)
def _iter_dir_entries(path):
path = _normalize_input_path(path)
if _is_unc_path(path):
if smbclient is None:
raise ModuleNotFoundError("smbclient is required to scan UNC result paths")
_register_smb_session_if_needed(path)
return list(smbclient.scandir(path))
return list(os.scandir(path))
def scan_results(results_dir_dut, results_dir_ref):
results_dir_dut = _normalize_input_path(results_dir_dut)
results_dir_ref = _normalize_input_path(results_dir_ref)
if not results_dir_dut or not results_dir_ref:
return
try:
dut_entries = [
entry.name
for entry in _iter_dir_entries(results_dir_dut)
if entry.is_dir()
]
except OSError as exc:
print(f"[scanner] Cannot read results dir: {exc}")
return
print(f"[scanner] results: {len(dut_entries)} result dir(s) found in DUT results dir")
try:
ref_entries = [
entry.name
for entry in _iter_dir_entries(results_dir_ref)
if entry.is_dir()
]
except OSError as exc:
print(f"[scanner] Cannot read results dir: {exc}")
return
print(f"[scanner] results: {len(ref_entries)} result dir(s) found in reference results dir")
completed_batch = []
unmatched_entries = []
for entry_name in dut_entries:
test_id = _extract_test_id_from_result_dir_name(entry_name)
if test_id:
completed_batch.append((test_id, DEVICE_DUT))
else:
unmatched_entries.append(entry_name)
for entry_name in ref_entries:
test_id = _extract_test_id_from_result_dir_name(entry_name)
if test_id:
completed_batch.append((test_id, DEVICE_REF))
else:
unmatched_entries.append(entry_name)
if unmatched_entries:
print(f"[scanner] skipped {len(unmatched_entries)} result dir(s) with no recognizable test id")
mark_tests_completed(completed_batch)