/* * Copyright 2015 Julien Viet * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.termd.core.http.websocket.server; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.termd.core.pty.PtyMaster; import io.termd.core.pty.TtyBridge; import io.undertow.server.HttpHandler; import io.undertow.websockets.WebSocketConnectionCallback; import io.undertow.websockets.WebSocketProtocolHandshakeHandler; import io.undertow.websockets.core.CloseMessage; import io.undertow.websockets.core.WebSockets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.function.Consumer; /** * @author <a href="mailto:julien@julienviet.com">Julien Viet</a> * @author <a href="mailto:matejonnet@gmail.com">Matej Lazar</a> */ class Term { private Logger log = LoggerFactory.getLogger(Term.class); final String context; private Runnable onDestroy; final Set<Consumer<TaskStatusUpdateEvent>> statusUpdateListeners = new HashSet<>(); private WebSocketTtyConnection webSocketTtyConnection; private boolean activeCommand; private ScheduledExecutorService executor; public Term(TermServer termServer, String context, Runnable onDestroy, ScheduledExecutorService executor) { this.context = context; this.onDestroy = onDestroy; this.executor = executor; } public void addStatusUpdateListener(Consumer<TaskStatusUpdateEvent> statusUpdateListener) { statusUpdateListeners.add(statusUpdateListener); } public void removeStatusUpdateListener(Consumer<TaskStatusUpdateEvent> statusUpdateListener) { statusUpdateListeners.remove(statusUpdateListener); } public Consumer<PtyMaster> onTaskCreated() { return (ptyMaster) -> { ptyMaster.setChangeHandler((prev, next) -> { notifyStatusUpdated( new TaskStatusUpdateEvent("" + ptyMaster.getId(), prev, next, context) ); }); }; } void notifyStatusUpdated(TaskStatusUpdateEvent event) { if (event.getNewStatus().isFinal()) { activeCommand = false; log.trace("Command [context:{} taskId:{}] execution completed with status {}.", event.getContext(), event.getTaskId(), event.getNewStatus()); destroyIfInactiveAndDisconnected(); } else { activeCommand = true; } for (Consumer<TaskStatusUpdateEvent> statusUpdateListener : statusUpdateListeners) { log.debug("Notifying listener {} in task {} with new status {}", statusUpdateListener, event.getTaskId(), event.getNewStatus()); statusUpdateListener.accept(event); } } private void destroyIfInactiveAndDisconnected() { if (!activeCommand && !webSocketTtyConnection.isOpen()) { log.debug("Destroying Term as there is no running command and no active connection."); onDestroy.run(); } } synchronized HttpHandler getWebSocketHandler() { WebSocketConnectionCallback onWebSocketConnected = (exchange, webSocketChannel) -> { if (webSocketTtyConnection == null) { webSocketTtyConnection = new WebSocketTtyConnection(webSocketChannel, executor); webSocketChannel.addCloseTask((task) -> {webSocketTtyConnection.removeWebSocketChannel(); destroyIfInactiveAndDisconnected();}); TtyBridge ttyBridge = new TtyBridge(webSocketTtyConnection); ttyBridge .setProcessListener(onTaskCreated()) .readline(); } else { if (webSocketTtyConnection.isOpen()) { webSocketTtyConnection.addReadonlyChannel(webSocketChannel); webSocketChannel.addCloseTask((task) -> {webSocketTtyConnection.removeReadonlyChannel(webSocketChannel); destroyIfInactiveAndDisconnected();}); } else { webSocketTtyConnection.setWebSocketChannel(webSocketChannel); webSocketChannel.addCloseTask((task) -> {webSocketTtyConnection.removeWebSocketChannel(); destroyIfInactiveAndDisconnected();}); } } }; return new WebSocketProtocolHandshakeHandler(onWebSocketConnected); } HttpHandler webSocketStatusUpdateHandler() { WebSocketConnectionCallback webSocketConnectionCallback = (exchange, webSocketChannel) -> { Consumer<TaskStatusUpdateEvent> statusUpdateListener = event -> { Map<String, Object> statusUpdate = new HashMap<>(); statusUpdate.put("action", "status-update"); statusUpdate.put("event", event); ObjectMapper objectMapper = new ObjectMapper(); try { String message = objectMapper.writeValueAsString(statusUpdate); WebSockets.sendText(message, webSocketChannel, null); } catch (JsonProcessingException e) { log.error("Cannot write object to JSON", e); String errorMessage = "Cannot write object to JSON: " + e.getMessage(); WebSockets.sendClose(CloseMessage.UNEXPECTED_ERROR, errorMessage, webSocketChannel, null); } }; log.debug("Registering new status update listener {}.", statusUpdateListener); addStatusUpdateListener(statusUpdateListener); webSocketChannel.addCloseTask((task) -> removeStatusUpdateListener(statusUpdateListener)); }; return new WebSocketProtocolHandshakeHandler(webSocketConnectionCallback); } }