2026-05-26 14:36:34 -04:00
|
|
|
import os
|
|
|
|
|
import re
|
2026-05-27 15:02:32 -04:00
|
|
|
import smbclient
|
2026-05-26 14:36:34 -04:00
|
|
|
|
2026-05-27 11:29:02 -04:00
|
|
|
from db_py import clear_tests, get_all_tests, mark_completed, set_coe_pair, upsert_test
|
2026-05-26 14:36:34 -04:00
|
|
|
from parser import (
|
|
|
|
|
parse_elapsed_time,
|
|
|
|
|
parse_result_filename,
|
|
|
|
|
parse_target_filename,
|
|
|
|
|
parse_timestamp,
|
|
|
|
|
parse_tput_rssi,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-05-27 15:02:32 -04:00
|
|
|
_SMB_SESSIONS = set()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_input_path(path_value):
|
|
|
|
|
if not path_value:
|
|
|
|
|
return path_value
|
|
|
|
|
|
|
|
|
|
path = str(path_value).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
|
|
|
|
|
|
|
|
|
|
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):
|
|
|
|
|
_register_smb_session_if_needed(path)
|
|
|
|
|
return list(smbclient.scandir(path))
|
|
|
|
|
return list(os.scandir(path))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _join_path(path, name):
|
|
|
|
|
if _is_unc_path(path):
|
|
|
|
|
base = path.rstrip("\\")
|
|
|
|
|
return f"{base}\\{name}"
|
|
|
|
|
return os.path.join(path, name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read_text_lines(path):
|
|
|
|
|
path = _normalize_input_path(path)
|
|
|
|
|
if _is_unc_path(path):
|
|
|
|
|
_register_smb_session_if_needed(path)
|
|
|
|
|
with smbclient.open_file(path, mode="r", encoding="utf-8", errors="ignore") as fh:
|
|
|
|
|
for line in fh:
|
|
|
|
|
yield line
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
with open(path, "r", encoding="utf-8", errors="ignore") as fh:
|
|
|
|
|
for line in fh:
|
|
|
|
|
yield line
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def scan_results_only(results_dir, results_dir_ref=None):
|
|
|
|
|
"""Re-scan only the results directories without clearing or re-scanning targets."""
|
|
|
|
|
results_dir = _normalize_input_path(results_dir)
|
|
|
|
|
results_dir_ref = _normalize_input_path(results_dir_ref)
|
|
|
|
|
|
|
|
|
|
if not results_dir:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print(f"[scanner] results-only scan dir : {results_dir}")
|
|
|
|
|
if results_dir_ref:
|
|
|
|
|
print(f"[scanner] results-only scan dir ref : {results_dir_ref}")
|
|
|
|
|
|
|
|
|
|
scan_results(results_dir)
|
|
|
|
|
if results_dir_ref:
|
|
|
|
|
scan_results(results_dir_ref)
|
|
|
|
|
update_p2p_coe_pairs()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def full_scan(target_dir, results_dir, results_dir_ref=None):
|
|
|
|
|
target_dir = _normalize_input_path(target_dir)
|
|
|
|
|
results_dir = _normalize_input_path(results_dir)
|
|
|
|
|
results_dir_ref = _normalize_input_path(results_dir_ref)
|
|
|
|
|
|
2026-05-26 14:36:34 -04:00
|
|
|
clear_tests()
|
|
|
|
|
if not target_dir or not results_dir:
|
|
|
|
|
return
|
|
|
|
|
|
2026-05-27 15:02:32 -04:00
|
|
|
print(f"[scanner] target dir : {target_dir}")
|
|
|
|
|
print(f"[scanner] results dir : {results_dir}")
|
|
|
|
|
if results_dir_ref:
|
|
|
|
|
print(f"[scanner] results dir ref : {results_dir_ref}")
|
2026-05-26 14:36:34 -04:00
|
|
|
|
|
|
|
|
scan_targets(target_dir)
|
|
|
|
|
scan_results(results_dir)
|
2026-05-27 15:02:32 -04:00
|
|
|
if results_dir_ref:
|
|
|
|
|
scan_results(results_dir_ref)
|
2026-05-27 11:29:02 -04:00
|
|
|
update_p2p_coe_pairs()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_p2p_coe_pairs():
|
|
|
|
|
pair_fields = [
|
|
|
|
|
"device",
|
|
|
|
|
"rotation",
|
|
|
|
|
"test_point",
|
|
|
|
|
"rssi",
|
|
|
|
|
"station",
|
|
|
|
|
"band",
|
|
|
|
|
"channel",
|
|
|
|
|
"bandwidth",
|
|
|
|
|
"direction",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
tests = get_all_tests()
|
|
|
|
|
coe_by_key = {}
|
|
|
|
|
|
|
|
|
|
for test in tests:
|
|
|
|
|
if test.get("interference") != "COE":
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
key = tuple(test.get(field) for field in pair_fields)
|
|
|
|
|
device = test.get("device")
|
|
|
|
|
test_id = test.get("test_id")
|
|
|
|
|
if not device or not test_id:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
coe_by_key.setdefault(key, []).append(f"{device}_{test_id}")
|
|
|
|
|
|
|
|
|
|
updated = 0
|
|
|
|
|
for test in tests:
|
|
|
|
|
if test.get("interference") != "P2P":
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
key = tuple(test.get(field) for field in pair_fields)
|
|
|
|
|
pairs = sorted(set(coe_by_key.get(key, [])))
|
|
|
|
|
set_coe_pair(test.get("id"), pairs)
|
|
|
|
|
updated += 1
|
|
|
|
|
|
|
|
|
|
print(f"[scanner] coe_pair updated for {updated} P2P test(s)")
|
2026-05-26 14:36:34 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def scan_targets(target_dir):
|
2026-05-27 15:02:32 -04:00
|
|
|
target_dir = _normalize_input_path(target_dir)
|
|
|
|
|
|
2026-05-26 14:36:34 -04:00
|
|
|
try:
|
|
|
|
|
parent_entries = [
|
2026-05-27 15:02:32 -04:00
|
|
|
entry.name
|
|
|
|
|
for entry in _iter_dir_entries(target_dir)
|
|
|
|
|
if entry.is_dir()
|
2026-05-26 14:36:34 -04:00
|
|
|
]
|
|
|
|
|
except OSError as exc:
|
|
|
|
|
print(f"[scanner] Cannot read target dir: {exc}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print(f"[scanner] subdirectories found: {len(parent_entries)}")
|
|
|
|
|
|
|
|
|
|
for parent_name in parent_entries:
|
2026-05-27 15:02:32 -04:00
|
|
|
parent_path = _join_path(target_dir, parent_name)
|
2026-05-26 14:36:34 -04:00
|
|
|
try:
|
|
|
|
|
files = [
|
2026-05-27 15:02:32 -04:00
|
|
|
entry.name
|
|
|
|
|
for entry in _iter_dir_entries(parent_path)
|
|
|
|
|
if entry.is_file()
|
|
|
|
|
and not entry.name.startswith("GLOBAL")
|
|
|
|
|
and entry.name.endswith(".ini")
|
2026-05-26 14:36:34 -04:00
|
|
|
]
|
|
|
|
|
except OSError as exc:
|
|
|
|
|
print(f"[scanner] Cannot read parent dir {parent_name}: {exc}")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
print(f"[scanner] {parent_name} -> {len(files)} target test file(s)")
|
|
|
|
|
|
|
|
|
|
for filename in files:
|
|
|
|
|
parsed = parse_target_filename(filename, parent_name)
|
|
|
|
|
if not parsed["test_id"]:
|
|
|
|
|
print(f"[scanner] skip (no test_id): {parent_name}/{filename}")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
print(
|
|
|
|
|
f"[scanner] found: {parent_name}/{filename} -> test_id={parsed['test_id']}"
|
|
|
|
|
)
|
|
|
|
|
upsert_test(
|
|
|
|
|
{
|
|
|
|
|
"id": parsed["id"],
|
|
|
|
|
"test_id": parsed["test_id"],
|
|
|
|
|
"parent_dir": parent_name,
|
|
|
|
|
"filename": filename,
|
|
|
|
|
"interference": parsed["interference"],
|
|
|
|
|
"device": parsed["device"],
|
|
|
|
|
"rotation": parsed["rotation"],
|
|
|
|
|
"test_point": parsed["test_point"],
|
|
|
|
|
"station": parsed["station"],
|
|
|
|
|
"band": parsed["band"],
|
|
|
|
|
"channel": parsed["channel"],
|
|
|
|
|
"bandwidth": parsed["bandwidth"],
|
|
|
|
|
"rssi": parsed["rssi"],
|
|
|
|
|
"direction": parsed["direction"],
|
2026-05-27 10:29:26 -04:00
|
|
|
"throttled": parsed["throttled"],
|
2026-05-26 14:36:34 -04:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def scan_results(results_dir):
|
2026-05-27 15:02:32 -04:00
|
|
|
results_dir = _normalize_input_path(results_dir)
|
|
|
|
|
|
2026-05-26 14:36:34 -04:00
|
|
|
try:
|
|
|
|
|
entries = [
|
2026-05-27 15:02:32 -04:00
|
|
|
entry.name
|
|
|
|
|
for entry in _iter_dir_entries(results_dir)
|
|
|
|
|
if entry.is_dir()
|
2026-05-26 14:36:34 -04:00
|
|
|
]
|
|
|
|
|
except OSError as exc:
|
|
|
|
|
print(f"[scanner] Cannot read results dir: {exc}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print(f"[scanner] results: {len(entries)} result dir(s) found")
|
|
|
|
|
for dir_name in entries:
|
|
|
|
|
process_result_dir(results_dir, dir_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def process_result_dir(results_dir, result_dir_name):
|
2026-05-27 15:02:32 -04:00
|
|
|
results_dir = _normalize_input_path(results_dir)
|
|
|
|
|
|
2026-05-26 14:36:34 -04:00
|
|
|
parsed = parse_result_filename(result_dir_name)
|
|
|
|
|
test_id = parsed["test_id"]
|
|
|
|
|
device = parsed["device"]
|
|
|
|
|
if not test_id or not device:
|
|
|
|
|
return
|
|
|
|
|
|
2026-05-27 15:02:32 -04:00
|
|
|
result_dir_path = _join_path(results_dir, result_dir_name)
|
2026-05-26 14:36:34 -04:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
log_files = [
|
2026-05-27 15:02:32 -04:00
|
|
|
entry.name
|
|
|
|
|
for entry in _iter_dir_entries(result_dir_path)
|
|
|
|
|
if entry.is_file()
|
|
|
|
|
and entry.name.endswith(".txt")
|
|
|
|
|
and test_id in entry.name
|
2026-05-26 14:36:34 -04:00
|
|
|
]
|
|
|
|
|
except OSError as exc:
|
|
|
|
|
print(f"[scanner] Cannot read result dir {result_dir_name}: {exc}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if not log_files:
|
|
|
|
|
print(f"[scanner] completed (no log yet): {test_id}")
|
|
|
|
|
mark_completed(test_id, device, None, None)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
latest_log = sorted(log_files)[-1]
|
2026-05-27 15:02:32 -04:00
|
|
|
log_path = _join_path(result_dir_path, latest_log)
|
2026-05-26 14:36:34 -04:00
|
|
|
completed_at = parse_timestamp(latest_log)
|
|
|
|
|
data = extract_log_data(log_path)
|
|
|
|
|
|
|
|
|
|
print(
|
|
|
|
|
f"[scanner] completed: {test_id} device={device} "
|
|
|
|
|
f"duration={data['duration_seconds']}s stations={len(data['tputResults'])} at={completed_at}"
|
|
|
|
|
)
|
|
|
|
|
mark_completed(
|
|
|
|
|
test_id,
|
|
|
|
|
device,
|
|
|
|
|
completed_at,
|
|
|
|
|
data["duration_seconds"],
|
|
|
|
|
data["tputResults"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_deleted_result_dir_name(dir_name):
|
|
|
|
|
segments = re.split(r"[_\-]", dir_name)
|
|
|
|
|
test_id = next((s for s in segments if re.match(r"^R\d+[A-Z0-9]+$", s, re.IGNORECASE)), None)
|
|
|
|
|
device = next((s for s in segments if re.match(r"^CGW\d+$", s, re.IGNORECASE)), None)
|
|
|
|
|
return test_id, device
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_log_data(log_file_path):
|
|
|
|
|
elapsed_regex = re.compile(r"\[.*?INFO\]\s+Elapsed time\s*:\s*([\d]+:[\d]{2}:[\d]{2}\.[\d]+)")
|
|
|
|
|
tput_regex = re.compile(
|
|
|
|
|
r"\[.*?INFO\]\s+STA(\d+) over angles >> AVG IxChariot TPUT: (\d+) Mbps, AVG DL RSSI: (-?\d+) dBm, AVG UL RSSI: (-?\d+) dBm"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result = {"duration_seconds": None, "tputResults": []}
|
|
|
|
|
try:
|
2026-05-27 15:02:32 -04:00
|
|
|
for line in _read_text_lines(log_file_path):
|
|
|
|
|
time_match = elapsed_regex.search(line)
|
|
|
|
|
if time_match:
|
|
|
|
|
result["duration_seconds"] = parse_elapsed_time(time_match.group(1))
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if tput_regex.search(line):
|
|
|
|
|
parsed = parse_tput_rssi(line)
|
|
|
|
|
if parsed:
|
|
|
|
|
result["tputResults"].append(parsed)
|
2026-05-26 14:36:34 -04:00
|
|
|
except OSError as exc:
|
|
|
|
|
print(f"[scanner] Cannot read log file {log_file_path}: {exc}")
|
|
|
|
|
|
|
|
|
|
return result
|