From a6b1b7eb016fb5eea06257fa97c5e65fe279713c Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 20 Aug 2025 08:33:55 +0200 Subject: [PATCH 01/23] First steps... --- app/logbook/olog/ui/pom.xml | 10 ++++ .../ui/write/LogEntryEditorController.java | 50 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/app/logbook/olog/ui/pom.xml b/app/logbook/olog/ui/pom.xml index e320463bb0..f4511cb46e 100644 --- a/app/logbook/olog/ui/pom.xml +++ b/app/logbook/olog/ui/pom.xml @@ -39,6 +39,16 @@ app-logbook-olog-client-es 5.0.3-SNAPSHOT + + org.springframework + spring-websocket + 5.3.22 + + + org.springframework + spring-messaging + 5.3.22 + org.jfxtras jfxtras-agenda diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java index a4ae21527c..56e147ad30 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.SimpleType; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.ReadOnlyBooleanProperty; @@ -58,8 +59,10 @@ import javafx.scene.paint.Color; +import java.lang.reflect.Type; import java.net.MalformedURLException; import java.net.URL; +import java.util.concurrent.ExecutionException; import java.util.regex.Pattern; import java.util.regex.Matcher; import org.phoebus.logbook.olog.ui.LogbookUIPreferences; @@ -94,6 +97,15 @@ import org.phoebus.ui.dialog.ListSelectionPopOver; import org.phoebus.ui.javafx.ImageCache; import org.phoebus.util.time.TimestampFormats; +import org.springframework.messaging.converter.StringMessageConverter; +import org.springframework.messaging.simp.stomp.StompFrameHandler; +import org.springframework.messaging.simp.stomp.StompHeaders; +import org.springframework.messaging.simp.stomp.StompSession; +import org.springframework.messaging.simp.stomp.StompSessionHandler; +import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; +import org.springframework.web.socket.client.WebSocketClient; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.messaging.WebSocketStompClient; import java.time.Instant; import java.util.ArrayList; @@ -544,6 +556,8 @@ public LogTemplate fromString(String name) { setupTextAreaContextMenu(); + doStompStuff(); + } /** @@ -1066,4 +1080,40 @@ private void loadTemplate(LogTemplate logTemplate) { selectedLogbooks.forEach(l -> updateDropDown(logbookDropDown, l, false)); } } + + private void doStompStuff(){ + WebSocketClient webSocketClient = new StandardWebSocketClient(); + WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient); + stompClient.setMessageConverter(new StringMessageConverter()); + String url = "ws://localhost:8080/websocket"; + StompSessionHandler sessionHandler = new MyStompSessionHandler(); + try { + StompSession stompSession = stompClient.connect(url, sessionHandler).get(); + stompSession.subscribe("/messages", new StompFrameHandler() { + @Override + public Type getPayloadType(StompHeaders headers) { + return String.class; + } + + @Override + public void handleFrame(StompHeaders headers, Object payload) { + System.out.println(payload); + } + }); + StompSession.Receiptable receiptable = stompSession.send("/websocket/echo", "Hello World"); + receiptable.getReceiptId(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + private static class MyStompSessionHandler extends StompSessionHandlerAdapter { + + @Override + public void afterConnected(StompSession session, StompHeaders connectedHeaders) { + System.out.println(); + } + } } From 2d5063791209dee4d572ffbc30c9e029d09e2a1e Mon Sep 17 00:00:00 2001 From: georgweiss Date: Fri, 22 Aug 2025 15:59:22 +0200 Subject: [PATCH 02/23] Initial step to Olog + web sockets --- .../olog/ui/LogEntryTableViewController.java | 108 +++++++++++++++++- .../olog/ui/LogbookSearchController.java | 11 +- .../olog/ui/websocket/MessageType.java | 25 ++++ .../olog/ui/websocket/WebSocketMessage.java | 22 ++++ .../ui/write/LogEntryEditorController.java | 4 +- 5 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/MessageType.java create mode 100644 app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/WebSocketMessage.java diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java index f1fad641dd..11d7bbdc51 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java @@ -1,5 +1,7 @@ package org.phoebus.logbook.olog.ui; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; @@ -49,8 +51,12 @@ import org.phoebus.logbook.olog.ui.query.OlogQuery; import org.phoebus.logbook.olog.ui.query.OlogQueryManager; import org.phoebus.logbook.olog.ui.spi.Decoration; +import org.phoebus.logbook.olog.ui.websocket.MessageType; +import org.phoebus.logbook.olog.ui.websocket.WebSocketMessage; import org.phoebus.logbook.olog.ui.write.EditMode; +import org.phoebus.logbook.olog.ui.write.LogEntryEditorController; import org.phoebus.logbook.olog.ui.write.LogEntryEditorStage; +import org.phoebus.olog.es.api.Preferences; import org.phoebus.olog.es.api.model.LogGroupProperty; import org.phoebus.olog.es.api.model.OlogLog; import org.phoebus.security.store.SecureStore; @@ -58,12 +64,27 @@ import org.phoebus.security.tokens.ScopedAuthenticationToken; import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; +import org.springframework.lang.Nullable; +import org.springframework.messaging.converter.StringMessageConverter; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompFrameHandler; +import org.springframework.messaging.simp.stomp.StompHeaders; +import org.springframework.messaging.simp.stomp.StompSession; +import org.springframework.messaging.simp.stomp.StompSessionHandler; +import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.web.socket.client.WebSocketClient; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.messaging.WebSocketStompClient; import java.io.IOException; +import java.lang.reflect.Type; +import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -116,11 +137,14 @@ public class LogEntryTableViewController extends LogbookSearchController { private Label openAdvancedSearchLabel; // Model private SearchResult searchResult; + + private ObjectMapper objectMapper = new ObjectMapper(); + /** * List of selected log entries */ private final ObservableList selectedLogEntries = FXCollections.observableArrayList(); - private final Logger logger = Logger.getLogger(LogEntryTableViewController.class.getName()); + private static final Logger logger = Logger.getLogger(LogEntryTableViewController.class.getName()); private final SimpleBooleanProperty showDetails = new SimpleBooleanProperty(); @@ -321,6 +345,8 @@ public void updateItem(TableViewListItem logEntry, boolean empty) { Messages.AdvancedSearchHide : Messages.AdvancedSearchOpen, advancedSearchVisibile)); + connectWebSocket(); + search(); } @@ -388,8 +414,8 @@ public void search() { searchResult1 -> { searchInProgress.set(false); setSearchResult(searchResult1); - logger.log(Level.INFO, "Starting periodic search: " + queryString); - periodicSearch(params, this::setSearchResult); + //logger.log(Level.INFO, "Starting periodic search: " + queryString); + //periodicSearch(params, this::setSearchResult); List queries = ologQueryManager.getQueries(); Platform.runLater(() -> { ologQueries.setAll(queries); @@ -635,4 +661,80 @@ public boolean selectLogEntry(LogEntry logEntry) { } return false; } + + private void connectWebSocket(){ + JobManager.schedule("Connect to web socket", monitor -> { + WebSocketClient webSocketClient = new StandardWebSocketClient(); + WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient); + stompClient.setMessageConverter(new StringMessageConverter()); + ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.initialize(); + stompClient.setTaskScheduler(threadPoolTaskScheduler); + stompClient.setDefaultHeartbeat(new long[]{10000, 10000}); + String baseUrl = Preferences.olog_url; + URI uri = URI.create(baseUrl); + String scheme = uri.getScheme(); + String host = uri.getHost(); + int port = uri.getPort(); + String path = uri.getPath(); + if(path.endsWith("/")){ + path = path.substring(0, path.length() - 1); + } + String webSocketScheme = scheme.toLowerCase().startsWith("https") ? "wss" : "ws"; + String webSocketUrl = webSocketScheme + "://" + host + (port > -1 ? (":" + port) : "") + "/web-socket"; + StompSessionHandler sessionHandler = new LogEntryTableViewController.MyStompSessionHandler(); + try { + stompSession = stompClient.connect(webSocketUrl, sessionHandler).get(); + stompSession.subscribe(path + "/web-socket/messages", new StompFrameHandler() { + @Override + public Type getPayloadType(StompHeaders headers) { + return String.class; + } + + @Override + public void handleFrame(StompHeaders headers, Object payload) { + logger.log(Level.INFO, "Handling subscription frame: " + payload); + try { + WebSocketMessage webSocketMessage = objectMapper.readValue(payload.toString(), WebSocketMessage.class); + if(webSocketMessage.messageType().equals(MessageType.NEW_LOG_ENTRY)){ + search(); + } + } catch (JsonProcessingException e) { + logger.log(Level.WARNING, "Unable to deserialize payload " + payload.toString(), e); + } + } + }); + //stompSession.send(path + "/web-socket/echo", "Hello World"); + } catch (Exception e) { + logger.log(Level.WARNING, "Web socket connection attempt failed", e); + } + }); + } + + private static class MyStompSessionHandler extends StompSessionHandlerAdapter { + + + @Override + public void handleFrame(StompHeaders headers, @Nullable Object payload) { + if(payload != null){ + logger.log(Level.INFO, "WebSocket frame received: " + payload); + } + } + + @Override + public void afterConnected(StompSession session, StompHeaders connectedHeaders) { + logger.log(Level.INFO, "Connected to web socket"); + } + + @Override + public void handleException(StompSession session, @Nullable StompCommand command, + StompHeaders headers, byte[] payload, Throwable exception) { + logger.log(Level.WARNING, "Exception encountered", exception); + } + + @Override + public void handleTransportError(StompSession session, Throwable exception) { + logger.log(Level.WARNING, "Handling web socket transport error: " + exception.getMessage(), exception); + } + } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java index ad62ca354c..bbdae65ed9 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java @@ -5,6 +5,7 @@ import org.phoebus.logbook.LogClient; import org.phoebus.logbook.LogEntry; import org.phoebus.logbook.SearchResult; +import org.springframework.messaging.simp.stomp.StompSession; import java.util.List; import java.util.Map; @@ -14,6 +15,8 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; /** * A basic controller for any ui performing logbook queries. The @@ -32,6 +35,8 @@ public abstract class LogbookSearchController { protected final SimpleBooleanProperty searchInProgress = new SimpleBooleanProperty(false); private static final int SEARCH_JOB_INTERVAL = 30; // seconds + protected StompSession stompSession; + public void setClient(LogClient client) { this.client = client; } @@ -91,6 +96,10 @@ private void cancelPeriodSearch() { * Utility method to cancel any ongoing periodic search jobs. */ public void shutdown() { - cancelPeriodSearch(); + //cancelPeriodSearch(); + if(stompSession != null && stompSession.isConnected()){ + Logger.getLogger(LogbookSearchController.class.getName()).log(Level.INFO, "Disconnecting from web socket"); + stompSession.disconnect(); + } } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/MessageType.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/MessageType.java new file mode 100644 index 0000000000..b7406c1ff1 --- /dev/null +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/MessageType.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2020 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.phoebus.logbook.olog.ui.websocket; + +public enum MessageType { + NEW_LOG_ENTRY, + LOG_ENTRY_UPDATED, + SHOW_BANNER +} diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/WebSocketMessage.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/WebSocketMessage.java new file mode 100644 index 0000000000..299304bcf8 --- /dev/null +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/websocket/WebSocketMessage.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2020 European Spallation Source ERIC. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.phoebus.logbook.olog.ui.websocket; + +public record WebSocketMessage(MessageType messageType, String payload) { +} diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java index 56e147ad30..9302de31b2 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java @@ -556,7 +556,7 @@ public LogTemplate fromString(String name) { setupTextAreaContextMenu(); - doStompStuff(); + //doStompStuff(); } @@ -1085,7 +1085,7 @@ private void doStompStuff(){ WebSocketClient webSocketClient = new StandardWebSocketClient(); WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient); stompClient.setMessageConverter(new StringMessageConverter()); - String url = "ws://localhost:8080/websocket"; + String url = "ws://localhost:8080/web-socket"; StompSessionHandler sessionHandler = new MyStompSessionHandler(); try { StompSession stompSession = stompClient.connect(url, sessionHandler).get(); From a672fe2cbf98e76f1a270af1af2aa5481ebeaa6a Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 26 Aug 2025 10:58:58 +0200 Subject: [PATCH 03/23] Moved Olog web socket client to core-websocket --- .../org/phoebus/core/websocket/WebSocketMessageHandler.java | 3 +++ .../core/websocket/springframework/WebSocketClientService.java | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketMessageHandler.java create mode 100644 core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketMessageHandler.java b/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketMessageHandler.java new file mode 100644 index 0000000000..96715d2494 --- /dev/null +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketMessageHandler.java @@ -0,0 +1,3 @@ +package org.phoebus.core.websocket;public interface WebSocketMessageHandler +{ +} diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java new file mode 100644 index 0000000000..b6b141da25 --- /dev/null +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java @@ -0,0 +1,3 @@ +package org.phoebus.core.websocket.springframework;public class WebSocketClientService +{ +} From e2794d4578017669b58cc41800c501758a6b90e0 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 26 Aug 2025 11:01:38 +0200 Subject: [PATCH 04/23] Olog web socket: use core-websocket --- app/logbook/olog/ui/pom.xml | 15 +- .../olog/ui/LogEntryTableViewController.java | 151 ++++++--------- .../org/phoebus/logbook/olog/ui/Messages.java | 2 + .../logbook/olog/ui/LogEntryTableView.fxml | 112 +++++------ .../logbook/olog/ui/messages.properties | 4 +- core/websocket/pom.xml | 20 ++ .../websocket/WebSocketMessageHandler.java | 11 +- .../WebSocketClientService.java | 175 +++++++++++++++++- 8 files changed, 318 insertions(+), 172 deletions(-) diff --git a/app/logbook/olog/ui/pom.xml b/app/logbook/olog/ui/pom.xml index f4511cb46e..9b26eb88b8 100644 --- a/app/logbook/olog/ui/pom.xml +++ b/app/logbook/olog/ui/pom.xml @@ -14,6 +14,11 @@ core-framework 5.0.3-SNAPSHOT + + org.phoebus + core-websocket + 5.0.3-SNAPSHOT + org.phoebus core-ui @@ -39,16 +44,6 @@ app-logbook-olog-client-es 5.0.3-SNAPSHOT - - org.springframework - spring-websocket - 5.3.22 - - - org.springframework - spring-messaging - 5.3.22 - org.jfxtras jfxtras-agenda diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java index 11d7bbdc51..a0fdf388b0 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java @@ -41,6 +41,8 @@ import javafx.util.Callback; import javafx.util.Duration; import javafx.util.StringConverter; +import org.phoebus.core.websocket.WebSocketMessageHandler; +import org.phoebus.core.websocket.springframework.WebSocketClientService; import org.phoebus.framework.jobs.JobManager; import org.phoebus.logbook.LogClient; import org.phoebus.logbook.LogEntry; @@ -54,7 +56,6 @@ import org.phoebus.logbook.olog.ui.websocket.MessageType; import org.phoebus.logbook.olog.ui.websocket.WebSocketMessage; import org.phoebus.logbook.olog.ui.write.EditMode; -import org.phoebus.logbook.olog.ui.write.LogEntryEditorController; import org.phoebus.logbook.olog.ui.write.LogEntryEditorStage; import org.phoebus.olog.es.api.Preferences; import org.phoebus.olog.es.api.model.LogGroupProperty; @@ -64,27 +65,13 @@ import org.phoebus.security.tokens.ScopedAuthenticationToken; import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; -import org.springframework.lang.Nullable; -import org.springframework.messaging.converter.StringMessageConverter; -import org.springframework.messaging.simp.stomp.StompCommand; -import org.springframework.messaging.simp.stomp.StompFrameHandler; -import org.springframework.messaging.simp.stomp.StompHeaders; -import org.springframework.messaging.simp.stomp.StompSession; -import org.springframework.messaging.simp.stomp.StompSessionHandler; -import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.web.socket.client.WebSocketClient; -import org.springframework.web.socket.client.standard.StandardWebSocketClient; -import org.springframework.web.socket.messaging.WebSocketStompClient; import java.io.IOException; -import java.lang.reflect.Type; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -96,7 +83,7 @@ * * @author Kunal Shroff */ -public class LogEntryTableViewController extends LogbookSearchController { +public class LogEntryTableViewController extends LogbookSearchController implements WebSocketMessageHandler { @FXML @SuppressWarnings("unused") @@ -140,6 +127,11 @@ public class LogEntryTableViewController extends LogbookSearchController { private ObjectMapper objectMapper = new ObjectMapper(); + @FXML + private Label autoUpdateStatusLabel; + + private SimpleBooleanProperty webSocketConnected = new SimpleBooleanProperty(); + /** * List of selected log entries */ @@ -148,7 +140,9 @@ public class LogEntryTableViewController extends LogbookSearchController { private final SimpleBooleanProperty showDetails = new SimpleBooleanProperty(); - private final SimpleBooleanProperty advancedSearchVisibile = new SimpleBooleanProperty(false); + private final SimpleBooleanProperty advancedSearchVisible = new SimpleBooleanProperty(false); + + private WebSocketClientService webSocketClientService; /** * Constructor. @@ -341,9 +335,20 @@ public void updateItem(TableViewListItem logEntry, boolean empty) { openAdvancedSearchLabel.setOnMouseClicked(e -> resize()); openAdvancedSearchLabel.textProperty() - .bind(Bindings.createStringBinding(() -> advancedSearchVisibile.get() ? + .bind(Bindings.createStringBinding(() -> advancedSearchVisible.get() ? Messages.AdvancedSearchHide : Messages.AdvancedSearchOpen, - advancedSearchVisibile)); + advancedSearchVisible)); + + webSocketConnected.addListener((obs, o, n) -> { + if(n){ + autoUpdateStatusLabel.setStyle("-fx-text-fill: black;"); + autoUpdateStatusLabel.setText(Messages.AutoRefreshOn); + } + else{ + autoUpdateStatusLabel.setStyle("-fx-text-fill: red;"); + autoUpdateStatusLabel.setText(Messages.AutoRefreshOff); + } + }); connectWebSocket(); @@ -359,13 +364,13 @@ public void resize() { if (!moving.compareAndExchangeAcquire(false, true)) { Duration cycleDuration = Duration.millis(400); Timeline timeline; - if (advancedSearchVisibile.get()) { + if (advancedSearchVisible.get()) { query.disableProperty().set(false); KeyValue kv = new KeyValue(advancedSearchViewController.getPane().minWidthProperty(), 0); KeyValue kv2 = new KeyValue(advancedSearchViewController.getPane().maxWidthProperty(), 0); timeline = new Timeline(new KeyFrame(cycleDuration, kv, kv2)); timeline.setOnFinished(event -> { - advancedSearchVisibile.set(false); + advancedSearchVisible.set(false); moving.set(false); search(); }); @@ -376,7 +381,7 @@ public void resize() { KeyValue kv2 = new KeyValue(advancedSearchViewController.getPane().prefWidthProperty(), width); timeline = new Timeline(new KeyFrame(cycleDuration, kv, kv2)); timeline.setOnFinished(event -> { - advancedSearchVisibile.set(true); + advancedSearchVisible.set(true); moving.set(false); query.disableProperty().set(true); }); @@ -522,7 +527,7 @@ private void createLogEntryGroup() { } }); } - +q @FXML @SuppressWarnings("unused") public void goToFirstPage() { @@ -635,6 +640,10 @@ public void logEntryChanged(LogEntry logEntry) { setLogEntry(logEntry); } + private void logEntryChanged(String logEntryId){ + search(); + } + protected LogEntry getLogEntry() { return logEntryDisplayController.getLogEntry(); } @@ -663,78 +672,38 @@ public boolean selectLogEntry(LogEntry logEntry) { } private void connectWebSocket(){ - JobManager.schedule("Connect to web socket", monitor -> { - WebSocketClient webSocketClient = new StandardWebSocketClient(); - WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient); - stompClient.setMessageConverter(new StringMessageConverter()); - ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); - threadPoolTaskScheduler.initialize(); - stompClient.setTaskScheduler(threadPoolTaskScheduler); - stompClient.setDefaultHeartbeat(new long[]{10000, 10000}); - String baseUrl = Preferences.olog_url; - URI uri = URI.create(baseUrl); - String scheme = uri.getScheme(); - String host = uri.getHost(); - int port = uri.getPort(); - String path = uri.getPath(); - if(path.endsWith("/")){ - path = path.substring(0, path.length() - 1); - } - String webSocketScheme = scheme.toLowerCase().startsWith("https") ? "wss" : "ws"; - String webSocketUrl = webSocketScheme + "://" + host + (port > -1 ? (":" + port) : "") + "/web-socket"; - StompSessionHandler sessionHandler = new LogEntryTableViewController.MyStompSessionHandler(); - try { - stompSession = stompClient.connect(webSocketUrl, sessionHandler).get(); - stompSession.subscribe(path + "/web-socket/messages", new StompFrameHandler() { - @Override - public Type getPayloadType(StompHeaders headers) { - return String.class; - } - - @Override - public void handleFrame(StompHeaders headers, Object payload) { - logger.log(Level.INFO, "Handling subscription frame: " + payload); - try { - WebSocketMessage webSocketMessage = objectMapper.readValue(payload.toString(), WebSocketMessage.class); - if(webSocketMessage.messageType().equals(MessageType.NEW_LOG_ENTRY)){ - search(); - } - } catch (JsonProcessingException e) { - logger.log(Level.WARNING, "Unable to deserialize payload " + payload.toString(), e); - } - } - }); - //stompSession.send(path + "/web-socket/echo", "Hello World"); - } catch (Exception e) { - logger.log(Level.WARNING, "Web socket connection attempt failed", e); - } + String baseUrl = Preferences.olog_url; + URI uri = URI.create(baseUrl); + String scheme = uri.getScheme(); + String host = uri.getHost(); + int port = uri.getPort(); + String path = uri.getPath(); + if(path.endsWith("/")){ + path = path.substring(0, path.length() - 1); + } + String webSocketScheme = scheme.toLowerCase().startsWith("https") ? "wss" : "ws"; + String webSocketUrl = webSocketScheme + "://" + host + (port > -1 ? (":" + port) : "") + path; + + webSocketClientService = new WebSocketClientService(() -> { + logger.log(Level.INFO, "Connected to web socket on " + webSocketUrl); + webSocketConnected.set(true); + }, () -> { + logger.log(Level.INFO, "Disconnected from web socket on " + webSocketUrl); + webSocketConnected.set(false); }); + webSocketClientService.addWebSocketMessageHandler(this); + webSocketClientService.connect(webSocketUrl); } - private static class MyStompSessionHandler extends StompSessionHandlerAdapter { - - - @Override - public void handleFrame(StompHeaders headers, @Nullable Object payload) { - if(payload != null){ - logger.log(Level.INFO, "WebSocket frame received: " + payload); + @Override + public void handleWebSocketMessage(String message){ + try { + WebSocketMessage webSocketMessage = objectMapper.readValue(message, WebSocketMessage.class); + if(webSocketMessage.messageType().equals(MessageType.NEW_LOG_ENTRY)){ + search(); } - } - - @Override - public void afterConnected(StompSession session, StompHeaders connectedHeaders) { - logger.log(Level.INFO, "Connected to web socket"); - } - - @Override - public void handleException(StompSession session, @Nullable StompCommand command, - StompHeaders headers, byte[] payload, Throwable exception) { - logger.log(Level.WARNING, "Exception encountered", exception); - } - - @Override - public void handleTransportError(StompSession session, Throwable exception) { - logger.log(Level.WARNING, "Handling web socket transport error: " + exception.getMessage(), exception); + } catch (JsonProcessingException e) { + logger.log(Level.WARNING, "Unable to deserialize message \"" + message + "\""); } } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java index 60d2bd92cc..b92638a38b 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java @@ -23,6 +23,8 @@ public class Messages AttachmentsDirectoryNotWritable, AttachmentsFileNotDirectory, AttachmentsNoStorage, + AutoRefreshOn, + AutoRefreshOff, AvailableTemplates, Back, CloseRequestHeader, diff --git a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryTableView.fxml b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryTableView.fxml index 55dbbcbf09..35c827d0d4 100644 --- a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryTableView.fxml +++ b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryTableView.fxml @@ -1,113 +1,94 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + - + - + - - + + - - - + + + - + - + - + - - - + + + - + - + - + - + - + - + - + - + @@ -119,13 +100,12 @@ - + - + diff --git a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties index d07f1c680a..7573638fcc 100644 --- a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties +++ b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties @@ -11,11 +11,13 @@ AttachmentsDirectoryNotWritable=Attachments directory {0} is not writable ArchivedSaveFailed=Failed to save archived log entries to file AttachmentsNoStorage=No storage for attachments ArchivedLaunchExternalAppFailed=Unable to launch external application to view file "{0}". Please check your settings. -Author=Author: Attachments=Attachments AttachmentsDirectoryFailedCreate=Failed to create directory {0} for attachments AttachmentsFileNotDirectory=File {0} exists but is not a directory AttachmentsSearchProperty=Attachments: +Author=Author: +AutoRefreshOn=Auto Refresh: ON +AutoRefreshOff=Auto Refresh: OFF AvailableTemplates=Available Templates Back=Back BrowseButton=Browse diff --git a/core/websocket/pom.xml b/core/websocket/pom.xml index 6bbe829423..cc29c6313b 100644 --- a/core/websocket/pom.xml +++ b/core/websocket/pom.xml @@ -12,7 +12,27 @@ 5.0.3-SNAPSHOT + + 5.3.22 + + + + org.phoebus + core-framework + 5.0.3-SNAPSHOT + + + org.springframework + spring-websocket + ${spring.framework.version} + + + org.springframework + spring-messaging + ${spring.framework.version} + + diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketMessageHandler.java b/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketMessageHandler.java index 96715d2494..7299c2a2fd 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketMessageHandler.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/WebSocketMessageHandler.java @@ -1,3 +1,10 @@ -package org.phoebus.core.websocket;public interface WebSocketMessageHandler -{ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.core.websocket; + +public interface WebSocketMessageHandler { + + void handleWebSocketMessage(String message); } diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java index b6b141da25..82ffd68b0c 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java @@ -1,3 +1,174 @@ -package org.phoebus.core.websocket.springframework;public class WebSocketClientService -{ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.core.websocket.springframework; + +import org.phoebus.core.websocket.WebSocketMessageHandler; +import org.phoebus.framework.jobs.JobManager; +import org.springframework.lang.Nullable; +import org.springframework.messaging.converter.StringMessageConverter; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompFrameHandler; +import org.springframework.messaging.simp.stomp.StompHeaders; +import org.springframework.messaging.simp.stomp.StompSession; +import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.web.socket.client.WebSocketClient; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.messaging.WebSocketStompClient; + +import java.lang.reflect.Type; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Service wrapping a STOMP web socket client based on Spring Framework APIs. + * Features: + *
    + *
  • Manages keep alive as supported by the Spring Framework libs
  • + *
  • Passes string messages to registered {@link WebSocketMessageHandler}a
  • + *
  • Calls {@link Runnable}s (if specified) to signal connection or disconnection
  • + *
  • Attempts to auto-reconnect if web socket is closed by remote peer.
  • + *
+ *

+ * All messages received from the remote peer are strings only, but may be JSON formatted. + * In other words, depending on the use case a client may wish to deserialize messages. + *

+ * Since web socket URL paths are currently hard coded, a remote peer (e.g. Spring Framework STOMP web socket service) must: + *

    + *
  • Publish a connect URL like ws(s)://host:port/path/web-socket, where path is optional.
  • + *
  • Publish a topic named /path/web-socket/messages, where path is optional.
  • + *
+ *

+ */ +public class WebSocketClientService { + + private String baseUrl; + private StompSession stompSession; + private Runnable connectCallback; + private Runnable disconnectCallback; + private final List webSocketMessageHandlers = Collections.synchronizedList(new ArrayList<>()); + + private static final Logger logger = Logger.getLogger(WebSocketClientService.class.getName()); + + public WebSocketClientService(){ + + } + + /** + * @param connectCallback The non-null method called when connection to the remote web socket has been successfully established. + * @param disconnectCallback The non-null method called when connection to the remote web socket has been lost, e.g. + * remote peer has been shut down. + */ + public WebSocketClientService(Runnable connectCallback, Runnable disconnectCallback) { + this.connectCallback = connectCallback; + this.disconnectCallback = disconnectCallback; + } + + public void setConnectCallback(Runnable connectCallback){ + this.connectCallback = connectCallback; + } + + public void setDisconnectCallback(Runnable disconnectCallback){ + this.disconnectCallback = disconnectCallback; + } + + public void addWebSocketMessageHandler(WebSocketMessageHandler webSocketMessageHandler) { + webSocketMessageHandlers.add(webSocketMessageHandler); + } + + public void removeWebSocketMessageHandler(WebSocketMessageHandler webSocketMessageHandler) { + webSocketMessageHandlers.remove(webSocketMessageHandler); + } + + private void connect(){ + connect(baseUrl); + } + + /** + * Attempts to connect to remote web socket. + * @param baseUrl The "base" URL of the web socket peer, must start with ws:// or wss://. Note that "web-socket" will be + * appended to this URL. Further, the URL may contain a path, e.g. ws://host:port/path. + * @throws IllegalArgumentException if baseUrl is null, empty or does not start with ws:// or wss://. + */ + public void connect(String baseUrl) { + if(baseUrl == null || baseUrl.isEmpty() || (!baseUrl.toLowerCase().startsWith("ws://") && !baseUrl.toLowerCase().startsWith("wss://"))){ + throw new IllegalArgumentException("URL \"" + baseUrl + "\" is not valid"); + } + this.baseUrl = baseUrl; + URI uri = URI.create(baseUrl); + String scheme = uri.getScheme(); + String host = uri.getHost(); + int port = uri.getPort(); + String path = uri.getPath(); + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + WebSocketClient webSocketClient = new StandardWebSocketClient(); + WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient); + stompClient.setMessageConverter(new StringMessageConverter()); + ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.initialize(); + stompClient.setTaskScheduler(threadPoolTaskScheduler); + stompClient.setDefaultHeartbeat(new long[]{30000, 30000}); + StompSessionHandler sessionHandler = new StompSessionHandler(); + String _path = path; + String webSocketUrl = scheme + "://" + host + (port > -1 ? (":" + port) : "") + "/web-socket"; + JobManager.schedule("Connect to web socket", monitor -> { + stompSession = stompClient.connect(webSocketUrl, sessionHandler).get(); + logger.log(Level.INFO, "Subscribing to messages on " + _path + "/web-socket/messages"); + stompSession.subscribe(_path + "/web-socket/messages", new StompFrameHandler() { + @Override + public Type getPayloadType(StompHeaders headers) { + return String.class; + } + + @Override + public void handleFrame(StompHeaders headers, Object payload) { + logger.log(Level.INFO, "Handling subscription frame: " + payload); + webSocketMessageHandlers.forEach(h -> h.handleWebSocketMessage((String) payload)); + } + }); + }); + } + + /** + * Handler used to perform housekeeping... + */ + private class StompSessionHandler extends StompSessionHandlerAdapter { + + @Override + public void handleFrame(StompHeaders headers, @Nullable Object payload) { + if (payload != null) { + logger.log(Level.INFO, "WebSocket frame received: " + payload); + } + } + + @Override + public void afterConnected(StompSession session, StompHeaders connectedHeaders) { + logger.log(Level.INFO, "Connected to web socket"); + if (connectCallback != null) { + connectCallback.run(); + } + } + + @Override + public void handleException(StompSession session, @Nullable StompCommand command, + StompHeaders headers, byte[] payload, Throwable exception) { + logger.log(Level.WARNING, "Exception encountered", exception); + } + + @Override + public void handleTransportError(StompSession session, Throwable exception) { + logger.log(Level.WARNING, "Handling web socket transport error: " + exception.getMessage(), exception); + if (disconnectCallback != null) { + disconnectCallback.run(); + } + } + } } From 79b2f7be007d22c28b79e2d4a159149dd80d61fb Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 26 Aug 2025 16:07:51 +0200 Subject: [PATCH 05/23] Make sure web socket URL considers path (context) if specified --- .../phoebus/logbook/olog/ui/LogEntryTableViewController.java | 2 +- .../core/websocket/springframework/WebSocketClientService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java index a0fdf388b0..5b6de03442 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java @@ -527,7 +527,7 @@ private void createLogEntryGroup() { } }); } -q + @FXML @SuppressWarnings("unused") public void goToFirstPage() { diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java index 82ffd68b0c..742f37821d 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java @@ -118,7 +118,7 @@ public void connect(String baseUrl) { stompClient.setDefaultHeartbeat(new long[]{30000, 30000}); StompSessionHandler sessionHandler = new StompSessionHandler(); String _path = path; - String webSocketUrl = scheme + "://" + host + (port > -1 ? (":" + port) : "") + "/web-socket"; + String webSocketUrl = scheme + "://" + host + (port > -1 ? (":" + port) : "") + _path + "/web-socket"; JobManager.schedule("Connect to web socket", monitor -> { stompSession = stompClient.connect(webSocketUrl, sessionHandler).get(); logger.log(Level.INFO, "Subscribing to messages on " + _path + "/web-socket/messages"); From e6ef4f24f9b11ea0c6332e8585349f938cc7731e Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 27 Aug 2025 12:42:53 +0200 Subject: [PATCH 06/23] Adding STOMP session reconnect logic --- .../olog/ui/LogEntryTableViewController.java | 6 +- .../WebSocketClientService.java | 101 +++++++++++++----- 2 files changed, 77 insertions(+), 30 deletions(-) diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java index 5b6de03442..7fe7b1f3cc 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java @@ -686,10 +686,10 @@ private void connectWebSocket(){ webSocketClientService = new WebSocketClientService(() -> { logger.log(Level.INFO, "Connected to web socket on " + webSocketUrl); - webSocketConnected.set(true); + Platform.runLater(() -> webSocketConnected.set(true)); }, () -> { logger.log(Level.INFO, "Disconnected from web socket on " + webSocketUrl); - webSocketConnected.set(false); + Platform.runLater(() -> webSocketConnected.set(false)); }); webSocketClientService.addWebSocketMessageHandler(this); webSocketClientService.connect(webSocketUrl); @@ -701,6 +701,8 @@ public void handleWebSocketMessage(String message){ WebSocketMessage webSocketMessage = objectMapper.readValue(message, WebSocketMessage.class); if(webSocketMessage.messageType().equals(MessageType.NEW_LOG_ENTRY)){ search(); + webSocketClientService.close(); + webSocketClientService.sendEcho("Hello World"); } } catch (JsonProcessingException e) { logger.log(Level.WARNING, "Unable to deserialize message \"" + message + "\""); diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java index 742f37821d..d6684ed75d 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java @@ -5,9 +5,9 @@ package org.phoebus.core.websocket.springframework; import org.phoebus.core.websocket.WebSocketMessageHandler; -import org.phoebus.framework.jobs.JobManager; import org.springframework.lang.Nullable; import org.springframework.messaging.converter.StringMessageConverter; +import org.springframework.messaging.simp.stomp.ConnectionLostException; import org.springframework.messaging.simp.stomp.StompCommand; import org.springframework.messaging.simp.stomp.StompFrameHandler; import org.springframework.messaging.simp.stomp.StompHeaders; @@ -23,6 +23,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -48,20 +50,28 @@ */ public class WebSocketClientService { - private String baseUrl; + /** + * URL to which the client connects + */ + private String webSocketConnectUrl; + /** + * Path string, depends on service deployment context, e.g. Olog + */ + private String contextPath; private StompSession stompSession; private Runnable connectCallback; private Runnable disconnectCallback; private final List webSocketMessageHandlers = Collections.synchronizedList(new ArrayList<>()); + private final AtomicBoolean attemptReconnect = new AtomicBoolean(); private static final Logger logger = Logger.getLogger(WebSocketClientService.class.getName()); - public WebSocketClientService(){ + public WebSocketClientService() { } /** - * @param connectCallback The non-null method called when connection to the remote web socket has been successfully established. + * @param connectCallback The non-null method called when connection to the remote web socket has been successfully established. * @param disconnectCallback The non-null method called when connection to the remote web socket has been lost, e.g. * remote peer has been shut down. */ @@ -70,11 +80,11 @@ public WebSocketClientService(Runnable connectCallback, Runnable disconnectCallb this.disconnectCallback = disconnectCallback; } - public void setConnectCallback(Runnable connectCallback){ + public void setConnectCallback(Runnable connectCallback) { this.connectCallback = connectCallback; } - public void setDisconnectCallback(Runnable disconnectCallback){ + public void setDisconnectCallback(Runnable disconnectCallback) { this.disconnectCallback = disconnectCallback; } @@ -86,21 +96,26 @@ public void removeWebSocketMessageHandler(WebSocketMessageHandler webSocketMessa webSocketMessageHandlers.remove(webSocketMessageHandler); } - private void connect(){ - connect(baseUrl); + + public void sendEcho(String message) { + stompSession.send(contextPath + "/web-socket/echo", message); + } + + public void close(){ + stompSession.disconnect(); } /** * Attempts to connect to remote web socket. + * * @param baseUrl The "base" URL of the web socket peer, must start with ws:// or wss://. Note that "web-socket" will be * appended to this URL. Further, the URL may contain a path, e.g. ws://host:port/path. * @throws IllegalArgumentException if baseUrl is null, empty or does not start with ws:// or wss://. */ public void connect(String baseUrl) { - if(baseUrl == null || baseUrl.isEmpty() || (!baseUrl.toLowerCase().startsWith("ws://") && !baseUrl.toLowerCase().startsWith("wss://"))){ + if (baseUrl == null || baseUrl.isEmpty() || (!baseUrl.toLowerCase().startsWith("ws://") && !baseUrl.toLowerCase().startsWith("wss://"))) { throw new IllegalArgumentException("URL \"" + baseUrl + "\" is not valid"); } - this.baseUrl = baseUrl; URI uri = URI.create(baseUrl); String scheme = uri.getScheme(); String host = uri.getHost(); @@ -109,6 +124,13 @@ public void connect(String baseUrl) { if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } + this.contextPath = path; + this.webSocketConnectUrl = scheme + "://" + host + (port > -1 ? (":" + port) : "") + this.contextPath + "/web-socket"; + doConnect(); + } + + private void doConnect() { + attemptReconnect.set(true); WebSocketClient webSocketClient = new StandardWebSocketClient(); WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient); stompClient.setMessageConverter(new StringMessageConverter()); @@ -117,28 +139,37 @@ public void connect(String baseUrl) { stompClient.setTaskScheduler(threadPoolTaskScheduler); stompClient.setDefaultHeartbeat(new long[]{30000, 30000}); StompSessionHandler sessionHandler = new StompSessionHandler(); - String _path = path; - String webSocketUrl = scheme + "://" + host + (port > -1 ? (":" + port) : "") + _path + "/web-socket"; - JobManager.schedule("Connect to web socket", monitor -> { - stompSession = stompClient.connect(webSocketUrl, sessionHandler).get(); - logger.log(Level.INFO, "Subscribing to messages on " + _path + "/web-socket/messages"); - stompSession.subscribe(_path + "/web-socket/messages", new StompFrameHandler() { - @Override - public Type getPayloadType(StompHeaders headers) { - return String.class; + new Thread(() -> { + while (attemptReconnect.get()) { + logger.log(Level.INFO, "Attempting web socket connection to " + webSocketConnectUrl); + try { + stompSession = stompClient.connect(this.webSocketConnectUrl, sessionHandler).get(); + stompSession.subscribe(contextPath + "/web-socket/messages", new StompFrameHandler() { + @Override + public Type getPayloadType(StompHeaders headers) { + return String.class; + } + + @Override + public void handleFrame(StompHeaders headers, Object payload) { + logger.log(Level.INFO, "Handling subscription frame: " + payload); + webSocketMessageHandlers.forEach(h -> h.handleWebSocketMessage((String) payload)); + } + }); + } catch (Exception e) { + logger.log(Level.WARNING, "Got exception when trying to connect", e); } - - @Override - public void handleFrame(StompHeaders headers, Object payload) { - logger.log(Level.INFO, "Handling subscription frame: " + payload); - webSocketMessageHandlers.forEach(h -> h.handleWebSocketMessage((String) payload)); + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + logger.log(Level.WARNING, "Got exception when trying to connect", e); } - }); - }); + } + }).start(); } /** - * Handler used to perform housekeeping... + * Handler used to perform housekeeping, e.g. trigger reconnection attempts if connection goes down. */ private class StompSessionHandler extends StompSessionHandlerAdapter { @@ -151,6 +182,7 @@ public void handleFrame(StompHeaders headers, @Nullable Object payload) { @Override public void afterConnected(StompSession session, StompHeaders connectedHeaders) { + attemptReconnect.set(false); logger.log(Level.INFO, "Connected to web socket"); if (connectCallback != null) { connectCallback.run(); @@ -163,9 +195,22 @@ public void handleException(StompSession session, @Nullable StompCommand command logger.log(Level.WARNING, "Exception encountered", exception); } + /** + * Note that this is called both on connection failure and if remote web socket peer + * goes away for whatever reason. + * @param session the client STOMP session + * @param exception the exception that occurred. This is evaluated to determine if a reconnection + * thread should be launched. + */ @Override public void handleTransportError(StompSession session, Throwable exception) { - logger.log(Level.WARNING, "Handling web socket transport error: " + exception.getMessage(), exception); + if(exception instanceof ConnectionLostException){ + logger.log(Level.WARNING, "Connection lost, will attempt to reconnect", exception); + doConnect(); + } + else{ + logger.log(Level.WARNING, "Handling transport exception", exception); + } if (disconnectCallback != null) { disconnectCallback.run(); } From ed6379820cdac63320165121b42d320cb00c8225 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 27 Aug 2025 13:52:44 +0200 Subject: [PATCH 07/23] Adding Javadoc --- .../olog/ui/LogEntryTableViewController.java | 2 - .../olog/ui/LogbookSearchController.java | 2 - .../WebSocketClientService.java | 58 +++++++++++++++---- 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java index 7fe7b1f3cc..50edb5cbd6 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java @@ -701,8 +701,6 @@ public void handleWebSocketMessage(String message){ WebSocketMessage webSocketMessage = objectMapper.readValue(message, WebSocketMessage.class); if(webSocketMessage.messageType().equals(MessageType.NEW_LOG_ENTRY)){ search(); - webSocketClientService.close(); - webSocketClientService.sendEcho("Hello World"); } } catch (JsonProcessingException e) { logger.log(Level.WARNING, "Unable to deserialize message \"" + message + "\""); diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java index bbdae65ed9..7cfdc87568 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java @@ -52,7 +52,6 @@ public LogClient getLogClient(){ * @param errorHandler Client side error handler that should notify user. */ public void search(Map searchParams, final Consumer resultHandler, final BiConsumer errorHandler) { - cancelPeriodSearch(); logbookSearchJob = LogbookSearchJob.submit(this.client, searchParams, resultHandler, @@ -96,7 +95,6 @@ private void cancelPeriodSearch() { * Utility method to cancel any ongoing periodic search jobs. */ public void shutdown() { - //cancelPeriodSearch(); if(stompSession != null && stompSession.isConnected()){ Logger.getLogger(LogbookSearchController.class.getName()).log(Level.INFO, "Disconnecting from web socket"); stompSession.disconnect(); diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java index d6684ed75d..096e2c8a24 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java @@ -23,7 +23,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,8 +65,11 @@ public class WebSocketClientService { private static final Logger logger = Logger.getLogger(WebSocketClientService.class.getName()); + /** + * Constructor if connect/disconnect callbacks are not needed. + */ + @SuppressWarnings("unused") public WebSocketClientService() { - } /** @@ -80,10 +82,12 @@ public WebSocketClientService(Runnable connectCallback, Runnable disconnectCallb this.disconnectCallback = disconnectCallback; } + @SuppressWarnings("unused") public void setConnectCallback(Runnable connectCallback) { this.connectCallback = connectCallback; } + @SuppressWarnings("unused") public void setDisconnectCallback(Runnable disconnectCallback) { this.disconnectCallback = disconnectCallback; } @@ -96,12 +100,16 @@ public void removeWebSocketMessageHandler(WebSocketMessageHandler webSocketMessa webSocketMessageHandlers.remove(webSocketMessageHandler); } - + /** + * For debugging purposes: peer should just echo back the message on the subscribed topic. + * @param message Message to echo + */ + @SuppressWarnings("unused") public void sendEcho(String message) { stompSession.send(contextPath + "/web-socket/echo", message); } - public void close(){ + public void close() { stompSession.disconnect(); } @@ -129,6 +137,9 @@ public void connect(String baseUrl) { doConnect(); } + /** + * + */ private void doConnect() { attemptReconnect.set(true); WebSocketClient webSocketClient = new StandardWebSocketClient(); @@ -173,13 +184,26 @@ public void handleFrame(StompHeaders headers, Object payload) { */ private class StompSessionHandler extends StompSessionHandlerAdapter { + /** + * Logs that web socket frame has been received. + * + * @param headers the headers of the frame + * @param payload the payload, or {@code null} if there was no payload + */ @Override public void handleFrame(StompHeaders headers, @Nullable Object payload) { if (payload != null) { - logger.log(Level.INFO, "WebSocket frame received: " + payload); + logger.log(Level.FINE, "WebSocket frame received: " + payload); } } + /** + * Handles connection success callback: thread to attempt connection is aborted, + * and connect callback is called, if set by API client. + * + * @param session the client STOMP session + * @param connectedHeaders the STOMP CONNECTED frame headers + */ @Override public void afterConnected(StompSession session, StompHeaders connectedHeaders) { attemptReconnect.set(false); @@ -189,6 +213,15 @@ public void afterConnected(StompSession session, StompHeaders connectedHeaders) } } + /** + * Hit for instance if an attempt is made to send a message to peer after {@link StompSession} has been closed. + * + * @param session the client STOMP session + * @param command the STOMP command of the frame + * @param headers the headers + * @param payload the raw payload + * @param exception the exception + */ @Override public void handleException(StompSession session, @Nullable StompCommand command, StompHeaders headers, byte[] payload, Throwable exception) { @@ -196,19 +229,22 @@ public void handleException(StompSession session, @Nullable StompCommand command } /** - * Note that this is called both on connection failure and if remote web socket peer - * goes away for whatever reason. - * @param session the client STOMP session + * If remote peer goes away because the service is shut down, or because + * of a network connection issue, we get a {@link ConnectionLostException}. In this case + * a reconnection thread is started. If on the other hand a connection attempt fails, we get + * a different type of exception (javax.websocket.DeploymentException), in which case a + * reconnection thread is not started. + * + * @param session the client STOMP session * @param exception the exception that occurred. This is evaluated to determine if a reconnection * thread should be launched. */ @Override public void handleTransportError(StompSession session, Throwable exception) { - if(exception instanceof ConnectionLostException){ + if (exception instanceof ConnectionLostException) { logger.log(Level.WARNING, "Connection lost, will attempt to reconnect", exception); doConnect(); - } - else{ + } else { logger.log(Level.WARNING, "Handling transport exception", exception); } if (disconnectCallback != null) { From c782641c2038bf7caaf049165b90661126a94b3f Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 27 Aug 2025 15:02:12 +0200 Subject: [PATCH 08/23] Added web socket to log calendar app, fixed shutdown of web sockets --- .../ui/LogEntryCalenderViewController.java | 101 +++++++++++++++--- .../olog/ui/LogEntryTableViewController.java | 23 ++-- .../olog/ui/LogbookSearchController.java | 39 +------ .../logbook/olog/ui/LogEntryCalenderView.fxml | 26 +++-- .../WebSocketClientService.java | 2 +- 5 files changed, 120 insertions(+), 71 deletions(-) diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java index 20530f9750..72a8af0080 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java @@ -1,9 +1,12 @@ package org.phoebus.logbook.olog.ui; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.application.Platform; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -12,6 +15,7 @@ import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.input.KeyCode; @@ -28,14 +32,20 @@ import jfxtras.scene.control.agenda.Agenda.Appointment; import jfxtras.scene.control.agenda.Agenda.AppointmentGroup; import jfxtras.scene.control.agenda.Agenda.AppointmentImplLocal; +import org.phoebus.core.websocket.WebSocketMessageHandler; +import org.phoebus.core.websocket.springframework.WebSocketClientService; import org.phoebus.framework.nls.NLS; import org.phoebus.logbook.LogClient; import org.phoebus.logbook.LogEntry; import org.phoebus.logbook.SearchResult; import org.phoebus.logbook.olog.ui.query.OlogQuery; import org.phoebus.logbook.olog.ui.query.OlogQueryManager; +import org.phoebus.logbook.olog.ui.websocket.MessageType; +import org.phoebus.logbook.olog.ui.websocket.WebSocketMessage; +import org.phoebus.olog.es.api.Preferences; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; +import java.net.URI; import java.net.URL; import java.time.LocalDateTime; import java.time.ZoneId; @@ -55,7 +65,7 @@ * * @author Kunal Shroff */ -public class LogEntryCalenderViewController extends LogbookSearchController { +public class LogEntryCalenderViewController extends LogbookSearchController implements WebSocketMessageHandler { private static final Logger logger = Logger.getLogger(LogEntryCalenderViewController.class.getName()); @@ -73,6 +83,7 @@ public class LogEntryCalenderViewController extends LogbookSearchController { // Model List logEntries; + @SuppressWarnings("unused") @FXML private AnchorPane agendaPane; @FXML @@ -80,14 +91,21 @@ public class LogEntryCalenderViewController extends LogbookSearchController { // Model private Map map; - private Map appointmentGroupMap = new TreeMap(); + private Map appointmentGroupMap = new TreeMap<>(); + @SuppressWarnings("unused") @FXML private AdvancedSearchViewController advancedSearchViewController; + @SuppressWarnings("unused") + @FXML + private Label autoUpdateStatusLabel; + private final OlogQueryManager ologQueryManager; private final ObservableList ologQueries = FXCollections.observableArrayList(); - private SearchParameters searchParameters; + private final SearchParameters searchParameters; + private final SimpleBooleanProperty webSocketConnected = new SimpleBooleanProperty(); + private final ObjectMapper objectMapper = new ObjectMapper(); public LogEntryCalenderViewController(LogClient logClient, OlogQueryManager ologQueryManager, SearchParameters searchParameters) { setClient(logClient); @@ -103,9 +121,7 @@ public void initialize() { // Set the search parameters in the advanced search controller so that it operates on the same object. ologQueries.setAll(ologQueryManager.getQueries()); - searchParameters.addListener((observable, oldValue, newValue) -> { - query.getEditor().setText(newValue); - }); + searchParameters.addListener((observable, oldValue, newValue) -> query.getEditor().setText(newValue)); agenda = new Agenda(); agenda.setEditAppointmentCallback(new Callback() { @@ -195,13 +211,29 @@ public Void call(Appointment appointment) { search.disableProperty().bind(searchInProgress); + webSocketConnected.addListener((obs, o, n) -> { + Platform.runLater(() -> { + if(n){ + autoUpdateStatusLabel.setStyle("-fx-text-fill: black;"); + autoUpdateStatusLabel.setText(Messages.AutoRefreshOn); + } + else{ + autoUpdateStatusLabel.setStyle("-fx-text-fill: red;"); + autoUpdateStatusLabel.setText(Messages.AutoRefreshOff); + } + }); + }); + + connectWebSocket(); + search(); } // Keeps track of when the animation is active. Multiple clicks will be ignored // until a give resize action is completed - private AtomicBoolean moving = new AtomicBoolean(false); + private final AtomicBoolean moving = new AtomicBoolean(false); + @SuppressWarnings("unused") @FXML public void resize() { if (!moving.compareAndExchangeAcquire(false, true)) { @@ -246,8 +278,6 @@ public void search() { searchResult1 -> { searchInProgress.set(false); setSearchResult(searchResult1); - logger.log(Level.INFO, "Starting periodic search: " + queryString); - periodicSearch(params, searchResult -> setSearchResult(searchResult)); List queries = ologQueryManager.getQueries(); Platform.runLater(() -> { ologQueries.setAll(queries); @@ -285,11 +315,15 @@ public Appointment apply(LogEntry logentry) { LocalDateTime.ofInstant(logentry.getCreatedDate().plusSeconds(2400), ZoneId.systemDefault())); List logbookNames = getLogbookNames(); if (logbookNames != null && !logbookNames.isEmpty()) { - int index = logbookNames.indexOf(logentry.getLogbooks().iterator().next().getName()); - if (index >= 0 && index <= 22) { - appointment.setAppointmentGroup(appointmentGroupMap.get(String.format("group%02d", (index + 1)))); - } else { - appointment.setAppointmentGroup(appointmentGroupMap.get(String.format("group%02d", 23))); + try { + int index = logbookNames.indexOf(logentry.getLogbooks().iterator().next().getName()); + if (index >= 0 && index <= 22) { + appointment.setAppointmentGroup(appointmentGroupMap.get(String.format("group%02d", (index + 1)))); + } else { + appointment.setAppointmentGroup(appointmentGroupMap.get(String.format("group%02d", 23))); + } + } catch (Exception e) { + throw new RuntimeException(e); } } return appointment; @@ -318,7 +352,8 @@ private List getLogbookNames() { } private void setSearchResult(SearchResult searchResult) { - setLogs(searchResult.getLogs());List queries = ologQueryManager.getQueries(); + setLogs(searchResult.getLogs()); + List queries = ologQueryManager.getQueries(); Platform.runLater(() -> { ologQueries.setAll(queries); // Top-most query is the one used in the search. @@ -376,4 +411,40 @@ public OlogQuery fromString(String s) { }); } + + private void connectWebSocket(){ + String baseUrl = Preferences.olog_url; + URI uri = URI.create(baseUrl); + String scheme = uri.getScheme(); + String host = uri.getHost(); + int port = uri.getPort(); + String path = uri.getPath(); + if(path.endsWith("/")){ + path = path.substring(0, path.length() - 1); + } + String webSocketScheme = scheme.toLowerCase().startsWith("https") ? "wss" : "ws"; + String webSocketUrl = webSocketScheme + "://" + host + (port > -1 ? (":" + port) : "") + path; + + webSocketClientService = new WebSocketClientService(() -> { + logger.log(Level.INFO, "Connected to web socket on " + webSocketUrl); + webSocketConnected.set(true); + }, () -> { + logger.log(Level.INFO, "Disconnected from web socket on " + webSocketUrl); + webSocketConnected.set(false); + }); + webSocketClientService.addWebSocketMessageHandler(this); + webSocketClientService.connect(webSocketUrl); + } + + @Override + public void handleWebSocketMessage(String message){ + try { + WebSocketMessage webSocketMessage = objectMapper.readValue(message, WebSocketMessage.class); + if(webSocketMessage.messageType().equals(MessageType.NEW_LOG_ENTRY)){ + search(); + } + } catch (JsonProcessingException e) { + logger.log(Level.WARNING, "Unable to deserialize message \"" + message + "\""); + } + } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java index 50edb5cbd6..5c72836525 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java @@ -142,7 +142,6 @@ public class LogEntryTableViewController extends LogbookSearchController impleme private final SimpleBooleanProperty advancedSearchVisible = new SimpleBooleanProperty(false); - private WebSocketClientService webSocketClientService; /** * Constructor. @@ -340,14 +339,16 @@ public void updateItem(TableViewListItem logEntry, boolean empty) { advancedSearchVisible)); webSocketConnected.addListener((obs, o, n) -> { - if(n){ - autoUpdateStatusLabel.setStyle("-fx-text-fill: black;"); - autoUpdateStatusLabel.setText(Messages.AutoRefreshOn); - } - else{ - autoUpdateStatusLabel.setStyle("-fx-text-fill: red;"); - autoUpdateStatusLabel.setText(Messages.AutoRefreshOff); - } + Platform.runLater(() -> { + if(n){ + autoUpdateStatusLabel.setStyle("-fx-text-fill: black;"); + autoUpdateStatusLabel.setText(Messages.AutoRefreshOn); + } + else{ + autoUpdateStatusLabel.setStyle("-fx-text-fill: red;"); + autoUpdateStatusLabel.setText(Messages.AutoRefreshOff); + } + }); }); connectWebSocket(); @@ -686,10 +687,10 @@ private void connectWebSocket(){ webSocketClientService = new WebSocketClientService(() -> { logger.log(Level.INFO, "Connected to web socket on " + webSocketUrl); - Platform.runLater(() -> webSocketConnected.set(true)); + webSocketConnected.set(true); }, () -> { logger.log(Level.INFO, "Disconnected from web socket on " + webSocketUrl); - Platform.runLater(() -> webSocketConnected.set(false)); + webSocketConnected.set(false); }); webSocketClientService.addWebSocketMessageHandler(this); webSocketClientService.connect(webSocketUrl); diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java index 7cfdc87568..7ebc94df2e 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java @@ -1,18 +1,17 @@ package org.phoebus.logbook.olog.ui; import javafx.beans.property.SimpleBooleanProperty; +import org.phoebus.core.websocket.springframework.WebSocketClientService; import org.phoebus.framework.jobs.Job; import org.phoebus.logbook.LogClient; import org.phoebus.logbook.LogEntry; import org.phoebus.logbook.SearchResult; -import org.springframework.messaging.simp.stomp.StompSession; import java.util.List; import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.logging.Level; @@ -35,7 +34,7 @@ public abstract class LogbookSearchController { protected final SimpleBooleanProperty searchInProgress = new SimpleBooleanProperty(false); private static final int SEARCH_JOB_INTERVAL = 30; // seconds - protected StompSession stompSession; + protected WebSocketClientService webSocketClientService; public void setClient(LogClient client) { this.client = client; @@ -58,46 +57,16 @@ public void search(Map searchParams, final Consumer searchParams, final Consumer resultHandler) { - cancelPeriodSearch(); - runningTask = executor.scheduleAtFixedRate(() -> logbookSearchJob = LogbookSearchJob.submit(this.client, - searchParams, - resultHandler, - (url, ex) -> { - searchInProgress.set(false); - cancelPeriodSearch(); - }), SEARCH_JOB_INTERVAL, SEARCH_JOB_INTERVAL, TimeUnit.SECONDS); - } - @Deprecated public abstract void setLogs(List logs); - /** - * Stops periodic search and ongoing search jobs, if any. - */ - private void cancelPeriodSearch() { - if (runningTask != null) { - runningTask.cancel(true); - } - - if (logbookSearchJob != null) { - logbookSearchJob.cancel(); - } - } - /** * Utility method to cancel any ongoing periodic search jobs. */ public void shutdown() { - if(stompSession != null && stompSession.isConnected()){ + if(webSocketClientService != null){ Logger.getLogger(LogbookSearchController.class.getName()).log(Level.INFO, "Disconnecting from web socket"); - stompSession.disconnect(); + webSocketClientService.disconnect(); } } } diff --git a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryCalenderView.fxml b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryCalenderView.fxml index 0c30107326..022563b632 100644 --- a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryCalenderView.fxml +++ b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryCalenderView.fxml @@ -4,29 +4,37 @@ - + - - - - + + + + + - - + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryTableView.fxml b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryTableView.fxml index 35c827d0d4..77cb41cf45 100644 --- a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryTableView.fxml +++ b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryTableView.fxml @@ -4,13 +4,14 @@ + - + @@ -39,21 +40,14 @@ - - + @@ -91,14 +85,22 @@ - + + + + + - + diff --git a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties index 7573638fcc..00cbf5904f 100644 --- a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties +++ b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties @@ -16,8 +16,6 @@ AttachmentsDirectoryFailedCreate=Failed to create directory {0} for attachments AttachmentsFileNotDirectory=File {0} exists but is not a directory AttachmentsSearchProperty=Attachments: Author=Author: -AutoRefreshOn=Auto Refresh: ON -AutoRefreshOff=Auto Refresh: OFF AvailableTemplates=Available Templates Back=Back BrowseButton=Browse @@ -63,6 +61,7 @@ Level=Level Logbook=Logbook Logbooks=Logbooks: LogbookNotSupported=No Logbook Support +LogbookServiceUnavailable=Logbook Service Unavailable LogbookServiceUnavailableTitle=Cannot create logbook entry LogbookServiceHasNoLogbooks=Logbook service "{0}" has no logbooks or is not available. LogbooksSearchFailTitle=Logbook Search Error diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java index aca659d8f8..6512c39a79 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java @@ -102,15 +102,23 @@ public void removeWebSocketMessageHandler(WebSocketMessageHandler webSocketMessa /** * For debugging purposes: peer should just echo back the message on the subscribed topic. - * @param message Message to echo + * @param message Message for the service to echo */ @SuppressWarnings("unused") public void sendEcho(String message) { - stompSession.send(contextPath + "/web-socket/echo", message); + if(stompSession != null && stompSession.isConnected()){ + stompSession.send(contextPath + "/web-socket/echo", message); + } + } + /** + * Disconnects the STOMP session if non-null and connected. + */ public void disconnect() { - stompSession.disconnect(); + if(stompSession != null && stompSession.isConnected()) { + stompSession.disconnect(); + } } /** From 181775155eb8f42e3b436fac8b0eeb445d9bde01 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 4 Sep 2025 10:34:40 +0200 Subject: [PATCH 13/23] Cleanup based on review feed-back --- .../olog/ui/LogEntryTableViewController.java | 1 - .../ui/write/LogEntryEditorController.java | 39 ------------------- .../WebSocketClientService.java | 3 +- 3 files changed, 2 insertions(+), 41 deletions(-) diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java index 10588f44a2..e97ee7c707 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java @@ -413,7 +413,6 @@ public void search() { }, (msg, ex) -> { searchInProgress.set(false); - //ExceptionDetailsErrorDialog.openError(Messages.LogbooksSearchFailTitle, ex.getMessage(), null); }); } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java index 9302de31b2..2a87b86cec 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java @@ -555,9 +555,6 @@ public LogTemplate fromString(String name) { getServerSideStaticData(); setupTextAreaContextMenu(); - - //doStompStuff(); - } /** @@ -1080,40 +1077,4 @@ private void loadTemplate(LogTemplate logTemplate) { selectedLogbooks.forEach(l -> updateDropDown(logbookDropDown, l, false)); } } - - private void doStompStuff(){ - WebSocketClient webSocketClient = new StandardWebSocketClient(); - WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient); - stompClient.setMessageConverter(new StringMessageConverter()); - String url = "ws://localhost:8080/web-socket"; - StompSessionHandler sessionHandler = new MyStompSessionHandler(); - try { - StompSession stompSession = stompClient.connect(url, sessionHandler).get(); - stompSession.subscribe("/messages", new StompFrameHandler() { - @Override - public Type getPayloadType(StompHeaders headers) { - return String.class; - } - - @Override - public void handleFrame(StompHeaders headers, Object payload) { - System.out.println(payload); - } - }); - StompSession.Receiptable receiptable = stompSession.send("/websocket/echo", "Hello World"); - receiptable.getReceiptId(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - throw new RuntimeException(e); - } - } - - private static class MyStompSessionHandler extends StompSessionHandlerAdapter { - - @Override - public void afterConnected(StompSession session, StompHeaders connectedHeaders) { - System.out.println(); - } - } } diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java index 6512c39a79..46d4a58678 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java @@ -146,7 +146,8 @@ public void connect(String baseUrl) { } /** - * + * Attempts to connect to the remote peer, both in initial connection and in a reconnection scenario. + * If connection fails, new attempts are made every 10s until successful. */ private void doConnect() { attemptReconnect.set(true); From 701d7e55c103a73cff7bc12481a776a7528efda1 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 8 Sep 2025 10:54:06 +0200 Subject: [PATCH 14/23] Add shutdown of Olog client to abort reconnect thread --- .../phoebus/logbook/olog/ui/LogbookSearchController.java | 4 ++-- .../websocket/springframework/WebSocketClientService.java | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java index 408d136f8f..eec88b8c84 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java @@ -87,8 +87,8 @@ public void search(Map searchParams, final Consumer Date: Tue, 23 Sep 2025 10:52:44 +0200 Subject: [PATCH 15/23] Add service API backwards compatibility for Olog client --- .../phoebus/olog/es/api/OlogHttpClient.java | 2 +- .../olog/ui/AdvancedSearchViewController.java | 26 ++-- .../logbook/olog/ui/LogEntryCalenderApp.java | 4 +- .../ui/LogEntryCalenderViewController.java | 11 +- .../olog/ui/LogEntryTableViewController.java | 19 ++- .../olog/ui/LogbookSearchController.java | 143 ++++++++++++++---- .../logbook/olog/ui/LogEntryCalenderView.fxml | 2 +- .../logbook/olog/ui/LogEntryTableView.fxml | 2 +- .../WebSocketClientService.java | 33 +++- 9 files changed, 184 insertions(+), 58 deletions(-) diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java index 5a82c242e0..5ecb15aeae 100644 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java @@ -568,7 +568,7 @@ public Collection listLevels(){ response.body(), new TypeReference>() { }); } catch (Exception e) { - LOGGER.log(Level.WARNING, "Unable to get templates from service", e); + LOGGER.log(Level.WARNING, "Unable to get levels from service", e); return Collections.emptySet(); } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AdvancedSearchViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AdvancedSearchViewController.java index 7c1eef8d56..b2b5d2f144 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AdvancedSearchViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AdvancedSearchViewController.java @@ -44,6 +44,7 @@ import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; +import org.phoebus.framework.jobs.JobManager; import org.phoebus.logbook.LogClient; import org.phoebus.logbook.LogEntryLevel; import org.phoebus.logbook.Logbook; @@ -317,18 +318,21 @@ public void initialize() { } }); - levelsList.addAll(logClient.listLevels().stream().map(LogEntryLevel::name).sorted().toList()); - levelsList.forEach(level -> { - LevelSelection levelSelection = new LevelSelection(level, false); - levelSelections.add(levelSelection); - CheckBox checkBox = new CheckBox(level); - LevelSelectionMenuItem levelSelectionMenuItem = new LevelSelectionMenuItem(checkBox); - levelSelectionMenuItem.setHideOnClick(false); - checkBox.selectedProperty().addListener((observable, oldValue, newValue) -> { - levelSelection.selected = newValue; - setSelectedLevelsString(); + // Fetch levels from service on separate thread + JobManager.schedule("Get logbook levels", monitor -> { + levelsList.addAll(logClient.listLevels().stream().map(LogEntryLevel::name).sorted().toList()); + levelsList.forEach(level -> { + LevelSelection levelSelection = new LevelSelection(level, false); + levelSelections.add(levelSelection); + CheckBox checkBox = new CheckBox(level); + LevelSelectionMenuItem levelSelectionMenuItem = new LevelSelectionMenuItem(checkBox); + levelSelectionMenuItem.setHideOnClick(false); + checkBox.selectedProperty().addListener((observable, oldValue, newValue) -> { + levelSelection.selected = newValue; + setSelectedLevelsString(); + }); + levelsContextMenu.getItems().add(levelSelectionMenuItem); }); - levelsContextMenu.getItems().add(levelSelectionMenuItem); }); sortOrderProperty.addListener(searchOnSortChange); diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderApp.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderApp.java index 3eb48128d3..a5a7e651c2 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderApp.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderApp.java @@ -17,8 +17,8 @@ public class LogEntryCalenderApp implements AppResourceDescriptor { public static final Logger logger = Logger.getLogger(LogEntryCalenderApp.class.getName()); static final Image icon = ImageCache.getImage(LogEntryCalenderApp.class, "/icons/logbook-16.png"); - public static final String NAME = "logEntryCalender"; - public static String DISPLAYNAME = "Log Entry Calender"; + public static final String NAME = "logEntryCalendar"; + public static String DISPLAYNAME = "Log Entry Calendar"; private static final String SUPPORTED_SCHEMA = "logCalender"; private LogFactory logFactory; diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java index 7d3e96b697..665b7a1cf0 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java @@ -97,9 +97,7 @@ public LogEntryCalenderViewController(LogClient logClient, OlogQueryManager olog } @FXML - @Override public void initialize() { - super.initialize(); advancedSearchViewController.setSearchCallback(this::search); configureComboBox(); @@ -196,9 +194,12 @@ public Void call(Appointment appointment) { search.disableProperty().bind(searchInProgress); - connectWebSocket(); - - search(); + determineConnectivity(() -> { + switch (connectivityModeObjectProperty.get()){ + case HTTP_ONLY -> search(); + case WEB_SOCKETS_SUPPORTED -> connectWebSocket(); + } + }); } // Keeps track of when the animation is active. Multiple clicks will be ignored diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java index 67c8b7d1bb..25f0e531de 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java @@ -86,7 +86,7 @@ public class LogEntryTableViewController extends LogbookSearchController impleme // elements associated with the various search @FXML @SuppressWarnings("unused") - private GridPane ViewSearchPane; + private GridPane viewSearchPane; // elements related to the table view of the log entries @FXML @@ -167,9 +167,7 @@ protected void setGoBackAndGoForwardActions(LogEntryTable.GoBackAndGoForwardActi protected Optional goBackAndGoForwardActions = Optional.empty(); @FXML - @Override public void initialize() { - super.initialize(); logEntryDisplayController.setLogEntryTableViewController(this); @@ -183,7 +181,6 @@ public void initialize() { }); tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - tableView.visibleProperty().bind(serviceConnected); MenuItem groupSelectedEntries = new MenuItem(Messages.GroupSelectedEntries); groupSelectedEntries.setOnAction(e -> createLogEntryGroup()); @@ -335,9 +332,13 @@ public void updateItem(TableViewListItem logEntry, boolean empty) { Messages.AdvancedSearchHide : Messages.AdvancedSearchOpen, advancedSearchVisible)); - logDetailView.disableProperty().bind(serviceConnected.not()); + determineConnectivity(() -> { + switch (connectivityModeObjectProperty.get()){ + case HTTP_ONLY -> search(); + case WEB_SOCKETS_SUPPORTED -> connectWebSocket(); + } + }); - connectWebSocket(); } // Keeps track of when the animation is active. Multiple clicks will be ignored @@ -361,7 +362,7 @@ public void resize() { }); } else { searchParameters.setQuery(query.getEditor().getText()); - double width = ViewSearchPane.getWidth() / 2.5; + double width = viewSearchPane.getWidth() / 2.5; KeyValue kv = new KeyValue(advancedSearchViewController.getPane().minWidthProperty(), width); KeyValue kv2 = new KeyValue(advancedSearchViewController.getPane().prefWidthProperty(), width); timeline = new Timeline(new KeyFrame(cycleDuration, kv, kv2)); @@ -406,6 +407,10 @@ public void search() { searchInProgress.set(false); setSearchResult(searchResult1); List queries = ologQueryManager.getQueries(); + if(connectivityModeObjectProperty.get().equals(ConnectivityMode.HTTP_ONLY)){ + logger.log(Level.INFO, "Starting periodic search: " + queryString); + periodicSearch(params, this::setSearchResult); + } Platform.runLater(() -> { ologQueries.setAll(queries); query.getSelectionModel().select(ologQueries.get(0)); diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java index eec88b8c84..78cf52bf42 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java @@ -2,13 +2,17 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import javafx.application.Platform; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.fxml.FXML; import javafx.scene.layout.GridPane; import javafx.scene.layout.VBox; import org.phoebus.core.websocket.WebSocketMessageHandler; import org.phoebus.core.websocket.springframework.WebSocketClientService; import org.phoebus.framework.jobs.Job; +import org.phoebus.framework.jobs.JobManager; import org.phoebus.logbook.LogClient; import org.phoebus.logbook.LogEntry; import org.phoebus.logbook.SearchResult; @@ -19,6 +23,11 @@ import java.net.URI; import java.util.List; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.logging.Level; @@ -39,9 +48,17 @@ public abstract class LogbookSearchController implements WebSocketMessageHandler private final ObjectMapper objectMapper = new ObjectMapper(); private final Logger logger = Logger.getLogger(LogbookSearchController.class.getName()); - protected final SimpleBooleanProperty serviceConnected = new SimpleBooleanProperty(); + protected final SimpleBooleanProperty webSocketConnected = new SimpleBooleanProperty(); + private static final int SEARCH_JOB_INTERVAL = 30; // 30 seconds + private ScheduledFuture runningTask; + private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + private Job logbookSearchJob; + protected final ObjectProperty connectivityModeObjectProperty = + new SimpleObjectProperty<>(ConnectivityMode.NOT_CONNECTED); protected WebSocketClientService webSocketClientService; + private final String webSocketUrl; + private final CountDownLatch connectivityCheckerCountDownLatch = new CountDownLatch(1); @SuppressWarnings("unused") @FXML @@ -49,12 +66,44 @@ public abstract class LogbookSearchController implements WebSocketMessageHandler @SuppressWarnings("unused") @FXML - private GridPane ViewSearchPane; + private GridPane viewSearchPane; - @FXML - public void initialize() { - errorPane.visibleProperty().bind(serviceConnected.not()); - ViewSearchPane.visibleProperty().bind(serviceConnected); + public LogbookSearchController() { + String baseUrl = Preferences.olog_url; + URI uri = URI.create(baseUrl); + String scheme = uri.getScheme(); + String host = uri.getHost(); + int port = uri.getPort(); + String path = uri.getPath(); + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + String webSocketScheme = scheme.toLowerCase().startsWith("https") ? "wss" : "ws"; + this.webSocketUrl = webSocketScheme + "://" + host + (port > -1 ? (":" + port) : "") + path; + } + + protected void determineConnectivity(Runnable completionHandler){ + + // Try to determine the connection mode: is the remote service available at all? + // If so, does it accept web socket connections? + JobManager.schedule("Connection mode probe", monitor -> { + String serviceInfo = client.serviceInfo(); + if (serviceInfo != null && !serviceInfo.isEmpty()) { // service online, check web socket availability + if (WebSocketClientService.checkAvailability(this.webSocketUrl)) { + connectivityModeObjectProperty.set(ConnectivityMode.WEB_SOCKETS_SUPPORTED); + } else { + connectivityModeObjectProperty.set(ConnectivityMode.HTTP_ONLY); + } + } + connectivityCheckerCountDownLatch.countDown(); + if (connectivityModeObjectProperty.get().equals(ConnectivityMode.NOT_CONNECTED)) { + Platform.runLater(() -> { + errorPane.visibleProperty().set(true); + viewSearchPane.visibleProperty().set(false); + }); + } + completionHandler.run(); + }); } public void setClient(LogClient client) { @@ -73,12 +122,44 @@ public LogClient getLogClient() { * @param errorHandler Client side error handler that should notify user. */ public void search(Map searchParams, final Consumer resultHandler, final BiConsumer errorHandler) { + cancelPeriodSearch(); LogbookSearchJob.submit(this.client, searchParams, resultHandler, errorHandler); } + /** + * Starts a search job every {@link #SEARCH_JOB_INTERVAL} seconds. If a search fails (e.g. service off-line or invalid search parameters), + * the period search is cancelled. User will need to implicitly start it again through a "manual" search in the UI. + * + * @param searchParams The search parameters + * @param resultHandler Handler taking care of the search result. + */ + public void periodicSearch(Map searchParams, final Consumer resultHandler) { + cancelPeriodSearch(); + runningTask = executor.scheduleAtFixedRate(() -> logbookSearchJob = LogbookSearchJob.submit(this.client, + searchParams, + resultHandler, + (url, ex) -> { + searchInProgress.set(false); + cancelPeriodSearch(); + }), SEARCH_JOB_INTERVAL, SEARCH_JOB_INTERVAL, TimeUnit.SECONDS); + } + + /** + * Stops periodic search and ongoing search jobs, if any. + */ + private void cancelPeriodSearch() { + if (runningTask != null) { + runningTask.cancel(true); + } + + if (logbookSearchJob != null) { + logbookSearchJob.cancel(); + } + } + @Deprecated public abstract void setLogs(List logs); @@ -90,33 +171,35 @@ public void shutdown() { Logger.getLogger(LogbookSearchController.class.getName()).log(Level.INFO, "Shutting down web socket"); webSocketClientService.shutdown(); } + if(connectivityModeObjectProperty.get().equals(ConnectivityMode.HTTP_ONLY)){ + cancelPeriodSearch(); + } } protected abstract void search(); protected void connectWebSocket() { - String baseUrl = Preferences.olog_url; - URI uri = URI.create(baseUrl); - String scheme = uri.getScheme(); - String host = uri.getHost(); - int port = uri.getPort(); - String path = uri.getPath(); - if (path.endsWith("/")) { - path = path.substring(0, path.length() - 1); + try { + if(connectivityCheckerCountDownLatch.await(3000, TimeUnit.MILLISECONDS)){ + if (connectivityModeObjectProperty.get().equals(ConnectivityMode.WEB_SOCKETS_SUPPORTED)) { + webSocketClientService = new WebSocketClientService(() -> { + logger.log(Level.INFO, "Connected to web socket on " + webSocketUrl); + webSocketConnected.setValue(true); + errorPane.visibleProperty().set(false); + search(); + }, () -> { + logger.log(Level.INFO, "Disconnected from web socket on " + webSocketUrl); + webSocketConnected.set(false); + errorPane.visibleProperty().set(true); + }); + webSocketClientService.addWebSocketMessageHandler(this); + webSocketClientService.connect(webSocketUrl); + } + } + } catch (InterruptedException e) { + Logger.getLogger(LogbookSearchController.class.getName()).log(Level.WARNING, "Timed out waiting for connectivity check"); + connectivityModeObjectProperty.set(ConnectivityMode.NOT_CONNECTED); } - String webSocketScheme = scheme.toLowerCase().startsWith("https") ? "wss" : "ws"; - String webSocketUrl = webSocketScheme + "://" + host + (port > -1 ? (":" + port) : "") + path; - - webSocketClientService = new WebSocketClientService(() -> { - logger.log(Level.INFO, "Connected to web socket on " + webSocketUrl); - serviceConnected.setValue(true); - search(); - }, () -> { - logger.log(Level.INFO, "Disconnected from web socket on " + webSocketUrl); - serviceConnected.set(false); - }); - webSocketClientService.addWebSocketMessageHandler(this); - webSocketClientService.connect(webSocketUrl); } @Override @@ -137,4 +220,10 @@ public void handleWebSocketMessage(String message) { logger.log(Level.WARNING, "Unable to deserialize message \"" + message + "\""); } } + + protected enum ConnectivityMode { + NOT_CONNECTED, + HTTP_ONLY, + WEB_SOCKETS_SUPPORTED + } } diff --git a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryCalenderView.fxml b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryCalenderView.fxml index 5ebef9e29d..82209c507c 100644 --- a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryCalenderView.fxml +++ b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryCalenderView.fxml @@ -10,7 +10,7 @@ - + diff --git a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryTableView.fxml b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryTableView.fxml index 77cb41cf45..bbc06dac4e 100644 --- a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryTableView.fxml +++ b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/LogEntryTableView.fxml @@ -13,7 +13,7 @@ - + diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java index 8230c6fc46..4cee6aceb6 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -102,11 +103,12 @@ public void removeWebSocketMessageHandler(WebSocketMessageHandler webSocketMessa /** * For debugging purposes: peer should just echo back the message on the subscribed topic. + * * @param message Message for the service to echo */ @SuppressWarnings("unused") public void sendEcho(String message) { - if(stompSession != null && stompSession.isConnected()){ + if (stompSession != null && stompSession.isConnected()) { stompSession.send(contextPath + "/web-socket/echo", message); } @@ -116,7 +118,7 @@ public void sendEcho(String message) { * Disconnects the STOMP session if non-null and connected. */ public void disconnect() { - if(stompSession != null && stompSession.isConnected()) { + if (stompSession != null && stompSession.isConnected()) { stompSession.disconnect(); } } @@ -124,7 +126,7 @@ public void disconnect() { /** * Disconnects the socket if connected and terminates connection thread. */ - public void shutdown(){ + public void shutdown() { disconnect(); attemptReconnect.set(false); } @@ -269,4 +271,29 @@ public void handleTransportError(StompSession session, Throwable exception) { } } } + + /** + * Utility method to check availability of a web socket connection. Tries to connect once, + * and - if successful - subsequently closes the web socket connection. + * + * @param webSocketConnectUrl The web socket URL + * @return true if connection to web socket succeeds within 3000 ms. + */ + public static boolean checkAvailability(String webSocketConnectUrl) { + WebSocketClient webSocketClient = new StandardWebSocketClient(); + WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient); + try { + StompSession stompSession = stompClient.connect(webSocketConnectUrl + "/web-socket", new StompSessionHandlerAdapter() { + @Override + public Type getPayloadType(StompHeaders headers) { + return super.getPayloadType(headers); + } + }).get(3000, TimeUnit.MILLISECONDS); + stompSession.disconnect(); + return true; + } catch (Exception e) { + logger.log(Level.WARNING, "Remote service on " + webSocketConnectUrl + " does not support web socket connection", e); + } + return false; + } } From 565860fb0d4d13fd43d477bc4648d368ac0dbcbc Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 23 Sep 2025 11:31:52 +0200 Subject: [PATCH 16/23] Moved web socket URLs and endpoints to API client --- .../olog/ui/LogbookSearchController.java | 25 +++++-- .../WebSocketClientService.java | 72 ++++++++----------- 2 files changed, 48 insertions(+), 49 deletions(-) diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java index 78cf52bf42..22a34779fa 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java @@ -57,7 +57,8 @@ public abstract class LogbookSearchController implements WebSocketMessageHandler new SimpleObjectProperty<>(ConnectivityMode.NOT_CONNECTED); protected WebSocketClientService webSocketClientService; - private final String webSocketUrl; + private final String webSocketConnectUrl; + private final String subscriptionEndpoint; private final CountDownLatch connectivityCheckerCountDownLatch = new CountDownLatch(1); @SuppressWarnings("unused") @@ -79,9 +80,16 @@ public LogbookSearchController() { path = path.substring(0, path.length() - 1); } String webSocketScheme = scheme.toLowerCase().startsWith("https") ? "wss" : "ws"; - this.webSocketUrl = webSocketScheme + "://" + host + (port > -1 ? (":" + port) : "") + path; + this.webSocketConnectUrl = webSocketScheme + "://" + host + (port > -1 ? (":" + port) : "") + path + "/web-socket"; + this.subscriptionEndpoint = path + "/web-socket/messages"; } + /** + * Determines how the client may connect to the remote service. The service info endpoint is called to establish + * availability of the service. If available, then a single web socket connection is attempted to determine + * if the service supports web sockets. + * @param completionHandler {@link Runnable} called when connection mode has been determined. + */ protected void determineConnectivity(Runnable completionHandler){ // Try to determine the connection mode: is the remote service available at all? @@ -89,7 +97,7 @@ protected void determineConnectivity(Runnable completionHandler){ JobManager.schedule("Connection mode probe", monitor -> { String serviceInfo = client.serviceInfo(); if (serviceInfo != null && !serviceInfo.isEmpty()) { // service online, check web socket availability - if (WebSocketClientService.checkAvailability(this.webSocketUrl)) { + if (WebSocketClientService.checkAvailability(this.webSocketConnectUrl)) { connectivityModeObjectProperty.set(ConnectivityMode.WEB_SOCKETS_SUPPORTED); } else { connectivityModeObjectProperty.set(ConnectivityMode.HTTP_ONLY); @@ -183,17 +191,17 @@ protected void connectWebSocket() { if(connectivityCheckerCountDownLatch.await(3000, TimeUnit.MILLISECONDS)){ if (connectivityModeObjectProperty.get().equals(ConnectivityMode.WEB_SOCKETS_SUPPORTED)) { webSocketClientService = new WebSocketClientService(() -> { - logger.log(Level.INFO, "Connected to web socket on " + webSocketUrl); + logger.log(Level.INFO, "Connected to web socket on " + webSocketConnectUrl); webSocketConnected.setValue(true); errorPane.visibleProperty().set(false); search(); }, () -> { - logger.log(Level.INFO, "Disconnected from web socket on " + webSocketUrl); + logger.log(Level.INFO, "Disconnected from web socket on " + webSocketConnectUrl); webSocketConnected.set(false); errorPane.visibleProperty().set(true); - }); + }, webSocketConnectUrl, subscriptionEndpoint, null); webSocketClientService.addWebSocketMessageHandler(this); - webSocketClientService.connect(webSocketUrl); + webSocketClientService.connect(); } } } catch (InterruptedException e) { @@ -221,6 +229,9 @@ public void handleWebSocketMessage(String message) { } } + /** + * Enum to indicate if and how the client may connect to remote Olog service. + */ protected enum ConnectivityMode { NOT_CONNECTED, HTTP_ONLY, diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java index 4cee6aceb6..1d21ee7c93 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java @@ -50,19 +50,24 @@ */ public class WebSocketClientService { - /** - * URL to which the client connects - */ - private String webSocketConnectUrl; - /** - * Path string, depends on service deployment context, e.g. Olog - */ - private String contextPath; + private StompSession stompSession; private Runnable connectCallback; private Runnable disconnectCallback; private final List webSocketMessageHandlers = Collections.synchronizedList(new ArrayList<>()); private final AtomicBoolean attemptReconnect = new AtomicBoolean(); + /** + * Full path to the web socket connection URL, e.g. ws://localhost:8080/Olog/web-socket + */ + private String connectUrl; + /** + * Subscription endpoint, e.g. /Olog/web-socket/messages + */ + private String subscriptionEndpoint; + /** + * Echo endpoint /Olog/web-socket/echo + */ + private String echoEndpoint; private static final Logger logger = Logger.getLogger(WebSocketClientService.class.getName()); @@ -70,15 +75,22 @@ public class WebSocketClientService { * Constructor if connect/disconnect callbacks are not needed. */ @SuppressWarnings("unused") - public WebSocketClientService() { + public WebSocketClientService(String connectUrl, String subscriptionEndpoint, String echoEndpoint) { + this.connectUrl = connectUrl; + this.subscriptionEndpoint = subscriptionEndpoint; + this.echoEndpoint = echoEndpoint; } /** * @param connectCallback The non-null method called when connection to the remote web socket has been successfully established. * @param disconnectCallback The non-null method called when connection to the remote web socket has been lost, e.g. * remote peer has been shut down. + * @param connectUrl URL to the service web socket, e.g. ws://localhost:8080/Olog/web.socket + * @param subscriptionEndpoint E.g. /Olog/web-socket/messages + * @param echoEndpoint E.g. /Olog/web-socket/echo. May be null if client has no need for echo messages. */ - public WebSocketClientService(Runnable connectCallback, Runnable disconnectCallback) { + public WebSocketClientService(Runnable connectCallback, Runnable disconnectCallback, String connectUrl, String subscriptionEndpoint, String echoEndpoint) { + this(connectUrl, subscriptionEndpoint, echoEndpoint); this.connectCallback = connectCallback; this.disconnectCallback = disconnectCallback; } @@ -108,8 +120,8 @@ public void removeWebSocketMessageHandler(WebSocketMessageHandler webSocketMessa */ @SuppressWarnings("unused") public void sendEcho(String message) { - if (stompSession != null && stompSession.isConnected()) { - stompSession.send(contextPath + "/web-socket/echo", message); + if (stompSession != null && stompSession.isConnected() && echoEndpoint != null) { + stompSession.send(echoEndpoint, message); } } @@ -131,35 +143,11 @@ public void shutdown() { attemptReconnect.set(false); } - /** - * Attempts to connect to remote web socket. - * - * @param baseUrl The "base" URL of the web socket peer, must start with ws:// or wss://. Note that "web-socket" will be - * appended to this URL. Further, the URL may contain a path, e.g. ws://host:port/path. - * @throws IllegalArgumentException if baseUrl is null, empty or does not start with ws:// or wss://. - */ - public void connect(String baseUrl) { - if (baseUrl == null || baseUrl.isEmpty() || (!baseUrl.toLowerCase().startsWith("ws://") && !baseUrl.toLowerCase().startsWith("wss://"))) { - throw new IllegalArgumentException("URL \"" + baseUrl + "\" is not valid"); - } - URI uri = URI.create(baseUrl); - String scheme = uri.getScheme(); - String host = uri.getHost(); - int port = uri.getPort(); - String path = uri.getPath(); - if (path.endsWith("/")) { - path = path.substring(0, path.length() - 1); - } - this.contextPath = path; - this.webSocketConnectUrl = scheme + "://" + host + (port > -1 ? (":" + port) : "") + this.contextPath + "/web-socket"; - doConnect(); - } - /** * Attempts to connect to the remote peer, both in initial connection and in a reconnection scenario. * If connection fails, new attempts are made every 10s until successful. */ - private void doConnect() { + public void connect() { attemptReconnect.set(true); WebSocketClient webSocketClient = new StandardWebSocketClient(); WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient); @@ -171,10 +159,10 @@ private void doConnect() { StompSessionHandler sessionHandler = new StompSessionHandler(); new Thread(() -> { while (attemptReconnect.get()) { - logger.log(Level.INFO, "Attempting web socket connection to " + webSocketConnectUrl); + logger.log(Level.INFO, "Attempting web socket connection to " + connectUrl); try { - stompSession = stompClient.connect(this.webSocketConnectUrl, sessionHandler).get(); - stompSession.subscribe(contextPath + "/web-socket/messages", new StompFrameHandler() { + stompSession = stompClient.connect(connectUrl, sessionHandler).get(); + stompSession.subscribe(this.subscriptionEndpoint, new StompFrameHandler() { @Override public Type getPayloadType(StompHeaders headers) { return String.class; @@ -262,7 +250,7 @@ public void handleException(StompSession session, @Nullable StompCommand command public void handleTransportError(StompSession session, Throwable exception) { if (exception instanceof ConnectionLostException) { logger.log(Level.WARNING, "Connection lost, will attempt to reconnect", exception); - doConnect(); + connect(); } else { logger.log(Level.WARNING, "Handling transport exception", exception); } @@ -283,7 +271,7 @@ public static boolean checkAvailability(String webSocketConnectUrl) { WebSocketClient webSocketClient = new StandardWebSocketClient(); WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient); try { - StompSession stompSession = stompClient.connect(webSocketConnectUrl + "/web-socket", new StompSessionHandlerAdapter() { + StompSession stompSession = stompClient.connect(webSocketConnectUrl, new StompSessionHandlerAdapter() { @Override public Type getPayloadType(StompHeaders headers) { return super.getPayloadType(headers); From da4ac48971421aeab3a2aea59262b184ba45f25c Mon Sep 17 00:00:00 2001 From: georgweiss Date: Tue, 23 Sep 2025 12:25:07 +0200 Subject: [PATCH 17/23] Update UI if web socket is closed when service is not reachable --- .../org/phoebus/logbook/olog/ui/LogbookSearchController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java index 22a34779fa..8bf2999a4e 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java @@ -172,7 +172,7 @@ private void cancelPeriodSearch() { public abstract void setLogs(List logs); /** - * Utility method to cancel any ongoing periodic search jobs. + * Utility method to cancel any ongoing periodic search jobs or close web socket. */ public void shutdown() { if (webSocketClientService != null) { @@ -193,11 +193,13 @@ protected void connectWebSocket() { webSocketClientService = new WebSocketClientService(() -> { logger.log(Level.INFO, "Connected to web socket on " + webSocketConnectUrl); webSocketConnected.setValue(true); + viewSearchPane.visibleProperty().set(true); errorPane.visibleProperty().set(false); search(); }, () -> { logger.log(Level.INFO, "Disconnected from web socket on " + webSocketConnectUrl); webSocketConnected.set(false); + viewSearchPane.visibleProperty().set(false); errorPane.visibleProperty().set(true); }, webSocketConnectUrl, subscriptionEndpoint, null); webSocketClientService.addWebSocketMessageHandler(this); From 338d8630763bf9a0da343cd282afba91d22cc6b7 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 1 Oct 2025 09:30:55 +0200 Subject: [PATCH 18/23] Remove web socket handler when shutting down Olog ui --- .../olog/ui/LogbookSearchController.java | 3 ++- .../springframework/WebSocketClientService.java | 17 ++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java index 8bf2999a4e..bac9f9d208 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java @@ -175,8 +175,9 @@ private void cancelPeriodSearch() { * Utility method to cancel any ongoing periodic search jobs or close web socket. */ public void shutdown() { - if (webSocketClientService != null) { + if (connectivityModeObjectProperty.get().equals(ConnectivityMode.WEB_SOCKETS_SUPPORTED) && webSocketClientService != null) { Logger.getLogger(LogbookSearchController.class.getName()).log(Level.INFO, "Shutting down web socket"); + webSocketClientService.removeWebSocketMessageHandler(this); webSocketClientService.shutdown(); } if(connectivityModeObjectProperty.get().equals(ConnectivityMode.HTTP_ONLY)){ diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java index 1d21ee7c93..1ce591e81c 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java @@ -19,7 +19,6 @@ import org.springframework.web.socket.messaging.WebSocketStompClient; import java.lang.reflect.Type; -import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -59,15 +58,15 @@ public class WebSocketClientService { /** * Full path to the web socket connection URL, e.g. ws://localhost:8080/Olog/web-socket */ - private String connectUrl; + private final String connectUrl; /** * Subscription endpoint, e.g. /Olog/web-socket/messages */ - private String subscriptionEndpoint; + private final String subscriptionEndpoint; /** * Echo endpoint /Olog/web-socket/echo */ - private String echoEndpoint; + private final String echoEndpoint; private static final Logger logger = Logger.getLogger(WebSocketClientService.class.getName()); @@ -82,12 +81,12 @@ public WebSocketClientService(String connectUrl, String subscriptionEndpoint, St } /** - * @param connectCallback The non-null method called when connection to the remote web socket has been successfully established. - * @param disconnectCallback The non-null method called when connection to the remote web socket has been lost, e.g. - * remote peer has been shut down. - * @param connectUrl URL to the service web socket, e.g. ws://localhost:8080/Olog/web.socket + * @param connectCallback The non-null method called when connection to the remote web socket has been successfully established. + * @param disconnectCallback The non-null method called when connection to the remote web socket has been lost, e.g. + * remote peer has been shut down. + * @param connectUrl URL to the service web socket, e.g. ws://localhost:8080/Olog/web.socket * @param subscriptionEndpoint E.g. /Olog/web-socket/messages - * @param echoEndpoint E.g. /Olog/web-socket/echo. May be null if client has no need for echo messages. + * @param echoEndpoint E.g. /Olog/web-socket/echo. May be null if client has no need for echo messages. */ public WebSocketClientService(Runnable connectCallback, Runnable disconnectCallback, String connectUrl, String subscriptionEndpoint, String echoEndpoint) { this(connectUrl, subscriptionEndpoint, echoEndpoint); From 67eff8c49eb67af3b3f89051e55088d310778f45 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 1 Oct 2025 12:42:03 +0200 Subject: [PATCH 19/23] Updates based on peer feedback --- .../olog/ui/LogEntryCalenderViewController.java | 3 ++- .../olog/ui/LogEntryTableViewController.java | 5 +++-- .../logbook/olog/ui/LogbookSearchController.java | 15 ++++++++------- .../springframework/WebSocketClientService.java | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java index 665b7a1cf0..0758e7fbcd 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java @@ -194,7 +194,8 @@ public Void call(Appointment appointment) { search.disableProperty().bind(searchInProgress); - determineConnectivity(() -> { + determineConnectivity(connectivityMode -> { + connectivityModeObjectProperty.set(connectivityMode); switch (connectivityModeObjectProperty.get()){ case HTTP_ONLY -> search(); case WEB_SOCKETS_SUPPORTED -> connectWebSocket(); diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java index 25f0e531de..d75bdb876a 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java @@ -332,8 +332,9 @@ public void updateItem(TableViewListItem logEntry, boolean empty) { Messages.AdvancedSearchHide : Messages.AdvancedSearchOpen, advancedSearchVisible)); - determineConnectivity(() -> { - switch (connectivityModeObjectProperty.get()){ + determineConnectivity(connectivityMode -> { + connectivityModeObjectProperty.set(connectivityMode); + switch (connectivityMode){ case HTTP_ONLY -> search(); case WEB_SOCKETS_SUPPORTED -> connectWebSocket(); } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java index bac9f9d208..8ba73c5d47 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java @@ -88,29 +88,30 @@ public LogbookSearchController() { * Determines how the client may connect to the remote service. The service info endpoint is called to establish * availability of the service. If available, then a single web socket connection is attempted to determine * if the service supports web sockets. - * @param completionHandler {@link Runnable} called when connection mode has been determined. + * @param consumer {@link Consumer} called when the connectivity mode has been determined. */ - protected void determineConnectivity(Runnable completionHandler){ + protected void determineConnectivity(Consumer consumer){ // Try to determine the connection mode: is the remote service available at all? // If so, does it accept web socket connections? JobManager.schedule("Connection mode probe", monitor -> { + ConnectivityMode connectivityMode = ConnectivityMode.NOT_CONNECTED; String serviceInfo = client.serviceInfo(); if (serviceInfo != null && !serviceInfo.isEmpty()) { // service online, check web socket availability if (WebSocketClientService.checkAvailability(this.webSocketConnectUrl)) { - connectivityModeObjectProperty.set(ConnectivityMode.WEB_SOCKETS_SUPPORTED); + connectivityMode = ConnectivityMode.WEB_SOCKETS_SUPPORTED; } else { - connectivityModeObjectProperty.set(ConnectivityMode.HTTP_ONLY); + connectivityMode = ConnectivityMode.HTTP_ONLY; } } connectivityCheckerCountDownLatch.countDown(); - if (connectivityModeObjectProperty.get().equals(ConnectivityMode.NOT_CONNECTED)) { + consumer.accept(connectivityMode); + if (connectivityMode.equals(ConnectivityMode.NOT_CONNECTED)) { Platform.runLater(() -> { errorPane.visibleProperty().set(true); viewSearchPane.visibleProperty().set(false); }); } - completionHandler.run(); }); } @@ -180,7 +181,7 @@ public void shutdown() { webSocketClientService.removeWebSocketMessageHandler(this); webSocketClientService.shutdown(); } - if(connectivityModeObjectProperty.get().equals(ConnectivityMode.HTTP_ONLY)){ + else if(connectivityModeObjectProperty.get().equals(ConnectivityMode.HTTP_ONLY)){ cancelPeriodSearch(); } } diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java index 1ce591e81c..453ac5ae3f 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java @@ -138,8 +138,8 @@ public void disconnect() { * Disconnects the socket if connected and terminates connection thread. */ public void shutdown() { - disconnect(); attemptReconnect.set(false); + disconnect(); } /** From 20ebf73afd052ece62ad90662a7db25585fafbc9 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 2 Oct 2025 15:04:34 +0200 Subject: [PATCH 20/23] Shutdown Olog UI checks connectivity in synchronized manner --- .../ui/LogEntryCalenderViewController.java | 1 + .../olog/ui/LogEntryTableViewController.java | 1 + .../olog/ui/LogbookSearchController.java | 45 +++++++++---------- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java index 0758e7fbcd..c6fe61f308 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryCalenderViewController.java @@ -196,6 +196,7 @@ public Void call(Appointment appointment) { determineConnectivity(connectivityMode -> { connectivityModeObjectProperty.set(connectivityMode); + connectivityCheckerCountDownLatch.countDown(); switch (connectivityModeObjectProperty.get()){ case HTTP_ONLY -> search(); case WEB_SOCKETS_SUPPORTED -> connectWebSocket(); diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java index d75bdb876a..facf69a4cf 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java @@ -334,6 +334,7 @@ public void updateItem(TableViewListItem logEntry, boolean empty) { determineConnectivity(connectivityMode -> { connectivityModeObjectProperty.set(connectivityMode); + connectivityCheckerCountDownLatch.countDown(); switch (connectivityMode){ case HTTP_ONLY -> search(); case WEB_SOCKETS_SUPPORTED -> connectWebSocket(); diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java index 8ba73c5d47..1f5a4ac0ee 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogbookSearchController.java @@ -59,7 +59,7 @@ public abstract class LogbookSearchController implements WebSocketMessageHandler protected WebSocketClientService webSocketClientService; private final String webSocketConnectUrl; private final String subscriptionEndpoint; - private final CountDownLatch connectivityCheckerCountDownLatch = new CountDownLatch(1); + protected final CountDownLatch connectivityCheckerCountDownLatch = new CountDownLatch(1); @SuppressWarnings("unused") @FXML @@ -104,7 +104,6 @@ protected void determineConnectivity(Consumer consumer){ connectivityMode = ConnectivityMode.HTTP_ONLY; } } - connectivityCheckerCountDownLatch.countDown(); consumer.accept(connectivityMode); if (connectivityMode.equals(ConnectivityMode.NOT_CONNECTED)) { Platform.runLater(() -> { @@ -176,6 +175,11 @@ private void cancelPeriodSearch() { * Utility method to cancel any ongoing periodic search jobs or close web socket. */ public void shutdown() { + try { + connectivityCheckerCountDownLatch.await(10000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Logger.getLogger(LogbookSearchController.class.getName()).log(Level.WARNING, "Got InterruptedException waiting for connectivity mode result"); + } if (connectivityModeObjectProperty.get().equals(ConnectivityMode.WEB_SOCKETS_SUPPORTED) && webSocketClientService != null) { Logger.getLogger(LogbookSearchController.class.getName()).log(Level.INFO, "Shutting down web socket"); webSocketClientService.removeWebSocketMessageHandler(this); @@ -189,29 +193,20 @@ else if(connectivityModeObjectProperty.get().equals(ConnectivityMode.HTTP_ONLY)) protected abstract void search(); protected void connectWebSocket() { - try { - if(connectivityCheckerCountDownLatch.await(3000, TimeUnit.MILLISECONDS)){ - if (connectivityModeObjectProperty.get().equals(ConnectivityMode.WEB_SOCKETS_SUPPORTED)) { - webSocketClientService = new WebSocketClientService(() -> { - logger.log(Level.INFO, "Connected to web socket on " + webSocketConnectUrl); - webSocketConnected.setValue(true); - viewSearchPane.visibleProperty().set(true); - errorPane.visibleProperty().set(false); - search(); - }, () -> { - logger.log(Level.INFO, "Disconnected from web socket on " + webSocketConnectUrl); - webSocketConnected.set(false); - viewSearchPane.visibleProperty().set(false); - errorPane.visibleProperty().set(true); - }, webSocketConnectUrl, subscriptionEndpoint, null); - webSocketClientService.addWebSocketMessageHandler(this); - webSocketClientService.connect(); - } - } - } catch (InterruptedException e) { - Logger.getLogger(LogbookSearchController.class.getName()).log(Level.WARNING, "Timed out waiting for connectivity check"); - connectivityModeObjectProperty.set(ConnectivityMode.NOT_CONNECTED); - } + webSocketClientService = new WebSocketClientService(() -> { + logger.log(Level.INFO, "Connected to web socket on " + webSocketConnectUrl); + webSocketConnected.setValue(true); + viewSearchPane.visibleProperty().set(true); + errorPane.visibleProperty().set(false); + search(); + }, () -> { + logger.log(Level.INFO, "Disconnected from web socket on " + webSocketConnectUrl); + webSocketConnected.set(false); + viewSearchPane.visibleProperty().set(false); + errorPane.visibleProperty().set(true); + }, webSocketConnectUrl, subscriptionEndpoint, null); + webSocketClientService.addWebSocketMessageHandler(this); + webSocketClientService.connect(); } @Override From b1a471b9b237fa1a50ef4dd399efe58cd8b0d3f8 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Fri, 3 Oct 2025 09:58:51 +0200 Subject: [PATCH 21/23] Make connect and disconnect thread safe --- .../WebSocketClientService.java | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java index 453ac5ae3f..7ab60ed456 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java @@ -46,6 +46,10 @@ *
  • Publish a topic named /path/web-socket/messages, where path is optional.
  • * *

    + *

    + * NOTE: client code must call the {@link #shutdown()} method to not + * leave the web socket connection alive. + *

    */ public class WebSocketClientService { @@ -122,26 +126,18 @@ public void sendEcho(String message) { if (stompSession != null && stompSession.isConnected() && echoEndpoint != null) { stompSession.send(echoEndpoint, message); } - } /** - * Disconnects the STOMP session if non-null and connected. + * Disconnects the socket if connected and terminates connection thread. */ - public void disconnect() { + public synchronized void shutdown() { + attemptReconnect.set(false); if (stompSession != null && stompSession.isConnected()) { stompSession.disconnect(); } } - /** - * Disconnects the socket if connected and terminates connection thread. - */ - public void shutdown() { - attemptReconnect.set(false); - disconnect(); - } - /** * Attempts to connect to the remote peer, both in initial connection and in a reconnection scenario. * If connection fails, new attempts are made every 10s until successful. @@ -156,23 +152,25 @@ public void connect() { stompClient.setTaskScheduler(threadPoolTaskScheduler); stompClient.setDefaultHeartbeat(new long[]{30000, 30000}); StompSessionHandler sessionHandler = new StompSessionHandler(); + logger.log(Level.INFO, "Attempting web socket connection to " + connectUrl); new Thread(() -> { while (attemptReconnect.get()) { - logger.log(Level.INFO, "Attempting web socket connection to " + connectUrl); try { - stompSession = stompClient.connect(connectUrl, sessionHandler).get(); - stompSession.subscribe(this.subscriptionEndpoint, new StompFrameHandler() { - @Override - public Type getPayloadType(StompHeaders headers) { - return String.class; - } + synchronized (WebSocketClientService.this) { + stompSession = stompClient.connect(connectUrl, sessionHandler).get(); + stompSession.subscribe(this.subscriptionEndpoint, new StompFrameHandler() { + @Override + public Type getPayloadType(StompHeaders headers) { + return String.class; + } - @Override - public void handleFrame(StompHeaders headers, Object payload) { - logger.log(Level.INFO, "Handling subscription frame: " + payload); - webSocketMessageHandlers.forEach(h -> h.handleWebSocketMessage((String) payload)); - } - }); + @Override + public void handleFrame(StompHeaders headers, Object payload) { + logger.log(Level.INFO, "Handling subscription frame: " + payload); + webSocketMessageHandlers.forEach(h -> h.handleWebSocketMessage((String) payload)); + } + }); + } } catch (Exception e) { logger.log(Level.WARNING, "Got exception when trying to connect", e); } From ca49f74eeb8fb0738b5f31c3815f29fb6adb4fa6 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Fri, 3 Oct 2025 10:06:17 +0200 Subject: [PATCH 22/23] Update controls from server data off UI thread --- .../olog/ui/AdvancedSearchViewController.java | 76 ++++++++++--------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AdvancedSearchViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AdvancedSearchViewController.java index b2b5d2f144..1cf7da8281 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AdvancedSearchViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AdvancedSearchViewController.java @@ -362,45 +362,47 @@ public AnchorPane getPane() { * @param queryString Query string containing search terms and values */ private void updateControls(String queryString) { - Map queryStringParameters = LogbookQueryUtil.parseHumanReadableQueryString(queryString); - queryStringParameters.entrySet().forEach(entry -> { - Keys keys = Keys.findKey(entry.getKey()); - if (keys != null) { - if (keys.equals(Keys.LEVEL)) { - List validatedLevels = getValidatedLevelsSelection(entry.getValue()); - if (validatedLevels.isEmpty()) { - searchParameters.levelsProperty().setValue(null); - } else { - String selectedLevels = - String.join(",", validatedLevels); - searchParameters.levelsProperty().setValue(selectedLevels); + JobManager.schedule("Update controls from server data", monitor -> { + Map queryStringParameters = LogbookQueryUtil.parseHumanReadableQueryString(queryString); + queryStringParameters.entrySet().forEach(entry -> { + Keys keys = Keys.findKey(entry.getKey()); + if (keys != null) { + if (keys.equals(Keys.LEVEL)) { + List validatedLevels = getValidatedLevelsSelection(entry.getValue()); + if (validatedLevels.isEmpty()) { + searchParameters.levelsProperty().setValue(null); + } else { + String selectedLevels = + String.join(",", validatedLevels); + searchParameters.levelsProperty().setValue(selectedLevels); + } + levelsContextMenu.getItems().forEach(mi -> { + LevelSelectionMenuItem levelSelectionMenuItem = + (LevelSelectionMenuItem) mi; + levelSelectionMenuItem.setSelected(validatedLevels.contains(levelSelectionMenuItem.getCheckBox().getText())); + }); + } else if (keys.equals(Keys.LOGBOOKS)) { + List validatedLogbookNames = getValidatedLogbooksSelection(entry.getValue()); + if (validatedLogbookNames.isEmpty()) { + searchParameters.logbooksProperty().setValue(null); + } else { + String selectedLogbooks = + String.join(",", validatedLogbookNames); + searchParameters.logbooksProperty().setValue(selectedLogbooks); + } + logbookSearchPopover.setSelected(validatedLogbookNames); + } else if (keys.equals(Keys.TAGS)) { + List validatedTagsNames = getValidatedTagsSelection(entry.getValue()); + if (validatedTagsNames.isEmpty()) { + searchParameters.tagsProperty().setValue(null); + } else { + String selectedTags = String.join(",", validatedTagsNames); + searchParameters.tagsProperty().setValue(selectedTags); + } + tagSearchPopover.setSelected(validatedTagsNames); } - levelsContextMenu.getItems().forEach(mi -> { - LevelSelectionMenuItem levelSelectionMenuItem = - (LevelSelectionMenuItem) mi; - levelSelectionMenuItem.setSelected(validatedLevels.contains(levelSelectionMenuItem.getCheckBox().getText())); - }); - } else if (keys.equals(Keys.LOGBOOKS)) { - List validatedLogbookNames = getValidatedLogbooksSelection(entry.getValue()); - if (validatedLogbookNames.isEmpty()) { - searchParameters.logbooksProperty().setValue(null); - } else { - String selectedLogbooks = - String.join(",", validatedLogbookNames); - searchParameters.logbooksProperty().setValue(selectedLogbooks); - } - logbookSearchPopover.setSelected(validatedLogbookNames); - } else if (keys.equals(Keys.TAGS)) { - List validatedTagsNames = getValidatedTagsSelection(entry.getValue()); - if (validatedTagsNames.isEmpty()) { - searchParameters.tagsProperty().setValue(null); - } else { - String selectedTags = String.join(",", validatedTagsNames); - searchParameters.tagsProperty().setValue(selectedTags); - } - tagSearchPopover.setSelected(validatedTagsNames); } - } + }); }); } From fdd3aea38f98cc765aa429031dd0fbbdbdc6bc72 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Fri, 3 Oct 2025 11:48:43 +0200 Subject: [PATCH 23/23] Update connect logic --- .../WebSocketClientService.java | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java index 7ab60ed456..5b83cb5bb6 100644 --- a/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java +++ b/core/websocket/src/main/java/org/phoebus/core/websocket/springframework/WebSocketClientService.java @@ -154,22 +154,26 @@ public void connect() { StompSessionHandler sessionHandler = new StompSessionHandler(); logger.log(Level.INFO, "Attempting web socket connection to " + connectUrl); new Thread(() -> { - while (attemptReconnect.get()) { - try { + while (true) { + try{ synchronized (WebSocketClientService.this) { - stompSession = stompClient.connect(connectUrl, sessionHandler).get(); - stompSession.subscribe(this.subscriptionEndpoint, new StompFrameHandler() { - @Override - public Type getPayloadType(StompHeaders headers) { - return String.class; - } + if(attemptReconnect.get()) { + stompSession = stompClient.connect(connectUrl, sessionHandler).get(); + stompSession.subscribe(this.subscriptionEndpoint, new StompFrameHandler() { + @Override + public Type getPayloadType(StompHeaders headers) { + return String.class; + } - @Override - public void handleFrame(StompHeaders headers, Object payload) { - logger.log(Level.INFO, "Handling subscription frame: " + payload); - webSocketMessageHandlers.forEach(h -> h.handleWebSocketMessage((String) payload)); - } - }); + @Override + public void handleFrame(StompHeaders headers, Object payload) { + logger.log(Level.INFO, "Handling subscription frame: " + payload); + webSocketMessageHandlers.forEach(h -> h.handleWebSocketMessage((String) payload)); + } + }); + attemptReconnect.set(false); + } + break; } } catch (Exception e) { logger.log(Level.WARNING, "Got exception when trying to connect", e); @@ -210,7 +214,6 @@ public void handleFrame(StompHeaders headers, @Nullable Object payload) { */ @Override public void afterConnected(StompSession session, StompHeaders connectedHeaders) { - attemptReconnect.set(false); logger.log(Level.INFO, "Connected to web socket"); if (connectCallback != null) { connectCallback.run();