diff --git a/src/protocols/rdp/Makefile.am b/src/protocols/rdp/Makefile.am index e8ddfeb4..2612bdff 100644 --- a/src/protocols/rdp/Makefile.am +++ b/src/protocols/rdp/Makefile.am @@ -56,6 +56,7 @@ libguac_client_rdp_la_SOURCES = \ channels/rdpdr/rdpdr-messages.c \ channels/rdpdr/rdpdr-printer.c \ channels/rdpdr/rdpdr.c \ + channels/rdpei.c \ channels/rdpsnd/rdpsnd-messages.c \ channels/rdpsnd/rdpsnd.c \ client.c \ @@ -101,6 +102,7 @@ noinst_HEADERS = \ channels/rdpdr/rdpdr-messages.h \ channels/rdpdr/rdpdr-printer.h \ channels/rdpdr/rdpdr.h \ + channels/rdpei.h \ channels/rdpsnd/rdpsnd-messages.h \ channels/rdpsnd/rdpsnd.h \ client.h \ diff --git a/src/protocols/rdp/channels/rdpei.c b/src/protocols/rdp/channels/rdpei.c new file mode 100644 index 00000000..f1c5d09e --- /dev/null +++ b/src/protocols/rdp/channels/rdpei.c @@ -0,0 +1,170 @@ +/* + * 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. + */ + +#include "channels/rdpei.h" +#include "common/surface.h" +#include "plugins/channels.h" +#include "rdp.h" +#include "settings.h" + +#include +#include +#include +#include +#include + +#include +#include + +guac_rdp_rdpei* guac_rdp_rdpei_alloc() { + + guac_rdp_rdpei* rdpei = malloc(sizeof(guac_rdp_rdpei)); + + /* Not yet connected */ + rdpei->rdpei = NULL; + + /* No active touches */ + for (int i = 0; i < GUAC_RDP_RDPEI_MAX_TOUCHES; i++) + rdpei->touch[i].active = 0; + + return rdpei; + +} + +void guac_rdp_rdpei_free(guac_rdp_rdpei* rdpei) { + free(rdpei); +} + +/** + * Callback which associates handlers specific to Guacamole with the + * RdpeiClientContext instance allocated by FreeRDP to deal with received + * RDPEI (multi-touch input) messages. + * + * This function is called whenever a channel connects via the PubSub event + * system within FreeRDP, but only has any effect if the connected channel is + * the RDPEI channel. This specific callback is registered with the + * PubSub system of the relevant rdpContext when guac_rdp_rdpei_load_plugin() is + * called. + * + * @param context + * The rdpContext associated with the active RDP session. + * + * @param e + * Event-specific arguments, mainly the name of the channel, and a + * reference to the associated plugin loaded for that channel by FreeRDP. + */ +static void guac_rdp_rdpei_channel_connected(rdpContext* context, + ChannelConnectedEventArgs* e) { + + guac_client* client = ((rdp_freerdp_context*) context)->client; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + guac_rdp_rdpei* guac_rdpei = rdp_client->rdpei; + + /* Ignore connection event if it's not for the RDPEI channel */ + if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) != 0) + return; + + /* Store reference to the RDPEI plugin once it's connected */ + RdpeiClientContext* rdpei = (RdpeiClientContext*) e->pInterface; + guac_rdpei->rdpei = rdpei; + + /* Declare level of multi-touch support */ + guac_common_surface_set_multitouch(rdp_client->display->default_surface, + GUAC_RDP_RDPEI_MAX_TOUCHES); + + guac_client_log(client, GUAC_LOG_DEBUG, "RDPEI channel will be used for " + "multi-touch support."); + +} + +void guac_rdp_rdpei_load_plugin(rdpContext* context) { + + /* Subscribe to and handle channel connected events */ + PubSub_SubscribeChannelConnected(context->pubSub, + (pChannelConnectedEventHandler) guac_rdp_rdpei_channel_connected); + + /* Add "rdpei" channel */ + guac_freerdp_dynamic_channel_collection_add(context->settings, "rdpei", NULL); + +} + +int guac_rdp_rdpei_touch_update(guac_rdp_rdpei* rdpei, int id, int x, int y, + double force) { + + int contact_id; /* Ignored */ + + /* Track touches only if channel is connected */ + RdpeiClientContext* context = rdpei->rdpei; + if (context == NULL) + return 1; + + /* Locate active touch having provided ID */ + guac_rdp_rdpei_touch* touch = NULL; + for (int i = 0; i < GUAC_RDP_RDPEI_MAX_TOUCHES; i++) { + if (rdpei->touch[i].active && rdpei->touch[i].id == id) { + touch = &rdpei->touch[i]; + break; + } + } + + /* If no such touch exists, add it */ + if (touch == NULL) { + for (int i = 0; i < GUAC_RDP_RDPEI_MAX_TOUCHES; i++) { + if (!rdpei->touch[i].active) { + touch = &rdpei->touch[i]; + touch->id = id; + break; + } + } + } + + /* If the touch couldn't be added, we're already at maximum touch capacity. + * Drop the event. */ + if (touch == NULL) + return 1; + + /* Signal the end of an established touch if touch force has become zero + * (this should be a safe comparison, as zero has an exact representation + * in floating point, and the client side will use an exact value to + * represent the absence of a touch) */ + if (force == 0.0) { + + /* Ignore release of touches that we aren't tracking */ + if (!touch->active) + return 1; + + context->TouchEnd(context, id, x, y, &contact_id); + touch->active = 0; + + } + + /* Signal the start of a touch if this is the first we've seen it */ + else if (!touch->active) { + context->TouchBegin(context, id, x, y, &contact_id); + touch->active = 1; + } + + /* Established touches need only be updated */ + else + context->TouchUpdate(context, id, x, y, &contact_id); + + return 0; + +} + diff --git a/src/protocols/rdp/channels/rdpei.h b/src/protocols/rdp/channels/rdpei.h new file mode 100644 index 00000000..5ca10c30 --- /dev/null +++ b/src/protocols/rdp/channels/rdpei.h @@ -0,0 +1,154 @@ +/* + * 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. + */ + +#ifndef GUAC_RDP_CHANNELS_RDPEI_H +#define GUAC_RDP_CHANNELS_RDPEI_H + +#include "settings.h" + +#include +#include +#include +#include + +/** + * The maximum number of simultaneously-tracked touches. + */ +#define GUAC_RDP_RDPEI_MAX_TOUCHES 10 + +/** + * A single, tracked touch contact. + */ +typedef struct guac_rdp_rdpei_touch { + + /** + * Whether this touch is active (1) or inactive (0). An active touch is + * being tracked, while an inactive touch is simple an empty space awaiting + * use by some future touch event. + */ + int active; + + /** + * The unique ID representing this touch contact. + */ + int id; + + /** + * The X-coordinate of this touch, in pixels. + */ + int x; + + /** + * The Y-coordinate of this touch, in pixels. + */ + int y; + +} guac_rdp_rdpei_touch; + +/** + * Multi-touch input module. + */ +typedef struct guac_rdp_rdpei { + + /** + * RDPEI control interface. + */ + RdpeiClientContext* rdpei; + + /** + * All currently-tracked touches. + */ + guac_rdp_rdpei_touch touch[GUAC_RDP_RDPEI_MAX_TOUCHES]; + +} guac_rdp_rdpei; + +/** + * Allocates a new RDPEI module, which will ultimately control the RDPEI + * channel once connected. The RDPEI channel allows multi-touch input + * events to be sent to the RDP server. + * + * @return + * A newly-allocated RDPEI module. + */ +guac_rdp_rdpei* guac_rdp_rdpei_alloc(); + +/** + * Frees the resources associated with support for the RDPEI channel. Only + * resources specific to Guacamole are freed. Resources specific to FreeRDP's + * handling of the RDPEI channel will be freed by FreeRDP. If no resources are + * currently allocated for RDPEI, this function has no effect. + * + * @param rdpei + * The RDPEI module to free. + */ +void guac_rdp_rdpei_free(guac_rdp_rdpei* rdpei); + +/** + * Adds FreeRDP's "rdpei" plugin to the list of dynamic virtual channel plugins + * to be loaded by FreeRDP's "drdynvc" plugin. The context of the plugin will + * automatically be assicated with the guac_rdp_rdpei instance pointed to by the + * current guac_rdp_client. The plugin will only be loaded once the "drdynvc" + * plugin is loaded. The "rdpei" plugin ultimately adds support for multi-touch + * input via the RDPEI channel. + * + * If failures occur, messages noting the specifics of those failures will be + * logged, and the RDP side of multi-touch support will not be functional. + * + * This MUST be called within the PreConnect callback of the freerdp instance + * for multi-touch support to be loaded. + * + * @param context + * The rdpContext associated with the active RDP session. + */ +void guac_rdp_rdpei_load_plugin(rdpContext* context); + +/** + * Reports to the RDP server that the status of a single touch contact has + * changed. Depending on the amount of force associated with the touch and + * whether the touch has been encountered before, this will result a new touch + * contact, updates to an existing contact, or removal of an existing contact. + * If the RDPEI channel has not yet been connected, touches will be ignored and + * dropped until it is connected. + * + * @param rdpei + * The RDPEI module associated with the RDP session. + * + * @param id + * An arbitrary integer ID unique to the touch being updated. + * + * @param x + * The X-coordinate of the touch, in pixels. + * + * @param y + * The Y-coordinate of the touch, in pixels. + * + * @param force + * The amount of force currently being exerted on the device by the touch + * contact in question, where 1.0 is the maximum amount of force + * representable and 0.0 indicates the contact has been lifted. + * + * @return + * Zero if the touch event was successfully processed, non-zero if the + * touch event had to be dropped. + */ +int guac_rdp_rdpei_touch_update(guac_rdp_rdpei* rdpei, int id, int x, int y, + double force); + +#endif + diff --git a/src/protocols/rdp/client.c b/src/protocols/rdp/client.c index 07c90a33..50808dcf 100644 --- a/src/protocols/rdp/client.c +++ b/src/protocols/rdp/client.c @@ -147,6 +147,9 @@ int guac_client_init(guac_client* client, int argc, char** argv) { /* Init display update module */ rdp_client->disp = guac_rdp_disp_alloc(); + /* Init multi-touch support module (RDPEI) */ + rdp_client->rdpei = guac_rdp_rdpei_alloc(); + /* Redirect FreeRDP log messages to guac_client_log() */ guac_rdp_redirect_wlog(client); @@ -187,6 +190,9 @@ int guac_rdp_client_free_handler(guac_client* client) { /* Free display update module */ guac_rdp_disp_free(rdp_client->disp); + /* Free multi-touch support module (RDPEI) */ + guac_rdp_rdpei_free(rdp_client->rdpei); + /* Clean up filesystem, if allocated */ if (rdp_client->filesystem != NULL) guac_rdp_fs_free(rdp_client->filesystem); diff --git a/src/protocols/rdp/input.c b/src/protocols/rdp/input.c index cb9bb10e..7b31cb33 100644 --- a/src/protocols/rdp/input.c +++ b/src/protocols/rdp/input.c @@ -18,6 +18,7 @@ */ #include "channels/disp.h" +#include "channels/rdpei.h" #include "common/cursor.h" #include "common/display.h" #include "common/recording.h" @@ -122,6 +123,28 @@ complete: return 0; } +int guac_rdp_user_touch_handler(guac_user* user, int id, int x, int y, + int x_radius, int y_radius, double angle, double force) { + + guac_client* client = user->client; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + + pthread_rwlock_rdlock(&(rdp_client->lock)); + + /* Skip if not yet connected */ + freerdp* rdp_inst = rdp_client->rdp_inst; + if (rdp_inst == NULL) + goto complete; + + /* Forward touch event along RDPEI channel */ + guac_rdp_rdpei_touch_update(rdp_client->rdpei, id, x, y, force); + +complete: + pthread_rwlock_unlock(&(rdp_client->lock)); + + return 0; +} + int guac_rdp_user_key_handler(guac_user* user, int keysym, int pressed) { guac_client* client = user->client; diff --git a/src/protocols/rdp/input.h b/src/protocols/rdp/input.h index 60ef0641..eb9e4825 100644 --- a/src/protocols/rdp/input.h +++ b/src/protocols/rdp/input.h @@ -27,6 +27,11 @@ */ guac_user_mouse_handler guac_rdp_user_mouse_handler; +/** + * Handler for Guacamole user touch events. + */ +guac_user_touch_handler guac_rdp_user_touch_handler; + /** * Handler for Guacamole user key events. */ diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index ce6a2796..f0e38f99 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -27,6 +27,7 @@ #include "channels/pipe-svc.h" #include "channels/rail.h" #include "channels/rdpdr/rdpdr.h" +#include "channels/rdpei.h" #include "channels/rdpsnd/rdpsnd.h" #include "client.h" #include "color.h" @@ -100,6 +101,10 @@ BOOL rdp_freerdp_pre_connect(freerdp* instance) { if (settings->resize_method == GUAC_RESIZE_DISPLAY_UPDATE) guac_rdp_disp_load_plugin(context); + /* Load "rdpei" plugin for multi-touch support */ + if (settings->enable_touch) + guac_rdp_rdpei_load_plugin(context); + /* Load "AUDIO_INPUT" plugin for audio input*/ if (settings->enable_audio_input) { rdp_client->audio_input = guac_rdp_audio_buffer_alloc(); diff --git a/src/protocols/rdp/rdp.h b/src/protocols/rdp/rdp.h index e65f702c..cc537966 100644 --- a/src/protocols/rdp/rdp.h +++ b/src/protocols/rdp/rdp.h @@ -23,6 +23,7 @@ #include "channels/audio-input/audio-buffer.h" #include "channels/cliprdr.h" #include "channels/disp.h" +#include "channels/rdpei.h" #include "common/clipboard.h" #include "common/display.h" #include "common/list.h" @@ -148,6 +149,11 @@ typedef struct guac_rdp_client { */ guac_rdp_disp* disp; + /** + * Multi-touch support module (RDPEI). + */ + guac_rdp_rdpei* rdpei; + /** * List of all available static virtual channels. */ diff --git a/src/protocols/rdp/settings.c b/src/protocols/rdp/settings.c index dd8a96fb..a5d7aebf 100644 --- a/src/protocols/rdp/settings.c +++ b/src/protocols/rdp/settings.c @@ -108,6 +108,7 @@ const char* GUAC_RDP_CLIENT_ARGS[] = { "create-recording-path", "resize-method", "enable-audio-input", + "enable-touch", "read-only", "gateway-hostname", @@ -527,6 +528,12 @@ enum RDP_ARGS_IDX { */ IDX_ENABLE_AUDIO_INPUT, + /** + * "true" if multi-touch support should be enabled for the RDP connection, + * "false" or blank otherwise. + */ + IDX_ENABLE_TOUCH, + /** * "true" if this connection should be read-only (user input should be * dropped), "false" or blank otherwise. @@ -1070,6 +1077,11 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, settings->resize_method = GUAC_RESIZE_NONE; } + /* Multi-touch input enable/disable */ + settings->enable_touch = + guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_ENABLE_TOUCH, 0); + /* Audio input enable/disable */ settings->enable_audio_input = guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, diff --git a/src/protocols/rdp/settings.h b/src/protocols/rdp/settings.h index 323ba5d3..1a6f75c1 100644 --- a/src/protocols/rdp/settings.h +++ b/src/protocols/rdp/settings.h @@ -525,6 +525,11 @@ typedef struct guac_rdp_settings { */ int enable_audio_input; + /** + * Whether multi-touch support is enabled. + */ + int enable_touch; + /** * The hostname of the remote desktop gateway that should be used as an * intermediary for the remote desktop connection. If no gateway should diff --git a/src/protocols/rdp/user.c b/src/protocols/rdp/user.c index 351c67e6..992e551b 100644 --- a/src/protocols/rdp/user.c +++ b/src/protocols/rdp/user.c @@ -105,6 +105,10 @@ int guac_rdp_user_join_handler(guac_user* user, int argc, char** argv) { user->mouse_handler = guac_rdp_user_mouse_handler; user->key_handler = guac_rdp_user_key_handler; + /* Multi-touch events */ + if (settings->enable_touch) + user->touch_handler = guac_rdp_user_touch_handler; + /* Inbound (client to server) clipboard transfer */ if (!settings->disable_paste) user->clipboard_handler = guac_rdp_clipboard_handler;