diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js b/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js index 03344b627f..952e71b2e1 100644 --- a/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js +++ b/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js @@ -45,15 +45,19 @@ angular.module('client').directive('guacClientNotification', [function guacClien function guacClientNotificationController($scope, $injector, $element) { // Required types + const ClientIdentifier = $injector.get('ClientIdentifier'); const ManagedClient = $injector.get('ManagedClient'); const ManagedClientState = $injector.get('ManagedClientState'); const Protocol = $injector.get('Protocol'); // Required services + const $interval = $injector.get('$interval'); const $location = $injector.get('$location'); const authenticationService = $injector.get('authenticationService'); const guacClientManager = $injector.get('guacClientManager'); const guacTranslate = $injector.get('guacTranslate'); + const connectionReachabilityService = $injector.get('connectionReachabilityService'); + const connectionService = $injector.get('connectionService'); const requestService = $injector.get('requestService'); const userPageService = $injector.get('userPageService'); @@ -66,6 +70,36 @@ angular.module('client').directive('guacClientNotification', [function guacClien */ $scope.status = false; + $scope.wolCountdown = 0; + $scope.wolCountdownActive = false; + $scope.wolMachineWasOff = false; + var wolTimer = null; + + var cancelWolTimer = function cancelWolTimer() { + if (wolTimer) { + $interval.cancel(wolTimer); + wolTimer = null; + } + $scope.wolCountdownActive = false; + // NOTE: wolMachineWasOff is intentionally NOT reset here. + // cancelWolTimer() is called from inside startWolCountdown() before + // wolMachineWasOff is set to true, so resetting it here would + // immediately clear the flag. Visibility is already controlled by + // wolCountdownActive via ng-show. + }; + + var startWolCountdown = function startWolCountdown(waitSeconds) { + cancelWolTimer(); + $scope.wolCountdown = waitSeconds; + $scope.wolCountdownActive = true; + wolTimer = $interval(function() { + $scope.wolCountdown--; + if ($scope.wolCountdown <= 0) { + cancelWolTimer(); + } + }, 1000); + }; + /** * All error codes for which automatic reconnection is appropriate when a * client error occurs. @@ -211,6 +245,28 @@ angular.module('client').directive('guacClientNotification', [function guacClien key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase() } }; + + // Start WoL countdown only in the WAITING state + if (connectionState === ManagedClientState.ConnectionState.WAITING) { + try { + var clientId = ClientIdentifier.fromString($scope.client.id); + if (clientId.type === ClientIdentifier.Types.CONNECTION) { + // !== true covers both false (confirmed off) and undefined (not yet polled) + var wasOff = connectionReachabilityService.reachable[clientId.id] !== true; + connectionService.getConnectionParameters(clientId.dataSource, clientId.id) + ['then'](function(params) { + var waitTime = parseInt(params['wol-wait-time'], 10); + if (waitTime > 0 && wasOff) { + $scope.wolMachineWasOff = true; + startWolCountdown(waitTime); + } + }) + ['catch'](requestService.IGNORE); + } + } catch(e) { /* ignore if connection ID cannot be parsed */ } + } else { + cancelWolTimer(); + } } // Client error @@ -283,8 +339,10 @@ angular.module('client').directive('guacClientNotification', [function guacClien } // Hide status for all other states - else + else { + cancelWolTimer(); $scope.status = false; + } }; @@ -402,6 +460,9 @@ angular.module('client').directive('guacClientNotification', [function guacClien $scope.$on('guacBeforeKeydown', preventDefaultDuringNotification); $scope.$on('guacBeforeKeyup', preventDefaultDuringNotification); + // Cancel the WoL timer on scope destruction to prevent interval leaks + $scope.$on('$destroy', cancelWolTimer); + }]; return directive; diff --git a/guacamole/src/main/frontend/src/app/client/styles/client.css b/guacamole/src/main/frontend/src/app/client/styles/client.css index 4d452980b7..048455d3ee 100644 --- a/guacamole/src/main/frontend/src/app/client/styles/client.css +++ b/guacamole/src/main/frontend/src/app/client/styles/client.css @@ -134,4 +134,20 @@ body.client { .client .drop-pending .display > *{ opacity: 0.5; -} \ No newline at end of file +} +.wol-countdown { + text-align: center; + margin: 0.75em 0 0; + opacity: 0.9; +} +.wol-countdown .wol-status { + color: #ffd580; + font-size: 0.95em; + margin: 0 0 0.25em; + font-style: italic; +} +.wol-countdown .wol-timer { + color: #cce5ff; + font-size: 1.1em; + margin: 0; +} diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacClientNotification.html b/guacamole/src/main/frontend/src/app/client/templates/guacClientNotification.html index 9948a2b1c2..76160b26c5 100644 --- a/guacamole/src/main/frontend/src/app/client/templates/guacClientNotification.html +++ b/guacamole/src/main/frontend/src/app/client/templates/guacClientNotification.html @@ -1,5 +1,9 @@
context.
- *
+ *
* @type Object
*/
context : '=',
@@ -63,7 +64,7 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList()
* Whether the root of the connection group hierarchy given should
* be shown. If false (the default), only the descendants of the
* given connection group will be listed.
- *
+ *
* @type Boolean
*/
showRootGroup : '=',
@@ -96,6 +97,8 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList()
var dataSourceService = $injector.get('dataSourceService');
var requestService = $injector.get('requestService');
+ var connectionReachabilityService = $injector.get('connectionReachabilityService');
+
// Required types
var GroupListItem = $injector.get('GroupListItem');
@@ -119,6 +122,19 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList()
*/
$scope.rootItems = [];
+ /**
+ * Exposes the reachability service to the scope so that the
+ * connection.html template can read
+ * reachabilityService.reachable[item.identifier] and apply the
+ * vm-running / vm-stopped CSS classes.
+ *
+ * Child scopes created by ng-repeat and ng-include inherit this
+ * object through the AngularJS prototype chain.
+ *
+ * @type {Object}
+ */
+ $scope.reachabilityService = connectionReachabilityService;
+
/**
* Returns the number of active usages of a given connection.
*
@@ -154,6 +170,24 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList()
return !!$scope.templates[type];
};
+ /**
+ * Recursively collects connection identifiers from a raw connection
+ * group and all its descendants, as returned by the REST API.
+ *
+ * @param {Object} group A connection group object from the REST API.
+ * @returns {String[]} Array of connection identifiers.
+ */
+ var collectConnectionGroupIds = function collectConnectionGroupIds(group) {
+ var ids = [];
+ angular.forEach(group.childConnections, function connectionFound(connection) {
+ ids.push(connection.identifier);
+ });
+ angular.forEach(group.childConnectionGroups, function groupFound(childGroup) {
+ ids = ids.concat(collectConnectionGroupIds(childGroup));
+ });
+ return ids;
+ };
+
// Set contents whenever the connection group is assigned or changed
$scope.$watch('connectionGroups', function setContents(connectionGroups) {
@@ -230,11 +264,19 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList()
if ($scope.decorator)
$scope.decorator($scope.rootItems);
+ // Register reachability targets for each data source so that
+ // the polling service knows which connections to probe via TCP.
+ angular.forEach(connectionGroups, function targetsRegistered(rootGroup, dataSource) {
+ var ids = collectConnectionGroupIds(rootGroup);
+ if (ids.length)
+ connectionReachabilityService.setTargets(dataSource, ids);
+ });
+
});
/**
* Toggle the open/closed status of a group list item.
- *
+ *
* @param {GroupListItem} groupListItem
* The list item to expand, which should represent a
* connection group.
diff --git a/guacamole/src/main/frontend/src/app/home/styles/home.css b/guacamole/src/main/frontend/src/app/home/styles/home.css
index 8ae0486ed6..7dc81e430a 100644
--- a/guacamole/src/main/frontend/src/app/home/styles/home.css
+++ b/guacamole/src/main/frontend/src/app/home/styles/home.css
@@ -110,3 +110,13 @@ a.home-connection, .empty.balancer a.home-connection-group {
.recent-connections .connection .remove-recent:hover {
opacity: 1.0;
}
+
+/* Colorize icon when the connection host is reachable via TCP. */
+.all-connections .icon.vm-running {
+ filter: invert(52%) sepia(65%) saturate(450%) hue-rotate(100deg) brightness(88%);
+}
+
+/* Dim icon when the connection host is not reachable. */
+.all-connections .icon.vm-stopped {
+ opacity: 0.35;
+}
diff --git a/guacamole/src/main/frontend/src/app/home/templates/connection.html b/guacamole/src/main/frontend/src/app/home/templates/connection.html
index 85a7f573b5..775c0ff09b 100644
--- a/guacamole/src/main/frontend/src/app/home/templates/connection.html
+++ b/guacamole/src/main/frontend/src/app/home/templates/connection.html
@@ -2,8 +2,9 @@
ng-href="{{ item.getClientURL() }}"
ng-class="{active: item.getActiveConnections()}">
-
-
+
{{item.name}}
diff --git a/guacamole/src/main/frontend/src/app/rest/services/connectionReachabilityService.js b/guacamole/src/main/frontend/src/app/rest/services/connectionReachabilityService.js
new file mode 100644
index 0000000000..aebe57576e
--- /dev/null
+++ b/guacamole/src/main/frontend/src/app/rest/services/connectionReachabilityService.js
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Service that maintains the TCP reachability state of Guacamole connections
+ * and refreshes it every 10 seconds by polling the backend.
+ *
+ * Typical usage (from guacGroupList.js):
+ * connectionReachabilityService.setTargets('mysql', ['1', '2', '14', '15']);
+ *
+ * In connection.html template:
+ * ng-class="{'vm-running': reachabilityService.reachable[item.identifier] === true,
+ * 'vm-stopped': reachabilityService.reachable[item.identifier] === false}"
+ */
+angular.module('rest').factory('connectionReachabilityService', ['$injector',
+ function connectionReachabilityServiceFactory($injector) {
+
+ // Servicios necesarios obtenidos via $injector (patron estandar de este proyecto)
+ var authenticationService = $injector.get('authenticationService');
+ var $interval = $injector.get('$interval');
+
+ var service = {};
+
+ /**
+ * Map of connection ID (String) to Boolean reachability state.
+ * true = host responds on its TCP port (machine is running).
+ * false = host is unreachable (machine is off or port is closed).
+ * No entry = state not yet known (first poll pending).
+ *
+ * IMPORTANT: this object is NEVER replaced; it is updated in-place via
+ * {@code angular.extend()} so that AngularJS bindings in child scopes
+ * continue to reference the same object.
+ */
+ service.reachable = {};
+
+ /** Identifier of the current data source (e.g. "mysql"). */
+ var currentDataSource = null;
+
+ /** List of connection IDs to probe on each polling cycle. */
+ var currentIds = [];
+
+ /** Handle to the active $interval, used to cancel it when targets change. */
+ var pollHandle = null;
+
+ /**
+ * Sends a GET request to the /reachable endpoint and merges the result
+ * into service.reachable. If the request fails (network error, expired
+ * session, etc.), it is silently ignored and the last known state is kept.
+ */
+ var pollReachability = function pollReachability() {
+ if (!currentDataSource || !currentIds.length)
+ return;
+
+ authenticationService.request({
+ method : 'GET',
+ url : 'api/session/data/' + encodeURIComponent(currentDataSource)
+ + '/connections/reachable',
+ params : { ids : currentIds }
+ }).then(function reachabilityReceived(result) {
+ // result is the {id: bool} map returned by the Java endpoint.
+ // angular.extend updates service.reachable without replacing the reference,
+ // preserving bindings in child scopes.
+ angular.extend(service.reachable, result);
+ });
+ // Errors are not propagated: if the backend fails transiently,
+ // the last known state is displayed until the next poll.
+ };
+
+ /**
+ * Registers the list of connections to monitor and (re)starts the polling loop.
+ * Must be called whenever the connection list changes.
+ *
+ * @param {String} dataSource Data source identifier (e.g. "mysql").
+ * @param {String[]} ids Array of Guacamole connection IDs to probe.
+ */
+ service.setTargets = function setTargets(dataSource, ids) {
+ currentDataSource = dataSource;
+ currentIds = ids;
+
+ // Cancel the previous polling interval if one is active
+ if (pollHandle) {
+ $interval.cancel(pollHandle);
+ pollHandle = null;
+ }
+
+ // Probe immediately, then again every 10 seconds
+ pollReachability();
+ pollHandle = $interval(pollReachability, 10000);
+ };
+
+ return service;
+
+}]);
diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json
index 05c52fa5f4..8a114e124b 100644
--- a/guacamole/src/main/frontend/src/translations/en.json
+++ b/guacamole/src/main/frontend/src/translations/en.json
@@ -168,6 +168,8 @@
"TEXT_USER_JOINED" : "{USERNAME} has joined the connection.",
"TEXT_USER_LEFT" : "{USERNAME} has left the connection.",
"TEXT_RECONNECT_COUNTDOWN" : "Reconnecting in {REMAINING} {REMAINING, plural, one{second} other{seconds}}...",
+ "INFO_WOL_MACHINE_OFF" : "Machine is powered off — sending Wake-on-LAN signal",
+ "INFO_WOL_CONNECTING" : "Automatically connecting in approximately {SECONDS} {SECONDS, plural, one{second} other{seconds}}…",
"TEXT_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}",
"URL_OSK_LAYOUT" : "layouts/en-us-qwerty.json"
diff --git a/guacamole/src/main/java/org/apache/guacamole/net/ReachabilityProbe.java b/guacamole/src/main/java/org/apache/guacamole/net/ReachabilityProbe.java
new file mode 100644
index 0000000000..0c2727776e
--- /dev/null
+++ b/guacamole/src/main/java/org/apache/guacamole/net/ReachabilityProbe.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.net;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Comprueba en paralelo si un conjunto de hosts:puerto responden via TCP.
+ *
+ * Cada prueba abre un socket TCP con un timeout de 1 segundo. Si la conexion
+ * se establece, el host se considera alcanzable (reachable = true). Si falla
+ * o expira el timeout, se considera inalcanzable (reachable = false).
+ *
+ * El probe NO verifica que el servicio Guacamole funcione correctamente, solo
+ * que el puerto este abierto. Esto es suficiente para saber si la VM esta
+ * encendida, ya que Windows y Linux dejan el puerto RDP/SSH abierto cuando
+ * estan activos (a diferencia de ICMP que suele estar bloqueado en Windows).
+ */
+public class ReachabilityProbe {
+
+ /** Numero maximo de sondas TCP en paralelo. */
+ private static final int THREAD_POOL_SIZE = 16;
+
+ /**
+ * Timeout en milisegundos para cada intento de conexion TCP.
+ * 1 segundo es suficiente para LAN local; evita bloquear la UI si una VM
+ * tarda en responder o esta apagada.
+ */
+ private static final int CONNECT_TIMEOUT_MS = 1000;
+
+ /**
+ * Representa un destino a sondear: el ID de la conexion Guacamole,
+ * el hostname o IP del host, y el puerto TCP.
+ */
+ public static class Target {
+
+ /** Identificador de la conexion Guacamole (clave en el resultado del probe). */
+ public final String id;
+
+ /** Hostname o IP del host a sondear. */
+ public final String host;
+
+ /** Puerto TCP a sondear (1-65535). */
+ public final int port;
+
+ /**
+ * Crea un nuevo Target.
+ *
+ * @param id Identificador de la conexion Guacamole.
+ * @param host Hostname o IP del host destino.
+ * @param port Puerto TCP a sondear (1-65535).
+ */
+ public Target(String id, String host, int port) {
+ this.id = id;
+ this.host = host;
+ this.port = port;
+ }
+ }
+
+ /**
+ * Sondea en paralelo todos los targets de la lista via TCP.
+ *
+ * Usa un ExecutorService con pool fijo de hasta THREAD_POOL_SIZE hilos.
+ * Cada hilo intenta abrir un socket TCP al host:port del target con un
+ * timeout de CONNECT_TIMEOUT_MS ms. El resultado se recoge con un timeout
+ * de CONNECT_TIMEOUT_MS + 500 ms para dar margen a la Future.
+ *
+ * @param targets Lista de targets a sondear. Puede ser nula o vacia.
+ * @return Mapa de connectionId (String) a boolean (true = alcanzable via TCP).
+ */
+ public static Map