Перейти к основному содержанию
`Ctrl`+`K`

# 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;
            }
        }
      }
    }