WebSocket консоль сервера#

Global 3 SE Server предоставляет консоль управления, доступную через WebSocket-соединение. С помощью команд, передаваемых через WebSocket, возможно:

  • Перезагружать код прикладных приложений.

  • Выполнять Jexl-скрипты.

Алгоритм работы с консолью#

  • Открытие WebSocket-соединения.

  • Отправка команды аутентификации пользователя в системе.

  • Отправка исполняемых команд.

  • Закрытие WebSocket-соединения.

Открытие WebSocket-соединения#

Для открытия WebSocket-соединения с консолью сервера необходимо выполнить HTTP-запрос по адресу:
ws[s]://{server[:port]}/app/sys/ws/console.

Формат команды#

Командой является строка в формате:

{command}[\n{arguments}]

где:

  • {command} — строка команды. Может состоять из одного или нескольких слов.

  • \n — символ новой строки #10. Является разделителем команды и её аргументов. Не обязателен, если у команды нет аргументов.

  • {arguments} — строка аргументов команды. Может содержать любые символы, включая переносы строк.

Формат результата выполнения команды#

Результатом выполнения команды является строка в формате JSON:

{
  "success": true,
  "data": null,
  "exception": null,
  "exceptionStack": null
}

где:

  • success — флаг успешности выполнения команды: true | false.

  • data — результат выполнения команды: null | "string" | JSON.

  • exception — сообщение возникшего исключения: null | "string".

  • exceptionStack — стек возникшего исключения: null | "string".

Список команд#

  • login — выполняет аутентификацию пользователя {user} в базе данных {database} и запускает рабочий сеанс пользователя.
    Использование команды login, в качестве способа аутентификации, было выбрано по причине невозможности использования HTTP-заголовков некоторыми WebSocket-клиентами. Например: JMeter WebSocket Load Testing Sampler.

    {user}/{password}@{database}
    
  • logout — закрывает рабочий сеанс пользователя.

  • reload sbt — выполняет перезагрузку прикладного кода текущего решения.

  • reload sbt force — выполняет перезагрузку инфраструктуры EclipseLink, прикладного кода и общих библиотек текущего решения.

  • jexl — любое значение, возвращённое из Jexl-скрипта, будет преобразовано в строку.

    // Произвольный текст Jexl-скрипта
    return 1 + 2;
    

Java пример взаимодествия с консолью#

Библиотеки, используемые в примере клиента:

  • «org.asynchttpclient» % «async-http-client» % «2.12.3».

  • «com.fasterxml.jackson.core» % «jackson-databind» % «2.8.9».

package ru.bitec.app.examples.ws.console;

import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.Dsl;
import org.asynchttpclient.ws.WebSocket;
import org.asynchttpclient.ws.WebSocketListener;
import org.asynchttpclient.ws.WebSocketUpgradeHandler;

import com.fasterxml.jackson.databind.ObjectMapper;

