commit 2c5fbe36bfcafccaf4c163baa0a507d2b7b41ac0
parent 25bd1759bb87ef7456ad7e4a997ac8425eda27bb
Author: Matthias Jaros <jaros@mailbox.org>
Date: Mon, 6 Jul 2026 13:02:54 +0200
Completetly reworked progress output. Now instead of individual hosts
give a statusline that prints current stats to err
Diffstat:
5 files changed, 169 insertions(+), 175 deletions(-)
diff --git a/src/main/java/com/it_jaros/jscanner/App.java b/src/main/java/com/it_jaros/jscanner/App.java
@@ -1,9 +1,6 @@
package com.it_jaros.jscanner;
-import java.util.BitSet;
-import java.util.List;
import java.util.concurrent.CountDownLatch;
-import java.util.stream.Collectors;
public class App {
@@ -14,7 +11,7 @@ public class App {
try {
run(args);
} catch (Exception e) {
- System.out.printf("ERROR: %s", e.getMessage());
+ System.out.printf("%nFATAL ERROR: %s", e.getMessage());
}
}
@@ -22,9 +19,12 @@ public class App {
ScanOptions options = parseOptions(args);
if (options == null) return;
try (Scanner scanner = new Scanner(options)) {
- Scan scan = Scan.create(options);
addShutdownHook(scanner);
- scanner.runScan(scan, result -> printPortsResults(result, options.showFilteredPorts()));
+ Scan scan = Scan.create(options);
+ ProgressBar progressBar = new ProgressBar(scan, options.quiet());
+ progressBar.start();
+ scanner.runScan(scan, result -> progressBar.printResult(result, options.showFilteredPorts()));
+ progressBar.stop();
} finally {
shutdownLatch.countDown();
removeShutdownHook();
@@ -44,49 +44,6 @@ public class App {
return null;
}
- private static void printScanResults(List<ScanResult> result, int max, boolean showFilteredPorts) {
- printStats(max);
- result.forEach((ScanResult r) -> printPortsResults(r, showFilteredPorts));
- }
-
- private static void printStats(int max) {
- System.out.println("--------------------");
- System.out.println("Stats:");
- System.out.println("Peak concurrent connects: " + max);
- System.out.println("--------------------");
- }
-
- private static void printPortsResults(ScanResult result, boolean showFilteredPorts) {
- if (result.openPorts().isEmpty() && (!showFilteredPorts || result.filteredPorts().isEmpty())) {
- return;
- }
-
- System.out.println(result.host());
- if (!result.openPorts().isEmpty()) {
- System.out.printf("\t (%d) open:\t%s%n", result.openPorts().cardinality(), map(result.openPorts(), result.bannerRecognition()));
- }
- if (showFilteredPorts && !result.filteredPorts().isEmpty()) {
- System.out.printf("\t (%d) filtered:\t%s%n", result.filteredPorts().cardinality(), map(result.filteredPorts()));
- }
- System.out.println();
- }
-
- private static String map(BitSet ports) {
- return ports.stream().mapToObj(String::valueOf).collect(Collectors.joining(","));
- }
-
- private static String map(BitSet ports, java.util.HashMap<Integer, ServiceType> bannerRecognition) {
- return ports.stream().mapToObj(port -> {
- ServiceType serviceType = bannerRecognition.getOrDefault(port, ServiceType.UNKNOWN);
-
- if (serviceType == ServiceType.UNKNOWN) {
- return String.valueOf(port);
- }
-
- return port + "/" + serviceType.name().toLowerCase();
- }).collect(Collectors.joining(","));
- }
-
/**
* Notice user that the signal was received
* Start shutdown of scanner
diff --git a/src/main/java/com/it_jaros/jscanner/Progress.java b/src/main/java/com/it_jaros/jscanner/Progress.java
@@ -1,5 +0,0 @@
-package com.it_jaros.jscanner;
-
-import java.util.concurrent.atomic.AtomicInteger;
-
-public record Progress(String host, int total, AtomicInteger done, AtomicInteger open, AtomicInteger filtered) {}
diff --git a/src/main/java/com/it_jaros/jscanner/ProgressBar.java b/src/main/java/com/it_jaros/jscanner/ProgressBar.java
@@ -2,29 +2,75 @@ package com.it_jaros.jscanner;
import java.io.BufferedReader;
import java.io.InputStreamReader;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.Iterator;
-import java.util.List;
+import java.util.BitSet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
public class ProgressBar {
- private final List<Progress> hosts = new ArrayList<>();
- private final ReentrantLock lock = new ReentrantLock();
private final ScheduledExecutorService ui = Executors.newSingleThreadScheduledExecutor();
+ private final Scan scan;
private final boolean quiet;
private final int columnWidth = ProgressBar.getColumnWidth();
+ private final Object outputLock = new Object();
- public ProgressBar(boolean quiet) {
+ public ProgressBar(Scan scan, boolean quiet) {
+ this.scan = scan;
this.quiet = quiet;
}
+ private void clearStatusLine() {
+ System.err.print("\r\033[2K");
+ System.err.flush();
+ }
+
+ public void printResult(ScanResult result, boolean showFilteredPorts) {
+ synchronized (outputLock) {
+ if (result.openPorts().isEmpty() && (!showFilteredPorts || result.filteredPorts().isEmpty())) {
+ return;
+ }
+
+ clearStatusLine();
+ printPortsResults(result, showFilteredPorts);
+ }
+ }
+
+ private void printPortsResults(ScanResult result, boolean showFilteredPorts) {
+ if (result.openPorts().isEmpty() && (!showFilteredPorts || result.filteredPorts().isEmpty())) {
+ return;
+ }
+
+ System.out.println(result.host());
+ if (!result.openPorts().isEmpty()) {
+ System.out.printf("\t (%d) open:\t%s%n", result.openPorts().cardinality(), map(result.openPorts(), result.bannerRecognition()));
+ }
+ if (showFilteredPorts && !result.filteredPorts().isEmpty()) {
+ System.out.printf("\t (%d) filtered:\t%s%n", result.filteredPorts().cardinality(), map(result.filteredPorts()));
+ }
+ System.out.println();
+ System.out.flush();
+ }
+
+ private static String map(BitSet ports) {
+ return ports.stream().mapToObj(String::valueOf).collect(Collectors.joining(","));
+ }
+
+ private static String map(BitSet ports, java.util.HashMap<Integer, ServiceType> bannerRecognition) {
+ return ports.stream().mapToObj(port -> {
+ ServiceType serviceType = bannerRecognition.getOrDefault(port, ServiceType.UNKNOWN);
+
+ if (serviceType == ServiceType.UNKNOWN) {
+ return String.valueOf(port);
+ }
+
+ return port + "/" + serviceType.name().toLowerCase();
+ }).collect(Collectors.joining(","));
+ }
+
private static int getColumnWidth() {
try {
Process process = new ProcessBuilder("sh", "-c", "stty size < /dev/tty").start();
@@ -47,86 +93,73 @@ public class ProgressBar {
}
public void start() {
- if (!quiet) {
- System.out.print("Scanning targets...\n\n");
+ if (quiet) {
+ return;
}
// although we don't print anything, we still need to
// gc all the done hosts
+ System.err.print("Scanning targets...\n\n");
ui.scheduleAtFixedRate(() -> {
- lock.lock();
- drawProgress(false);
- lock.unlock();
- }, 1, 500, TimeUnit.MILLISECONDS);
- }
-
- private void drawProgress(boolean last) {
- // sort -> the more progressed the host the more up it is
- hosts.sort(Comparator.comparingInt((Progress p) -> p.done().get()).reversed());
-
- // find and print finished hosts one last time
- for (Iterator<Progress> it = hosts.iterator(); it.hasNext(); ) {
- Progress p = it.next();
- if (p.done().get() == p.total()) {
- it.remove();
- drawBar(p);
- } else {
- break;
+ synchronized (outputLock) {
+ printStatusLine(scan);
}
- }
-
- for (Progress progress : hosts) {
- drawBar(progress);
- }
+ }, 1, 1000, TimeUnit.MILLISECONDS);
+ }
- if (!last) {
- // move cursor up again
- if (!quiet) {
- System.out.print("\u001b[" + hosts.size() + "A");
- }
+ public void stop() {
+ ui.shutdownNow();
+ try {
+ ui.awaitTermination(1, TimeUnit.SECONDS);
+ } catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt();
}
+ printStats();
}
- public void add(Progress progress) {
- lock.lock();
- hosts.add(progress);
- lock.unlock();
+ private void printStats() {
+ System.out.println("\n--------------------");
+ System.out.println("Stats:");
+ System.out.println("Total hosts scanned: " + scan.getHostTotalCounter().max());
+ System.out.println("Peak concurrent connects: " + scan.getSocketCounter().max());
+ System.out.println("Peak concurrent threads: " + scan.getThreadCounter().max());
+ System.out.println("--------------------\n");
}
- private void drawBar(Progress progress) {
- if (quiet) return;
- System.out.print("\u001b[2K\r"); // clear whole line
- System.out.println(createBar(
- progress.host(),
- progress.open().get(),
- progress.filtered().get(),
- progress.done().get(),
- progress.total()));
+ private void printStatusLine(final Scan scan) {
+ this.printStatusLine(scan.getHostCounter(), scan.getHostTotalCounter(), scan.getSocketCounter(), scan.getThreadCounter());
}
- public String createBar(String label, int open, int filtered, int done, int total) {
- int terminalWidth = columnWidth;
- int bracketWidth = terminalWidth / 3;
- double pct = total == 0 ? 1.0 : (done / (double) total);
- int percent = (int) (pct * 100);
-
- String statsLeft = String.format("open:%d filtered:%d (%d/%d)", open, filtered, done, total);
- String statsRight = String.format("%3d%%", percent);
- int filled = (int) (pct * bracketWidth);
- String brackets = "[" + "#".repeat(filled) + "-".repeat(bracketWidth - filled) + "]";
-
- int spacesBetween = terminalWidth - bracketWidth - (label + statsLeft + statsRight).length() - 5;
- return String.format("%s%s%s %s %s", label, " ".repeat(spacesBetween), statsLeft, brackets, statsRight);
+ private void printStatusLine(
+ Counter hostCounter, Counter hostTotalCounter, Counter
+ socketCounter, Counter
+ threadCounter) {
+ int doneHosts = hostTotalCounter.current();
+ int runningHosts = hostCounter.current();
+ int total = doneHosts + runningHosts;
+ long percent = total == 0 ? 100 : doneHosts * 100L / total;
+
+ String line = String.format(
+ "hosts: %d running, %d done | sockets: %d active, %d peak | workers: %d active, %d peak",
+ runningHosts,
+ doneHosts,
+ socketCounter.current(),
+ socketCounter.max(),
+ threadCounter.current(),
+ threadCounter.max()
+ );
+
+ String barFormat = " | [%s] %3d%%";
+ int barWidth = Math.max(5, this.columnWidth - line.length() - barFormat.length());
+ String progressBar = progressBar(percent, barWidth);
+ String status = line + String.format(barFormat, progressBar, percent);
+ System.err.print("\r" + status);
+ System.err.flush();
}
- public void stop() {
- ui.shutdown();
- try {
- ui.awaitTermination(1, TimeUnit.SECONDS);
- // we need to draw one last time for the 100% to appear
- drawProgress(true);
- } catch (InterruptedException ignored) {
- Thread.currentThread().interrupt();
- }
+ private static String progressBar(long percent, int width) {
+ int filled = (int) (percent * width / 100);
+
+ return "#".repeat(filled) + "-".repeat(width - filled);
}
}
diff --git a/src/main/java/com/it_jaros/jscanner/Scan.java b/src/main/java/com/it_jaros/jscanner/Scan.java
@@ -12,27 +12,27 @@ import java.util.stream.Stream;
* Object holder for stateful volatile data during scan
*/
public class Scan {
+ private final Counter hostCounter;
+ private final Counter hostTotalCounter;
+ private final Counter socketCounter;
+ private final Counter threadCounter;
private final Stream<String> hosts;
private final String ports;
- private final Counter counter;
- private final ProgressBar progressBar;
- private Scan(Stream<String> hosts, String ports, boolean quiet) {
+ private Scan(Stream<String> hosts, String ports) {
this.hosts = hosts;
this.ports = ports;
- this.counter = new Counter();
- this.progressBar = new ProgressBar(quiet);
- }
-
- public int getPeakConcurrentConnects() {
- return counter.max();
+ this.hostCounter = new Counter();
+ this.socketCounter = new Counter();
+ this.threadCounter = new Counter();
+ this.hostTotalCounter = new Counter();
}
public static Scan create(ScanOptions options) throws IOException {
if (options.hostsFile() != null) {
- return create(options.hostsFile(), options.ports(), options.quiet());
+ return create(options.hostsFile(), options.ports());
}
- return create(options.hostsArgv(), options.ports(), options.quiet());
+ return create(options.hostsArgv(), options.ports());
}
/**
@@ -40,11 +40,10 @@ public class Scan {
*
* @param sourceFile
* @param ports
- * @param quiet
* @return
* @throws IOException
*/
- public static Scan create(String sourceFile, String ports, boolean quiet) throws IOException {
+ public static Scan create(String sourceFile, String ports) throws IOException {
Stream<String> hosts;
if ("-".equals(sourceFile)) {
BufferedReader stdinReader = new BufferedReader(new InputStreamReader(System.in));
@@ -60,15 +59,15 @@ public class Scan {
});
}
- return create(hosts, ports, quiet);
+ return create(hosts, ports);
}
- private static Scan create(Stream<String> lines, String ports, boolean quiet) {
- return new Scan(lines, ports, quiet);
+ private static Scan create(Stream<String> lines, String ports) {
+ return new Scan(lines, ports);
}
- public static Scan create(List<String> hostsArgv, String ports, boolean quiet) {
- return new Scan(hostsArgv.stream(), ports, quiet);
+ public static Scan create(List<String> hostsArgv, String ports) {
+ return new Scan(hostsArgv.stream(), ports);
}
public Stream<String> getHosts() {
@@ -80,23 +79,26 @@ public class Scan {
}
public void start() {
- progressBar.start();
+ // start
}
public void stop() {
- progressBar.stop();
hosts.close(); // close stream
}
- public void addHostProgress(Progress progress) {
- progressBar.add(progress);
+ public Counter getThreadCounter() {
+ return threadCounter;
+ }
+
+ public Counter getHostCounter() {
+ return hostCounter;
}
- public void incSocketCounter() {
- counter.inc();
+ public Counter getSocketCounter() {
+ return socketCounter;
}
- public void decSocketCounter() {
- counter.dec();
+ public Counter getHostTotalCounter() {
+ return hostTotalCounter;
}
}
diff --git a/src/main/java/com/it_jaros/jscanner/Scanner.java b/src/main/java/com/it_jaros/jscanner/Scanner.java
@@ -67,16 +67,23 @@ public class Scanner implements AutoCloseable {
final ProducerState<ScanResult> state = startProducer(
scan.getHosts().iterator(),
maxHostsLimit,
- host -> () -> scanHostPorts(host, scan)
+ host -> () -> {
+ try {
+ scan.getHostCounter().inc();
+ return scanHostPorts(host, scan);
+ } finally {
+ scan.getHostCounter().dec();
+ }
+ },
+ scan.getThreadCounter()
);
// the main thread is the consumer
- // Let the consumer run as long as
- // as the producer runs
+ // Let the consumer run as long as the producer runs
// or if still tasks are pending in pipeline
// we do not listen to canceled here because we want
- // because we want all results (also partial) ones collected here to print
- // whatever is already there
+ // all results (also partial) collected for the consumer
+ // with whatever is there already
while (state.running().get() || state.inPipeline().get() > 0) {
try {
// let's check for results and give add them to our scan data holder object
@@ -92,6 +99,8 @@ public class Scanner implements AutoCloseable {
consumer.accept(result);
}
} finally {
+ scan.getHostTotalCounter().inc();
+ scan.getThreadCounter().dec();
state.activeWorkers().release();
state.inPipeline().decrementAndGet();
}
@@ -118,13 +127,8 @@ public class Scanner implements AutoCloseable {
BitSet filteredPorts = new BitSet(PortRange.MAX_PORT);
HashMap<Integer, ServiceType> serviceTypes = new HashMap<>();
- // Give progressbar the current progress object which is then updated in the sub virtual threads
- Progress progress = new Progress(host, portRange.getTotal(), new AtomicInteger(), new AtomicInteger(), new AtomicInteger());
- scan.addHostProgress(progress);
-
if (!disableOnlineCheck && !checkHostOnline(host)) {
- // Visually show that this host is basically done
- progress.done().set(progress.total());
+ // Unreachable host
return new ScanResult(host, openPorts, filteredPorts, serviceTypes);
}
@@ -138,7 +142,8 @@ public class Scanner implements AutoCloseable {
waitForSlot(portSlotFactory);
if (cancelled) return null;
return checkPort(host, port, scan);
- }
+ },
+ scan.getThreadCounter()
);
// consumer is the main thread
@@ -159,21 +164,19 @@ public class Scanner implements AutoCloseable {
// bitset is not thread-safe, so it is set
// outside the other virtual threads that update progress
openPorts.set(portResult.getPort());
- progress.open().incrementAndGet();
serviceTypes.put(portResult.getPort(), ServiceDetector.detect(portResult.getBanner()));
}
case FILTERED -> {
filteredPorts.set(portResult.getPort());
- progress.filtered().incrementAndGet();
}
default -> {
// sonarcube glücklich machen
}
}
} finally {
+ scan.getThreadCounter().dec();
state.activeWorkers().release();
state.inPipeline().decrementAndGet();
- progress.done().incrementAndGet();
}
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
@@ -201,13 +204,14 @@ public class Scanner implements AutoCloseable {
* @param <OUTPUT>
* @return
*/
- private <INPUT, OUTPUT> ProducerState<OUTPUT> startProducer(Iterator<INPUT> queue, int maxWorkers, Function<INPUT, Callable<OUTPUT>> taskFactory) {
+ private <INPUT, OUTPUT> ProducerState<OUTPUT> startProducer(Iterator<INPUT> queue, int maxWorkers, Function<INPUT, Callable<OUTPUT>> taskFactory, Counter threadCounter) {
final AtomicInteger inPipeline = new AtomicInteger(0);
final AtomicBoolean running = new AtomicBoolean(true);
final Semaphore activeWorkers = new Semaphore(maxWorkers);
CompletionService<OUTPUT> completionService = new ExecutorCompletionService<>(executor);
executor.submit(() -> {
try {
+ threadCounter.inc();
while (queue.hasNext() && !cancelled) {
activeWorkers.acquire();
@@ -226,6 +230,7 @@ public class Scanner implements AutoCloseable {
final INPUT item = queue.next();
Callable<OUTPUT> task = taskFactory.apply(item);
inPipeline.incrementAndGet();
+ threadCounter.inc();
try {
// here we are filling the completion service
// host <-> virtual thread
@@ -233,6 +238,7 @@ public class Scanner implements AutoCloseable {
submitted = true;
} catch (Throwable e) {
inPipeline.decrementAndGet();
+ threadCounter.dec();
throw e;
}
} finally {
@@ -244,6 +250,7 @@ public class Scanner implements AutoCloseable {
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
+ threadCounter.dec();
running.set(false);
}
});
@@ -277,7 +284,7 @@ public class Scanner implements AutoCloseable {
// don't allow more sockets then specified
socketLimit.acquire();
// count how many ports are concurrently checked
- scan.incSocketCounter();
+ scan.getSocketCounter().inc();
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(host, port), timeoutInMillis);
result.setState(PortState.OPEN);
@@ -292,7 +299,7 @@ public class Scanner implements AutoCloseable {
// NoRouteToHostException: this can be safely ignored because the port is closed if a host is unreachable
// Will happen a lot when scanning for open ports, so not needed
} finally {
- scan.decSocketCounter();
+ scan.getSocketCounter().dec();
socketLimit.release();
}
@@ -314,6 +321,10 @@ public class Scanner implements AutoCloseable {
return null;
}
+ public boolean awaitTermination(Duration duration) throws InterruptedException {
+ return executor.awaitTermination(duration.toMillis(), TimeUnit.MILLISECONDS);
+ }
+
public void cancel() {
if (!cancelled) {
cancelled = true;
@@ -332,8 +343,4 @@ public class Scanner implements AutoCloseable {
public void close() throws Exception {
cancel();
}
-
- public boolean awaitTermination(Duration duration) throws InterruptedException {
- return executor.awaitTermination(duration.toMillis(), TimeUnit.MILLISECONDS);
- }
}