mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
CB-2885-TE-web-session-state-synchronisation-between-several-nodes (#1556)
* CB-3209 Add instance ID to web session (#1532) * CB-3209 Add instance ID to web session * CB-3209 Add instance ID to web session Moves applicationRunId to BaseWebApplication * CB-3267 Send session expired event on socket create attempt Implements session restore on socket creation * CB-3267 Send session expired event on socket create attempt Changes logging * CB-3267 Add dialog and notification for node change * CB-3154 save user creds to secrets in initConnection (#1502) * CB-3154 save user creds to secrets in initConnection * CB-3155 add tab 'change credentials' * CB-2964 feat: lazy loaders * CB-2964 add reconnect, display tab conditionally * CB-2964 Init connection on dialog result * CB-2964 fix logic * CB-2964 Add logic * CB-2964 Update ConnectionAuthService.ts Co-authored-by: Alexey <wrouds@gmail.com> --------- Co-authored-by: Tatiana Fokina <tatiana.fokina@dbeaver.com> Co-authored-by: dariamarutkina <125263541+dariamarutkina@users.noreply.github.com> Co-authored-by: Aleksey Potsetsuev <wrouds@gmail.com> Co-authored-by: Tatiana Fokina <123386439+tfokina@users.noreply.github.com> * CB-3267 Send session expired event on socket create attempt Implements session restoration on '/sessionState' request * CB-3267 Send session expired event on socket create attempt Fixes WebSocketTest.initClient test * CB-3267 Send session expired event on socket create attempt Fixes code style issues * CB-3267 fix localization text * CB-3267 Send session expired event on socket create attempt Fixes code style issues * CB-2885 TE: web session state synchronisation between several nodes Reverts session restoration on '/sessionState' request * CB-2885 PR fixes, stop opening websocket when node changes * CB-2885 TE: web session state synchronisation between several nodes Replaces Collections.synchronizedList by CopyOnWriteArrayList in CBJettyWebSocketManager#socketBySessionId * CB-2885 TE: web session state synchronisation between several nodes Fixes code style issues * CB-2885 TE: web session state synchronisation between several nodes Implements restoring old authentication information before creating a new WebSession instance. * CB-2885 move restore logic to separate method --------- Co-authored-by: Tatiana Fokina <tatiana.fokina@dbeaver.com> Co-authored-by: Alexander Skoblikov <aleksandr.skoblikov@dbeaver.com> Co-authored-by: dariamarutkina <125263541+dariamarutkina@users.noreply.github.com> Co-authored-by: Aleksey Potsetsuev <wrouds@gmail.com> Co-authored-by: Tatiana Fokina <123386439+tfokina@users.noreply.github.com>
This commit is contained in:
co-authored by
Alexey
Tatiana Fokina
dariamarutkina
Tatiana Fokina
Alexander Skoblikov
parent
2107f4f968
commit
0a5caea6b9
+8
@@ -47,6 +47,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Web application
|
||||
@@ -59,6 +60,13 @@ public abstract class BaseWebApplication extends BaseApplicationImpl implements
|
||||
|
||||
private static final Log log = Log.getLog(BaseWebApplication.class);
|
||||
|
||||
private final String applicationRunId = UUID.randomUUID().toString();
|
||||
|
||||
@NotNull
|
||||
public String getApplicationRunId() {
|
||||
return applicationRunId;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public DBPWorkspace createWorkspace(@NotNull DBPPlatform platform, @NotNull IWorkspace eclipseWorkspace) {
|
||||
|
||||
@@ -75,6 +75,12 @@ public interface WebApplication extends DBPApplication {
|
||||
|
||||
String getApplicationInstanceId() throws DBException;
|
||||
|
||||
/**
|
||||
* Returns random UUID, new on every launch
|
||||
*/
|
||||
@NotNull
|
||||
String getApplicationRunId();
|
||||
|
||||
WSEventController getEventController();
|
||||
|
||||
/**
|
||||
|
||||
+5
@@ -125,6 +125,11 @@ public abstract class BaseWebSession extends AbstractSessionPersistent {
|
||||
return id;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public WebApplication getApplication() {
|
||||
return application;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public LocalDateTime getSessionStart() {
|
||||
|
||||
@@ -142,10 +142,6 @@ public class WebSession extends BaseWebSession
|
||||
}
|
||||
}
|
||||
|
||||
public WebApplication getApplication() {
|
||||
return application;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public DBPProject getSingletonProject() {
|
||||
return getWorkspace().getActiveProject();
|
||||
|
||||
@@ -8,6 +8,8 @@ enum CBServerEventId {
|
||||
|
||||
cb_session_log_updated,
|
||||
|
||||
cb_session_websocket_connected,
|
||||
|
||||
cb_datasource_created,
|
||||
cb_datasource_updated,
|
||||
cb_datasource_deleted,
|
||||
@@ -35,6 +37,7 @@ enum CBClientEventId {
|
||||
enum CBEventTopic {
|
||||
cb_config,
|
||||
cb_session_log,
|
||||
cb_session,
|
||||
cb_datasource,
|
||||
cb_datasource_folder,
|
||||
cb_scripts,
|
||||
@@ -89,6 +92,13 @@ type CBSessionLogEvent implements CBServerEvent {
|
||||
topicId: CBEventTopic
|
||||
}
|
||||
|
||||
# WebSocket connected event
|
||||
type WSSocketConnectedEvent implements CBServerEvent {
|
||||
id: CBServerEventId!
|
||||
topicId: CBEventTopic
|
||||
applicationRunId: String!
|
||||
}
|
||||
|
||||
# RM project update event
|
||||
type CBProjectUpdateEvent implements CBServerEvent {
|
||||
id: CBServerEventId!
|
||||
|
||||
+9
-4
@@ -25,11 +25,11 @@ import org.eclipse.jetty.websocket.api.WebSocketAdapter;
|
||||
import org.eclipse.jetty.websocket.api.WriteCallback;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.websocket.WSConstants;
|
||||
import org.jkiss.dbeaver.model.websocket.WSUtils;
|
||||
import org.jkiss.dbeaver.model.websocket.event.WSClientEvent;
|
||||
import org.jkiss.dbeaver.model.websocket.event.WSClientEventType;
|
||||
import org.jkiss.dbeaver.model.websocket.event.WSEvent;
|
||||
import org.jkiss.dbeaver.model.websocket.event.WSSocketConnectedEvent;
|
||||
import org.jkiss.dbeaver.model.websocket.event.client.WSUpdateActiveProjectsClientEvent;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -40,19 +40,20 @@ public class CBEventsWebSocket extends WebSocketAdapter implements CBWebSessionE
|
||||
|
||||
@NotNull
|
||||
private final BaseWebSession webSession;
|
||||
|
||||
@NotNull
|
||||
private final WriteCallback callback;
|
||||
|
||||
public CBEventsWebSocket(@NotNull BaseWebSession webSession) {
|
||||
this.webSession = webSession;
|
||||
this.callback = new WebSocketPingPongCallback(webSession);
|
||||
|
||||
callback = new WebSocketPingPongCallback(webSession);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
super.onWebSocketConnect(session);
|
||||
this.webSession.addEventHandler(this);
|
||||
handeWebSessionEvent(new WSSocketConnectedEvent(webSession.getApplication().getApplicationRunId()));
|
||||
log.debug("EventWebSocket connected to the " + webSession.getSessionId() + " session");
|
||||
}
|
||||
|
||||
@@ -121,7 +122,11 @@ public class CBEventsWebSocket extends WebSocketAdapter implements CBWebSessionE
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
getSession().close();
|
||||
var session = getSession();
|
||||
// the socket may not be connected to the client
|
||||
if (session != null) {
|
||||
getSession().close();
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
+35
-22
@@ -17,65 +17,78 @@
|
||||
package io.cloudbeaver.server.websockets;
|
||||
|
||||
import io.cloudbeaver.model.session.BaseWebSession;
|
||||
import io.cloudbeaver.model.session.WebHeadlessSession;
|
||||
import io.cloudbeaver.server.CBPlatform;
|
||||
import io.cloudbeaver.service.session.WebSessionManager;
|
||||
import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse;
|
||||
import org.eclipse.jetty.websocket.server.JettyWebSocketCreator;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
public class CBJettyWebSocketManager implements JettyWebSocketCreator {
|
||||
private static final Log log = Log.getLog(CBJettyWebSocketManager.class);
|
||||
private final Map<String, List<CBEventsWebSocket>> socketBySessionId = new ConcurrentHashMap<>();
|
||||
private final WebSessionManager webSessionManager;
|
||||
|
||||
public CBJettyWebSocketManager(WebSessionManager webSessionManager) {
|
||||
public CBJettyWebSocketManager(@NotNull WebSessionManager webSessionManager) {
|
||||
this.webSessionManager = webSessionManager;
|
||||
|
||||
new WebSocketPingPongJob(CBPlatform.getInstance(), this).scheduleMonitor();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object createWebSocket(JettyServerUpgradeRequest request, JettyServerUpgradeResponse resp) {
|
||||
BaseWebSession webSession;
|
||||
public Object createWebSocket(@NotNull JettyServerUpgradeRequest request, JettyServerUpgradeResponse resp) {
|
||||
var httpRequest = request.getHttpServletRequest();
|
||||
var webSession = webSessionManager.getOrRestoreSession(httpRequest);
|
||||
if (webSession != null) {
|
||||
// web client session
|
||||
return createNewEventsWebSocket(webSession);
|
||||
}
|
||||
// possible desktop client session
|
||||
try {
|
||||
webSession = resolveWebSession(request);
|
||||
var headlessSession = createHeadlessSession(httpRequest);
|
||||
if (headlessSession == null) {
|
||||
log.debug("Couldn't create headless session");
|
||||
return null;
|
||||
}
|
||||
return createNewEventsWebSocket(headlessSession);
|
||||
} catch (DBException e) {
|
||||
log.error("Error resolve websocket session", e);
|
||||
return null;
|
||||
}
|
||||
if (webSession == null) {
|
||||
return null;
|
||||
}
|
||||
var webSessionId = webSession.getSessionId();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CBEventsWebSocket createNewEventsWebSocket(@NotNull BaseWebSession webSession) {
|
||||
var sessionId = webSession.getSessionId();
|
||||
var newWebSocket = new CBEventsWebSocket(webSession);
|
||||
socketBySessionId.computeIfAbsent(webSessionId, key -> new ArrayList<>()).add(newWebSocket);
|
||||
log.info("Websocket created for session: " + webSessionId);
|
||||
socketBySessionId.computeIfAbsent(sessionId, key -> new CopyOnWriteArrayList<>())
|
||||
.add(newWebSocket);
|
||||
log.info("Websocket created for session: " + sessionId);
|
||||
return newWebSocket;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private BaseWebSession resolveWebSession(JettyServerUpgradeRequest request) throws DBException {
|
||||
if (request.getHttpServletRequest().getSession() == null) {
|
||||
private WebHeadlessSession createHeadlessSession(@NotNull HttpServletRequest request) throws DBException {
|
||||
var httpSession = request.getSession(false);
|
||||
if (httpSession == null) {
|
||||
log.debug("CloudBeaver web session not exist, try to create headless session");
|
||||
return webSessionManager.getHeadlessSession(request.getHttpServletRequest(), true);
|
||||
} else {
|
||||
log.debug("CloudBeaver session not found with id " + httpSession.getId() + ", try to create headless session");
|
||||
}
|
||||
var webSessionId = request.getHttpServletRequest().getSession().getId();
|
||||
var webSession = webSessionManager.getSession(webSessionId);
|
||||
if (webSession != null) {
|
||||
return webSession;
|
||||
}
|
||||
log.error("CloudBeaver session not found with id " + webSessionId + ", try to create headless session");
|
||||
|
||||
return webSessionManager.getHeadlessSession(request.getHttpServletRequest(), true);
|
||||
return webSessionManager.getHeadlessSession(request, true);
|
||||
}
|
||||
|
||||
public void sendPing() {
|
||||
|
||||
+7
-4
@@ -28,20 +28,19 @@ import io.cloudbeaver.server.CBPlatform;
|
||||
import io.cloudbeaver.server.graphql.GraphQLEndpoint;
|
||||
import io.cloudbeaver.service.security.SMUtils;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.rm.RMProject;
|
||||
import org.jkiss.utils.ArrayUtils;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.lang.reflect.*;
|
||||
import java.util.Set;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* Web service implementation
|
||||
@@ -121,6 +120,10 @@ public abstract class WebServiceBindingBase<API_TYPE extends DBWService> impleme
|
||||
return getWebConnection(getWebSession(env), getProjectReference(env), env.getArgument("connectionId"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns WebSession from cache or null
|
||||
*/
|
||||
@Nullable
|
||||
public static WebSession findWebSession(DataFetchingEnvironment env) {
|
||||
return CBPlatform.getInstance().getSessionManager().findWebSession(
|
||||
getServletRequest(env));
|
||||
|
||||
+89
-24
@@ -37,11 +37,11 @@ import org.jkiss.dbeaver.model.websocket.WSConstants;
|
||||
import org.jkiss.dbeaver.runtime.DBWorkbench;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Web session manager
|
||||
@@ -62,7 +62,10 @@ public class WebSessionManager {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
public BaseWebSession closeSession(@NotNull HttpServletRequest request) throws DBException {
|
||||
/**
|
||||
* Closes Web Session, associated to HttpSession from {@code request}
|
||||
*/
|
||||
public BaseWebSession closeSession(@NotNull HttpServletRequest request) {
|
||||
HttpSession session = request.getSession();
|
||||
if (session != null) {
|
||||
BaseWebSession webSession;
|
||||
@@ -108,24 +111,36 @@ public class WebSessionManager {
|
||||
WebSession webSession;
|
||||
synchronized (sessionMap) {
|
||||
var baseWebSession = sessionMap.get(sessionId);
|
||||
if (baseWebSession == null) {
|
||||
if (baseWebSession == null && CBApplication.getInstance().isConfigurationMode()) {
|
||||
try {
|
||||
webSession = createWebSessionImpl(httpSession);
|
||||
} catch (DBException e) {
|
||||
throw new DBWebException("Failed to create web session", e);
|
||||
}
|
||||
sessionMap.put(sessionId, webSession);
|
||||
|
||||
if (!CBApplication.getInstance().isConfigurationMode()) {
|
||||
if (!httpSession.isNew()) {
|
||||
webSession.setCacheExpired(true);
|
||||
if (errorOnNoFound) {
|
||||
throw new DBWebException("Session has expired", DBWebException.ERROR_CODE_SESSION_EXPIRED);
|
||||
}
|
||||
}
|
||||
tryToRestorePreviousUserSession(webSession);
|
||||
log.debug("> New web session '" + webSession.getSessionId() + "'");
|
||||
} else if (baseWebSession == null) {
|
||||
try {
|
||||
webSession = createWebSessionImpl(httpSession);
|
||||
} catch (DBException e) {
|
||||
throw new DBWebException("Failed to create web session", e);
|
||||
}
|
||||
|
||||
boolean restored = false;
|
||||
try {
|
||||
restored = restorePreviousUserSession(webSession);
|
||||
} catch (DBException e) {
|
||||
log.error("Failed to restore previous user session", e);
|
||||
}
|
||||
|
||||
if (!restored && errorOnNoFound && !httpSession.isNew()) {
|
||||
throw new DBWebException("Session has expired", DBWebException.ERROR_CODE_SESSION_EXPIRED);
|
||||
}
|
||||
|
||||
log.debug((restored ? "Restored " : "New ") + "web session '" + webSession.getSessionId() + "'");
|
||||
|
||||
webSession.setCacheExpired(!httpSession.isNew());
|
||||
|
||||
sessionMap.put(sessionId, webSession);
|
||||
} else {
|
||||
if (!(baseWebSession instanceof WebSession)) {
|
||||
throw new DBWebException("Unexpected session type: " + baseWebSession.getClass().getName());
|
||||
@@ -144,17 +159,67 @@ public class WebSessionManager {
|
||||
return webSession;
|
||||
}
|
||||
|
||||
private void tryToRestorePreviousUserSession(WebSession webSession) {
|
||||
try {
|
||||
SMAuthInfo oldAuthInfo = webSession.getSecurityController().restoreUserSession(webSession.getSessionId());
|
||||
if (oldAuthInfo == null) {
|
||||
return;
|
||||
}
|
||||
boolean linkWithActiveUser = false; // because it's old credentials and should already be linked if needed
|
||||
new WebSessionAuthProcessor(webSession, oldAuthInfo, linkWithActiveUser).authenticateSession();
|
||||
} catch (DBException e) {
|
||||
log.error("Failed to restore previous user session", e);
|
||||
/**
|
||||
* Returns not expired session from cache, or restore it.
|
||||
*
|
||||
* @return WebSession object or null, if session expired or invalid
|
||||
*/
|
||||
@Nullable
|
||||
public WebSession getOrRestoreSession(@NotNull HttpServletRequest request) {
|
||||
var httpSession = request.getSession();
|
||||
if (httpSession == null) {
|
||||
log.debug("Http session is null. No Web Session returned");
|
||||
return null;
|
||||
}
|
||||
var sessionId = httpSession.getId();
|
||||
WebSession webSession;
|
||||
synchronized (sessionMap) {
|
||||
if (sessionMap.containsKey(sessionId)) {
|
||||
var cachedWebSession = sessionMap.get(sessionId);
|
||||
if (!(cachedWebSession instanceof WebSession)) {
|
||||
log.warn("Unexpected session type: " + cachedWebSession.getClass().getName());
|
||||
return null;
|
||||
}
|
||||
return (WebSession) cachedWebSession;
|
||||
} else {
|
||||
try {
|
||||
var oldAuthInfo = getApplication().getSecurityController().restoreUserSession(sessionId);
|
||||
if (oldAuthInfo == null) {
|
||||
log.debug("Couldn't restore previous user session '" + sessionId + "'");
|
||||
return null;
|
||||
}
|
||||
|
||||
webSession = createWebSessionImpl(httpSession);
|
||||
restorePreviousUserSession(webSession, oldAuthInfo);
|
||||
|
||||
sessionMap.put(sessionId, webSession);
|
||||
log.debug("Web session restored");
|
||||
return webSession;
|
||||
} catch (DBException e) {
|
||||
log.error("Failed to restore previous user session", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean restorePreviousUserSession(@NotNull WebSession webSession) throws DBException {
|
||||
var oldAuthInfo = webSession.getSecurityController().restoreUserSession(webSession.getSessionId());
|
||||
if (oldAuthInfo == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
restorePreviousUserSession(webSession, oldAuthInfo);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void restorePreviousUserSession(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull SMAuthInfo authInfo
|
||||
) throws DBException {
|
||||
var linkWithActiveUser = false; // because its old credentials and should already be linked if needed
|
||||
new WebSessionAuthProcessor(webSession, authInfo, linkWithActiveUser)
|
||||
.authenticateSession();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -10,6 +10,7 @@ export default [
|
||||
['ui_processing_loading', 'Loading...'],
|
||||
['ui_processing_cancel', 'Cancel'],
|
||||
['ui_processing_canceling', 'Cancelling...'],
|
||||
['ui_processing_reload', 'Reload'],
|
||||
['ui_processing_retry', 'Retry'],
|
||||
['ui_processing_ok', 'Ok'],
|
||||
['ui_processing_create', 'Create'],
|
||||
@@ -101,6 +102,8 @@ export default [
|
||||
['app_root_session_expired_title', 'Session expired'],
|
||||
['app_root_session_expired_message', 'Session has expired. Do you want to reload?'],
|
||||
['app_root_session_expired_reload', 'Reload'],
|
||||
['app_root_server_node_changed_title', 'Server node changed'],
|
||||
['app_root_server_node_changed_message', 'Server node changed. You need to reload the application.'],
|
||||
['app_root_quota_exceeded', 'Quota exceeded'],
|
||||
['core_eventsLog_dbeaverErrorDetails', 'Error details'],
|
||||
];
|
||||
|
||||
@@ -8,6 +8,7 @@ export default [
|
||||
['ui_processing_loading', 'Загрузка...'],
|
||||
['ui_processing_cancel', 'Отменить'],
|
||||
['ui_processing_canceling', 'Отмена...'],
|
||||
['ui_processing_reload', 'Перезагрузить'],
|
||||
['ui_processing_retry', 'Повторить'],
|
||||
['ui_processing_ok', 'Принять'],
|
||||
['ui_processing_create', 'Создать'],
|
||||
@@ -99,6 +100,8 @@ export default [
|
||||
['app_root_session_expired_title', 'Сессия истекла'],
|
||||
['app_root_session_expired_message', 'Сессия истекла. Хотите перезагрузить страницу?'],
|
||||
['app_root_session_expired_reload', 'Перезагрузить'],
|
||||
['app_root_server_node_changed_title', 'Узел сервера изменён'],
|
||||
['app_root_server_node_changed_message', 'Узел сервера изменён. Необходимо перезагрузить приложение.'],
|
||||
['app_root_quota_exceeded', 'Квота превышена'],
|
||||
['core_eventsLog_dbeaverErrorDetails', 'Детали ошибки'],
|
||||
];
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export class ServerNodeError extends Error {
|
||||
constructor(message: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
this.name = 'Server Node Error';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { Dependency, injectable } from '@cloudbeaver/core-di';
|
||||
import { Executor, IExecutor } from '@cloudbeaver/core-executor';
|
||||
import { GraphQLService, WsSocketConnectedEvent } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import { ServerNodeError } from './ServerNodeError';
|
||||
import { ServerEventId, SessionEventSource } from './SessionEventSource';
|
||||
|
||||
@injectable()
|
||||
export class ServerNodeService extends Dependency {
|
||||
private applicationRunId: string | null;
|
||||
onApplicationRunIdChange: IExecutor;
|
||||
constructor(
|
||||
private readonly graphQLService: GraphQLService,
|
||||
private readonly sessionEventSource: SessionEventSource
|
||||
) {
|
||||
super();
|
||||
this.onApplicationRunIdChange = new Executor();
|
||||
this.applicationRunId = null;
|
||||
this.sessionEventSource.onEvent<WsSocketConnectedEvent>(ServerEventId.CbSessionWebsocketConnected, data => {
|
||||
this.applicationRunIdChanged(data.applicationRunId);
|
||||
});
|
||||
}
|
||||
|
||||
private applicationRunIdChanged(applicationRunId: string): void {
|
||||
if (this.applicationRunId === null) {
|
||||
this.applicationRunId = applicationRunId;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.applicationRunId === applicationRunId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applicationRunId = applicationRunId;
|
||||
this.graphQLService.blockRequests(new ServerNodeError('Server node changed'));
|
||||
this.sessionEventSource.disconnect();
|
||||
this.onApplicationRunIdChange.execute();
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ implements IServerEventEmitter<ISessionEvent, ISessionEvent, SessionEventId, Ses
|
||||
private readonly subject: WebSocketSubject<ISessionEvent>;
|
||||
private readonly oldEventsSubject: Subject<ISessionEvent>;
|
||||
private readonly retryTimer: Observable<number>;
|
||||
private disconnected: boolean;
|
||||
|
||||
constructor(
|
||||
private readonly networkStateService: NetworkStateService,
|
||||
@@ -66,9 +67,10 @@ implements IServerEventEmitter<ISessionEvent, ISessionEvent, SessionEventId, Ses
|
||||
this.closeSubject = new Subject();
|
||||
this.openSubject = new Subject();
|
||||
this.errorSubject = new Subject();
|
||||
this.disconnected = false;
|
||||
this.retryTimer = interval(RETRY_INTERVAL)
|
||||
.pipe(
|
||||
filter(() => !this.sessionExpireService.expired && networkStateService.state)
|
||||
filter(() => !this.sessionExpireService.expired && networkStateService.state && !this.disconnected)
|
||||
);
|
||||
this.subject = webSocket({
|
||||
url: environmentService.wsEndpoint,
|
||||
@@ -154,6 +156,10 @@ implements IServerEventEmitter<ISessionEvent, ISessionEvent, SessionEventId, Ses
|
||||
return this;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.disconnected = true;
|
||||
}
|
||||
|
||||
private handleErrors() {
|
||||
return (source: Observable<ISessionEvent>):Observable<ISessionEvent> => source.pipe(
|
||||
share(),
|
||||
|
||||
@@ -21,5 +21,6 @@ export * from './SessionActionService';
|
||||
export * from './SessionDataResource';
|
||||
export * from './SessionSettingsService';
|
||||
export * from './SessionExpireService';
|
||||
export * from './ServerNodeService';
|
||||
export * from './WindowEventsService';
|
||||
export { manifest as coreRootManifest } from './manifest';
|
||||
|
||||
@@ -15,6 +15,7 @@ import { PermissionsService } from './PermissionsService';
|
||||
import { QuotasService } from './QuotasService';
|
||||
import { ServerConfigEventHandler } from './ServerConfigEventHandler';
|
||||
import { ServerConfigResource } from './ServerConfigResource';
|
||||
import { ServerNodeService } from './ServerNodeService';
|
||||
import { ServerService } from './ServerService';
|
||||
import { ServerSettingsService } from './ServerSettingsService';
|
||||
import { SessionActionService } from './SessionActionService';
|
||||
@@ -44,6 +45,7 @@ export const manifest: PluginManifest = {
|
||||
SessionActionService,
|
||||
SessionDataResource,
|
||||
SessionExpireService,
|
||||
ServerNodeService,
|
||||
SessionResource,
|
||||
SessionService,
|
||||
SessionSettingsService,
|
||||
|
||||
@@ -202,6 +202,7 @@ export enum CbEventTopic {
|
||||
CbDatasourceFolder = 'cb_datasource_folder',
|
||||
CbProjects = 'cb_projects',
|
||||
CbScripts = 'cb_scripts',
|
||||
CbSession = 'cb_session',
|
||||
CbSessionLog = 'cb_session_log'
|
||||
}
|
||||
|
||||
@@ -248,7 +249,8 @@ export enum CbServerEventId {
|
||||
CbRmResourceCreated = 'cb_rm_resource_created',
|
||||
CbRmResourceDeleted = 'cb_rm_resource_deleted',
|
||||
CbRmResourceUpdated = 'cb_rm_resource_updated',
|
||||
CbSessionLogUpdated = 'cb_session_log_updated'
|
||||
CbSessionLogUpdated = 'cb_session_log_updated',
|
||||
CbSessionWebsocketConnected = 'cb_session_websocket_connected'
|
||||
}
|
||||
|
||||
export interface CbSessionLogEvent extends CbServerEvent {
|
||||
@@ -1730,6 +1732,12 @@ export interface UserInfo {
|
||||
userId: Scalars['ID'];
|
||||
}
|
||||
|
||||
export interface WsSocketConnectedEvent extends CbServerEvent {
|
||||
applicationRunId: Scalars['String'];
|
||||
id: CbServerEventId;
|
||||
topicId?: Maybe<CbEventTopic>;
|
||||
}
|
||||
|
||||
export interface WebFeatureSet {
|
||||
description?: Maybe<Scalars['String']>;
|
||||
enabled: Scalars['Boolean'];
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { Button, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogBody, CommonDialogFooter, CommonDialogHeader, CommonDialogWrapper, DialogComponent } from '@cloudbeaver/core-dialogs';
|
||||
import { RouterService } from '@cloudbeaver/core-routing';
|
||||
|
||||
const styles = css`
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
CommonDialogFooter {
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ServerNodeChangedDialog: DialogComponent<null, null> = observer(function ServerNodeChangedDialog({
|
||||
rejectDialog,
|
||||
}) {
|
||||
const routerService = useService(RouterService);
|
||||
const translate = useTranslate();
|
||||
function reload() {
|
||||
routerService.reload();
|
||||
}
|
||||
|
||||
return styled(styles)(
|
||||
<CommonDialogWrapper size='small' fixedSize>
|
||||
<CommonDialogHeader title="app_root_server_node_changed_title" onReject={rejectDialog} />
|
||||
<CommonDialogBody noOverflow>
|
||||
<p>{translate('app_root_server_node_changed_message')}</p>
|
||||
</CommonDialogBody>
|
||||
<CommonDialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
mod={['outlined']}
|
||||
onClick={rejectDialog}
|
||||
>
|
||||
{translate('ui_processing_cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
mod={['unelevated']}
|
||||
onClick={reload}
|
||||
>
|
||||
{translate('ui_processing_reload')}
|
||||
</Button>
|
||||
</CommonDialogFooter>
|
||||
</CommonDialogWrapper>
|
||||
);
|
||||
});
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { ActionSnackbar } from '@cloudbeaver/core-blocks';
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
import { ENotificationType, NotificationService } from '@cloudbeaver/core-events';
|
||||
import { ServerNodeService } from '@cloudbeaver/core-root';
|
||||
import { RouterService } from '@cloudbeaver/core-routing';
|
||||
|
||||
import { ServerNodeChangedDialog } from './ServerNodeChangedDialog';
|
||||
|
||||
@injectable()
|
||||
export class ServerNodeChangedDialogService extends Bootstrap {
|
||||
constructor(
|
||||
private readonly routerService: RouterService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly commonDialogService: CommonDialogService,
|
||||
private readonly serverNodeService: ServerNodeService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
register(): void {
|
||||
this.serverNodeService.onApplicationRunIdChange.addPostHandler(this.handleServerNodeChanged.bind(this));
|
||||
}
|
||||
|
||||
load(): void | Promise<void> { }
|
||||
|
||||
private async handleServerNodeChanged(): Promise<void> {
|
||||
const state = await this.commonDialogService.open(ServerNodeChangedDialog, null);
|
||||
|
||||
if (state === DialogueStateResult.Rejected) {
|
||||
this.notificationService.customNotification(() => ActionSnackbar, {
|
||||
actionText: 'ui_processing_reload',
|
||||
onAction: () => this.routerService.reload(),
|
||||
}, { title: 'app_root_server_node_changed_title', message: 'app_root_server_node_changed_message', persistent: true, type: ENotificationType.Error });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { DataSynchronizationResolverBootstrap } from './DataSynchronization/Data
|
||||
import { LocaleService } from './LocaleService';
|
||||
import { NetworkStateNotificationService } from './NetworkStateNotification/NetworkStateNotificationService';
|
||||
import { PluginBootstrap } from './PluginBootstrap';
|
||||
import { ServerNodeChangedDialogService } from './ServerNodeChangedDialog/ServerNodeChangedDialogService';
|
||||
import { SessionExpiredDialogService } from './SessionExpireDialog/SessionExpiredDialogService';
|
||||
import { SessionExpireWarningDialogService } from './SessionExpireWarningDialog/SessionExpireWarningDialogService';
|
||||
|
||||
@@ -21,6 +22,7 @@ export const rootPlugin: PluginManifest = {
|
||||
LocaleService,
|
||||
PluginBootstrap,
|
||||
SessionExpiredDialogService,
|
||||
ServerNodeChangedDialogService,
|
||||
SessionExpireWarningDialogService,
|
||||
NetworkStateNotificationService,
|
||||
DataSynchronizationResolverBootstrap,
|
||||
|
||||
Reference in New Issue
Block a user