import javax.websocket.CloseReason;
import java.io.Serializable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class WsConsoleClientTest {

  public static void main(String[] args) throws Exception {
      try (WsConsoleClient client = WsConsoleClient.open("ws://localhost:8080/app/sys/ws/console")) {
          client.login("admin", "admin", "pgtest");
          System.out.println("1 + 2 = " + client.evaluateJexl("return 1 + 2;"));
          client.reloadSbt(false);
          client.logout();
      }
  }   

  public class WsConsoleClient implements AutoCloseable {

    public static WsConsoleClient open(String url) throws Exception {
        return open(url, 3000);
    }

    public static WsConsoleClient open(String url, int timeout) throws Exception {
        return new WsConsoleClient().connect(url, timeout);
    }

    private final ObjectMapper objectMapper_ = new ObjectMapper();
    private int commandTimeout_ = 60000;
    private AsyncHttpClient asyncHttpClient_;
    private WebSocket webSocketClient_;
    private volatile CompletableFuture<String> webSocketResultFuture_;
    private final WebSocketUpgradeHandler wsHandler = new WebSocketUpgradeHandler.Builder().addWebSocketListener(new WebSocketListener() {
        @Override
        public void onOpen(WebSocket websocket) {
            // WebSocket connection opened
        }

        @Override
        public void onClose(WebSocket websocket, int code, String reason) {
            if (code != CloseReason.CloseCodes.NORMAL_CLOSURE.getCode()) {
                webSocketResultFuture_.completeExceptionally(new Exception(reason));
            }
        }

        @Override
        public void onError(Throwable t) {
            webSocketResultFuture_.completeExceptionally(t);
        }

        @Override
        public void onTextFrame(String payload, boolean finalFragment, int rsv) {
            webSocketResultFuture_.complete(payload);
        }
    }).build();

    public WsConsoleClient connect(String url, int timeout) throws Exception {
        AsyncHttpClient syncHttpClient = Dsl.asyncHttpClient();
        try {
            webSocketClient_ = syncHttpClient
                    .prepareGet(url)
                    .setRequestTimeout(timeout)
                    .execute(wsHandler)
                    .get();
        } catch (Exception e) {
            syncHttpClient.close();
            throw e;
        }
        asyncHttpClient_ = syncHttpClient;
        return this;
    }

    public void close() throws Exception {
        if (asyncHttpClient_ != null) {
            try {
                if (webSocketClient_ != null && webSocketClient_.isOpen()) {
                    try {
                        webSocketClient_.sendCloseFrame().get();
                    } finally {
                        webSocketClient_ = null;
                    }
                }
            } finally {
                asyncHttpClient_.close();
            }
        }
    }

    public int getCommandTimeout() {
        return commandTimeout_;
    }

    public void setCommandTimeout(int commandTimeout) {
        this.commandTimeout_ = commandTimeout;
    }

    public void login(String user, String password, String database) throws Exception {
        sendCommand("login", String.format("%s/%s@%s", user, password, database));
    }

    public void logout() throws Exception {
        sendCommand("logout");
    }

    public String evaluateJexl(String script) throws Exception {
        return sendCommand("jexl", script);
    }

    public void reloadSbt(boolean force) throws Exception {
        sendCommand("reload sbt" + (force ? " force" : ""));
    }

    String sendCommand(String command) throws Exception {
        return sendCommand(command, null);
    }

    String sendCommand(String command, String args) throws Exception {
        validateConnection();
        if (command == null || command.trim().isEmpty()) {
            throw new Exception("Command can not be null or empty.");
        }
        webSocketResultFuture_ = new CompletableFuture<>();
        try {
            String payload = command + "\n" + (args != null ? args : "");
            webSocketClient_.sendTextFrame(payload).get();
        } catch (Exception e) {
            webSocketResultFuture_.completeExceptionally(e);
        }
        return parseResponse(webSocketResultFuture_.get(commandTimeout_, TimeUnit.MILLISECONDS));
    }

    private String parseResponse(String response) throws Exception {
        ConsoleResponse consoleResponse = objectMapper_.readValue(response, ConsoleResponse.class);
        if (consoleResponse.success) {
            return consoleResponse.data;
        } else {
            throw new Exception(consoleResponse.exception);
        }
    }

    private void validateConnection() throws Exception {
        if (webSocketClient_ == null) {
            throw new Exception("WebSocket connection is not connected.");
        }
        if (!webSocketClient_.isOpen()) {
            throw new Exception("WebSocket connection is closed");
        }
        CompletableFuture<String> webSocketResultFuture = webSocketResultFuture_;
        if (webSocketResultFuture != null && !webSocketResultFuture.isDone()) {
            throw new Exception("Prior console command is not completed. Wait for the result of the previous command.");
        }
    }

    private final static class ConsoleResponse implements Serializable {

        private static final long serialVersionUID = -5577579081118070434L;
        /**
         * Флаг успешного выполнения запроса. false, если при обработке запроса возникло исключение.
         */
        private boolean success = false;
        /**
         * Строковые данные
         */
        private String data;
        /**
         * Текст возникшего исключения
         */
        private String exception;
        /**
         * Стек возникшего исключения
         */
        private String exceptionStack;

        public boolean getSuccess() {
            return success;
        }

        public void setSuccess(boolean success) {
            this.success = success;
        }

        public String getData() {
            return data;
        }

        public void setData(String data) {
            this.data = data;
        }

        public String getException() {
            return exception;
        }

        public void setException(String exception) {
            this.exception = exception;
        }

        public String getExceptionStack() {
            return exceptionStack;
        }

        public void setExceptionStack(String exceptionStack) {
            this.exceptionStack = exceptionStack;
        }
    }
  }
}