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
120 changes: 120 additions & 0 deletions guacamole-common-js/src/main/webapp/modules/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,84 @@ Guacamole.Client = function(tunnel) {

};

/**
* Notifies the server that a USB device has been connected and is
* available for use.
*
* @param {!string} deviceId
* The unique identifier for the USB device.
*
* @param {!number} vendorId
* The vendor ID of the USB device.
*
* @param {!number} productId
* The product ID of the USB device.
*
* @param {!string} deviceName
* The human-readable name of the device.
*
* @param {!string} serialNumber
* The serial number of the device.
*
* @param {!number} deviceClass
* The USB device class.
*
* @param {!number} deviceSubclass
* The USB device subclass.
*
* @param {!number} deviceProtocol
* The USB device protocol.
*
* @param {!string} interfaceData
* Encoded string containing interface and endpoint information.
*/
this.sendUSBConnect = function(deviceId, vendorId, productId, deviceName,
serialNumber, deviceClass, deviceSubclass, deviceProtocol,
interfaceData) {
if (!isConnected())
return;

tunnel.sendMessage("usbconnect", deviceId, vendorId, productId,
deviceName, serialNumber, deviceClass, deviceSubclass,
deviceProtocol, interfaceData);
};

/**
* Sends USB data from a Web USB connected device to the server.
*
* @param {!string} deviceId
* The unique identifier for the USB device.
*
* @param {!number} endpoint
* The endpoint number this data originated from on the USB device.
*
* @param {!string} data
* The base64-encoded data received from the USB device.
*
* @param {!string} type
* The transfer type (bulk, interrupt, isochronous, control).
*/
this.sendUSBData = function(deviceId, endpoint, data, type) {
if (!isConnected())
return;

tunnel.sendMessage("usbdata", deviceId, endpoint, data, type);
};

/**
* Notifies the server that a USB device has been disconnected and is
* no longer available.
*
* @param {!string} deviceId
* The unique identifier for the USB device that was disconnected.
*/
this.sendUSBDisconnect = function(deviceId) {
if (!isConnected())
return;

tunnel.sendMessage("usbdisconnect", deviceId);
};

/**
* Fired whenever the state of this Guacamole.Client changes.
*
Expand Down Expand Up @@ -930,6 +1008,30 @@ Guacamole.Client = function(tunnel) {
*/
this.onsync = null;

/**
* Fired when the server requests USB device disconnection.
*
* @event
* @param {!string} deviceId
* The unique identifier of the USB device to disconnect.
*/
this.onusbdisconnect = null;

/**
* Fired when the server sends USB data to a specific device endpoint.
*
* @event
* @param {!string} deviceId
* The unique identifier of the USB device.
*
* @param {!number} endpointNumber
* The endpoint number the data is intended for.
*
* @param {!string} data
* The base64-encoded USB data.
*/
this.onusbdata = null;

/**
* Returns the layer with the given index, creating it if necessary.
* Positive indices refer to visible layers, an index of zero refers to
Expand Down Expand Up @@ -1735,6 +1837,24 @@ Guacamole.Client = function(tunnel) {

},

"usbdata": function(parameters) {
var deviceId = parameters[0];
var endpointNumber = parseInt(parameters[1]);
var data = parameters[2];

// Forward to the right USB device with endpoint information
if (guac_client.onusbdata)
guac_client.onusbdata(deviceId, endpointNumber, data);
},

"usbdisconnect": function(parameters) {
var deviceId = parameters[0];

// Notify client of USB disconnection
if (guac_client.onusbdisconnect)
guac_client.onusbdisconnect(deviceId);
},

"video": function(parameters) {

var stream_index = parseInt(parameters[0]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,11 @@
{
"name" : "static-channels",
"type" : "TEXT"
},
{
"name" : "enable-usb",
"type" : "BOOLEAN",
"options" : [ "true" ]
}
]
},
Expand Down
3 changes: 2 additions & 1 deletion guacamole/src/main/frontend/src/app/client/clientModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ angular.module('client', [
'osk',
'rest',
'textInput',
'touch'
'touch',
"usb"
]);
16 changes: 13 additions & 3 deletions guacamole/src/main/frontend/src/app/client/templates/client.html
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,21 @@ <h3>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h3>
</div>

<!-- Devices -->
<div class="menu-section" id="devices" ng-if="focusedClient.filesystems.length">
<div class="menu-section" id="devices" ng-if="focusedClient">
<h3>{{'CLIENT.SECTION_HEADER_DEVICES' | translate}}</h3>
<div class="content">
<div class="device filesystem" ng-repeat="filesystem in focusedClient.filesystems" ng-click="showFilesystemMenu(filesystem)">
{{filesystem.name}}
<!-- Filesystems -->
<div ng-if="focusedClient.filesystems.length">
<h4>Filesystems</h4>
<div class="device filesystem" ng-repeat="filesystem in focusedClient.filesystems" ng-click="showFilesystemMenu(filesystem)">
{{filesystem.name}}
</div>
<br>
</div>

<!-- USB Devices -->
<div>
<guac-usb client="focusedClient"></guac-usb>
</div>
</div>
</div>
Expand Down
105 changes: 105 additions & 0 deletions guacamole/src/main/frontend/src/app/client/types/ManagedClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
const ManagedFilesystem = $injector.get('ManagedFilesystem');
const ManagedFileUpload = $injector.get('ManagedFileUpload');
const ManagedShareLink = $injector.get('ManagedShareLink');
const ManagedUSB = $injector.get('ManagedUSB');

// Required services
const $document = $injector.get('$document');
Expand Down Expand Up @@ -293,6 +294,13 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
*/
this.deferredPipeStreamHandlers = template.deferredPipeStreamHandlers || {};

