diff --git a/src/main/java/kst4contest/controller/ChatController.java b/src/main/java/kst4contest/controller/ChatController.java index 6e9d0bcd..43c247cf 100644 --- a/src/main/java/kst4contest/controller/ChatController.java +++ b/src/main/java/kst4contest/controller/ChatController.java @@ -1083,9 +1083,35 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList rotatorClient.stop(); rotatorClient = null; } + + releaseBackgroundExecutors(); } } + /** + * Stops the background executors that live as long as this controller. + * + *
These are not bound to one ON4KST session, so disconnecting leaves them running + * on purpose. When the controller itself is discarded they have to go, otherwise a + * discarded controller stays reachable through its own threads.
+ */ + private void releaseBackgroundExecutors() { + + on4KstConnectionManager.shutdown(); + skedReminderService.shutdown(); + + if (reachabilityService != null) { + reachabilityService.shutdown(); + } + + if (pendingRotatorRetry != null) { + pendingRotatorRetry.cancel(false); + pendingRotatorRetry = null; + } + + rotatorCommandScheduler.shutdownNow(); + } + private void cancelTimer(Timer timer) { if (timer != null) { timer.cancel(); diff --git a/src/main/java/kst4contest/controller/On4KstConnectionManager.java b/src/main/java/kst4contest/controller/On4KstConnectionManager.java index a60e351a..64598999 100644 --- a/src/main/java/kst4contest/controller/On4KstConnectionManager.java +++ b/src/main/java/kst4contest/controller/On4KstConnectionManager.java @@ -139,6 +139,20 @@ final class On4KstConnectionManager { scheduler.execute(() -> openConnection(token)); } + /** + * Stops the supervisor thread of this manager for good. + * + *{@link #stopByUser()} only ends the current ON4KST session; the periodic + * session monitor keeps running. That is correct while the application lives, but a + * manager belonging to a discarded runtime must release its thread, otherwise every + * operator profile switch would leave another supervisor behind holding the dead + * controller.
+ */ + void shutdown() { + scheduler.shutdownNow(); + LOGGER.fine("ON4KST connection supervisor shut down"); + } + /** * Stops the current session and invalidates every scheduled callback or reconnect * belonging to it. diff --git a/src/main/java/kst4contest/controller/SkedReminderService.java b/src/main/java/kst4contest/controller/SkedReminderService.java index 99e16f90..aaa4f83d 100644 --- a/src/main/java/kst4contest/controller/SkedReminderService.java +++ b/src/main/java/kst4contest/controller/SkedReminderService.java @@ -33,6 +33,24 @@ public final class SkedReminderService { this.controller = controller; } + /** + * Cancels every armed reminder and stops the scheduler thread. + * + *Called when the runtime that owns this service is torn down, so a discarded + * runtime does not keep a thread and pending reminders alive.
+ */ + public void shutdown() { + + for (ListNeeded when the runtime owning this bridge is discarded, for example during an + * operator profile switch. A running {@link PauseTransition} would otherwise keep + * firing into a dead user interface.
+ */ + public void uninstall() { + + refreshCoalescer.stop(); + + stationMapView.setOnCallsignRawSelected(null); + stationMapView.setOnTriggerClusterSpot(null); + stationMapView.setOnResetView(null); + + if (chatMemberListListener != null) { + chatController.getLst_chatMemberSortedFilteredList().removeListener(chatMemberListListener); + chatMemberListListener = null; + } + + if (selectedChatMemberListener != null) { + chatController.getScoreService().selectedChatMemberProperty() + .removeListener(selectedChatMemberListener); + selectedChatMemberListener = null; + } + + if (antennaDirectionListener != null) { + chatController.getChatPreferences().getActualQTF().removeListener(antennaDirectionListener); + antennaDirectionListener = null; + } + + if (filterPredicateListener != null) { + chatController.getLst_chatMemberListFilterPredicates().removeListener(filterPredicateListener); + filterPredicateListener = null; + } + } + private void handleMapReset() { Runnable resetAction = () -> { /* diff --git a/src/main/java/kst4contest/view/map/StationMapView.java b/src/main/java/kst4contest/view/map/StationMapView.java index a9f4cc4f..6c4bb98c 100644 --- a/src/main/java/kst4contest/view/map/StationMapView.java +++ b/src/main/java/kst4contest/view/map/StationMapView.java @@ -244,6 +244,25 @@ public final class StationMapView { stage.hide(); } + /** + * Releases every resource this map window owns. + * + *The tile proxy is a local server socket with its own thread pool. It used to + * live until the process ended, which was harmless while the map existed exactly + * once per process. A runtime that is discarded, for example during an operator + * profile switch, has to hand it back.
+ */ + public void dispose() { + + if (tileProxyServer != null) { + tileProxyServer.stop(); + tileProxyServer = null; + } + + webEngine.load(null); + stage.close(); + } + public boolean isShowing() { return stage.isShowing(); }