fix(scan): two-phase progress + hosts_processed; clarify status at 99% SNMP tail
Made-with: Cursor
This commit is contained in:
+57
-17
@@ -33,6 +33,24 @@ func NewRunner(store Store, cfg RunnerConfig) *Runner {
|
|||||||
return &Runner{store: store, cfg: cfg}
|
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) {
|
func (r *Runner) Start(job ScanJob) {
|
||||||
go r.run(job)
|
go r.run(job)
|
||||||
}
|
}
|
||||||
@@ -64,7 +82,7 @@ func (r *Runner) run(job ScanJob) {
|
|||||||
_, _ = r.store.UpdateScan(job.ID, ScanUpdate{
|
_, _ = r.store.UpdateScan(job.ID, ScanUpdate{
|
||||||
Status: "done",
|
Status: "done",
|
||||||
Progress: 100,
|
Progress: 100,
|
||||||
Stats: ScanStats{HostsTotal: 0, HostsUp: 0},
|
Stats: ScanStats{},
|
||||||
FinishedAt: &finished,
|
FinishedAt: &finished,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -76,11 +94,29 @@ func (r *Runner) run(job ScanJob) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
var done int
|
var quickDone int // ping (+ TCP-порты)
|
||||||
|
var fullDone int // полностью, включая SNMP
|
||||||
var up int
|
var up int
|
||||||
workCh := make(chan string)
|
workCh := make(chan string)
|
||||||
wg := sync.WaitGroup{}
|
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() {
|
worker := func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for ip := range workCh {
|
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 {
|
if isUp && r.cfg.SNMPEnabled {
|
||||||
snmpRes, lldpRes, ifRes, portDevRes := probeSNMPAndLLDP(ip, r.cfg.SNMPCommunity, checkedAt, job.Options.PingTimeoutMS)
|
snmpRes, lldpRes, ifRes, portDevRes := probeSNMPAndLLDP(ip, r.cfg.SNMPCommunity, checkedAt, job.Options.PingTimeoutMS)
|
||||||
_ = r.store.SaveSNMPResult(job.ID, snmpRes)
|
_ = r.store.SaveSNMPResult(job.ID, snmpRes)
|
||||||
@@ -122,19 +167,9 @@ func (r *Runner) run(job ScanJob) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
done++
|
fullDone++
|
||||||
if isUp {
|
|
||||||
up++
|
|
||||||
}
|
|
||||||
progress := done * 100 / total
|
|
||||||
stats := ScanStats{HostsTotal: total, HostsUp: up}
|
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
pushProgress()
|
||||||
_, _ = r.store.UpdateScan(job.ID, ScanUpdate{
|
|
||||||
Status: "running",
|
|
||||||
Progress: progress,
|
|
||||||
Stats: stats,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,9 +186,14 @@ func (r *Runner) run(job ScanJob) {
|
|||||||
|
|
||||||
finished := time.Now().UTC()
|
finished := time.Now().UTC()
|
||||||
_, _ = r.store.UpdateScan(job.ID, ScanUpdate{
|
_, _ = r.store.UpdateScan(job.ID, ScanUpdate{
|
||||||
Status: "done",
|
Status: "done",
|
||||||
Progress: 100,
|
Progress: 100,
|
||||||
Stats: ScanStats{HostsTotal: total, HostsUp: up},
|
Stats: ScanStats{
|
||||||
|
HostsTotal: total,
|
||||||
|
HostsUp: up,
|
||||||
|
HostsPingPhaseDone: total,
|
||||||
|
HostsProcessed: total,
|
||||||
|
},
|
||||||
FinishedAt: &finished,
|
FinishedAt: &finished,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,10 @@ type ScanJob struct {
|
|||||||
type ScanStats struct {
|
type ScanStats struct {
|
||||||
HostsTotal int `json:"hosts_total"`
|
HostsTotal int `json:"hosts_total"`
|
||||||
HostsUp int `json:"hosts_up"`
|
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 {
|
type ScanUpdate struct {
|
||||||
|
|||||||
@@ -273,7 +273,18 @@
|
|||||||
return `Задача ${id}: в очереди, подготовка списка адресов…`;
|
return `Задача ${id}: в очереди, подготовка списка адресов…`;
|
||||||
}
|
}
|
||||||
if (st === "running") {
|
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") {
|
if (st === "done") {
|
||||||
return `Скан ${id}: завершён. Всего адресов в задании: ${t}, ответили на ping: ${up}.`;
|
return `Скан ${id}: завершён. Всего адресов в задании: ${t}, ответили на ping: ${up}.`;
|
||||||
|
|||||||
Reference in New Issue
Block a user