fix(scan): two-phase progress + hosts_processed; clarify status at 99% SNMP tail

Made-with: Cursor
This commit is contained in:
PTah
2026-04-13 13:53:46 +10:00
parent 0de279926f
commit 70fff4f0ea
4 changed files with 93 additions and 18 deletions
+57 -17
View File
@@ -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,
})
}
+20
View File
@@ -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)
}
}
+4
View File
@@ -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 {
+12 -1
View File
@@ -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}.`;