diff --git a/internal/scans/runner.go b/internal/scans/runner.go index 737e1bd..dc73339 100644 --- a/internal/scans/runner.go +++ b/internal/scans/runner.go @@ -33,6 +33,24 @@ func NewRunner(store Store, cfg RunnerConfig) *Runner { return &Runner{store: store, cfg: cfg} } +// scanProgressBlend: часть прогресса за ping/порты (быстро), основная — за полное завершение хоста (SNMP и т.д.). +// Пока не все хосты завершены полностью, процент не поднимается выше 99 — чтобы не казалось, что скан «завис на 100%». +func scanProgressBlend(quickDone, fullDone, total int) int { + if total <= 0 { + return 100 + } + if fullDone >= total { + return 100 + } + // Округление к ближайшему проценту, иначе 15*256+85*253 даёт 98 при ожидании «почти 99». + num := 15*quickDone + 85*fullDone + p := (num + total/2) / total + if p >= 99 { + return 99 + } + return p +} + func (r *Runner) Start(job ScanJob) { go r.run(job) } @@ -64,7 +82,7 @@ func (r *Runner) run(job ScanJob) { _, _ = r.store.UpdateScan(job.ID, ScanUpdate{ Status: "done", Progress: 100, - Stats: ScanStats{HostsTotal: 0, HostsUp: 0}, + Stats: ScanStats{}, FinishedAt: &finished, }) return @@ -76,11 +94,29 @@ func (r *Runner) run(job ScanJob) { } var mu sync.Mutex - var done int + var quickDone int // ping (+ TCP-порты) + var fullDone int // полностью, включая SNMP var up int workCh := make(chan string) wg := sync.WaitGroup{} + pushProgress := func() { + mu.Lock() + q, f, u := quickDone, fullDone, up + mu.Unlock() + prog := scanProgressBlend(q, f, total) + _, _ = r.store.UpdateScan(job.ID, ScanUpdate{ + Status: "running", + Progress: prog, + Stats: ScanStats{ + HostsTotal: total, + HostsUp: u, + HostsPingPhaseDone: q, + HostsProcessed: f, + }, + }) + } + worker := func() { defer wg.Done() for ip := range workCh { @@ -101,6 +137,15 @@ func (r *Runner) run(job ScanJob) { }) } } + + mu.Lock() + if isUp { + up++ + } + quickDone++ + mu.Unlock() + pushProgress() + if isUp && r.cfg.SNMPEnabled { snmpRes, lldpRes, ifRes, portDevRes := probeSNMPAndLLDP(ip, r.cfg.SNMPCommunity, checkedAt, job.Options.PingTimeoutMS) _ = r.store.SaveSNMPResult(job.ID, snmpRes) @@ -122,19 +167,9 @@ func (r *Runner) run(job ScanJob) { } mu.Lock() - done++ - if isUp { - up++ - } - progress := done * 100 / total - stats := ScanStats{HostsTotal: total, HostsUp: up} + fullDone++ mu.Unlock() - - _, _ = r.store.UpdateScan(job.ID, ScanUpdate{ - Status: "running", - Progress: progress, - Stats: stats, - }) + pushProgress() } } @@ -151,9 +186,14 @@ func (r *Runner) run(job ScanJob) { finished := time.Now().UTC() _, _ = r.store.UpdateScan(job.ID, ScanUpdate{ - Status: "done", - Progress: 100, - Stats: ScanStats{HostsTotal: total, HostsUp: up}, + Status: "done", + Progress: 100, + Stats: ScanStats{ + HostsTotal: total, + HostsUp: up, + HostsPingPhaseDone: total, + HostsProcessed: total, + }, FinishedAt: &finished, }) } diff --git a/internal/scans/runner_progress_test.go b/internal/scans/runner_progress_test.go new file mode 100644 index 0000000..4c1a02d --- /dev/null +++ b/internal/scans/runner_progress_test.go @@ -0,0 +1,20 @@ +package scans + +import "testing" + +func TestScanProgressBlend(t *testing.T) { + const total = 256 + if g := scanProgressBlend(0, 0, total); g != 0 { + t.Fatalf("0,0 -> 0 got %d", g) + } + if g := scanProgressBlend(total, total-1, total); g != 99 { + t.Fatalf("quick=total full=total-1 -> 99 got %d", g) + } + if g := scanProgressBlend(total, total, total); g != 100 { + t.Fatalf("complete -> 100 got %d", g) + } + // типичный «застряли на последних SNMP»: ping по всем, SNMP не догнан + if g := scanProgressBlend(256, 253, total); g != 99 { + t.Fatalf("256,253 -> 99 got %d", g) + } +} diff --git a/internal/scans/store.go b/internal/scans/store.go index bd74160..096be83 100644 --- a/internal/scans/store.go +++ b/internal/scans/store.go @@ -44,6 +44,10 @@ type ScanJob struct { type ScanStats struct { HostsTotal int `json:"hosts_total"` HostsUp int `json:"hosts_up"` + // HostsPingPhaseDone — завершены ping и (если включено) проверка TCP-портов; SNMP ещё может идти. + HostsPingPhaseDone int `json:"hosts_ping_phase_done"` + // HostsProcessed — хост полностью обработан (включая SNMP/LLDP/FDB при необходимости). + HostsProcessed int `json:"hosts_processed"` } type ScanUpdate struct { diff --git a/internal/webui/index.html b/internal/webui/index.html index 64482dd..c331315 100644 --- a/internal/webui/index.html +++ b/internal/webui/index.html @@ -273,7 +273,18 @@ return `Задача ${id}: в очереди, подготовка списка адресов…`; } if (st === "running") { - return `Скан ${id}: выполняется обход сети — обработано ~${p}% адресов из ${t}; отвечают на ping (уже проверены): ${up}. Для каждого отвечающего: порты → SNMP → LLDP.`; + const pingDone = job.stats?.hosts_ping_phase_done; + const proc = job.stats?.hosts_processed; + let phase = ""; + if (t > 0 && (pingDone != null || proc != null)) { + const pd = pingDone != null ? pingDone : "?"; + const pr = proc != null ? proc : "?"; + phase = ` Этапы: ping+порты ${pd}/${t} адр.; полностью (SNMP/LLDP/FDB) ${pr}/${t}.`; + if (proc != null && proc < t) { + phase += ` Пока ${pr} < ${t} — идёт тяжёлый SNMP на оставшихся (часто коммутаторы с большим FDB), это нормально и может занять минуты.`; + } + } + return `Скан ${id}: обход сети — прогресс ~${p}% (из ${t} адресов); отвечают на ping: ${up}.${phase} Для отвечающих: порты → SNMP → LLDP.`; } if (st === "done") { return `Скан ${id}: завершён. Всего адресов в задании: ${t}, ответили на ping: ${up}.`;