import os import re import smbclient from db_py import ( clear_tests, get_all_tests, mark_completed, set_coe_pair, set_p3p_pair, upsert_test, ) from parser import ( parse_elapsed_time, parse_result_filename, parse_target_filename, parse_timestamp, parse_tput_rssi, ) _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 ///... 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() update_p3p_throttle_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) clear_tests() if not target_dir or not results_dir: return 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}") scan_targets(target_dir) scan_results(results_dir) if results_dir_ref: scan_results(results_dir_ref) update_p2p_coe_pairs() update_p3p_throttle_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)") def update_p3p_throttle_pairs(): tests = get_all_tests() p3p_lookup = {} for test in tests: if test.get("interference") != "P3P": continue device = test.get("device") test_id = test.get("test_id") if not device or not test_id: continue p3p_lookup.setdefault(f"{str(device).upper()}_{str(test_id).upper()}", []).append(test) def _pair_test_id(test_id, throttled): if not test_id or not throttled: return None upper_id = str(test_id).upper() upper_throttled = str(throttled).upper() if upper_throttled == "TH": return re.sub("TH", "UT", upper_id, count=1) if upper_throttled == "UT": return re.sub("UT", "TH", upper_id, count=1) return None updated = 0 for test in tests: if test.get("interference") != "P3P": continue device = test.get("device") test_id = test.get("test_id") pair_test_id = _pair_test_id(test_id, test.get("throttled")) pairs = [] if device and pair_test_id: lookup_key = f"{str(device).upper()}_{str(pair_test_id).upper()}" matches = p3p_lookup.get(lookup_key, []) for match in matches: match_device = match.get("device") match_test_id = match.get("test_id") if match_device and match_test_id: pairs.append(f"{match_device}_{match_test_id}") set_p3p_pair(test.get("id"), sorted(set(pairs))) updated += 1 print(f"[scanner] p3p_pair updated for {updated} P3P test(s)") def scan_targets(target_dir): target_dir = _normalize_input_path(target_dir) try: parent_entries = [ entry.name for entry in _iter_dir_entries(target_dir) if entry.is_dir() ] 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: parent_path = _join_path(target_dir, parent_name) try: files = [ 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") ] 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"], "throttled": parsed["throttled"], } ) def scan_results(results_dir): results_dir = _normalize_input_path(results_dir) try: entries = [ entry.name for entry in _iter_dir_entries(results_dir) if entry.is_dir() ] 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): results_dir = _normalize_input_path(results_dir) parsed = parse_result_filename(result_dir_name) test_id = parsed["test_id"] device = parsed["device"] if not test_id or not device: return result_dir_path = _join_path(results_dir, result_dir_name) try: log_files = [ 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 ] 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] log_path = _join_path(result_dir_path, latest_log) 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: 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) except OSError as exc: print(f"[scanner] Cannot read log file {log_file_path}: {exc}") return result