/**
* All currently-connected USB devices for this client.
*
* @type ManagedUSB[]
*/
this.usbDevices = template.usbDevices || [];

};

/**
Expand Down Expand Up @@ -726,6 +734,40 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
});
});
};

// Handle USB disconnection notifications from server
client.onusbdisconnect = function usbDisconnectNotify(deviceId) {
$rootScope.$apply(function handleUSBDisconnect() {

// Find the corresponding ManagedUSB device
const managedUSB = managedClient.usbDevices.find(usb =>
(usb.id || usb.device.serialNumber) === deviceId);

if (managedUSB) {
console.log("Server requested USB device disconnection:", deviceId);

// Disconnect the device locally
ManagedClient.disconnectUSBDevice(managedClient, managedUSB)
.catch(error => {
console.error("Error disconnecting USB device:", error);
});
}
});
};

// Handle incoming data to a specific USB device endpoint
client.onusbdata = function usbDataReceived(deviceId, endpointNumber, data) {
// Find the corresponding ManagedUSB device
const managedUSB = managedClient.usbDevices.find(usb =>
(usb.id || usb.device.serialNumber) === deviceId);

if (managedUSB)
// Forward data with endpoint information to the ManagedUSB instance
managedUSB.handleRemoteData(data, endpointNumber);

else
console.warn("Received USB data for unknown device:", deviceId);
};

// Manage the client display
managedClient.managedDisplay = ManagedDisplay.getInstance(client.getDisplay());
Expand Down Expand Up @@ -1142,6 +1184,69 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
delete managedClient.deferredPipeStreamHandlers[name];
};

/**
* Connects a WebUSB device to this client, creating a ManagedUSB
* instance to handle the device connection.
*
* @param {ManagedClient} managedClient
* The client that should use the USB device.
*
* @param {USBDevice} device
* The WebUSB device to connect.
*
* @returns {Promise.<ManagedUSB>}
* A promise that resolves with the created ManagedUSB instance when
* the device is connected, or rejects if the connection fails.
*/
ManagedClient.connectUSBDevice = function connectUSBDevice(managedClient, device) {

// Create a ManagedUSB instance to handle this device
var managedUSB = ManagedUSB.getInstance(managedClient, device);

// Add to the client's collection of USB devices
managedClient.usbDevices.push(managedUSB);

// Connect the device
return managedUSB.connect()
.catch(function connectionFailed(error) {
// Remove from collection if connection fails
var index = managedClient.usbDevices.indexOf(managedUSB);
if (index !== -1)
managedClient.usbDevices.splice(index, 1);

throw error;
});

};

/**
* Disconnects a USB device from this client.
*
* @param {ManagedClient} managedClient
* The client the device is connected to.
*
* @param {ManagedUSB} managedUSB
* The ManagedUSB device to disconnect.
*
* @returns {Promise}
* A promise that resolves when the device is disconnected.
*/
ManagedClient.disconnectUSBDevice = function disconnectUSBDevice(
managedClient, managedUSB) {

// Don't proceed if the device isn't in this client's collection
var index = managedClient.usbDevices.indexOf(managedUSB);
if (index === -1)
return $q.resolve();

// Remove from collection
managedClient.usbDevices.splice(index, 1);

// Disconnect the device
return managedUSB.disconnect();

};

return ManagedClient;

}]);
Loading