Files
sif/internal/output/progress.go
T
TigahandGitHub d13622dc1c feat(scan): add -concurrency flag to scan targets in parallel (#367)
* refactor(cli): extract per-target scan into scanTarget

pull the target loop body into scanTarget, which returns a targetScan
holding that target's findings, report rows, scan labels and log files
instead of appending onto run-wide slices. the run loop merges those in
target order and finishRun handles the post-loop notify/silent/report/
summary steps. behavior is identical for the sequential run today; the
isolation is the seam a bounded worker pool needs next.

* feat(cli): scan targets in parallel with -concurrency

add -concurrency N (default 1) to run the per-target scan pool over
several targets at once. scanTarget already returns isolated
accumulators, so workers share only the console: output.SetConcurrent
serializes sink writes and de-animates spinners/progress so parallel
lines stay whole. results merge in input order, and concurrency 1 keeps
the sequential path unchanged.
2026-07-12 14:23:35 -07:00

198 lines
4.6 KiB
Go

/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package output
import (
"fmt"
"sync"
"sync/atomic"
)
// Progress bar configuration
const (
progressWidth = 30
progressFilled = "="
progressCurrent = ">"
progressEmpty = " "
)
// Progress displays a progress bar for operations with known counts
type Progress struct {
total int64
current int64
message string
lastItem string
mu sync.Mutex
paused bool
lastShown int // last printed milestone bucket in non-tty mode
}
// NewProgress creates a new progress bar
func NewProgress(total int, message string) *Progress {
return &Progress{
total: int64(total),
message: message,
}
}
// Increment advances the progress by 1 and optionally updates the current item
func (p *Progress) Increment(item string) {
atomic.AddInt64(&p.current, 1)
p.mu.Lock()
p.lastItem = item
paused := p.paused
p.mu.Unlock()
if !paused {
p.render()
}
}
// Set sets the progress to a specific value
func (p *Progress) Set(current int, item string) {
atomic.StoreInt64(&p.current, int64(current))
p.mu.Lock()
p.lastItem = item
paused := p.paused
p.mu.Unlock()
if !paused {
p.render()
}
}
// Pause temporarily stops rendering (use before printing other output)
func (p *Progress) Pause() {
p.mu.Lock()
p.paused = true
p.mu.Unlock()
ClearLine()
}
// Resume resumes rendering after a pause
func (p *Progress) Resume() {
p.mu.Lock()
p.paused = false
p.mu.Unlock()
p.render()
}
// Done clears the progress bar line
func (p *Progress) Done() {
// under -concurrency there is no live line to clear (render printed milestones),
// and clearing would wipe another target's output.
if apiMode || !IsTTY || concurrent {
return
}
ClearLine()
}
func (p *Progress) render() {
if apiMode || silent {
return
}
// In non-TTY mode, or under -concurrency, print progress at milestones only
// rather than redrawing a live line that parallel targets would corrupt.
if !IsTTY || concurrent {
current := atomic.LoadInt64(&p.current)
total := p.total
if total <= 0 {
return
}
percent := int(current * 100 / total)
// map current to a milestone bucket (0=none,1..5). concurrent workers
// hammer the same bucket, so only print when the bucket advances.
bucket := 0
switch {
case current >= total:
bucket = 5
case percent >= 75:
bucket = 4
case percent >= 50:
bucket = 3
case percent >= 25:
bucket = 2
case current >= 1:
bucket = 1
}
p.mu.Lock()
advanced := bucket > p.lastShown
if advanced {
p.lastShown = bucket
}
p.mu.Unlock()
if advanced {
fmt.Fprintf(sink, " [%d%%] %d/%d\n", percent, current, total)
}
return
}
current := atomic.LoadInt64(&p.current)
total := p.total
p.mu.Lock()
lastItem := p.lastItem
p.mu.Unlock()
// Calculate percentage
percent := 0
if total > 0 {
percent = int(current * 100 / total)
}
// Build progress bar
filled := 0
if total > 0 {
filled = int(progressWidth * current / total)
}
if filled > progressWidth {
filled = progressWidth
}
bar := ""
for i := 0; i < progressWidth; i++ {
switch {
case i < filled:
bar += progressFilled
case i == filled && current < total:
bar += progressCurrent
default:
bar += progressEmpty
}
}
// Truncate item if too long
maxItemLen := 30
if len(lastItem) > maxItemLen {
lastItem = lastItem[:maxItemLen-3] + "..."
}
// Format: [========> ] 45% (4500/10000) /admin
line := fmt.Sprintf(" [%s] %3d%% (%d/%d) %s",
prefixInfo.Render(bar),
percent,
current,
total,
Muted.Render(lastItem),
)
ClearLine()
fmt.Fprint(sink, line)
}