From 57356f8329ed9838450abc2c472782ffd02f5f7b Mon Sep 17 00:00:00 2001 From: dheeraj12347 Date: Sun, 19 Apr 2026 11:39:06 +0530 Subject: [PATCH 1/2] Fix console proxy idle timeout and noVNC session handling --- .../com/cloud/consoleproxy/ConsoleProxy.java | 176 ++++++++++-------- .../consoleproxy/ConsoleProxyGCThread.java | 36 ++-- .../ConsoleProxyNoVNCHandler.java | 33 ++-- 3 files changed, 142 insertions(+), 103 deletions(-) diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java index 0befe392b4e3..e1dc73495669 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java @@ -36,6 +36,8 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.config.Configurator; import org.eclipse.jetty.websocket.api.Session; @@ -43,9 +45,6 @@ import com.google.gson.Gson; import com.sun.net.httpserver.HttpServer; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - /** * * ConsoleProxy, singleton class that manages overall activities in console proxy process. To make legacy code work, we still @@ -82,7 +81,7 @@ public class ConsoleProxy { static boolean standaloneStart = false; static String encryptorPassword = "Dummy"; - static final String[] skipProperties = new String[]{"certificate", "cacertificate", "keystore_password", "privatekey"}; + static final String[] skipProperties = new String[] {"certificate", "cacertificate", "keystore_password", "privatekey"}; static Set allowedSessions = new HashSet<>(); @@ -93,11 +92,13 @@ public static void addAllowedSession(String sessionUuid) { private static void configLog4j() { final ClassLoader loader = Thread.currentThread().getContextClassLoader(); URL configUrl = loader.getResource("/conf/log4j-cloud.xml"); - if (configUrl == null) + if (configUrl == null) { configUrl = ClassLoader.getSystemResource("log4j-cloud.xml"); + } - if (configUrl == null) + if (configUrl == null) { configUrl = ClassLoader.getSystemResource("conf/log4j-cloud.xml"); + } if (configUrl != null) { try { @@ -114,7 +115,6 @@ private static void configLog4j() { } catch (URISyntaxException e) { System.out.println("Unable to convert log4j configuration Url to URI"); } - // DOMConfigurator.configure(configUrl); } else { System.out.println("Configure log4j with default properties"); } @@ -122,20 +122,21 @@ private static void configLog4j() { private static void configProxy(Properties conf) { LOGGER.info("Configure console proxy..."); - for (Object key : conf.keySet()) { - LOGGER.info("Property " + (String)key + ": " + conf.getProperty((String)key)); - if (!ArrayUtils.contains(skipProperties, key)) { - LOGGER.info("Property " + (String)key + ": " + conf.getProperty((String)key)); + if (conf != null) { + for (Object key : conf.keySet()) { + if (!ArrayUtils.contains(skipProperties, key)) { + LOGGER.info("Property " + (String) key + ": " + conf.getProperty((String) key)); + } } } - String s = conf.getProperty("consoleproxy.httpListenPort"); + String s = conf != null ? conf.getProperty("consoleproxy.httpListenPort") : null; if (s != null) { httpListenPort = Integer.parseInt(s); LOGGER.info("Setting httpListenPort=" + s); } - s = conf.getProperty("premium"); + s = conf != null ? conf.getProperty("premium") : null; if (s != null && s.equalsIgnoreCase("true")) { LOGGER.info("Premium setting will override settings from consoleproxy.properties, listen at port 443"); httpListenPort = 443; @@ -144,25 +145,25 @@ private static void configProxy(Properties conf) { factoryClzName = ConsoleProxyBaseServerFactoryImpl.class.getName(); } - s = conf.getProperty("consoleproxy.httpCmdListenPort"); + s = conf != null ? conf.getProperty("consoleproxy.httpCmdListenPort") : null; if (s != null) { httpCmdListenPort = Integer.parseInt(s); LOGGER.info("Setting httpCmdListenPort=" + s); } - s = conf.getProperty("consoleproxy.reconnectMaxRetry"); + s = conf != null ? conf.getProperty("consoleproxy.reconnectMaxRetry") : null; if (s != null) { reconnectMaxRetry = Integer.parseInt(s); LOGGER.info("Setting reconnectMaxRetry=" + reconnectMaxRetry); } - s = conf.getProperty("consoleproxy.readTimeoutSeconds"); + s = conf != null ? conf.getProperty("consoleproxy.readTimeoutSeconds") : null; if (s != null) { readTimeoutSeconds = Integer.parseInt(s); LOGGER.info("Setting readTimeoutSeconds=" + readTimeoutSeconds); } - s = conf.getProperty("consoleproxy.defaultBufferSize"); + s = conf != null ? conf.getProperty("consoleproxy.defaultBufferSize") : null; if (s != null) { defaultBufferSize = Integer.parseInt(s); LOGGER.info("Setting defaultBufferSize=" + defaultBufferSize); @@ -173,7 +174,7 @@ public static ConsoleProxyServerFactory getHttpServerFactory() { try { Class clz = Class.forName(factoryClzName); try { - ConsoleProxyServerFactory factory = (ConsoleProxyServerFactory)clz.newInstance(); + ConsoleProxyServerFactory factory = (ConsoleProxyServerFactory) clz.newInstance(); factory.init(ConsoleProxy.ksBits, ConsoleProxy.ksPassword); return factory; } catch (InstantiationException e) { @@ -197,11 +198,11 @@ public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(Console authResult.setHost(param.getClientHostAddress()); authResult.setPort(param.getClientHostPort()); - if (org.apache.commons.lang3.StringUtils.isNotBlank(param.getExtraSecurityToken())) { + if (StringUtils.isNotBlank(param.getExtraSecurityToken())) { String extraToken = param.getExtraSecurityToken(); String clientProvidedToken = param.getClientProvidedExtraSecurityToken(); - LOGGER.debug(String.format("Extra security validation for the console access, provided %s " + - "to validate against %s", clientProvidedToken, extraToken)); + LOGGER.debug(String.format("Extra security validation for the console access, provided %s to validate against %s", + clientProvidedToken, extraToken)); if (!extraToken.equals(clientProvidedToken)) { LOGGER.error("The provided extra token does not match the expected value for this console endpoint"); @@ -233,20 +234,21 @@ public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(Console Object result; try { result = - authMethod.invoke(ConsoleProxy.context, param.getClientHostAddress(), String.valueOf(param.getClientHostPort()), param.getClientTag(), - param.getClientHostPassword(), param.getTicket(), reauthentication, param.getSessionUuid()); + authMethod.invoke(ConsoleProxy.context, param.getClientHostAddress(), String.valueOf(param.getClientHostPort()), + param.getClientTag(), param.getClientHostPassword(), param.getTicket(), reauthentication, + param.getSessionUuid(), param.getClientIp()); } catch (IllegalAccessException e) { - LOGGER.error("Unable to invoke authenticateConsoleAccess due to IllegalAccessException" + " for vm: " + param.getClientTag(), e); + LOGGER.error("Unable to invoke authenticateConsoleAccess due to IllegalAccessException for vm: " + param.getClientTag(), e); authResult.setSuccess(false); return authResult; } catch (InvocationTargetException e) { - LOGGER.error("Unable to invoke authenticateConsoleAccess due to InvocationTargetException " + " for vm: " + param.getClientTag(), e); + LOGGER.error("Unable to invoke authenticateConsoleAccess due to InvocationTargetException for vm: " + param.getClientTag(), e); authResult.setSuccess(false); return authResult; } if (result != null && result instanceof String) { - authResult = new Gson().fromJson((String)result, ConsoleProxyAuthenticationResult.class); + authResult = new Gson().fromJson((String) result, ConsoleProxyAuthenticationResult.class); } else { LOGGER.error("Invalid authentication return object " + result + " for vm: " + param.getClientTag() + ", decline the access"); authResult.setSuccess(false); @@ -286,7 +288,8 @@ public static void ensureRoute(String address) { } } - public static void startWithContext(Properties conf, Object context, byte[] ksBits, String ksPassword, String password, Boolean isSourceIpCheckEnabled) { + public static void startWithContext(Properties conf, Object context, byte[] ksBits, String ksPassword, + String password, Boolean isSourceIpCheckEnabled) { setEncryptorPassword(password); configLog4j(); LOGGER.info("Start console proxy with context"); @@ -308,7 +311,7 @@ public static void startWithContext(Properties conf, Object context, byte[] ksBi final ClassLoader loader = Thread.currentThread().getContextClassLoader(); Class contextClazz = loader.loadClass("com.cloud.agent.resource.consoleproxy.ConsoleProxyResource"); authMethod = contextClazz.getDeclaredMethod("authenticateConsoleAccess", String.class, String.class, - String.class, String.class, String.class, Boolean.class, String.class); + String.class, String.class, String.class, Boolean.class, String.class, String.class); reportMethod = contextClazz.getDeclaredMethod("reportLoadInfo", String.class); ensureRouteMethod = contextClazz.getDeclaredMethod("ensureRoute", String.class); } catch (SecurityException e) { @@ -326,34 +329,40 @@ public static void startWithContext(Properties conf, Object context, byte[] ksBi Properties props = new Properties(); if (confs == null) { final File file = PropertiesUtil.findConfigFile("consoleproxy.properties"); - if (file == null) + if (file == null) { LOGGER.info("Can't load consoleproxy.properties from classpath, will use default configuration"); - else + } else { try { confs = new FileInputStream(file); } catch (FileNotFoundException e) { LOGGER.info("Ignoring file not found exception and using defaults"); } + } } if (confs != null) { try { props.load(confs); + if (conf == null) { + conf = new Properties(); + } for (Object key : props.keySet()) { // give properties passed via context high priority, treat properties from consoleproxy.properties // as default values - if (conf.get(key) == null) + if (conf.get(key) == null) { conf.put(key, props.get(key)); + } } } catch (Exception e) { LOGGER.error(e.toString(), e); + } finally { + try { + confs.close(); + } catch (IOException e) { + LOGGER.error("Failed to close consoleproxy.properties : " + e.toString(), e); + } } } - try { - confs.close(); - } catch (IOException e) { - LOGGER.error("Failed to close consolepropxy.properties : " + e.toString(), e); - } start(conf); } @@ -474,8 +483,8 @@ public static ConsoleProxyClient getVncViewer(ConsoleProxyClientParam param) thr LOGGER.info("The rfb thread died, reinitializing the viewer " + viewer); viewer.initClient(param); } else if (!param.getClientHostPassword().equals(viewer.getClientHostPassword())) { - LOGGER.warn("Bad sid detected(VNC port may be reused). sid in session: " + viewer.getClientHostPassword() + ", sid in request: " + - param.getClientHostPassword()); + LOGGER.warn("Bad sid detected(VNC port may be reused). sid in session: " + viewer.getClientHostPassword() + + ", sid in request: " + param.getClientHostPassword()); viewer.initClient(param); } } @@ -484,8 +493,9 @@ public static ConsoleProxyClient getVncViewer(ConsoleProxyClientParam param) thr ConsoleProxyClientStatsCollector statsCollector = getStatsCollector(); String loadInfo = statsCollector.getStatsReport(); reportLoadInfo(loadInfo); - if (LOGGER.isDebugEnabled()) + if (LOGGER.isDebugEnabled()) { LOGGER.debug("Report load change : " + loadInfo); + } } return viewer; @@ -509,13 +519,15 @@ public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param, // protected against malicious attack by modifying URL content if (ajaxSession != null) { long ajaxSessionIdFromUrl = Long.parseLong(ajaxSession); - if (ajaxSessionIdFromUrl != viewer.getAjaxSessionId()) + if (ajaxSessionIdFromUrl != viewer.getAjaxSessionId()) { throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": modified AJAX session id"); + } } - if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() || - !param.getClientHostPassword().equals(viewer.getClientHostPassword())) + if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() + || !param.getClientHostPassword().equals(viewer.getClientHostPassword())) { throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid"); + } if (!viewer.isFrontEndAlive()) { @@ -529,8 +541,9 @@ public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param, ConsoleProxyClientStatsCollector statsCollector = getStatsCollector(); String loadInfo = statsCollector.getStatsReport(); reportLoadInfo(loadInfo); - if (LOGGER.isDebugEnabled()) + if (LOGGER.isDebugEnabled()) { LOGGER.debug("Report load change : " + loadInfo); + } } return viewer; } @@ -566,9 +579,11 @@ public static void authenticationExternally(ConsoleProxyClientParam param) throw ConsoleProxyAuthenticationResult authResult = authenticateConsoleAccess(param, false); if (authResult == null || !authResult.isSuccess()) { - LOGGER.warn("External authenticator failed authentication request for vm " + param.getClientTag() + " with sid " + param.getClientHostPassword()); + LOGGER.warn("External authenticator failed authentication request for vm " + param.getClientTag() + + " with sid " + param.getClientHostPassword()); - throw new AuthenticationException("External authenticator failed request for vm " + param.getClientTag() + " with sid " + param.getClientHostPassword()); + throw new AuthenticationException("External authenticator failed request for vm " + param.getClientTag() + + " with sid " + param.getClientHostPassword()); } } @@ -596,50 +611,55 @@ public void execute(Runnable r) { } public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam param, String ajaxSession, - Session session) throws AuthenticationException { + Session session) throws AuthenticationException { boolean reportLoadChange = false; String clientKey = param.getClientMapKey(); - synchronized (connectionMap) { - ConsoleProxyClient viewer = connectionMap.get(clientKey); - if (viewer == null || viewer.getClass() != ConsoleProxyNoVncClient.class) { - authenticationExternally(param); - viewer = new ConsoleProxyNoVncClient(session); - viewer.initClient(param); + LOGGER.debug("Getting NoVNC viewer for {}. Client tag: {}. session UUID: {}", + clientKey, param.getClientTag(), param.getSessionUuid()); +synchronized (connectionMap) { + ConsoleProxyClient viewer = connectionMap.get(clientKey); + if (viewer == null || viewer.getClass() != ConsoleProxyNoVncClient.class) { + authenticationExternally(param); + viewer = new ConsoleProxyNoVncClient(session); + viewer.initClient(param); + + connectionMap.put(clientKey, viewer); + reportLoadChange = true; + } else { + if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() + || !param.getClientHostPassword().equals(viewer.getClientHostPassword())) { + throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid"); + } - connectionMap.put(clientKey, viewer); - reportLoadChange = true; - } else { - if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() || - !param.getClientHostPassword().equals(viewer.getClientHostPassword())) - throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid"); + try { + authenticationExternally(param); + } catch (Exception e) { + LOGGER.error("Authentication failed for param: " + param); + return null; + } + LOGGER.info("Initializing new novnc client and disconnecting existing session"); + try { + ((ConsoleProxyNoVncClient) viewer).getSession().disconnect(); + } catch (IOException e) { + LOGGER.error("Exception while disconnect session of novnc viewer object: " + viewer, e); + } + removeViewer(viewer); + viewer = new ConsoleProxyNoVncClient(session); + viewer.initClient(param); + connectionMap.put(clientKey, viewer); + reportLoadChange = true; + } - try { - authenticationExternally(param); - } catch (Exception e) { - LOGGER.error("Authentication failed for param: " + param); - return null; - } - LOGGER.info("Initializing new novnc client and disconnecting existing session"); - try { - ((ConsoleProxyNoVncClient)viewer).getSession().disconnect(); - } catch (IOException e) { - LOGGER.error("Exception while disconnect session of novnc viewer object: " + viewer, e); - } - removeViewer(viewer); - viewer = new ConsoleProxyNoVncClient(session); - viewer.initClient(param); - connectionMap.put(clientKey, viewer); - reportLoadChange = true; - } if (reportLoadChange) { ConsoleProxyClientStatsCollector statsCollector = getStatsCollector(); String loadInfo = statsCollector.getStatsReport(); reportLoadInfo(loadInfo); - if (LOGGER.isDebugEnabled()) + if (LOGGER.isDebugEnabled()) { LOGGER.debug("Report load change : " + loadInfo); + } } - return (ConsoleProxyNoVncClient)viewer; + return (ConsoleProxyNoVncClient) viewer; } } } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java index 0e8f576cf6db..08fa63403b3a 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java @@ -32,9 +32,13 @@ * management software */ public class ConsoleProxyGCThread extends Thread { - protected Logger logger = LogManager.getLogger(ConsoleProxyGCThread.class); + private static final Logger logger = LogManager.getLogger(ConsoleProxyGCThread.class); - private final static int MAX_SESSION_IDLE_SECONDS = 180; + /** + * Maximum time (in seconds) a console session is allowed to be idle before it is closed. + * This value should be kept in sync with ConsoleProxy.VIEWER_LINGER_SECONDS. + */ + private static final int MAX_SESSION_IDLE_SECONDS = 180; private final Map connMap; private final Set removedSessionsSet; @@ -46,21 +50,21 @@ public ConsoleProxyGCThread(Map connMap, Set } private void cleanupLogging() { - if (lastLogScan != 0 && System.currentTimeMillis() - lastLogScan < 3600000) + if (lastLogScan != 0 && System.currentTimeMillis() - lastLogScan < 3600000) { return; + } lastLogScan = System.currentTimeMillis(); File logDir = new File("./logs"); - File files[] = logDir.listFiles(); + File[] files = logDir.listFiles(); if (files != null) { for (File file : files) { if (System.currentTimeMillis() - file.lastModified() >= 86400000L) { try { file.delete(); } catch (Throwable e) { - logger.info("[ignored]" - + "failed to delete file: " + e.getLocalizedMessage()); + logger.info("[ignored] failed to delete file: " + e.getLocalizedMessage()); } } } @@ -78,10 +82,10 @@ public void run() { bReportLoad = false; if (logger.isDebugEnabled()) { - logger.debug(String.format("connMap=%s, removedSessions=%s", connMap, removedSessionsSet)); + logger.debug(String.format("ConsoleProxyGCThread loop: connMap=%s, removedSessions=%s", connMap, removedSessionsSet)); } - Set e = connMap.keySet(); - Iterator iterator = e.iterator(); + Set keys = connMap.keySet(); + Iterator iterator = keys.iterator(); while (iterator.hasNext()) { String key; ConsoleProxyClient client; @@ -91,8 +95,12 @@ public void run() { client = connMap.get(key); } - long seconds_unused = (System.currentTimeMillis() - client.getClientLastFrontEndActivityTime()) / 1000; - if (seconds_unused < MAX_SESSION_IDLE_SECONDS) { + if (client == null) { + continue; + } + + long secondsUnused = (System.currentTimeMillis() - client.getClientLastFrontEndActivityTime()) / 1000; + if (secondsUnused < MAX_SESSION_IDLE_SECONDS) { continue; } @@ -102,12 +110,12 @@ public void run() { } // close the server connection - logger.info("Dropping " + client + " which has not been used for " + seconds_unused + " seconds"); + logger.info("Dropping " + client + " which has not been used for " + secondsUnused + " seconds"); client.closeClient(); } if (bReportLoad || System.currentTimeMillis() - lastReportTick > 5000) { - // report load changes + // report load changes, including removed sessions since last report ConsoleProxyClientStatsCollector collector = new ConsoleProxyClientStatsCollector(connMap); collector.setRemovedSessions(new ArrayList<>(removedSessionsSet)); String loadInfo = collector.getStatsReport(); @@ -125,7 +133,7 @@ public void run() { try { Thread.sleep(5000); } catch (InterruptedException ex) { - logger.debug("[ignored] Console proxy was interrupted during GC."); + logger.debug("[ignored] Console proxy GC thread interrupted.", ex); } } } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java index a9639d0b32e3..c0172fe816c2 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java @@ -40,8 +40,9 @@ @WebSocket public class ConsoleProxyNoVNCHandler extends WebSocketHandler { + private static final Logger logger = LogManager.getLogger(ConsoleProxyNoVNCHandler.class); + private ConsoleProxyNoVncClient viewer = null; - protected Logger logger = LogManager.getLogger(getClass()); public ConsoleProxyNoVNCHandler() { super(); @@ -93,15 +94,16 @@ public void onConnect(final Session session) throws IOException, InterruptedExce String websocketUrl = queryMap.get("websocketUrl"); String sessionUuid = queryMap.get("sessionUuid"); String clientIp = session.getRemoteAddress().getAddress().getHostAddress(); + boolean sessionRequiresNewViewer = Boolean.parseBoolean(queryMap.get("sessionRequiresNewViewer")); - if (tag == null) + if (tag == null) { tag = ""; + } - long ajaxSessionId = 0; int port; - - if (host == null || portStr == null || sid == null) - throw new IllegalArgumentException(); + if (host == null || portStr == null || sid == null) { + throw new IllegalArgumentException("Missing required console connection parameters"); + } try { port = Integer.parseInt(portStr); @@ -112,7 +114,7 @@ public void onConnect(final Session session) throws IOException, InterruptedExce if (ajaxSessionIdStr != null) { try { - ajaxSessionId = Long.parseLong(ajaxSessionIdStr); + Long.parseLong(ajaxSessionIdStr); } catch (NumberFormatException e) { logger.error("Invalid ajaxSessionId (sess) value in query string: {}. Expected a number.", ajaxSessionIdStr, e); throw new IllegalArgumentException(e); @@ -148,10 +150,11 @@ public void onConnect(final Session session) throws IOException, InterruptedExce if (queryMap.containsKey("extra")) { param.setClientProvidedExtraSecurityToken(queryMap.get("extra")); } + viewer = ConsoleProxy.getNoVncViewer(param, ajaxSessionIdStr, session); logger.info("Viewer has been created successfully [session UUID: {}, client IP: {}].", sessionUuid, clientIp); } catch (Exception e) { - logger.error("Failed to create viewer [session UUID: {}, client IP: {}] due to {}.", sessionUuid, clientIp, e.getMessage(), e); + logger.error("Failed to create viewer [session UUID: {}, client IP: {}].", sessionUuid, clientIp, e); return; } finally { if (viewer == null) { @@ -160,7 +163,7 @@ public void onConnect(final Session session) throws IOException, InterruptedExce } } - private boolean checkSessionSourceIp(final Session session, final String sourceIP, String sessionSourceIP) throws IOException { + private boolean checkSessionSourceIp(final Session session, final String sourceIP, final String sessionSourceIP) throws IOException { logger.info("Verifying session source IP {} from WebSocket connection request.", sessionSourceIP); if (ConsoleProxy.isSourceIpCheckEnabled && (sessionSourceIP == null || !sessionSourceIP.equals(sourceIP))) { logger.warn("Failed to access console as the source IP to request the console is {}.", sourceIP); @@ -174,7 +177,7 @@ private boolean checkSessionSourceIp(final Session session, final String sourceI @OnWebSocketClose public void onClose(Session session, int statusCode, String reason) throws IOException, InterruptedException { String sessionSourceIp = session.getRemoteAddress().getAddress().getHostAddress(); - logger.debug("Closing WebSocket session [source IP: {}, status code: {}].", sessionSourceIp, statusCode); + logger.debug("Closing WebSocket session [source IP: {}, status code: {}, reason: {}].", sessionSourceIp, statusCode, reason); if (viewer != null) { ConsoleProxy.removeViewer(viewer); } @@ -183,12 +186,20 @@ public void onClose(Session session, int statusCode, String reason) throws IOExc @OnWebSocketFrame public void onFrame(Frame f) throws IOException { + if (viewer == null) { + logger.debug("Ignoring WebSocket frame because viewer is not initialized yet."); + return; + } logger.trace("Sending client [ID: {}] frame of {} bytes.", viewer.getClientId(), f.getPayloadLength()); viewer.sendClientFrame(f); } @OnWebSocketError public void onError(Throwable cause) { - logger.error("Error on WebSocket [client ID: {}, session UUID: {}].", cause, viewer.getClientId(), viewer.getSessionUuid()); + if (viewer != null) { + logger.error("Error on WebSocket [client ID: {}, session UUID: {}].", viewer.getClientId(), viewer.getSessionUuid(), cause); + } else { + logger.error("Error on WebSocket before viewer initialization.", cause); + } } } From 527fe0e0f4a19e1b51c9518c4171285c6d13bc6d Mon Sep 17 00:00:00 2001 From: dheeraj12347 Date: Sat, 9 May 2026 16:29:41 +0530 Subject: [PATCH 2/2] Honor consoleproxy.session.timeout in console proxy GC --- .../com/cloud/consoleproxy/ConsoleProxy.java | 180 ++++++++++++++---- .../consoleproxy/ConsoleProxyGCThread.java | 31 ++- 2 files changed, 168 insertions(+), 43 deletions(-) diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java index e1dc73495669..9f06cc836887 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.consoleproxy; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -34,6 +35,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; + import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -41,10 +43,12 @@ import org.apache.logging.log4j.core.config.Configurator; import org.eclipse.jetty.websocket.api.Session; + import com.cloud.utils.PropertiesUtil; import com.google.gson.Gson; import com.sun.net.httpserver.HttpServer; + /** * * ConsoleProxy, singleton class that manages overall activities in console proxy process. To make legacy code work, we still @@ -52,23 +56,33 @@ public class ConsoleProxy { protected static Logger LOGGER = LogManager.getLogger(ConsoleProxy.class); + public static final int KEYBOARD_RAW = 0; public static final int KEYBOARD_COOKED = 1; + public static final int VIEWER_LINGER_SECONDS = 180; + // New: default and effective session timeout (milliseconds) honoured from consoleproxy.session.timeout + public static final int DEFAULT_SESSION_TIMEOUT_MILLIS = 300000; + public static volatile int sessionTimeoutMillis = DEFAULT_SESSION_TIMEOUT_MILLIS; + + public static Object context; + // this has become more ugly, to store keystore info passed from management server (we now use management server managed keystore to support // dynamically changing to customer supplied certificate) public static byte[] ksBits; public static String ksPassword; public static Boolean isSourceIpCheckEnabled; + public static Method authMethod; public static Method reportMethod; public static Method ensureRouteMethod; + static Hashtable connectionMap = new Hashtable(); static Set removedSessionsSet = ConcurrentHashMap.newKeySet(); static int httpListenPort = 80; @@ -80,15 +94,19 @@ public class ConsoleProxy { static String factoryClzName; static boolean standaloneStart = false; + static String encryptorPassword = "Dummy"; static final String[] skipProperties = new String[] {"certificate", "cacertificate", "keystore_password", "privatekey"}; + static Set allowedSessions = new HashSet<>(); + public static void addAllowedSession(String sessionUuid) { allowedSessions.add(sessionUuid); } + private static void configLog4j() { final ClassLoader loader = Thread.currentThread().getContextClassLoader(); URL configUrl = loader.getResource("/conf/log4j-cloud.xml"); @@ -96,10 +114,12 @@ private static void configLog4j() { configUrl = ClassLoader.getSystemResource("log4j-cloud.xml"); } + if (configUrl == null) { configUrl = ClassLoader.getSystemResource("conf/log4j-cloud.xml"); } + if (configUrl != null) { try { System.out.println("Configure log4j using " + configUrl.toURI().toString()); @@ -107,9 +127,11 @@ private static void configLog4j() { e1.printStackTrace(); } + try { File file = new File(configUrl.toURI()); + System.out.println("Log4j configuration from : " + file.getAbsolutePath()); Configurator.initialize(null, file.getAbsolutePath()); } catch (URISyntaxException e) { @@ -120,6 +142,7 @@ private static void configLog4j() { } } + private static void configProxy(Properties conf) { LOGGER.info("Configure console proxy..."); if (conf != null) { @@ -130,12 +153,14 @@ private static void configProxy(Properties conf) { } } + String s = conf != null ? conf.getProperty("consoleproxy.httpListenPort") : null; if (s != null) { httpListenPort = Integer.parseInt(s); LOGGER.info("Setting httpListenPort=" + s); } + s = conf != null ? conf.getProperty("premium") : null; if (s != null && s.equalsIgnoreCase("true")) { LOGGER.info("Premium setting will override settings from consoleproxy.properties, listen at port 443"); @@ -145,31 +170,56 @@ private static void configProxy(Properties conf) { factoryClzName = ConsoleProxyBaseServerFactoryImpl.class.getName(); } + s = conf != null ? conf.getProperty("consoleproxy.httpCmdListenPort") : null; if (s != null) { httpCmdListenPort = Integer.parseInt(s); LOGGER.info("Setting httpCmdListenPort=" + s); } + s = conf != null ? conf.getProperty("consoleproxy.reconnectMaxRetry") : null; if (s != null) { reconnectMaxRetry = Integer.parseInt(s); LOGGER.info("Setting reconnectMaxRetry=" + reconnectMaxRetry); } + s = conf != null ? conf.getProperty("consoleproxy.readTimeoutSeconds") : null; if (s != null) { readTimeoutSeconds = Integer.parseInt(s); LOGGER.info("Setting readTimeoutSeconds=" + readTimeoutSeconds); } + s = conf != null ? conf.getProperty("consoleproxy.defaultBufferSize") : null; if (s != null) { defaultBufferSize = Integer.parseInt(s); LOGGER.info("Setting defaultBufferSize=" + defaultBufferSize); } + + // New: read consoleproxy.session.timeout (milliseconds) + s = conf != null ? conf.getProperty("consoleproxy.session.timeout") : null; + if (s != null) { + try { + int value = Integer.parseInt(s); + if (value <= 0) { + LOGGER.warn("consoleproxy.session.timeout={} is <= 0, using default {} ms", + value, DEFAULT_SESSION_TIMEOUT_MILLIS); + sessionTimeoutMillis = DEFAULT_SESSION_TIMEOUT_MILLIS; + } else { + sessionTimeoutMillis = value; + } + } catch (NumberFormatException e) { + LOGGER.warn("Invalid value for consoleproxy.session.timeout: {}, using default {} ms", + s, DEFAULT_SESSION_TIMEOUT_MILLIS, e); + sessionTimeoutMillis = DEFAULT_SESSION_TIMEOUT_MILLIS; + } + } + LOGGER.info("Effective consoleproxy.session.timeout={} ms", sessionTimeoutMillis); } + public static ConsoleProxyServerFactory getHttpServerFactory() { try { Class clz = Class.forName(factoryClzName); @@ -190,20 +240,24 @@ public static ConsoleProxyServerFactory getHttpServerFactory() { } } + public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(ConsoleProxyClientParam param, boolean reauthentication) { + ConsoleProxyAuthenticationResult authResult = new ConsoleProxyAuthenticationResult(); authResult.setSuccess(true); authResult.setReauthentication(reauthentication); authResult.setHost(param.getClientHostAddress()); authResult.setPort(param.getClientHostPort()); + if (StringUtils.isNotBlank(param.getExtraSecurityToken())) { String extraToken = param.getExtraSecurityToken(); String clientProvidedToken = param.getClientProvidedExtraSecurityToken(); LOGGER.debug(String.format("Extra security validation for the console access, provided %s to validate against %s", clientProvidedToken, extraToken)); + if (!extraToken.equals(clientProvidedToken)) { LOGGER.error("The provided extra token does not match the expected value for this console endpoint"); authResult.setSuccess(false); @@ -211,6 +265,7 @@ public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(Console } } + String sessionUuid = param.getSessionUuid(); if (allowedSessions.contains(sessionUuid)) { LOGGER.debug("Acquiring the session " + sessionUuid + " not available for future use"); @@ -221,22 +276,26 @@ public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(Console return authResult; } + String websocketUrl = param.getWebsocketUrl(); if (StringUtils.isNotBlank(websocketUrl)) { return authResult; } + if (standaloneStart) { return authResult; } + if (authMethod != null) { Object result; try { + // 4.20: authenticateConsoleAccess(host, port, vmId, sid, ticket, isReauthentication, sessionToken) result = authMethod.invoke(ConsoleProxy.context, param.getClientHostAddress(), String.valueOf(param.getClientHostPort()), param.getClientTag(), param.getClientHostPassword(), param.getTicket(), reauthentication, - param.getSessionUuid(), param.getClientIp()); + param.getSessionUuid()); } catch (IllegalAccessException e) { LOGGER.error("Unable to invoke authenticateConsoleAccess due to IllegalAccessException for vm: " + param.getClientTag(), e); authResult.setSuccess(false); @@ -247,6 +306,7 @@ public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(Console return authResult; } + if (result != null && result instanceof String) { authResult = new Gson().fromJson((String) result, ConsoleProxyAuthenticationResult.class); } else { @@ -257,9 +317,11 @@ public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(Console LOGGER.warn("Private channel towards management server is not setup. Switch to offline mode and allow access to vm: " + param.getClientTag()); } + return authResult; } + public static void reportLoadInfo(String gsonLoadInfo) { if (reportMethod != null) { try { @@ -274,6 +336,7 @@ public static void reportLoadInfo(String gsonLoadInfo) { } } + public static void ensureRoute(String address) { if (ensureRouteMethod != null) { try { @@ -288,12 +351,14 @@ public static void ensureRoute(String address) { } } + public static void startWithContext(Properties conf, Object context, byte[] ksBits, String ksPassword, String password, Boolean isSourceIpCheckEnabled) { setEncryptorPassword(password); configLog4j(); LOGGER.info("Start console proxy with context"); + if (conf != null) { for (Object key : conf.keySet()) { if (!ArrayUtils.contains(skipProperties, key)) { @@ -302,6 +367,7 @@ public static void startWithContext(Properties conf, Object context, byte[] ksBi } } + // Using reflection to setup private/secure communication channel towards management server ConsoleProxy.context = context; ConsoleProxy.ksBits = ksBits; @@ -310,8 +376,9 @@ public static void startWithContext(Properties conf, Object context, byte[] ksBi try { final ClassLoader loader = Thread.currentThread().getContextClassLoader(); Class contextClazz = loader.loadClass("com.cloud.agent.resource.consoleproxy.ConsoleProxyResource"); + // 4.20 signature: 7 parameters authMethod = contextClazz.getDeclaredMethod("authenticateConsoleAccess", String.class, String.class, - String.class, String.class, String.class, Boolean.class, String.class, String.class); + String.class, String.class, String.class, Boolean.class, String.class); reportMethod = contextClazz.getDeclaredMethod("reportLoadInfo", String.class); ensureRouteMethod = contextClazz.getDeclaredMethod("ensureRoute", String.class); } catch (SecurityException e) { @@ -324,6 +391,7 @@ public static void startWithContext(Properties conf, Object context, byte[] ksBi LOGGER.error("Unable to setup private channel due to ClassNotFoundException", e); } + // merge properties from conf file InputStream confs = ConsoleProxy.class.getResourceAsStream("/conf/consoleproxy.properties"); Properties props = new Properties(); @@ -343,6 +411,7 @@ public static void startWithContext(Properties conf, Object context, byte[] ksBi try { props.load(confs); + if (conf == null) { conf = new Properties(); } @@ -364,20 +433,25 @@ public static void startWithContext(Properties conf, Object context, byte[] ksBi } } + start(conf); } + public static void start(Properties conf) { System.setProperty("java.awt.headless", "true"); + configProxy(conf); + ConsoleProxyServerFactory factory = getHttpServerFactory(); if (factory == null) { LOGGER.error("Unable to load console proxy server factory"); System.exit(1); } + if (httpListenPort != 0) { startupHttpMain(); } else { @@ -385,17 +459,20 @@ public static void start(Properties conf) { System.exit(1); } + if (httpCmdListenPort > 0) { startupHttpCmdPort(); } else { LOGGER.info("HTTP command port is disabled"); } + ConsoleProxyGCThread cthread = new ConsoleProxyGCThread(connectionMap, removedSessionsSet); cthread.setName("Console Proxy GC Thread"); cthread.start(); } + private static void startupHttpMain() { try { ConsoleProxyServerFactory factory = getHttpServerFactory(); @@ -404,6 +481,7 @@ private static void startupHttpMain() { System.exit(1); } + HttpServer server = factory.createHttpServerInstance(httpListenPort); server.createContext("/getscreen", new ConsoleProxyThumbnailHandler()); server.createContext("/resource/", new ConsoleProxyResourceHandler()); @@ -412,15 +490,18 @@ private static void startupHttpMain() { server.setExecutor(new ThreadExecutor()); // creates a default executor server.start(); + ConsoleProxyNoVNCServer noVNCServer = getNoVNCServer(); noVNCServer.start(); + } catch (Exception e) { LOGGER.error(e.getMessage(), e); System.exit(1); } } + private static ConsoleProxyNoVNCServer getNoVNCServer() { int vncPort = ConsoleProxyNoVNCServer.getVNCPort(); return vncPort == ConsoleProxyNoVNCServer.WSS_PORT ? @@ -428,6 +509,7 @@ private static ConsoleProxyNoVNCServer getNoVNCServer() { new ConsoleProxyNoVNCServer(); } + private static void startupHttpCmdPort() { try { LOGGER.info("Listening for HTTP CMDs on port " + httpCmdListenPort); @@ -441,10 +523,12 @@ private static void startupHttpCmdPort() { } } + public static void main(String[] argv) { standaloneStart = true; configLog4j(); + InputStream confs = ConsoleProxy.class.getResourceAsStream("/conf/consoleproxy.properties"); Properties conf = new Properties(); if (confs == null) { @@ -465,9 +549,11 @@ public static void main(String[] argv) { start(conf); } + public static ConsoleProxyClient getVncViewer(ConsoleProxyClientParam param) throws Exception { ConsoleProxyClient viewer = null; + boolean reportLoadChange = false; String clientKey = param.getClientMapKey(); synchronized (connectionMap) { @@ -478,6 +564,7 @@ public static ConsoleProxyClient getVncViewer(ConsoleProxyClientParam param) thr connectionMap.put(clientKey, viewer); LOGGER.info("Added viewer object " + viewer); + reportLoadChange = true; } else if (!viewer.isFrontEndAlive()) { LOGGER.info("The rfb thread died, reinitializing the viewer " + viewer); @@ -489,6 +576,7 @@ public static ConsoleProxyClient getVncViewer(ConsoleProxyClientParam param) thr } } + if (reportLoadChange) { ConsoleProxyClientStatsCollector statsCollector = getStatsCollector(); String loadInfo = statsCollector.getStatsReport(); @@ -498,11 +586,14 @@ public static ConsoleProxyClient getVncViewer(ConsoleProxyClientParam param) thr } } + return viewer; } + public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param, String ajaxSession) throws Exception { + boolean reportLoadChange = false; String clientKey = param.getClientMapKey(); synchronized (connectionMap) { @@ -512,6 +603,7 @@ public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param, viewer = getClient(param); viewer.initClient(param); + connectionMap.put(clientKey, viewer); LOGGER.info("Added viewer object " + viewer); reportLoadChange = true; @@ -524,19 +616,23 @@ public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param, } } + if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() || !param.getClientHostPassword().equals(viewer.getClientHostPassword())) { throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid"); } + if (!viewer.isFrontEndAlive()) { + authenticationExternally(param); viewer.initClient(param); reportLoadChange = true; } } + if (reportLoadChange) { ConsoleProxyClientStatsCollector statsCollector = getStatsCollector(); String loadInfo = statsCollector.getStatsReport(); @@ -549,6 +645,7 @@ public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param, } } + private static ConsoleProxyClient getClient(ConsoleProxyClientParam param) { if (param.getHypervHost() != null) { return new ConsoleProxyRdpClient(); @@ -557,6 +654,7 @@ private static ConsoleProxyClient getClient(ConsoleProxyClientParam param) { } } + public static void removeViewer(ConsoleProxyClient viewer) { synchronized (connectionMap) { for (Map.Entry entry : connectionMap.entrySet()) { @@ -569,40 +667,49 @@ public static void removeViewer(ConsoleProxyClient viewer) { } } + public static ConsoleProxyClientStatsCollector getStatsCollector() { synchronized (connectionMap) { return new ConsoleProxyClientStatsCollector(connectionMap); } } + public static void authenticationExternally(ConsoleProxyClientParam param) throws AuthenticationException { ConsoleProxyAuthenticationResult authResult = authenticateConsoleAccess(param, false); + if (authResult == null || !authResult.isSuccess()) { LOGGER.warn("External authenticator failed authentication request for vm " + param.getClientTag() + " with sid " + param.getClientHostPassword()); + throw new AuthenticationException("External authenticator failed request for vm " + param.getClientTag() + " with sid " + param.getClientHostPassword()); } } + public static ConsoleProxyAuthenticationResult reAuthenticationExternally(ConsoleProxyClientParam param) { return authenticateConsoleAccess(param, true); } + public static String getEncryptorPassword() { return encryptorPassword; } + public static void setEncryptorPassword(String password) { encryptorPassword = password; } + public static void setIsSourceIpCheckEnabled(Boolean isEnabled) { isSourceIpCheckEnabled = isEnabled; } + static class ThreadExecutor implements Executor { @Override public void execute(Runnable r) { @@ -610,45 +717,48 @@ public void execute(Runnable r) { } } + public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam param, String ajaxSession, Session session) throws AuthenticationException { boolean reportLoadChange = false; String clientKey = param.getClientMapKey(); LOGGER.debug("Getting NoVNC viewer for {}. Client tag: {}. session UUID: {}", - clientKey, param.getClientTag(), param.getSessionUuid()); -synchronized (connectionMap) { - ConsoleProxyClient viewer = connectionMap.get(clientKey); - if (viewer == null || viewer.getClass() != ConsoleProxyNoVncClient.class) { - authenticationExternally(param); - viewer = new ConsoleProxyNoVncClient(session); - viewer.initClient(param); - - connectionMap.put(clientKey, viewer); - reportLoadChange = true; - } else { - if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() - || !param.getClientHostPassword().equals(viewer.getClientHostPassword())) { - throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid"); - } + clientKey, param.getClientTag(), param.getSessionUuid()); + synchronized (connectionMap) { + ConsoleProxyClient viewer = connectionMap.get(clientKey); + if (viewer == null || viewer.getClass() != ConsoleProxyNoVncClient.class) { + authenticationExternally(param); + viewer = new ConsoleProxyNoVncClient(session); + viewer.initClient(param); - try { - authenticationExternally(param); - } catch (Exception e) { - LOGGER.error("Authentication failed for param: " + param); - return null; - } - LOGGER.info("Initializing new novnc client and disconnecting existing session"); - try { - ((ConsoleProxyNoVncClient) viewer).getSession().disconnect(); - } catch (IOException e) { - LOGGER.error("Exception while disconnect session of novnc viewer object: " + viewer, e); - } - removeViewer(viewer); - viewer = new ConsoleProxyNoVncClient(session); - viewer.initClient(param); - connectionMap.put(clientKey, viewer); - reportLoadChange = true; - } + + connectionMap.put(clientKey, viewer); + reportLoadChange = true; + } else { + if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() + || !param.getClientHostPassword().equals(viewer.getClientHostPassword())) { + throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid"); + } + + + try { + authenticationExternally(param); + } catch (Exception e) { + LOGGER.error("Authentication failed for param: {}", param, e); + return null; + } + LOGGER.info("Initializing new novnc client and disconnecting existing session"); + try { + ((ConsoleProxyNoVncClient) viewer).getSession().disconnect(); + } catch (IOException e) { + LOGGER.error("Exception while disconnect session of novnc viewer object: " + viewer, e); + } + removeViewer(viewer); + viewer = new ConsoleProxyNoVncClient(session); + viewer.initClient(param); + connectionMap.put(clientKey, viewer); + reportLoadChange = true; + } if (reportLoadChange) { diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java index 08fa63403b3a..a69f29706db9 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java @@ -16,15 +16,18 @@ // under the License. package com.cloud.consoleproxy; + import java.io.File; import java.util.ArrayList; import java.util.Iterator; import java.util.Map; import java.util.Set; + import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; + /** * * ConsoleProxyGCThread does house-keeping work for the process, it helps cleanup log files, @@ -34,28 +37,27 @@ public class ConsoleProxyGCThread extends Thread { private static final Logger logger = LogManager.getLogger(ConsoleProxyGCThread.class); - /** - * Maximum time (in seconds) a console session is allowed to be idle before it is closed. - * This value should be kept in sync with ConsoleProxy.VIEWER_LINGER_SECONDS. - */ - private static final int MAX_SESSION_IDLE_SECONDS = 180; private final Map connMap; private final Set removedSessionsSet; private long lastLogScan = 0; + public ConsoleProxyGCThread(Map connMap, Set removedSet) { this.connMap = connMap; this.removedSessionsSet = removedSet; } + private void cleanupLogging() { if (lastLogScan != 0 && System.currentTimeMillis() - lastLogScan < 3600000) { return; } + lastLogScan = System.currentTimeMillis(); + File logDir = new File("./logs"); File[] files = logDir.listFiles(); if (files != null) { @@ -71,16 +73,20 @@ private void cleanupLogging() { } } + @Override public void run() { + boolean bReportLoad = false; long lastReportTick = System.currentTimeMillis(); + while (true) { cleanupLogging(); bReportLoad = false; + if (logger.isDebugEnabled()) { logger.debug(String.format("ConsoleProxyGCThread loop: connMap=%s, removedSessions=%s", connMap, removedSessionsSet)); } @@ -90,30 +96,37 @@ public void run() { String key; ConsoleProxyClient client; + synchronized (connMap) { key = iterator.next(); client = connMap.get(key); } + if (client == null) { continue; } - long secondsUnused = (System.currentTimeMillis() - client.getClientLastFrontEndActivityTime()) / 1000; - if (secondsUnused < MAX_SESSION_IDLE_SECONDS) { + + long millisecondsUnused = System.currentTimeMillis() - client.getClientLastFrontEndActivityTime(); + if (millisecondsUnused < ConsoleProxy.sessionTimeoutMillis) { continue; } + synchronized (connMap) { connMap.remove(key); bReportLoad = true; } + // close the server connection - logger.info("Dropping " + client + " which has not been used for " + secondsUnused + " seconds"); + logger.info("Dropping " + client + " which has not been used for " + millisecondsUnused + + " ms (configured timeout: " + ConsoleProxy.sessionTimeoutMillis + " ms)"); client.closeClient(); } + if (bReportLoad || System.currentTimeMillis() - lastReportTick > 5000) { // report load changes, including removed sessions since last report ConsoleProxyClientStatsCollector collector = new ConsoleProxyClientStatsCollector(connMap); @@ -125,11 +138,13 @@ public void run() { removedSessionsSet.clear(); } + if (logger.isDebugEnabled()) { logger.debug("Report load change : " + loadInfo); } } + try { Thread.sleep(5000); } catch (InterruptedException ex) {