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:
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);
}
}