jscanner

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 6bea8a3abd0b657cfabb55385ee77b1d4b3f8906
parent 5ff81526ddfa739fb374a07a6bd20d0f1a5e6594
Author: Matthias Jaros <jaros@mailbox.org>
Date:   Thu,  2 Jul 2026 19:33:03 +0200

Finally managed to get a gracefull shutdown with information about the
current state of scanned ports

Diffstat:
Msrc/main/java/com/it_jaros/jscanner/App.java | 93+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Msrc/main/java/com/it_jaros/jscanner/Scanner.java | 52+++++++++++++++++++++++++++++++++++-----------------
2 files changed, 90 insertions(+), 55 deletions(-)

diff --git a/src/main/java/com/it_jaros/jscanner/App.java b/src/main/java/com/it_jaros/jscanner/App.java @@ -1,12 +1,18 @@ package com.it_jaros.jscanner; +import java.time.Duration; import java.util.BitSet; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; public class App { private static Thread shutdownHook; + private static final CountDownLatch shutdownLatch = new CountDownLatch(1); + private static final int shutdownTimeoutInMillis = 10000; public static void main(String[] args) { try { @@ -21,10 +27,10 @@ public class App { if (options == null) return; try (Scanner scanner = new Scanner(options)) { Scan scan = Scan.create(options); - addShutdownHook(scan, scanner, options); - scanner.runScan(scan); - printScanResults(scan, options); + addShutdownHook(scanner, options); + printScanResults(scanner.runScan(scan), scan.getPeakConcurrentConnects(), options.showFilteredPorts()); } finally { + shutdownLatch.countDown(); removeShutdownHook(); } } @@ -42,42 +48,9 @@ public class App { return null; } - /** - * Allows us to handle signal int events - * Prints intermediate result - * - * @param scan - * @param scanner - * @param scanOptions - */ - private static void addShutdownHook(Scan scan, Scanner scanner, ScanOptions scanOptions) { - shutdownHook = new Thread(() -> { - System.err.println("\nCtrl-C received."); - scanner.cancel(); - printScanResults(scan, scanOptions); - }); - Runtime.getRuntime().addShutdownHook(shutdownHook); - } - - private static void removeShutdownHook() { - if (shutdownHook == null) { - return; - } - - try { - Runtime.getRuntime().removeShutdownHook(shutdownHook); - } catch (IllegalStateException ignored) { - // JVM is already shutting down - } - } - - public static void printScanResults(Scan scan, ScanOptions scanOptions) { - printScanResults(scan.getResults(), scan.getPeakConcurrentConnects(), scanOptions); - } - - private static void printScanResults(List<ScanResult> result, int max, ScanOptions options) { + private static void printScanResults(List<ScanResult> result, int max, boolean showFilteredPorts) { printStats(max); - result.forEach((ScanResult r) -> printPortsResults(r, options.showFilteredPorts())); + result.forEach((ScanResult r) -> printPortsResults(r, showFilteredPorts)); } private static void printStats(int max) { @@ -117,4 +90,48 @@ public class App { return port + "/" + serviceType.name().toLowerCase(); }).collect(Collectors.joining(",")); } + + /** + * Notice user that the signal was received + * Start shutdown of scanner + * + * @param scanner + */ + private static void addShutdownHook(Scanner scanner, ScanOptions options) { + shutdownHook = new Thread(() -> { + System.err.printf("%nCtrl-C received. Shutting down... Waiting for timeout of %d seconds%n", shutdownTimeoutInMillis / 1000); + shutdown(scanner); + }); + Runtime.getRuntime().addShutdownHook(shutdownHook); + } + + /** + * Try to gracefully shutdown scanner + * and wait + * + * @param scanner + */ + private static void shutdown(Scanner scanner) { + try { + scanner.cancel(); + scanner.awaitTermination(Duration.ofMillis(shutdownTimeoutInMillis)); + shutdownLatch.await(shutdownTimeoutInMillis, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (TimeoutException e) { + // Time ran out + } + } + + private static void removeShutdownHook() { + if (shutdownHook == null) { + return; + } + + try { + Runtime.getRuntime().removeShutdownHook(shutdownHook); + } catch (IllegalStateException ignored) { + // JVM is already shutting down + } + } } diff --git a/src/main/java/com/it_jaros/jscanner/Scanner.java b/src/main/java/com/it_jaros/jscanner/Scanner.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.*; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; @@ -23,6 +24,7 @@ public class Scanner implements AutoCloseable { private final int maxWorkersPerHost; private final int timeoutInMillis; private final long delayInNanos; + private final Phaser runningScans = new Phaser(0); public Scanner( int socketLimit, @@ -53,15 +55,20 @@ public class Scanner implements AutoCloseable { } scan.start(); + runningScans.register(); try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { CompletionService<ScanResult> completionService = new ExecutorCompletionService<>(executor); Queue<String> hostsInQueue = new ArrayDeque<>(scan.getHosts()); int hostsToProcess = hostsInQueue.size(); int activeHostWorkers = 0; - while (hostsToProcess > 0 && !cancelled) { + while ((hostsToProcess > 0 && !cancelled) || activeHostWorkers > 0) { // process queue - while (!hostsInQueue.isEmpty() && activeHostWorkers < maxHostsLimit && !cancelled) { + while ( + !hostsInQueue.isEmpty() // continue as long as we have hosts to process + && activeHostWorkers < maxHostsLimit // limit is not reached + && !cancelled // and scanner not closed + ) { activeHostWorkers++; final String host = hostsInQueue.poll(); completionService.submit(() -> scanHostPorts(host, scan.getPorts(), scan)); @@ -81,10 +88,8 @@ public class Scanner implements AutoCloseable { System.out.printf("%s -> %s%n", e.getClass().getSimpleName(), e.getMessage()); } } - - if (cancelled) { - executor.shutdown(); - } + } finally { + runningScans.arriveAndDeregister(); } scan.stop(); @@ -113,8 +118,12 @@ public class Scanner implements AutoCloseable { AtomicLong portSlotFactory = new AtomicLong(System.nanoTime()); int activePerHostWorkers = 0; int portsToProcess = portRange.getTotal(); - while (portsToProcess > 0 && !cancelled) { - while (!cancelled && portRange.hasNext() && activePerHostWorkers < maxWorkersPerHost) { + while ((portsToProcess > 0 && !cancelled) || activePerHostWorkers > 0) { + while ( + portRange.hasNext() // continue as long as we have ports + && activePerHostWorkers < maxWorkersPerHost // and we have not reached our limit + && !cancelled // and the scanner is not about to close + ) { activePerHostWorkers++; final int currentPort = portRange.next(); completionService.submit(() -> { @@ -128,19 +137,24 @@ public class Scanner implements AutoCloseable { if (portResultFuture == null) { continue; } + + PortResult portResult = portResultFuture.get(); + activePerHostWorkers--; portsToProcess--; progress.done().incrementAndGet(); - - // bitset is not thread-safe, so it is set - // outside the other virtual threads that update progress - PortResult portResult = portResultFuture.get(); switch (portResult.getState()) { case OPEN -> { + // 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()); + case FILTERED -> { + filteredPorts.set(portResult.getPort()); + progress.filtered().incrementAndGet(); + } } } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); @@ -149,10 +163,6 @@ public class Scanner implements AutoCloseable { } } - if (!cancelled) { - executor.shutdown(); - } - if (!errors.isEmpty()) { System.out.printf("Errors happened during scan of host %s%nErrors:%s -> %s", host, errors.size(), errors); } @@ -233,5 +243,13 @@ public class Scanner implements AutoCloseable { @Override public void close() throws Exception { cancel(); + awaitTermination(Duration.ofMillis(10000)); + } + + public void awaitTermination(Duration duration) throws InterruptedException, TimeoutException { + runningScans.awaitAdvanceInterruptibly( + runningScans.getPhase(), + duration.toMillis(), + TimeUnit.MILLISECONDS); } }