Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -283,8 +339,10 @@ angular.module('client').directive('guacClientNotification', [function guacClien
}

// Hide status for all other states
else
else {
cancelWolTimer();
$scope.status = false;
}

};

Expand Down Expand Up @@ -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;
Expand Down
18 changes: 17 additions & 1 deletion guacamole/src/main/frontend/src/app/client/styles/client.css
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,20 @@ body.client {

.client .drop-pending .display > *{
opacity: 0.5;
}
}
.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;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<div class="client-status-modal" ng-class="{ shown: status }">
<guac-modal>
<guac-notification notification="status"></guac-notification>
<div class="wol-countdown" ng-show="wolMachineWasOff && wolCountdownActive && wolCountdown > 0">
<p class="wol-status" translate="CLIENT.INFO_WOL_MACHINE_OFF"></p>
<p class="wol-timer" translate="CLIENT.INFO_WOL_CONNECTING" translate-values="{SECONDS: wolCountdown}"></p>
</div>
</guac-modal>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
/**
* A directive which displays the contents of a connection group within an
* automatically-paginated view.

*/
angular.module('groupList').directive('guacGroupList', [function guacGroupList() {

Expand All @@ -40,7 +41,7 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList()
* Arbitrary object which shall be made available to the connection
* and connection group templates within the scope as
* <code>context</code>.
*
*
* @type Object
*/
context : '=',
Expand All @@ -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 : '=',
Expand Down Expand Up @@ -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');

Expand All @@ -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.
*
Expand Down Expand Up @@ -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) {

Expand Down Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions guacamole/src/main/frontend/src/app/home/styles/home.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
ng-href="{{ item.getClientURL() }}"
ng-class="{active: item.getActiveConnections()}">

<!-- Connection icon -->
<div class="icon type" ng-class="item.protocol"></div>
<div class="icon type" ng-class="[item.protocol,
{'vm-running': reachabilityService.reachable[item.identifier] === true,
'vm-stopped': reachabilityService.reachable[item.identifier] === false}]"></div>

<!-- Connection name -->
<span class="name">{{item.name}}</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

}]);
2 changes: 2 additions & 0 deletions guacamole/src/main/frontend/src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading