'use strict'; const fs = require('fs'); const path = require('path'); const readline = require('readline'); const { parseTargetFilename, parseResultFilename, parseTimestamp, parseElapsedTime, parseTputRssi } = require('./parser'); const { upsertTest, markCompleted, clearTests } = require('./db'); /** * Full rebuild: clears the DB, walks target dir to build the test list, * then walks results dir to mark completions. */ async function fullScan(targetDir, resultsDir) { clearTests(); if (!targetDir || !resultsDir) return; console.log(`[scanner] target dir : ${targetDir}`); console.log(`[scanner] results dir: ${resultsDir}`); await scanTargets(targetDir); await scanResults(resultsDir); } async function scanTargets(targetDir) { // Walk the target directory, find all TC_WIFI_*.ini files, parse their names, // and upsert them into the DB. Ignore files without a valid test_id. let parentEntries; try { parentEntries = fs.readdirSync(targetDir, { withFileTypes: true }).filter(d => d.isDirectory()); } catch (e) { console.error('[scanner] Cannot read target dir:', e.message); return; } console.log(`[scanner] subdirectories found: ${parentEntries.length}`); for (const parentEntry of parentEntries) { const parentDirPath = path.join(targetDir, parentEntry.name); let files; try { files = fs.readdirSync(parentDirPath, { withFileTypes: true }) .filter(f => f.isFile() && !f.name.startsWith('GLOBAL') && f.name.endsWith('.ini')); } catch (e) { console.error(`[scanner] Cannot read parent dir ${parentEntry.name}:`, e.message); continue; } console.log(`[scanner] ${parentEntry.name} → ${files.length} target test file(s)`); for (const file of files) { const parsed = parseTargetFilename(file.name, parentEntry.name); if (!parsed.test_id) { console.log(`[scanner] skip (no test_id): ${parentEntry.name}/${file.name}`); continue; } console.log(`[scanner] found: ${parentEntry.name}/${file.name} → test_id=${parsed.test_id}`); upsertTest({ id: parsed.id, test_id: parsed.test_id, parent_dir: parentEntry.name, filename: file.name, 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, }); } } } /** * Scan the results directory and mark all matched tests as completed. */ async function scanResults(resultsDir) { let entries; try { entries = fs.readdirSync(resultsDir, { withFileTypes: true }).filter(d => d.isDirectory()); } catch (e) { console.error('[scanner] Cannot read results dir:', e.message); return; } console.log(`[scanner] results: ${entries.length} result dir(s) found`); for (const entry of entries) { await processResultDir(resultsDir, entry.name); } } /** * Process one result directory: extract test_id, find the latest matching * log file, parse its elapsed-time line, and mark the test as completed. */ async function processResultDir(resultsDir, resultDirName) { const { test_id, device } = parseResultFilename(resultDirName); //console.log(`[scanner] result dir: ${resultDirName} → test_id=${test_id} device=${device}`); if (!test_id || !device) return; const resultDirPath = path.join(resultsDir, resultDirName); let logFiles; try { logFiles = fs.readdirSync(resultDirPath, { withFileTypes: true }) .filter(f => f.isFile() && f.name.endsWith('.txt') && f.name.includes(test_id)) .map(f => f.name); } catch (e) { console.error(`[scanner] Cannot read result dir ${resultDirName}:`, e.message); return; } if (logFiles.length === 0) { // Result directory exists but log not written yet — mark complete without timing console.log(`[scanner] completed (no log yet): ${test_id}`); markCompleted(test_id, device, null, null); return; } // For re-runs, the latest log file has the latest timestamp in its name const latestLog = logFiles.sort().at(-1); const logPath = path.join(resultDirPath, latestLog); const completed_at = parseTimestamp(latestLog); const { duration_seconds, tputResults } = await extractLogData(logPath); console.log(`[scanner] completed: ${test_id} device=${device} duration=${duration_seconds}s stations=${tputResults.length} at=${completed_at}`); markCompleted(test_id, device, completed_at, duration_seconds, tputResults); } /** * Single-pass log file reader: extracts elapsed time and all station tput/rssi entries. */ function extractLogData(logFilePath) { const ELAPSED_REGEX = /\[.*?INFO\]\s+Elapsed time\s*:\s*([\d]+:[\d]{2}:[\d]{2}\.[\d]+)/; const TPUT_RSSI_REGEX = /\[.*?INFO\]\s+STA(\d+) over angles >> AVG IxChariot TPUT: (\d+) Mbps, AVG DL RSSI: (-?\d+) dBm, AVG UL RSSI: (-?\d+) dBm/ return new Promise((resolve) => { const result = { duration_seconds: null, tputResults: [] }; let stream; try { stream = fs.createReadStream(logFilePath, { encoding: 'utf8' }); } catch (e) { console.error(`[scanner] Cannot read log file ${logFilePath}:`, e.message); return resolve(result); } const rl = readline.createInterface({ input: stream, crlfDelay: Infinity }); rl.on('line', (line) => { const timeMatch = line.match(ELAPSED_REGEX); const tputMatch = line.match(TPUT_RSSI_REGEX); if (timeMatch) result.duration_seconds = parseElapsedTime(timeMatch[1]); else if (tputMatch) { parsed = parseTputRssi(line); if (parsed) result.tputResults.push(parsed); } }); rl.on('close', () => resolve(result)); rl.on('error', () => resolve(result)); stream.on('error', () => resolve(result)); }); } module.exports = { fullScan, scanResults, processResultDir };