diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml new file mode 100644 index 000000000..1aa64079e --- /dev/null +++ b/.github/workflows/pr-build.yml @@ -0,0 +1,29 @@ +name: Pull request CI build + +# Run build for all pull requests +on: + pull_request: + +# Limit to only one build for a given PR source branch at a time, +# cancelling any in-progress builds +concurrency: + group: guacamole-server-pr-${{ github.head_ref }} + cancel-in-progress: true + +jobs: + + docker_build: + name: Run docker build + runs-on: ubuntu-latest + steps: + + - name: Check out code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Build Docker container + shell: sh + run: | + docker build --pull --no-cache --force-rm . diff --git a/.gitignore b/.gitignore index e72000d8e..d8b26f01f 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,7 @@ doc/*/doxygen-output # IDE metadata nbproject/ + +# Compilation database, as may be generated by tools like Bear +.cache/ +compile_commands.json diff --git a/Dockerfile b/Dockerfile index 0206975db..0288dcb50 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,18 +22,26 @@ # # The Alpine Linux image that should be used as the basis for the guacd image -ARG ALPINE_BASE_IMAGE=latest +# NOTE: Using 3.18 because the required openssl1.1-compat-dev package was +# removed in more recent versions. +ARG ALPINE_BASE_IMAGE=3.18 FROM alpine:${ALPINE_BASE_IMAGE} AS builder +# FreeRDP version (default to version 3) +ARG FREERDP_VERSION=3 + # Install build dependencies RUN apk add --no-cache \ autoconf \ automake \ build-base \ cairo-dev \ + cjson-dev \ cmake \ + cunit-dev \ git \ grep \ + krb5-dev \ libjpeg-turbo-dev \ libpng-dev \ libtool \ @@ -42,7 +50,10 @@ RUN apk add --no-cache \ openssl1.1-compat-dev \ pango-dev \ pulseaudio-dev \ - util-linux-dev + sdl2-dev \ + sdl2_ttf-dev \ + util-linux-dev \ + webkit2gtk-dev # Copy source to container for sake of build ARG BUILD_DIR=/tmp/guacamole-server @@ -61,7 +72,7 @@ ARG PREFIX_DIR=/opt/guacamole # library (these can be overridden at build time if a specific version is # needed) # -ARG WITH_FREERDP='2(\.\d+)+' +ARG WITH_FREERDP="${FREERDP_VERSION}(\.\d+)+" ARG WITH_LIBSSH2='libssh2-\d+(\.\d+)+' ARG WITH_LIBTELNET='\d+(\.\d+)+' ARG WITH_LIBVNCCLIENT='LibVNCServer-\d+(\.\d+)+' @@ -83,22 +94,26 @@ ARG FREERDP_OPTS="\ -DWITH_CUPS=OFF \ -DWITH_DIRECTFB=OFF \ -DWITH_FFMPEG=OFF \ + -DWITH_FUSE=OFF \ -DWITH_GSM=OFF \ -DWITH_GSSAPI=OFF \ -DWITH_IPP=OFF \ -DWITH_JPEG=ON \ + -DWITH_KRB5=ON \ -DWITH_LIBSYSTEMD=OFF \ -DWITH_MANPAGES=OFF \ -DWITH_OPENH264=OFF \ -DWITH_OPENSSL=ON \ -DWITH_OSS=OFF \ -DWITH_PCSC=OFF \ + -DWITH_PKCS11=OFF \ -DWITH_PULSE=OFF \ -DWITH_SERVER=OFF \ -DWITH_SERVER_INTERFACE=OFF \ -DWITH_SHADOW_MAC=OFF \ -DWITH_SHADOW_X11=OFF \ -DWITH_SSE2=ON \ + -DWITH_SWSCALE=OFF \ -DWITH_WAYLAND=OFF \ -DWITH_X11=OFF \ -DWITH_X264=OFF \ @@ -138,11 +153,14 @@ ARG LIBWEBSOCKETS_OPTS="\ # Build guacamole-server and its core protocol library dependencies RUN ${BUILD_DIR}/src/guacd-docker/bin/build-all.sh +# Determine location of the FREERDP library based on the version. +ARG FREERDP_LIB_PATH=${PREFIX_DIR}/lib/freerdp${FREERDP_VERSION} + # Record the packages of all runtime library dependencies RUN ${BUILD_DIR}/src/guacd-docker/bin/list-dependencies.sh \ ${PREFIX_DIR}/sbin/guacd \ ${PREFIX_DIR}/lib/libguac-client-*.so \ - ${PREFIX_DIR}/lib/freerdp2/*guac*.so \ + ${FREERDP_LIB_PATH}/*guac*.so \ > ${PREFIX_DIR}/DEPENDENCIES # Use same Alpine version as the base for the runtime image @@ -199,4 +217,3 @@ EXPOSE 4822 # PREFIX_DIR build argument. # CMD /opt/guacamole/sbin/guacd -b 0.0.0.0 -L $GUACD_LOG_LEVEL -f - diff --git a/bin/guacctl b/bin/guacctl index 26659a6f9..35dabf254 100755 --- a/bin/guacctl +++ b/bin/guacctl @@ -117,7 +117,7 @@ error() { ## usage() { cat >&2 <]]) + # librt AC_CHECK_FUNC([timer_create], [AC_MSG_RESULT([timer_create was found without librt.])], [AC_CHECK_LIB([rt], [timer_create], @@ -145,34 +157,34 @@ AC_SUBST(CUNIT_LIBS) AC_CHECK_FUNCS([clock_gettime gettimeofday memmove memset select strdup nanosleep]) AC_CHECK_DECL([png_get_io_ptr], - [AC_DEFINE([HAVE_PNG_GET_IO_PTR],, + [AC_DEFINE([HAVE_PNG_GET_IO_PTR],, [Whether png_get_io_ptr() is defined])],, - [#include ]) + [#include ]) AC_CHECK_DECL([cairo_format_stride_for_width], - [AC_DEFINE([HAVE_CAIRO_FORMAT_STRIDE_FOR_WIDTH],, + [AC_DEFINE([HAVE_CAIRO_FORMAT_STRIDE_FOR_WIDTH],, [Whether cairo_format_stride_for_width() is defined])],, - [#include ]) + [#include ]) AC_CHECK_DECL([poll], - [AC_DEFINE([HAVE_POLL],, + [AC_DEFINE([HAVE_POLL],, [Whether poll() is defined])],, - [#include ]) + [#include ]) AC_CHECK_DECL([strlcpy], - [AC_DEFINE([HAVE_STRLCPY],, + [AC_DEFINE([HAVE_STRLCPY],, [Whether strlcpy() is defined])],, - [#include ]) + [#include ]) AC_CHECK_DECL([strlcat], - [AC_DEFINE([HAVE_STRLCAT],, + [AC_DEFINE([HAVE_STRLCAT],, [Whether strlcat() is defined])],, - [#include ]) + [#include ]) AC_CHECK_DECL([strnstr], - [AC_DEFINE([HAVE_STRNSTR],, + [AC_DEFINE([HAVE_STRNSTR],, [Whether strnstr() is defined])],, - [#include ]) + [#include ]) # Typedefs AC_TYPE_SIZE_T @@ -255,10 +267,11 @@ AM_CONDITIONAL([ENABLE_AVCODEC], [test "x${have_libavcodec}" = "xyes"]) have_libavformat=disabled AC_ARG_WITH([libavformat], - [AS_HELP_STRING([--with-libavformat], - [use libavformat when encoding video @<:@default=check@:>@])], - []. + [AS_HELP_STRING([--with-libavformat], + [use libavformat when encoding video @<:@default=check@:>@])], + [], [with_libavformat=check]) + if test "x$with_libavformat" != "xno" then have_libavformat=yes @@ -644,11 +657,72 @@ then fi # -# FreeRDP 2 (libfreerdp2, libfreerdp-client2, and libwinpr2) +# libVNCserver support for the rfbSetDesktopSizeMsg message and the screen +# data structure, both of which are required in order to properly request that +# a remote server resize its screen to match the dimensions that the client +# sends. If libvnc lacks either this message or the screen data structure +# remote resize will be completely disabled. # -have_freerdp2=disabled -FREERDP2_PLUGIN_DIR= +if test "x${have_libvncserver}" = "xyes" +then + + have_vnc_resize_support=yes + AC_CHECK_TYPE([rfbSetDesktopSizeMsg], + [], [have_vnc_resize_support=no], + [[#include ]]) + + AC_CHECK_MEMBERS([rfbClient.screen], + [], [have_vnc_resize_support=no], + [[#include ]]) + + if test "x${have_vnc_resize_support}" = "xno" + then + AC_MSG_WARN([ + -------------------------------------------------------- + The libvncclient library lacks support for either the + rfbSetDesktopSizeMsg message or for extended screen + support. Resizing of remote displays will be disabled. + --------------------------------------------------------]) + else + AC_DEFINE([LIBVNC_HAS_RESIZE_SUPPORT],, + [Whether VNC client will support sending desktop size messages.]) + fi + +fi + +# +# libVNCserver support for the requestedResize member, which enables the +# client to pause frame buffer updates during a resize operation. If support +# for this is missing, Guacamole may still attempt to send the resize requests +# to the remote display, but there may be odd display behavior just before, +# during, or just after the resize, if a display update message happens to +# coincide closely enough with a display resize message. +# + +if test "x${have_libvncserver}" = "xyes" +then + + have_vnc_requestedresize=yes + AC_CHECK_MEMBERS([rfbClient.requestedResize], + [], [have_vnc_requestedresize=no], + [[#include ]]) + + if test "x${have_vnc_requestedresize}" = "xyes" + then + AC_DEFINE([LIBVNC_CLIENT_HAS_REQUESTED_RESIZE],, + [Whether rfbClient contains the requestedResize member.]) + fi + +fi + +# +# FreeRDP (libfreerdpX, libfreerdp-clientX, and libwinprX) +# + +freerdp_version= +have_freerdp=disabled +FREERDP_PLUGIN_DIR= AC_ARG_WITH([rdp], [AS_HELP_STRING([--with-rdp], @@ -660,7 +734,7 @@ AC_ARG_WITH([rdp], AC_ARG_WITH(freerdp_plugin_dir, [AS_HELP_STRING([--with-freerdp-plugin-dir=], [install FreeRDP plugins to the given directory @<:@default=check@:>@]) - ],FREERDP2_PLUGIN_DIR=$withval) + ],FREERDP_PLUGIN_DIR=$withval) # Preserve CPPFLAGS so it can be restored later, following the addition of # options specific to FreeRDP tests @@ -668,21 +742,38 @@ OLDCPPFLAGS="$CPPFLAGS" if test "x$with_rdp" != "xno" then - have_freerdp2=yes + freerdp_version="(3.x)" + have_freerdp=yes + PKG_CHECK_MODULES([RDP], [freerdp3 freerdp-client3 winpr3], + [CPPFLAGS="${RDP_CFLAGS} -Werror $CPPFLAGS"] + [AS_IF([test "x${FREERDP_PLUGIN_DIR}" = "x"], + [FREERDP_PLUGIN_DIR="`$PKG_CONFIG --variable=libdir freerdp3`/freerdp3"])], + [AC_MSG_WARN([ + -------------------------------------------- + Unable to find FreeRDP3 (libfreerdp3 / libfreerdp-client3 / libwinpr3). + Checking for FreeRDP2. + --------------------------------------------]) + have_freerdp=no]) +fi + +if test "x$with_rdp" != "xno" -a "x${have_freerdp}" = "xno" +then + freerdp_version="(2.x)" + have_freerdp=yes PKG_CHECK_MODULES([RDP], [freerdp2 freerdp-client2 winpr2], [CPPFLAGS="${RDP_CFLAGS} -Werror $CPPFLAGS"] - [AS_IF([test "x${FREERDP2_PLUGIN_DIR}" = "x"], - [FREERDP2_PLUGIN_DIR="`$PKG_CONFIG --variable=libdir freerdp2`/freerdp2"])], + [AS_IF([test "x${FREERDP_PLUGIN_DIR}" = "x"], + [FREERDP_PLUGIN_DIR="`$PKG_CONFIG --variable=libdir freerdp2`/freerdp2"])], [AC_MSG_WARN([ -------------------------------------------- - Unable to find FreeRDP (libfreerdp2 / libfreerdp-client2 / libwinpr2) + Unable to find FreeRDP2 (libfreerdp2 / libfreerdp-client2 / libwinpr2) RDP will be disabled. --------------------------------------------]) - have_freerdp2=no]) + have_freerdp=no]) fi # Available color conversion functions -if test "x$have_freerdp2" = "xyes" +if test "x${have_freerdp}" = "xyes" then # FreeRDP 2.0.0-rc3 and older referred to FreeRDPConvertColor() as @@ -704,7 +795,7 @@ AC_ARG_ENABLE(allow_freerdp_snapshots, [allow building against unknown development snapshots of FreeRDP]) ],allow_freerdp_snapshots=yes) -if test "x${have_freerdp2}" = "xyes" -a "x${allow_freerdp_snapshots}" != "xyes" +if test "x${have_freerdp}" = "xyes" -a "x${allow_freerdp_snapshots}" != "xyes" then AC_MSG_CHECKING([whether FreeRDP appears to be a development version]) @@ -732,7 +823,7 @@ then fi # Variation in memory internal allocation/free behavior for bitmaps -if test "x${have_freerdp2}" = "xyes" +if test "x${have_freerdp}" = "xyes" then # FreeRDP 2.0.0-rc0 and older automatically free rdpBitmap and its @@ -756,7 +847,7 @@ then fi # Variation in memory internal allocation/free behavior for channel streams -if test "x${have_freerdp2}" = "xyes" +if test "x${have_freerdp}" = "xyes" then # FreeRDP 2.0.0-rc3 through 2.0.0-rc4 automatically free the wStream @@ -778,7 +869,7 @@ then fi # Glyph callback variants -if test "x${have_freerdp2}" = "xyes" +if test "x${have_freerdp}" = "xyes" then # FreeRDP 2.0.0-rc3 and older used UINT32 for integer parameters to all @@ -811,7 +902,7 @@ then fi # CLIPRDR callback variants -if test "x${have_freerdp2}" = "xyes" +if test "x${have_freerdp}" = "xyes" then # FreeRDP 2.0.0-rc3 and older did not use const for CLIPRDR callbacks @@ -841,7 +932,7 @@ then fi # RAIL callback variants -if test "x${have_freerdp2}" = "xyes" +if test "x${have_freerdp}" = "xyes" then # FreeRDP 2.0.0-rc3 and older did not use const for RAIL callbacks @@ -871,7 +962,7 @@ then fi # Support for receiving unannounced orders from the RDP server -if test "x${have_freerdp2}" = "xyes" +if test "x${have_freerdp}" = "xyes" then AC_CHECK_MEMBERS([rdpSettings.AllowUnanouncedOrdersFromServer],, [AC_MSG_WARN([ @@ -887,17 +978,188 @@ fi # Updated certificate verification callback (introduced with 2.0.0, not present # in 2.0.0-rc4 or earlier) -if test "x${have_freerdp2}" = "xyes" +if test "x${have_freerdp}" = "xyes" then AC_CHECK_MEMBERS([freerdp.VerifyCertificateEx],,, - [[#include ]]) + [[#include ]]) +fi + +if test "x${have_freerdp}" = "xyes" +then + AC_CHECK_DECLS([winpr_aligned_free], + [AC_DEFINE([HAVE_WINPR_ALIGNED],, + [Defined if winpr_aligned_free() and winpr_aligned_malloc() are available])],, + [#include ]) +fi + +if test "x${have_freerdp}" = "xyes" +then + AC_MSG_CHECKING([whether CLIPRDR structs have a common CLIPRDR_HEADER]) + AC_COMPILE_IFELSE([AC_LANG_SOURCE([[ + #include + int main() { + CLIPRDR_FORMAT_LIST list; + list.common.msgType = 0; + (void)list; + return 0; + } + ]])], + [AC_MSG_RESULT([yes])] + [AC_DEFINE([HAVE_CLIPRDR_HEADER],, + [Defined if CLIPRDR structs have a common CLIPRDR_HEADER])], + [AC_MSG_RESULT([no])]) +fi + +if test "x${have_freerdp}" = "xyes" +then + AC_CHECK_DECLS([FreeRDPReadColor], + [AC_DEFINE([USE_UPDATED_RW_COLOR_FUNCS],, + [Defined if FreeRDPReadColor() and FreeRDPWriteColor() are available])],, + [#include ]) +fi + +if test "x${have_freerdp}" = "xyes" +then + AC_CHECK_DECLS([freerdp_settings_set_pointer], + [AC_DEFINE([HAVE_SETTERS_GETTERS],, + [Defined if freerdp_settings_set_pointer is available])],, + [#include ]) +fi + +if test "x${have_freerdp}" = "xyes" +then + AC_MSG_CHECKING([whether freerdp structs have a context]) + AC_COMPILE_IFELSE([AC_LANG_SOURCE([[ + #include + int main() { + freerdp* instance = freerdp_new(); + /* We cast to void to prevent unused variable warnings */ + (void)instance->context->input; + (void)instance->context->settings; + freerdp_free(instance); + return 0; + } + ]])], + [AC_MSG_RESULT([yes])] + [AC_DEFINE([FREERDP_HAS_CONTEXT],, + [FreeRDP structs have a context])], + [AC_MSG_RESULT([no])]) +fi + +if test "x${have_freerdp}" = "xyes" +then + AC_CHECK_DECL([freerdp_shall_disconnect_context], + [AC_DEFINE([HAVE_DISCONNECT_CONTEXT],, + [Defined if 'freerdp_shall_disconnect_context' is available in FreeRDP])],, + [#include ]) +fi +if test "x${have_freerdp}" = "xyes" +then + # Check whether FreeRDP 3.x requires const for GetPluginData + AC_MSG_CHECKING([whether GetPluginData requires const for the returned args]) + AC_COMPILE_IFELSE([AC_LANG_SOURCE([[ + #include + /* A dummy function that matches the expected signature of GetPluginData */ + const ADDIN_ARGV* dummy_GetPluginData(IDRDYNVC_ENTRY_POINTS* pEntryPoints) { + return NULL; + } + + int main() { + /* Create a dummy IDRDYNVC_ENTRY_POINTS struct */ + IDRDYNVC_ENTRY_POINTS entryPoints; + + /* Manually set the GetPluginData pointer for testing */ + entryPoints.GetPluginData = dummy_GetPluginData; + const ADDIN_ARGV* result = entryPoints.GetPluginData(&entryPoints); + + /* Casting to void to avoid unused variable warning */ + (void)result; + + return 0; + } + ]])], + [AC_MSG_RESULT([yes])] + [AC_DEFINE([PLUGIN_DATA_CONST],, + [Defined if GetPluginData returns a pointer to a const ADDIN_ARGV])], + [AC_MSG_RESULT([no])]) +fi + +if test "x${have_freerdp}" = "xyes" +then + # Check whether glyph.New expects a const rdpGlyph* parameter + AC_MSG_CHECKING([whether glyph.New expects a const rdpGlyph* parameter]) + AC_COMPILE_IFELSE([AC_LANG_SOURCE([[ + #include + BOOL mock_glyph_new(rdpContext* context, const rdpGlyph* glyph) { + return TRUE; + } + + int main() { + rdpGlyph* glyph = (rdpGlyph*) malloc(sizeof(rdpGlyph)); + glyph->New = mock_glyph_new; + free(glyph); + return 0; + } + ]])], + [AC_MSG_RESULT([yes])] + [AC_DEFINE([RDP_GLYPH_NEW_REQUIRES_CONST],, + [Defined if glyph.New expects a const rdpGlyph* parameter])], + [AC_MSG_RESULT([no])]) +fi + +if test "x${have_freerdp}" = "xyes" +then + # Check whether pointer.Set expects a const rdpPointer* parameter + AC_MSG_CHECKING([whether pointer.Set expects a const rdpPointer* parameter]) + AC_COMPILE_IFELSE([AC_LANG_SOURCE([[ + #include + + BOOL mock_pointer_set(rdpContext* context, const rdpPointer* pointer) { + return TRUE; + } + + int main() { + rdpPointer* pointer = (rdpPointer*) malloc(sizeof(rdpPointer)); + pointer->Set = mock_pointer_set; + free(pointer); + return 0; + } + ]])], + [AC_MSG_RESULT([yes])] + [AC_DEFINE([RDP_POINTER_SET_REQUIRES_CONST],, + [Defined if pointer.Set expects a const rdpPointer* parameter])], + [AC_MSG_RESULT([no])]) +fi + +if test "x${have_freerdp}" = "xyes" +then + AC_MSG_CHECKING([whether freerdp instance supports LoadChannels]) + AC_COMPILE_IFELSE([AC_LANG_SOURCE([[ + #include + + /* Mock LoadChannels function with the expected signature */ + BOOL load_channels(freerdp* instance) { + return TRUE; + } + + int main() { + freerdp* instance = freerdp_new(); + instance->LoadChannels = load_channels; + freerdp_free(instance); + return 0; + } + ]])], + [AC_MSG_RESULT([yes])] + [AC_DEFINE([RDP_INST_HAS_LOAD_CHANNELS],, + [Defined if freerdp instance supports LoadChannels])], + [AC_MSG_RESULT([no])]) fi # Restore CPPFLAGS, removing FreeRDP-specific options needed for testing CPPFLAGS="$OLDCPPFLAGS" -AC_SUBST(FREERDP2_PLUGIN_DIR) -AM_CONDITIONAL([ENABLE_RDP], [test "x${have_freerdp2}" = "xyes"]) +AC_SUBST(FREERDP_PLUGIN_DIR) +AM_CONDITIONAL([ENABLE_RDP], [test "x${have_freerdp}" = "xyes"]) # # libssh2 @@ -984,9 +1246,8 @@ then fi -AM_CONDITIONAL([ENABLE_SSH_AGENT], - [test "x${have_ssh_agent}" = "xyes" \ - -a "x${enable_ssh_agent}" = "xyes"]) +AM_CONDITIONAL([ENABLE_SSH_AGENT], [test "x${have_ssh_agent}" = "xyes" \ + -a "x${enable_ssh_agent}" = "xyes"]) # # libtelnet @@ -1234,7 +1495,7 @@ AM_COND_IF([ENABLE_SYSTEMD], [build_systemd="${systemd_dir}"], [build_systemd=no # FreeRDP plugins # -AM_COND_IF([ENABLE_RDP], [build_rdp_plugins="${FREERDP2_PLUGIN_DIR}"], [build_rdp_plugins=no]) +AM_COND_IF([ENABLE_RDP], [build_rdp_plugins="${FREERDP_PLUGIN_DIR}"], [build_rdp_plugins=no]) # # Display summary @@ -1247,10 +1508,10 @@ $PACKAGE_NAME version $PACKAGE_VERSION Library status: - freerdp2 ............ ${have_freerdp2} + freerdp ............. ${have_freerdp} ${freerdp_version} pango ............... ${have_pango} libavcodec .......... ${have_libavcodec} - libavformat.......... ${have_libavformat} + libavformat ......... ${have_libavformat} libavutil ........... ${have_libavutil} libssh2 ............. ${have_libssh2} libssl .............. ${have_ssl} @@ -1283,4 +1544,3 @@ $PACKAGE_NAME version $PACKAGE_VERSION Type \"make\" to compile $PACKAGE_NAME. " - diff --git a/src/common-ssh/common-ssh/ssh.h b/src/common-ssh/common-ssh/ssh.h index 986c63b29..d5805b4a9 100644 --- a/src/common-ssh/common-ssh/ssh.h +++ b/src/common-ssh/common-ssh/ssh.h @@ -114,6 +114,10 @@ void guac_common_ssh_uninit(); * * @param user * The user to authenticate as, once connected. + * + * @param timeout + * The number of seconds to attempt to connect to the SSH server before + * timing out. * * @param keepalive * How frequently the connection should send keepalive packets, in @@ -138,7 +142,7 @@ void guac_common_ssh_uninit(); */ guac_common_ssh_session* guac_common_ssh_create_session(guac_client* client, const char* hostname, const char* port, guac_common_ssh_user* user, - int keepalive, const char* host_key, + int timeout, int keepalive, const char* host_key, guac_ssh_credential_handler* credential_handler); /** diff --git a/src/common-ssh/common-ssh/user.h b/src/common-ssh/common-ssh/user.h index 745728f1b..f3065f93a 100644 --- a/src/common-ssh/common-ssh/user.h +++ b/src/common-ssh/common-ssh/user.h @@ -44,6 +44,12 @@ typedef struct guac_common_ssh_user { */ guac_common_ssh_key* private_key; + /** + * The public key which should be used to authenticate this user, if any, + * or NULL if a password or just a private key will be used instead. + */ + char* public_key; + } guac_common_ssh_user; /** @@ -104,5 +110,23 @@ void guac_common_ssh_user_set_password(guac_common_ssh_user* user, int guac_common_ssh_user_import_key(guac_common_ssh_user* user, char* private_key, char* passphrase); +/** + * Imports the given public key, associating that key with the given user. + * If the public key is imported successfully, it will be used for + * future authentication attempts. + * + * @param user + * The user to associate with the given private key. + * + * @param public_key + * The base64-encoded public key to import. + * + * @return + * Zero if public key is successfully imported, or non-zero if the + * public key could not be imported due to an error. + */ +int guac_common_ssh_user_import_public_key(guac_common_ssh_user* user, + char* public_key); + #endif diff --git a/src/common-ssh/key.c b/src/common-ssh/key.c index 4a88aead2..a7f861d89 100644 --- a/src/common-ssh/key.c +++ b/src/common-ssh/key.c @@ -132,18 +132,25 @@ guac_common_ssh_key* guac_common_ssh_key_alloc(char* data, int length, * different key algorithms) we need to perform a heuristic here to check * if a passphrase is needed. This could allow junk keys through that * would never be able to auth. libssh2 should display errors to help - * admins track down malformed keys and delete or replace them. - */ + * admins track down malformed keys and delete or replace them. */ if (is_passphrase_needed(data, length) && (passphrase == NULL || *passphrase == '\0')) return NULL; guac_common_ssh_key* key = guac_mem_alloc(sizeof(guac_common_ssh_key)); + /* NOTE: Older versions of libssh2 will at times ignore the declared key + * length and instead recalculate the length using strlen(). This has since + * been fixed, but as of this writing the fix has not yet been released. + * Below, we add our own null terminator to ensure that such calls to + * strlen() will work without issue. We can remove this workaround once + * copies of libssh2 that use strlen() on key data are not in common use. */ + /* Copy private key to structure */ key->private_key_length = length; - key->private_key = guac_mem_alloc(length); + key->private_key = guac_mem_alloc(guac_mem_ckd_add_or_die(length, 1)); /* Extra byte added here for null terminator (see above) */ memcpy(key->private_key, data, length); + key->private_key[length] = '\0'; /* Manually-added null terminator (see above) */ key->passphrase = guac_strdup(passphrase); return key; diff --git a/src/common-ssh/ssh.c b/src/common-ssh/ssh.c index 373e00156..2cd980bc9 100644 --- a/src/common-ssh/ssh.c +++ b/src/common-ssh/ssh.c @@ -17,6 +17,8 @@ * under the License. */ +#include "common/string.h" + #include "common-ssh/key.h" #include "common-ssh/ssh.h" #include "common-ssh/user.h" @@ -25,6 +27,7 @@ #include #include #include +#include #include #ifdef LIBSSH2_USES_GCRYPT @@ -35,13 +38,16 @@ #include #include +#include #include #include #include #include +#include #include #include #include +#include #include #include @@ -61,7 +67,7 @@ GCRY_THREAD_OPTION_PTHREAD_IMPL; /** * A list of ciphers that are both FIPS-compliant, and OpenSSL-supported. */ -#define FIPS_COMPLIANT_CIPHERS "aes128-ctr,aes192-ctr,aes256-ctr,aes128-cbc,aes192-cbc,aes256-cbc" +#define FIPS_COMPLIANT_CIPHERS "aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-cbc,aes192-cbc,aes256-cbc" #ifdef OPENSSL_REQUIRES_THREADING_CALLBACKS /** @@ -182,9 +188,11 @@ int guac_common_ssh_init(guac_client* client) { CRYPTO_set_locking_callback(guac_common_ssh_openssl_locking_callback); #endif - /* Init OpenSSL */ +#if OPENSSL_VERSION_NUMBER < 0x10100000L + /* Init OpenSSL - only required for OpenSSL Versions < 1.1.0 */ SSL_library_init(); ERR_load_crypto_strings(); +#endif /* Init libssh2 */ libssh2_init(0); @@ -284,6 +292,8 @@ static int guac_common_ssh_authenticate(guac_common_ssh_session* common_session) /* Get user credentials */ guac_common_ssh_key* key = user->private_key; + char* public_key = user->public_key; + /* Validate username provided */ if (user->username == NULL) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED, @@ -317,9 +327,11 @@ static int guac_common_ssh_authenticate(guac_common_ssh_session* common_session) return 1; } + int public_key_length = public_key == NULL ? 0 : strlen(public_key); + /* Attempt public key auth */ if (libssh2_userauth_publickey_frommemory(session, user->username, - username_len, NULL, 0, key->private_key, + username_len, public_key, public_key_length, key->private_key, key->private_key_length, key->passphrase)) { /* Abort on failure */ @@ -403,86 +415,66 @@ static int guac_common_ssh_authenticate(guac_common_ssh_session* common_session) } +/** + * Verifies if given algorithms are supported by libssh2. + * Writes log messages if an algorithm is not supported or + * could not get the list of supported algorithms from libssh2. + * + * @param client + * The Guacamole client that is using SSH. + * + * @param session + * The session associated with the user to be authenticated. + * + * @param method_type + * One of the libssh2 Method Type constants for libssh2_session_method_pref(). + * + * @param algs + * A string with preferred list of algorithms, for example FIPS_COMPLIANT_CIPHERS. + * + */ +static void check_if_algs_are_supported(guac_client* client, LIBSSH2_SESSION* session, + int method_type, const char* algs) { + + /* Request the list of supported algorithms/cyphers from libssh2. */ + const char** supported_algs; + int supported_algs_count = + libssh2_session_supported_algs(session, method_type, &supported_algs); + + if (supported_algs_count > 0) { + char** preferred_algs = guac_split(algs, ','); + for (int i = 0; preferred_algs[i]; i++) { + bool found = false; + /* Check if the algorithm is found in the libssh2 supported list. */ + for (int j = 0; j < supported_algs_count; j++) { + if (strcmp(preferred_algs[i], supported_algs[j]) == 0) { + found = true; + break; + } + } + if (!found) { + guac_client_log(client, GUAC_LOG_WARNING, + "Preferred algorithm/cipher '%s' is not supported by libssh2", preferred_algs[i]); + } + } + guac_mem_free(preferred_algs); + } + else { + guac_client_log(client, GUAC_LOG_WARNING, + "libssh2 reports that no ciphers/algorithms are supported. This may be a bug in libssh2." + "If the SSH connection fails, it may not be possible to log the cause here."); + } +} + guac_common_ssh_session* guac_common_ssh_create_session(guac_client* client, const char* hostname, const char* port, guac_common_ssh_user* user, - int keepalive, const char* host_key, + int timeout, int keepalive, const char* host_key, guac_ssh_credential_handler* credential_handler) { - int retval; - - int fd; - struct addrinfo* addresses; - struct addrinfo* current_address; - - char connected_address[1024]; - char connected_port[64]; - - struct addrinfo hints = { - .ai_family = AF_UNSPEC, - .ai_socktype = SOCK_STREAM, - .ai_protocol = IPPROTO_TCP - }; - - /* Get addresses connection */ - if ((retval = getaddrinfo(hostname, port, &hints, &addresses))) { + int fd = guac_tcp_connect(hostname, port, timeout); + if (fd < 0) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, - "Error parsing given address or port: %s", - gai_strerror(retval)); - return NULL; - } - - /* Attempt connection to each address until success */ - current_address = addresses; - while (current_address != NULL) { - - /* Resolve hostname */ - if ((retval = getnameinfo(current_address->ai_addr, - current_address->ai_addrlen, - connected_address, sizeof(connected_address), - connected_port, sizeof(connected_port), - NI_NUMERICHOST | NI_NUMERICSERV))) - guac_client_log(client, GUAC_LOG_DEBUG, - "Unable to resolve host: %s", gai_strerror(retval)); - - /* Get socket */ - fd = socket(current_address->ai_family, SOCK_STREAM, 0); - if (fd < 0) { - guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, - "Unable to create socket: %s", strerror(errno)); - freeaddrinfo(addresses); - return NULL; - } - - /* Connect */ - if (connect(fd, current_address->ai_addr, - current_address->ai_addrlen) == 0) { - - guac_client_log(client, GUAC_LOG_DEBUG, - "Successfully connected to host %s, port %s", - connected_address, connected_port); - - /* Done if successful connect */ - break; - - } - - /* Otherwise log information regarding bind failure */ - guac_client_log(client, GUAC_LOG_DEBUG, "Unable to connect to " - "host %s, port %s: %s", - connected_address, connected_port, strerror(errno)); - - close(fd); - current_address = current_address->ai_next; - - } - - /* Free addrinfo */ - freeaddrinfo(addresses); - - /* If unable to connect to anything, fail */ - if (current_address == NULL) { - guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND, - "Unable to connect to any addresses."); + "Failed to open TCP connection to %s on %s.", hostname, port); return NULL; } @@ -507,8 +499,16 @@ guac_common_ssh_session* guac_common_ssh_create_session(guac_client* client, * https://csrc.nist.gov/CSRC/media/projects/cryptographic-module-validation-program/documents/security-policies/140sp2906.pdf */ if (guac_fips_enabled()) { + /* + * The following algorithm check is only to simplify debugging. + * libssh2_session_method_pref() ignores unsupported methods. + * So they are not sent to the remote host during protocol negotiation anyways. + */ + check_if_algs_are_supported(client, session, LIBSSH2_METHOD_KEX, FIPS_COMPLIANT_KEX_ALGORITHMS); libssh2_session_method_pref(session, LIBSSH2_METHOD_KEX, FIPS_COMPLIANT_KEX_ALGORITHMS); + check_if_algs_are_supported(client, session, LIBSSH2_METHOD_CRYPT_CS, FIPS_COMPLIANT_CIPHERS); libssh2_session_method_pref(session, LIBSSH2_METHOD_CRYPT_CS, FIPS_COMPLIANT_CIPHERS); + check_if_algs_are_supported(client, session, LIBSSH2_METHOD_CRYPT_SC, FIPS_COMPLIANT_CIPHERS); libssh2_session_method_pref(session, LIBSSH2_METHOD_CRYPT_SC, FIPS_COMPLIANT_CIPHERS); } diff --git a/src/common-ssh/user.c b/src/common-ssh/user.c index 24c74dced..3ebb31d59 100644 --- a/src/common-ssh/user.c +++ b/src/common-ssh/user.c @@ -34,6 +34,7 @@ guac_common_ssh_user* guac_common_ssh_create_user(const char* username) { user->username = guac_strdup(username); user->password = NULL; user->private_key = NULL; + user->public_key = NULL; return user; @@ -48,6 +49,7 @@ void guac_common_ssh_destroy_user(guac_common_ssh_user* user) { /* Free all other data */ guac_mem_free(user->password); guac_mem_free(user->username); + guac_mem_free(user->public_key); guac_mem_free(user); } @@ -83,3 +85,15 @@ int guac_common_ssh_user_import_key(guac_common_ssh_user* user, } +int guac_common_ssh_user_import_public_key(guac_common_ssh_user* user, + char* public_key) { + + /* Free existing public key, if present */ + guac_mem_free(user->public_key); + user->public_key = guac_strdup(public_key); + + /* Fail if key could not be read */ + return user->public_key == NULL; + +} + diff --git a/src/common/Makefile.am b/src/common/Makefile.am index 947104aeb..6f17b6538 100644 --- a/src/common/Makefile.am +++ b/src/common/Makefile.am @@ -35,7 +35,6 @@ noinst_HEADERS = \ common/clipboard.h \ common/cursor.h \ common/defaults.h \ - common/display.h \ common/dot_cursor.h \ common/ibar_cursor.h \ common/iconv.h \ @@ -51,7 +50,6 @@ libguac_common_la_SOURCES = \ blank_cursor.c \ clipboard.c \ cursor.c \ - display.c \ dot_cursor.c \ ibar_cursor.c \ iconv.c \ diff --git a/src/common/common/display.h b/src/common/common/display.h deleted file mode 100644 index 33950a177..000000000 --- a/src/common/common/display.h +++ /dev/null @@ -1,262 +0,0 @@ -/* - * 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_COMMON_DISPLAY_H -#define GUAC_COMMON_DISPLAY_H - -#include "cursor.h" -#include "surface.h" - -#include -#include - -#include - -/** - * A list element representing a pairing of a Guacamole layer with a - * corresponding guac_common_surface which wraps that layer. Adjacent layers - * within the same list are pointed to with traditional prev/next pointers. The - * order of layers in lists need not correspond in any way to the natural - * ordering of those layers' indexes nor their stacking order (Z-order) within - * the display. - */ -typedef struct guac_common_display_layer guac_common_display_layer; - -struct guac_common_display_layer { - - /** - * A Guacamole layer. - */ - guac_layer* layer; - - /** - * The surface which wraps the associated layer. - */ - guac_common_surface* surface; - - /** - * The layer immediately prior to this layer within the list containing - * this layer, or NULL if this is the first layer/buffer in the list. - */ - guac_common_display_layer* prev; - - /** - * The layer immediately following this layer within the list containing - * this layer, or NULL if this is the last layer/buffer in the list. - */ - guac_common_display_layer* next; - -}; - -/** - * Abstracts a remote Guacamole display, having an associated client, - * default surface, mouse cursor, and various allocated buffers and layers. - */ -typedef struct guac_common_display { - - /** - * The client associate with this display. - */ - guac_client* client; - - /** - * The default surface of the client display. - */ - guac_common_surface* default_surface; - - /** - * Client-wide cursor, synchronized across all users. - */ - guac_common_cursor* cursor; - - /** - * The first element within a linked list of all currently-allocated - * layers, or NULL if no layers are currently allocated. The default layer, - * layer #0, is stored within default_surface and will not have a - * corresponding element within this list. - */ - guac_common_display_layer* layers; - - /** - * The first element within a linked list of all currently-allocated - * buffers, or NULL if no buffers are currently allocated. - */ - guac_common_display_layer* buffers; - - /** - * Non-zero if all graphical updates for this display should use lossless - * compression, 0 otherwise. By default, newly-created displays will use - * lossy compression when heuristics determine it is appropriate. - */ - int lossless; - - /** - * Mutex which is locked internally when access to the display must be - * synchronized. All public functions of guac_common_display should be - * considered threadsafe. - */ - pthread_mutex_t _lock; - -} guac_common_display; - -/** - * Allocates a new display, abstracting the cursor and buffer/layer allocation - * operations of the given guac_client such that client state can be easily - * synchronized to joining users. - * - * @param client - * The guac_client to associate with this display. - * - * @param width - * The initial width of the display, in pixels. - * - * @param height - * The initial height of the display, in pixels. - * - * @return - * The newly-allocated display. - */ -guac_common_display* guac_common_display_alloc(guac_client* client, - int width, int height); - -/** - * Frees the given display, and any associated resources, including any - * allocated buffers/layers. - * - * @param display - * The display to free. - */ -void guac_common_display_free(guac_common_display* display); - -/** - * Duplicates the state of the given display to the given socket. Any pending - * changes to buffers, layers, or the default layer are not flushed. - * - * @param display - * The display whose state should be sent along the given socket. - * - * @param client - * The client associated with the users receiving the display state. - * - * @param socket - * The socket over which the display state should be sent. - */ -void guac_common_display_dup( - guac_common_display* display, guac_client* client, - guac_socket* socket); - -/** - * Flushes pending changes to the given display. All pending operations will - * become visible to any connected users. - * - * @param display - * The display to flush. - */ -void guac_common_display_flush(guac_common_display* display); - -/** - * Allocates a new layer, returning a new wrapped layer and corresponding - * surface. The layer may be reused from a previous allocation, if that layer - * has since been freed. - * - * @param display - * The display to allocate a new layer from. - * - * @param width - * The width of the layer to allocate, in pixels. - * - * @param height - * The height of the layer to allocate, in pixels. - * - * @return - * A newly-allocated layer. - */ -guac_common_display_layer* guac_common_display_alloc_layer( - guac_common_display* display, int width, int height); - -/** - * Allocates a new buffer, returning a new wrapped buffer and corresponding - * surface. The buffer may be reused from a previous allocation, if that buffer - * has since been freed. - * - * @param display - * The display to allocate a new buffer from. - * - * @param width - * The width of the buffer to allocate, in pixels. - * - * @param height - * The height of the buffer to allocate, in pixels. - * - * @return - * A newly-allocated buffer. - */ -guac_common_display_layer* guac_common_display_alloc_buffer( - guac_common_display* display, int width, int height); - -/** - * Frees the given surface and associated layer, returning the layer to the - * given display for future use. - * - * @param display - * The display originally allocating the layer. - * - * @param display_layer - * The layer to free. - */ -void guac_common_display_free_layer(guac_common_display* display, - guac_common_display_layer* display_layer); - -/** - * Frees the given surface and associated buffer, returning the buffer to the - * given display for future use. - * - * @param display - * The display originally allocating the buffer. - * - * @param display_buffer - * The buffer to free. - */ -void guac_common_display_free_buffer(guac_common_display* display, - guac_common_display_layer* display_buffer); - -/** - * Sets the overall lossless compression policy of the given display to the - * given value, affecting all current and future layers/buffers maintained by - * the display. By default, newly-created displays will use lossy compression - * for graphical updates when heuristics determine that doing so is - * appropriate. Specifying a non-zero value here will force all graphical - * updates to always use lossless compression, whereas specifying zero will - * restore the default policy. - * - * Note that this can also be adjusted on a per-layer / per-buffer basis with - * guac_common_surface_set_lossless(). - * - * @param display - * The display to modify. - * - * @param lossless - * Non-zero if all graphical updates for this display should use lossless - * compression, 0 otherwise. - */ -void guac_common_display_set_lossless(guac_common_display* display, - int lossless); - -#endif - diff --git a/src/common/display.c b/src/common/display.c deleted file mode 100644 index 00b3cdbdf..000000000 --- a/src/common/display.c +++ /dev/null @@ -1,389 +0,0 @@ -/* - * 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 "common/cursor.h" -#include "common/display.h" -#include "common/surface.h" - -#include -#include -#include - -#include -#include -#include - -/** - * Synchronizes all surfaces within the given linked list to the given socket. - * If the provided pointer to the linked list is NULL, this function has no - * effect. - * - * @param layers - * The head element of the linked list of layers to synchronize, which may - * be NULL if the list is currently empty. - * - * @param client - * The client associated with the users receiving the layers. - * - * @param socket - * The socket over which each layer should be sent. - */ -static void guac_common_display_dup_layers(guac_common_display_layer* layers, - guac_client* client, guac_socket* socket) { - - guac_common_display_layer* current = layers; - - /* Synchronize all surfaces in given list */ - while (current != NULL) { - guac_common_surface_dup(current->surface, client, socket); - current = current->next; - } - -} - -/** - * Frees all layers and associated surfaces within the given list, as well as - * their corresponding list elements. If the provided pointer to the linked - * list is NULL, this function has no effect. - * - * @param layers - * The head element of the linked list of layers to free, which may be NULL - * if the list is currently empty. - * - * @param client - * The client owning the layers wrapped by each of the layers in the list. - */ -static void guac_common_display_free_layers(guac_common_display_layer* layers, - guac_client* client) { - - guac_common_display_layer* current = layers; - - /* Free each surface in given list */ - while (current != NULL) { - - guac_common_display_layer* next = current->next; - guac_layer* layer = current->layer; - - /* Free surface */ - guac_common_surface_free(current->surface); - - /* Destroy layer within remotely-connected client */ - guac_protocol_send_dispose(client->socket, layer); - - /* Free layer or buffer depending on index */ - if (layer->index < 0) - guac_client_free_buffer(client, layer); - else if (layer->index > 0) - guac_client_free_layer(client, layer); - - /* Free current element and advance to next */ - guac_mem_free(current); - current = next; - - } - -} - -/** - * Allocates a display and a cursor which are used to represent the remote - * display and cursor. - * - * @param client - * The client owning the cursor. - * - * @param width - * The desired width of the display. - * - * @param height - * The desired height of the display. - * - * @return - * The newly-allocated display or NULL if display cannot be allocated. - */ -guac_common_display* guac_common_display_alloc(guac_client* client, - int width, int height) { - - /* Allocate display */ - guac_common_display* display = guac_mem_alloc(sizeof(guac_common_display)); - if (display == NULL) - return NULL; - - /* Allocate shared cursor */ - display->cursor = guac_common_cursor_alloc(client); - if (display->cursor == NULL) { - guac_mem_free(display); - return NULL; - } - - pthread_mutex_init(&display->_lock, NULL); - - /* Associate display with given client */ - display->client = client; - - display->default_surface = guac_common_surface_alloc(client, - client->socket, GUAC_DEFAULT_LAYER, width, height); - - /* No initial layers or buffers */ - display->layers = NULL; - display->buffers = NULL; - - return display; - -} - -void guac_common_display_free(guac_common_display* display) { - - /* Free shared cursor */ - guac_common_cursor_free(display->cursor); - - /* Free default surface */ - guac_common_surface_free(display->default_surface); - - /* Free all layers and buffers */ - guac_common_display_free_layers(display->buffers, display->client); - guac_common_display_free_layers(display->layers, display->client); - - pthread_mutex_destroy(&display->_lock); - guac_mem_free(display); - -} - -void guac_common_display_dup( - guac_common_display* display, guac_client* client, - guac_socket* socket) { - - pthread_mutex_lock(&display->_lock); - - /* Synchronize shared cursor */ - guac_common_cursor_dup(display->cursor, client, socket); - - /* Synchronize default surface */ - guac_common_surface_dup(display->default_surface, client, socket); - - /* Synchronize all layers and buffers */ - guac_common_display_dup_layers(display->layers, client, socket); - guac_common_display_dup_layers(display->buffers, client, socket); - - pthread_mutex_unlock(&display->_lock); - -} - -void guac_common_display_set_lossless(guac_common_display* display, - int lossless) { - - pthread_mutex_lock(&display->_lock); - - /* Update lossless setting to be applied to all newly-allocated - * layers/buffers */ - display->lossless = lossless; - - /* Update losslessness of all allocated layers/buffers */ - guac_common_display_layer* current = display->layers; - while (current != NULL) { - guac_common_surface_set_lossless(current->surface, lossless); - current = current->next; - } - - /* Update losslessness of default display layer (not included within layers - * list) */ - guac_common_surface_set_lossless(display->default_surface, lossless); - - pthread_mutex_unlock(&display->_lock); - -} - -void guac_common_display_flush(guac_common_display* display) { - - pthread_mutex_lock(&display->_lock); - - guac_common_display_layer* current = display->layers; - - /* Flush all surfaces */ - while (current != NULL) { - guac_common_surface_flush(current->surface); - current = current->next; - } - - guac_common_surface_flush(display->default_surface); - - pthread_mutex_unlock(&display->_lock); - -} - -/** - * Allocates and inserts a new element into the given linked list of display - * layers, associating it with the given layer and surface. - * - * @param head - * A pointer to the head pointer of the list of layers. The head pointer - * will be updated by this function to point to the newly-allocated - * display layer. - * - * @param layer - * The Guacamole layer to associated with the new display layer. - * - * @param surface - * The surface associated with the given Guacamole layer and which should - * be associated with the new display layer. - * - * @return - * The newly-allocated display layer, which has been associated with the - * provided layer and surface. - */ -static guac_common_display_layer* guac_common_display_add_layer( - guac_common_display_layer** head, guac_layer* layer, - guac_common_surface* surface) { - - guac_common_display_layer* old_head = *head; - - guac_common_display_layer* display_layer = - guac_mem_alloc(sizeof(guac_common_display_layer)); - - /* Init layer/surface pair */ - display_layer->layer = layer; - display_layer->surface = surface; - - /* Insert list element as the new head */ - display_layer->prev = NULL; - display_layer->next = old_head; - *head = display_layer; - - /* Update old head to point to new element, if it existed */ - if (old_head != NULL) - old_head->prev = display_layer; - - return display_layer; - -} - -/** - * Removes the given display layer from the linked list whose head pointer is - * provided. - * - * @param head - * A pointer to the head pointer of the list of layers. The head pointer - * will be updated by this function if necessary, and will be set to NULL - * if the display layer being removed is the only layer in the list. - * - * @param display_layer - * The display layer to remove from the given list. - */ -static void guac_common_display_remove_layer(guac_common_display_layer** head, - guac_common_display_layer* display_layer) { - - /* Update previous element, if it exists */ - if (display_layer->prev != NULL) - display_layer->prev->next = display_layer->next; - - /* If there is no previous element, update the list head */ - else - *head = display_layer->next; - - /* Update next element, if it exists */ - if (display_layer->next != NULL) - display_layer->next->prev = display_layer->prev; - -} - -guac_common_display_layer* guac_common_display_alloc_layer( - guac_common_display* display, int width, int height) { - - pthread_mutex_lock(&display->_lock); - - /* Allocate Guacamole layer */ - guac_layer* layer = guac_client_alloc_layer(display->client); - - /* Allocate corresponding surface */ - guac_common_surface* surface = guac_common_surface_alloc(display->client, - display->client->socket, layer, width, height); - - /* Apply current display losslessness */ - guac_common_surface_set_lossless(surface, display->lossless); - - /* Add layer and surface to list */ - guac_common_display_layer* display_layer = - guac_common_display_add_layer(&display->layers, layer, surface); - - pthread_mutex_unlock(&display->_lock); - return display_layer; - -} - -guac_common_display_layer* guac_common_display_alloc_buffer( - guac_common_display* display, int width, int height) { - - pthread_mutex_lock(&display->_lock); - - /* Allocate Guacamole buffer */ - guac_layer* buffer = guac_client_alloc_buffer(display->client); - - /* Allocate corresponding surface */ - guac_common_surface* surface = guac_common_surface_alloc(display->client, - display->client->socket, buffer, width, height); - - /* Apply current display losslessness */ - guac_common_surface_set_lossless(surface, display->lossless); - - /* Add buffer and surface to list */ - guac_common_display_layer* display_layer = - guac_common_display_add_layer(&display->buffers, buffer, surface); - - pthread_mutex_unlock(&display->_lock); - return display_layer; - -} - -void guac_common_display_free_layer(guac_common_display* display, - guac_common_display_layer* display_layer) { - - pthread_mutex_lock(&display->_lock); - - /* Remove list element from list */ - guac_common_display_remove_layer(&display->layers, display_layer); - - /* Free associated layer and surface */ - guac_common_surface_free(display_layer->surface); - guac_client_free_layer(display->client, display_layer->layer); - - /* Free list element */ - guac_mem_free(display_layer); - - pthread_mutex_unlock(&display->_lock); - -} - -void guac_common_display_free_buffer(guac_common_display* display, - guac_common_display_layer* display_buffer) { - - pthread_mutex_lock(&display->_lock); - - /* Remove list element from list */ - guac_common_display_remove_layer(&display->buffers, display_buffer); - - /* Free associated layer and surface */ - guac_common_surface_free(display_buffer->surface); - guac_client_free_buffer(display->client, display_buffer->layer); - - /* Free list element */ - guac_mem_free(display_buffer); - - pthread_mutex_unlock(&display->_lock); - -} - diff --git a/src/guacd-docker/bin/build-all.sh b/src/guacd-docker/bin/build-all.sh index 52bd150c7..430419cfe 100755 --- a/src/guacd-docker/bin/build-all.sh +++ b/src/guacd-docker/bin/build-all.sh @@ -84,22 +84,44 @@ install_from_git() { # Configure build using CMake or GNU Autotools, whichever happens to be # used by the library being built if [ -e CMakeLists.txt ]; then - cmake -DCMAKE_INSTALL_PREFIX:PATH="$PREFIX_DIR" "$@" . + cmake \ + -B "${REPO_DIR}-build" -S . \ + -DCMAKE_INSTALL_PREFIX:PATH="$PREFIX_DIR" \ + "$@" + + # Build and install + cmake --build "${REPO_DIR}-build" + cmake --install "${REPO_DIR}-build" else [ -e configure ] || autoreconf -fi ./configure --prefix="$PREFIX_DIR" "$@" + + # Build and install + make && make install fi +} - # Build and install - make && make install +# +# Determine any option overrides to guarantee successful build +# -} +export BUILD_ARCHITECTURE="$(arch)" # Determine architecture building on +echo "Build architecture: $BUILD_ARCHITECTURE" + +case $BUILD_ARCHITECTURE in + armv6l|armv7l|aarch64) + export FREERDP_OPTS_OVERRIDES="-DWITH_SSE2=OFF" # Disable SSE2 on ARM + ;; + *) + export FREERDP_OPTS_OVERRIDES="" + ;; +esac # # Build and install core protocol library dependencies # -install_from_git "https://github.com/FreeRDP/FreeRDP" "$WITH_FREERDP" $FREERDP_OPTS +install_from_git "https://github.com/FreeRDP/FreeRDP" "$WITH_FREERDP" $FREERDP_OPTS $FREERDP_OPTS_OVERRIDES install_from_git "https://github.com/libssh2/libssh2" "$WITH_LIBSSH2" $LIBSSH2_OPTS install_from_git "https://github.com/seanmiddleditch/libtelnet" "$WITH_LIBTELNET" $LIBTELNET_OPTS install_from_git "https://github.com/LibVNC/libvncserver" "$WITH_LIBVNCCLIENT" $LIBVNCCLIENT_OPTS @@ -111,5 +133,4 @@ install_from_git "https://github.com/warmcat/libwebsockets" "$WITH_LIBWEBSOCKETS cd "$BUILD_DIR" autoreconf -fi && ./configure --prefix="$PREFIX_DIR" $GUACAMOLE_SERVER_OPTS -make && make install - +make && make check && make install diff --git a/src/guacd/daemon.c b/src/guacd/daemon.c index ac848f10b..1701a366a 100644 --- a/src/guacd/daemon.c +++ b/src/guacd/daemon.c @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -320,6 +321,14 @@ int main(int argc, char* argv[]) { /* General */ int retval; +#ifdef HAVE_DECL_PTHREAD_SETATTR_DEFAULT_NP + /* Set default stack size */ + pthread_attr_t default_pthread_attr; + pthread_attr_init(&default_pthread_attr); + pthread_attr_setstacksize(&default_pthread_attr, GUACD_THREAD_STACK_SIZE); + pthread_setattr_default_np(&default_pthread_attr); +#endif // HAVE_DECL_PTHREAD_SETATTR_DEFAULT_NP + /* Load configuration */ guacd_config* config = guacd_conf_load(); if (config == NULL || guacd_conf_parse_args(config, argc, argv)) @@ -426,10 +435,15 @@ int main(int argc, char* argv[]) { CRYPTO_set_locking_callback(guacd_openssl_locking_callback); #endif - /* Init SSL */ +#if OPENSSL_VERSION_NUMBER < 0x10100000L + /* Init OpenSSL for OpenSSL Versions < 1.1.0 */ SSL_library_init(); SSL_load_error_strings(); ssl_context = SSL_CTX_new(SSLv23_server_method()); +#else + /* Set up OpenSSL for OpenSSL Versions >= 1.1.0 */ + ssl_context = SSL_CTX_new(TLS_server_method()); +#endif /* Load key */ if (config->key_file != NULL) { diff --git a/src/guacd/proc-map.h b/src/guacd/proc-map.h index adce8cf6d..5bae48978 100644 --- a/src/guacd/proc-map.h +++ b/src/guacd/proc-map.h @@ -32,6 +32,11 @@ */ #define GUACD_CLIENT_MAX_CONNECTIONS 65536 +/** + * The pthread stack size for the guacd daemon. + */ +#define GUACD_THREAD_STACK_SIZE 8388608 + /** * The number of hash buckets in each process map. */ diff --git a/src/guacenc/video.c b/src/guacenc/video.c index 124764862..61e1842e1 100644 --- a/src/guacenc/video.c +++ b/src/guacenc/video.c @@ -500,7 +500,9 @@ int guacenc_video_free(guacenc_video* video) { /* Clean up encoding context */ if (video->context != NULL) { +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(61, 3, 100) avcodec_close(video->context); +#endif avcodec_free_context(&(video->context)); } diff --git a/src/libguac/Makefile.am b/src/libguac/Makefile.am index b52f476e4..daf148198 100644 --- a/src/libguac/Makefile.am +++ b/src/libguac/Makefile.am @@ -39,16 +39,25 @@ libguacinc_HEADERS = \ guacamole/argv.h \ guacamole/argv-constants.h \ guacamole/argv-fntypes.h \ + guacamole/assert.h \ guacamole/audio.h \ guacamole/audio-fntypes.h \ guacamole/audio-types.h \ - guacamole/client-constants.h \ guacamole/client.h \ + guacamole/client-constants.h \ guacamole/client-fntypes.h \ guacamole/client-types.h \ + guacamole/display.h \ + guacamole/display-constants.h \ + guacamole/display-types.h \ guacamole/error.h \ guacamole/error-types.h \ + guacamole/fifo.h \ + guacamole/fifo-constants.h \ + guacamole/fifo-types.h \ guacamole/fips.h \ + guacamole/flag.h \ + guacamole/flag-types.h \ guacamole/hash.h \ guacamole/layer.h \ guacamole/layer-types.h \ @@ -66,14 +75,17 @@ libguacinc_HEADERS = \ guacamole/protocol-constants.h \ guacamole/protocol-types.h \ guacamole/recording.h \ + guacamole/rect.h \ + guacamole/rect-types.h \ guacamole/rwlock.h \ - guacamole/socket-constants.h \ guacamole/socket.h \ + guacamole/socket-constants.h \ guacamole/socket-fntypes.h \ guacamole/socket-types.h \ guacamole/stream.h \ guacamole/stream-types.h \ guacamole/string.h \ + guacamole/tcp.h \ guacamole/timestamp.h \ guacamole/timestamp-types.h \ guacamole/unicode.h \ @@ -97,45 +109,64 @@ libguacprivinc_HEADERS = \ # Private, non-installed headers # -noinst_HEADERS = \ - id.h \ - encode-jpeg.h \ - encode-png.h \ - palette.h \ - user-handlers.h \ - raw_encoder.h \ +noinst_HEADERS = \ + display-builtin-cursors.h \ + display-plan.h \ + display-priv.h \ + encode-jpeg.h \ + encode-png.h \ + id.h \ + palette.h \ + raw_encoder.h \ + user-handlers.h \ wait-fd.h -libguac_la_SOURCES = \ - argv.c \ - audio.c \ - client.c \ - encode-jpeg.c \ - encode-png.c \ - error.c \ - fips.c \ - hash.c \ - id.c \ - mem.c \ - rwlock.c \ - palette.c \ - parser.c \ - pool.c \ - protocol.c \ - raw_encoder.c \ - recording.c \ - socket.c \ - socket-broadcast.c \ - socket-fd.c \ - socket-nest.c \ - socket-tee.c \ - string.c \ - timestamp.c \ - unicode.c \ - user.c \ - user-handlers.c \ - user-handshake.c \ - wait-fd.c \ +libguac_la_SOURCES = \ + argv.c \ + audio.c \ + client.c \ + display.c \ + display-builtin-cursors.c \ + display-cursor.c \ + display-flush.c \ + display-layer.c \ + display-layer-list.c \ + display-plan.c \ + display-plan-combine.c \ + display-plan-rect.c \ + display-plan-search.c \ + display-render-thread.c \ + display-worker.c \ + encode-jpeg.c \ + encode-png.c \ + error.c \ + fifo.c \ + fips.c \ + flag.c \ + hash.c \ + id.c \ + mem.c \ + rwlock.c \ + palette.c \ + parser.c \ + pool.c \ + protocol.c \ + raw_encoder.c \ + recording.c \ + rect.c \ + socket.c \ + socket-broadcast.c \ + socket-fd.c \ + socket-nest.c \ + socket-tee.c \ + string.c \ + tcp.c \ + timestamp.c \ + unicode.c \ + user.c \ + user-handlers.c \ + user-handshake.c \ + wait-fd.c \ wol.c # Compile WebP support if available diff --git a/src/libguac/client.c b/src/libguac/client.c index a69c06c67..5926e9ca6 100644 --- a/src/libguac/client.c +++ b/src/libguac/client.c @@ -48,10 +48,10 @@ #include /** - * The number of nanoseconds between times that the pending users list will be + * The number of milliseconds between times that the pending users list will be * synchronized and emptied (250 milliseconds aka 1/4 second). */ -#define GUAC_CLIENT_PENDING_USERS_REFRESH_INTERVAL 250000000 +#define GUAC_CLIENT_PENDING_USERS_REFRESH_INTERVAL 250 /** * A value that indicates that the pending users timer has yet to be @@ -127,13 +127,11 @@ guac_stream* guac_client_alloc_stream(guac_client* client) { guac_stream* allocd_stream; int stream_index; - /* Refuse to allocate beyond maximum */ - if (client->__stream_pool->active == GUAC_CLIENT_MAX_STREAMS) + /* Allocate stream, but refuse to allocate beyond maximum */ + stream_index = guac_pool_next_int_below(client->__stream_pool, GUAC_CLIENT_MAX_STREAMS); + if (stream_index < 0) return NULL; - /* Allocate stream */ - stream_index = guac_pool_next_int(client->__stream_pool); - /* Initialize stream with odd index (even indices are user-level) */ allocd_stream = &(client->__output_streams[stream_index]); allocd_stream->index = (stream_index * 2) + 1; @@ -148,40 +146,23 @@ guac_stream* guac_client_alloc_stream(guac_client* client) { void guac_client_free_stream(guac_client* client, guac_stream* stream) { - /* Release index to pool */ - guac_pool_free_int(client->__stream_pool, (stream->index - 1) / 2); - /* Mark stream as closed */ + int freed_index = stream->index; stream->index = GUAC_CLIENT_CLOSED_STREAM_INDEX; + /* Release index to pool */ + guac_pool_free_int(client->__stream_pool, (freed_index - 1) / 2); + } /** * Promote all pending users to full users, calling the join pending handler * before, if any. * - * @param data + * @param client * The client for which all pending users should be promoted. */ -static void guac_client_promote_pending_users(union sigval data) { - - guac_client* client = (guac_client*) data.sival_ptr; - - pthread_mutex_lock(&(client->__pending_users_timer_mutex)); - - /* Check if the previous instance of this handler is still running */ - int already_running = ( - client->__pending_users_timer_state - == GUAC_CLIENT_PENDING_TIMER_TRIGGERED); - - /* Mark the handler as running if it isn't already */ - client->__pending_users_timer_state = GUAC_CLIENT_PENDING_TIMER_TRIGGERED; - - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); - - /* Do not start the handler if the previous instance is still running */ - if (already_running) - return; +static void guac_client_promote_pending_users(guac_client* client) { /* Acquire the lock for reading and modifying the list of pending users */ guac_rwlock_acquire_write_lock(&(client->__pending_users_lock)); @@ -244,10 +225,29 @@ static void guac_client_promote_pending_users(union sigval data) { * to ensure that all users are always on exactly one of these lists) */ guac_rwlock_release_lock(&(client->__pending_users_lock)); - /* Mark the handler as complete so the next instance can run */ - pthread_mutex_lock(&(client->__pending_users_timer_mutex)); - client->__pending_users_timer_state = GUAC_CLIENT_PENDING_TIMER_REGISTERED; - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); +} + +/** + * Thread that periodically checks for users that have requested to join the + * current connection (pending users). The check is performed every + * GUAC_CLIENT_PENDING_USERS_REFRESH_INTERVAL milliseconds. + * + * @param data + * A pointer to the guac_client associated with the connection. + * + * @return + * Always NULL. + */ +static void* guac_client_pending_users_thread(void* data) { + + guac_client* client = (guac_client*) data; + + while (client->state == GUAC_CLIENT_RUNNING) { + guac_client_promote_pending_users(client); + guac_timestamp_msleep(GUAC_CLIENT_PENDING_USERS_REFRESH_INTERVAL); + } + + return NULL; } @@ -295,12 +295,6 @@ guac_client* guac_client_alloc() { guac_rwlock_init(&(client->__users_lock)); guac_rwlock_init(&(client->__pending_users_lock)); - /* The timer will be lazily created in the child process */ - client->__pending_users_timer_state = GUAC_CLIENT_PENDING_TIMER_UNREGISTERED; - - /* Set up the pending user promotion mutex */ - pthread_mutex_init(&(client->__pending_users_timer_mutex), NULL); - /* Set up broadcast sockets */ client->socket = guac_socket_broadcast(client); client->pending_socket = guac_socket_broadcast_pending(client); @@ -311,6 +305,9 @@ guac_client* guac_client_alloc() { void guac_client_free(guac_client* client) { + /* Ensure that anything waiting for the client can begin shutting down */ + guac_client_stop(client); + /* Acquire write locks before referencing user pointers */ guac_rwlock_acquire_write_lock(&(client->__pending_users_lock)); guac_rwlock_acquire_write_lock(&(client->__users_lock)); @@ -323,6 +320,11 @@ void guac_client_free(guac_client* client) { while (client->__users != NULL) guac_client_remove_user(client, client->__users); + /* Clean up the thread monitoring for new pending users, if it's been + * started */ + if (client->__pending_users_thread_started) + pthread_join(client->__pending_users_thread, NULL); + /* Release the locks */ guac_rwlock_release_lock(&(client->__users_lock)); guac_rwlock_release_lock(&(client->__pending_users_lock)); @@ -354,19 +356,6 @@ void guac_client_free(guac_client* client) { guac_client_log(client, GUAC_LOG_ERROR, "Unable to close plugin: %s", dlerror()); } - /* Find out if the pending user promotion timer was ever started */ - pthread_mutex_lock(&(client->__pending_users_timer_mutex)); - int was_started = ( - client->__pending_users_timer_state - != GUAC_CLIENT_PENDING_TIMER_UNREGISTERED); - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); - - /* If the timer was registered, stop it before destroying the lock */ - if (was_started) - timer_delete(client->__pending_users_timer); - - pthread_mutex_destroy(&(client->__pending_users_timer_mutex)); - /* Destroy the reentrant read-write locks */ guac_rwlock_destroy(&(client->__users_lock)); guac_rwlock_destroy(&(client->__pending_users_lock)); @@ -444,12 +433,19 @@ void guac_client_abort(guac_client* client, guac_protocol_status status, * @param user * The user to add to the pending list. */ -static void guac_client_add_pending_user( - guac_client* client, guac_user* user) { +static void guac_client_add_pending_user(guac_client* client, + guac_user* user) { /* Acquire the lock for modifying the list of pending users */ guac_rwlock_acquire_write_lock(&(client->__pending_users_lock)); + /* Set up the pending user promotion mutex */ + if (!client->__pending_users_thread_started) { + pthread_create(&client->__pending_users_thread, NULL, + guac_client_pending_users_thread, (void*) client); + client->__pending_users_thread_started = 1; + } + user->__prev = NULL; user->__next = client->__pending_users; @@ -466,82 +462,8 @@ static void guac_client_add_pending_user( } -/** - * Periodically promote pending users to full users. Returns zero if the timer - * is already running, or successfully created, or a non-zero value if the - * timer could not be created and started. - * - * @param client - * The guac client for which the new timer should be started, if not - * already running. - * - * @return - * Zero if the timer was successfully created and started, or a negative - * value otherwise. - */ -static int guac_client_start_pending_users_timer(guac_client* client) { - - pthread_mutex_lock(&(client->__pending_users_timer_mutex)); - - /* Return success if the timer is already created and running */ - if (client->__pending_users_timer_state - != GUAC_CLIENT_PENDING_TIMER_UNREGISTERED) { - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); - return 0; - } - - /* Configure the timer to synchronize and clear the pending users */ - struct sigevent signal_config = { - .sigev_notify = SIGEV_THREAD, - .sigev_notify_function = guac_client_promote_pending_users, - .sigev_value = { .sival_ptr = client }}; - - /* Create a timer to synchronize any pending users periodically */ - if (timer_create( - CLOCK_MONOTONIC, - &signal_config, - &(client->__pending_users_timer))) { - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); - return 1; - } - - /* Configure the pending users timer to run on the defined interval */ - struct itimerspec time_config = { - .it_interval = { .tv_nsec = GUAC_CLIENT_PENDING_USERS_REFRESH_INTERVAL }, - .it_value = { .tv_nsec = GUAC_CLIENT_PENDING_USERS_REFRESH_INTERVAL } - }; - - /* Start the timer */ - if (timer_settime( - client->__pending_users_timer, 0, &time_config, NULL) < 0) { - timer_delete(client->__pending_users_timer); - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); - return 1; - } - - /* Mark the timer as registered but not yet running */ - client->__pending_users_timer_state = GUAC_CLIENT_PENDING_TIMER_REGISTERED; - - pthread_mutex_unlock(&(client->__pending_users_timer_mutex)); - return 0; - -} - int guac_client_add_user(guac_client* client, guac_user* user, int argc, char** argv) { - /* Create and start the timer if it hasn't already been initialized */ - if (guac_client_start_pending_users_timer(client)) { - - /** - * - * If the timer could not be created, do not add the user - they cannot - * be synchronized without the timer. - */ - guac_client_log(client, GUAC_LOG_ERROR, - "Could not start pending user timer: %s.", strerror(errno)); - return 1; - } - int retval = 0; /* Call handler, if defined */ @@ -698,15 +620,19 @@ void* guac_client_for_user(guac_client* client, guac_user* user, } int guac_client_end_frame(guac_client* client) { + return guac_client_end_multiple_frames(client, 0); +} + +int guac_client_end_multiple_frames(guac_client* client, int frames) { /* Update and send timestamp */ client->last_sent_timestamp = guac_timestamp_current(); /* Log received timestamp and calculated lag (at TRACE level only) */ guac_client_log(client, GUAC_LOG_TRACE, "Server completed " - "frame %" PRIu64 "ms.", client->last_sent_timestamp); + "frame %" PRIu64 "ms (%i logical frames)", client->last_sent_timestamp, frames); - return guac_protocol_send_sync(client->socket, client->last_sent_timestamp); + return guac_protocol_send_sync(client->socket, client->last_sent_timestamp, frames); } diff --git a/src/libguac/display-builtin-cursors.c b/src/libguac/display-builtin-cursors.c new file mode 100644 index 000000000..a5d12fa9c --- /dev/null +++ b/src/libguac/display-builtin-cursors.c @@ -0,0 +1,149 @@ +/* + * 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 "display-builtin-cursors.h" + +/** + * Opaque black. This macro evaluates to the 4 bytes of the single pixel of a + * 32-bit ARGB image that represent opaque black and is expected to be used + * only within this file to help make embedded cursor graphics more readable. + */ +#define X 0x00,0x00,0x00,0xFF + +/** + * Opaque gray. This macro evaluates to the 4 bytes of the single pixel of a + * 32-bit ARGB image that represent opaque gray and is expected to be used only + * within this file to help make embedded cursor graphics more readable. + */ +#define U 0x80,0x80,0x80,0xFF + +/** + * Opaque white. This macro evaluates to the 4 bytes of the single pixel of a + * 32-bit ARGB image that represent opaque white and is expected to be used + * only within this file to help make embedded cursor graphics more readable. + */ +#define O 0xFF,0xFF,0xFF,0xFF + +/** + * Full transparency. This macro evaluates to the 4 bytes of the single pixel + * of a 32-bit ARGB image that represent full transparency and is expected to + * be used only within this file to help make embedded cursor graphics more + * readable. + */ +#define _ 0x00,0x00,0x00,0x00 + +const guac_display_builtin_cursor guac_display_cursor_none = { + + .hotspot_x = 0, + .hotspot_y = 0, + + .buffer = (unsigned char[]) { + _ /* Single, transparent pixel */ + }, + + .width = 1, + .height = 1, + .stride = 4 + +}; + +const guac_display_builtin_cursor guac_display_cursor_dot = { + + .hotspot_x = 2, + .hotspot_y = 2, + + .buffer = (unsigned char[]) { + + _,O,O,O,_, + O,X,X,X,O, + O,X,X,X,O, + O,X,X,X,O, + _,O,O,O,_ + + }, + + .width = 5, + .height = 5, + .stride = 20 + +}; + +const guac_display_builtin_cursor guac_display_cursor_ibar = { + + .hotspot_x = 3, + .hotspot_y = 7, + + .buffer = (unsigned char[]) { + + X,X,X,X,X,X,X, + X,O,O,U,O,O,X, + X,X,X,O,X,X,X, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + _,_,X,O,X,_,_, + X,X,X,O,X,X,X, + X,O,O,U,O,O,X, + X,X,X,X,X,X,X + + }, + + .width = 7, + .height = 16, + .stride = 28 + +}; + +const guac_display_builtin_cursor guac_display_cursor_pointer = { + + .hotspot_x = 0, + .hotspot_y = 0, + + .buffer = (unsigned char[]) { + + O,_,_,_,_,_,_,_,_,_,_, + O,O,_,_,_,_,_,_,_,_,_, + O,X,O,_,_,_,_,_,_,_,_, + O,X,X,O,_,_,_,_,_,_,_, + O,X,X,X,O,_,_,_,_,_,_, + O,X,X,X,X,O,_,_,_,_,_, + O,X,X,X,X,X,O,_,_,_,_, + O,X,X,X,X,X,X,O,_,_,_, + O,X,X,X,X,X,X,X,O,_,_, + O,X,X,X,X,X,X,X,X,O,_, + O,X,X,X,X,X,O,O,O,O,O, + O,X,X,O,X,X,O,_,_,_,_, + O,X,O,_,O,X,X,O,_,_,_, + O,O,_,_,O,X,X,O,_,_,_, + O,_,_,_,_,O,X,X,O,_,_, + _,_,_,_,_,O,O,O,O,_,_ + + }, + + .width = 11, + .height = 16, + .stride = 44 + +}; diff --git a/src/libguac/display-builtin-cursors.h b/src/libguac/display-builtin-cursors.h new file mode 100644 index 000000000..01ec38186 --- /dev/null +++ b/src/libguac/display-builtin-cursors.h @@ -0,0 +1,93 @@ +/* + * 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_DISPLAY_BUILTIN_CURSORS_H +#define GUAC_DISPLAY_BUILTIN_CURSORS_H + +#include + +/** + * Mouse cursor image that is built into libguac. Each actual instance of this + * structure will correspond to a value within the guac_display_cursor_type + * enum. + */ +typedef struct guac_display_builtin_cursor { + + /** + * The raw, 32-bit ARGB image for this mouse cursor. + */ + const unsigned char* const buffer; + + /** + * The width of this mouse cursor image, in pixels. + */ + const unsigned int width; + + /** + * The height of this mouse cursor image, in pixels. + */ + const unsigned int height; + + /** + * The size of each row of image data, in bytes. + */ + const size_t stride; + + /** + * The X coordinate of the relative position of the pointer hotspot within + * the cursor image. The hotspot is the location that the mouse pointer is + * actually reported, with the cursor image visibly positioned relative to + * that location. + */ + int hotspot_x; + + /** + * The Y coordinate of the relative position of the pointer hotspot within + * the cursor image. The hotspot is the location that the mouse pointer is + * actually reported, with the cursor image visibly positioned relative to + * that location. + */ + int hotspot_y; + +} guac_display_builtin_cursor; + +/** + * An empty (invisible/hidden) mouse cursor. + */ +extern const guac_display_builtin_cursor guac_display_cursor_none; + +/** + * A small dot. This is typically used in situations where cursor information + * for the remote desktop is not available, thus all cursor rendering must + * happen remotely, but it's still important that the user be able to see the + * current location of their local mouse pointer. + */ +extern const guac_display_builtin_cursor guac_display_cursor_dot; + +/** + * A vertical, I-shaped bar indicating text input or selection. + */ +extern const guac_display_builtin_cursor guac_display_cursor_ibar; + +/** + * A standard, general-purpose pointer. + */ +extern const guac_display_builtin_cursor guac_display_cursor_pointer; + +#endif diff --git a/src/libguac/display-cursor.c b/src/libguac/display-cursor.c new file mode 100644 index 000000000..6d119caa9 --- /dev/null +++ b/src/libguac/display-cursor.c @@ -0,0 +1,104 @@ +/* + * 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 "display-builtin-cursors.h" +#include "display-priv.h" +#include "guacamole/assert.h" +#include "guacamole/display.h" +#include "guacamole/mem.h" +#include "guacamole/rect.h" +#include "guacamole/rwlock.h" + +#include + +guac_display_layer* guac_display_cursor(guac_display* display) { + return display->cursor_buffer; +} + +void guac_display_set_cursor_hotspot(guac_display* display, int x, int y) { + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + display->pending_frame.cursor_hotspot_x = x; + display->pending_frame.cursor_hotspot_y = y; + + guac_rwlock_release_lock(&display->pending_frame.lock); +} + +void guac_display_set_cursor(guac_display* display, + guac_display_cursor_type cursor_type) { + + /* Translate requested type into built-in cursor */ + const guac_display_builtin_cursor* cursor; + switch (cursor_type) { + + case GUAC_DISPLAY_CURSOR_NONE: + cursor = &guac_display_cursor_none; + break; + + case GUAC_DISPLAY_CURSOR_DOT: + cursor = &guac_display_cursor_dot; + break; + + case GUAC_DISPLAY_CURSOR_IBAR: + cursor = &guac_display_cursor_ibar; + break; + + case GUAC_DISPLAY_CURSOR_POINTER: + default: + cursor = &guac_display_cursor_pointer; + break; + + } + + /* Resize cursor to fit requested icon */ + guac_display_layer* cursor_layer = guac_display_cursor(display); + guac_display_layer_resize(cursor_layer, cursor->width, cursor->height); + + /* Copy over graphical content of cursor icon ... */ + + guac_display_layer_raw_context* context = guac_display_layer_open_raw(cursor_layer); + GUAC_ASSERT(!cursor_layer->pending_frame.buffer_is_external); + + const unsigned char* src_cursor_row = cursor->buffer; + unsigned char* dst_cursor_row = context->buffer; + size_t row_length = guac_mem_ckd_mul_or_die(cursor->width, 4); + + for (int y = 0; y < cursor->height; y++) { + memcpy(dst_cursor_row, src_cursor_row, row_length); + src_cursor_row += cursor->stride; + dst_cursor_row += context->stride; + } + + /* ... and cursor hotspot */ + guac_display_set_cursor_hotspot(display, cursor->hotspot_x, cursor->hotspot_y); + + /* Update to cursor icon is now complete - notify display */ + + context->dirty = (guac_rect) { + .left = 0, + .top = 0, + .right = cursor->width, + .bottom = cursor->height + }; + + guac_display_layer_close_raw(cursor_layer, context); + + guac_display_end_mouse_frame(display); + +} diff --git a/src/libguac/display-flush.c b/src/libguac/display-flush.c new file mode 100644 index 000000000..44436168a --- /dev/null +++ b/src/libguac/display-flush.c @@ -0,0 +1,391 @@ +/* + * 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 "display-plan.h" +#include "display-priv.h" +#include "guacamole/assert.h" +#include "guacamole/client.h" +#include "guacamole/display.h" +#include "guacamole/fifo.h" +#include "guacamole/flag.h" +#include "guacamole/mem.h" +#include "guacamole/protocol.h" +#include "guacamole/rect.h" +#include "guacamole/rwlock.h" +#include "guacamole/user.h" + +#include + +/** + * Begins a section related to an optimization phase that should be tracked for + * performance at the "trace" log level. + */ +#define GUAC_DISPLAY_PLAN_BEGIN_PHASE() \ + do { \ + guac_timestamp phase_start = guac_timestamp_current(); + +/** + * Ends a section related to an optimization phase that should be tracked for + * performance at the "trace" log level. + * + * @param display + * The guac_display related to the optimizations being performed. + * + * @param phase + * A human-readable name for the optimization phase being tracked. + * + * @param n + * The ordinal number of this phase relative to other phases, where the + * first phase is phase 1. + * + * @param total + * The total number of optimization phases. + */ +#define GUAC_DISPLAY_PLAN_END_PHASE(display, phase, n, total) \ + guac_timestamp phase_end = guac_timestamp_current(); \ + guac_client_log(display->client, GUAC_LOG_TRACE, "Render planning " \ + "phase %i/%i (%s): %ims", n, total, phase, \ + (int) (phase_end - phase_start)); \ + } while (0) + +void guac_display_end_frame(guac_display* display) { + guac_display_end_multiple_frames(display, 0); +} + +/** + * Callback for guac_client_foreach_user() which sends the current cursor + * position and button state to any given user except the user that moved the + * cursor last. + * + * @param data + * A pointer to the guac_display whose cursor state should be broadcast to + * all users except the user that moved the cursor last. + * + * @return + * Always NULL. + */ +static void* LFR_guac_display_broadcast_cursor_state(guac_user* user, void* data) { + + guac_display* display = (guac_display*) data; + + /* Send cursor state only if the user is not moving the cursor */ + if (user != display->last_frame.cursor_user) + guac_protocol_send_mouse(user->socket, + display->last_frame.cursor_x, display->last_frame.cursor_y, + display->last_frame.cursor_mask, display->last_frame.timestamp); + + return NULL; + +} + +/** + * Finalizes the current pending frame, storing that state as the copy of the + * last frame. All layer properties that have changed since the last frame will + * be sent out to connected clients. + * + * @param display + * The display whose pending frame should be finalized and persisted as the + * last frame. + * + * @return + * Non-zero if any layers within the pending frame had any changes + * whatsoever that needed to be sent as part of the frame, zero otherwise. + */ +static int PFW_LFW_guac_display_frame_complete(guac_display* display) { + + guac_client* client = display->client; + int retval = 0; + + display->last_frame.layers = display->pending_frame.layers; + guac_display_layer* current = display->pending_frame.layers; + while (current != NULL) { + + /* Skip processing any layers whose buffers have been replaced with + * NULL (this is intentionally allowed to ensure references to external + * buffers can be safely removed if necessary, even before guac_display + * is freed) */ + if (current->pending_frame.buffer == NULL) { + GUAC_ASSERT(current->pending_frame.buffer_is_external); + continue; + } + + /* Always resize the last_frame buffer to match the pending_frame prior + * to copying over any changes (this is particularly important given + * that the pending_frame buffer can be replaced with an external + * buffer). Since this involves copying over all data from the + * pending frame, we can skip the later pending frame copy based on + * whether the pending frame is dirty. */ + if (current->last_frame.buffer_stride != current->pending_frame.buffer_stride + || current->last_frame.buffer_width != current->pending_frame.buffer_width + || current->last_frame.buffer_height != current->pending_frame.buffer_height) { + + size_t buffer_size = guac_mem_ckd_mul_or_die(current->pending_frame.buffer_height, + current->pending_frame.buffer_stride); + + guac_mem_free(current->last_frame.buffer); + current->last_frame.buffer = guac_mem_zalloc(buffer_size); + memcpy(current->last_frame.buffer, current->pending_frame.buffer, buffer_size); + + current->last_frame.buffer_stride = current->pending_frame.buffer_stride; + current->last_frame.buffer_width = current->pending_frame.buffer_width; + current->last_frame.buffer_height = current->pending_frame.buffer_height; + + current->last_frame.dirty = current->pending_frame.dirty; + current->pending_frame.dirty = (guac_rect) { 0 }; + + retval = 1; + + } + + /* Copy over pending frame contents if actually changed (this is not + * necessary if the last_frame buffer was resized to match + * pending_frame, as a copy from pending_frame to last_frame is + * inherently part of that) */ + else if (!guac_rect_is_empty(¤t->pending_frame.dirty)) { + + unsigned char* pending_frame = current->pending_frame.buffer; + unsigned char* last_frame = current->last_frame.buffer; + size_t row_length = guac_mem_ckd_mul_or_die(current->pending_frame.width, 4); + + for (int y = 0; y < current->pending_frame.height; y++) { + memcpy(last_frame, pending_frame, row_length); + last_frame += current->last_frame.buffer_stride; + pending_frame += current->pending_frame.buffer_stride; + } + + current->last_frame.dirty = current->pending_frame.dirty; + current->pending_frame.dirty = (guac_rect) { 0 }; + + retval = 1; + + } + + /* Commit any change in layer size */ + if (current->pending_frame.width != current->last_frame.width + || current->pending_frame.height != current->last_frame.height) { + + guac_protocol_send_size(client->socket, current->layer, + current->pending_frame.width, current->pending_frame.height); + + current->last_frame.width = current->pending_frame.width; + current->last_frame.height = current->pending_frame.height; + + retval = 1; + + } + + /* Commit any change in layer opacity */ + if (current->pending_frame.opacity != current->last_frame.opacity) { + + guac_protocol_send_shade(client->socket, current->layer, + current->pending_frame.opacity); + + current->last_frame.opacity = current->pending_frame.opacity; + + retval = 1; + + } + + /* Commit any change in layer location/hierarchy */ + if (current->pending_frame.x != current->last_frame.x + || current->pending_frame.y != current->last_frame.y + || current->pending_frame.z != current->last_frame.z + || current->pending_frame.parent != current->last_frame.parent) { + + guac_protocol_send_move(client->socket, current->layer, + current->pending_frame.parent, + current->pending_frame.x, + current->pending_frame.y, + current->pending_frame.z); + + current->last_frame.x = current->pending_frame.x; + current->last_frame.y = current->pending_frame.y; + current->last_frame.z = current->pending_frame.z; + current->last_frame.parent = current->pending_frame.parent; + + retval = 1; + + } + + /* Commit any change in layer multitouch support */ + if (current->pending_frame.touches != current->last_frame.touches) { + guac_protocol_send_set_int(client->socket, current->layer, + GUAC_PROTOCOL_LAYER_PARAMETER_MULTI_TOUCH, + current->pending_frame.touches); + current->last_frame.touches = current->pending_frame.touches; + } + + /* Commit any hinting regarding scroll/copy optimization (NOTE: While + * this value is copied for consistency, it will already have taken + * effect in the context of the pending frame due to the scroll/copy + * optimization pass having occurred prior to the call to this + * function) */ + current->last_frame.search_for_copies = current->pending_frame.search_for_copies; + current->pending_frame.search_for_copies = 0; + + /* Commit any change in lossless setting (no need to synchronize this + * to the client - it affects only how last_frame is interpreted) */ + current->last_frame.lossless = current->pending_frame.lossless; + + /* Duplicate layers from pending frame to last frame */ + current->last_frame.prev = current->pending_frame.prev; + current->last_frame.next = current->pending_frame.next; + current = current->pending_frame.next; + + } + + display->last_frame.timestamp = display->pending_frame.timestamp; + display->last_frame.frames = display->pending_frame.frames; + + display->pending_frame.frames = 0; + display->pending_frame_dirty_excluding_mouse = 0; + + /* Commit cursor hotspot */ + display->last_frame.cursor_hotspot_x = display->pending_frame.cursor_hotspot_x; + display->last_frame.cursor_hotspot_y = display->pending_frame.cursor_hotspot_y; + + /* Commit mouse cursor location and notify all other users of change in + * cursor state */ + if (display->pending_frame.cursor_x != display->last_frame.cursor_x + || display->pending_frame.cursor_y != display->last_frame.cursor_y + || display->pending_frame.cursor_mask != display->last_frame.cursor_mask) { + + display->last_frame.cursor_user = display->pending_frame.cursor_user; + display->last_frame.cursor_x = display->pending_frame.cursor_x; + display->last_frame.cursor_y = display->pending_frame.cursor_y; + display->last_frame.cursor_mask = display->pending_frame.cursor_mask; + guac_client_foreach_user(client, LFR_guac_display_broadcast_cursor_state, display); + + retval = 1; + + } + + return retval; + +} + +void guac_display_end_mouse_frame(guac_display* display) { + + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + if (!display->pending_frame_dirty_excluding_mouse) + guac_display_end_multiple_frames(display, 0); + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_end_multiple_frames(guac_display* display, int frames) { + + guac_display_plan* plan = NULL; + + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + display->pending_frame.frames += frames; + + /* Defer rendering of further frames until after any in-progress frame has + * finished. Graphical changes will meanwhile continue being accumulated in + * the pending frame. */ + + guac_fifo_lock(&display->ops); + int defer_frame = display->frame_deferred = + (display->ops.state.value & GUAC_FIFO_STATE_NONEMPTY) || display->active_workers; + guac_fifo_unlock(&display->ops); + + if (defer_frame) + goto finished_with_pending_frame_lock; + + guac_rwlock_acquire_write_lock(&display->last_frame.lock); + + /* PASS 0: Create naive plan, identify minimal dirty rects by comparing the + * changes between the pending and last frames. + * + * This plan will contain operations covering only the minimal parts of the + * display that have changed, but is naive in the sense that it only + * produces draw operations covering 64x64 cells. There is room for + * optimization of those operations, which will be performed by further + * passes. */ + GUAC_DISPLAY_PLAN_BEGIN_PHASE(); + plan = PFW_LFR_guac_display_plan_create(display); + GUAC_DISPLAY_PLAN_END_PHASE(display, "draft", 1, 5); + + if (plan != NULL) { + + display->pending_frame.timestamp = plan->frame_end; + + /* PASS 1: Identify draw operations that only apply a single color, and + * replace those operations with simple rectangle draws. */ + GUAC_DISPLAY_PLAN_BEGIN_PHASE(); + PFR_guac_display_plan_rewrite_as_rects(plan); + GUAC_DISPLAY_PLAN_END_PHASE(display, "rects", 2, 5); + + /* PASS 2 (and 3): Index all modified cells by their graphical contents and + * search the previous frame for occurrences of the same content. Where any + * draws could instead be represented as copies from the previous frame, do + * so instead of sending new image data. */ + GUAC_DISPLAY_PLAN_BEGIN_PHASE(); + PFR_guac_display_plan_index_dirty_cells(plan); + PFR_LFR_guac_display_plan_rewrite_as_copies(plan); + GUAC_DISPLAY_PLAN_END_PHASE(display, "search", 3, 5); + + /* PASS 4 (and 5): Combine adjacent updates in horizontal and vertical + * directions where doing so would be more efficient. The goal of these + * passes is to ensure that graphics can be encoded and decoded + * efficiently, without defeating the parralelism provided by providing the + * worker threads with many smaller operations. */ + GUAC_DISPLAY_PLAN_BEGIN_PHASE(); + PFW_guac_display_plan_combine_horizontally(plan); + PFW_guac_display_plan_combine_vertically(plan); + GUAC_DISPLAY_PLAN_END_PHASE(display, "combine", 4, 5); + + } + + /* + * With all optimizations now performed, finalize the pending frame. This + * sets the worker threads in motion and frees up the pending frame + * surfaces for writing. Drawing to the next pending frame can now occur + * without disturbing the encoding performed by the worker threads. + */ + + int frame_nonempty; + + GUAC_DISPLAY_PLAN_BEGIN_PHASE(); + frame_nonempty = PFW_LFW_guac_display_frame_complete(display); + GUAC_DISPLAY_PLAN_END_PHASE(display, "commit", 5, 5); + + /* Not all frames are graphical. If we end up with a frame containing + * nothing but layer property changes, then we must still send a frame + * boundary even though there is no display plan to optimize. */ + if (plan == NULL && frame_nonempty) { + guac_display_plan_operation end_frame_op = { + .type = GUAC_DISPLAY_PLAN_END_FRAME + }; + guac_fifo_enqueue(&display->ops, &end_frame_op); + } + + guac_rwlock_release_lock(&display->last_frame.lock); + +finished_with_pending_frame_lock: + guac_rwlock_release_lock(&display->pending_frame.lock); + + if (plan != NULL) { + guac_display_plan_apply(plan); + guac_display_plan_free(plan); + } + +} diff --git a/src/libguac/display-layer-list.c b/src/libguac/display-layer-list.c new file mode 100644 index 000000000..f80aa3a4e --- /dev/null +++ b/src/libguac/display-layer-list.c @@ -0,0 +1,396 @@ +/* + * 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 "display-priv.h" +#include "guacamole/assert.h" +#include "guacamole/client.h" +#include "guacamole/display.h" +#include "guacamole/layer.h" +#include "guacamole/mem.h" +#include "guacamole/rwlock.h" + +#include +#include +#include + +/** + * Performs a bulk copy of image data from a source buffer to a destination + * buffer. The two buffers need not match in size and stride. If the + * destination is smaller than the desired source, the source dimensions will + * be adjusted to fit the available space. + * + * @param dst + * A pointer to the first byte of image data in the destination buffer. + * + * @param dst_stride + * The number of bytes in each row of image data in the destination buffer. + * + * @param dst_width + * The width of the destination buffer relative to the provided first byte, + * in pixels. + * + * @param dst_height + * The height of the destination buffer relative to the provided first byte, + * in pixels. + * + * @param src + * A pointer to the first byte of image data in the source buffer. + * + * @param src_stride + * The number of bytes in each row of image data in the source buffer. + * + * @param src_width + * The width of the source buffer relative to the provided first byte, in + * pixels. If this value is larger than dst_width, it will be adjusted to + * fit the available space. + * + * @param src_height + * The height of the source buffer relative to the provided first byte, in + * pixels. If this value is larger than dst_height, it will be adjusted to + * fit the available space. + * + * @param pixel_size + * The size of each pixel of image data, in bytes. The size of each pixel + * in both the destination and source buffers must be identical. + */ +static void guac_imgcpy(void* dst, size_t dst_stride, int dst_width, int dst_height, + void* src, size_t src_stride, int src_width, int src_height, + size_t pixel_size) { + + int width = dst_width; + int height = dst_height; + + if (src_width < width) width = src_width; + if (src_height < height) height = src_height; + + GUAC_ASSERT(width >= 0); + GUAC_ASSERT(height >= 0); + + size_t length = guac_mem_ckd_mul_or_die(width, pixel_size); + + for (size_t i = 0; i < height; i++) { + memcpy(dst, src, length); + dst = ((char*) dst) + dst_stride; + src = ((char*) src) + src_stride; + } + +} + +/** + * Resizes the layer represented by the given pair of layer states to the given + * dimensions, allocating a larger underlying image buffer if necessary. If no + * image buffer has yet been allocated, an image buffer large enough to hold + * the given dimensions will be automatically allocated. + * + * This function DOES NOT resize the pending cells array, which is not stored + * on the guac_display_layer_state. When resizing a layer, the pending cells + * array must be separately resized with a call to + * PFW_guac_display_layer_pending_frame_cells_resize(). + * + * @param last_frame + * The guac_display_layer_state representing the state of the layer at the + * end of the last frame sent to connected clients. + * + * @param pending_frame + * The guac_display_layer_state representing the current pending state of + * the layer for the upcoming frame to be eventually sent to connected + * clients. + * + * @param width + * The new width, in pixels. + * + * @param height + * The new height, in pixels. + */ +static void XFW_guac_display_layer_buffer_resize(guac_display_layer_state* frame_state, + int width, int height) { + + /* We should never be trying to resize an externally-maintained buffer */ + GUAC_ASSERT(!frame_state->buffer_is_external); + + /* Round up to nearest multiple of resize factor */ + width = ((width + GUAC_DISPLAY_RESIZE_FACTOR - 1) / GUAC_DISPLAY_RESIZE_FACTOR) * GUAC_DISPLAY_RESIZE_FACTOR; + height = ((height + GUAC_DISPLAY_RESIZE_FACTOR - 1) / GUAC_DISPLAY_RESIZE_FACTOR) * GUAC_DISPLAY_RESIZE_FACTOR; + + /* Do nothing if size isn't actually changing */ + if (width == frame_state->buffer_width + && height == frame_state->buffer_height) + return; + + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); + unsigned char* buffer = guac_mem_zalloc(height, stride); + + /* Copy over data from old shared buffer, if that data exists and is + * relevant */ + + if (frame_state->buffer != NULL) { + + guac_imgcpy( + + /* Copy to newly-allocated frame buffer ... */ + buffer, stride, + width, height, + + /* ... from old frame buffer. */ + frame_state->buffer, frame_state->buffer_stride, + frame_state->buffer_width, frame_state->buffer_height, + + /* All pixels are 32-bit */ + GUAC_DISPLAY_LAYER_RAW_BPP); + + guac_mem_free(frame_state->buffer); + + } + + frame_state->buffer = buffer; + frame_state->buffer_width = width; + frame_state->buffer_height = height; + frame_state->buffer_stride = stride; + +} + +/** + * Fully initializes the last and pending frame states for a newly-allocated + * layer, including its underlying image buffers. + * + * @param last_frame + * The guac_display_layer_state representing the state of the layer at the + * end of the last frame sent to connected clients. + * + * @param pending_frame + * The guac_display_layer_state representing the current pending state of + * the layer for the upcoming frame to be eventually sent to connected + * clients. + */ +static void PFW_LFW_guac_display_layer_state_init(guac_display_layer_state* last_frame, + guac_display_layer_state* pending_frame) { + + last_frame->width = pending_frame->width = GUAC_DISPLAY_RESIZE_FACTOR; + last_frame->height = pending_frame->height = GUAC_DISPLAY_RESIZE_FACTOR; + last_frame->opacity = pending_frame->opacity = 0xFF; + last_frame->parent = pending_frame->parent = GUAC_DEFAULT_LAYER; + + XFW_guac_display_layer_buffer_resize(last_frame, + last_frame->width, last_frame->height); + + XFW_guac_display_layer_buffer_resize(pending_frame, + pending_frame->width, pending_frame->height); + +} + +/** + * Resizes the pending_frame_cells array of the given layer to the given + * dimensions. + * + * @param layer + * The layer whose pending_frame_cells array should be resized. + * + * @param width + * The new width, in pixels. + * + * @param height + * The new height, in pixels. + */ +static void PFW_guac_display_layer_pending_frame_cells_resize(guac_display_layer* layer, + int width, int height) { + + int new_pending_frame_cells_width = GUAC_DISPLAY_CELL_DIMENSION(width); + int new_pending_frame_cells_height = GUAC_DISPLAY_CELL_DIMENSION(height); + + /* Do nothing if size isn't actually changing */ + if (new_pending_frame_cells_width == layer->pending_frame_cells_width + && new_pending_frame_cells_height == layer->pending_frame_cells_height) + return; + + guac_display_layer_cell* new_pending_frame_cells = guac_mem_zalloc(sizeof(guac_display_layer_cell), + new_pending_frame_cells_width, new_pending_frame_cells_height); + + /* Copy existing cells over to new memory if present */ + if (layer->pending_frame_cells != NULL) { + + size_t new_stride = guac_mem_ckd_mul_or_die(new_pending_frame_cells_width, sizeof(guac_display_layer_cell)); + size_t old_stride = guac_mem_ckd_mul_or_die(layer->pending_frame_cells_width, sizeof(guac_display_layer_cell)); + + guac_imgcpy( + + /* Copy to newly-allocated pending frame cells ... */ + new_pending_frame_cells, new_stride, + new_pending_frame_cells_width, new_pending_frame_cells_height, + + /* ... from old pending frame cells. */ + layer->pending_frame_cells, old_stride, + layer->pending_frame_cells_width, layer->pending_frame_cells_height, + + /* All "pixels" are guac_display_layer_cell structures */ + sizeof(guac_display_layer_cell)); + + } + + guac_mem_free(layer->pending_frame_cells); + layer->pending_frame_cells = new_pending_frame_cells; + layer->pending_frame_cells_width = new_pending_frame_cells_width; + layer->pending_frame_cells_height = new_pending_frame_cells_height; + +} + +guac_display_layer* guac_display_add_layer(guac_display* display, guac_layer* layer, int opaque) { + + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + /* Init core layer members */ + guac_display_layer* display_layer = guac_mem_zalloc(sizeof(guac_display_layer)); + display_layer->display = display; + display_layer->layer = layer; + display_layer->opaque = opaque; + + /* Init tracking of pending and last frames (NOTE: We need not acquire the + * display-wide last_frame.lock here as this new layer will not actually be + * part of the last frame layer list until the pending frame is flushed) */ + PFW_LFW_guac_display_layer_state_init(&display_layer->last_frame, &display_layer->pending_frame); + display_layer->last_frame_buffer = guac_client_alloc_buffer(display->client); + PFW_guac_display_layer_pending_frame_cells_resize(display_layer, + display_layer->pending_frame.width, + display_layer->pending_frame.height); + + /* Insert list element as the new head */ + guac_display_layer* old_head = display->pending_frame.layers; + display_layer->pending_frame.prev = NULL; + display_layer->pending_frame.next = old_head; + display->pending_frame.layers = display_layer; + + /* Update old head to point to new element, if it existed */ + if (old_head != NULL) + old_head->pending_frame.prev = display_layer; + + guac_rwlock_release_lock(&display->pending_frame.lock); + + return display_layer; + +} + +void guac_display_remove_layer(guac_display_layer* display_layer) { + + guac_display* display = display_layer->display; + + /* + * Remove layer from pending frame + */ + + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + /* Update previous element, if it exists */ + if (display_layer->pending_frame.prev != NULL) + display_layer->pending_frame.prev->pending_frame.next = display_layer->pending_frame.next; + + /* If there is no previous element, then this element is the list head if + * the list has any elements at all. Update the list head accordingly. */ + else if (display->pending_frame.layers != NULL) { + GUAC_ASSERT(display->pending_frame.layers == display_layer); + display->pending_frame.layers = display_layer->pending_frame.next; + } + + /* Update next element, if it exists */ + if (display_layer->pending_frame.next != NULL) + display_layer->pending_frame.next->pending_frame.prev = display_layer->pending_frame.prev; + + guac_rwlock_release_lock(&display->pending_frame.lock); + + /* + * Remove layer from last frame + */ + + guac_rwlock_acquire_write_lock(&display->last_frame.lock); + + /* Update previous element, if it exists */ + if (display_layer->last_frame.prev != NULL) + display_layer->last_frame.prev->last_frame.next = display_layer->last_frame.next; + + /* If there is no previous element, then this element is the list head if + * the list has any elements at all. Update the list head accordingly. */ + else if (display->last_frame.layers != NULL) { + GUAC_ASSERT(display->last_frame.layers == display_layer); + display->last_frame.layers = display_layer->last_frame.next; + } + + /* Update next element, if it exists */ + if (display_layer->last_frame.next != NULL) + display_layer->last_frame.next->last_frame.prev = display_layer->last_frame.prev; + + guac_rwlock_release_lock(&display->last_frame.lock); + + /* + * Layer has now been removed from both pending and last frame lists and + * can be safely freed + */ + + guac_client* client = display->client; + guac_client_free_buffer(client, display_layer->last_frame_buffer); + + /* Release any Cairo resources */ + guac_display_layer_cairo_context* cairo_context = &(display_layer->pending_frame_cairo_context); + if (cairo_context->surface != NULL) { + + cairo_surface_destroy(cairo_context->surface); + cairo_context->surface = NULL; + + cairo_destroy(cairo_context->cairo); + cairo_context->cairo = NULL; + + } + + /* Free memory for underlying image surface and change tracking cells. Note + * that we do NOT free the associated memory for the pending frame if it + * was replaced with an external buffer. */ + + if (!display_layer->pending_frame.buffer_is_external) + guac_mem_free(display_layer->pending_frame.buffer); + + guac_mem_free(display_layer->last_frame.buffer); + guac_mem_free(display_layer->pending_frame_cells); + + guac_mem_free(display_layer); + +} + +void PFW_guac_display_layer_resize(guac_display_layer* layer, int width, int height) { + + /* Flush and destroy any cached Cairo context */ + guac_display_layer_cairo_context* cairo_context = &(layer->pending_frame_cairo_context); + if (cairo_context->surface != NULL) { + + cairo_surface_flush(cairo_context->surface); + cairo_surface_destroy(cairo_context->surface); + cairo_destroy(cairo_context->cairo); + + cairo_context->surface = NULL; + cairo_context->cairo = NULL; + + } + + /* Skip resizing underlying buffer if it's the caller that's responsible + * for resizing the buffer */ + if (!layer->pending_frame.buffer_is_external) + XFW_guac_display_layer_buffer_resize(&layer->pending_frame, width, height); + + PFW_guac_display_layer_pending_frame_cells_resize(layer, width, height); + + layer->pending_frame.width = width; + layer->pending_frame.height = height; + +} diff --git a/src/libguac/display-layer.c b/src/libguac/display-layer.c new file mode 100644 index 000000000..2c983a157 --- /dev/null +++ b/src/libguac/display-layer.c @@ -0,0 +1,324 @@ +/* + * 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 "display-priv.h" +#include "guacamole/assert.h" +#include "guacamole/display.h" +#include "guacamole/rect.h" +#include "guacamole/rwlock.h" + +#include +#include +#include + +/** + * Notifies the display associated with the given layer that the given layer + * has been modified in some way for the current pending frame. If the layer is + * not the cursor layer, the pending_frame_dirty_excluding_mouse flag of the + * display is updated accordingly. + * + * @param layer + * The layer that was modified. + */ +static void PFW_guac_display_layer_touch(guac_display_layer* layer) { + + guac_display* display = layer->display; + + if (layer != display->cursor_buffer) + display->pending_frame_dirty_excluding_mouse = 1; + +} + +void guac_display_layer_get_bounds(guac_display_layer* layer, guac_rect* bounds) { + + guac_display* display = layer->display; + guac_rwlock_acquire_read_lock(&display->pending_frame.lock); + + *bounds = (guac_rect) { + .left = 0, + .top = 0, + .right = layer->pending_frame.width, + .bottom = layer->pending_frame.height + }; + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_layer_move(guac_display_layer* layer, int x, int y) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + layer->pending_frame.x = x; + layer->pending_frame.y = y; + PFW_guac_display_layer_touch(layer); + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_layer_stack(guac_display_layer* layer, int z) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + layer->pending_frame.z = z; + PFW_guac_display_layer_touch(layer); + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_layer_set_parent(guac_display_layer* layer, const guac_display_layer* parent) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + layer->pending_frame.parent = parent->layer; + PFW_guac_display_layer_touch(layer); + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_layer_set_opacity(guac_display_layer* layer, int opacity) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + layer->pending_frame.opacity = opacity; + PFW_guac_display_layer_touch(layer); + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_layer_set_lossless(guac_display_layer* layer, int lossless) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + layer->pending_frame.lossless = lossless; + PFW_guac_display_layer_touch(layer); + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_layer_set_multitouch(guac_display_layer* layer, int touches) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + layer->pending_frame.touches = touches; + PFW_guac_display_layer_touch(layer); + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_layer_resize(guac_display_layer* layer, int width, int height) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + PFW_guac_display_layer_resize(layer, width, height); + PFW_guac_display_layer_touch(layer); + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +void guac_display_layer_raw_context_set(guac_display_layer_raw_context* context, + const guac_rect* dst, uint32_t color) { + + size_t dst_stride = context->stride; + unsigned char* restrict dst_buffer = GUAC_DISPLAY_LAYER_RAW_BUFFER(context, *dst); + + for (int dy = dst->top; dy < dst->bottom; dy++) { + + uint32_t* dst_pixel = (uint32_t*) dst_buffer; + dst_buffer += dst_stride; + + for (int dx = dst->left; dx < dst->right; dx++) + *(dst_pixel++) = color; + + } + + guac_rect_extend(&(context->dirty), dst); + +} + +void guac_display_layer_raw_context_put(guac_display_layer_raw_context* context, + const guac_rect* dst, const void* restrict buffer, size_t stride) { + + size_t dst_stride = context->stride; + unsigned char* restrict dst_buffer = GUAC_DISPLAY_LAYER_RAW_BUFFER(context, *dst); + const unsigned char* restrict src_buffer = (const unsigned char*) buffer; + + size_t copy_length = guac_mem_ckd_mul_or_die(guac_rect_width(dst), + GUAC_DISPLAY_LAYER_RAW_BPP); + + for (int dy = dst->top; dy < dst->bottom; dy++) { + memcpy(dst_buffer, src_buffer, copy_length); + dst_buffer += dst_stride; + src_buffer += stride; + } + + guac_rect_extend(&(context->dirty), dst); + +} + +guac_display_layer_raw_context* guac_display_layer_open_raw(guac_display_layer* layer) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + /* Flush any outstanding Cairo operations before directly accessing buffer */ + guac_display_layer_cairo_context* cairo_context = &(layer->pending_frame_cairo_context); + if (cairo_context->surface != NULL) + cairo_surface_flush(cairo_context->surface); + + layer->pending_frame_raw_context = (guac_display_layer_raw_context) { + .buffer = layer->pending_frame.buffer, + .stride = layer->pending_frame.buffer_stride, + .dirty = { 0 }, + .hint_from = layer, + .bounds = { + .left = 0, + .top = 0, + .right = layer->pending_frame.buffer_width, + .bottom = layer->pending_frame.buffer_height + } + }; + + return &layer->pending_frame_raw_context; + +} + +void guac_display_layer_close_raw(guac_display_layer* layer, guac_display_layer_raw_context* context) { + + guac_display* display = layer->display; + + /* Replace buffer if requested with an external buffer. This intentionally + * falls through to the following buffer_is_external check to update the + * buffer details. */ + if (context->buffer != layer->pending_frame.buffer + && !layer->pending_frame.buffer_is_external) { + guac_mem_free(layer->pending_frame.buffer); + layer->pending_frame.buffer_is_external = 1; + } + + /* The details covering the structure of the buffer and the dimensions of + * the layer must be copied from the context if the buffer is external + * (there is no other way to resize a layer with an external buffer) */ + if (layer->pending_frame.buffer_is_external) { + + int width = guac_rect_width(&context->bounds); + if (width > GUAC_DISPLAY_MAX_WIDTH) + width = GUAC_DISPLAY_MAX_WIDTH; + + int height = guac_rect_height(&context->bounds); + if (height > GUAC_DISPLAY_MAX_HEIGHT) + height = GUAC_DISPLAY_MAX_HEIGHT; + + /* Release any Cairo surface that was created around the external + * buffer, in case the details of the buffer have now changed */ + guac_display_layer_cairo_context* cairo_context = &(layer->pending_frame_cairo_context); + if (cairo_context->surface != NULL) { + cairo_surface_destroy(cairo_context->surface); + cairo_context->surface = NULL; + } + + layer->pending_frame.buffer = context->buffer; + layer->pending_frame.buffer_width = width; + layer->pending_frame.buffer_height = height; + layer->pending_frame.buffer_stride = context->stride; + + layer->pending_frame.width = layer->pending_frame.buffer_width; + layer->pending_frame.height = layer->pending_frame.buffer_height; + + } + + guac_rect_extend(&layer->pending_frame.dirty, &context->dirty); + PFW_guac_display_layer_touch(layer); + + /* Apply any hinting regarding scroll/copy optimization */ + if (context->hint_from != NULL) + context->hint_from->pending_frame.search_for_copies = 1; + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} + +guac_display_layer_cairo_context* guac_display_layer_open_cairo(guac_display_layer* layer) { + + guac_display* display = layer->display; + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + /* It is intentionally allowed that the pending frame buffer can be + * replaced with NULL to ensure that references to external buffers can be + * removed prior to guac_display being freed. If the buffer has been + * manually replaced with NULL, further use of that buffer via Cairo + * contexts is not safe nor allowed. */ + GUAC_ASSERT(layer->pending_frame.buffer != NULL); + + guac_display_layer_cairo_context* context = &(layer->pending_frame_cairo_context); + + context->dirty = (guac_rect) { 0 }; + context->hint_from = layer; + context->bounds = (guac_rect) { + .left = 0, + .top = 0, + .right = layer->pending_frame.buffer_width, + .bottom = layer->pending_frame.buffer_height + }; + + if (context->surface == NULL) { + + context->surface = cairo_image_surface_create_for_data( + layer->pending_frame.buffer, + layer->opaque ? CAIRO_FORMAT_RGB24 : CAIRO_FORMAT_ARGB32, + layer->pending_frame.buffer_width, + layer->pending_frame.buffer_height, + layer->pending_frame.buffer_stride); + + context->cairo = cairo_create(context->surface); + + } + + return context; + +} + +void guac_display_layer_close_cairo(guac_display_layer* layer, guac_display_layer_cairo_context* context) { + + guac_display* display = layer->display; + + guac_rect_extend(&layer->pending_frame.dirty, &context->dirty); + PFW_guac_display_layer_touch(layer); + + /* Apply any hinting regarding scroll/copy optimization */ + if (context->hint_from != NULL) + context->hint_from->pending_frame.search_for_copies = 1; + + guac_rwlock_release_lock(&display->pending_frame.lock); + +} diff --git a/src/libguac/display-plan-combine.c b/src/libguac/display-plan-combine.c new file mode 100644 index 000000000..5e663c206 --- /dev/null +++ b/src/libguac/display-plan-combine.c @@ -0,0 +1,334 @@ +/* + * 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 "display-plan.h" +#include "display-priv.h" +#include "guacamole/display.h" +#include "guacamole/rect.h" + +/** + * Returns whether the given rectangle crosses the boundaries of any two + * adjacent cells in a grid, where each cell in the grid is + * 2^GUAC_DISPLAY_MAX_COMBINED_SIZE pixels on each side. + * + * This function exists because combination of adjacent image updates is + * intentionally limited to a certain size in order to favor parallelism. + * Greedily combining in the horizontal direction works, but in practice tends + * to produce a vertical series of strips that are offset from each other to + * the point that they cannot be further combined. Anchoring combined image + * updates to a grid helps prevent ths. + * + * @param rect + * The rectangle to test. + * + * @return + * Non-zero if the rectangle crosses the boundary of any adjacent pair of + * cells in a grid, where each cell is 2^GUAC_DISPLAY_MAX_COMBINED_SIZE + * pixels on each side, zero otherwise. + */ +static int guac_display_plan_rect_crosses_boundary(const guac_rect* rect) { + + /* A particular rectangle crosses a grid boundary if and only if expanding + * that rectangle to fit the grid would mean increasing the size of that + * rectangle beyond a single grid cell */ + + guac_rect rect_copy = *rect; + guac_rect_align(&rect_copy, GUAC_DISPLAY_MAX_COMBINED_SIZE); + + const int max_size_pixels = 1 << GUAC_DISPLAY_MAX_COMBINED_SIZE; + return guac_rect_width(&rect_copy) > max_size_pixels + || guac_rect_height(&rect_copy) > max_size_pixels; + +} + +/** + * Returns whether the two rectangles are adjacent and share exactly one common + * edge. + * + * @param op_a + * One of the rectangles to compare. + * + * @param op_b + * The rectangle to compare op_a with. + * + * @return + * Non-zero if the rectangles are adjacent and share exactly one common + * edge, zero otherwise. + */ +static int guac_display_plan_has_common_edge(const guac_display_plan_operation* op_a, + const guac_display_plan_operation* op_b) { + + /* Two operations share a common edge if they are perfectly aligned + * vertically and have the same left/right or right/left edge */ + if (op_a->dest.top == op_b->dest.top + && op_a->dest.bottom == op_b->dest.bottom) { + + return op_a->dest.right == op_b->dest.left + || op_a->dest.left == op_b->dest.right; + + } + + /* Two operations share a common edge if they are perfectly aligned + * horizontally and have the same top/bottom or bottom/top edge */ + else if (op_a->dest.left == op_b->dest.left + && op_a->dest.right == op_b->dest.right) { + + return op_a->dest.top == op_b->dest.bottom + || op_a->dest.bottom == op_b->dest.top; + + } + + /* There are no other cases where two operations share a common edge */ + return 0; + +} + +/** + * Returns whether the given pair of operations should be combined into a + * single operation. + * + * @param op_a + * The first operation to check. + * + * @param op_b + * The second operation to check. + * + * @return + * Non-zero if the operations would be better represented as a single, + * combined operation, zero otherwise. + */ +static int guac_display_plan_should_combine(const guac_display_plan_operation* op_a, + const guac_display_plan_operation* op_b) { + + /* Operations can only be combined within the same layer */ + if (op_a->layer != op_b->layer) + return 0; + + /* Simulate combination */ + guac_rect combined = op_a->dest; + guac_rect_extend(&combined, &op_b->dest); + + /* Operations of the same type can be trivially unified under specific + * circumstances */ + if (op_a->type == op_b->type) { + switch (op_a->type) { + + /* Copy operations can be combined if they are perfectly adjacent + * (exactly share an edge) and copy from the same source layer in + * the same direction */ + case GUAC_DISPLAY_PLAN_OPERATION_COPY: + if (op_a->src.layer_rect.layer == op_b->src.layer_rect.layer + && guac_display_plan_has_common_edge(op_a, op_b)) { + + int delta_xa = op_a->dest.left - op_a->src.layer_rect.rect.left; + int delta_ya = op_a->dest.top - op_a->src.layer_rect.rect.top; + int delta_xb = op_b->dest.left - op_b->src.layer_rect.rect.left; + int delta_yb = op_b->dest.top - op_b->src.layer_rect.rect.top; + + return delta_xa == delta_xb + && delta_ya == delta_yb + && !guac_display_plan_rect_crosses_boundary(&combined); + + } + break; + + /* Rectangle-drawing operations can be combined if they are + * perfectly adjacent (exactly share an edge) and draw the same + * color */ + case GUAC_DISPLAY_PLAN_OPERATION_RECT: + return op_a->src.color == op_b->src.color + && guac_display_plan_has_common_edge(op_a, op_b) + && !guac_display_plan_rect_crosses_boundary(&combined); + + /* Image-drawing operations can be combined if doing so wouldn't + * exceed the size limits for images (we enforce size limits here + * to promote parallelism) */ + case GUAC_DISPLAY_PLAN_OPERATION_IMG: + return !guac_display_plan_rect_crosses_boundary(&combined); + + /* Other combinations require more complex logic... (see below) */ + default: + break; + + } + } + + /* Combine if result is still small */ + int combined_width = guac_rect_width(&combined); + int combined_height = guac_rect_height(&combined); + if (combined_width <= GUAC_DISPLAY_NEGLIGIBLE_WIDTH && combined_height <= GUAC_DISPLAY_NEGLIGIBLE_HEIGHT) + return 1; + + /* Estimate costs of the existing update, new update, and both combined */ + int cost_ab = GUAC_DISPLAY_BASE_COST + combined_width * combined_height; + int cost_a = GUAC_DISPLAY_BASE_COST + op_a->dirty_size; + int cost_b = GUAC_DISPLAY_BASE_COST + op_b->dirty_size; + + /* Reduce cost if no image data */ + if (op_a->type != GUAC_DISPLAY_PLAN_OPERATION_IMG) cost_a /= GUAC_DISPLAY_DATA_FACTOR; + if (op_b->type != GUAC_DISPLAY_PLAN_OPERATION_IMG) cost_b /= GUAC_DISPLAY_DATA_FACTOR; + + /* Combine if cost estimate shows benefit or the increase in cost is + * negligible */ + if ((cost_ab <= cost_b + cost_a) + || (cost_ab - cost_a <= cost_a / GUAC_DISPLAY_NEGLIGIBLE_INCREASE) + || (cost_ab - cost_b <= cost_b / GUAC_DISPLAY_NEGLIGIBLE_INCREASE)) + return 1; + + /* Otherwise, do not combine */ + return 0; + +} + +/** + * Combines the given pair of operations into a single operation if doing so is + * advantageous (results in an operation of lesser or negligibly-worse cost). + * + * @param op_a + * The first of the pair of operations to be combined. If they operations + * are combined, the combined operation will be stored here. + * + * @param op_b + * The second of the pair of operations to be combined, which may + * potentially be identical to the first. If the operations are combined, + * this operation will be updated to be a GUAC_DISPLAY_PLAN_OPERATION_NOP + * operation. + * + * @return + * Non-zero if the operations were combined, zero otherwise. + */ +static int guac_display_plan_combine_if_improved(guac_display_plan_operation* op_a, + guac_display_plan_operation* op_b) { + + if (op_a == op_b) + return 0; + + /* Combine any adjacent operations that match the combination criteria + * (combining produces a net lower cost) */ + if (guac_display_plan_should_combine(op_a, op_b)) { + + guac_rect_extend(&op_a->dest, &op_b->dest); + + /* Operations of different types can only be combined as images */ + if (op_a->type != op_b->type) + op_a->type = GUAC_DISPLAY_PLAN_OPERATION_IMG; + + /* When combining two copy operations, additionally combine their + * source rects (NOT just the destination rects) */ + else if (op_a->type == GUAC_DISPLAY_PLAN_OPERATION_COPY) + guac_rect_extend(&op_a->src.layer_rect.rect, &op_b->src.layer_rect.rect); + + op_a->dirty_size += op_b->dirty_size; + + if (op_b->last_frame > op_a->last_frame) + op_a->last_frame = op_b->last_frame; + + op_b->type = GUAC_DISPLAY_PLAN_OPERATION_NOP; + + return 1; + + } + + return 0; + +} + +void PFW_guac_display_plan_combine_horizontally(guac_display_plan* plan) { + + guac_display* display = plan->display; + guac_display_layer* current = display->pending_frame.layers; + while (current != NULL) { + + /* Process only layers that have been modified */ + if (!guac_rect_is_empty(¤t->pending_frame.dirty)) { + + /* Loop through all cells in left-to-right, top-to-bottom order, + * combining any operations that are combinable and horizontally + * adjacent. */ + + guac_display_layer_cell* cell = current->pending_frame_cells; + for (int y = 0; y < current->pending_frame_cells_height; y++) { + + guac_display_layer_cell* previous = cell++; + for (int x = 1; x < current->pending_frame_cells_width; x++) { + + /* Combine adjacent updates if doing so is advantageous */ + if (previous->related_op != NULL && cell->related_op != NULL + && guac_display_plan_combine_if_improved(previous->related_op, cell->related_op)) { + cell->related_op = previous->related_op; + } + + previous++; + cell++; + + } + } + + } + + current = current->pending_frame.next; + + } + +} + +void PFW_guac_display_plan_combine_vertically(guac_display_plan* plan) { + + guac_display* display = plan->display; + guac_display_layer* current = display->pending_frame.layers; + while (current != NULL) { + + /* Process only layers that have been modified */ + if (!guac_rect_is_empty(¤t->pending_frame.dirty)) { + + /* Loop through all cells in top-to-bottom, left-to-right order, + * combining any operations that are combinable and horizontally + * adjacent. */ + + guac_display_layer_cell* cell_col = current->pending_frame_cells; + for (int x = 0; x < current->pending_frame_cells_width; x++) { + + guac_display_layer_cell* previous = cell_col; + guac_display_layer_cell* cell = cell_col + current->pending_frame_cells_width; + + for (int y = 1; y < current->pending_frame_cells_height; y++) { + + /* Combine adjacent updates if doing so is advantageous */ + if (previous->related_op != NULL && cell->related_op != NULL + && guac_display_plan_has_common_edge(previous->related_op, cell->related_op) + && guac_display_plan_combine_if_improved(previous->related_op, cell->related_op)) { + cell->related_op = previous->related_op; + } + + previous += current->pending_frame_cells_width; + cell += current->pending_frame_cells_width; + + } + + cell_col++; + + } + + } + + current = current->pending_frame.next; + + } + +} diff --git a/src/libguac/display-plan-rect.c b/src/libguac/display-plan-rect.c new file mode 100644 index 000000000..c2a4196a2 --- /dev/null +++ b/src/libguac/display-plan-rect.c @@ -0,0 +1,240 @@ +/* + * 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 "display-plan.h" +#include "display-priv.h" +#include "guacamole/display.h" +#include "guacamole/mem.h" +#include "guacamole/rect.h" + +#include +#include + +/** + * Rounds the given value down to the nearest power of two. + * + * @param value + * The value to round. + * + * @return + * The power of two that is closest to the given value without exceeding + * that value. + */ +static size_t guac_display_plan_round_pot(size_t value) { + + if (value <= 2) + return value; + + size_t rounded = 1; + while (value >>= 1) + rounded <<= 1; + + return rounded; + +} + +/** + * Returns whether the given buffer consists entirely of the same 32-bit + * quantity (ie: a single ARGB pixel), repeated throughout the buffer. + * + * This function attempts to perform a fast comparison leveraging memcmp() to + * reduce the search space, rather than simply looping through each pixel one + * at a time. Basic benchmarks show this approach to be roughly twice as fast + * as a simple loop for arbitrary buffer lengths and four times as fast for + * buffer lengths that are powers of two. + * + * @param buffer + * The buffer to check. + * + * @param length + * The number of bytes in the buffer. + * + * @param color + * A pointer to a uint32_t to receive the value of the 32-bit quantity that + * is repeated, if applicable. + * + * @return + * Non-zero if the same 32-bit quantity is repeated throughout the buffer, + * zero otherwise. If the same value is indeed repeated throughout the + * buffer, that value is stored in the variable pointed to by the "color" + * pointer. If the value is not repeated, the variable pointed to by the + * "color" pointer is left untouched. + */ +static int guac_display_plan_is_single_color(const unsigned char* restrict buffer, + size_t length, uint32_t* restrict color) { + + /* It is vacuously true that all the 32-bit quantities in an empty buffer + * are the same */ + if (length == 0) { + *color = 0x00000000; + return 1; + } + + /* A single 32-bit value is the same as itself */ + if (length == 4) { + *color = ((const uint32_t*) buffer)[0]; + return 1; + } + + /* Simply directly compare if there are only two values */ + if (length == 8) { + uint32_t a = ((const uint32_t*) buffer)[0]; + uint32_t b = ((const uint32_t*) buffer)[1]; + if (a == b) { + *color = a; + return 1; + } + } + + /* For all other lengths, avoid comparing if finding a match is impossible. + * A buffer can consist entirely of the same 32-bit (4-byte) quantity + * repeated throughout the buffer only if that buffer's length is a + * multiple of 4. */ + if ((length % 4) != 0) + return 0; + + /* A buffer consists entirely of the same 32-bit quantity repeated + * throughout if (1) the two halves of the buffer are the same and (2) one + * of those halves is known to consist entirely of the same 32-bit quantity + * repeated throughout. */ + + size_t pot_length = guac_display_plan_round_pot(guac_mem_ckd_sub_or_die(length, 1)); + size_t remaining_length = guac_mem_ckd_sub_or_die(length, pot_length); + + /* Easiest recursive case: the buffer is already a power of two and can be + * split into two very easy-to-compare halves */ + if (pot_length == remaining_length) { + return !memcmp(buffer, buffer + pot_length, pot_length) + && guac_display_plan_is_single_color(buffer, pot_length, color); + } + + /* For buffers that can't be split into two power-of-two halves, decide + * based on one easy power-of-two case and one not-so-easy case of whatever + * remains */ + uint32_t color_a = 0, color_b = 0; + if (guac_display_plan_is_single_color(buffer, pot_length, &color_a) + && guac_display_plan_is_single_color(buffer + pot_length, remaining_length, &color_b) + && color_a == color_b) { + + *color = color_a; + return 1; + + } + + return 0; + +} + +/** + * Returns whether the given rectangle within given buffer consists entirely of + * the same 32-bit quantity (ie: a single ARGB pixel), repeated throughout the + * rectangular region. + * + * This function attempts to perform a fast comparison leveraging memcmp() to + * reduce the search space, rather than simply looping through each pixel one + * at a time. Basic benchmarks show this approach to be roughly twice as fast + * as a simple loop for arbitrary buffer lengths and four times as fast for + * buffer lengths that are powers of two. + * + * @param buffer + * The buffer to check. + * + * @param stride + * The number of bytes in each row of image data within the buffer. + * + * @param rect + * The rectangle representing the region to be checked within the buffer. + * + * @param color + * A pointer to a uint32_t to receive the value of the 32-bit quantity that + * is repeated, if applicable. + * + * @return + * Non-zero if the same 32-bit quantity is repeated throughout the + * rectangular region, zero otherwise. If the same value is indeed repeated + * throughout the rectangle, that value is stored in the variable pointed + * to by the "color" pointer. If the value is not repeated, the variable + * pointed to by the "color" pointer is left untouched. + */ +static int guac_display_plan_is_rect_single_color(const unsigned char* restrict buffer, + size_t stride, const guac_rect* restrict rect, uint32_t* restrict color) { + + size_t row_length = guac_mem_ckd_mul_or_die(guac_rect_width(rect), GUAC_DISPLAY_LAYER_RAW_BPP); + buffer = GUAC_RECT_CONST_BUFFER(*rect, buffer, stride, GUAC_DISPLAY_LAYER_RAW_BPP); + + /* Verify that the first row consists of a single color */ + uint32_t first_color = 0x00000000; + if (!guac_display_plan_is_single_color(buffer, row_length, &first_color)) + return 0; + + /* The whole rectangle consists of a single color if each row is identical + * and it's already known that one of those rows consists of the a single + * color */ + const unsigned char* previous = buffer; + for (int y = rect->top + 1; y < rect->bottom; y++) { + + const unsigned char* current = previous + stride; + if (memcmp(previous, current, row_length)) + return 0; + + previous = current; + + } + + *color = first_color; + return 1; + +} + +void PFR_guac_display_plan_rewrite_as_rects(guac_display_plan* plan) { + + uint32_t color = 0x00000000; + + guac_display_plan_operation* op = plan->ops; + for (int i = 0; i < plan->length; i++) { + + if (op->type == GUAC_DISPLAY_PLAN_OPERATION_IMG) { + + guac_display_layer* layer = op->layer; + size_t stride = layer->pending_frame.buffer_stride; + const unsigned char* buffer = layer->pending_frame.buffer; + + /* NOTE: Processing of operations referring to layers whose buffers + * have been replaced with NULL is intentionally allowed to ensure + * references to external buffers can be safely removed if + * necessary, even before guac_display is freed */ + + if (buffer != NULL && guac_display_plan_is_rect_single_color(buffer, stride, &op->dest, &color)) { + + /* Ignore alpha channel for opaque layers */ + if (layer->opaque) + color |= 0xFF000000; + + op->type = GUAC_DISPLAY_PLAN_OPERATION_RECT; + op->src.color = color; + + } + + } + + op++; + + } + +} diff --git a/src/libguac/display-plan-search.c b/src/libguac/display-plan-search.c new file mode 100644 index 000000000..0d4c241be --- /dev/null +++ b/src/libguac/display-plan-search.c @@ -0,0 +1,436 @@ +/* + * 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 "display-plan.h" +#include "display-priv.h" +#include "guacamole/display.h" +#include "guacamole/rect.h" + +#include +#include + +/** + * Stores the given operation within the ops_by_hash table of the given display + * plan based on the given hash value. The hash function applied for storing + * the operation is GUAC_DISPLAY_PLAN_OPERATION_HASH(). If another operation is + * already stored at the same location within ops_by_hash, that operation will + * be replaced. + * + * @param plan + * The plan to store the operation within. + * + * @param hash + * The hash value to use to calculate the storage location. This value will + * be further hashed with GUAC_DISPLAY_PLAN_OPERATION_HASH(). + * + * @param op + * The operation to store. + */ +static void guac_display_plan_store_indexed_op(guac_display_plan* plan, uint64_t hash, + guac_display_plan_operation* op) { + + size_t index = GUAC_DISPLAY_PLAN_OPERATION_HASH(hash); + guac_display_plan_indexed_operation* entry = &(plan->ops_by_hash[index]); + + if (entry->op == NULL) { + entry->hash = hash; + entry->op = op; + } + +} + +/** + * Removes and returns a pointer to the matching operation stored within the + * ops_by_hash table of the given display plan, if any. If no such operation is + * stored, NULL is returned. + * + * @param plan + * The plan to retrieve the operation from. + * + * @param hash + * The hash value to use to calculate the storage location. This value will + * be further hashed with GUAC_DISPLAY_PLAN_OPERATION_HASH(). + * + * @return + * The operation that was stored under the given hash, if any, or NULL if + * no such operation was found. + */ +static guac_display_plan_operation* guac_display_plan_remove_indexed_op(guac_display_plan* plan, uint64_t hash) { + + size_t index = GUAC_DISPLAY_PLAN_OPERATION_HASH(hash); + guac_display_plan_indexed_operation* entry = &(plan->ops_by_hash[index]); + + /* NOTE: We verify the hash value here because the lookup performed is + * actually a hash of a hash. There's an additional chance of collisions + * between hash values at this second level of hashing. */ + + guac_display_plan_operation* op = entry->op; + if (op != NULL && entry->hash == hash) { + entry->op = NULL; + return op; + } + + return NULL; + +} + +/** + * Callback invoked by guac_hash_foreach_image_rect() for each 64x64 rectangle + * of image data. + * + * @param plan + * The display plan related to the call to guac_hash_foreach_image_rect(). + * + * @param x + * The X coordinate of the upper-left corner of the current 64x64 rectangle + * within the search region. + * + * @param y + * The Y coordinate of the upper-left corner of the current 64x64 rectangle + * within the search region. + * + * @param hash + * The hash value that applies to the current 64x64 rectangle. + * + * @param closure + * The closure value that was originally provided to the call to + * guac_hash_foreach_image_rect(). + */ +typedef void guac_hash_callback(guac_display_plan* plan, int x, int y, uint64_t hash, void* closure); + +/** + * Iterates through each 64x64 subrectangle within the given rectangular region + * of the underlying buffer of the given layer state, invoking the given + * callback for each such subrectangle. Each 64x64 subrectangle within the + * rectangular region is evaluated by sliding a 64x64 window over each pixel of + * the region such that every 64x64 subrectangle in the region is eventually + * covered. + * + * @param plan + * The display plan related to the search/indexing operation being + * performed. + * + * @param layer_state + * The layer state containing the image buffer to hash. + * + * @param rect + * The rectangular region within the image buffer that should be hashed. + * + * @param callback + * The callback to invoke for each 64x64 subrectangle of the given region. + * + * @param closure + * The arbitrary value to pass the given callback each time it is invoked + * through this function call. + */ +static int guac_hash_foreach_image_rect(guac_display_plan* plan, + const guac_display_layer_state* layer_state, const guac_rect* rect, + guac_hash_callback* callback, void* closure) { + + size_t stride = layer_state->buffer_stride; + const unsigned char* data = GUAC_DISPLAY_LAYER_STATE_CONST_BUFFER(*layer_state, *rect); + + int x, y; + uint64_t cell_hash[GUAC_DISPLAY_MAX_WIDTH] = { 0 }; + + /* NOTE: Because the hash value of the sliding 64x64 window is available + * only upon reaching the bottom-right corner of that window, we offset the + * coordinates here by the relative location of the bottom-right corner + * (GUAC_DISPLAY_CELL_SIZE - 1) so that we have easy access to the + * coordinates of the upper-left corner of the sliding window, as required + * by the callback being invoked. + * + * This also allows us to easily determine when the hash is valid and it's + * safe to invoke the callback. Once the coordinates are within the given + * rect, we have evaluated a full 64x64 rectangle and have a valid hash. */ + + int start_x = rect->left - GUAC_DISPLAY_CELL_SIZE + 1; + int end_x = rect->right - GUAC_DISPLAY_CELL_SIZE + 1; + int start_y = rect->top - GUAC_DISPLAY_CELL_SIZE + 1; + int end_y = rect->bottom - GUAC_DISPLAY_CELL_SIZE + 1; + + for (y = start_y; y < end_y; y++) { + + uint64_t* current_cell_hash = cell_hash; + + /* Get current row */ + uint32_t* row = (uint32_t*) data; + data += stride; + + /* Calculate row segment hashes for entire row */ + uint64_t row_hash = 0; + for (x = start_x; x < end_x; x++) { + + /* Get current pixel */ + uint32_t pixel = *(row++); + + /* Update hash value for current row segment */ + row_hash = ((row_hash * 31) << 1) + pixel; + + /* Incorporate row hash value into overall cell hash */ + uint64_t cell_hash = ((*current_cell_hash * 31) << 1) + row_hash; + *(current_cell_hash++) = cell_hash; + + /* Invoke callback for every hash generated, breaking out early if + * requested */ + if (y >= rect->top && x >= rect->left) + callback(plan, x, y, cell_hash, closure); + + } + + } /* end for each row */ + + return 0; + +} + +/** + * Initializes the given rectangle with the bounds of the pending frame cell + * containing the given coordinate. + * + * @param rect + * The rectangle to initialize. + * + * @param x + * The X coordinate of the point that the rectangle must contain. + * + * @param y + * The Y coordinate of the point that the rectangle must contain. + */ +static void guac_display_cell_init_rect(guac_rect* rect, int x, int y) { + x = (x / GUAC_DISPLAY_CELL_SIZE) * GUAC_DISPLAY_CELL_SIZE; + y = (y / GUAC_DISPLAY_CELL_SIZE) * GUAC_DISPLAY_CELL_SIZE; + guac_rect_init(rect, x, y, GUAC_DISPLAY_CELL_SIZE, GUAC_DISPLAY_CELL_SIZE); +} + +/** + * Callback for guac_hash_foreach_image_rect() which stores the given operation + * in the ops_by_hash table of the given display plan. + * + * @param plan + * The display plan to store the given operation in. + * + * @param x + * The X coordinate of the upper-left corner of the 64x64 rectangle + * modified by the given operation. + * + * @param y + * The Y coordinate of the upper-left corner of the 64x64 rectangle + * modified by the given operation. + * + * @param hash + * The hash value that applies to the 64x64 rectangle at the given + * coordinates. + * + * @param closure + * A pointer to the guac_display_plan_operation that should be stored + * within the ops_by_hash table of the given display plan. + */ +static void guac_display_plan_index_op_for_cell(guac_display_plan* plan, int x, int y, uint64_t hash, void* closure) { + guac_display_plan_store_indexed_op(plan, hash, (guac_display_plan_operation*) closure); +} + +void PFR_guac_display_plan_index_dirty_cells(guac_display_plan* plan) { + + memset(plan->ops_by_hash, 0, sizeof(plan->ops_by_hash)); + + guac_display_plan_operation* op = plan->ops; + for (int i = 0; i < plan->length; i++) { + + if (op->type == GUAC_DISPLAY_PLAN_OPERATION_IMG) { + + guac_display_layer* layer = op->layer; + + guac_rect layer_bounds; + guac_display_layer_get_bounds(layer, &layer_bounds); + + guac_rect cell; + guac_display_cell_init_rect(&cell, op->dest.left, op->dest.top); + + guac_rect_constrain(&cell, &layer_bounds); + if (guac_rect_width(&cell) == GUAC_DISPLAY_CELL_SIZE + && guac_rect_height(&cell) == GUAC_DISPLAY_CELL_SIZE) { + guac_hash_foreach_image_rect(plan, &layer->pending_frame, + &cell, guac_display_plan_index_op_for_cell, op); + } + + } + + op++; + + } + +} + +/** + * Compares two rectangular regions of two arbitrary buffers, returning whether + * those regions contain identical data. + * + * @param data_a + * A pointer to the first byte of image data within the first region being + * compared. + * + * @param width_a + * The width of the first region, in pixels. + * + * @param height_a + * The height of the first region, in pixels. + * + * @param stride_a + * The number of bytes in each row of image data in the first region. + * + * @param data_b + * A pointer to the first byte of image data within the second region being + * compared. + * + * @param width_b + * The width of the second region, in pixels. + * + * @param height_b + * The height of the second region, in pixels. + * + * @param stride_b + * The number of bytes in each row of image data in the first region. + * + * @return + * Non-zero if the regions contain at least one differing pixel, zero + * otherwise. + */ +static int guac_image_cmp(const unsigned char* restrict data_a, int width_a, int height_a, + int stride_a, const unsigned char* restrict data_b, int width_b, int height_b, + int stride_b) { + + int y; + + /* If core dimensions differ, just compare those. Done. */ + if (width_a != width_b) return width_a - width_b; + if (height_a != height_b) return height_a - height_b; + + size_t length = guac_mem_ckd_mul_or_die(width_a, GUAC_DISPLAY_LAYER_RAW_BPP); + + for (y = 0; y < height_a; y++) { + + /* Compare row. If different, use that result. */ + int cmp_result = memcmp(data_a, data_b, length); + if (cmp_result != 0) + return cmp_result; + + /* Next row */ + data_a += stride_a; + data_b += stride_b; + + } + + /* Otherwise, same. */ + return 0; + +} + +/** + * Callback for guac_hash_foreach_image_rect() which searches the ops_by_hash + * table of the given display plan for occurrences of the given hash, replacing + * the matching operation with a copy operation if a match is found. + * + * NOTE: While this function will search for and optimize operations that copy + * existing data, it can only do so for distinct image data. Multiple + * operations that copy the same exact data (like a region tiled with multiple + * copies of some pattern) can only be stored in the table once, and therefore + * will only match once. + * + * @param plan + * The display plan to update with any copies found. + * + * @param x + * The X coordinate of the upper-left corner of the 64x64 region currently + * being checked. + * + * @param y + * The Y coordinate of the upper-left corner of the 64x64 region currently + * being checked. + * + * @param hash + * The hash value that applies to the 64x64 rectangle at the given + * coordinates. + * + * @param closure + * A pointer to the guac_display_layer that is being searched. + */ +static void PFR_LFR_guac_display_plan_find_copies(guac_display_plan* plan, + int x, int y, uint64_t hash, void* closure) { + + guac_display_layer* copy_from_layer = (guac_display_layer*) closure; + + /* Transform the matching operation into a copy of the current region if + * any operations match, banning the underlying hash from further checks if + * a collision occurs */ + guac_display_plan_operation* op = guac_display_plan_remove_indexed_op(plan, hash); + if (op != NULL) { + + guac_display_layer* copy_to_layer = op->layer; + + guac_rect src_rect; + guac_rect_init(&src_rect, x, y, GUAC_DISPLAY_CELL_SIZE, GUAC_DISPLAY_CELL_SIZE); + + guac_rect dst_rect; + guac_display_cell_init_rect(&dst_rect, op->dest.left, op->dest.top); + + const unsigned char* copy_from = GUAC_DISPLAY_LAYER_STATE_CONST_BUFFER(copy_from_layer->last_frame, src_rect); + const unsigned char* copy_to = GUAC_DISPLAY_LAYER_STATE_CONST_BUFFER(copy_to_layer->pending_frame, dst_rect); + + /* Only transform into a copy if the image data is truly identical (not a collision) */ + if (!guac_image_cmp(copy_from, GUAC_DISPLAY_CELL_SIZE, GUAC_DISPLAY_CELL_SIZE, copy_from_layer->last_frame.buffer_stride, + copy_to, GUAC_DISPLAY_CELL_SIZE, GUAC_DISPLAY_CELL_SIZE, copy_to_layer->pending_frame.buffer_stride)) { + op->type = GUAC_DISPLAY_PLAN_OPERATION_COPY; + op->src.layer_rect.layer = copy_from_layer->last_frame_buffer; + op->src.layer_rect.rect = src_rect; + op->dest = dst_rect; + } + + } + +} + +void PFR_LFR_guac_display_plan_rewrite_as_copies(guac_display_plan* plan) { + + guac_display* display = plan->display; + guac_display_layer* current = display->last_frame.layers; + while (current != NULL) { + + /* Search only the layers that are specifically noted as possible + * sources for copies */ + if (current->pending_frame.search_for_copies) { + + guac_rect search_region; + guac_rect_init(&search_region, 0, 0, current->last_frame.width, current->last_frame.height); + + /* Avoid excessive computation by restricting the search region to only + * the area that was changed in the upcoming frame (in the case of + * scrolling, absolutely all data relevant to the scroll will have been + * modified) */ + guac_rect_constrain(&search_region, ¤t->pending_frame.dirty); + + guac_hash_foreach_image_rect(plan, ¤t->last_frame, &search_region, + PFR_LFR_guac_display_plan_find_copies, current); + } + + current = current->last_frame.next; + + } + +} diff --git a/src/libguac/display-plan.c b/src/libguac/display-plan.c new file mode 100644 index 000000000..212d93461 --- /dev/null +++ b/src/libguac/display-plan.c @@ -0,0 +1,441 @@ +/* + * 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 "display-plan.h" +#include "display-priv.h" +#include "guacamole/assert.h" +#include "guacamole/client.h" +#include "guacamole/display.h" +#include "guacamole/fifo.h" +#include "guacamole/mem.h" +#include "guacamole/protocol.h" +#include "guacamole/socket.h" +#include "guacamole/timestamp.h" + +#include +#include + +/** + * Updates the dirty rect in the given cell to note that a horizontal line of + * image data at the given location and having the given width has changed + * since the last frame. A provided counter of the overall number of changed + * cells is updated accordingly. + * + * @param layer + * The layer that changed. + * + * @param cell + * The cell containing the line of image data that changed. + * + * @param count + * A pointer to a counter that contains the current number of cells that + * have been marked as having changed since the last frame. + * + * @param x + * The X coordinate of the leftmost pixel of the horizontal line. + * + * @param y + * The Y coordinate of the leftmost pixel of the horizontal line. + * + * @param width + * The width of the line, in pixels. + */ +static void guac_display_plan_mark_dirty(guac_display_layer* layer, + guac_display_layer_cell* cell, size_t* count, int x, int y, + int width) { + + if (!cell->dirty_size) { + guac_rect_init(&cell->dirty, x, y, width, 1); + cell->dirty_size = width; + (*count)++; + } + + else { + guac_rect dirty; + guac_rect_init(&dirty, x, y, width, 1); + guac_rect_extend(&cell->dirty, &dirty); + cell->dirty_size += width; + } + +} + +/** + * Variant of memcmp() which specifically compares series of 32-bit quantities + * and determines the overall location and length of the differences in the two + * provided buffers. The length and location determined are the length and + * location of the smallest contiguous series of 32-bit quantities that differ + * between the buffers. + * + * @param buffer_a + * The first buffer to compare. + * + * @param buffer_b + * The buffer to compare with buffer_a. + * + * @param count + * The number of 32-bit quantities in each buffer. + * + * @param pos + * A pointer to a size_t that should receive the offset of the difference, + * if the two buffers turn out to contain different data. The value of the + * size_t will only be modified if at least one difference is found. + * + * @return + * The number of 32-bit quantities after and including the offset returned + * via pos that are different between buffer_a and buffer_b, or zero if + * there are no such differences. + */ +static size_t guac_display_memcmp(const uint32_t* restrict buffer_a, + const uint32_t* restrict buffer_b, size_t count, size_t* pos) { + + /* Locate first difference between the buffers, if any */ + size_t first = 0; + while (first < count) { + + if (*(buffer_a++) != *(buffer_b++)) + break; + + first++; + + } + + /* If we reached the end without finding any differences, no need to search + * further - the buffers are identical */ + if (first >= count) + return 0; + + /* Search through all remaining values in the buffers for the last + * difference (which may be identical to the first) */ + size_t last = first; + size_t offset = first + 1; + while (offset < count) { + + if (*(buffer_a++) != *(buffer_b++)) + last = offset; + + offset++; + + } + + /* Final difference found - provide caller with the starting offset and + * length (in 32-bit quantities) of differences */ + *pos = first; + return last - first + 1; + +} + +guac_display_plan* PFW_LFR_guac_display_plan_create(guac_display* display) { + + guac_display_layer* current; + guac_timestamp frame_end = guac_timestamp_current(); + size_t op_count = 0; + + /* Loop through each layer, searching for modified regions */ + current = display->pending_frame.layers; + while (current != NULL) { + + /* Skip processing any layers whose buffers have been replaced with + * NULL (this is intentionally allowed to ensure references to external + * buffers can be safely removed if necessary, even before guac_display + * is freed) */ + if (current->pending_frame.buffer == NULL) { + GUAC_ASSERT(current->pending_frame.buffer_is_external); + continue; + } + + /* Check only within layer dirty region, skipping the layer if + * unmodified. This pass should reset and refine that region, but + * otherwise rely on proper reporting of modified regions by callers of + * the open/close layer functions. */ + guac_rect dirty = current->pending_frame.dirty; + if (guac_rect_is_empty(&dirty)) { + current = current->pending_frame.next; + continue; + } + + /* Flush any outstanding Cairo operations before directly accessing buffer */ + guac_display_layer_cairo_context* cairo_context = &(current->pending_frame_cairo_context); + if (cairo_context->surface != NULL) + cairo_surface_flush(cairo_context->surface); + + /* Re-align the dirty rect with nearest multiple of 64 to ensure each + * step of the dirty rect refinement loop starts at the topmost + * boundary of a cell */ + guac_rect_align(&dirty, GUAC_DISPLAY_CELL_SIZE_EXPONENT); + + guac_rect pending_frame_bounds = { + .left = 0, + .top = 0, + .right = current->pending_frame.width, + .bottom = current->pending_frame.height + }; + + /* Limit size of dirty rect by bounds of backing surface for pending + * frame ONLY (bounds checks against the last frame are performed + * within the loop such that everything outside the bounds of the last + * frame is considered dirty) */ + guac_rect_constrain(&dirty, &pending_frame_bounds); + + const unsigned char* flushed_row = GUAC_DISPLAY_LAYER_STATE_CONST_BUFFER(current->last_frame, dirty); + unsigned char* buffer_row = GUAC_DISPLAY_LAYER_STATE_MUTABLE_BUFFER(current->pending_frame, dirty); + + guac_display_layer_cell* cell_row = current->pending_frame_cells + + guac_mem_ckd_mul_or_die(dirty.top / GUAC_DISPLAY_CELL_SIZE, current->pending_frame_cells_width) + + dirty.left / GUAC_DISPLAY_CELL_SIZE; + + /* Loop through the rough modified region, refining the dirty rects of + * each cell to more accurately contain only what has actually changed + * since last frame */ + current->pending_frame.dirty = (guac_rect) { 0 }; + for (int corner_y = dirty.top; corner_y < dirty.bottom; corner_y += GUAC_DISPLAY_CELL_SIZE) { + + int height = GUAC_DISPLAY_CELL_SIZE; + if (corner_y + height > dirty.bottom) + height = dirty.bottom - corner_y; + + /* Iteration through the pending_frame_cells array and the image + * buffer is a bit complex here, as the pending_frame_cells array + * contains cells that represent 64x64 regions, while the image + * buffers contain absolutely all pixels. The outer loop goes + * through just the pending cells, while the following loop goes + * through the Y coordinates that make up that cell. */ + + for (int y_off = 0; y_off < height; y_off++) { + + /* At this point, we need to loop through the horizontal + * dimension, comparing the 64-pixel rows of image data in the + * current line (corner_y + y_off) that are in each applicable + * cell. We jump forward by one cell for each comparison. */ + + int y = corner_y + y_off; + + guac_display_layer_cell* current_cell = cell_row; + uint32_t* current_flushed = (uint32_t*) flushed_row; + uint32_t* current_buffer = (uint32_t*) buffer_row; + for (int corner_x = dirty.left; corner_x < dirty.right; corner_x += GUAC_DISPLAY_CELL_SIZE) { + + int width = GUAC_DISPLAY_CELL_SIZE; + if (corner_x + width > dirty.right) + width = dirty.right - corner_x; + + /* This SHOULD be impossible, as corner_x would need to + * somehow be outside the bounds of the dirty rect, which + * would have failed the loop condition earlier) */ + GUAC_ASSERT(width >= 0); + + /* Any line that is completely outside the bounds of the + * previous frame is dirty (nothing to compare against) */ + if (y >= current->last_frame.height || corner_x >= current->last_frame.width) { + guac_display_plan_mark_dirty(current, current_cell, &op_count, corner_x, y, width); + guac_rect_extend(¤t->pending_frame.dirty, ¤t_cell->dirty); + } + + /* All other regions must be processed further to determine + * what portion is dirty */ + else { + + /* Only the pixels that are within the bounds of BOTH + * the last_frame and pending_frame are directly + * comparable. Others are inherently dirty by virtue of + * being outside the bounds of last_frame */ + int comparable_width = width; + if (corner_x + comparable_width > current->last_frame.width) + comparable_width = current->last_frame.width - corner_x; + + /* It is impossible for this value to be negative + * because of the last_frame bounds checks that occur + * in the if block prior to this else block */ + GUAC_ASSERT(comparable_width >= 0); + + /* Any region outside the right edge of the previous frame is dirty */ + if (width > comparable_width) { + guac_display_plan_mark_dirty(current, current_cell, &op_count, corner_x + comparable_width, y, width - comparable_width); + guac_rect_extend(¤t->pending_frame.dirty, ¤t_cell->dirty); + } + + /* Mark the relevant region of the cell as dirty if the + * current 64-pixel line has changed in any way */ + size_t length, pos; + if ((length = guac_display_memcmp(current_buffer, current_flushed, comparable_width, &pos)) != 0) { + guac_display_plan_mark_dirty(current, current_cell, &op_count, corner_x + pos, y, length); + guac_rect_extend(¤t->pending_frame.dirty, ¤t_cell->dirty); + } + + } + + current_flushed += GUAC_DISPLAY_CELL_SIZE; + current_buffer += GUAC_DISPLAY_CELL_SIZE; + current_cell++; + + } + + flushed_row += current->last_frame.buffer_stride; + buffer_row += current->pending_frame.buffer_stride; + + } + + cell_row += current->pending_frame_cells_width; + + } + + current = current->pending_frame.next; + + } + + /* If no layer has been modified, there's no need to create a plan */ + if (!op_count) + return NULL; + + guac_display_plan* plan = guac_mem_alloc(sizeof(guac_display_plan)); + plan->display = display; + plan->frame_end = frame_end; + plan->length = guac_mem_ckd_add_or_die(op_count, 1); + plan->ops = guac_mem_alloc(plan->length, sizeof(guac_display_plan_operation)); + + /* Convert the dirty rectangles stored in each layer's cells to individual + * image operations for later optimization */ + size_t added_ops = 0; + guac_display_plan_operation* current_op = plan->ops; + current = display->pending_frame.layers; + while (current != NULL) { + + guac_display_layer_cell* cell = current->pending_frame_cells; + for (int y = 0; y < current->pending_frame_cells_height; y++) { + for (int x = 0; x < current->pending_frame_cells_width; x++) { + + if (cell->dirty_size) { + + /* The overall number of ops that we try to add via these + * nested loops should always exactly align with the + * anticipated count produced earlier and therefore not + * overrun the ops array at any point unless there is a bug + * in the way the original operation count was calculated */ + GUAC_ASSERT(added_ops < op_count); + + current_op->layer = current; + current_op->type = GUAC_DISPLAY_PLAN_OPERATION_IMG; + current_op->dest = cell->dirty; + current_op->dirty_size = cell->dirty_size; + current_op->last_frame = cell->last_frame; + current_op->current_frame = frame_end; + + cell->related_op = current_op; + cell->dirty_size = 0; + cell->last_frame = frame_end; + + current_op++; + added_ops++; + + } + else + cell->related_op = NULL; + + cell++; + + } + } + + current = current->pending_frame.next; + + } + + /* At this point, the number of operations added should exactly match the + * predicted quantity */ + GUAC_ASSERT(added_ops == op_count); + + /* Worker threads must be aware of end-of-frame to know when to send sync, + * etc. Noticing that the operation queue is empty is insufficient, as the + * queue may become empty while a frame is in progress if the worker + * threads happen to be processing things quickly. */ + current_op->type = GUAC_DISPLAY_PLAN_END_FRAME; + + return plan; + +} + +void guac_display_plan_free(guac_display_plan* plan) { + guac_mem_free(plan->ops); + guac_mem_free(plan); +} + +void guac_display_plan_apply(guac_display_plan* plan) { + + guac_display* display = plan->display; + guac_client* client = display->client; + guac_display_plan_operation* op = plan->ops; + + /* Do not allow worker threads to move forward with image encoding until + * AFTER the non-image instructions have finished being written */ + guac_fifo_lock(&display->ops); + + /* Immediately send instructions for all updates that do not involve + * significant processing (do not involve encoding anything). This allows + * us to use the worker threads solely for encoding, reducing contention + * between the threads. */ + for (int i = 0; i < plan->length; i++) { + + guac_display_layer* display_layer = op->layer; + switch (op->type) { + + case GUAC_DISPLAY_PLAN_OPERATION_COPY: + guac_protocol_send_copy(client->socket, op->src.layer_rect.layer, + op->src.layer_rect.rect.left, op->src.layer_rect.rect.top, + guac_rect_width(&op->src.layer_rect.rect), guac_rect_height(&op->src.layer_rect.rect), + GUAC_COMP_OVER, display_layer->layer, op->dest.left, op->dest.top); + break; + + case GUAC_DISPLAY_PLAN_OPERATION_RECT: + + guac_protocol_send_rect(client->socket, display_layer->layer, + op->dest.left, op->dest.top, guac_rect_width(&op->dest), guac_rect_height(&op->dest)); + + int alpha = (op->src.color & 0xFF000000) >> 24; + int red = (op->src.color & 0x00FF0000) >> 16; + int green = (op->src.color & 0x0000FF00) >> 8; + int blue = (op->src.color & 0x000000FF); + + /* Clear before drawing if layer is not opaque (transparency + * will not be copied correctly otherwise) */ + if (!display_layer->opaque) { + guac_protocol_send_cfill(client->socket, GUAC_COMP_ROUT, display_layer->layer, 0x00, 0x00, 0x00, 0xFF); + guac_protocol_send_cfill(client->socket, GUAC_COMP_OVER, display_layer->layer, red, green, blue, alpha); + } + else + guac_protocol_send_cfill(client->socket, GUAC_COMP_OVER, display_layer->layer, red, green, blue, 0xFF); + + break; + + /* Simply ignore and drop NOP */ + case GUAC_DISPLAY_PLAN_OPERATION_NOP: + break; + + /* All other operations should be handled by the workers */ + default: + guac_fifo_enqueue(&display->ops, op); + break; + + } + + op++; + + } + + guac_fifo_unlock(&display->ops); + +} diff --git a/src/libguac/display-plan.h b/src/libguac/display-plan.h new file mode 100644 index 000000000..d1a520b7f --- /dev/null +++ b/src/libguac/display-plan.h @@ -0,0 +1,399 @@ +/* + * 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_DISPLAY_PLAN_H +#define GUAC_DISPLAY_PLAN_H + +#include "guacamole/display.h" +#include "guacamole/rect.h" +#include "guacamole/timestamp.h" + +#include +#include + +/** + * The width of an update which should be considered negible and thus + * trivial overhead compared to the cost of two updates. + */ +#define GUAC_DISPLAY_NEGLIGIBLE_WIDTH 64 + +/** + * The height of an update which should be considered negible and thus + * trivial overhead compared to the cost of two updates. + */ +#define GUAC_DISPLAY_NEGLIGIBLE_HEIGHT 64 + +/** + * The proportional increase in cost contributed by transfer and processing of + * image data, compared to processing an equivalent amount of client-side + * data. + */ +#define GUAC_DISPLAY_DATA_FACTOR 128 + +/** + * The maximum width or height to allow when combining any pair of rendering + * operations into a single operation, in pixels, as the exponent of a power of + * two. This value is intended to be large enough to avoid unnecessarily + * increasing the number of drawing operations, yet also small enough to allow + * larger updates to be easily parallelized via the worker threads. + * + * The current value of 9 means that each encoded image will be no larger than + * 512x512 pixels. + */ +#define GUAC_DISPLAY_MAX_COMBINED_SIZE 9 + +/** + * The base cost of every update. Each update should be considered to have + * this starting cost, plus any additional cost estimated from its + * content. + */ +#define GUAC_DISPLAY_BASE_COST 4096 + +/** + * An increase in cost is negligible if it is less than + * 1/GUAC_DISPLAY_NEGLIGIBLE_INCREASE of the old cost. + */ +#define GUAC_DISPLAY_NEGLIGIBLE_INCREASE 4 + +/** + * The framerate which, if exceeded, indicates that JPEG is preferred. + */ +#define GUAC_DISPLAY_JPEG_FRAMERATE 3 + +/** + * Minimum JPEG bitmap size (area). If the bitmap is smaller than this threshold, + * it should be compressed as a PNG image to avoid the JPEG compression tax. + */ +#define GUAC_DISPLAY_JPEG_MIN_BITMAP_SIZE 4096 + +/** + * The JPEG compression min block size, as the exponent of a power of two. This + * defines the optimal rectangle block size factor for JPEG compression. + * Usually 8x8 would suffice, but we use 16x16 here to reduce the occurrence of + * ringing artifacts further. + */ +#define GUAC_SURFACE_JPEG_BLOCK_SIZE 4 + +/** + * The WebP compression min block size, as the exponent of a power of two. This + * defines the optimal rectangle block size factor for WebP compression. WebP + * does utilize variable block size, but ensuring a block size factor reduces + * any noise on the image edges. + */ +#define GUAC_SURFACE_WEBP_BLOCK_SIZE 3 + +/** + * The number of hash buckets within each guac_display_plan. + */ +#define GUAC_DISPLAY_PLAN_OPERATION_INDEX_SIZE 0x10000 + +/** + * Hash function which hashes a larger, 64-bit hash into a 16-bit hash that + * will fit within GUAC_DISPLAY_PLAN_OPERATION_INDEX_SIZE. Note that the random + * distribution of this hash relies entirely on the random distribution of the + * value being hashed. + */ +#define GUAC_DISPLAY_PLAN_OPERATION_HASH(hash) (\ + ( hash & 0xFFFF) \ + ^ ((hash >> 16) & 0xFFFF) \ + ^ ((hash >> 32) & 0xFFFF) \ + ^ ((hash >> 48) & 0xFFFF) \ + ) + +/** + * The type of a graphical operation that may be part of a guac_display_plan. + */ +typedef enum guac_display_plan_operation_type { + + /** + * Do nothing (no-op). + */ + GUAC_DISPLAY_PLAN_OPERATION_NOP = 0, + + /** + * Copy image data from the associated source rect to the destination rect. + * The source and destination layers are not necessarily the same. + */ + GUAC_DISPLAY_PLAN_OPERATION_COPY, + + /** + * Fill a rectangular region of the destination layer with the source + * color. + */ + GUAC_DISPLAY_PLAN_OPERATION_RECT, + + /** + * Draw arbitrary image data to the destination rect. + */ + GUAC_DISPLAY_PLAN_OPERATION_IMG, + + /** + * Finish the frame, sending the frame boundary to all connected users. + */ + GUAC_DISPLAY_PLAN_END_FRAME + +} guac_display_plan_operation_type; + +/** + * A reference to a rectangular region of image data within a layer of the + * remote Guacamole display. + */ +typedef struct guac_display_plan_layer_rect { + + /** + * The rectangular region that should serve as source data for an + * operation. + */ + guac_rect rect; + + /** + * The layer that the source data is coming from. + */ + const guac_layer* layer; + +} guac_display_plan_layer_rect; + +/** + * Any one of several operations that may be contained in a guac_display_plan. + */ +typedef struct guac_display_plan_operation { + + /** + * The destination layer (recipient of graphical output/changes). + */ + guac_display_layer* layer; + + /** + * The operation being performed on the destination layer. + */ + guac_display_plan_operation_type type; + + /** + * The location within the destination layer that will receive these + * changes. + */ + guac_rect dest; + + /** + * The approximate number of pixels that have actually changed as a result + * of this operation. This value will not necessarily be the same as the + * area of the destination rect if some pixels remain unchanged. + */ + size_t dirty_size; + + /** + * The timestamp of the last frame that made any change within the + * destination rect of the destination layer. + */ + guac_timestamp last_frame; + + /** + * The timestamp of the change being made. This will be the timestamp of + * the frame at the time the frame was ended, not the timestamp of the + * server at the time this operation was added to the plan. + */ + guac_timestamp current_frame; + + union { + + /** + * The color that should be used to fill the destination rect. This + * value applies only to GUAC_DISPLAY_PLAN_OPERATION_RECT operations. + */ + uint32_t color; + + /** + * The rectangle that should be copied to the destination rect. This + * value applies only to GUAC_DISPLAY_PLAN_OPERATION_COPY operations. + */ + guac_display_plan_layer_rect layer_rect; + + } src; + +} guac_display_plan_operation; + +/** + * A guac_display_plan_operation that has been hashed and stored within a + * guac_display_plan. + */ +typedef struct guac_display_plan_indexed_operation { + + /** + * The operation. + */ + guac_display_plan_operation* op; + + /** + * The hash value associated with the operation. This hash value is derived + * from the actual image contents of the region that was changed, using the + * new contents of that region. The intent of this hash is to allow + * operations to be quickly located based on the output they will produce, + * such that image draw operations can be automatically replaced with + * simple copies if they reuse data from elsewhere in a layer. + */ + uint64_t hash; + +} guac_display_plan_indexed_operation; + +/** + * The set of operations required to transform the display state from what each + * user currently sees (the previous frame) to the current state of the + * guac_display (the current frame). The operations within a plan are quickly + * generated based on simple image comparisons, and are then refined by an + * optimizer based on estimated costs. + */ +typedef struct guac_display_plan { + + /** + * The display that this plan was created for. + */ + guac_display* display; + + /** + * The time that the frame ended. + */ + guac_timestamp frame_end; + + /** + * Array of all operations that should be applied, in order. The operations + * in this array do not overlap nor depend on each other. They may be + * safely reordered without any impact on the image that results from + * applying those operations. + */ + guac_display_plan_operation* ops; + + /** + * The number of operations stored in the ops array. + */ + size_t length; + + /** + * Index of operations in the plan by their image contents. Only operations + * that can be easily stored without collisions will be represented here. + */ + guac_display_plan_indexed_operation ops_by_hash[GUAC_DISPLAY_PLAN_OPERATION_INDEX_SIZE]; + +} guac_display_plan; + +/** + * Creates a new guac_display_plan representing the changes necessary to + * transform the current remote display state seen by each connected user (the + * previous frame) to the current local display state represented by the + * guac_display (the current frame). The actual operations within the plan are + * chosen based on the result of passing the naive set of operations through an + * optimizer. + * + * There are cases where no plan will be generated. If no changes have occurred + * since the last frame, or if the last frame is still being encoded by the + * guac_display, NULL is returned. In the event that NULL is returned but + * changes have been made, those changes will eventually be automatically + * picked up after the currently-pending frame has finished encoded. + * + * The returned guac_display_plan must eventually be manually freed by a call + * to guac_display_plan_free(). + * + * IMPORTANT: The calling thread must already hold the write lock for the + * display's pending_frame.lock, and must at least hold the read lock for the + * display's last_frame.lock. + * + * @param display + * The guac_display to create a plan for. + * + * @return + * A newly-allocated guac_display_plan representing the changes necessary + * to transform the current remote display state to that of the local + * guac_display, or NULL if no plan could be created. If non-NULL, this + * value must eventually be freed by a call to guac_display_plan_free(). + */ +guac_display_plan* PFW_LFR_guac_display_plan_create(guac_display* display); + +/** + * Frees all memory associated with the given guac_display_plan. + * + * @param plan + * The plan to free. + */ +void guac_display_plan_free(guac_display_plan* plan); + +/** + * Walks through all operations currently in the given guac_display_plan, + * replacing draw operations with simple rects wherever draws consist only of a + * single color. + * + * @param plan + * The guac_display_plan to modify. + */ +void PFR_guac_display_plan_rewrite_as_rects(guac_display_plan* plan); + +/** + * Walks through all operations currently in the given guac_display_plan, + * storing the hashes of each outstanding draw operation within ops_by_hash. + * This function must be invoked before guac_display_plan_rewrite_as_copies() + * can be used for the current pending frame. + * + * @param plan + * The guac_display_plan to index. + */ +void PFR_guac_display_plan_index_dirty_cells(guac_display_plan* plan); + +/** + * Walks through all operations currently in the given guac_display_plan, + * replacing draw operations with simple copies wherever draws can be rewritten + * as copies that pull image data from the previous frame. The display plan + * must first be indexed by guac_display_plan_index_dirty_cells() before this + * function can be used. + * + * @param plan + * The guac_display_plan to modify. + */ +void PFR_LFR_guac_display_plan_rewrite_as_copies(guac_display_plan* plan); + +/** + * Walks through all operations currently in the given guac_display_plan, + * combining horizontally-adjacent operations wherever doing so appears to be + * more efficient than performing those operations separately. + * + * @param plan + * The guac_display_plan to modify. + */ +void PFW_guac_display_plan_combine_horizontally(guac_display_plan* plan); + +/** + * Walks through all operations currently in the given guac_display_plan, + * combining vertically-adjacent operations wherever doing so appears to be + * more efficient than performing those operations separately. + * + * @param plan + * The guac_display_plan to modify. + */ +void PFW_guac_display_plan_combine_vertically(guac_display_plan* plan); + +/** + * Enqueues all operations from the given plan within the operation FIFO used + * by the worker threads of the display associated with that plan. The + * display's worker threads will immediately begin picking up and performing + * these operations, with the final operation resulting in a frame boundary + * ("sync" instruction) being sent to connected users. + * + * @param plan + * The guac_display_plan to apply. + */ +void guac_display_plan_apply(guac_display_plan* plan); + +#endif diff --git a/src/libguac/display-priv.h b/src/libguac/display-priv.h new file mode 100644 index 000000000..d66020fa7 --- /dev/null +++ b/src/libguac/display-priv.h @@ -0,0 +1,813 @@ +/* + * 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_DISPLAY_PRIV_H +#define GUAC_DISPLAY_PRIV_H + +#include "display-plan.h" +#include "guacamole/client.h" +#include "guacamole/display.h" +#include "guacamole/fifo.h" +#include "guacamole/rect.h" +#include "guacamole/socket.h" + +#include + +/** + * The maximum amount of time to wait after flushing a frame when compensating + * for client-side processing delays, in milliseconds. If a connected client is + * taking longer than this amount of additional time to process a received + * frame, processing lag compensation will be only partial (to avoid delaying + * further processing without bound for extremely slow clients). + */ +#define GUAC_DISPLAY_MAX_LAG_COMPENSATION 500 + +/* + * IMPORTANT: All functions defined within the internals of guac_display that + * DO NOT acquire locks on their own are given prefixes based on whether they + * access or modify the pending frame, last frame, or both. It is the + * responsibility of the caller of such functions to ensure that the required + * locks are either held or not relevant. + * + * The prefixes that may be added to function names are: + * + * "PFR_" + * The function reads (but does not write) the state of the pending frame. + * This prefix and "PFW_" are mutually-exclusive. + * + * "PFW_" + * The function writes (and possibly reads) the state of the pending frame. + * This prefix and "PFW_" are mutually-exclusive. + * + * "LFR_" + * The function reads (but does not write) the state of the last frame. + * This prefix and "LFW_" are mutually-exclusive. + * + * "LFW_" + * The function writes (and possibly reads) the state of the last frame. + * This prefix and "LFR_" are mutually-exclusive. + * + * "XFR_" + * The function reads (but does not write) the state of a frame, and + * whether that frame is the pending frame or the last frame depends on + * which frame is provided via function parameters. This prefix and "XFW_" + * are mutually-exclusive. + * + * "XFW_" + * The function writes (but does not read) the state of a frame, and + * whether that frame is the pending frame or the last frame depends on + * which frame is provided via function parameters. This prefix and "XFR_" + * are mutually-exclusive. + * + * Any functions lacking these prefixes either do not access last/pending + * frames in any way or take care of acquiring/releasing locks entirely on + * their own. + * + * These conventions are used for all functions in the internals of + * guac_display, not just those defined in this header. + */ + +/* + * IMPORTANT: In cases where a single thread must acquire multiple locks used + * by guac_display, proper acquisition order must be observed to avoid + * deadlock. The correct order is: + * + * 1) pending_frame.lock + * 2) last_frame.lock + * 3) ops + * 4) render_state + * + * Acquiring these locks in any other order risks deadlock. Don't do it. + */ + +/** + * The size of the image tiles (cells) that will be used to track changes to + * each layer, including gathering framerate statistics and performing indexing + * based on contents. Each side of each cell will consist of this many pixels. + * + * IMPORTANT: The hashing algorithm used to search the previous frame for + * content in the pending frame that has been reused (ie: scrolling) strongly + * depends on this value being 64. Any adjustment to this value will require + * corresponding and careful changes to the hashing algorithm. + */ +#define GUAC_DISPLAY_CELL_SIZE 64 + +/** + * The exponent of the power-of-two value that dictates the size of the image + * tiles (cells) that will be used to track changes to each layer + * (GUAC_DISPLAY_CELL_SIZE). + */ +#define GUAC_DISPLAY_CELL_SIZE_EXPONENT 6 + +/** + * The amount that the width/height of internal storage for graphical data + * should be rounded up to avoid unnecessary reallocations and copying. + */ +#define GUAC_DISPLAY_RESIZE_FACTOR 64 + +/** + * Given the width (or height) of a layer in pixels, calculates the width (or + * height) of that layer's pending_frame_cells array in cells. + * + * NOTE: It is not necessary to recalculate these values except when resizing a + * layer. In all other cases, the width/height of a layer in cells can be found + * in the pending_frame_cells_width and pending_frame_cells_height members + * respectively. + * + * @param pixels + * The width or height of the layer, in pixels. + * + * @return + * The width or height of that layer's pending_frame_cells array, in cells. + */ +#define GUAC_DISPLAY_CELL_DIMENSION(pixels) \ + ((pixels + GUAC_DISPLAY_CELL_SIZE - 1) / GUAC_DISPLAY_CELL_SIZE) + +/** + * The size of the operation FIFO read by the display worker threads. This + * value is the number of operation slots in the FIFO, not bytes. The amount of + * space currently specified here is roughly sufficient 8 worst-case frames + * worth of outstanding operations. + */ +#define GUAC_DISPLAY_WORKER_FIFO_SIZE ( \ + GUAC_DISPLAY_MAX_WIDTH * GUAC_DISPLAY_MAX_HEIGHT \ + / GUAC_DISPLAY_CELL_SIZE \ + / GUAC_DISPLAY_CELL_SIZE \ + * 8) + +/** + * Returns the memory address of the given rectangle within the mutable image + * buffer of the given guac_display_layer_state, where the upper-left corner of + * the given buffer is (0, 0). If the memory address cannot be calculated + * because doing so would overflow the maximum value of a size_t, execution of + * the current process is automatically aborted. + * + * IMPORTANT: No checks are performed on whether the rectangle extends beyond + * the bounds of the buffer, including considering whether the left/top + * position of the rectangle is negative. If the rectangle has not already been + * contrained to be within the bounds of the buffer, such checks must be + * performed before dereferencing the value returned by this macro. + * + * @param layer_state + * The guac_display_layer_state associated with the image buffer within + * which the address of the given rectangle should be determined. + * + * @param rect + * The rectangle to determine the offset of. + * + * @return + * The memory address of the given rectangle within the buffer of the given + * layer state. + */ +#define GUAC_DISPLAY_LAYER_STATE_MUTABLE_BUFFER(layer_state, rect) \ + GUAC_RECT_MUTABLE_BUFFER(rect, (layer_state).buffer, (layer_state).buffer_stride, GUAC_DISPLAY_LAYER_RAW_BPP) + +/** + * Returns the memory address of the given rectangle within the immutable + * (const) image buffer of the given guac_display_layer_state, where the + * upper-left corner of the given buffer is (0, 0). If the memory address + * cannot be calculated because doing so would overflow the maximum value of a + * size_t, execution of the current process is automatically aborted. + * + * IMPORTANT: No checks are performed on whether the rectangle extends beyond + * the bounds of the buffer, including considering whether the left/top + * position of the rectangle is negative. If the rectangle has not already been + * contrained to be within the bounds of the buffer, such checks must be + * performed before dereferencing the value returned by this macro. + * + * @param layer_state + * The guac_display_layer_state associated with the image buffer within + * which the address of the given rectangle should be determined. + * + * @param rect + * The rectangle to determine the offset of. + * + * @return + * The memory address of the given rectangle within the buffer of the given + * layer state. + */ +#define GUAC_DISPLAY_LAYER_STATE_CONST_BUFFER(layer_state, rect) \ + GUAC_RECT_CONST_BUFFER(rect, (layer_state).buffer, (layer_state).buffer_stride, GUAC_DISPLAY_LAYER_RAW_BPP) + +/** + * Bitwise flag set on the render_state flag in guac_display when rendering of + * a pending frame is in progress (Guacamole instructions that draw the pending + * frame are being sent to connected users). + */ +#define GUAC_DISPLAY_RENDER_STATE_FRAME_IN_PROGRESS 1 + +/** + * Bitwise flag set on the render_state flag in guac_display when rendering of + * a pending frame is NOT in progress (Guacamole instructions that draw the + * pending frame are NOT being sent to connected users). + */ +#define GUAC_DISPLAY_RENDER_STATE_FRAME_NOT_IN_PROGRESS 2 + +/** + * Bitwise flag that is set on the state of a guac_display_render_thread when + * the thread should be stopped. + */ +#define GUAC_DISPLAY_RENDER_THREAD_STATE_STOPPING 1 + +/** + * Bitwise flag that is set on the state of a guac_display_render_thread when + * visible, graphical changes have been made. + */ +#define GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_MODIFIED 2 + +/** + * Bitwise flag that is set on the state of a guac_display_render_thread when + * a frame boundary has been reached. + */ +#define GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_READY 4 + +struct guac_display_render_thread { + + /** + * The display this render thread should render to. + */ + guac_display* display; + + /** + * The actual underlying POSIX thread. + */ + pthread_t thread; + + /** + * Flag representing render state. This flag is used to store whether the + * render thread is stopping and whether the current frame has been + * modified or is ready. + * + * @see GUAC_DISPLAY_RENDER_THREAD_STATE_STOPPING + * @see GUAC_DISPLAY_RENDER_THREAD_FRAME_MODIFIED + * @see GUAC_DISPLAY_RENDER_THREAD_FRAME_READY + */ + guac_flag state; + + /** + * The number of frames that have been explicitly marked as ready since the + * last frame sent. This will be zero if explicit frame boundaries are not + * currently being used. + */ + unsigned int frames; + +}; + +/** + * Approximation of how often a region of a layer is modified, as well as what + * changes have been made to that region since the last frame. This information + * is used to help advise future optimizations, such as whether lossy + * compression is appropriate and whether parts of the layer can be copied from + * other regions rather than resend image data. + */ +typedef struct guac_display_layer_cell { + + /** + * The last time this particular cell was part of a frame (used to + * calculate framerate). + */ + guac_timestamp last_frame; + + /** + * The region of this cell that has been modified since the last frame was + * flushed. If the cell has not been modified at all, this will be an empty + * rect. + */ + guac_rect dirty; + + /** + * The rough number of pixels in the dirty rect that have been modified. If + * the cell has not been modified at all, this will be zero. + */ + size_t dirty_size; + + /** + * The display plan operation that is associated with this cell. If a + * display plan is not currently being created or optimized, this will be + * NULL. + */ + guac_display_plan_operation* related_op; + +} guac_display_layer_cell; + +/** + * The state of a Guacamole layer or buffer at some point in time. Within + * guac_display_layer, copies of this structure are used to represent the + * previous frame and the current, in-progress frame. The previous and + * in-progress frames are compared during flush to determine what graphical + * operations need to be sent to connected clients to efficiently transform the + * remote display from its previous state to the now-current state. + * + * IMPORTANT: The lock of the corresponding guac_display_state must be acquired + * before reading or modifying the values of any member of this structure. + */ +typedef struct guac_display_layer_state { + + /** + * The width of this layer in pixels. + */ + int width; + + /** + * The height of this layer in pixels. + */ + int height; + + /** + * The layer which contains this layer. This is only applicable to visible + * (non-buffer) layers which are not the default layer. + */ + const guac_layer* parent; + + /** + * The X coordinate of the upper-left corner of this layer, in pixels, + * relative to its parent layer. This is only applicable to visible + * (non-buffer) layers which are not the default layer. + */ + int x; + + /** + * The Y coordinate of the upper-left corner of this layer, in pixels, + * relative to its parent layer. This is only applicable to visible + * (non-buffer) layers which are not the default layer. + */ + int y; + + /** + * The Z-order of this layer, relative to sibling layers. This is only + * applicable to visible (non-buffer) layers which are not the default + * layer. + */ + int z; + + /** + * The level of opacity applied to this layer. Fully opaque is 255, while + * fully transparent is 0. This is only applicable to visible (non-buffer) + * layers which are not the default layer. + */ + int opacity; + + /** + * The number of simultaneous touches that this surface can accept, where 0 + * indicates that the surface does not support touch events at all. + */ + int touches; + + /** + * Non-zero if all graphical updates for this surface should use lossless + * compression, 0 otherwise. By default, newly-created surfaces will use + * lossy compression when heuristics determine it is appropriate. + */ + int lossless; + + /** + * The raw, 32-bit buffer of ARGB image data. If the layer was allocated as + * opaque, the alpha channel of each ARGB pixel will not be considered when + * compositing or when encoding images. + * + * So that large regions of image data can be easily compared, a consistent + * value for the alpha channel SHOULD be provided so that each 32-bit pixel + * can be compared without having to separately masking the channel. + * Optimizations within guac_display, including scroll detection, may + * assume that the alpha channel can always be considered when comparing + * pixel values for equivalence. + */ + unsigned char* buffer; + + /** + * The width of the image data, in pixels. This is not necessarily the same + * as the width of the layer. + */ + int buffer_width; + + /** + * The height of the image data, in pixels. This is not necessarily the + * same as the height of the layer. + */ + int buffer_height; + + /** + * The number of bytes in each row of image data. This is not necessarily + * equivalent to 4 * width. + */ + size_t buffer_stride; + + /** + * Non-zero if the image data referenced by the buffer pointer was + * allocated externally and should not be automatically freed or managed by + * guac_display, zero otherwise. + */ + int buffer_is_external; + + /** + * The approximate rectangular region containing all pixels within this + * layer that have been modified since the frame that occurred before this + * frame. If the layer was not modified, this will be an empty rect (zero + * width or zero height). + */ + guac_rect dirty; + + /** + * Whether this layer should be searched for possible scroll/copy + * optimizations. + */ + int search_for_copies; + + /* ---------------- LAYER LIST POINTERS ---------------- */ + + /** + * The layer immediately prior to this layer within the list containing + * this layer, or NULL if this is the first layer/buffer in the list. + */ + guac_display_layer* prev; + + /** + * The layer immediately following this layer within the list containing + * this layer, or NULL if this is the last layer/buffer in the list. + */ + guac_display_layer* next; + +} guac_display_layer_state; + +struct guac_display_layer { + + /** + * The guac_display instance that allocated this layer/buffer. + */ + guac_display* display; + + /** + * The Guacamole layer (or buffer) that this guac_display_layer will draw + * to when flushing a frame. + * + * NOTE: This value is set only during allocation and may safely be + * accessed without acquiring the overall layer lock. + */ + const guac_layer* layer; + + /** + * Whether the graphical data that will be written to this layer/buffer + * will only ever be opaque (no alpha channel). Compositing of graphical + * updates can be faster when no alpha channel need be considered. + */ + int opaque; + + /* ---------------- LAYER PREVIOUS FRAME STATE ---------------- */ + + /** + * The state of this layer when the last frame was flushed to connected clients. + * + * IMPORTANT: The display-level last_frame.lock MUST be acquired before + * modifying or reading this member. + */ + guac_display_layer_state last_frame; + + /** + * Off-screen buffer storing the contents of the previously-rendered frame + * for later use. If graphical updates are recognized as reusing data from + * a previous frame, that data will be copied from this buffer. Doing this + * simplifies the copy operation (there is no longer any need to perform + * those copies in a specific order) and ensures the copies are efficient + * on the client side (copying from one part of a graphical surface to + * another part of the same surface can be inefficient, particularly if the + * regions overlap). In practice, there is ample time between frames for + * the client to copy a layer's current contents to an off-screen buffer + * while awaiting the next frame. + * + * NOTE: This value is set only during allocation and may safely be + * accessed without acquiring the display-level last_frame.lock. + */ + guac_layer* last_frame_buffer; + + /* ---------------- LAYER PENDING FRAME STATE ---------------- */ + + /** + * The upcoming state of this layer when the current, in-progress frame is + * flushed to connected clients. + * + * IMPORTANT: The display-level pending_frame.lock MUST be acquired before + * modifying or reading this member. + */ + guac_display_layer_state pending_frame; + + /** + * The Cairo context and surface containing the graphical data of the + * pending frame. The actual underlying buffer and details of the graphical + * surface are also available via pending_frame_raw_context. + * + * IMPORTANT: The display-level pending_frame.lock MUST be acquired before + * modifying or reading this member. + */ + guac_display_layer_cairo_context pending_frame_cairo_context; + + /** + * The raw underlying buffer and details of the surface containing the + * graphical data of the pending frame. A Cairo context and surface backed + * by this buffer are also available via pending_frame_cairo_context. + * + * IMPORTANT: The display-level pending_frame.lock MUST be acquired before + * modifying or reading this member. + */ + guac_display_layer_raw_context pending_frame_raw_context; + + /** + * A two-dimensional array of square tiles representing the nature of + * changes made to corresponding regions of the display. This is used both + * to track how frequently certain regions are being updated (to help + * inform whether lossy compression is appropriate), to track what parts of + * the frame have actually changed, and to aid in determining whether + * adjacent updated regions should be combined into a single update. + * + * IMPORTANT: The display-level pending_frame.lock MUST be acquired before + * modifying or reading this member. + */ + guac_display_layer_cell* pending_frame_cells; + + /** + * The width of the pending_frame_cells array, in cells. + * + * IMPORTANT: The display-level pending_frame.lock MUST be acquired before + * modifying or reading this member. + */ + size_t pending_frame_cells_width; + + /** + * The height of the pending_frame_cells array, in cells. + * + * IMPORTANT: The display-level pending_frame.lock MUST be acquired before + * modifying or reading this member. + */ + size_t pending_frame_cells_height; + +}; + +typedef struct guac_display_state { + + /** + * Lock that guards concurrent access to any member of ANY STRUCTURE that + * relates to this guac_display_state, including the members of this + * structure. Unless explicitly documented otherwise, this lock MUST be + * acquired before accessing or modifying the members of this + * guac_display_state or any nested structure. + */ + guac_rwlock lock; + + /** + * The specific point in time that this guac_display_state represents. + */ + guac_timestamp timestamp; + + /** + * All layers and buffers that were part of the display at the time that + * the frame/snapshot represented by this guac_display_state was updated. + * + * NOTE: For each guac_display, there are two distinct lists of layers: the + * last frame layer list and the pending frame layer list: + * + * LAST FRAME LAYER LIST + * + * - HEAD: display->last_frame.layers + * - NEXT: layer->last_frame.next + * - PREV: layer->last_frame.prev + * + * PENDING LAYER LIST + * + * - HEAD: display->pending_frame.layers + * - NEXT: layer->pending_frame.next + * - PREV: layer->pending_frame.prev + * + * Existing layers are deleted only at the time a frame is flushed when a + * layer in the last frame layer list is found to no longer exist in the + * pending frame layer list. The same goes for the addition of new layers: + * they are added only during flush when a layer that was not present in + * the last frame layer list is found to be present in the pending frame + * layer list. + */ + guac_display_layer* layers; + + /** + * The X coordinate of the hotspot of the mouse cursor. The cursor image is + * stored/updated via the cursor_buffer member of guac_display. + */ + int cursor_hotspot_x; + + /** + * The Y coordinate of the hotspot of the mouse cursor. The cursor image is + * stored/updated via the cursor_buffer member of guac_display. + */ + int cursor_hotspot_y; + + /** + * The user that moved or clicked the mouse. This is used to ensure we + * don't attempt to synchronize an out-of-date mouse position to the user + * that is actively moving the mouse. + */ + guac_user* cursor_user; + + /** + * The X coordinate of the mouse cursor. + */ + int cursor_x; + + /** + * The Y coordinate of the mouse cursor. + */ + int cursor_y; + + /** + * The mask representing the states of all mouse buttons. + */ + int cursor_mask; + + /** + * The number of logical frames that have been rendered to this display + * state since the previous display state. + */ + unsigned int frames; + +} guac_display_state; + +struct guac_display { + + /* NOTE: Any member of this structure that requires protection against + * concurrent access is protected by its own lock. The overall display does + * not have nor need a top-level lock. */ + + /** + * The client associated with this display. + */ + guac_client* client; + + /* ---------------- DISPLAY FRAME STATES ---------------- */ + + /** + * The state of this display at the time the last frame was sent to + * connected users. + */ + guac_display_state last_frame; + + /** + * The pending state of this display that will become the next frame once + * it is sent to connected users. + */ + guac_display_state pending_frame; + + /** + * Whether the pending frame has been modified in any way outside of + * changing the mouse cursor or moving the mouse. This is used to help + * inform whether a frame should be flushed to update connected clients + * with respect to mouse cursor changes, or whether those changes can be + * safely assumed to be part of a larger frame containing general graphical + * updates. + * + * IMPORTANT: The display-level pending_frame.lock MUST be acquired before + * modifying or reading this member. + */ + int pending_frame_dirty_excluding_mouse; + + /* ---------------- WELL-KNOWN LAYERS / BUFFERS ---------------- */ + + /** + * The default layer of the client display. + */ + guac_display_layer* default_layer; + + /** + * The buffer storing the current mouse cursor. The hotspot position within + * the cursor is stored within cursor_hotspot_x and cursor_hotspot_y of + * guac_display_state. + */ + guac_display_layer* cursor_buffer; + + /* ---------------- FRAME ENCODING WORKER THREADS ---------------- */ + + /** + * The number of worker threads in the worker_threads array. + */ + int worker_thread_count; + + /** + * Pool of worker threads that automatically pull from the ops FIFO, + * sending corresponding Guacamole instructions to all connected clients. + */ + pthread_t* worker_threads; + + /** + * FIFO of all graphical operations required to transform the remote + * display state from the previous frame to the next frame. Operations + * added to this FIFO will automatically be pulled and processed by a + * worker thread. + */ + guac_fifo ops; + + /** + * Storage for any items within the ops fifo. + */ + guac_display_plan_operation ops_items[GUAC_DISPLAY_WORKER_FIFO_SIZE]; + + /** + * The current number of active worker threads. + * + * IMPORTANT: This member must only be accessed or modified while the ops + * FIFO is locked. + */ + unsigned int active_workers; + + /** + * Whether least one pending frame has been deferred due to the encoding + * process being underway for a previous frame at the time it was + * completed. + * + * IMPORTANT: This member must only be accessed or modified while the ops + * FIFO is locked. + */ + int frame_deferred; + + /** + * The current state of the rendering process. Code that needs to be aware + * of whether a frame is currently in the process of being rendered can + * monitor the state of this flag, watching for either the + * GUAC_DISPLAY_RENDER_STATE_FRAME_IN_PROGRESS or + * GUAC_DISPLAY_RENDER_STATE_FRAME_NOT_IN_PROGRESS values. + */ + guac_flag render_state; + +}; + +/** + * Allocates and inserts a new element into the given linked list of display + * layers, associating it with the given layer and surface. + * + * @param head + * A pointer to the head pointer of the list of layers. The head pointer + * will be updated by this function to point to the newly-allocated + * display layer. + * + * @param layer + * The Guacamole layer to associated with the new display layer. + * + * @param opaque + * Non-zero if the new layer will only ever contain opaque image contents + * (the alpha channel should be ignored), zero otherwise. + * + * @return + * The newly-allocated display layer, which has been associated with the + * provided layer and surface. + */ +guac_display_layer* guac_display_add_layer(guac_display* display, guac_layer* layer, int opaque); + +/** + * Removes the given layer from all linked lists containing that layer and + * frees all associated memory. + * + * @param display_layer + * The layer to remove. + */ +void guac_display_remove_layer(guac_display_layer* display_layer); + +/** + * Resizes the given layer to the given dimensions, including any underlying + * image buffers. + * + * @param layer + * The layer to resize. + * + * @param width + * The new width, in pixels. + * + * @param height + * The new height, in pixels. + */ +void PFW_guac_display_layer_resize(guac_display_layer* layer, + int width, int height); + +/** + * Worker thread that continuously pulls operations from the operation FIFO of + * the given guac_display, applying those operations by seding corresponding + * instructions to connected clients. + * + * @param data + * A pointer to the guac_display. + * + * @return + * Always NULL. + */ +void* guac_display_worker_thread(void* data); + +#endif diff --git a/src/libguac/display-render-thread.c b/src/libguac/display-render-thread.c new file mode 100644 index 000000000..ac4a38892 --- /dev/null +++ b/src/libguac/display-render-thread.c @@ -0,0 +1,173 @@ +/* + * 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 "config.h" +#include "display-priv.h" +#include "guacamole/client.h" +#include "guacamole/display.h" +#include "guacamole/flag.h" +#include "guacamole/mem.h" +#include "guacamole/timestamp.h" + +/** + * The maximum duration of a frame in milliseconds. This ensures we at least + * meet a reasonable minimum framerate in the case that the remote desktop + * server provides no frame boundaries and streams data continuously enough + * that frame boundaries are not discernable through timing. + * + * The current value of 100 is equivalent to 10 frames per second. + */ +#define GUAC_DISPLAY_RENDER_THREAD_MAX_FRAME_DURATION 100 + +/** + * The minimum duration of a frame in milliseconds. This ensures we don't start + * flushing a ton of tiny frames if a remote desktop server provides no frame + * boundaries and streams data inconsistently enough that timing would suggest + * frame boundaries in the middle of a frame. + * + * The current value of 10 is equivalent to 100 frames per second. + */ +#define GUAC_DISPLAY_RENDER_THREAD_MIN_FRAME_DURATION 10 + +/** + * The start routine for the display render thread, consisting of a single + * render loop. The render loop will proceed until signalled to stop, + * determining frame boundaries via a combination of heuristics and explicit + * marking (if available). + * + * @param data + * The guac_display_render_thread structure containing the render thread + * state. + * + * @return + * Always NULL. + */ +static void* guac_display_render_loop(void* data) { + + guac_display_render_thread* render_thread = (guac_display_render_thread*) data; + guac_display* display = render_thread->display; + + for (;;) { + + /* Wait indefinitely for any change to the frame state */ + guac_flag_wait_and_lock(&render_thread->state, + GUAC_DISPLAY_RENDER_THREAD_STATE_STOPPING + | GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_READY + | GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_MODIFIED); + + /* Bail out immediately upon upcoming disconnect */ + if (render_thread->state.value & GUAC_DISPLAY_RENDER_THREAD_STATE_STOPPING) { + guac_flag_unlock(&render_thread->state); + return NULL; + } + + int rendered_frames = 0; + + /* Lacking explicit frame boundaries, handle the change in frame state, + * continuing to accumulate frame modifications while still within + * heuristically determined frame boundaries */ + int allowed_wait = 0; + guac_timestamp frame_start = guac_timestamp_current(); + do { + + /* Continue processing messages for up to a reasonable + * minimum framerate without an explicit frame boundary + * indicating that the frame is not yet complete */ + int frame_duration = guac_timestamp_current() - frame_start; + if (frame_duration > GUAC_DISPLAY_RENDER_THREAD_MAX_FRAME_DURATION) { + guac_flag_unlock(&render_thread->state); + break; + } + + /* Use explicit frame boundaries whenever available */ + if (render_thread->state.value & GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_READY) { + + rendered_frames = render_thread->frames; + render_thread->frames = 0; + + guac_flag_clear(&render_thread->state, + GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_READY + | GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_MODIFIED); + guac_flag_unlock(&render_thread->state); + break; + + } + + /* Do not exceed a reasonable maximum framerate without an + * explicit frame boundary terminating the frame early */ + allowed_wait = GUAC_DISPLAY_RENDER_THREAD_MIN_FRAME_DURATION - frame_duration; + if (allowed_wait < 0) + allowed_wait = 0; + + /* Wait for further modifications or other changes to frame state */ + + guac_flag_clear(&render_thread->state, GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_MODIFIED); + guac_flag_unlock(&render_thread->state); + + } while (guac_flag_timedwait_and_lock(&render_thread->state, + GUAC_DISPLAY_RENDER_THREAD_STATE_STOPPING + | GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_READY + | GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_MODIFIED, allowed_wait)); + + guac_display_end_multiple_frames(display, rendered_frames); + + } + + return NULL; + +} + +guac_display_render_thread* guac_display_render_thread_create(guac_display* display) { + + guac_display_render_thread* render_thread = guac_mem_alloc(sizeof(guac_display_render_thread)); + + guac_flag_init(&render_thread->state); + render_thread->display = display; + render_thread->frames = 0; + + /* Start render thread (this will immediately begin blocking until frame + * modification or readiness is signalled) */ + pthread_create(&render_thread->thread, NULL, guac_display_render_loop, render_thread); + + return render_thread; + +} + +void guac_display_render_thread_notify_modified(guac_display_render_thread* render_thread) { + guac_flag_set(&render_thread->state, GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_MODIFIED); +} + +void guac_display_render_thread_notify_frame(guac_display_render_thread* render_thread) { + guac_flag_set_and_lock(&render_thread->state, GUAC_DISPLAY_RENDER_THREAD_STATE_FRAME_READY); + render_thread->frames++; + guac_flag_unlock(&render_thread->state); +} + +void guac_display_render_thread_destroy(guac_display_render_thread* render_thread) { + + /* Clean up render thread after signalling it to stop */ + guac_flag_set(&render_thread->state, GUAC_DISPLAY_RENDER_THREAD_STATE_STOPPING); + pthread_join(render_thread->thread, NULL); + + /* Free remaining resources */ + guac_flag_destroy(&render_thread->state); + guac_mem_free(render_thread); + +} + diff --git a/src/libguac/display-worker.c b/src/libguac/display-worker.c new file mode 100644 index 000000000..39649f3f7 --- /dev/null +++ b/src/libguac/display-worker.c @@ -0,0 +1,522 @@ +/* + * 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 "display-plan.h" +#include "display-priv.h" +#include "guacamole/client.h" +#include "guacamole/display.h" +#include "guacamole/fifo.h" +#include "guacamole/layer.h" +#include "guacamole/protocol-types.h" +#include "guacamole/protocol.h" +#include "guacamole/rect.h" +#include "guacamole/rwlock.h" +#include "guacamole/socket.h" +#include "guacamole/timestamp.h" + +#include +#include +#include +#include + +/** + * Returns a new Cairo surface representing the contents of the given dirty + * rectangle from the given layer. The returned surface must eventually be + * freed with a call to cairo_surface_destroy(). The graphical contents will be + * referenced from the layer's last_frame buffer. If sending the contents of a + * pending frame, that pending frame must have been copied over to the + * last_frame buffer before calling this function. + * + * @param display_layer + * The layer whose data should be referenced by the returned Cairo surface. + * + * @param dirty + * The region of the layer that should be referenced by the returned Cairo + * surface. + * + * @return + * A new Cairo surface that points to the given rectangle of image data + * from the last_frame buffer of the given layer. This surface must + * eventually be freed with a call to cairo_surface_destroy(). + */ +static cairo_surface_t* LFR_guac_display_layer_cairo_rect(guac_display_layer* display_layer, + guac_rect* dirty) { + + /* Get Cairo surface covering dirty rect */ + unsigned char* buffer = GUAC_DISPLAY_LAYER_STATE_MUTABLE_BUFFER(display_layer->last_frame, *dirty); + cairo_surface_t* rect; + + /* Use RGB24 if the image is fully opaque */ + if (display_layer->opaque) + rect = cairo_image_surface_create_for_data(buffer, + CAIRO_FORMAT_RGB24, guac_rect_width(dirty), + guac_rect_height(dirty), display_layer->last_frame.buffer_stride); + + /* Otherwise ARGB32 is needed, and the destination must be cleared */ + else + rect = cairo_image_surface_create_for_data(buffer, + CAIRO_FORMAT_ARGB32, guac_rect_width(dirty), + guac_rect_height(dirty), display_layer->last_frame.buffer_stride); + + return rect; + +} + +/** + * Sends instructions over the Guacamole connection to clear the given + * rectangle of the given layer if that layer is non-opaque. This is necessary + * prior to sending image data to layers with alpha transparency, as image data + * from multiple updates will otherwise be composited together. + * + * @param display_layer + * The layer that should possibly be cleared in preparation for a future + * drawing operation. + * + * @param dirty + * The rectangular region of the drawing operation. + */ +static void guac_display_layer_clear_non_opaque(guac_display_layer* display_layer, + guac_rect* dirty) { + + guac_display* display = display_layer->display; + const guac_layer* layer = display_layer->layer; + + guac_client* client = display->client; + guac_socket* socket = client->socket; + + /* Clear destination region only if necessary due to the relevant layer + * being non-opaque */ + if (!display_layer->opaque) { + + guac_protocol_send_rect(socket, layer, dirty->left, dirty->top, + guac_rect_width(dirty), guac_rect_height(dirty)); + + guac_protocol_send_cfill(socket, GUAC_COMP_ROUT, layer, + 0x00, 0x00, 0x00, 0xFF); + + } + +} + +/** + * Returns an appropriate quality between 0 and 100 for lossy encoding + * depending on the current processing lag calculated for the given client. + * + * @param client + * The client for which the lossy quality is being calculated. + * + * @return + * A value between 0 and 100 inclusive which seems appropriate for the + * client based on lag measurements. + */ +static int guac_display_suggest_quality(guac_client* client) { + + int lag = guac_client_get_processing_lag(client); + + /* Scale quality linearly from 90 to 30 as lag varies from 20ms to 80ms */ + int quality = 90 - (lag - 20); + + /* Do not exceed 90 for quality */ + if (quality > 90) + return 90; + + /* Do not go below 30 for quality */ + if (quality < 30) + return 30; + + return quality; + +} + +/** + * Guesses whether a rectangle within a particular layer would be better + * compressed as PNG or using a lossy format like JPEG. Positive values + * indicate PNG is likely to be superior, while negative values indicate the + * opposite. + * + * @param layer + * The layer containing the image data to check. + * + * @param rect + * The rect to check within the given layer. + * + * @return + * Positive values if PNG compression is likely to perform better than + * lossy alternatives, or negative values if PNG is likely to perform + * worse. + */ +static int LFR_guac_display_layer_png_optimality(guac_display_layer* layer, + const guac_rect* rect) { + + int x, y; + + int num_same = 0; + int num_different = 1; + + /* Get buffer from layer */ + size_t stride = layer->last_frame.buffer_stride; + const unsigned char* buffer = GUAC_DISPLAY_LAYER_STATE_CONST_BUFFER(layer->last_frame, *rect); + + /* Image must be at least 1x1 */ + if (rect->right - rect->left < 1 || rect->bottom - rect->top< 1) + return 0; + + /* For each row */ + for (y = rect->top; y < rect->bottom; y++) { + + uint32_t* row = (uint32_t*) buffer; + uint32_t last_pixel = *(row++) | 0xFF000000; + + /* For each pixel in current row */ + for (x = rect->left + 1; x < rect->right; x++) { + + /* Get next pixel */ + uint32_t current_pixel = *(row++) | 0xFF000000; + + /* Update same/different counts according to pixel value */ + if (current_pixel == last_pixel) + num_same++; + else + num_different++; + + last_pixel = current_pixel; + + } + + /* Advance to next row */ + buffer += stride; + + } + + /* Return rough approximation of optimality for PNG compression. As PNG + * leverages lossless DEFLATE compression (which works by reducing the + * number of bytes required to represent repeated data), an approximation + * of the amount of repeated image data within the image is a reasonable + * approximation for how well an image will compress. */ + return 0x100 * num_same / num_different - 0x400; + +} + +/** + * Returns whether the given rectangle would be optimally encoded as JPEG + * rather than PNG. + * + * @param layer + * The layer to be queried. + * + * @param rect + * The rectangle to check. + * + * @param framerate + * The rate that the region covered by the given rectangle has historically + * been being updated within the given layer, in frames per second. + * + * @return + * Non-zero if the rectangle would be optimally encoded as JPEG, zero + * otherwise. + */ +static int LFR_guac_display_layer_should_use_jpeg(guac_display_layer* layer, + const guac_rect* rect, int framerate) { + + /* Do not use JPEG if lossless quality is required */ + if (layer->last_frame.lossless) + return 0; + + int rect_width = rect->right - rect->left; + int rect_height = rect->bottom - rect->top; + int rect_size = rect_width * rect_height; + + /* JPEG is preferred if: + * - frame rate is high enough + * - image size is large enough + * - PNG is not more optimal based on image contents */ + return framerate >= GUAC_DISPLAY_JPEG_FRAMERATE + && rect_size > GUAC_DISPLAY_JPEG_MIN_BITMAP_SIZE + && LFR_guac_display_layer_png_optimality(layer, rect) < 0; + +} + +/** + * Returns whether the given rectangle would be optimally encoded as WebP + * rather than PNG. + * + * @param layer + * The layer to be queried. + * + * @param rect + * The rectangle to check. + * + * @param framerate + * The rate that the region covered by the given rectangle has historically + * been being updated within the given layer, in frames per second. + * + * @return + * Non-zero if the rectangle would be optimally encoded as WebP, zero + * otherwise. + */ +static int LFR_guac_display_layer_should_use_webp(guac_display_layer* layer, + const guac_rect* rect, int framerate) { + + /* Do not use WebP if not supported */ + if (!guac_client_supports_webp(layer->display->client)) + return 0; + + /* WebP is preferred if: + * - frame rate is high enough + * - PNG is not more optimal based on image contents */ + return framerate >= GUAC_DISPLAY_JPEG_FRAMERATE + && LFR_guac_display_layer_png_optimality(layer, rect) < 0; + +} + +void* guac_display_worker_thread(void* data) { + + int framerate; + int has_outstanding_frames = 0; + + guac_display* display = (guac_display*) data; + guac_client* client = display->client; + guac_socket* socket = client->socket; + + guac_display_plan_operation op; + while (guac_fifo_dequeue_and_lock(&display->ops, &op)) { + + /* Notify any watchers of render_state that a frame is now in progress */ + guac_flag_set_and_lock(&display->render_state, GUAC_DISPLAY_RENDER_STATE_FRAME_IN_PROGRESS); + guac_flag_clear(&display->render_state, GUAC_DISPLAY_RENDER_STATE_FRAME_NOT_IN_PROGRESS); + guac_flag_unlock(&display->render_state); + + /* NOTE: Any thread that locks the operation queue can know that there + * are no pending operations in progress if the queue is empty and + * there are no active workers */ + display->active_workers++; + guac_fifo_unlock(&display->ops); + + guac_rwlock_acquire_read_lock(&display->last_frame.lock); + guac_display_layer* display_layer = op.layer; + switch (op.type) { + + case GUAC_DISPLAY_PLAN_OPERATION_IMG: + + framerate = INT_MAX; + if (op.current_frame > op.last_frame) + framerate = 1000 / (op.current_frame - op.last_frame); + + guac_rect* dirty = &op.dest; + + /* TODO: Determine whether to use PNG/WebP/JPEG purely + * based on whether lossless encoding is required, the + * expected time until another frame is received (time + * since last frame), and estimated encoding times. The + * time allowed per update should be divided up + * proportionately based on the dirty_size of the update. */ + + /* TODO: Stream PNG/WebP/JPEG using progressive encoding such + * that a frame that is currently being encoded can be + * preempted by the next frame, with the connected client then + * simply receiving a lower-quality intermediate frame. If + * necessary, progressive encoding can be achieved by manually + * dividing images into multiple reduced-resolution stages, + * such that each image streamed is actually only one quarter + * the size of the original image. Compositing via Guacamole + * protocol instructions can reassemble those stages. */ + + cairo_surface_t* rect = LFR_guac_display_layer_cairo_rect(display_layer, dirty); + const guac_layer* layer = display_layer->layer; + + /* Clear relevant rect of destination layer if necessary to + * ensure fresh data is not drawn on top of old data for layers + * with alpha transparency */ + guac_display_layer_clear_non_opaque(display_layer, dirty); + + /* Prefer WebP when reasonable */ + if (LFR_guac_display_layer_should_use_webp(display_layer, dirty, framerate)) + guac_client_stream_webp(client, socket, GUAC_COMP_OVER, layer, + dirty->left, dirty->top, rect, + guac_display_suggest_quality(client), + display_layer->last_frame.lossless ? 1 : 0); + + /* If not WebP, JPEG is the next best (lossy) choice */ + else if (display_layer->opaque && LFR_guac_display_layer_should_use_jpeg(display_layer, dirty, framerate)) + guac_client_stream_jpeg(client, socket, GUAC_COMP_OVER, layer, + dirty->left, dirty->top, rect, + guac_display_suggest_quality(client)); + + /* Use PNG if no lossy formats are appropriate */ + else + guac_client_stream_png(client, socket, GUAC_COMP_OVER, + layer, dirty->left, dirty->top, rect); + + cairo_surface_destroy(rect); + break; + + case GUAC_DISPLAY_PLAN_OPERATION_COPY: + case GUAC_DISPLAY_PLAN_OPERATION_RECT: + case GUAC_DISPLAY_PLAN_OPERATION_NOP: + guac_client_log(client, GUAC_LOG_DEBUG, "Operation type %i " + "should NOT be present in the set of operations given " + "to guac_display worker thread. All operations except " + "IMG and END_FRAME are handled during the initial, " + "single-threaded flush step. This is likely a bug.", + op.type); + break; + + case GUAC_DISPLAY_PLAN_END_FRAME: + + guac_fifo_lock(&display->ops); + int other_workers_busy = (display->active_workers > 1); + guac_fifo_unlock(&display->ops); + + /* If other workers are still busy, push the frame boundary + * back on the queue so that it's picked up by one of those + * workers */ + if (other_workers_busy) { + guac_fifo_enqueue(&display->ops, &op); + } + + /* Otherwise, we've reached the end of the frame, and this is + * the worker that will be sending that boundary to connected + * users */ + else { + + /* Update the mouse cursor if it's been changed since the + * last frame */ + guac_display_layer* cursor = display->cursor_buffer; + if (!guac_rect_is_empty(&cursor->last_frame.dirty)) { + guac_protocol_send_cursor(client->socket, + display->last_frame.cursor_hotspot_x, + display->last_frame.cursor_hotspot_y, + cursor->layer, 0, 0, + cursor->last_frame.width, + cursor->last_frame.height); + } + + /* Use the amount of time that the client has been waiting + * for a frame vs. the amount of time that it took the + * client to process the most recently acknowledged frame + * to calculate the amount of additional delay required to + * allow the client to catch up. This value is used later, + * after everything else related to the frame has been + * finalized. */ + int time_since_last_frame = guac_timestamp_current() - client->last_sent_timestamp; + int processing_lag = guac_client_get_processing_lag(client); + int required_wait = processing_lag - time_since_last_frame; + + /* Allow connected clients to move forward with rendering */ + guac_client_end_multiple_frames(client, display->last_frame.frames); + + /* While connected clients moves forward with rendering, + * commit any changed contents to client-side backing buffer */ + guac_display_layer* current = display->last_frame.layers; + while (current != NULL) { + + /* Save a copy of the changed region if the layer has + * been modified since the last frame */ + guac_rect* dirty = ¤t->last_frame.dirty; + if (!guac_rect_is_empty(dirty)) { + + int x = dirty->left; + int y = dirty->top; + int width = guac_rect_width(dirty); + int height = guac_rect_height(dirty); + + /* Ensure destination region is cleared out first if the alpha channel need be considered, + * as GUAC_COMP_OVER is significantly faster than GUAC_COMP_SRC on the browser side */ + if (!current->opaque) { + guac_protocol_send_rect(client->socket, current->last_frame_buffer, x, y, width, height); + guac_protocol_send_cfill(client->socket, GUAC_COMP_RATOP, current->last_frame_buffer, + 0x00, 0x00, 0x00, 0x00); + } + + guac_protocol_send_copy(client->socket, + current->layer, x, y, width, height, + GUAC_COMP_OVER, current->last_frame_buffer, x, y); + + } + + current = current->last_frame.next; + + } + + /* Include an additional frame boundary to allow the client to also move forward with committing + * changes to the backing buffer while the server is receiving and preparing the next frame */ + guac_client_end_multiple_frames(client, 0); + + /* This is now absolutely everything for the current frame, + * and it's safe to flush any outstanding data */ + guac_socket_flush(client->socket); + + /* Notify any watchers of render_state that a frame is no longer in progress */ + guac_flag_set_and_lock(&display->render_state, GUAC_DISPLAY_RENDER_STATE_FRAME_NOT_IN_PROGRESS); + guac_flag_clear(&display->render_state, GUAC_DISPLAY_RENDER_STATE_FRAME_IN_PROGRESS); + guac_flag_unlock(&display->render_state); + + /* Exclude local, server-side frame processing latency from + * waiting period */ + int latency = (int) (guac_timestamp_current() - display->last_frame.timestamp); + if (latency >= 0) { + guac_client_log(display->client, GUAC_LOG_TRACE, + "Rendering latency: %ims (%i:1 frame)\n", + latency, display->last_frame.frames); + required_wait -= latency; + } + + /* Ensure we don't wait without bound when compensating for + * client-side processing delays */ + if (required_wait > GUAC_DISPLAY_MAX_LAG_COMPENSATION) + required_wait = GUAC_DISPLAY_MAX_LAG_COMPENSATION; + + /* Allow connected clients to catch up if they're taking + * longer to process frames than the server is taking to + * generate them */ + if (required_wait > 0) { + guac_client_log(display->client, GUAC_LOG_TRACE, + "Waiting %ims to compensate for client-side " + "processing delays.\n", required_wait); + guac_timestamp_msleep(required_wait); + } + + guac_fifo_lock(&display->ops); + has_outstanding_frames = display->frame_deferred; + guac_fifo_unlock(&display->ops); + + } + + break; + + } + + guac_rwlock_release_lock(&display->last_frame.lock); + + guac_fifo_lock(&display->ops); + display->active_workers--; + guac_fifo_unlock(&display->ops); + + /* Trigger additional flush if frames were completed while we were + * still processing the previous frame */ + if (has_outstanding_frames) { + guac_display_end_multiple_frames(display, 0); + has_outstanding_frames = 0; + } + + } + + return NULL; + +} diff --git a/src/libguac/display.c b/src/libguac/display.c new file mode 100644 index 000000000..3e080e36b --- /dev/null +++ b/src/libguac/display.c @@ -0,0 +1,351 @@ +/* + * 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 "config.h" +#include "display-plan.h" +#include "display-priv.h" +#include "guacamole/client.h" +#include "guacamole/display.h" +#include "guacamole/fifo.h" +#include "guacamole/layer.h" +#include "guacamole/mem.h" +#include "guacamole/protocol.h" +#include "guacamole/rect.h" +#include "guacamole/rwlock.h" +#include "guacamole/socket.h" +#include "guacamole/timestamp.h" +#include "guacamole/user.h" + +#ifdef __MINGW32__ +#include +#endif + +#include +#include +#include +#include + +/** + * The number of worker threads to create per processor. + */ +#define GUAC_DISPLAY_CPU_THREAD_FACTOR 1 + +/** + * Returns the number of processors available to this process. If possible, + * limits on otherwise available processors like CPU affinity will be taken + * into account. If the number of available processors cannot be determined, + * zero is returned. + * + * @return + * The number of available processors, or zero if this value cannot be + * determined for any reason. + */ +static unsigned long guac_display_nproc() { + +#if defined(HAVE_SCHED_GETAFFINITY) + + /* Linux, etc. implementation leveraging sched_getaffinity() (this is + * specific to glibc and MUSL libc and is non-portable) */ + + cpu_set_t cpu_set; + CPU_ZERO(&cpu_set); + + if (sched_getaffinity(0, sizeof(cpu_set), &cpu_set) == 0) { + long cpu_count = CPU_COUNT(&cpu_set); + if (cpu_count > 0) + return cpu_count; + } + +#elif defined(_SC_NPROCESSORS_ONLN) + + /* Linux, etc. implementation leveraging sysconf() and _SC_NPROCESSORS_ONLN + * (which is also non-portable) */ + + long cpu_count = sysconf(_SC_NPROCESSORS_ONLN); + if (cpu_count > 0) + return cpu_count; + +#elif defined(__MINGW32__) + + /* Windows-specific implementation (clearly also non-portable) */ + + unsigned long cpu_count = 0; + DWORD_PTR process_mask, system_mask; + for (GetProcessAffinityMask(GetCurrentProcess(), &process_mask, &system_mask); + process_mask != 0; process_mask >>= 1) { + + if (process_mask & 1) + cpu_count++; + + } + + if (cpu_count > 0) + return cpu_count; + +#else + + /* Fallback implementation that does not query the number of CPUs available + * at all, returning an error code (as portable as it gets) */ + + long cpu_count = 0; + +#endif + + return 0; + +} + +guac_display* guac_display_alloc(guac_client* client) { + + /* Allocate and init core properties (really just the client pointer) */ + guac_display* display = guac_mem_zalloc(sizeof(guac_display)); + display->client = client; + + /* Init last frame and pending frame tracking */ + guac_rwlock_init(&display->last_frame.lock); + guac_rwlock_init(&display->pending_frame.lock); + display->last_frame.timestamp = display->pending_frame.timestamp = guac_timestamp_current(); + + /* It's safe to discard const of the default layer here, as + * guac_display_free_layer() function is specifically written to consider + * the default layer as const */ + display->default_layer = guac_display_add_layer(display, (guac_layer*) GUAC_DEFAULT_LAYER, 1); + display->cursor_buffer = guac_display_alloc_buffer(display, 0); + + /* Init operation FIFO used by worker threads */ + guac_fifo_init(&display->ops, display->ops_items, + GUAC_DISPLAY_WORKER_FIFO_SIZE, sizeof(guac_display_plan_operation)); + + /* Init flag used to notify threads that need to monitor whether a frame is + * currently being rendered */ + guac_flag_init(&display->render_state); + guac_flag_set(&display->render_state, GUAC_DISPLAY_RENDER_STATE_FRAME_NOT_IN_PROGRESS); + + int cpu_count = guac_display_nproc(); + if (cpu_count <= 0) { + guac_client_log(client, GUAC_LOG_WARNING, "Number of available " + "processors could not be determined. Assuming single-processor."); + cpu_count = 1; + } + else { + guac_client_log(client, GUAC_LOG_INFO, "Local system reports %i " + "processor(s) are available.", cpu_count); + } + + display->worker_thread_count = cpu_count * GUAC_DISPLAY_CPU_THREAD_FACTOR; + display->worker_threads = guac_mem_alloc(display->worker_thread_count, sizeof(pthread_t)); + guac_client_log(client, GUAC_LOG_INFO, "Graphical updates will be encoded " + "using %i worker thread(s).", display->worker_thread_count); + + /* Now that the core of the display has been fully initialized, it's safe + * to start the worker threads */ + for (int i = 0; i < display->worker_thread_count; i++) + pthread_create(&(display->worker_threads[i]), NULL, guac_display_worker_thread, display); + + return display; + +} + +void guac_display_free(guac_display* display) { + + /* Stop further use of the operation FIFO */ + guac_fifo_invalidate(&display->ops); + + /* Wait for all worker threads to terminate (they should nearly immediately + * terminate following invalidation of the FIFO) */ + for (int i = 0; i < display->worker_thread_count; i++) + pthread_join(display->worker_threads[i], NULL); + + /* All locks, FIFOs, etc. are now unused and can be safely destroyed */ + guac_flag_destroy(&display->render_state); + guac_fifo_destroy(&display->ops); + guac_rwlock_destroy(&display->last_frame.lock); + guac_rwlock_destroy(&display->pending_frame.lock); + + /* Free all layers within the pending_frame list (NOTE: This will also free + * those layers from the last_frame list) */ + while (display->pending_frame.layers != NULL) + guac_display_free_layer(display->pending_frame.layers); + + /* Free any remaining layers that were present only on the last_frame list + * and not on the pending_frame list */ + while (display->last_frame.layers != NULL) + guac_display_free_layer(display->last_frame.layers); + + guac_mem_free(display->worker_threads); + guac_mem_free(display); + +} + +void guac_display_dup(guac_display* display, guac_socket* socket) { + + guac_client* client = display->client; + guac_rwlock_acquire_read_lock(&display->last_frame.lock); + + /* Wait for any pending frame to finish being sent to established users of + * the connection before syncing any new users (doing otherwise could + * result in trailing instructions of that pending frame getting sent to + * new users after they finish joining, even though they are already in + * sync with that frame, and those trailing instructions may not have the + * intended meaning in context of the new users' remote displays) */ + guac_flag_wait_and_lock(&display->render_state, + GUAC_DISPLAY_RENDER_STATE_FRAME_NOT_IN_PROGRESS); + + /* Sync the state of all layers/buffers */ + guac_display_layer* current = display->last_frame.layers; + while (current != NULL) { + + const guac_layer* layer = current->layer; + + guac_rect layer_bounds; + guac_display_layer_get_bounds(current, &layer_bounds); + + int width = guac_rect_width(&layer_bounds); + int height = guac_rect_height(&layer_bounds); + guac_protocol_send_size(socket, layer, width, height); + + if (width > 0 && height > 0) { + + /* Get Cairo surface covering layer bounds */ + unsigned char* buffer = GUAC_DISPLAY_LAYER_STATE_MUTABLE_BUFFER(current->last_frame, layer_bounds); + cairo_surface_t* rect = cairo_image_surface_create_for_data(buffer, + current->opaque ? CAIRO_FORMAT_RGB24 : CAIRO_FORMAT_ARGB32, + width, height, current->last_frame.buffer_stride); + + /* Send PNG for rect */ + guac_client_stream_png(client, socket, GUAC_COMP_OVER, layer, 0, 0, rect); + + /* Resync copy of previous frame */ + guac_protocol_send_copy(socket, + layer, 0, 0, width, height, + GUAC_COMP_OVER, current->last_frame_buffer, 0, 0); + + cairo_surface_destroy(rect); + + } + + /* Resync any properties that are specific to non-buffer layers */ + if (current->layer->index > 0) { + + /* Resync layer opacity */ + guac_protocol_send_shade(socket, current->layer, + current->last_frame.opacity); + + /* Resync layer position/hierarchy */ + guac_protocol_send_move(socket, current->layer, + current->last_frame.parent, + current->last_frame.x, + current->last_frame.y, + current->last_frame.z); + + } + + /* Resync multitouch support */ + if (current->layer->index >= 0) { + guac_protocol_send_set_int(socket, current->layer, + GUAC_PROTOCOL_LAYER_PARAMETER_MULTI_TOUCH, + current->last_frame.touches); + } + + current = current->last_frame.next; + + } + + /* Synchronize mouse cursor */ + guac_display_layer* cursor = display->cursor_buffer; + guac_protocol_send_cursor(socket, + display->last_frame.cursor_hotspot_x, + display->last_frame.cursor_hotspot_y, + cursor->layer, 0, 0, + cursor->last_frame.width, + cursor->last_frame.height); + + /* Synchronize mouse location */ + guac_protocol_send_mouse(socket, display->last_frame.cursor_x, display->last_frame.cursor_y, + display->last_frame.cursor_mask, client->last_sent_timestamp); + + /* The initial frame synchronizing the newly-joined users is now complete */ + guac_protocol_send_sync(socket, client->last_sent_timestamp, display->last_frame.frames); + + /* Further rendering for the current connection can now safely continue */ + guac_flag_unlock(&display->render_state); + guac_rwlock_release_lock(&display->last_frame.lock); + + guac_socket_flush(socket); + +} + +void guac_display_notify_user_left(guac_display* display, guac_user* user) { + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + + /* Update to reflect leaving user, if necessary */ + if (display->pending_frame.cursor_user == user) + display->pending_frame.cursor_user = NULL; + + guac_rwlock_release_lock(&display->pending_frame.lock); +} + +void guac_display_notify_user_moved_mouse(guac_display* display, guac_user* user, int x, int y, int mask) { + + guac_rwlock_acquire_write_lock(&display->pending_frame.lock); + display->pending_frame.cursor_user = user; + display->pending_frame.cursor_x = x; + display->pending_frame.cursor_y = y; + display->pending_frame.cursor_mask = mask; + guac_rwlock_release_lock(&display->pending_frame.lock); + + guac_display_end_mouse_frame(display); + +} + +guac_display_layer* guac_display_default_layer(guac_display* display) { + return display->default_layer; +} + +guac_display_layer* guac_display_alloc_layer(guac_display* display, int opaque) { + return guac_display_add_layer(display, guac_client_alloc_layer(display->client), opaque); +} + +guac_display_layer* guac_display_alloc_buffer(guac_display* display, int opaque) { + return guac_display_add_layer(display, guac_client_alloc_buffer(display->client), opaque); +} + +void guac_display_free_layer(guac_display_layer* display_layer) { + + guac_display* display = display_layer->display; + const guac_layer* layer = display_layer->layer; + + guac_display_remove_layer(display_layer); + + if (layer->index != 0) { + + guac_client* client = display->client; + guac_protocol_send_dispose(client->socket, layer); + + /* As long as this isn't the display layer, it's safe to cast away the + * constness and free the underlying layer/buffer. Only the default + * layer (layer #0) is truly const. */ + if (layer->index > 0) + guac_client_free_layer(client, (guac_layer*) layer); + else + guac_client_free_buffer(client, (guac_layer*) layer); + + } + +} diff --git a/src/libguac/encode-webp.c b/src/libguac/encode-webp.c index 43c5a00c5..02411cddf 100644 --- a/src/libguac/encode-webp.c +++ b/src/libguac/encode-webp.c @@ -194,20 +194,26 @@ int guac_webp_write(guac_socket* socket, guac_stream* stream, /* Add additional tuning */ config.lossless = lossless; config.quality = quality; - config.thread_level = 1; /* Multi threaded */ + config.thread_level = 0; /* NOT multi-threaded (threading results in unnecessary overhead vs. the worker threads used by guac_display) */ config.method = 2; /* Compression method (0=fast/larger, 6=slow/smaller) */ /* Validate configuration */ - WebPValidateConfig(&config); + if (!WebPValidateConfig(&config)) { + return -1; + } /* Set up WebP picture */ - WebPPictureInit(&picture); + if (!WebPPictureInit(&picture)) { + return -1; + } picture.use_argb = 1; picture.width = width; picture.height = height; /* Allocate and init writer */ - WebPPictureAlloc(&picture); + if (!WebPPictureAlloc(&picture)) { + return -1; + } picture.writer = guac_webp_stream_write; picture.custom_ptr = &writer; guac_webp_stream_writer_init(&writer, socket, stream); @@ -244,7 +250,7 @@ int guac_webp_write(guac_socket* socket, guac_stream* stream, } /* Encode image */ - WebPEncode(&config, &picture); + const int result = WebPEncode(&config, &picture) ? 0 : -1; /* Free picture */ WebPPictureFree(&picture); @@ -252,7 +258,7 @@ int guac_webp_write(guac_socket* socket, guac_stream* stream, /* Ensure all data is written */ guac_webp_flush_data(&writer); - return 0; + return result; } diff --git a/src/libguac/fifo.c b/src/libguac/fifo.c new file mode 100644 index 000000000..30aef7c6e --- /dev/null +++ b/src/libguac/fifo.c @@ -0,0 +1,210 @@ +/* + * 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 "guacamole/fifo.h" +#include "guacamole/flag.h" + +#include +#include +#include + +void guac_fifo_init(guac_fifo* fifo, void* items, + size_t max_items, size_t item_size) { + + /* Init values describing the memory structure of the items array */ + fifo->items_offset = (char*) items - (char*) fifo; + fifo->max_items = max_items; + fifo->item_size = item_size; + + /* The fifo is currently empty */ + guac_flag_init(&fifo->state); + guac_flag_set(&fifo->state, GUAC_FIFO_STATE_READY); + fifo->head = 0; + fifo->item_count = 0; + +} + +void guac_fifo_destroy(guac_fifo* fifo) { + guac_flag_destroy(&fifo->state); +} + +void guac_fifo_invalidate(guac_fifo* fifo) { + guac_flag_set(&fifo->state, GUAC_FIFO_STATE_INVALID); +} + +void guac_fifo_lock(guac_fifo* fifo) { + guac_flag_lock(&fifo->state); +} + +void guac_fifo_unlock(guac_fifo* fifo) { + guac_flag_unlock(&fifo->state); +} + +int guac_fifo_is_valid(guac_fifo* fifo) { + /* We don't need to acquire the lock here as (1) we are only reading the + * flag and (2) the flag in question is a one-way, single-use signal (it's + * only set, never cleared) */ + return !(fifo->state.value & GUAC_FIFO_STATE_INVALID); +} + +int guac_fifo_enqueue(guac_fifo* fifo, + const void* item) { + + if (!guac_fifo_enqueue_and_lock(fifo, item)) + return 0; + + guac_flag_unlock(&fifo->state); + return 1; + +} + +int guac_fifo_enqueue_and_lock(guac_fifo* fifo, + const void* item) { + + /* Block until fifo is ready for further items OR until the fifo is + * invalidated */ + guac_flag_wait_and_lock(&fifo->state, + GUAC_FIFO_STATE_INVALID | GUAC_FIFO_STATE_READY); + + /* Bail out if the fifo has become invalid */ + if (fifo->state.value & GUAC_FIFO_STATE_INVALID) { + guac_flag_unlock(&fifo->state); + return 0; + } + + /* Abort program execution entirely if the fifo reports readiness but + * somehow actually does not have available space (this should never happen + * and indicates a bug) */ + if (fifo->item_count >= fifo->max_items) + abort(); + + /* Update count of items within the fifo, clearing the readiness flag if + * there is no longer any space for further items */ + fifo->item_count++; + if (fifo->item_count == fifo->max_items) + guac_flag_clear(&fifo->state, GUAC_FIFO_STATE_READY); + + /* NOTE: At this point, there are `item_count - 1` items present in the + * fifo, and `item_count - 1` is the index of the space in the items array + * that should receive the item being added (relative to head) */ + + /* Copy data of item buffer into last item in fifo */ + size_t tail = (fifo->head + fifo->item_count - 1) % fifo->max_items; + void* tail_item = ((char*) fifo) + fifo->items_offset + fifo->item_size * tail; + memcpy(tail_item, item, fifo->item_size); + + /* Advise any waiting threads that the fifo is now non-empty */ + guac_flag_set(&fifo->state, GUAC_FIFO_STATE_NONEMPTY); + + /* Item enqueued successfully */ + return 1; + +} + +/** + * Dequeues a single item from the given guac_fifo, storing a copy + * of that item in the provided buffer. The event fifo MUST be non-empty. The + * state flag of the fifo MUST already be locked. + * + * @param fifo + * The guac_fifo to dequeue an item from. + * + * @param item + * The buffer that should receive a copy of the dequeued item. + */ +static void dequeue(guac_fifo* fifo, void* item) { + + /* Copy data of first item in fifo to provided output buffer */ + void* head_item = ((char*) fifo) + fifo->items_offset + fifo->item_size * fifo->head; + memcpy(item, head_item, fifo->item_size); + + /* Advance to next item in fifo, if any */ + fifo->item_count--; + fifo->head = (fifo->head + 1) % fifo->max_items; + + /* Keep state flag up-to-date with respect to non-emptiness ... */ + if (fifo->item_count == 0) + guac_flag_clear(&fifo->state, GUAC_FIFO_STATE_NONEMPTY); + + /* ... and readiness for further items */ + guac_flag_set(&fifo->state, GUAC_FIFO_STATE_READY); + + /* Item has been dequeued successfully */ + +} + +int guac_fifo_dequeue(guac_fifo* fifo, void* item) { + + if (!guac_fifo_dequeue_and_lock(fifo, item)) + return 0; + + guac_flag_unlock(&fifo->state); + return 1; + +} + +int guac_fifo_timed_dequeue(guac_fifo* fifo, + void* item, int msec_timeout) { + + if (!guac_fifo_timed_dequeue_and_lock(fifo, item, msec_timeout)) + return 0; + + guac_flag_unlock(&fifo->state); + return 1; + +} + +int guac_fifo_dequeue_and_lock(guac_fifo* fifo, void* item) { + + /* Block indefinitely while waiting for an item to be added, but bail out + * if the fifo becomes invalid */ + guac_flag_wait_and_lock(&fifo->state, + GUAC_FIFO_STATE_NONEMPTY | GUAC_FIFO_STATE_INVALID); + + if (fifo->state.value & GUAC_FIFO_STATE_INVALID) { + guac_flag_unlock(&fifo->state); + return 0; + } + + dequeue(fifo, item); + return 1; + +} + +int guac_fifo_timed_dequeue_and_lock(guac_fifo* fifo, + void* item, int msec_timeout) { + + /* Wait up to timeout for an item to be present in the fifo, failing if no + * items enter the fifo before the timeout lapses */ + if (!guac_flag_timedwait_and_lock(&fifo->state, + GUAC_FIFO_STATE_NONEMPTY | GUAC_FIFO_STATE_INVALID, + msec_timeout)) { + return 0; + } + + if (fifo->state.value & GUAC_FIFO_STATE_INVALID) { + guac_flag_unlock(&fifo->state); + return 0; + } + + dequeue(fifo, item); + return 1; + +} + diff --git a/src/libguac/flag.c b/src/libguac/flag.c new file mode 100644 index 000000000..5f44ac31e --- /dev/null +++ b/src/libguac/flag.c @@ -0,0 +1,167 @@ +/* + * 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 "guacamole/flag.h" + +#include +#include +#include +#include +#include + +/** + * The number of nanoseconds in a whole second. + */ +#define NANOS_PER_SECOND 1000000000L + +void guac_flag_init(guac_flag* event_flag) { + + /* The condition used by guac_flag to signal changes in its + * value must be safe to share between processes, and must use the + * system-wide monotonic clock (not the realtime clock, which is subject to + * time changes) */ + pthread_condattr_t cond_attr; + pthread_condattr_init(&cond_attr); + pthread_condattr_setclock(&cond_attr, CLOCK_MONOTONIC); + pthread_condattr_setpshared(&cond_attr, PTHREAD_PROCESS_SHARED); + pthread_cond_init(&event_flag->value_changed, &cond_attr); + + /* In addition to being safe to share between processes, the mutex used by + * guac_flag to guard concurrent access to its value (AND to + * signal changes in its value) must be recursive (you can lock the mutex + * again even if the current thread has already locked it) */ + pthread_mutexattr_t mutex_attr; + pthread_mutexattr_init(&mutex_attr); + pthread_mutexattr_setpshared(&mutex_attr, PTHREAD_PROCESS_SHARED); + pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(&event_flag->value_mutex, &mutex_attr); + + /* The initial value of all flags is unset (0) */ + event_flag->value = 0; + +} + +void guac_flag_destroy(guac_flag* event_flag) { + pthread_cond_destroy(&event_flag->value_changed); + pthread_mutex_destroy(&event_flag->value_mutex); +} + +void guac_flag_set_and_lock(guac_flag* event_flag, + unsigned int flags) { + + guac_flag_lock(event_flag); + + /* Set specific bits of flag, leaving other bits unaffected */ + unsigned int old_value = event_flag->value; + event_flag->value |= flags; + + /* Signal other threads only if flag has changed as a result of this call */ + if (event_flag->value != old_value) + pthread_cond_broadcast(&event_flag->value_changed); + +} + +void guac_flag_set(guac_flag* event_flag, + unsigned int flags) { + guac_flag_set_and_lock(event_flag, flags); + guac_flag_unlock(event_flag); +} + +void guac_flag_clear_and_lock(guac_flag* event_flag, + unsigned int flags) { + + guac_flag_lock(event_flag); + + /* Clear specific bits of flag, leaving other bits unaffected */ + event_flag->value &= ~flags; + + /* NOTE: Other threads are NOT signalled here. Threads wait only for flags + * to be set, not for flags to be cleared. */ + +} + +void guac_flag_clear(guac_flag* event_flag, + unsigned int flags) { + guac_flag_clear_and_lock(event_flag, flags); + guac_flag_unlock(event_flag); +} + +void guac_flag_lock(guac_flag* event_flag) { + pthread_mutex_lock(&event_flag->value_mutex); +} + +void guac_flag_unlock(guac_flag* event_flag) { + pthread_mutex_unlock(&event_flag->value_mutex); +} + +void guac_flag_wait_and_lock(guac_flag* event_flag, + unsigned int flags) { + + guac_flag_lock(event_flag); + + /* Continue waiting until at least one of the desired flags has been set */ + while (!(event_flag->value & flags)) { + + /* Wait for any change to any flags, bailing out if something is wrong + * that would prevent waiting from ever succeeding (such a failure + * would turn this into a busy loop) */ + if (pthread_cond_wait(&event_flag->value_changed, + &event_flag->value_mutex)) { + abort(); /* This should not happen except due to a bug */ + } + + } + + /* If we reach this point, at least one of the desired flags has been set, + * and it is intentional that we continue to hold the lock (acquired on + * behalf of the caller) */ + +} + +int guac_flag_timedwait_and_lock(guac_flag* event_flag, + unsigned int flags, unsigned int msec_timeout) { + + guac_flag_lock(event_flag); + + struct timespec ts_timeout; + clock_gettime(CLOCK_MONOTONIC, &ts_timeout); + + uint64_t nsec_timeout = msec_timeout * 1000000 + ts_timeout.tv_nsec; + ts_timeout.tv_sec += nsec_timeout / NANOS_PER_SECOND; + ts_timeout.tv_nsec = nsec_timeout % NANOS_PER_SECOND; + + /* Continue waiting until at least one of the desired flags has been set */ + while (!(event_flag->value & flags)) { + + /* Wait for any change to any flags, failing if a timeout occurs */ + if (pthread_cond_timedwait(&event_flag->value_changed, + &event_flag->value_mutex, &ts_timeout)) { + guac_flag_unlock(event_flag); + return 0; + } + + } + + /* If we reach this point, at least one of the desired flags has been set, + * and it is intentional that we continue to hold the lock (acquired on + * behalf of the caller) */ + return 1; + +} + diff --git a/src/libguac/guacamole/assert.h b/src/libguac/guacamole/assert.h new file mode 100644 index 000000000..f8fd246ca --- /dev/null +++ b/src/libguac/guacamole/assert.h @@ -0,0 +1,47 @@ +/* + * 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_ASSERT_H +#define GUAC_ASSERT_H + +#include +#include + +/** + * Performs a runtime assertion that verifies the given condition evaluates to + * true (non-zero). If the condition evaluates to false (zero), execution is + * aborted with abort(). + * + * This macro should be used only in cases where the performance impact of + * verifying the assertion is negligible and it is benificial to always verify + * the assertion. Unlike the standard assert(), this macro will never be + * omitted by the compiler. + * + * @param expression + * The condition to test. + */ +#define GUAC_ASSERT(expression) do { \ + if (!(expression)) { \ + fprintf(stderr, "GUAC_ASSERT in %s() failed at %s:%i.\n", \ + __func__, __FILE__, __LINE__); \ + abort(); \ + } \ + } while(0) + +#endif diff --git a/src/libguac/guacamole/client-constants.h b/src/libguac/guacamole/client-constants.h index daf45b0f8..5683278dc 100644 --- a/src/libguac/guacamole/client-constants.h +++ b/src/libguac/guacamole/client-constants.h @@ -30,7 +30,7 @@ * The maximum number of inbound or outbound streams supported by any one * guac_client. */ -#define GUAC_CLIENT_MAX_STREAMS 64 +#define GUAC_CLIENT_MAX_STREAMS 512 /** * The index of a closed stream. diff --git a/src/libguac/guacamole/client.h b/src/libguac/guacamole/client.h index ccc9039cd..01ecbc842 100644 --- a/src/libguac/guacamole/client.h +++ b/src/libguac/guacamole/client.h @@ -182,7 +182,8 @@ struct guac_client { /** * Lock which is acquired when the pending users list is being manipulated, - * or when the pending users list is being iterated. + * or iterated, or when checking/altering the + * __pending_users_thread_started flag. */ guac_rwlock __pending_users_lock; @@ -192,18 +193,14 @@ struct guac_client { * use within the client. This will be NULL until the first user joins * the connection, as it is lazily instantiated at that time. */ - timer_t __pending_users_timer; + pthread_t __pending_users_thread; /** - * A flag storing the current state of the pending users timer. + * Whether the pending users thread has started for this guac_client. The + * __pending_users_lock must be acquired before checking or altering this + * value. */ - int __pending_users_timer_state; - - /** - * A mutex that must be acquired before modifying or checking the value of - * the timer state. - */ - pthread_mutex_t __pending_users_timer_mutex; + int __pending_users_thread_started; /** * The first user within the list of connected users who have not yet had @@ -586,18 +583,47 @@ void* guac_client_for_user(guac_client* client, guac_user* user, guac_user_callback* callback, void* data); /** - * Marks the end of the current frame by sending a "sync" instruction to - * all connected users. This instruction will contain the current timestamp. - * The last_sent_timestamp member of guac_client will be updated accordingly. + * Marks the end of the current frame by sending a "sync" instruction to all + * connected users, where the number of input frames that were considered in + * creating this frame is either unknown or inapplicable. This instruction will + * contain the current timestamp. The last_sent_timestamp member of guac_client + * will be updated accordingly. * * If an error occurs sending the instruction, a non-zero value is * returned, and guac_error is set appropriately. * - * @param client The guac_client which has finished a frame. - * @return Zero on success, non-zero on error. + * @param client + * The guac_client which has finished a frame. + * + * @return + * Zero on success, non-zero on error. */ int guac_client_end_frame(guac_client* client); +/** + * Marks the end of the current frame by sending a "sync" instruction to all + * connected users, where that frame may combine or otherwise represent the + * changes of an arbitrary number of input frames. This instruction will + * contain the current timestamp, as well as the number of frames that were + * considered in creating that frame. The last_sent_timestamp member of + * guac_client will be updated accordingly. + * + * If an error occurs sending the instruction, a non-zero value is + * returned, and guac_error is set appropriately. + * + * @param client + * The guac_client which has finished a frame. + * + * @param frames + * The number of distinct frames that were considered or combined when + * generating the current frame, or zero if the boundaries of relevant + * frames are unknown. + * + * @return + * Zero on success, non-zero on error. + */ +int guac_client_end_multiple_frames(guac_client* client, int frames); + /** * Initializes the given guac_client using the initialization routine provided * by the plugin corresponding to the named protocol. This will automatically diff --git a/src/libguac/guacamole/display-constants.h b/src/libguac/guacamole/display-constants.h new file mode 100644 index 000000000..0f015a646 --- /dev/null +++ b/src/libguac/guacamole/display-constants.h @@ -0,0 +1,56 @@ +/* + * 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_DISPLAY_CONSTANTS_H +#define GUAC_DISPLAY_CONSTANTS_H + +/** + * @addtogroup display + * @{ + */ + +/** + * Provides constants related to the abstract display implementation + * (guac_display). + * + * @file display-constants.h + */ + +/** + * The maximum width of any guac_display, in pixels. + */ +#define GUAC_DISPLAY_MAX_WIDTH 8192 + +/** + * The maximum height of any guac_display, in pixels. + */ +#define GUAC_DISPLAY_MAX_HEIGHT 8192 + +/** + * The number of bytes in each pixel of raw image data within a + * guac_display_layer, as made accessible through a call to + * guac_display_layer_open_raw(). + */ +#define GUAC_DISPLAY_LAYER_RAW_BPP 4 + +/** + * @} + */ + +#endif diff --git a/src/libguac/guacamole/display-types.h b/src/libguac/guacamole/display-types.h new file mode 100644 index 000000000..61bbb4061 --- /dev/null +++ b/src/libguac/guacamole/display-types.h @@ -0,0 +1,106 @@ +/* + * 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_DISPLAY_TYPES_H +#define GUAC_DISPLAY_TYPES_H + +/** + * @addtogroup display + * @{ + */ + +/** + * Provides type definitions related to the abstract display implementation + * (guac_display). + * + * @file display-types.h + */ + +/** + * Opaque representation of the remote (client-side) display of a Guacamole + * connection (guac_client). + */ +typedef struct guac_display guac_display; + +/** + * Opaque representation of a thread that continuously renders updated + * graphical data to the remote display. + */ +typedef struct guac_display_render_thread guac_display_render_thread; + +/** + * Opaque representation of a layer within a guac_display. This may be a + * visible layer or an off-screen buffer, and is effectively the guac_display + * equivalent of a guac_layer. + */ +typedef struct guac_display_layer guac_display_layer; + +/** + * The current Cairo drawing context of a guac_display_layer, including a Cairo + * image surface wrapping the underlying drawing buffer of the + * guac_display_layer. After making graphical changes, the dirty rectangle of + * this context must be updated such that it includes the regions modified by + * those changes. + */ +typedef struct guac_display_layer_cairo_context guac_display_layer_cairo_context; + +/** + * The current raw drawing context of a guac_display_layer, including the + * underlying drawing buffer of the guac_display_layer and memory layout + * information. After making graphical changes, the dirty rectangle of this + * context must be updated such that it includes the regions modified by those + * changes. + */ +typedef struct guac_display_layer_raw_context guac_display_layer_raw_context; + +/** + * Pre-defined mouse cursor graphics. + */ +typedef enum guac_display_cursor_type { + + /** + * An empty (invisible/hidden) mouse cursor. + */ + GUAC_DISPLAY_CURSOR_NONE, + + /** + * A small dot. This is typically used in situations where cursor + * information for the remote desktop is not available, thus all cursor + * rendering must happen remotely, but it's still important that the user + * be able to see the current location of their local mouse pointer. + */ + GUAC_DISPLAY_CURSOR_DOT, + + /** + * A vertical, I-shaped bar indicating text input or selection. + */ + GUAC_DISPLAY_CURSOR_IBAR, + + /** + * A standard, general-purpose pointer. + */ + GUAC_DISPLAY_CURSOR_POINTER + +} guac_display_cursor_type; + +/** + * @} + */ + +#endif diff --git a/src/libguac/guacamole/display.h b/src/libguac/guacamole/display.h new file mode 100644 index 000000000..84c4c9082 --- /dev/null +++ b/src/libguac/guacamole/display.h @@ -0,0 +1,780 @@ +/* + * 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_DISPLAY_H +#define GUAC_DISPLAY_H + +/** + * An abstract display implementation which handles optimization automatically. + * Current optimizations include: + * + * - Scroll/copy detection + * - Solid color detection + * - Dirty rectangle reduction + * - Dynamic selection of PNG/JPEG/WebP compression depending on update content + * and frequency + * - Combining/rewriting of updates based on estimated cost + * + * @defgroup display guac_display + * @{ + */ + +/** + * Provides an abstract display implementation (guac_display), which handles + * optimization automatically. + * + * @file display.h + */ + +#include "client.h" +#include "display-constants.h" +#include "display-types.h" +#include "rect.h" +#include "socket.h" + +#include +#include + +/** + * Returns the memory address of the given rectangle within the image buffer of + * the given guac_display_layer_raw_context, where the upper-left corner of the + * given buffer is (0, 0). If the memory address cannot be calculated because + * doing so would overflow the maximum value of a size_t, execution of the + * current process is automatically aborted. + * + * IMPORTANT: No checks are performed on whether the rectangle extends beyond + * the bounds of the buffer, including considering whether the left/top + * position of the rectangle is negative. If the rectangle has not already been + * contrained to be within the bounds of the buffer, such checks must be + * performed before dereferencing the value returned by this macro. + * + * @param context + * The guac_display_layer_raw_context associated with the image buffer + * within which the address of the given rectangle should be determined. + * + * @param rect + * The rectangle to determine the offset of. + */ +#define GUAC_DISPLAY_LAYER_RAW_BUFFER(context, rect) \ + GUAC_RECT_MUTABLE_BUFFER(rect, context->buffer, context->stride, GUAC_DISPLAY_LAYER_RAW_BPP) + +struct guac_display_layer_cairo_context { + + /** + * A Cairo context created for the Cairo surface. This Cairo context is + * persistent and will maintain its state between different calls to + * guac_display_layer_open_cairo() for the same layer. + */ + cairo_t* cairo; + + /** + * A Cairo image surface wrapping the image buffer of this + * guac_display_layer. + */ + cairo_surface_t* surface; + + /** + * A rectangle covering the current bounds of the graphical surface. + */ + guac_rect bounds; + + /** + * A rectangle covering the region of the guac_display_layer that has + * changed since the last frame. This rectangle is initially empty and must + * be manually updated to cover any additional changed regions before + * closing the guac_display_layer_cairo_context. + */ + guac_rect dirty; + + /** + * The layer that should be searched for possible scroll/copy operations + * related to the changes being made via this guac_display_layer_cairo_context. + * This value is initially the layer being drawn to and must be updated + * before closing the context if a different source layer should be + * considered for scroll/copy optimizations. This value may be set to NULL + * to hint that no scroll/copy optimization should be performed. + */ + guac_display_layer* hint_from; + +}; + +struct guac_display_layer_raw_context { + + /** + * The raw, underlying image buffer of the guac_display_layer. If the layer + * was created as opaque, this image is 32-bit RGB with 8 bits per color + * component, where the lowest-order byte is the blue component and the + * highest-order byte is ignored. If the layer was not created as opaque, + * this image is 32-bit ARGB with 8 bits per color component, where the + * lowest-order byte is the blue component and the highest-order byte is + * alpha. + * + * This value may be replaced with a manually-allocated buffer if the + * associated layer should instead use that manually-allocated buffer for + * future rendering operations. If the buffer is replaced, it must be + * maintained manually going forward, including when the buffer needs to be + * resized or after the corresponding layer/display have been freed. + * + * If necessary (such as when a manually-allocated buffer must be freed + * before freeing the guac_display), all guac_display references to a + * manually-allocated buffer may be removed by setting this value to NULL + * and closing the context. Layers with a NULL buffer will not be + * considered for graphical changes in subsequent frames. + */ + unsigned char* buffer; + + /** + * The number of bytes in each row of image data. This value is not + * necessarily the same as the width of the image multiplied by the size of + * each pixel. Additional space may be allocated to allow for memory + * alignment or to make future resize operations more efficient. + * + * If the buffer for this layer is replaced with an external buffer, or if + * the external buffer changes structure, then this value must be manually + * kept up-to-date with the stride of the external buffer. + */ + size_t stride; + + /** + * A rectangle covering the current bounds of the graphical surface. The + * buffer must not be addressed outside these bounds. + * + * If the buffer for this layer is replaced with an external buffer, or if + * the external buffer changes size, then the dimensions of this bounds + * rect must be manually kept up-to-date with the dimensions of the + * external buffer. These dimensions will also be passed through to become + * the dimensions of the layer, since layers with external buffers cannot + * be resized with guac_display_layer_resize(). + * + * NOTE: If an external buffer is used and bounds dimensions are provided + * that are greater than GUAC_DISPLAY_MAX_WIDTH and + * GUAC_DISPLAY_MAX_HEIGHT, those values will instead be interpreted as + * equal to GUAC_DISPLAY_MAX_WIDTH and GUAC_DISPLAY_MAX_HEIGHT. + */ + guac_rect bounds; + + /** + * A rectangle covering the region of the guac_display_layer that has + * changed since the last frame. This rectangle is initially empty and must + * be manually updated to cover any additional changed regions before + * closing the guac_display_layer_raw_context. + */ + guac_rect dirty; + + /** + * The layer that should be searched for possible scroll/copy operations + * related to the changes being made via this guac_display_layer_raw_context. + * This value is initially the layer being drawn to and must be updated + * before closing the context if a different source layer should be + * considered for scroll/copy optimizations. This value may be set to NULL + * to hint that no scroll/copy optimization should be performed. + */ + guac_display_layer* hint_from; + +}; + +/** + * Allocates a new guac_display representing the remote display shared by all + * connected users of the given guac_client. The dimensions of the display + * should be set with guac_display_default_layer() and + * guac_display_layer_resize() once the desired display size is known. The + * guac_display must eventually be freed through a call to guac_display_free(). + * + * NOTE: If the buffer of a layer has been replaced with an externally + * maintained buffer, this function CANNOT be used to resize the layer. The + * layer must instead be resized through changing the bounds of a + * guac_display_layer_raw_context and, if necessary, replacing the underlying + * buffer again. + * + * @param client + * The guac_client whose remote display should be represented by the new + * guac_display. + * + * @return + * A newly-allocated guac_display representing the remote display of the + * given guac_client. + */ +guac_display* guac_display_alloc(guac_client* client); + +/** + * Frees all resources associated with the given guac_display. + * + * @param display + * The guac_display to free. + */ +void guac_display_free(guac_display* display); + +/** + * Replicates the current remote display state across the given socket. When + * new users join a particular guac_client, this function should be used to + * synchronize those users with the current display state. + * + * @param display + * The display that should be synchronized to all users at the other end of + * the given guac_socket. + * + * @param socket + * The socket to send the current remote display state over. + */ +void guac_display_dup(guac_display* display, guac_socket* socket); + +/** + * Notifies the given guac_display that a specific user has left the connection + * and need no longer be considered for future updates/events. This SHOULD + * always be called when a user leaves the connection to ensure other future, + * user-related events are interpreted correctly. + * + * @param display + * The guac_display to notify. + * + * @param user + * The user that left the connection. + */ +void guac_display_notify_user_left(guac_display* display, guac_user* user); + +/** + * Notifies the given guac_display that a specific user has changed the state + * of the mouse, such as through moving the pointer or pressing/releasing a + * mouse button. This function automatically invokes + * guac_display_end_mouse_frame(). + * + * @param display + * The guac_display to notify. + * + * @param user + * The user that moved the mouse or pressed/released a mouse button. + * + * @param x + * The X position of the mouse, in pixels. + * + * @param y + * The Y position of the mouse, in pixels. + * + * @param mask + * An integer value representing the current state of each button, where + * the Nth bit within the integer is set to 1 if and only if the Nth mouse + * button is currently pressed. The lowest-order bit is the left mouse + * button, followed by the middle button, right button, and finally the up + * and down buttons of the scroll wheel. + * + * @see GUAC_CLIENT_MOUSE_LEFT + * @see GUAC_CLIENT_MOUSE_MIDDLE + * @see GUAC_CLIENT_MOUSE_RIGHT + * @see GUAC_CLIENT_MOUSE_SCROLL_UP + * @see GUAC_CLIENT_MOUSE_SCROLL_DOWN + */ +void guac_display_notify_user_moved_mouse(guac_display* display, guac_user* user, int x, int y, int mask); + +/** + * Ends the current frame, where the number of input frames that were + * considered in creating this frame is either unknown or inapplicable, + * allowing the guac_display to complete sending the frame to connected + * clients. + * + * @param display + * The guac_display that should send the current frame. + */ +void guac_display_end_frame(guac_display* display); + +/** + * Ends the current frame only if the user-visible changes consist purely of + * updates to the mouse cursor position or icon. If other visible changes have + * been made, such as graphical updates to the display itself, this function + * has no effect. + * + * @param display + * The guac_display that should send the current frame if only the mouse + * cursor is visibly affected. + */ +void guac_display_end_mouse_frame(guac_display* display); + +/** + * Ends the current frame, where that frame may combine or otherwise represent the + * changes of an arbitrary number of input frames, allowing the guac_display to + * complete sending the frame to connected clients. + * + * @param display + * The guac_display that should send the current frame. + * + * @param + * The number of distinct frames that were considered or combined when + * generating the current frame, or zero if the boundaries of relevant + * frames are unknown. + */ +void guac_display_end_multiple_frames(guac_display* display, int frames); + +/** + * Returns the default layer for the given display. The default layer is the + * only layer that always exists and serves as the root-level layer for all + * other layers. + * + * @see GUAC_DEFAULT_LAYER + * + * @param display + * The guac_display to return the default layer from. + * + * @return + * A guac_display_layer representing the default layer for the given + * guac_display. + */ +guac_display_layer* guac_display_default_layer(guac_display* display); + +/** + * Allocates a new layer for the given display. The new layer will initially be + * a direct child of the display's default layer. When the layer is no longer + * needed, it may be freed through calling guac_display_free_layer(). If not + * freed manually through a call to guac_display_free_layer(), it will be freed + * when the display is freed with guac_display_free(). + * + * @param display + * The guac_display to allocate a new layer for. + * + * @param opaque + * Non-zero if the new layer will only ever contain opaque image contents + * (the alpha channel should be ignored), zero otherwise. + * + * @return + * A newly-allocated guac_display_layer that is initially a direct child of + * the default layer. + */ +guac_display_layer* guac_display_alloc_layer(guac_display* display, int opaque); + +/** + * Allocates a new buffer (offscreen layer) for the given display. When the + * buffer is no longer needed, it may be freed through calling + * guac_display_free_layer(). If not freed manually through a call to + * guac_display_free_layer(), it will be freed when the display is freed with + * guac_display_free(). + * + * @param display + * The guac_display to allocate a new buffer for. + * + * @param opaque + * Non-zero if the new buffer will only ever contain opaque image contents + * (the alpha channel should be ignored), zero otherwise. + * + * @return + * A newly-allocated guac_display_layer representing the new buffer. + */ +guac_display_layer* guac_display_alloc_buffer(guac_display* display, int opaque); + +/** + * Frees the given layer, releasing any underlying memory. If the layer has + * already been used for rendering, it will be freed on the remote side, as + * well, when the current pending frame is complete. + * + * @param display_layer + * The layer to free. + */ +void guac_display_free_layer(guac_display_layer* display_layer); + +/** + * Returns a layer representing the current mouse cursor icon. Changes to the + * contents of this layer will affect the remote mouse cursor after the current + * pending frame is complete. + * + * Callers should consider using guac_display_end_mouse_frame() to update + * connected users as soon as all changes to the mouse cursor are completed. + * Doing so avoids needing to couple changes to the mouse cursor with + * complicated logic around changes to the remote desktop display. + * + * @param display + * The guac_display to return the cursor layer for. + * + * @return + * A guac_display_layer representing the mouse cursor of the given + * guac_display. + */ +guac_display_layer* guac_display_cursor(guac_display* display); + +/** + * Sets the remote mouse cursor to the given built-in cursor icon. This + * function automatically invokes guac_display_end_mouse_frame(). + * + * Callers should consider using guac_display_end_mouse_frame() to update + * connected users as soon as all changes to the mouse cursor are completed. + * Doing so avoids needing to couple changes to the mouse cursor with + * complicated logic around changes to the remote desktop display. + * + * @param display + * The guac_display to set the cursor of. + * + * @param cursor_type + * The built-in cursor icon to set the remote cursor to. + */ +void guac_display_set_cursor(guac_display* display, + guac_display_cursor_type cursor_type); + +/** + * Sets the hotspot location of the remote mouse cursor. The hotspot is the + * point within the mouse cursor where the click occurs. Changes to the hotspot + * of the remote mouse cursor will take effect after the current pending frame + * is complete. + * + * Callers should consider using guac_display_end_mouse_frame() to update + * connected users as soon as all changes to the mouse cursor are completed. + * Doing so avoids needing to couple changes to the mouse cursor with + * complicated logic around changes to the remote desktop display. + * + * @param display + * The guac_display to set the cursor hotspot of. + * + * @param x + * The X coordinate of the cursor hotspot, in pixels. + * + * @param y + * The Y coordinate of the cursor hotspot, in pixels. + */ +void guac_display_set_cursor_hotspot(guac_display* display, int x, int y); + +/** + * Stores the current bounding rectangle of the given layer in the given + * guac_rect. The boundary stored will be the boundary of the current pending + * frame. + * + * @oaram layer + * The layer to determine the dimensions of. + * + * @param bounds + * The guac_rect that should receive the bounding rectangle of the given + * layer. + */ +void guac_display_layer_get_bounds(guac_display_layer* layer, guac_rect* bounds); + +/** + * Moves the given layer to the given coordinates. The changes to the given + * layer will be made as part of the current pending frame, and will not take + * effect on remote displays until the pending frame is complete. + * + * @param layer + * The layer to set the position of. + * + * @param x + * The X coordinate of the upper-left corner of the layer, in pixels. + * + * @param y + * The Y coordinate of the upper-left corner of the layer, in pixels. + */ +void guac_display_layer_move(guac_display_layer* layer, int x, int y); + +/** + * Sets the stacking position of the given layer relative to all other sibling + * layers (direct children of the same parent). The change in relative layer + * stacking position will be made as part of the current pending frame, and + * will not take effect on remote displays until the pending frame is complete. + * + * @param layer + * The layer to set the stacking position of. + * + * @param z + * The relative order of this layer. + */ +void guac_display_layer_stack(guac_display_layer* layer, int z); + +/** + * Reparents the given layer such that it is a direct child of the given parent + * layer. The change in layer hierarchy will be made as part of the current + * pending frame, and will not take effect on remote displays until the pending + * frame is complete. + * + * @param layer + * The layer to change the parent of. + * + * @param parent + * The layer that should be the new parent. + */ +void guac_display_layer_set_parent(guac_display_layer* layer, const guac_display_layer* parent); + +/** + * Sets the opacity of the given layer. The change in layer opacity will be + * made as part of the current pending frame, and will not take effect on + * remote displays until the pending frame is complete. + * + * @param layer + * The layer to change the opacity of. + * + * @param opacity + * The opacity to assign to the given layer, as a value between 0 and 255 + * inclusive, where 0 is completely transparent and 255 is completely + * opaque. + */ +void guac_display_layer_set_opacity(guac_display_layer* layer, int opacity); + +/** + * Sets whether graphical changes to the given layer are allowed to be + * represented, updated, or sent using methods that can cause some loss of + * information, such as JPEG or WebP compression. By default, layers are + * allowed to use lossy methods. Changes to lossy vs. lossless behavior will + * affect the current pending frame, as well as any frames that follow. + * + * @param layer + * The layer to change the lossy behavior of. + * + * @param lossless + * Non-zero if the layer should be allowed to use lossy methods (the + * default behavior), zero if the layer should use strictly lossless + * methods. + */ +void guac_display_layer_set_lossless(guac_display_layer* layer, int lossless); + +/** + * Sets the level of multitouch support available for the given layer. The + * change in layer multitouch support will be made as part of the current + * pending frame, and will not take effect on remote displays until the pending + * frame is complete. Setting multitouch support only has any effect on the + * default layer. + * + * @param layer + * The layer to set the multitouch support level of. + * + * @param touches + * The maximum number of simultaneous touches tracked by the layer, where 0 + * represents no touch support. + */ +void guac_display_layer_set_multitouch(guac_display_layer* layer, int touches); + +/** + * Resizes the given layer to the given dimensions. The change in layer size + * will be made as part of the current pending frame, and will not take effect + * on remote displays until the pending frame is complete. + * + * This function will not resize the underlying buffer containing image data if + * the layer has been manually reassociated with a different, + * externally-maintained buffer using a guac_display_layer_raw_context. If this + * is the case, that buffer must instead be manually maintained by the caller, + * and resizing will typically involve replacing the buffer again. + * + * IMPORTANT: While it is safe to call this function while holding an open + * context (raw or Cairo), this should only be done if the underlying buffer is + * maintained externally or if the context is finished being used. Resizing a + * layer can result in the underlying buffer being replaced. + * + * @param layer + * The layer to set the size of. + * + * @param width + * The new width to assign to the layer, in pixels. Any values provided + * that are greater than GUAC_DISPLAY_MAX_WIDTH will instead be interpreted + * as equal to GUAC_DISPLAY_MAX_WIDTH. + * + * @param height + * The new height to assign to the layer, in pixels. Any values provided + * that are greater than GUAC_DISPLAY_MAX_HEIGHT will instead be + * interpreted as equal to GUAC_DISPLAY_MAX_HEIGHT. + */ +void guac_display_layer_resize(guac_display_layer* layer, int width, int height); + +/** + * Begins a drawing operation for the given layer, returning a context that can + * be used to draw directly to the raw image buffer containing the layer's + * current pending frame. + * + * Starting a draw operation acquires exclusive access to the display for the + * current thread. When complete, the original calling thread must relinquish + * exclusive access and free the graphical context by calling + * guac_display_layer_close_raw(). It is the responsibility of the caller to + * ensure the dirty rect within the returned context is updated to contain the + * region modified, such as by calling guac_rect_expand(). + * + * @param layer + * The layer to draw to. + * + * @return + * A mutable graphical context containing the current raw pending frame + * state of the given layer. + */ +guac_display_layer_raw_context* guac_display_layer_open_raw(guac_display_layer* layer); + +/** + * Ends a drawing operation that was started with a call to + * guac_display_layer_open_raw() and relinquishes exclusive access to the + * display. All graphical changes made to the layer through the raw context + * will be committed to the layer and will be included in the current pending + * frame. + * + * This function MUST NOT be called by any thread other than the thread that called + * guac_display_layer_open_raw() to obtain the given context. + * + * @param layer + * The layer that finished being drawn to. + * + * @param context + * The raw context of the drawing operation that has completed, as returned + * by a previous call to guac_display_layer_open_raw(). + */ +void guac_display_layer_close_raw(guac_display_layer* layer, guac_display_layer_raw_context* context); + +/** + * Fills a rectangle of image data within the given raw context with a single + * color. All pixels within the rectangle are replaced with the given color. If + * applicable, this includes the alpha channel. Compositing is not performed by + * this function. + * + * @param context + * The raw context of the layer that is being drawn to. + * + * @param dst + * The rectangular area that should be filled with the given color. + * + * @param color + * The color that should replace all current pixel values within the given + * rectangular region. + */ +void guac_display_layer_raw_context_set(guac_display_layer_raw_context* context, + const guac_rect* dst, uint32_t color); + +/** + * Copies a rectangle of image data from the given buffer to the given raw + * context, replacing all pixel values within the given rectangle. Compositing + * is not performed by this function. + * + * The size of the image data copied and the destination location of that data + * within the layer are dictated by the given rectangle. If any offset needs to + * be applied to the source image buffer, it is expected that this offset will + * already have been applied via the address of the buffer provided to this + * function, such as through an earlier call to GUAC_RECT_CONST_BUFFER(). + * + * @param context + * The raw context of the layer that is being drawn to. + * + * @param dst + * The rectangular area that should be filled with the image data from the + * given buffer. + * + * @param buffer + * The containing the image data that should replace all current pixel + * values within the given rectangular region. + * + * @param stride + * The number of bytes in each row of image data within the given buffer. + */ +void guac_display_layer_raw_context_put(guac_display_layer_raw_context* context, + const guac_rect* dst, const void* restrict buffer, size_t stride); + +/** + * Begins a drawing operation for the given layer, returning a context that can + * be used to draw to a Cairo surface containing the layer's current pending + * frame. The underlying Cairo state within the returned context will be + * preserved between calls to guac_display_layer_open_cairo(). + * + * Starting a draw operation acquires exclusive access to the display for the + * current thread. When complete, the original calling thread must relinquish + * exclusive access and free the graphical context by calling + * guac_display_layer_close_cairo(). It is the responsibility of the caller to + * ensure the dirty rect within the returned context is updated to contain the + * region modified, such as by calling guac_rect_expand(). + * + * @param layer + * The layer to draw to. + * + * @return + * A mutable graphical context containing the current pending frame state + * of the given layer in the form of a Cairo surface. + */ +guac_display_layer_cairo_context* guac_display_layer_open_cairo(guac_display_layer* layer); + +/** + * Ends a drawing operation that was started with a call to + * guac_display_layer_open_cairo() and relinquishes exclusive access to the + * display. All graphical changes made to the layer through the Cairo context + * will be committed to the layer and will be included in the current pending + * frame. + * + * This function MUST NOT be called by any thread other than the thread that called + * guac_display_layer_open_cairo() to obtain the given context. + * + * @param layer + * The layer that finished being drawn to. + * + * @param context + * The Cairo context of the drawing operation that has completed, as + * returned by a previous call to guac_display_layer_open_cairo(). + */ +void guac_display_layer_close_cairo(guac_display_layer* layer, guac_display_layer_cairo_context* context); + +/** + * Creates and starts a rendering thread for the given guac_display. The + * returned thread must eventually be freed with a call to + * guac_display_render_thread_destroy(). The rendering thread simplifies + * efficient handling of guac_display, but is not a requirement. If your use + * case is not well-served by the provided render thread, you can use your own + * render loop, thread, etc. + * + * The render thread will finalize and send frames after being notified that + * graphical changes have occurred, heuristically determining frame boundaries + * based on the lull in modifications that occurs between frames. In the event + * that modifications are made continuously without pause, the render thread + * will finalize and send frames at a reasonable minimum rate. + * + * If explicit frame boundaries are available, the render thread can be + * notified of these boundaries. Explicit boundaries will be preferred by the + * render thread over heuristically determined boundaries. + * + * @see guac_display_render_thread_notify_modified() + * @see guac_display_render_thread_notify_frame() + * + * @param display + * The display to start a rendering thread for. + * + * @return + * An opaque reference to the created, running rendering thread. This + * thread must be eventually freed through a call to + * guac_display_render_thread_destroy(). + */ +guac_display_render_thread* guac_display_render_thread_create(guac_display* display); + +/** + * Notifies the given render thread that the graphical state of the display has + * been modified in some visible way. The changes will then be included in a + * future frame by the render thread once a frame boundary has been reached. + * If frame boundaries are currently being determined heuristically by the + * render thread, it is the timing of calls to this function that determine the + * boundaries of frames. + * + * @param render_thread + * The render thread to notify of display modifications. + */ +void guac_display_render_thread_notify_modified(guac_display_render_thread* render_thread); + +/** + * Notifies the given render thread that a frame boundary has been reached. + * Further heuristic detection of frame boundaries by the render thread will + * stop, and all further frames must be marked through calls to this function. + * + * @param render_thread + * The render thread to notify of an explicit frame boundary. + */ +void guac_display_render_thread_notify_frame(guac_display_render_thread* render_thread); + +/** + * Safely stops and frees all resources associated with the given render + * thread. The provided pointer to the render thread is no longer valid after a + * call to this function. The guac_display associated with the render thread is + * unaffected. + * + * @param render_thread + * The render thread to stop and free. + */ +void guac_display_render_thread_destroy(guac_display_render_thread* render_thread); + +/** + * @} + */ + +#endif diff --git a/src/libguac/guacamole/fifo-constants.h b/src/libguac/guacamole/fifo-constants.h new file mode 100644 index 000000000..ff90d71c6 --- /dev/null +++ b/src/libguac/guacamole/fifo-constants.h @@ -0,0 +1,57 @@ +/* + * 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_FIFO_CONSTANTS_H +#define GUAC_FIFO_CONSTANTS_H + +/** + * @addtogroup fifo + * @{ + */ + +/** + * Provides constants for the abstract FIFO implementation (guac_fifo). + * + * @file fifo-constants.h + */ + +/** + * The bitwise flag used by the "state" member of guac_fifo to represent that + * the fifo has space for at least one item. + */ +#define GUAC_FIFO_STATE_READY 1 + +/** + * The bitwise flag used by the "state" member of guac_fifo to represent that + * the fifo contains at least one item. + */ +#define GUAC_FIFO_STATE_NONEMPTY 2 + +/** + * The bitwise flag used by the "state" member of guac_fifo to represent that + * the fifo is no longer valid and may not be used for any further operations. + */ +#define GUAC_FIFO_STATE_INVALID 4 + +/** + * @} + */ + +#endif + diff --git a/src/libguac/guacamole/fifo-types.h b/src/libguac/guacamole/fifo-types.h new file mode 100644 index 000000000..7ab78b46b --- /dev/null +++ b/src/libguac/guacamole/fifo-types.h @@ -0,0 +1,53 @@ +/* + * 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_FIFO_TYPES_H +#define GUAC_FIFO_TYPES_H + +/** + * @addtogroup fifo + * @{ + */ + +/** + * Provides type definitions for the abstract FIFO implementation (guac_fifo). + * + * @file fifo-types.h + */ + +/** + * Generic base structure for a FIFO of arbitrary events. The size of the FIFO + * and each event are up to the implementation. Each implementation must + * provide this base structure with a pointer to the underlying array of items, + * the maximum number of items supported, and the size in bytes of each item + * through a call to guac_fifo_init(). + * + * This generic base may be safely included in shared memory, but + * implementations building off this base must ensure the base is initialized + * with a call to guac_fifo_init() and that any additional + * implementation-specific aspects are also safe for shared memory usage. + */ +typedef struct guac_fifo guac_fifo; + +/** + * @} + */ + +#endif + diff --git a/src/libguac/guacamole/fifo.h b/src/libguac/guacamole/fifo.h new file mode 100644 index 000000000..bfc3dda7c --- /dev/null +++ b/src/libguac/guacamole/fifo.h @@ -0,0 +1,314 @@ +/* + * 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_FIFO_H +#define GUAC_FIFO_H + +#include "fifo-constants.h" +#include "fifo-types.h" +#include "flag.h" + +#include +#include + +/** + * Base FIFO implementation that allows arbitrary element sizes and arbitrary + * element storage. + * + * @defgroup fifo guac_fifo + * @{ + */ + +/** + * Provides an abstract FIFO implementation (guac_fifo), which can support + * arbitrary element sizes and storage. + * + * @file fifo.h + */ + +struct guac_fifo { + + /** + * The current state of this FIFO. This state primarily represents whether + * the FIFO contains at least one item (is non-empty), but it is also used + * to represent whether the FIFO is invalid (no longer permitted to contain + * any items). + */ + guac_flag state; + + /** + * The maximum number of items that may be stored in this FIFO. + */ + size_t max_items; + + /** + * The size of each individual item, in bytes. All FIFO items must have a + * constant size, though that size is implementation-dependent. + */ + size_t item_size; + + /** + * The index of the first item within this FIFO. As items are + * added/removed, this value will advance as necessary to avoid needing to + * spend CPU time moving existing items around in memory. + */ + size_t head; + + /** + * The current number of items stored within this FIFO. + */ + size_t item_count; + + /** + * The offset of the first byte of the implementation-specific array of + * items within this FIFO, relative to the first byte of guac_fifo + * structure. + */ + ssize_t items_offset; + +}; + +/** + * Initializes the given guac_fifo such that it may be safely included in + * shared memory and accessed by multiple processes. This function MUST be + * invoked once (and ONLY once) for each guac_fifo being used, and MUST be + * invoked before any such FIFO is used. + * + * The FIFO is empty upon initialization. + * + * @param fifo + * The FIFO to initialize. + * + * @param items + * The storage that the base implementation should use for queued items. + * This storage MUST be large enough to contain the maximum number of items + * as a contiguous array. + * + * @param max_items + * The maximum number of items supported by the provided storage. + * + * @param item_size + * The number of bytes required for each individual item in storage. + */ +void guac_fifo_init(guac_fifo* fifo, void* items, + size_t max_items, size_t item_size); + +/** + * Releases all underlying resources used by the given guac_fifo, such as + * pthread mutexes and conditions. The given guac_fifo MAY NOT be used after + * this function has been called. This function MAY NOT be called while + * exclusive access to the underlying state flag is held by any thread. + * + * This function does NOT free() the given guac_fifo pointer. If the memory + * associated with the given guac_fifo has been manually allocated, it must be + * manually freed as necessary. + * + * @param fifo + * The FIFO to destroy. + */ +void guac_fifo_destroy(guac_fifo* fifo); + +/** + * Marks the given FIFO as invalid, preventing any further additions or + * removals from the FIFO. Attempts to add/remove items from the FIFO from this + * point forward will fail immediately, as will any outstanding attempts to + * remove items that are currently blocked. + * + * This function is primarily necessary to allow for threadsafe cleanup of + * queues. Lacking this function, there is no guarantee that an outstanding + * call to guac_fifo_dequeue() won't still be indefinitely blocking. + * Internally, such a condition would mean that the mutex of the state flag is + * still held, which would mean that the FIFO can never be safely destroyed. + * + * @param fifo + * The FIFO to invalidate. + */ +void guac_fifo_invalidate(guac_fifo* fifo); + +/** + * Returns whether the given FIFO is still valid. A FIFO is valid if it has not + * yet been invalidated through a call to guac_fifo_invalidate(). + * + * @param fifo + * The FIFO to test. + * + * @return + * Non-zero if the given FIFO is still valid, zero otherwise. + */ +int guac_fifo_is_valid(guac_fifo* fifo); + +/** + * Acquires exclusive access to this guac_fifo. When exclusive access is no + * longer required, it must be manually relinquished through a call to + * guac_fifo_unlock(). This function may be safely called while the current + * thread already has exclusive access, however every such call must eventually + * have a matching call to guac_fifo_unlock(). + * + * NOTE: It is intended that locking/unlocking a guac_fifo may be used in lieu + * of a mutex to guard concurrent access to any number of shared resources + * related to the FIFO. + * + * @param fifo + * The guac_fifo to lock. + */ +void guac_fifo_lock(guac_fifo* fifo); + +/** + * Relinquishes exclusive access to this guac_fifo. This function may only be + * called by a thread that currently has exclusive access to the guac_fifo. + * + * NOTE: It is intended that locking/unlocking a guac_fifo may be used in lieu + * of a mutex to guard concurrent access to any number of shared resources + * related to the FIFO. + * + * @param fifo + * The guac_fifo to unlock. + */ +void guac_fifo_unlock(guac_fifo* fifo); + +/** + * Adds a copy of the given item to the end of the given FIFO, and signals any + * waiting threads that the FIFO is now non-empty. If there is insufficient + * space in the FIFO, this function will block until at space is available. If + * the FIFO is invalid or becomes invalid, this function returns immediately. + * + * @param fifo + * The FIFO to add an item to. + * + * @param item + * The item to add. + * + * @return + * Non-zero if the item was successfully added, zero if items cannot be + * added to the FIFO because the FIFO has been invalidated. + */ +int guac_fifo_enqueue(guac_fifo* fifo, const void* item); + +/** + * Atomically adds a copy of the given item to the end of the given FIFO, + * signals any waiting threads that the FIFO is now non-empty, and leaves the + * given FIFO locked. If there is insufficient space in the FIFO, this function + * will block until at space is available. If the FIFO is invalid or becomes + * invalid, this function returns immediately and the FIFO is not locked. + * + * @param fifo + * The FIFO to add an item to. + * + * @param item + * The item to add. + * + * @return + * Non-zero if the item was successfully added, zero if items cannot be + * added to the FIFO because the FIFO has been invalidated. + */ +int guac_fifo_enqueue_and_lock(guac_fifo* fifo, const void* item); + +/** + * Removes the oldest (first) item from the FIFO, storing a copy of that item + * within the provided buffer. If the FIFO is currently empty, this function + * will block until at least one item has been added to the FIFO or until the + * FIFO becomes invalid. + * + * @param fifo + * The FIFO to remove an item from. + * + * @param item + * The buffer that should receive a copy of the removed item. + * + * @return + * Non-zero if an item was successfully removed, zero if items cannot be + * removed from the FIFO because the FIFO has been invalidated. + */ +int guac_fifo_dequeue(guac_fifo* fifo, void* item); + +/** + * Atomically removes the oldest (first) item from the FIFO, storing a copy of + * that item within the provided buffer. If this function successfully removes + * an item, the FIFO is left locked after this function returns. If the FIFO is + * currently empty, this function will block until at least one item has been + * added to the FIFO or until the FIFO becomes invalid. + * + * @param fifo + * The FIFO to remove an item from. + * + * @param item + * The buffer that should receive a copy of the removed item. + * + * @return + * Non-zero if an item was successfully removed, zero if items cannot be + * removed from the FIFO because the FIFO has been invalidated. + */ +int guac_fifo_dequeue_and_lock(guac_fifo* fifo, void* item); + +/** + * Removes the oldest (first) item from the FIFO, storing a copy of that item + * within the provided buffer. If the FIFO is currently empty, this function + * will block until at least one item has been added to the FIFO, until the + * given timeout has elapsed, or until the FIFO becomes invalid. + * + * @param fifo + * The FIFO to remove an item from. + * + * @param item + * The buffer that should receive a copy of the removed item. + * + * @param msec_timeout + * The maximum number of milliseconds to wait for at least one item to be + * present within the FIFO (or for the FIFO to become invalid). + * + * @return + * Non-zero if an item was successfully removed, zero if the timeout has + * elapsed or if items cannot be removed from the FIFO because the FIFO has + * been invalidated. + */ +int guac_fifo_timed_dequeue(guac_fifo* fifo, + void* item, int msec_timeout); + +/** + * Atomically removes the oldest (first) item from the FIFO, storing a copy of + * that item within the provided buffer. If this function successfully removes + * an item, the FIFO is left locked after this function returns. If the FIFO is + * currently empty, this function will block until at least one item has been + * added to the FIFO, until the given timeout has elapsed, or until the FIFO + * becomes invalid. + * + * @param fifo + * The FIFO to remove an item from. + * + * @param item + * The buffer that should receive a copy of the removed item. + * + * @param msec_timeout + * The maximum number of milliseconds to wait for at least one item to be + * present within the FIFO (or for the FIFO to become invalid). + * + * @return + * Non-zero if an item was successfully removed, zero if the timeout has + * elapsed or if items cannot be removed from the FIFO because the FIFO has + * been invalidated. + */ +int guac_fifo_timed_dequeue_and_lock(guac_fifo* fifo, + void* item, int msec_timeout); + +/** + * @} + */ + +#endif + diff --git a/src/libguac/guacamole/flag-types.h b/src/libguac/guacamole/flag-types.h new file mode 100644 index 000000000..e67fa5e9c --- /dev/null +++ b/src/libguac/guacamole/flag-types.h @@ -0,0 +1,36 @@ +/* + * 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_FLAG_TYPES_H +#define GUAC_FLAG_TYPES_H + +/** + * Generic integer flag intended for signalling of arbitrary events between + * processes. This flag may be safely included in shared memory, but must be + * initialized with guac_flag_init(). + * + * In addition to basic signalling and tracking of flag values, it is intended + * that the locking/unlocking facilities of guac_flag may be used in + * lieu of a mutex to guard concurrent access to any number of shared resources + * related to the flag. + */ +typedef struct guac_flag guac_flag; + +#endif + diff --git a/src/libguac/guacamole/flag.h b/src/libguac/guacamole/flag.h new file mode 100644 index 000000000..00f2f1aff --- /dev/null +++ b/src/libguac/guacamole/flag.h @@ -0,0 +1,244 @@ +/* + * 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_FLAG_H +#define GUAC_FLAG_H + +#include "flag-types.h" + +#include + +struct guac_flag { + + /** + * The mutex used to ensure concurrent changes to the value of this flag + * are threadsafe, as well as to satisfy the requirements of the pthread + * conditional used to signal changes to the value of this flag. + */ + pthread_mutex_t value_mutex; + + /** + * Condition variable that signals when the value of this flag has changed. + */ + pthread_cond_t value_changed; + + /** + * The current value of this flag. This value may be the bitwise OR'd value + * of any number of arbitrary flags, so long as those flags fit within an + * int. It is entirely up to the user of this guac_flag to + * define the meaning of any value(s) assigned. + */ + unsigned int value; + +}; + +/** + * Initializes the given guac_flag such that it may be safely + * included in shared memory and accessed by multiple processes. This function + * MUST be invoked once (and ONLY once) for each guac_flag being + * used, and MUST be invoked before any such flag is used. + * + * The value of the flag upon initialization is 0 (no flags set). + * + * @param event_flag + * The flag to initialize. + */ +void guac_flag_init(guac_flag* event_flag); + +/** + * Releases all underlying resources used by the given guac_flag, + * such as pthread mutexes and conditions. The given guac_flag MAY + * NOT be used after this function has been called. This function MAY NOT be + * called while exclusive access to the guac_flag is held by any + * thread. + * + * This function does NOT free() the given guac_flag pointer. If the + * memory associated with the given guac_flag has been manually + * allocated, it must be manually freed as necessary. + * + * @param event_flag + * The flag to destroy. + */ +void guac_flag_destroy(guac_flag* event_flag); + +/** + * Sets the given bitwise flag(s) within the value of the given + * guac_flag, setting their corresponding bits to 1. The values of + * other bitwise flags are not affected. If other threads are waiting for any + * of these flags to be set, and at least one such flag has been set as a + * result of this call, they will be signalled accordingly. + * + * This function is threadsafe and will acquire exclusive access to the given + * guac_flag prior to changing the flag value. It is also safe to + * call this function if exclusive access has already been acquired through + * guac_flag_lock() or similar. + * + * @param event_flag + * The guac_flag to modify. + * + * @param flags + * The bitwise OR'd value of the flags to be set. + */ +void guac_flag_set(guac_flag* event_flag, + unsigned int flags); + +/** + * Sets the given bitwise flag(s) within the value of the given guac_flag, + * setting their corresponding bits to 1, while also acquiring exclusive access + * to the guac_flag. The values of other bitwise flags are not affected. If + * other threads are waiting for any of these flags to be set, and at least one + * such flag has been set as a result of this call, they will be signalled + * accordingly. + * + * This function is threadsafe and will acquire exclusive access to the given + * guac_flag prior to changing the flag value. It is also safe to + * call this function if exclusive access has already been acquired through + * guac_flag_lock() or similar. + * + * @param event_flag + * The guac_flag to modify. + * + * @param flags + * The bitwise OR'd value of the flags to be set. + */ +void guac_flag_set_and_lock(guac_flag* event_flag, + unsigned int flags); + +/** + * Clears the given bitwise flag(s) within the value of the given + * guac_flag, setting their corresponding bits to 0. The values of + * other bitwise flags are not affected. Unlike guac_flag_set(), + * no threads will be notified that these flag values have changed. + * + * This function is threadsafe and will acquire exclusive access to the given + * guac_flag prior to changing the flag value. It is also safe to + * call this function if exclusive access has already been acquired through + * guac_flag_lock() or similar. + * + * @param event_flag + * The guac_flag to modify. + * + * @param flags + * The bitwise OR'd value of the flags to be cleared. Each bit in this + * value that is set to 1 will be set to 0 in the value of the + * guac_flag. + */ +void guac_flag_clear(guac_flag* event_flag, + unsigned int flags); + +/** + * Clears the given bitwise flag(s) within the value of the given guac_flag, + * setting their corresponding bits to 0, while also acquiring exclusive access + * to the guac_flag. The values of other bitwise flags are not affected. Unlike + * guac_flag_set(), no threads will be notified that these flag values have + * changed. + * + * This function is threadsafe and will acquire exclusive access to the given + * guac_flag prior to changing the flag value. It is also safe to + * call this function if exclusive access has already been acquired through + * guac_flag_lock() or similar. + * + * @param event_flag + * The guac_flag to modify. + * + * @param flags + * The bitwise OR'd value of the flags to be cleared. Each bit in this + * value that is set to 1 will be set to 0 in the value of the + * guac_flag. + */ +void guac_flag_clear_and_lock(guac_flag* event_flag, + unsigned int flags); + +/** + * Acquires exclusive access to this guac_flag. When exclusive + * access is no longer required, it must be manually relinquished through a + * call to guac_flag_unlock(). This function may be safely called + * while the current thread already has exclusive access, however every such + * call must eventually have a matching call to guac_flag_unlock(). + * + * NOTE: It is intended that locking/unlocking a guac_flag may be + * used in lieu of a mutex to guard concurrent access to any number of shared + * resources related to the flag. + * + * @param event_flag + * The guac_flag to lock. + */ +void guac_flag_lock(guac_flag* event_flag); + +/** + * Relinquishes exclusive access to this guac_flag. This function + * may only be called by a thread that currently has exclusive access to the + * guac_flag. + * + * NOTE: It is intended that locking/unlocking a guac_flag may be + * used in lieu of a mutex to guard concurrent access to any number of shared + * resources related to the flag. + * + * @param event_flag + * The guac_flag to unlock. + */ +void guac_flag_unlock(guac_flag* event_flag); + +/** + * Waits indefinitely for any of the given flags to be set within the given + * guac_flag. This function returns only after at least one of the + * given flags has been set. After this function returns, the current thread + * has exclusive access to the guac_flag and MUST relinquish that + * access with a call to guac_flag_unlock() when finished. + * + * @param event_flag + * The guac_flag to wait on. + * + * @param flags + * The bitwise OR'd value of the specific flag(s) to wait for. + */ +void guac_flag_wait_and_lock(guac_flag* event_flag, + unsigned int flags); + +/** + * Waits no longer than the given number of milliseconds for any of the given + * flags to be set within the given guac_flag. This function returns + * after at least one of the given flags has been set, or after the provided + * time limit expires. After this function returns successfully, the current + * thread has exclusive access to the guac_flag and MUST relinquish + * that access with a call to guac_flag_unlock() when finished. If + * the time limit lapses before any of the given flags has been set, this + * function returns unsuccessfully without acquiring exclusive access. + * + * @param event_flag + * The guac_flag to wait on. + * + * @param flags + * The bitwise OR'd value of the specific flag(s) to wait for. + * + * @param msec_timeout + * The maximum number of milliseconds to wait for at least one of the + * desired flags to be set. + * + * @return + * Non-zero if at least one of the desired flags has been set and the + * current thread now has exclusive access to the guac_flag, zero if none + * of the desired flags were set within the time limit and the current + * thread DOES NOT have exclusive access. + */ +int guac_flag_timedwait_and_lock(guac_flag* event_flag, + unsigned int flags, unsigned int msec_timeout); + +#endif + diff --git a/src/libguac/guacamole/pool.h b/src/libguac/guacamole/pool.h index 0a250046a..4ff0877ee 100644 --- a/src/libguac/guacamole/pool.h +++ b/src/libguac/guacamole/pool.h @@ -102,8 +102,8 @@ void guac_pool_free(guac_pool* pool); /** * Returns the next available integer from the given guac_pool. All integers - * returned are non-negative, and are returned in sequences, starting from 0. - * This operation is threadsafe. + * returned are non-negative, and are returned in sequence, starting from 0. + * This operation is atomic. * * @param pool * The guac_pool to retrieve an integer from. @@ -111,14 +111,65 @@ void guac_pool_free(guac_pool* pool); * @return * The next available integer, which may be either an integer not yet * returned by a call to guac_pool_next_int, or an integer which was - * previously returned, but has since been freed. + * previously returned but has since been freed. */ int guac_pool_next_int(guac_pool* pool); +/** + * Returns the next available integer from the given guac_pool that is below + * the given limit. If no such integer can be obtained because all such + * integers are already in use, -1 will be returned instead. All integers + * successfully returned are non-negative, and are returned in sequence, + * starting from 0. This operation is atomic. + * + * @param pool + * The guac_pool to retrieve an integer from. + * + * @param limit + * The exclusive upper bound to enforce on all integers returned by this + * function. Integers of this value or greater will never be returned. If + * all other integers are already in use, -1 will be returned instead. + * + * @return + * The next available integer, which may be either an integer not yet + * returned by a call to guac_pool_next_int, or an integer which was + * previously returned but has since been freed. If all integers are + * currently in use and no integer can be returned without reaching the + * given limit, -1 is returned. + */ +int guac_pool_next_int_below(guac_pool* pool, int limit); + +/** + * Returns the next available integer from the given guac_pool that is below + * the given limit. If no such integer can be obtained because all such + * integers are already in use, the current process will abort and this + * function will not return. All integers successfully returned are + * non-negative, and are returned in sequence, starting from 0. This operation + * is atomic. + * + * @param pool + * The guac_pool to retrieve an integer from. + * + * @param limit + * The exclusive upper bound to enforce on all integers returned by this + * function. Integers of this value or greater will never be returned. If + * all other integers are already in use, the current process will abort + * and this function will not return. + * + * @return + * The next available integer, which may be either an integer not yet + * returned by a call to guac_pool_next_int, or an integer which was + * previously returned but has since been freed. If all integers are + * currently in use and no integer can be returned without reaching the + * given limit, the current process will abort and this function will not + * return. + */ +int guac_pool_next_int_below_or_die(guac_pool* pool, int limit); + /** * Frees the given integer back into the given guac_pool. The integer given * will be available for future calls to guac_pool_next_int. This operation is - * threadsafe. + * atomic. * * @param pool * The guac_pool to free the given integer into. diff --git a/src/libguac/guacamole/protocol.h b/src/libguac/guacamole/protocol.h index 7be0ef2a9..409a4083e 100644 --- a/src/libguac/guacamole/protocol.h +++ b/src/libguac/guacamole/protocol.h @@ -384,11 +384,22 @@ int guac_protocol_send_select(guac_socket* socket, const char* protocol); * If an error occurs sending the instruction, a non-zero value is * returned, and guac_error is set appropriately. * - * @param socket The guac_socket connection to use. - * @param timestamp The current timestamp (in milliseconds). - * @return Zero on success, non-zero on error. + * @param socket + * The guac_socket connection to use. + * + * @param timestamp + * The current timestamp (in milliseconds). + * + * @param frames + * The number of distinct frames that were considered or combined when + * generating the frame terminated by this instruction, or zero if the + * boundaries of relevant frames are unknown. + * + * @return + * Zero on success, non-zero on error. */ -int guac_protocol_send_sync(guac_socket* socket, guac_timestamp timestamp); +int guac_protocol_send_sync(guac_socket* socket, guac_timestamp timestamp, + int frames); /* OBJECT INSTRUCTIONS */ diff --git a/src/libguac/guacamole/recording.h b/src/libguac/guacamole/recording.h index 667713d16..e44bc8468 100644 --- a/src/libguac/guacamole/recording.h +++ b/src/libguac/guacamole/recording.h @@ -148,6 +148,10 @@ typedef struct guac_recording { * caution. Key events can easily contain sensitive information, such as * passwords, credit card numbers, etc. * + * @param allow_write_existing + * Non-zero if writing to an existing file should be allowed, or zero + * otherwise. + * * @return * A new guac_recording structure representing the in-progress * recording if the recording file has been successfully created and a @@ -156,7 +160,7 @@ typedef struct guac_recording { guac_recording* guac_recording_create(guac_client* client, const char* path, const char* name, int create_path, int include_output, int include_mouse, int include_touch, - int include_keys); + int include_keys, int allow_write_existing); /** * Frees the resources associated with the given in-progress recording. Note diff --git a/src/libguac/guacamole/rect-types.h b/src/libguac/guacamole/rect-types.h new file mode 100644 index 000000000..73a1bdc93 --- /dev/null +++ b/src/libguac/guacamole/rect-types.h @@ -0,0 +1,33 @@ +/* + * 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_RECT_TYPES_H +#define GUAC_RECT_TYPES_H + +/** + * A rectangle defined by its upper-left and lower-right corners. The + * upper-left corner is inclusive (represents the start of the area contained + * by the guac_rect), while the lower-right corner is exclusive (represents the + * start of the area NOT contained by the guac_rect). All coordinates may be + * negative. + */ +typedef struct guac_rect guac_rect; + +#endif + diff --git a/src/libguac/guacamole/rect.h b/src/libguac/guacamole/rect.h new file mode 100644 index 000000000..477fec6bc --- /dev/null +++ b/src/libguac/guacamole/rect.h @@ -0,0 +1,271 @@ +/* + * 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_RECT_H +#define GUAC_RECT_H + +#include "mem.h" +#include "rect-types.h" + +/** + * Returns the memory address of the given rectangle within the given mutable + * buffer, where the upper-left corner of the given buffer is (0, 0). If the + * memory address cannot be calculated because doing so would overflow the + * maximum value of a size_t, execution of the current process is automatically + * aborted. + * + * IMPORTANT: No checks are performed on whether the rectangle extends beyond + * the bounds of the buffer, including considering whether the left/top + * position of the rectangle is negative. If the rectangle has not already been + * contrained to be within the bounds of the buffer, such checks must be + * performed before dereferencing the value returned by this macro. + * + * @param rect + * The rectangle to determine the offset of. + * + * @param buffer + * The mutable buffer within which the address of the given rectangle + * should be determined. + * + * @param stride + * The number of bytes in each row of image data within the buffer. + * + * @param bpp + * The number of bytes in each pixel of image data. + * + * @return + * The memory address of the given rectangle within the given buffer. + */ +#define GUAC_RECT_MUTABLE_BUFFER(rect, buffer, stride, bpp) ((void*) ( \ + ((unsigned char*) (buffer)) \ + + guac_mem_ckd_mul_or_die((rect).top, stride) \ + + guac_mem_ckd_mul_or_die((rect).left, bpp))) + +/** + * Returns the memory address of the given rectangle within the given immutable + * (const) buffer, where the upper-left corner of the given buffer is (0, 0). + * If the memory address cannot be calculated because doing so would overflow + * the maximum value of a size_t, execution of the current process is + * automatically aborted. + * + * IMPORTANT: No checks are performed on whether the rectangle extends beyond + * the bounds of the buffer, including considering whether the left/top + * position of the rectangle is negative. If the rectangle has not already been + * contrained to be within the bounds of the buffer, such checks must be + * performed before dereferencing the value returned by this macro. + * + * @param rect + * The rectangle to determine the offset of. + * + * @param buffer + * The const buffer within which the address of the given rectangle should + * be determined. + * + * @param stride + * The number of bytes in each row of image data within the buffer. + * + * @param bpp + * The number of bytes in each pixel of image data. + * + * @return + * The memory address of the given rectangle within the given buffer. + */ +#define GUAC_RECT_CONST_BUFFER(rect, buffer, stride, bpp) ((const void*) ( \ + ((const unsigned char*) (buffer)) \ + + guac_mem_ckd_mul_or_die((rect).top, stride) \ + + guac_mem_ckd_mul_or_die((rect).left, bpp))) + +struct guac_rect { + + /** + * The X coordinate of the upper-left corner of this rectangle (inclusive). + * This value represents the least integer X coordinate that is part of + * this rectangle, with greater integer X coordinates being part of this + * rectangle up to but excluding the right boundary. + * + * This value MUST be less than or equal to the right boundary. If this + * value is equal to the right boundary, the rectangle is empty (has no + * width). + */ + int left; + + /** + * The Y coordinate of the upper-left corner of this rectangle (inclusive). + * This value represents the least integer Y coordinate that is part of + * this rectangle, with greater integer Y coordinates being part of this + * rectangle up to but excluding the bottom boundary. + * + * This value MUST be less than or equal to the bottom boundary. If this + * value is equal to the bottom boundary, the rectangle is empty (has no + * height). + */ + int top; + + /** + * The X coordinate of the lower-right corner of this rectangle + * (exclusive). This value represents the least integer X coordinate that + * is NOT part of this rectangle, with lesser integer X coordinates being + * part of this rectangle up to and including the left boundary. + * + * This value MUST be greater than or equal to the left boundary. If this + * value is equal to the left boundary, the rectangle is empty (has no + * width). + */ + int right; + + /** + * The Y coordinate of the lower-right corner of this rectangle + * (exclusive). This value represents the least integer Y coordinate that + * is NOT part of this rectangle, with lesser integer Y coordinates being + * part of this rectangle up to and including the top boundary. + * + * This value MUST be greater than or equal to the top boundary. If this + * value is equal to the top boundary, the rectangle is empty (has no + * height). + */ + int bottom; + +}; + +/** + * Initializes the given rectangle with the given coordinates and dimensions. + * If a dimenion is negative, it is interpreted as if zero. + * + * @param rect + * The rectangle to initialize. + * + * @param x + * The X coordinate of the upper-left corner of the rectangle. + * + * @param y + * The Y coordinate of the upper-left corner of the rectangle. + * + * @param width + * The width of the rectangle. + * + * @param height + * The height of the rectangle. + */ +void guac_rect_init(guac_rect* rect, int x, int y, int width, int height); + +/** + * Extends the given rectangle such that each edge of the rectangle falls on + * the edge of an NxN cell in a regular grid anchored at the upper-left corner, + * where N is a power of two. + * + * @param rect + * The rectangle to adjust. + * + * @param bits + * The size of the cells in the grid, as the exponent of the power of two + * size of each grid cell edge. For example, to align the given rectangle + * to the edges of a grid containing 8x8 cells, use a value of 3. + */ +void guac_rect_align(guac_rect* rect, unsigned int bits); + +/** + * Extends the given rectangle such that it contains at least the specified + * minimum rectangle. + * + * @param rect + * The rectangle to extend. + * + * @param min + * The minimum area which must be contained within the given rectangle. + */ +void guac_rect_extend(guac_rect* rect, const guac_rect* min); + +/** + * Collapses the given rectangle such that it exists only within the bounds of + * the given maximum rectangle. + * + * @param rect + * The rectangle to collapse. + * + * @param max + * The maximum area in which the given rectangle can exist. + */ +void guac_rect_constrain(guac_rect* rect, const guac_rect* max); + +/** + * Reduces the size of the given rectangle such that it does not exceed the + * given width and height. The aspect ratio of the given rectangle is + * preserved. If the original rectangle is already smaller than the given width + * and height, this function has no effect. + * + * @param rect + * The rectangle to shrink while preserving aspect ratio. + * + * @param max_width + * The maximum width that the given rectangle may have. + * + * @param max_height + * The maximum height that the given rectangle may have. + */ +void guac_rect_shrink(guac_rect* rect, int max_width, int max_height); + +/** + * Returns whether the two given rectangles intersect. + * + * @param a + * One of the rectangles to check. + * + * @param b + * The other rectangle to check. + * + * @return + * Non-zero if the rectangles intersect, zero otherwise. + */ +int guac_rect_intersects(const guac_rect* a, const guac_rect* b); + +/** + * Returns whether the given rectangle is empty. A rectangle is empty if it has + * no area (has an effective width or height of zero). + * + * @param rect + * The rectangle to test. + * + * @return + * Non-zero if the rectangle is empty, zero otherwise. + */ +int guac_rect_is_empty(const guac_rect* rect); + +/** + * Returns the width of the given rectangle. + * + * @param rect + * The rectangle to determine the width of. + * + * @return + * The width of the given rectangle. + */ +int guac_rect_width(const guac_rect* rect); + +/** + * Returns the height of the given rectangle. + * + * @param rect + * The rectangle to determine the height of. + * + * @return + * The height of the given rectangle. + */ +int guac_rect_height(const guac_rect* rect); + +#endif diff --git a/src/libguac/guacamole/socket-constants.h b/src/libguac/guacamole/socket-constants.h index 60f06c5ab..8bb78318c 100644 --- a/src/libguac/guacamole/socket-constants.h +++ b/src/libguac/guacamole/socket-constants.h @@ -37,5 +37,16 @@ */ #define GUAC_SOCKET_KEEP_ALIVE_INTERVAL 5000 +/** + * The number of bytes of data to buffer prior to bulk conversion to base64. + */ +#define GUAC_SOCKET_BASE64_READY_BUFFER_SIZE 768 + +/** + * The size of the buffer required to hold GUAC_SOCKET_BASE64_READY_BUFFER_SIZE + * bytes encoded as base64. + */ +#define GUAC_SOCKET_BASE64_ENCODED_BUFFER_SIZE 1024 + #endif diff --git a/src/libguac/guacamole/socket.h b/src/libguac/guacamole/socket.h index af57e8745..6075f39b2 100644 --- a/src/libguac/guacamole/socket.h +++ b/src/libguac/guacamole/socket.h @@ -98,10 +98,15 @@ struct guac_socket { int __ready; /** - * The base64 "ready" buffer. Once this buffer is filled, base64 data is - * flushed to the main write buffer. + * The base64 "ready" buffer. Once this buffer is filled, the data is encoded + * as base64 and flushed to the main write buffer. */ - int __ready_buf[3]; + unsigned char __ready_buf[GUAC_SOCKET_BASE64_READY_BUFFER_SIZE]; + + /** + * The buffer to hold the result of encoding the ready buffer as base64. + */ + char __encoded_buf[GUAC_SOCKET_BASE64_ENCODED_BUFFER_SIZE]; /** * Whether automatic keep-alive is enabled. diff --git a/src/libguac/guacamole/string.h b/src/libguac/guacamole/string.h index a94d53c29..0f20674ba 100644 --- a/src/libguac/guacamole/string.h +++ b/src/libguac/guacamole/string.h @@ -29,6 +29,24 @@ #include #include +/** + * Convert the provided unsigned integer into a string, returning the number of + * characters written into the destination string, or a negative value if an + * error occurs. + * + * @param dest + * The destination string to copy the data into, which should already be + * allocated and at a size that can handle the string representation of the + * inteer. + * + * @param integer + * The unsigned integer to convert to a string. + * + * @return + * The number of characters written into the dest string. + */ +int guac_itoa(char* restrict dest, unsigned int integer); + /** * Copies a limited number of bytes from the given source string to the given * destination buffer. The resulting buffer will always be null-terminated, @@ -131,6 +149,38 @@ size_t guac_strlcat(char* restrict dest, const char* restrict src, size_t n); */ char* guac_strnstr(const char *haystack, const char *needle, size_t len); +/** + * Duplicates up to the given number of characters from the provided string, + * returning a newly-allocated string containing the copied contents. The + * provided string must be null-terminated, and only the first 'n' characters + * will be considered for duplication, or the full string length if it is + * shorter than 'n'. The memory block for the newly-allocated string will + * include enough space for these characters, as well as for the null + * terminator. + * + * The pointer returned by guac_strndup() SHOULD be freed with a subsequent call + * to guac_mem_free(), but MAY instead be freed with a subsequent call to free(). + * + * This function behaves similarly to the POSIX strndup() function, except that + * NULL will be returned if the provided string is NULL or if memory allocation + * fails. Also, the length of the string to be duplicated will be checked to + * prevent overflow if adding space for the null terminator. + * + * @param str + * The string of which up to the first 'n' characters should be duplicated + * as a newly-allocated string. If 'n' exceeds the length of the string, + * the entire string is duplicated. + * + * @param n + * The maximum number of characters to duplicate from the given string. + * + * @return + * A newly-allocated string containing up to the first 'n' characters from + * the given string, including a terminating null byte, or NULL if the + * provided string was NULL or if memory allocation fails. + */ +char* guac_strndup(const char* str, size_t n); + /** * Duplicates the given string, returning a newly-allocated string containing * the same contents. The provided string must be null-terminated. The size of @@ -202,4 +252,3 @@ size_t guac_strljoin(char* restrict dest, const char* restrict const* elements, int nmemb, const char* restrict delim, size_t n); #endif - diff --git a/src/libguac/guacamole/tcp.h b/src/libguac/guacamole/tcp.h new file mode 100644 index 000000000..d24e45971 --- /dev/null +++ b/src/libguac/guacamole/tcp.h @@ -0,0 +1,55 @@ +/* + * 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_TCP_H +#define GUAC_TCP_H + +/** + * Provides convenience functions for establishing low-level TCP connections. + * + * @file tcp.h + */ + +#include "config.h" + +#include + +/** + * Given a hostname or IP address and port, attempt to connect to that system, + * returning the file descriptor of an open socket if the connection succeeds, + * or a negative value if it fails. The returned file descriptor must + * eventually be freed with a call to close(). If this function fails, + * guac_error will be set appropriately. + * + * @param hostname + * The hostname or IP address to which to attempt connections. + * + * @param port + * The TCP port to which to attempt to connect. + * + * @param timeout + * The number of seconds to try the TCP connection before timing out. + * + * @return + * A valid socket if the connection succeeds, or a negative integer if it + * fails. + */ +int guac_tcp_connect(const char* hostname, const char* port, const int timeout); + +#endif // GUAC_TCP_H diff --git a/src/libguac/guacamole/user-constants.h b/src/libguac/guacamole/user-constants.h index 0da48c0bb..40832c16c 100644 --- a/src/libguac/guacamole/user-constants.h +++ b/src/libguac/guacamole/user-constants.h @@ -35,7 +35,7 @@ * The maximum number of inbound or outbound streams supported by any one * guac_user. */ -#define GUAC_USER_MAX_STREAMS 64 +#define GUAC_USER_MAX_STREAMS 512 /** * The index of a closed stream. diff --git a/src/libguac/guacamole/wol-constants.h b/src/libguac/guacamole/wol-constants.h index 215e6b881..831a475e3 100644 --- a/src/libguac/guacamole/wol-constants.h +++ b/src/libguac/guacamole/wol-constants.h @@ -27,6 +27,18 @@ * @file wol-constants.h */ +/** + * The default number of times to retry a connection to a server after waking + * the server with a WOL packet before giving up. + */ +#define GUAC_WOL_DEFAULT_CONNECT_RETRIES 5 + +/** + * The default number of seconds for the connection timeout when attempting + * to connect to the remote system to see if it is awake. + */ +#define GUAC_WOL_DEFAULT_CONNECTION_TIMEOUT 10 + /** * The value for the local IPv4 broadcast address. */ diff --git a/src/libguac/guacamole/wol.h b/src/libguac/guacamole/wol.h index 9af1a8d03..21036ed66 100644 --- a/src/libguac/guacamole/wol.h +++ b/src/libguac/guacamole/wol.h @@ -52,5 +52,49 @@ int guac_wol_wake(const char* mac_addr, const char* broadcast_addr, const unsigned short udp_port); +/** + * Send the wake-up packet to the specified destination, returning zero if the + * wake was sent successfully, or non-zero if an error occurs sending the + * wake packet. Note that the return value does not specify whether the + * system actually wakes up successfully, only whether or not the packet + * is transmitted. + * + * @param mac_addr + * The MAC address to place in the magic Wake-on-LAN packet. + * + * @param broadcast_addr + * The broadcast address to which to send the magic Wake-on-LAN packet. + * + * @param udp_port + * The UDP port to use when sending the WoL packet. + * + * @param wait_time + * The number of seconds to wait between connection attempts after the WOL + * packet has been sent. + * + * @param retries + * The number of attempts to make to connect to the system before giving up + * on the connection. + * + * @param hostname + * The hostname or IP address of the system that has been woken up and to + * to which the connection will be attempted. + * + * @param port + * The TCP port of the remote system on which the connection will be + * attempted after the system has been woken. + * + * @param timeout + * The number of seconds to wait when attempting the connection to the + * remote system when checking to see if it is awake. + * + * @return + * Zero if the packet is successfully sent to the destination; non-zero + * if the packet cannot be sent. + */ +int guac_wol_wake_and_wait(const char* mac_addr, const char* broadcast_addr, + const unsigned short udp_port, int wait_time, int retries, + const char* hostname, const char* port, const int timeout); + #endif /* GUAC_WOL_H */ diff --git a/src/libguac/mem.c b/src/libguac/mem.c index 8f9a842a6..06735bf00 100644 --- a/src/libguac/mem.c +++ b/src/libguac/mem.c @@ -17,6 +17,7 @@ * under the License. */ +#include "guacamole/assert.h" #include "guacamole/error.h" #include "guacamole/mem.h" #include "guacamole/private/mem.h" @@ -126,8 +127,7 @@ size_t PRIV_guac_mem_ckd_mul_or_die(size_t factor_count, const size_t* factors) /* Perform request multiplication, aborting the entire process if the * calculation overflows */ size_t result = 0; - if (PRIV_guac_mem_ckd_mul(&result, factor_count, factors)) - abort(); + GUAC_ASSERT(!PRIV_guac_mem_ckd_mul(&result, factor_count, factors)); return result; @@ -138,8 +138,7 @@ size_t PRIV_guac_mem_ckd_add_or_die(size_t term_count, const size_t* terms) { /* Perform request addition, aborting the entire process if the calculation * overflows */ size_t result = 0; - if (PRIV_guac_mem_ckd_add(&result, term_count, terms)) - abort(); + GUAC_ASSERT(!PRIV_guac_mem_ckd_add(&result, term_count, terms)); return result; @@ -150,8 +149,7 @@ size_t PRIV_guac_mem_ckd_sub_or_die(size_t term_count, const size_t* terms) { /* Perform request subtraction, aborting the entire process if the * calculation overflows */ size_t result = 0; - if (PRIV_guac_mem_ckd_sub(&result, term_count, terms)) - abort(); + GUAC_ASSERT(!PRIV_guac_mem_ckd_sub(&result, term_count, terms)); return result; @@ -238,8 +236,7 @@ void* PRIV_guac_mem_realloc_or_die(void* mem, size_t factor_count, const size_t* /* Perform requested resize, aborting the entire process if this cannot be * done */ void* resized_mem = PRIV_guac_mem_realloc(mem, factor_count, factors); - if (resized_mem == NULL && guac_error != GUAC_STATUS_SUCCESS) - abort(); + GUAC_ASSERT(resized_mem != NULL || guac_error == GUAC_STATUS_SUCCESS); return resized_mem; diff --git a/src/libguac/pool.c b/src/libguac/pool.c index b22c20934..291129183 100644 --- a/src/libguac/pool.c +++ b/src/libguac/pool.c @@ -19,9 +19,11 @@ #include "config.h" +#include "guacamole/assert.h" #include "guacamole/mem.h" #include "guacamole/pool.h" +#include #include guac_pool* guac_pool_alloc(int size) { @@ -69,42 +71,115 @@ void guac_pool_free(guac_pool* pool) { } -int guac_pool_next_int(guac_pool* pool) { +/** + * Returns the next available integer from the given guac_pool. All integers + * returned are non-negative, and are returned in sequence, starting from 0. + * + * Unlike the public guac_pool_next_int() function, this function is NOT atomic + * and depends on the caller having already acquired the pool's lock. + * + * @param pool + * The guac_pool to retrieve an integer from. + * + * @return + * The next available integer, which may be either an integer not yet + * returned by a call to guac_pool_next_int, or an integer which was + * previously returned but has since been freed. + */ +static int __guac_pool_next_int(guac_pool* pool) { int value; - /* Acquire exclusive access */ - pthread_mutex_lock(&(pool->__lock)); + /* It's unlikely that any usage of guac_pool will ever manage to reach + * INT_MAX concurrent requests for integers, but we definitely should bail + * out if ever this does happen. Tracing this sort of issue down would be + * extremely difficult without fail-fast behavior. */ + GUAC_ASSERT(pool->__next_value < INT_MAX); + GUAC_ASSERT(pool->active < INT_MAX); pool->active++; /* If more integers are needed, return a new one. */ - if (pool->__head == NULL || pool->__next_value < pool->min_size) { + if (pool->__head == NULL || pool->__next_value < pool->min_size) value = pool->__next_value++; - pthread_mutex_unlock(&(pool->__lock)); - return value; + + /* Otherwise, reuse a previously freed integer */ + else { + + value = pool->__head->value; + + /* If only one element exists, reset pool to empty. */ + if (pool->__tail == pool->__head) { + guac_mem_free(pool->__head); + pool->__head = NULL; + pool->__tail = NULL; + } + + /* Otherwise, advance head. */ + else { + guac_pool_int* old_head = pool->__head; + pool->__head = old_head->__next; + guac_mem_free(old_head); + } + } - /* Otherwise, remove first integer. */ - value = pool->__head->value; + /* Again, this should never happen and would be a sign of some fairly + * fundamental assumption failing. It's important for such things to fail + * fast. */ + GUAC_ASSERT(value >= 0); + + return value; + +} + +int guac_pool_next_int(guac_pool* pool) { + + pthread_mutex_lock(&(pool->__lock)); + int value = __guac_pool_next_int(pool); + pthread_mutex_unlock(&(pool->__lock)); + + return value; + +} + +int guac_pool_next_int_below(guac_pool* pool, int limit) { + + pthread_mutex_lock(&(pool->__lock)); + + int value; - /* If only one element exists, reset pool to empty. */ - if (pool->__tail == pool->__head) { - guac_mem_free(pool->__head); - pool->__head = NULL; - pool->__tail = NULL; + /* Explicitly bail out now if there we would need to return a new integer, + * but can't without reaching the given limit */ + if (pool->active >= limit || (pool->__next_value >= limit && pool->__head == NULL)) { + value = -1; } - /* Otherwise, advance head. */ + /* In all other cases, attempt to obtain the requested integer (either + * reusing a freed integer or allocating a new one), but verify that some + * fundamental misuse of guac_pool hasn't resulted in values defying + * expectations */ else { - guac_pool_int* old_head = pool->__head; - pool->__head = old_head->__next; - guac_mem_free(old_head); + value = __guac_pool_next_int(pool); + GUAC_ASSERT(value < limit); } - /* Return retrieved value. */ pthread_mutex_unlock(&(pool->__lock)); + + return value; + +} + +int guac_pool_next_int_below_or_die(guac_pool* pool, int limit) { + + int value = guac_pool_next_int_below(pool, limit); + + /* Abort current process entirely if no integer can be obtained without + * reaching the given limit */ + GUAC_ASSERT(value >= 0); + return value; + } void guac_pool_free_int(guac_pool* pool, int value) { @@ -117,6 +192,7 @@ void guac_pool_free_int(guac_pool* pool, int value) { /* Acquire exclusive access */ pthread_mutex_lock(&(pool->__lock)); + GUAC_ASSERT(pool->active > 0); pool->active--; /* If pool empty, store as sole entry. */ diff --git a/src/libguac/protocol.c b/src/libguac/protocol.c index 3421af50f..b415dac8a 100644 --- a/src/libguac/protocol.c +++ b/src/libguac/protocol.c @@ -1199,7 +1199,8 @@ int guac_protocol_send_start(guac_socket* socket, const guac_layer* layer, } -int guac_protocol_send_sync(guac_socket* socket, guac_timestamp timestamp) { +int guac_protocol_send_sync(guac_socket* socket, guac_timestamp timestamp, + int frames) { int ret_val; @@ -1207,6 +1208,8 @@ int guac_protocol_send_sync(guac_socket* socket, guac_timestamp timestamp) { ret_val = guac_socket_write_string(socket, "4.sync,") || __guac_socket_write_length_int(socket, timestamp) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_int(socket, frames) || guac_socket_write_string(socket, ";"); guac_socket_instruction_end(socket); diff --git a/src/libguac/recording.c b/src/libguac/recording.c index 7544cc144..7e36f4613 100644 --- a/src/libguac/recording.c +++ b/src/libguac/recording.c @@ -39,11 +39,13 @@ /** * Attempts to open a new recording within the given path and having the given - * name. If such a file already exists, sequential numeric suffixes (.1, .2, - * .3, etc.) are appended until a filename is found which does not exist (or - * until the maximum number of numeric suffixes has been tried). If the file - * absolutely cannot be opened due to an error, -1 is returned and errno is set - * appropriately. + * name. If opening the file fails for any reason, or if such a file already + * exists and allow_write_existing is not set, sequential numeric suffixes + * (.1, .2, .3, etc.) are appended until a filename is found which does not + * exist (or until the maximum number of numeric suffixes has been tried). + * If the file exists and allow_write_existing is set, the recording will be + * appended to any existing file contents. If the file absolutely cannot be + * opened due to an error, -1 is returned and errno is set appropriately. * * @param path * The full path to the directory in which the data file should be created. @@ -60,12 +62,17 @@ * @param basename_size * The number of bytes available within the provided basename buffer. * + * @param allow_write_existing + * Non-zero if writing to an existing file should be allowed, or zero + * otherwise. + * * @return * The file descriptor of the open data file if open succeeded, or -1 on * failure. */ static int guac_recording_open(const char* path, - const char* name, char* basename, int basename_size) { + const char* name, char* basename, int basename_size, + int allow_write_existing) { int i; @@ -81,10 +88,11 @@ static int guac_recording_open(const char* path, return -1; } + /* Require the file not exist already if allow_write_existing not set */ + int flags = O_CREAT | O_WRONLY | (allow_write_existing ? 0 : O_EXCL); + /* Attempt to open recording */ - int fd = open(basename, - O_CREAT | O_EXCL | O_WRONLY, - S_IRUSR | S_IWUSR | S_IRGRP); + int fd = open(basename, flags, S_IRUSR | S_IWUSR | S_IRGRP); /* Continuously retry with alternate names on failure */ if (fd == -1) { @@ -101,9 +109,7 @@ static int guac_recording_open(const char* path, sprintf(suffix, "%i", i); /* Retry with newly-suffixed filename */ - fd = open(basename, - O_CREAT | O_EXCL | O_WRONLY, - S_IRUSR | S_IWUSR | S_IRGRP); + fd = open(basename, flags, S_IRUSR | S_IWUSR | S_IRGRP); } @@ -138,7 +144,7 @@ static int guac_recording_open(const char* path, guac_recording* guac_recording_create(guac_client* client, const char* path, const char* name, int create_path, int include_output, int include_mouse, int include_touch, - int include_keys) { + int include_keys, int allow_write_existing) { char filename[GUAC_COMMON_RECORDING_MAX_NAME_LENGTH]; @@ -155,7 +161,8 @@ guac_recording* guac_recording_create(guac_client* client, } /* Attempt to open recording file */ - int fd = guac_recording_open(path, name, filename, sizeof(filename)); + int fd = guac_recording_open( + path, name, filename, sizeof(filename), allow_write_existing); if (fd == -1) { guac_client_log(client, GUAC_LOG_ERROR, "Creation of recording failed: %s", strerror(errno)); diff --git a/src/libguac/rect.c b/src/libguac/rect.c new file mode 100644 index 000000000..2d372fe67 --- /dev/null +++ b/src/libguac/rect.c @@ -0,0 +1,178 @@ +/* + * 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 "guacamole/rect.h" + +/** + * Given a bitmask that is one less than a power of two (ie: 0xF, 0x1F, etc.), + * rounds the given value in the negative direction to the nearest multiple of + * that power of two. Positive values are rounded down towards zero while + * negative values are rounded up toward negative values of greater magnitude. + * + * @param value + * The value to round. + * + * @param mask + * A bitmask whose integer value is one less than a power of two. + * + * @return + * The given value, rounded to the nearest multiple of the power of two + * represented by the given mask, where that rounding is performed in the + * negative direction. + */ +#define GUAC_RECT_ROUND_NEG(value, mask) (value & ~mask) + +/** + * Given a bitmask that is one less than a power of two (ie: 0xF, 0x1F, etc.), + * rounds the given value in the positive direction to the nearest multiple of + * that power of two. Negative values are rounded down towards zero while + * positive values are rounded up toward positive values of greater magnitude. + * + * @param value + * The value to round. + * + * @param mask + * A bitmask whose integer value is one less than a power of two. + * + * @return + * The given value, rounded to the nearest multiple of the power of two + * represented by the given mask, where that rounding is performed in the + * positive direction. + */ +#define GUAC_RECT_ROUND_POS(value, mask) ((value + mask) & ~mask) + +void guac_rect_init(guac_rect* rect, int x, int y, int width, int height) { + *rect = (guac_rect) { + .left = x, + .top = y, + .right = width > 0 ? x + width : x, + .bottom = height > 0 ? y + height : y + }; +} + +void guac_rect_extend(guac_rect* rect, const guac_rect* min) { + + /* The union of an empty rect and the provided rect should be that provided + * rect. Considering the garbage coordinates that may be present in an + * empty rect can otherwise produce incorrect results. */ + if (guac_rect_is_empty(rect)) { + *rect = *min; + return; + } + + /* Extend edges of rectangle such that it contains the provided minimum + * rectangle */ + if (min->left < rect->left) rect->left = min->left; + if (min->top < rect->top) rect->top = min->top; + if (min->right > rect->right) rect->right = min->right; + if (min->bottom > rect->bottom) rect->bottom = min->bottom; + +} + +void guac_rect_constrain(guac_rect* rect, const guac_rect* max) { + + /* Shrink edges of rectangle such that it is contained by the provided + * maximum rectangle */ + if (max->left > rect->left) rect->left = max->left; + if (max->top > rect->top) rect->top = max->top; + if (max->right < rect->right) rect->right = max->right; + if (max->bottom < rect->bottom) rect->bottom = max->bottom; + +} + +void guac_rect_shrink(guac_rect* rect, int max_width, int max_height) { + + int original_width = guac_rect_width(rect); + int original_height = guac_rect_height(rect); + + /* Shrink only; do not _expand_ to reach the max width/height */ + if (original_width < max_width) max_width = original_width; + if (original_height < max_height) max_height = original_height; + + /* BOTH the width and height must be adjusted by the same factor in + * order to preserve aspect ratio. Choosing the smallest adjustment + * factor guarantees that the rectangle will be within bounds while + * preserving aspect ratio to the greatest degree possible (there + * is unavoidable integer rounding error). */ + + int scale_numerator, scale_denominator; + + /* NOTE: The following test is mathematically equivalent to: + * + * if (max_width / original_width < max_height / original_height) { + * ... + * } + * + * but does not require floating point arithmetic. */ + if (max_width * original_height < max_height * original_width) { + scale_numerator = max_width; + scale_denominator = original_width; + } + else { + scale_numerator = max_height; + scale_denominator = original_height; + } + + rect->right = rect->left + original_width * scale_numerator / scale_denominator; + rect->bottom = rect->top + original_height * scale_numerator / scale_denominator; + +} + + +void guac_rect_align(guac_rect* rect, unsigned int bits) { + + if (bits == 0) + return; + + int factor = 1 << bits; + int mask = factor - 1; + + /* Expand and shift rectangle as necessary for its edges to be aligned + * along multiples of the given power of two */ + rect->left = GUAC_RECT_ROUND_NEG(rect->left, mask); + rect->top = GUAC_RECT_ROUND_NEG(rect->top, mask); + rect->right = GUAC_RECT_ROUND_POS(rect->right, mask); + rect->bottom = GUAC_RECT_ROUND_POS(rect->bottom, mask); + +} + +int guac_rect_intersects(const guac_rect* a, const guac_rect* b) { + + /* Two rectangles intersect if neither rectangle is wholly outside the + * other */ + return !( + b->right <= a->left || a->right <= b->left + || b->bottom <= a->top || a->bottom <= b->top + ); + +} + +int guac_rect_is_empty(const guac_rect* rect) { + return rect->right <= rect->left || rect->bottom <= rect->top; +} + +int guac_rect_width(const guac_rect* rect) { + int width = rect->right - rect->left; + return width > 0 ? width : 0; +} + +int guac_rect_height(const guac_rect* rect) { + int height = rect->bottom - rect->top; + return height > 0 ? height : 0; +} diff --git a/src/libguac/rwlock.c b/src/libguac/rwlock.c index 22d041b2d..1ed717664 100644 --- a/src/libguac/rwlock.c +++ b/src/libguac/rwlock.c @@ -179,7 +179,7 @@ int guac_rwlock_acquire_write_lock(guac_rwlock* reentrant_rwlock) { * write lock by another function without the caller knowing about it. This * shouldn't cause any issues, however. */ - if (key_value == GUAC_REENTRANT_LOCK_READ_LOCK) + if (flag == GUAC_REENTRANT_LOCK_READ_LOCK) pthread_rwlock_unlock(&(reentrant_rwlock->lock)); /* Acquire the write lock */ diff --git a/src/libguac/socket-nest.c b/src/libguac/socket-nest.c index 29319097f..8bf94352a 100644 --- a/src/libguac/socket-nest.c +++ b/src/libguac/socket-nest.c @@ -315,7 +315,7 @@ guac_socket* guac_socket_nest(guac_socket* parent, int index) { /* Allocate socket and associated data */ guac_socket* socket = guac_socket_alloc(); - guac_socket_nest_data* data = guac_mem_alloc(sizeof(guac_socket_nest_data)); + guac_socket_nest_data* data = guac_mem_zalloc(sizeof(guac_socket_nest_data)); /* Store nested socket details as socket data */ data->parent = parent; diff --git a/src/libguac/socket.c b/src/libguac/socket.c index e0e01fef9..b7a4b77e8 100644 --- a/src/libguac/socket.c +++ b/src/libguac/socket.c @@ -238,10 +238,32 @@ ssize_t guac_socket_write_string(guac_socket* socket, const char* str) { } -ssize_t __guac_socket_write_base64_triplet(guac_socket* socket, - int a, int b, int c) { - - char output[4]; +/** + * Encodes one to three bytes of data as four characters in base64 encoding. + * + * This function takes int arguments even though it's working with bytes to + * allow -1 to be used as distinct sentinel value for missing data. + * + * @param a An int holding the first byte of data to encode. Only the + * least-significant byte will be used. This will always be inserted + * into the output. + * + * @param b An int holding the second byte of data to encode. Only the + * least-significant byte will be used. If this is less than zero, + * the second and third bytes will be ignored and the last two + * characters of the output will be '=' padding characters. + * + * @param c An int holding the third byte of data to encode. Only the + * least-significant byte will be used. If this is less than zero, + * the third byte will be ignored and the last character of the + * output will be a '=' padding character. + * + * @param output Character buffer to hold the output. Exactly four characters + * will be written to the buffer starting at this location. + * + * @return Returns zero. + */ +ssize_t __guac_socket_encode_base64(int a, int b, int c, char* output) { /* Byte 0:[AAAAAA] AABBBB BBBBCC CCCCCC */ output[0] = __guac_socket_BASE64_CHARACTERS[(a & 0xFC) >> 2]; @@ -251,7 +273,7 @@ ssize_t __guac_socket_write_base64_triplet(guac_socket* socket, /* Byte 1: AAAAAA [AABBBB] BBBBCC CCCCCC */ output[1] = __guac_socket_BASE64_CHARACTERS[((a & 0x03) << 4) | ((b & 0xF0) >> 4)]; - /* + /* * Bytes 2 and 3, zero characters of padding: * * AAAAAA AABBBB [BBBBCC] CCCCCC @@ -262,19 +284,19 @@ ssize_t __guac_socket_write_base64_triplet(guac_socket* socket, output[3] = __guac_socket_BASE64_CHARACTERS[c & 0x3F]; } - /* + /* * Bytes 2 and 3, one character of padding: * * AAAAAA AABBBB [BBBB--] ------ * AAAAAA AABBBB BBBB-- [------] */ - else { + else { output[2] = __guac_socket_BASE64_CHARACTERS[((b & 0x0F) << 2)]; output[3] = '='; } } - /* + /* * Bytes 1, 2, and 3, two characters of padding: * * AAAAAA [AA----] ------ ------ @@ -287,56 +309,70 @@ ssize_t __guac_socket_write_base64_triplet(guac_socket* socket, output[3] = '='; } - /* At this point, 4 base64 bytes have been written */ - if (guac_socket_write(socket, output, 4)) - return -1; - - /* If no second byte was provided, only one byte was written */ - if (b < 0) - return 1; - - /* If no third byte was provided, only two bytes were written */ - if (c < 0) - return 2; + return 0; +} - /* Otherwise, three bytes were written */ - return 3; +ssize_t guac_socket_flush_base64(guac_socket* socket) { + const unsigned char* src = socket->__ready_buf; -} + int encodedCount = 0; + int remaining = socket->__ready; -ssize_t __guac_socket_write_base64_byte(guac_socket* socket, int buf) { + /* Encode bytes in groups of three */ + while (remaining > 2) { + __guac_socket_encode_base64(src[0], src[1], src[2], socket->__encoded_buf + encodedCount); - int* __ready_buf = socket->__ready_buf; + remaining -= 3; + src += 3; + encodedCount += 4; + } - int retval; + /* Take care of partial remnants */ + if (remaining == 2) { + __guac_socket_encode_base64(src[0], src[1], -1, socket->__encoded_buf + encodedCount); + encodedCount += 4; + } + else if (remaining == 1) { + __guac_socket_encode_base64(src[0], -1, -1, socket->__encoded_buf + encodedCount); + encodedCount += 4; + } - __ready_buf[socket->__ready++] = buf; + /* Write buffer to socket */ + int retval = guac_socket_write(socket, socket->__encoded_buf, encodedCount); + if (retval < 0) + return retval; - /* Flush triplet */ - if (socket->__ready == 3) { - retval = __guac_socket_write_base64_triplet(socket, __ready_buf[0], __ready_buf[1], __ready_buf[2]); - if (retval < 0) - return retval; + socket->__ready = 0; - socket->__ready = 0; - } + return 0; - return 1; } ssize_t guac_socket_write_base64(guac_socket* socket, const void* buf, size_t count) { + const unsigned char* src = (const unsigned char*)buf; + size_t remaining = count; + int len; int retval; - const unsigned char* char_buf = (const unsigned char*) buf; - const unsigned char* end = char_buf + count; + while (remaining > 0) { + /* Fill ready buffer as much as possible */ + len = GUAC_SOCKET_BASE64_READY_BUFFER_SIZE - socket->__ready; + if (remaining < len) + len = remaining; - while (char_buf < end) { + memcpy(socket->__ready_buf + socket->__ready, src, len); - retval = __guac_socket_write_base64_byte(socket, *(char_buf++)); - if (retval < 0) - return retval; + socket->__ready += len; + src += len; + remaining -= len; + /* Flush ready buffer when full */ + if (socket->__ready == GUAC_SOCKET_BASE64_READY_BUFFER_SIZE) { + retval = guac_socket_flush_base64(socket); + if (retval < 0) + return retval; + } } return 0; @@ -353,21 +389,3 @@ ssize_t guac_socket_flush(guac_socket* socket) { return 0; } - -ssize_t guac_socket_flush_base64(guac_socket* socket) { - - int retval; - - /* Flush triplet to output buffer */ - while (socket->__ready > 0) { - - retval = __guac_socket_write_base64_byte(socket, -1); - if (retval < 0) - return retval; - - } - - return 0; - -} - diff --git a/src/libguac/string.c b/src/libguac/string.c index 2a7ec2cd4..a2a6e74c5 100644 --- a/src/libguac/string.c +++ b/src/libguac/string.c @@ -22,6 +22,7 @@ #include "guacamole/mem.h" #include +#include #include /** @@ -44,6 +45,20 @@ */ #define REMAINING(n, length) (((n) < (length)) ? 0 : ((n) - (length))) +int guac_itoa(char* restrict dest, unsigned int integer) { + + /* Determine size of string. */ + int str_size = snprintf(dest, 0, "%i", integer); + + /* If an error occurs, just return that and skip the conversion. */ + if (str_size < 0) + return str_size; + + /* Do the conversion and return. */ + return snprintf(dest, (str_size + 1), "%i", integer); + +} + size_t guac_strlcpy(char* restrict dest, const char* restrict src, size_t n) { #ifdef HAVE_STRLCPY @@ -115,7 +130,7 @@ char* guac_strnstr(const char *haystack, const char *needle, size_t len) { } -char* guac_strdup(const char* str) { +char* guac_strndup(const char* str, size_t n) { /* Return NULL if no string provided */ if (str == NULL) @@ -124,18 +139,30 @@ char* guac_strdup(const char* str) { /* Do not attempt to duplicate if the length is somehow magically so * obscenely large that it will not be possible to add a null terminator */ size_t length; - if (guac_mem_ckd_add(&length, strlen(str), 1)) + size_t length_to_copy = strnlen(str, n); + if (guac_mem_ckd_add(&length, length_to_copy, 1)) return NULL; - /* Otherwise just copy to a new string in same manner as strdup() */ - void* new_str = guac_mem_alloc(length); - if (new_str != NULL) - memcpy(new_str, str, length); + /* Otherwise just copy to a new string in same manner as strndup() */ + char* new_str = (char*)guac_mem_alloc(length); + if (new_str != NULL) { + memcpy(new_str, str, length_to_copy); + new_str[length_to_copy] = '\0'; + } return new_str; } +char* guac_strdup(const char* str) { + + /* Return NULL if no string provided */ + if (str == NULL) + return NULL; + + return guac_strndup(str, strlen(str)); +} + size_t guac_strljoin(char* restrict dest, const char* restrict const* elements, int nmemb, const char* restrict delim, size_t n) { @@ -159,4 +186,3 @@ size_t guac_strljoin(char* restrict dest, const char* restrict const* elements, return length; } - diff --git a/src/libguac/tcp.c b/src/libguac/tcp.c new file mode 100644 index 000000000..03b0d9f58 --- /dev/null +++ b/src/libguac/tcp.c @@ -0,0 +1,160 @@ +/* + * 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 "config.h" +#include "guacamole/error.h" +#include "guacamole/tcp.h" + +#include +#include +#include +#include +#include +#include +#include + +int guac_tcp_connect(const char* hostname, const char* port, const int timeout) { + + int retval; + + int fd = EBADFD; + struct addrinfo* addresses; + struct addrinfo* current_address; + + char connected_address[1024]; + char connected_port[64]; + + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_STREAM, + .ai_protocol = IPPROTO_TCP + }; + + /* Get addresses for requested hostname and port. */ + if ((retval = getaddrinfo(hostname, port, &hints, &addresses))) { + guac_error = GUAC_STATUS_INVALID_ARGUMENT; + guac_error_message = "Error parsing address or port."; + return retval; + } + + /* Attempt connection to each address until success */ + for (current_address = addresses; current_address != NULL; current_address = current_address->ai_next) { + + /* Resolve hostname */ + if ((retval = getnameinfo(current_address->ai_addr, + current_address->ai_addrlen, + connected_address, sizeof(connected_address), + connected_port, sizeof(connected_port), + NI_NUMERICHOST | NI_NUMERICSERV))) { + + guac_error = GUAC_STATUS_INVALID_ARGUMENT; + guac_error_message = "Error resolving host."; + continue; + } + + /* Get socket or return the error. */ + fd = socket(current_address->ai_family, SOCK_STREAM, 0); + if (fd < 0) { + freeaddrinfo(addresses); + return fd; + } + + /* Variable to store current socket options. */ + int opt; + + /* Get current socket options */ + if ((opt = fcntl(fd, F_GETFL, NULL)) < 0) { + guac_error = GUAC_STATUS_INVALID_ARGUMENT; + guac_error_message = "Failed to retrieve socket options."; + close(fd); + continue; + } + + /* Set socket to non-blocking */ + if (fcntl(fd, F_SETFL, opt | O_NONBLOCK) < 0) { + guac_error = GUAC_STATUS_INVALID_ARGUMENT; + guac_error_message = "Failed to set non-blocking socket."; + close(fd); + continue; + } + + /* Structure that stores our timeout setting. */ + struct timeval tv; + tv.tv_sec = timeout; + tv.tv_usec = 0; + + /* Connect and wait for timeout */ + if ((retval = connect(fd, current_address->ai_addr, current_address->ai_addrlen)) < 0) { + if (errno == EINPROGRESS) { + /* Set up timeout. */ + fd_set fdset; + FD_ZERO(&fdset); + FD_SET(fd, &fdset); + + retval = select(fd + 1, NULL, &fdset, NULL, &tv); + } + + else { + guac_error = GUAC_STATUS_REFUSED; + guac_error_message = "Unable to connect via socket."; + close(fd); + continue; + } + } + + /* Successful connection */ + if (retval > 0) { + /* Restore previous socket options. */ + if (fcntl(fd, F_SETFL, opt) < 0) { + guac_error = GUAC_STATUS_INVALID_ARGUMENT; + guac_error_message = "Failed to reset socket options."; + close(fd); + continue; + } + + break; + } + + if (retval == 0) { + guac_error = GUAC_STATUS_REFUSED; + guac_error_message = "Timeout connecting via socket."; + } + else { + guac_error = GUAC_STATUS_INVALID_ARGUMENT; + guac_error_message = "Error attempting to connect via socket."; + } + + /* Some error has occurred - free resources before next iteration. */ + close(fd); + + } + + /* Free addrinfo */ + freeaddrinfo(addresses); + + /* If unable to connect to anything, set error status. */ + if (current_address == NULL) { + guac_error = GUAC_STATUS_REFUSED; + guac_error_message = "Unable to connect to remote host."; + } + + /* Return the fd, or the error message if the socket connection failed. */ + return fd; + +} diff --git a/src/libguac/tests/Makefile.am b/src/libguac/tests/Makefile.am index dba842419..1d2f45f11 100644 --- a/src/libguac/tests/Makefile.am +++ b/src/libguac/tests/Makefile.am @@ -39,6 +39,8 @@ noinst_HEADERS = \ test_libguac_SOURCES = \ client/buffer_pool.c \ client/layer_pool.c \ + fifo/fifo.c \ + flag/flag.c \ id/generate.c \ mem/alloc.c \ mem/ckd_add.c \ @@ -56,6 +58,11 @@ test_libguac_SOURCES = \ pool/next_free.c \ protocol/base64_decode.c \ protocol/guac_protocol_version.c \ + rect/align.c \ + rect/constrain.c \ + rect/extend.c \ + rect/init.c \ + rect/intersects.c \ socket/fd_send_instruction.c \ socket/nested_send_instruction.c \ string/strdup.c \ diff --git a/src/libguac/tests/fifo/fifo.c b/src/libguac/tests/fifo/fifo.c new file mode 100644 index 000000000..67617e2f3 --- /dev/null +++ b/src/libguac/tests/fifo/fifo.c @@ -0,0 +1,298 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include + +/** + * The maximum number of milliseconds to wait for a test event to be added to a + * fifo. + */ +#define TEST_TIMEOUT 250 + +/** + * The maximum number of items permitted in test_fifo. + */ +#define TEST_FIFO_MAX_ITEMS 4 + +/** + * The rough amount of time to wait between fifo reads within the test thread, + * in milliseconds. A random delay between 0ms and this value will be added + * before each read. This is done to verify that the fifo behaves correctly + * for cases where the sending thread is producing data much faster than it's + * being read, slower than it's read, etc. + */ +#define TEST_READ_INTERVAL 10 + +/** + * Zero-terminated set of arbitrarily-chosen values that will be provided as + * the test_value of a sequence of test_events. + */ +unsigned int TEST_VALUES[] = { + 32, 32, 226, 136, 167, 44, 44, 44, + 226, 136, 167, 32, 32, 32, 32, 32, + 65, 112, 97, 119, 99, 104, 101, 10, + 32, 40, 226, 128, 162, 32, 226, 169, + 138, 32, 226, 128, 162, 41, 32, 32, + 71, 117, 97, 99, 97, 109, 101, 111, + 119, 108, 101, 33, 10, /* END */ 0 +}; + +/** + * Test event for an event fifo. This particular event contains a single + * integer for verifying that events are received in the order expected, and a + * chunk of arbitrary padding to ensure the base fifo is capable of supporting + * events of arbitrary size. + */ +typedef union test_event { + + /** + * Arbitrary integer test value. This value is primarily intended to allow + * unit tests to verify the order of received events matches the order they + * were sent. + */ + unsigned int test_value; + + /** + * Arbitrary padding. This member is entirely ignored and is used only to + * increase the storage size of this event. A wonky prime value is used + * here to help ensure the tests inherently verify that the base fifo + * implementation does not somehow depend on power-of-two alignment. + */ + char padding[73]; + +} test_event; + +/** + * Test event fifo that extends the guac_fifo base. This event + * fifo differs from the base only in that it specifically stores test_events + * alongside an array of expected event values. + */ +typedef struct test_fifo { + + /** + * The base fifo implementation. + */ + guac_fifo base; + + /** + * Storage for all event items in this fifo. + */ + test_event items[TEST_FIFO_MAX_ITEMS]; + + /** + * A zero-terminated array of all integer values expected to be received as + * test events, in the order that they are expected to be received. + */ + unsigned int* expected_values; + +} test_fifo; + +/** + * Initializes the given test_fifo, assigning the given set of expected + * values for later reference by unit tests. The pointer to the expected values + * MUST remain valid until the text_fifo is destroyed. + * + * @param fifo + * The test_fifo to initialize. + * + * @param expected_values + * The zero-terminated set of expected values to be associated with the + * given test_fifo. + */ +void test_fifo_init(test_fifo* fifo, unsigned int* expected_values) { + + guac_fifo_init((guac_fifo*) fifo, &fifo->items, + TEST_FIFO_MAX_ITEMS, sizeof(test_event)); + + fifo->expected_values = expected_values; + +} + +/** + * Destroys the given test_fifo, releasing any associated resources. It + * is safe to clean up the set of expected values originally provided to + * test_fifo_init() after this function has been invoked. + * + * @param fifo + * The test_fifo to destroy. + */ +void test_fifo_destroy(test_fifo* fifo) { + guac_fifo_destroy((guac_fifo*) fifo); +} + +/** + * Thread that continuously reads events from the given test_fifo, + * verifying that each expected value is read in the correct order. + * + * @param data + * The test_fifo to read from. + * + * @return + * Always NULL. + */ +static void* queue_read_thread(void* data) { + + test_fifo* fifo = (test_fifo*) data; + test_event event; + + /* Continuously read values until zero (end of expected values) is reached */ + for (unsigned int* current_expected_value = fifo->expected_values; + /* Exit condition checked in body of loop*/; current_expected_value++) { + + /* Induce random delays in reading to simulate real-world conditions + * that may cause the fifo to fill */ + guac_timestamp_msleep(rand() % TEST_READ_INTERVAL); + + int retval = guac_fifo_timed_dequeue( + (guac_fifo*) fifo, &event, TEST_TIMEOUT); + + /* A value of zero marks the end of the set of expected values, so the + * fifo SHOULD fail to read at this point */ + if (*current_expected_value == 0) { + printf(" | END\n"); + CU_ASSERT_FALSE(retval); + break; + } + + /* For all other cases, the fifo should succeed in reading the next + * event, and the value of that event should match the current value + * from the set of expected values */ + else { + printf(" | %i\n", event.test_value); + CU_ASSERT_TRUE(retval); + CU_ASSERT_EQUAL(event.test_value, *current_expected_value); + } + + /* Do not continue waiting for more events if the fifo is timing out + * incorrectly */ + if (!retval) + break; + + } + + return NULL; + +} + +/** + * Generic base test that sends all values in TEST_VALUES at the given + * interval. Values are read by a separate thread that instead reads at + * TEST_READ_INTERVAL, allowing the send/receive rates to differ. Timing + * between each send/receive attempt is varied randomly but is always bounded + * by the relevant interval. + * + * @param send_interval + * The rough number of milliseconds to wait between sending each event. The + * true number of milliseconds that elapse between each subsequent send + * attempt is varied randomly, with this provided value functioning as an + * upper bound. + */ +static void verify_send_receive(int send_interval) { + + test_fifo fifo; + + /* Create a test fifo that verifies each value within TEST_VALUES is + * received in order */ + test_fifo_init(&fifo, TEST_VALUES); + + /* Both this function and the thread it spawns will log sent/received event + * values to STDOUT for sake of debugging and verification */ + printf("Sent | Received\n" + "---- | --------\n"); + + /* Spawn thread that can independently wait for events to be flagged */ + pthread_t test_thread; + CU_ASSERT_FALSE_FATAL(pthread_create(&test_thread, NULL, queue_read_thread, &fifo)); + + /* Send all test values in order */ + for (unsigned int* current = TEST_VALUES; *current != 0; current++) { + + /* Pull next test value from TEST_VALUES array */ + test_event event = { + .test_value = *current + }; + + /* Induce random delays in reading to simulate real-world conditions + * that may cause the fifo to fill */ + if (send_interval) + guac_timestamp_msleep(rand() % send_interval); + + printf("%4i |\n", event.test_value); + guac_fifo_enqueue((guac_fifo*) &fifo, &event); + + } + + /* All test values have now been sent */ + printf(" END |\n"); + + /* Wait for thread to finish waiting for events */ + CU_ASSERT_FALSE(pthread_join(test_thread, NULL)); + + test_fifo_destroy(&fifo); + +} + +/** + * Verify that the base fifo implementation functions correctly when events + * are sent slower than they are read. + */ +void test_fifo__slow_add() { + + /* Add context for subsequent logging of sent/received values to STDOUT */ + printf("-------- %s() --------\n", __func__); + + /* Send at half the speed of the reading thread */ + verify_send_receive(TEST_READ_INTERVAL * 2); + +} + +/** + * Verify that the base fifo implementation functions correctly when events + * are sent faster than they are read. + */ +void test_fifo__fast_add() { + + /* Add context for subsequent logging of sent/received values to STDOUT */ + printf("-------- %s() --------\n", __func__); + + /* Send as quickly as possible (much faster than reading thread) */ + verify_send_receive(0); + +} + +/** + * Verify that the base fifo implementation functions correctly when events + * are sent at roughly the same speed as the reading thread. + */ +void test_fifo__interleaved() { + + /* Add context for subsequent logging of sent/received values to STDOUT */ + printf("-------- %s() --------\n", __func__); + + /* Send at roughly same speed as reading thread */ + verify_send_receive(TEST_READ_INTERVAL); + +} + diff --git a/src/libguac/tests/flag/flag.c b/src/libguac/tests/flag/flag.c new file mode 100644 index 000000000..c822a6a7e --- /dev/null +++ b/src/libguac/tests/flag/flag.c @@ -0,0 +1,168 @@ +/* + * 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 +#include +#include +#include + +/** + * The maximum number of milliseconds to wait for a test event to be flagged. + */ +#define TEST_TIMEOUT 250 + +/** + * Arbitrary test event #1. + */ +#define TEST_EVENT_A 1 + +/** + * Arbitrary test event #2. + */ +#define TEST_EVENT_B 2 + +/** + * Arbitrary test event #3. + */ +#define TEST_EVENT_C 16 + +/** + * Arbitrary test event #4. + */ +#define TEST_EVENT_D 64 + +/** + * Thread that waits up to TEST_TIMEOUT milliseconds for TEST_EVENT_B or + * TEST_EVENT_C to be flagged on a given guac_flag, returning the + * result of that wait. + * + * @param data + * The guac_flag to wait on. + * + * @return + * An intptr_t (NOT a pointer) containing the value returned by + * guac_flag_timedwait_and_lock(). + */ +static void* flag_wait_thread(void* data) { + + guac_flag* flag = (guac_flag*) data; + + int retval = guac_flag_timedwait_and_lock(flag, TEST_EVENT_B | TEST_EVENT_C, TEST_TIMEOUT); + guac_flag_unlock(flag); + + return (void*) ((intptr_t) retval); + +} + +/** + * Waits up to TEST_TIMEOUT milliseconds for TEST_EVENT_B or TEST_EVENT_C to be + * flagged on the given guac_flag, returning the result of that + * wait. If provided, optional sets of flags will be additionally set or + * cleared after the wait for the flag has started. + * + * @param flag + * The guac_flag to wait on. + * + * @param set_flags + * The flags that should be set, if any. + * + * @param clear_flags + * The flags that should be cleared, if any. + * + * @return + * The value returned by guac_flag_timedwait_and_lock() after + * waiting for TEST_EVENT_B or TEST_EVENT_C to be flagged. + */ +static int wait_for_flag(guac_flag* flag, int set_flags, int clear_flags) { + + /* Spawn thread that can independently wait for events to be flagged */ + pthread_t test_thread; + CU_ASSERT_FALSE_FATAL(pthread_create(&test_thread, NULL, flag_wait_thread, flag)); + + /* Set/clear any requested event flags */ + if (set_flags) guac_flag_set(flag, set_flags); + if (clear_flags) guac_flag_clear(flag, clear_flags); + + /* Wait for thread to finish waiting for events */ + void* retval; + CU_ASSERT_FALSE(pthread_join(test_thread, &retval)); + + return (int) ((intptr_t) retval); + +} + +/** + * Verifies that a thread waiting on a particular event will NOT be notified if + * absolutely zero events ever occur. + */ +void test_flag__ignore_total_silence() { + + guac_flag test_flag; + guac_flag_init(&test_flag); + + /* Verify no interesting events occur if we set zero flags */ + CU_ASSERT_FALSE(wait_for_flag(&test_flag, 0, 0)); + + guac_flag_destroy(&test_flag); + +} + +/** + * Verifies that a thread waiting on a particular event will NOT be notified if + * that event never occurs, even if other events are occurring. + */ +void test_flag__ignore_uninteresting_events() { + + guac_flag test_flag; + guac_flag_init(&test_flag); + + /* Verify no interesting events occurred if we only fire uninteresting + * events */ + CU_ASSERT_FALSE(wait_for_flag(&test_flag, TEST_EVENT_A, 0)); + CU_ASSERT_FALSE(wait_for_flag(&test_flag, TEST_EVENT_D, TEST_EVENT_C)); + CU_ASSERT_FALSE(wait_for_flag(&test_flag, TEST_EVENT_A | TEST_EVENT_D, 0)); + + guac_flag_destroy(&test_flag); + +} + +/** + * Verifies that a thread waiting on a particular event will be notified when + * that event occurs. + */ +void test_flag__wake_for_interesting_events() { + + guac_flag test_flag; + guac_flag_init(&test_flag); + + /* Verify interesting events are reported if fired ... */ + CU_ASSERT_TRUE(wait_for_flag(&test_flag, TEST_EVENT_B | TEST_EVENT_C, 0)); + + /* ... and continue to be reported if they remain set ... */ + guac_flag_clear(&test_flag, TEST_EVENT_B); + CU_ASSERT_TRUE(wait_for_flag(&test_flag, 0, 0)); + + /* ... but not if all interesting events have since been cleared */ + guac_flag_clear(&test_flag, TEST_EVENT_C); + CU_ASSERT_FALSE(wait_for_flag(&test_flag, 0, 0)); + + guac_flag_destroy(&test_flag); + +} + diff --git a/src/libguac/tests/rect/align.c b/src/libguac/tests/rect/align.c new file mode 100644 index 000000000..bed197269 --- /dev/null +++ b/src/libguac/tests/rect/align.c @@ -0,0 +1,70 @@ +/* + * 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 +#include + +/** + * Test which verifies guac_rect_align() properly shifts and resizes rectangles + * to fit an NxN grid. + */ +void test_rect__align() { + + /* A cell size of 4 is 2^4 (16) */ + const int cell_size = 4; + + guac_rect rect; + + /* Simple case where only the rectangle dimensions need adjustment */ + guac_rect_init(&rect, 0, 0, 25, 25); + guac_rect_align(&rect, cell_size); + CU_ASSERT_EQUAL(0, rect.left); + CU_ASSERT_EQUAL(0, rect.top); + CU_ASSERT_EQUAL(32, rect.right); + CU_ASSERT_EQUAL(32, rect.bottom); + + /* More complex case where the rectangle location AND dimensions both need + * adjustment */ + guac_rect_init(&rect, 75, 75, 25, 25); + guac_rect_align(&rect, cell_size); + CU_ASSERT_EQUAL(64, rect.left); + CU_ASSERT_EQUAL(64, rect.top); + CU_ASSERT_EQUAL(112, rect.right); + CU_ASSERT_EQUAL(112, rect.bottom); + + /* Complex case where the rectangle location AND dimensions both need + * adjustment, and the rectangle location is negative */ + guac_rect_init(&rect, -5, -5, 25, 25); + guac_rect_align(&rect, cell_size); + CU_ASSERT_EQUAL(-16, rect.left); + CU_ASSERT_EQUAL(-16, rect.top); + CU_ASSERT_EQUAL(32, rect.right); + CU_ASSERT_EQUAL(32, rect.bottom); + + /* Complex case where the rectangle location AND dimensions both need + * adjustment, and all rectangle coordinates are negative */ + guac_rect_init(&rect, -30, -30, 25, 25); + guac_rect_align(&rect, cell_size); + CU_ASSERT_EQUAL(-32, rect.left); + CU_ASSERT_EQUAL(-32, rect.top); + CU_ASSERT_EQUAL(0, rect.right); + CU_ASSERT_EQUAL(0, rect.bottom); + +} + diff --git a/src/libguac/tests/rect/constrain.c b/src/libguac/tests/rect/constrain.c new file mode 100644 index 000000000..18ddc7a87 --- /dev/null +++ b/src/libguac/tests/rect/constrain.c @@ -0,0 +1,42 @@ +/* + * 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 +#include + +/** + * Test which verifies that guac_rect_constrain() restricts a given rectangle + * to arbitrary bounds. + */ +void test_rect__constrain() { + + guac_rect max; + guac_rect rect; + + guac_rect_init(&rect, -10, -10, 110, 110); + guac_rect_init(&max, 0, 0, 100, 100); + guac_rect_constrain(&rect, &max); + + CU_ASSERT_EQUAL(0, rect.left); + CU_ASSERT_EQUAL(0, rect.top); + CU_ASSERT_EQUAL(100, rect.right); + CU_ASSERT_EQUAL(100, rect.bottom); + +} + diff --git a/src/libguac/tests/rect/extend.c b/src/libguac/tests/rect/extend.c new file mode 100644 index 000000000..264ab3bfc --- /dev/null +++ b/src/libguac/tests/rect/extend.c @@ -0,0 +1,41 @@ +/* + * 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 +#include + +/** + * Test which verifies that guac_rect_extend() expands the given rectangle as + * necessary to contain at least the given bounds. + */ +void test_rect__extend() { + + guac_rect max; + guac_rect rect; + + guac_rect_init(&rect, 10, 10, 90, 90); + guac_rect_init(&max, 0, 0, 100, 100); + guac_rect_extend(&rect, &max); + CU_ASSERT_EQUAL(0, rect.left); + CU_ASSERT_EQUAL(0, rect.top); + CU_ASSERT_EQUAL(100, rect.right); + CU_ASSERT_EQUAL(100, rect.bottom); + +} + diff --git a/src/libguac/tests/rect/init.c b/src/libguac/tests/rect/init.c new file mode 100644 index 000000000..c91efec25 --- /dev/null +++ b/src/libguac/tests/rect/init.c @@ -0,0 +1,38 @@ +/* + * 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 +#include + +/** + * Test which verifies rectangle initialization via guac_rect_init(). + */ +void test_rect__init() { + + guac_rect max; + + guac_rect_init(&max, 0, 0, 100, 100); + + CU_ASSERT_EQUAL(0, max.left); + CU_ASSERT_EQUAL(0, max.top); + CU_ASSERT_EQUAL(100, max.right); + CU_ASSERT_EQUAL(100, max.bottom); + +} + diff --git a/src/libguac/tests/rect/intersects.c b/src/libguac/tests/rect/intersects.c new file mode 100644 index 000000000..e6b005e74 --- /dev/null +++ b/src/libguac/tests/rect/intersects.c @@ -0,0 +1,85 @@ +/* + * 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 +#include + +/** + * Test which verifies intersection testing via guac_rect_intersects(). + */ +void test_rect__intersects() { + + int res; + + guac_rect min; + guac_rect rect; + + /* NOTE: This rectangle will extend from (10, 10) inclusive to (20, 20) exclusive */ + guac_rect_init(&min, 10, 10, 10, 10); + + /* Rectangle that does not intersect by a fair margin */ + guac_rect_init(&rect, 25, 25, 5, 5); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_FALSE(res); + + /* Rectangle that barely does not intersect (one pixel away from intersecting) */ + guac_rect_init(&rect, 20, 20, 5, 5); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_FALSE(res); + + /* Rectangle that intersects by being entirely inside the other */ + guac_rect_init(&rect, 11, 11, 5, 5); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_TRUE(res); + + /* Rectangle that intersects with the upper-left corner */ + guac_rect_init(&rect, 8, 8, 5, 5); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_TRUE(res); + + /* Rectangle that intersects with the lower-right corner */ + guac_rect_init(&rect, 18, 18, 5, 5); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_TRUE(res); + + /* Rectangle that intersects with the uppper-left corner and shares both + * the upper and left edges */ + guac_rect_init(&rect, 10, 10, 5, 5); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_TRUE(res); + + /* Rectangle that barely fails to intersect the upper-left corner (one + * pixel away) */ + guac_rect_init(&rect, 5, 10, 5, 5); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_FALSE(res); + + /* Rectangle that barely fails to intersect the upper-right corner (one + * pixel away) */ + guac_rect_init(&rect, 20, 10, 5, 5); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_FALSE(res); + + /* Rectangle that intersects by entirely containing the other */ + guac_rect_init(&rect, 5, 5, 20, 20); + res = guac_rect_intersects(&rect, &min); + CU_ASSERT_TRUE(res); + +} + diff --git a/src/libguac/tests/socket/fd_send_instruction.c b/src/libguac/tests/socket/fd_send_instruction.c index 7d991e6b4..abbf5cc8f 100644 --- a/src/libguac/tests/socket/fd_send_instruction.c +++ b/src/libguac/tests/socket/fd_send_instruction.c @@ -54,7 +54,7 @@ static void write_instructions(int fd) { /* Write instructions */ guac_protocol_send_name(socket, "a" UTF8_4 "b" UTF8_4 "c"); - guac_protocol_send_sync(socket, 12345); + guac_protocol_send_sync(socket, 12345, 1); guac_socket_flush(socket); /* Close and free socket */ @@ -76,7 +76,7 @@ static void read_expected_instructions(int fd) { char expected[] = "4.name,11.a" UTF8_4 "b" UTF8_4 "c;" - "4.sync,5.12345;"; + "4.sync,5.12345,1.1;"; int numread; char buffer[1024]; diff --git a/src/libguac/tests/socket/nested_send_instruction.c b/src/libguac/tests/socket/nested_send_instruction.c index c6d337542..14e876b25 100644 --- a/src/libguac/tests/socket/nested_send_instruction.c +++ b/src/libguac/tests/socket/nested_send_instruction.c @@ -65,7 +65,7 @@ static void write_instructions(int fd) { /* Write instructions */ guac_protocol_send_name(nested_socket, "a" UTF8_4 "b" UTF8_4 "c"); - guac_protocol_send_sync(nested_socket, 12345); + guac_protocol_send_sync(nested_socket, 12345, 1); /* Close and free sockets */ guac_socket_free(nested_socket); @@ -86,9 +86,9 @@ static void write_instructions(int fd) { static void read_expected_instructions(int fd) { char expected[] = - "4.nest,3.123,37." + "4.nest,3.123,41." "4.name,11.a" UTF8_4 "b" UTF8_4 "c;" - "4.sync,5.12345;" + "4.sync,5.12345,1.1;" ";"; int numread; diff --git a/src/libguac/user-handlers.c b/src/libguac/user-handlers.c index 1dfef55fd..b9013bc7c 100644 --- a/src/libguac/user-handlers.c +++ b/src/libguac/user-handlers.c @@ -123,31 +123,39 @@ int __guac_handle_sync(guac_user* user, int argc, char** argv) { /* Calculate length of frame, including network and processing lag */ frame_duration = current - timestamp; - /* Update lag statistics if at least one frame has been rendered */ + /* Calculate processing lag portion of length of frame */ + int frame_processing_lag = 0; if (user->last_frame_duration != 0) { /* Calculate lag using the previous frame as a baseline */ - int processing_lag = frame_duration - user->last_frame_duration; + frame_processing_lag = frame_duration - user->last_frame_duration; /* Adjust back to zero if cumulative error leads to a negative * value */ - if (processing_lag < 0) - processing_lag = 0; - - user->processing_lag = processing_lag; + if (frame_processing_lag < 0) + frame_processing_lag = 0; } - /* Record baseline duration of frame by excluding lag */ - user->last_frame_duration = frame_duration - user->processing_lag; + /* Record baseline duration of frame by excluding lag (this is the + * network round-trip time) */ + int estimated_rtt = frame_duration - frame_processing_lag; + user->last_frame_duration = estimated_rtt; + + /* Calculate cumulative accumulated processing lag relative to server timeline */ + int processing_lag = current - user->last_received_timestamp - estimated_rtt; + if (processing_lag < 0) + processing_lag = 0; + + user->processing_lag = processing_lag; } /* Log received timestamp and calculated lag (at TRACE level only) */ guac_user_log(user, GUAC_LOG_TRACE, "User confirmation of frame %" PRIu64 "ms received " - "at %" PRIu64 "ms (processing_lag=%ims)", - timestamp, current, user->processing_lag); + "at %" PRIu64 "ms (processing_lag=%ims, estimated_rtt=%ims)", + timestamp, current, user->processing_lag, user->last_frame_duration); if (user->sync_handler) return user->sync_handler(user, timestamp); diff --git a/src/libguac/user.c b/src/libguac/user.c index 846d0eddd..753aca3fa 100644 --- a/src/libguac/user.c +++ b/src/libguac/user.c @@ -107,13 +107,11 @@ guac_stream* guac_user_alloc_stream(guac_user* user) { guac_stream* allocd_stream; int stream_index; - /* Refuse to allocate beyond maximum */ - if (user->__stream_pool->active == GUAC_USER_MAX_STREAMS) + /* Allocate stream, but refuse to allocate beyond maximum */ + stream_index = guac_pool_next_int_below(user->__stream_pool, GUAC_USER_MAX_STREAMS); + if (stream_index < 0) return NULL; - /* Allocate stream */ - stream_index = guac_pool_next_int(user->__stream_pool); - /* Initialize stream with even index (odd indices are client-level) */ allocd_stream = &(user->__output_streams[stream_index]); allocd_stream->index = stream_index * 2; @@ -128,12 +126,13 @@ guac_stream* guac_user_alloc_stream(guac_user* user) { void guac_user_free_stream(guac_user* user, guac_stream* stream) { - /* Release index to pool */ - guac_pool_free_int(user->__stream_pool, stream->index / 2); - /* Mark stream as closed */ + int freed_index = stream->index; stream->index = GUAC_USER_CLOSED_STREAM_INDEX; + /* Release index to pool */ + guac_pool_free_int(user->__stream_pool, freed_index / 2); + } guac_object* guac_user_alloc_object(guac_user* user) { @@ -141,13 +140,11 @@ guac_object* guac_user_alloc_object(guac_user* user) { guac_object* allocd_object; int object_index; - /* Refuse to allocate beyond maximum */ - if (user->__object_pool->active == GUAC_USER_MAX_OBJECTS) + /* Allocate object, but refuse to allocate beyond maximum */ + object_index = guac_pool_next_int_below(user->__object_pool, GUAC_USER_MAX_OBJECTS); + if (object_index < 0) return NULL; - /* Allocate object */ - object_index = guac_pool_next_int(user->__object_pool); - /* Initialize object */ allocd_object = &(user->__objects[object_index]); allocd_object->index = object_index; @@ -161,12 +158,13 @@ guac_object* guac_user_alloc_object(guac_user* user) { void guac_user_free_object(guac_user* user, guac_object* object) { - /* Release index to pool */ - guac_pool_free_int(user->__object_pool, object->index); - /* Mark object as undefined */ + int freed_index = object->index; object->index = GUAC_USER_UNDEFINED_OBJECT_INDEX; + /* Release index to pool */ + guac_pool_free_int(user->__object_pool, freed_index); + } int guac_user_handle_instruction(guac_user* user, const char* opcode, int argc, char** argv) { diff --git a/src/libguac/wol.c b/src/libguac/wol.c index 9d69306c9..1671abcfc 100644 --- a/src/libguac/wol.c +++ b/src/libguac/wol.c @@ -20,6 +20,8 @@ #include "config.h" #include "guacamole/error.h" +#include "guacamole/tcp.h" +#include "guacamole/timestamp.h" #include "guacamole/wol.h" #include @@ -195,4 +197,53 @@ int guac_wol_wake(const char* mac_addr, const char* broadcast_addr, return 0; return -1; -} \ No newline at end of file +} + +int guac_wol_wake_and_wait(const char* mac_addr, const char* broadcast_addr, + const unsigned short udp_port, int wait_time, int retries, + const char* hostname, const char* port, const int timeout) { + + /* Attempt to connect, first. */ + int sockfd = guac_tcp_connect(hostname, port, timeout); + + /* If connection succeeds, no need to wake the system. */ + if (sockfd > 0) { + close(sockfd); + return 0; + } + + /* Close the fd to avoid resource leak. */ + close(sockfd); + + /* Send the magic WOL packet and store return value. */ + int retval = guac_wol_wake(mac_addr, broadcast_addr, udp_port); + + /* If sending WOL packet fails, just return the received return value. */ + if (retval) + return retval; + + /* Try to connect on the specified TCP port and hostname or IP. */ + for (int i = 0; i < retries; i++) { + + sockfd = guac_tcp_connect(hostname, port, timeout); + + /* Connection succeeded - close socket and exit. */ + if (sockfd > 0) { + close(sockfd); + return 0; + } + + /** + * Connection did not succed - close the socket and sleep for the + * specified amount of time before retrying. + */ + close(sockfd); + guac_timestamp_msleep(wait_time * 1000); + } + + /* Failed to connect, set error message and return an error. */ + guac_error = GUAC_STATUS_REFUSED; + guac_error_message = "Unable to connect to remote host."; + return -1; + +} diff --git a/src/protocols/kubernetes/kubernetes.c b/src/protocols/kubernetes/kubernetes.c index 163c55626..a4ce74a0e 100644 --- a/src/protocols/kubernetes/kubernetes.c +++ b/src/protocols/kubernetes/kubernetes.c @@ -237,7 +237,8 @@ void* guac_kubernetes_client_thread(void* data) { !settings->recording_exclude_output, !settings->recording_exclude_mouse, 0, /* Touch events not supported */ - settings->recording_include_keys); + settings->recording_include_keys, + settings->recording_write_existing); } /* Create terminal options with required parameters */ @@ -274,7 +275,8 @@ void* guac_kubernetes_client_thread(void* data) { guac_terminal_create_typescript(kubernetes_client->term, settings->typescript_path, settings->typescript_name, - settings->create_typescript_path); + settings->create_typescript_path, + settings->typescript_write_existing); } /* Init libwebsockets context creation parameters */ diff --git a/src/protocols/kubernetes/settings.c b/src/protocols/kubernetes/settings.c index 5da7d8bec..698fcc1ed 100644 --- a/src/protocols/kubernetes/settings.c +++ b/src/protocols/kubernetes/settings.c @@ -45,12 +45,14 @@ const char* GUAC_KUBERNETES_CLIENT_ARGS[] = { "typescript-path", "typescript-name", "create-typescript-path", + "typescript-write-existing", "recording-path", "recording-name", "recording-exclude-output", "recording-exclude-mouse", "recording-include-keys", "create-recording-path", + "recording-write-existing", "read-only", "backspace", "scrollback", @@ -166,6 +168,12 @@ enum KUBERNETES_ARGS_IDX { */ IDX_CREATE_TYPESCRIPT_PATH, + /** + * Whether existing files should be appended to when creating a new + * typescript. Disabled by default. + */ + IDX_TYPESCRIPT_WRITE_EXISTING, + /** * The full absolute path to the directory in which screen recordings * should be written. @@ -210,6 +218,12 @@ enum KUBERNETES_ARGS_IDX { */ IDX_CREATE_RECORDING_PATH, + /** + * Whether existing files should be appended to when creating a new recording. + * Disabled by default. + */ + IDX_RECORDING_WRITE_EXISTING, + /** * "true" if this connection should be read-only (user input should be * dropped), "false" or blank otherwise. @@ -359,6 +373,11 @@ guac_kubernetes_settings* guac_kubernetes_parse_args(guac_user* user, guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, IDX_CREATE_TYPESCRIPT_PATH, false); + /* Parse allow write existing file flag */ + settings->typescript_write_existing = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_TYPESCRIPT_WRITE_EXISTING, false); + /* Read recording path */ settings->recording_path = guac_user_parse_args_string(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, @@ -389,6 +408,11 @@ guac_kubernetes_settings* guac_kubernetes_parse_args(guac_user* user, guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, IDX_CREATE_RECORDING_PATH, false); + /* Parse allow write existing file flag */ + settings->recording_write_existing = + guac_user_parse_args_boolean(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, + IDX_RECORDING_WRITE_EXISTING, false); + /* Parse backspace key code */ settings->backspace = guac_user_parse_args_int(user, GUAC_KUBERNETES_CLIENT_ARGS, argv, diff --git a/src/protocols/kubernetes/settings.h b/src/protocols/kubernetes/settings.h index f7c9c1c37..468029c93 100644 --- a/src/protocols/kubernetes/settings.h +++ b/src/protocols/kubernetes/settings.h @@ -191,6 +191,12 @@ typedef struct guac_kubernetes_settings { */ bool create_typescript_path; + /** + * Whether existing files should be appended to when creating a new + * typescript. Disabled by default. + */ + bool typescript_write_existing; + /** * The path in which the screen recording should be saved, if enabled. If * no screen recording should be saved, this will be NULL. @@ -234,6 +240,12 @@ typedef struct guac_kubernetes_settings { */ bool recording_include_keys; + /** + * Whether existing files should be appended to when creating a new recording. + * Disabled by default. + */ + bool recording_write_existing; + /** * The ASCII code, as an integer, that the Kubernetes client will use when * the backspace key is pressed. By default, this is 127, ASCII delete, if diff --git a/src/protocols/kubernetes/user.c b/src/protocols/kubernetes/user.c index 369923de2..e972520b9 100644 --- a/src/protocols/kubernetes/user.c +++ b/src/protocols/kubernetes/user.c @@ -19,7 +19,6 @@ #include "argv.h" #include "clipboard.h" -#include "common/cursor.h" #include "input.h" #include "kubernetes.h" #include "pipe.h" diff --git a/src/protocols/rdp/Makefile.am b/src/protocols/rdp/Makefile.am index 89c094bb9..12a2e68b4 100644 --- a/src/protocols/rdp/Makefile.am +++ b/src/protocols/rdp/Makefile.am @@ -40,7 +40,6 @@ nodist_libguac_client_rdp_la_SOURCES = \ libguac_client_rdp_la_SOURCES = \ argv.c \ beep.c \ - bitmap.c \ channels/audio-input/audio-buffer.c \ channels/audio-input/audio-input.c \ channels/cliprdr.c \ @@ -57,6 +56,7 @@ libguac_client_rdp_la_SOURCES = \ channels/rdpdr/rdpdr-printer.c \ channels/rdpdr/rdpdr.c \ channels/rdpei.c \ + channels/rdpgfx.c \ channels/rdpsnd/rdpsnd-messages.c \ channels/rdpsnd/rdpsnd.c \ client.c \ @@ -66,7 +66,6 @@ libguac_client_rdp_la_SOURCES = \ error.c \ fs.c \ gdi.c \ - glyph.c \ input.c \ keyboard.c \ keymap.c \ @@ -86,7 +85,6 @@ libguac_client_rdp_la_SOURCES = \ noinst_HEADERS = \ argv.h \ beep.h \ - bitmap.h \ channels/audio-input/audio-buffer.h \ channels/audio-input/audio-input.h \ channels/cliprdr.h \ @@ -103,6 +101,7 @@ noinst_HEADERS = \ channels/rdpdr/rdpdr-printer.h \ channels/rdpdr/rdpdr.h \ channels/rdpei.h \ + channels/rdpgfx.h \ channels/rdpsnd/rdpsnd-messages.h \ channels/rdpsnd/rdpsnd.h \ client.h \ @@ -112,7 +111,6 @@ noinst_HEADERS = \ error.h \ fs.h \ gdi.h \ - glyph.h \ input.h \ keyboard.h \ keymap.h \ @@ -156,7 +154,7 @@ freerdp_LTLIBRARIES = \ libguac-common-svc-client.la \ libguacai-client.la -freerdpdir = @FREERDP2_PLUGIN_DIR@ +freerdpdir = @FREERDP_PLUGIN_DIR@ # # Common SVC plugin (shared by RDPDR, RDPSND, etc.) @@ -227,7 +225,9 @@ BUILT_SOURCES = \ rdp_keymaps = \ $(srcdir)/keymaps/base.keymap \ + $(srcdir)/keymaps/base_altgr.keymap \ $(srcdir)/keymaps/failsafe.keymap \ + $(srcdir)/keymaps/cs-cz-qwertz.keymap \ $(srcdir)/keymaps/de_de_qwertz.keymap \ $(srcdir)/keymaps/de_ch_qwertz.keymap \ $(srcdir)/keymaps/en_gb_qwerty.keymap \ @@ -235,6 +235,7 @@ rdp_keymaps = \ $(srcdir)/keymaps/es_es_qwerty.keymap \ $(srcdir)/keymaps/es_latam_qwerty.keymap \ $(srcdir)/keymaps/fr_be_azerty.keymap \ + $(srcdir)/keymaps/fr_ca_qwerty.keymap \ $(srcdir)/keymaps/fr_ch_qwertz.keymap \ $(srcdir)/keymaps/fr_fr_azerty.keymap \ $(srcdir)/keymaps/hu_hu_qwertz.keymap \ @@ -243,6 +244,8 @@ rdp_keymaps = \ $(srcdir)/keymaps/no_no_qwerty.keymap \ $(srcdir)/keymaps/pl_pl_qwerty.keymap \ $(srcdir)/keymaps/pt_br_qwerty.keymap \ + $(srcdir)/keymaps/pt_pt_qwerty.keymap \ + $(srcdir)/keymaps/ro_ro_qwerty.keymap \ $(srcdir)/keymaps/sv_se_qwerty.keymap \ $(srcdir)/keymaps/da_dk_qwerty.keymap \ $(srcdir)/keymaps/tr_tr_qwerty.keymap @@ -257,4 +260,3 @@ EXTRA_DIST = \ $(rdp_keymaps) \ keymaps/generate.pl \ plugins/generate-entry-wrappers.pl - diff --git a/src/protocols/rdp/bitmap.c b/src/protocols/rdp/bitmap.c deleted file mode 100644 index 05f9e214b..000000000 --- a/src/protocols/rdp/bitmap.c +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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 "bitmap.h" -#include "common/display.h" -#include "common/surface.h" -#include "config.h" -#include "rdp.h" - -#include -#include -#include -#include -#include - -#include -#include - -void guac_rdp_cache_bitmap(rdpContext* context, rdpBitmap* bitmap) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - - /* Allocate buffer */ - guac_common_display_layer* buffer = guac_common_display_alloc_buffer( - rdp_client->display, bitmap->width, bitmap->height); - - /* Cache image data if present */ - if (bitmap->data != NULL) { - - /* Create surface from image data */ - cairo_surface_t* image = cairo_image_surface_create_for_data( - bitmap->data, CAIRO_FORMAT_RGB24, - bitmap->width, bitmap->height, 4*bitmap->width); - - /* Send surface to buffer */ - guac_common_surface_draw(buffer->surface, 0, 0, image); - - /* Free surface */ - cairo_surface_destroy(image); - - } - - /* Store buffer reference in bitmap */ - ((guac_rdp_bitmap*) bitmap)->layer = buffer; - -} - -BOOL guac_rdp_bitmap_new(rdpContext* context, rdpBitmap* bitmap) { - - /* No corresponding surface yet - caching is deferred. */ - ((guac_rdp_bitmap*) bitmap)->layer = NULL; - - /* Start at zero usage */ - ((guac_rdp_bitmap*) bitmap)->used = 0; - - return TRUE; - -} - -BOOL guac_rdp_bitmap_paint(rdpContext* context, rdpBitmap* bitmap) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - - guac_common_display_layer* buffer = ((guac_rdp_bitmap*) bitmap)->layer; - - int width = bitmap->right - bitmap->left + 1; - int height = bitmap->bottom - bitmap->top + 1; - - /* If not cached, cache if necessary */ - if (buffer == NULL && ((guac_rdp_bitmap*) bitmap)->used >= 1) - guac_rdp_cache_bitmap(context, bitmap); - - /* If cached, retrieve from cache */ - if (buffer != NULL) - guac_common_surface_copy(buffer->surface, 0, 0, width, height, - rdp_client->display->default_surface, - bitmap->left, bitmap->top); - - /* Otherwise, draw with stored image data */ - else if (bitmap->data != NULL) { - - /* Create surface from image data */ - cairo_surface_t* image = cairo_image_surface_create_for_data( - bitmap->data, CAIRO_FORMAT_RGB24, - width, height, 4*bitmap->width); - - /* Draw image on default surface */ - guac_common_surface_draw(rdp_client->display->default_surface, - bitmap->left, bitmap->top, image); - - /* Free surface */ - cairo_surface_destroy(image); - - } - - /* Increment usage counter */ - ((guac_rdp_bitmap*) bitmap)->used++; - - return TRUE; - -} - -void guac_rdp_bitmap_free(rdpContext* context, rdpBitmap* bitmap) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - guac_common_display_layer* buffer = ((guac_rdp_bitmap*) bitmap)->layer; - - /* If cached, free buffer */ - if (buffer != NULL) - guac_common_display_free_buffer(rdp_client->display, buffer); - -#ifndef FREERDP_BITMAP_FREE_FREES_BITMAP - /* NOTE: Except in FreeRDP 2.0.0-rc0 and earlier, FreeRDP-allocated memory - * for the rdpBitmap will NOT be automatically released after this free - * handler is invoked, thus we must do so manually here */ - - _aligned_free(bitmap->data); - free(bitmap); -#endif - -} - -BOOL guac_rdp_bitmap_setsurface(rdpContext* context, rdpBitmap* bitmap, BOOL primary) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - - if (primary) - rdp_client->current_surface = rdp_client->display->default_surface; - - else { - - /* Make sure that the received bitmap is not NULL before processing */ - if (bitmap == NULL) { - guac_client_log(client, GUAC_LOG_INFO, "NULL bitmap found in bitmap_setsurface instruction."); - return TRUE; - } - - /* If not available as a surface, make available. */ - if (((guac_rdp_bitmap*) bitmap)->layer == NULL) - guac_rdp_cache_bitmap(context, bitmap); - - rdp_client->current_surface = - ((guac_rdp_bitmap*) bitmap)->layer->surface; - - } - - return TRUE; - -} - diff --git a/src/protocols/rdp/bitmap.h b/src/protocols/rdp/bitmap.h deleted file mode 100644 index 297230c50..000000000 --- a/src/protocols/rdp/bitmap.h +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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_BITMAP_H -#define GUAC_RDP_BITMAP_H - -#include "config.h" -#include "common/display.h" - -#include -#include -#include -#include - -/** - * Guacamole-specific rdpBitmap data. - */ -typedef struct guac_rdp_bitmap { - - /** - * FreeRDP bitmap data - MUST GO FIRST. - */ - rdpBitmap bitmap; - - /** - * Layer containing cached image data. - */ - guac_common_display_layer* layer; - - /** - * The number of times a bitmap has been used. - */ - int used; - -} guac_rdp_bitmap; - -/** - * Caches the given bitmap immediately, storing its data in a remote Guacamole - * buffer. As RDP bitmaps are frequently created, used once, and immediately - * destroyed, we defer actual remote-side caching of RDP bitmaps until they are - * used at least once. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param bitmap - * The bitmap to cache. - */ -void guac_rdp_cache_bitmap(rdpContext* context, rdpBitmap* bitmap); - -/** - * Initializes the given newly-created rdpBitmap. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param bitmap - * The bitmap to initialize. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_bitmap_new(rdpContext* context, rdpBitmap* bitmap); - -/** - * Paints the given rdpBitmap on the primary display surface. Note that this - * operation does NOT draw to the "current" surface set by calls to - * guac_rdp_bitmap_setsurface(). - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param bitmap - * The bitmap to paint. This structure will also contain the specifics of - * the paint operation to perform, including the destination X/Y - * coordinates. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_bitmap_paint(rdpContext* context, rdpBitmap* bitmap); - -/** - * Frees any Guacamole-specific data associated with the given rdpBitmap. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param bitmap - * The bitmap whose Guacamole-specific data is to be freed. - */ -void guac_rdp_bitmap_free(rdpContext* context, rdpBitmap* bitmap); - -/** - * Sets the given rdpBitmap as the drawing surface for future operations or, - * if the primary flag is set, resets the current drawing surface to the - * primary drawing surface of the remote display. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param bitmap - * The rdpBitmap to set as the current drawing surface. This parameter is - * only valid if the primary flag is FALSE. - * - * @param primary - * TRUE if the bitmap parameter should be ignored, and the current drawing - * surface should be reset to the primary drawing surface of the remote - * display, FALSE otherwise. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_bitmap_setsurface(rdpContext* context, rdpBitmap* bitmap, - BOOL primary); - -#endif diff --git a/src/protocols/rdp/channels/cliprdr.c b/src/protocols/rdp/channels/cliprdr.c index 014dd57ca..34bacf3ba 100644 --- a/src/protocols/rdp/channels/cliprdr.c +++ b/src/protocols/rdp/channels/cliprdr.c @@ -82,7 +82,13 @@ static UINT guac_rdp_cliprdr_send_format_list(CliprdrClientContext* cliprdr) { /* We support CP-1252 and UTF-16 text */ CLIPRDR_FORMAT_LIST format_list = { +#ifdef HAVE_CLIPRDR_HEADER + .common = { + .msgType = CB_FORMAT_LIST + }, +#else .msgType = CB_FORMAT_LIST, +#endif .formats = (CLIPRDR_FORMAT[]) { { .formatId = CF_TEXT }, { .formatId = CF_UNICODETEXT } @@ -298,9 +304,15 @@ static UINT guac_rdp_cliprdr_format_list(CliprdrClientContext* cliprdr, guac_client_log(client, GUAC_LOG_TRACE, "CLIPRDR: Received format list."); CLIPRDR_FORMAT_LIST_RESPONSE format_list_response = { +#ifdef HAVE_CLIPRDR_HEADER + .common = { + .msgType = CB_FORMAT_LIST_RESPONSE, + .msgFlags = CB_RESPONSE_OK + } +#else .msgFlags = CB_RESPONSE_OK +#endif }; - /* Report successful processing of format list */ pthread_mutex_lock(&(rdp_client->message_lock)); cliprdr->ClientFormatListResponse(cliprdr, &format_list_response); @@ -394,8 +406,16 @@ static UINT guac_rdp_cliprdr_format_data_request(CliprdrClientContext* cliprdr, CLIPRDR_FORMAT_DATA_RESPONSE data_response = { .requestedFormatData = (BYTE*) start, +#ifdef HAVE_CLIPRDR_HEADER + .common = { + .msgType = CB_FORMAT_DATA_RESPONSE, + .msgFlags = CB_RESPONSE_OK, + .dataLen = ((BYTE*) output) - start, + } +#else .dataLen = ((BYTE*) output) - start, .msgFlags = CB_RESPONSE_OK +#endif }; guac_client_log(client, GUAC_LOG_TRACE, "CLIPRDR: Sending format data response."); @@ -482,9 +502,16 @@ static UINT guac_rdp_cliprdr_format_data_response(CliprdrClientContext* cliprdr, } + int data_len; + #ifdef HAVE_CLIPRDR_HEADER + data_len = format_data_response->common.dataLen; + #else + data_len = format_data_response->dataLen; + #endif + /* Convert, store, and forward the clipboard data received from RDP * server */ - if (guac_iconv(remote_reader, &input, format_data_response->dataLen, + if (guac_iconv(remote_reader, &input, data_len, GUAC_WRITE_UTF8, &output, sizeof(received_data))) { int length = strnlen(received_data, sizeof(received_data)); guac_common_clipboard_reset(clipboard->clipboard, "text/plain"); @@ -510,12 +537,12 @@ static UINT guac_rdp_cliprdr_format_data_response(CliprdrClientContext* cliprdr, * @param context * The rdpContext associated with the active RDP session. * - * @param e + * @param args * 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_cliprdr_channel_connected(rdpContext* context, - ChannelConnectedEventArgs* e) { + ChannelConnectedEventArgs* args) { guac_client* client = ((rdp_freerdp_context*) context)->client; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; @@ -527,12 +554,12 @@ static void guac_rdp_cliprdr_channel_connected(rdpContext* context, assert(clipboard != NULL); /* Ignore connection event if it's not for the CLIPRDR channel */ - if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) != 0) + if (strcmp(args->name, CLIPRDR_SVC_CHANNEL_NAME) != 0) return; /* The structure pointed to by pInterface is guaranteed to be a * CliprdrClientContext if the channel is CLIPRDR */ - CliprdrClientContext* cliprdr = (CliprdrClientContext*) e->pInterface; + CliprdrClientContext* cliprdr = (CliprdrClientContext*) args->pInterface; /* Associate FreeRDP CLIPRDR context and its Guacamole counterpart with * eachother */ @@ -563,12 +590,12 @@ static void guac_rdp_cliprdr_channel_connected(rdpContext* context, * @param context * The rdpContext associated with the active RDP session. * - * @param e + * @param args * 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_cliprdr_channel_disconnected(rdpContext* context, - ChannelDisconnectedEventArgs* e) { + ChannelDisconnectedEventArgs* args) { guac_client* client = ((rdp_freerdp_context*) context)->client; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; @@ -580,7 +607,7 @@ static void guac_rdp_cliprdr_channel_disconnected(rdpContext* context, assert(clipboard != NULL); /* Ignore disconnection event if it's not for the CLIPRDR channel */ - if (strcmp(e->name, CLIPRDR_SVC_CHANNEL_NAME) != 0) + if (strcmp(args->name, CLIPRDR_SVC_CHANNEL_NAME) != 0) return; /* Channel is no longer connected */ @@ -712,4 +739,3 @@ int guac_rdp_clipboard_end_handler(guac_user* user, guac_stream* stream) { return 0; } - diff --git a/src/protocols/rdp/channels/disp.c b/src/protocols/rdp/channels/disp.c index a8be66e00..da1ca800d 100644 --- a/src/protocols/rdp/channels/disp.c +++ b/src/protocols/rdp/channels/disp.c @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -69,19 +70,19 @@ void guac_rdp_disp_free(guac_rdp_disp* disp) { * @param context * The rdpContext associated with the active RDP session. * - * @param e + * @param args * 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_disp_channel_connected(rdpContext* context, - ChannelConnectedEventArgs* e) { + ChannelConnectedEventArgs* args) { guac_client* client = ((rdp_freerdp_context*) context)->client; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; guac_rdp_disp* guac_disp = rdp_client->disp; /* Ignore connection event if it's not for the Display Update channel */ - if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) != 0) + if (strcmp(args->name, DISP_DVC_CHANNEL_NAME) != 0) return; /* Init module with current display size */ @@ -90,7 +91,7 @@ static void guac_rdp_disp_channel_connected(rdpContext* context, guac_rdp_get_height(context->instance)); /* Store reference to the display update plugin once it's connected */ - DispClientContext* disp = (DispClientContext*) e->pInterface; + DispClientContext* disp = (DispClientContext*) args->pInterface; guac_disp->disp = disp; guac_client_log(client, GUAC_LOG_DEBUG, "Display update channel " @@ -111,19 +112,19 @@ static void guac_rdp_disp_channel_connected(rdpContext* context, * @param context * The rdpContext associated with the active RDP session. * - * @param e + * @param args * 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_disp_channel_disconnected(rdpContext* context, - ChannelDisconnectedEventArgs* e) { + ChannelDisconnectedEventArgs* args) { guac_client* client = ((rdp_freerdp_context*) context)->client; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; guac_rdp_disp* guac_disp = rdp_client->disp; /* Ignore disconnection event if it's not for the Display Update channel */ - if (strcmp(e->name, DISP_DVC_CHANNEL_NAME) != 0) + if (strcmp(args->name, DISP_DVC_CHANNEL_NAME) != 0) return; /* Channel is no longer connected */ @@ -149,56 +150,28 @@ void guac_rdp_disp_load_plugin(rdpContext* context) { } -/** - * Fits a given dimension within the allowed bounds for Display Update - * messages, adjusting the other dimension such that aspect ratio is - * maintained. - * - * @param a The dimension to fit within allowed bounds. - * - * @param b - * The other dimension to adjust if and only if necessary to preserve - * aspect ratio. - */ -static void guac_rdp_disp_fit(int* a, int* b) { - - int a_value = *a; - int b_value = *b; - - /* Ensure first dimension is within allowed range */ - if (a_value < GUAC_RDP_DISP_MIN_SIZE) { - - /* Adjust other dimension to maintain aspect ratio */ - int adjusted_b = b_value * GUAC_RDP_DISP_MIN_SIZE / a_value; - if (adjusted_b > GUAC_RDP_DISP_MAX_SIZE) - adjusted_b = GUAC_RDP_DISP_MAX_SIZE; - - *a = GUAC_RDP_DISP_MIN_SIZE; - *b = adjusted_b; - - } - else if (a_value > GUAC_RDP_DISP_MAX_SIZE) { - - /* Adjust other dimension to maintain aspect ratio */ - int adjusted_b = b_value * GUAC_RDP_DISP_MAX_SIZE / a_value; - if (adjusted_b < GUAC_RDP_DISP_MIN_SIZE) - adjusted_b = GUAC_RDP_DISP_MIN_SIZE; - - *a = GUAC_RDP_DISP_MAX_SIZE; - *b = adjusted_b; - - } - -} - void guac_rdp_disp_set_size(guac_rdp_disp* disp, guac_rdp_settings* settings, freerdp* rdp_inst, int width, int height) { - /* Fit width within bounds, adjusting height to maintain aspect ratio */ - guac_rdp_disp_fit(&width, &height); - - /* Fit height within bounds, adjusting width to maintain aspect ratio */ - guac_rdp_disp_fit(&height, &width); + guac_rect resize = { + .left = 0, + .top = 0, + .right = width, + .bottom = height + }; + + /* Fit width and height within bounds, maintaining aspect ratio */ + guac_rect_shrink(&resize, GUAC_RDP_DISP_MAX_SIZE, GUAC_RDP_DISP_MAX_SIZE); + + width = guac_rect_width(&resize); + height = guac_rect_height(&resize); + + /* As it's possible for a rectangle to exceed the maximum allowed + * dimensions, yet fall below the minimum allowed dimensions once adjusted, + * we don't bother preserving aspect ratio for the unlikely case that a + * dimension is below the minimums (consider a rectangle like 16384x256) */ + if (width < GUAC_RDP_DISP_MIN_SIZE) width = GUAC_RDP_DISP_MIN_SIZE; + if (height < GUAC_RDP_DISP_MIN_SIZE) height = GUAC_RDP_DISP_MIN_SIZE; /* Width must be even */ if (width % 2 == 1) diff --git a/src/protocols/rdp/channels/rail.c b/src/protocols/rdp/channels/rail.c index 6f8d7f7f0..276f6ee2e 100644 --- a/src/protocols/rdp/channels/rail.c +++ b/src/protocols/rdp/channels/rail.c @@ -26,27 +26,15 @@ #include #include #include +#include #include +#include #include #include #include #include -#ifdef FREERDP_RAIL_CALLBACKS_REQUIRE_CONST -/** - * FreeRDP 2.0.0-rc4 and newer requires the final argument for all RAIL - * callbacks to be const. - */ -#define RAIL_CONST const -#else -/** - * FreeRDP 2.0.0-rc3 and older requires the final argument for all RAIL - * callbacks to NOT be const. - */ -#define RAIL_CONST -#endif - /** * Completes initialization of the RemoteApp session, responding to the server * handshake, sending client status and system parameters, and executing the @@ -86,6 +74,7 @@ static UINT guac_rdp_rail_complete_handshake(RailClientContext* rail) { }; /* Send client handshake response */ + guac_client_log(client, GUAC_LOG_TRACE, "Sending RAIL handshake."); pthread_mutex_lock(&(rdp_client->message_lock)); status = rail->ClientHandshake(rail, &handshake); pthread_mutex_unlock(&(rdp_client->message_lock)); @@ -94,10 +83,13 @@ static UINT guac_rdp_rail_complete_handshake(RailClientContext* rail) { return status; RAIL_CLIENT_STATUS_ORDER client_status = { - .flags = 0x00 + .flags = + TS_RAIL_CLIENTSTATUS_ALLOWLOCALMOVESIZE + | TS_RAIL_CLIENTSTATUS_APPBAR_REMOTING_SUPPORTED }; /* Send client status */ + guac_client_log(client, GUAC_LOG_TRACE, "Sending RAIL client status."); pthread_mutex_lock(&(rdp_client->message_lock)); status = rail->ClientInformation(rail, &client_status); pthread_mutex_unlock(&(rdp_client->message_lock)); @@ -135,8 +127,7 @@ static UINT guac_rdp_rail_complete_handshake(RailClientContext* rail) { }, .params = - SPI_MASK_SET_DRAG_FULL_WINDOWS - | SPI_MASK_SET_HIGH_CONTRAST + SPI_MASK_SET_HIGH_CONTRAST | SPI_MASK_SET_KEYBOARD_CUES | SPI_MASK_SET_KEYBOARD_PREF | SPI_MASK_SET_MOUSE_BUTTON_SWAP @@ -145,6 +136,7 @@ static UINT guac_rdp_rail_complete_handshake(RailClientContext* rail) { }; /* Send client system parameters */ + guac_client_log(client, GUAC_LOG_TRACE, "Sending RAIL client system parameters."); pthread_mutex_lock(&(rdp_client->message_lock)); status = rail->ClientSystemParam(rail, &sysparam); pthread_mutex_unlock(&(rdp_client->message_lock)); @@ -160,6 +152,7 @@ static UINT guac_rdp_rail_complete_handshake(RailClientContext* rail) { }; /* Execute desired RemoteApp command */ + guac_client_log(client, GUAC_LOG_TRACE, "Executing remote application."); pthread_mutex_lock(&(rdp_client->message_lock)); status = rail->ClientExecute(rail, &exec); pthread_mutex_unlock(&(rdp_client->message_lock)); @@ -168,6 +161,37 @@ static UINT guac_rdp_rail_complete_handshake(RailClientContext* rail) { } +/** + * A callback function that is invoked when the RDP server sends the result + * of the Remote App (RAIL) execution command back to the client, so that the + * client can handle any required actions associated with the result. + * + * @param context + * A pointer to the RAIL data structure associated with the current + * RDP connection. + * + * @param execResult + * A data structure containing the result of the RAIL command. + * + * @return + * CHANNEL_RC_OK (zero) if the result was handled successfully, otherwise + * a non-zero error code. This implementation always returns + * CHANNEL_RC_OK. + */ +static UINT guac_rdp_rail_execute_result(RailClientContext* context, + const RAIL_EXEC_RESULT_ORDER* execResult) { + + guac_client* client = (guac_client*) context->custom; + + if (execResult->execResult != RAIL_EXEC_S_OK) { + guac_client_log(client, GUAC_LOG_DEBUG, "Failed to execute RAIL command on server: %d", execResult->execResult); + guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE, "Failed to execute RAIL command."); + } + + return CHANNEL_RC_OK; + +} + /** * Callback which is invoked when a Handshake PDU is received from the RDP * server. No communication for RemoteApp may occur until the Handshake PDU @@ -189,6 +213,8 @@ static UINT guac_rdp_rail_complete_handshake(RailClientContext* rail) { */ static UINT guac_rdp_rail_handshake(RailClientContext* rail, RAIL_CONST RAIL_HANDSHAKE_ORDER* handshake) { + guac_client* client = (guac_client*) rail->custom; + guac_client_log(client, GUAC_LOG_TRACE, "RAIL handshake callback."); return guac_rdp_rail_complete_handshake(rail); } @@ -213,9 +239,63 @@ static UINT guac_rdp_rail_handshake(RailClientContext* rail, */ static UINT guac_rdp_rail_handshake_ex(RailClientContext* rail, RAIL_CONST RAIL_HANDSHAKE_EX_ORDER* handshake_ex) { + guac_client* client = (guac_client*) rail->custom; + guac_client_log(client, GUAC_LOG_TRACE, "RAIL handshake ex callback."); return guac_rdp_rail_complete_handshake(rail); } +/** + * A callback function that is executed when an update for a RAIL window is + * received from the RDP server. + * + * @param context + * A pointer to the rdpContext structure used by FreeRDP to handle the + * window update. + * + * @param orderInfo + * A pointer to the data structure that contains information about what + * window was updated what updates were performed. + * + * @param windowState + * A pointer to the data structure that contains details of the updates + * to the window, as indicated by flags in the orderInfo field. + * + * @return + * TRUE if the client-side processing of the updates as successful; otherwise + * FALSE. This implementation always returns TRUE. + */ +static BOOL guac_rdp_rail_window_update(rdpContext* context, + RAIL_CONST WINDOW_ORDER_INFO* orderInfo, + RAIL_CONST WINDOW_STATE_ORDER* windowState) { + + guac_client* client = ((rdp_freerdp_context*) context)->client; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + + guac_client_log(client, GUAC_LOG_TRACE, "RAIL window update callback: %d", orderInfo->fieldFlags); + + UINT32 fieldFlags = orderInfo->fieldFlags; + + /* If the flag for window visibilty is set, check visibility. */ + if (fieldFlags & WINDOW_ORDER_FIELD_SHOW) { + guac_client_log(client, GUAC_LOG_TRACE, "RAIL window visibility change: %d", windowState->showState); + + /* State is either hidden or minimized - send restore command. */ + if (windowState->showState == GUAC_RDP_RAIL_WINDOW_STATE_HIDDEN + || windowState->showState == GUAC_RDP_RAIL_WINDOW_STATE_MINIMIZED) { + + guac_client_log(client, GUAC_LOG_DEBUG, "RAIL window minimized, sending restore command."); + + RAIL_SYSCOMMAND_ORDER syscommand; + syscommand.windowId = orderInfo->windowId; + syscommand.command = SC_RESTORE; + rdp_client->rail_interface->ClientSystemCommand(rdp_client->rail_interface, &syscommand); + } + } + + return TRUE; + +} + /** * Callback which associates handlers specific to Guacamole with the * RailClientContext instance allocated by FreeRDP to deal with received @@ -230,28 +310,32 @@ static UINT guac_rdp_rail_handshake_ex(RailClientContext* rail, * @param context * The rdpContext associated with the active RDP session. * - * @param e + * @param args * 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_rail_channel_connected(rdpContext* context, - ChannelConnectedEventArgs* e) { + ChannelConnectedEventArgs* args) { guac_client* client = ((rdp_freerdp_context*) context)->client; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; /* Ignore connection event if it's not for the RAIL channel */ - if (strcmp(e->name, RAIL_SVC_CHANNEL_NAME) != 0) + if (strcmp(args->name, RAIL_SVC_CHANNEL_NAME) != 0) return; /* The structure pointed to by pInterface is guaranteed to be a * RailClientContext if the channel is RAIL */ - RailClientContext* rail = (RailClientContext*) e->pInterface; + RailClientContext* rail = (RailClientContext*) args->pInterface; + rdp_client->rail_interface = rail; /* Init FreeRDP RAIL context, ensuring the guac_client can be accessed from * within any RAIL-specific callbacks */ rail->custom = client; + rail->ServerExecuteResult = guac_rdp_rail_execute_result; rail->ServerHandshake = guac_rdp_rail_handshake; rail->ServerHandshakeEx = guac_rdp_rail_handshake_ex; + context->update->window->WindowUpdate = guac_rdp_rail_window_update; guac_client_log(client, GUAC_LOG_DEBUG, "RAIL (RemoteApp) channel " "connected."); @@ -280,4 +364,3 @@ void guac_rdp_rail_load_plugin(rdpContext* context) { "registered. Awaiting channel connection."); } - diff --git a/src/protocols/rdp/channels/rail.h b/src/protocols/rdp/channels/rail.h index 085603436..084287138 100644 --- a/src/protocols/rdp/channels/rail.h +++ b/src/protocols/rdp/channels/rail.h @@ -20,7 +20,34 @@ #ifndef GUAC_RDP_CHANNELS_RAIL_H #define GUAC_RDP_CHANNELS_RAIL_H +#include "config.h" + #include +#include + +#ifdef FREERDP_RAIL_CALLBACKS_REQUIRE_CONST +/** + * FreeRDP 2.0.0-rc4 and newer requires the final arguments for RAIL + * callbacks to be const. + */ +#define RAIL_CONST const +#else +/** + * FreeRDP 2.0.0-rc3 and older requires the final arguments for RAIL + * callbacks to NOT be const. + */ +#define RAIL_CONST +#endif + +/** + * The RAIL window state that indicates a hidden window. + */ +#define GUAC_RDP_RAIL_WINDOW_STATE_HIDDEN 0x00 + +/** + * The RAIL window state that indicates a visible but minimized window. + */ +#define GUAC_RDP_RAIL_WINDOW_STATE_MINIMIZED 0x02 /** * Initializes RemoteApp support for RDP and handling of the RAIL channel. If diff --git a/src/protocols/rdp/channels/rdpei.c b/src/protocols/rdp/channels/rdpei.c index 74b705de5..a94faa9f4 100644 --- a/src/protocols/rdp/channels/rdpei.c +++ b/src/protocols/rdp/channels/rdpei.c @@ -18,7 +18,6 @@ */ #include "channels/rdpei.h" -#include "common/surface.h" #include "plugins/channels.h" #include "rdp.h" #include "settings.h" @@ -27,6 +26,7 @@ #include #include #include +#include #include #include @@ -67,27 +67,27 @@ void guac_rdp_rdpei_free(guac_rdp_rdpei* rdpei) { * @param context * The rdpContext associated with the active RDP session. * - * @param e + * @param args * 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) { + ChannelConnectedEventArgs* args) { 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) + if (strcmp(args->name, RDPEI_DVC_CHANNEL_NAME) != 0) return; /* Store reference to the RDPEI plugin once it's connected */ - RdpeiClientContext* rdpei = (RdpeiClientContext*) e->pInterface; + RdpeiClientContext* rdpei = (RdpeiClientContext*) args->pInterface; guac_rdpei->rdpei = rdpei; /* Declare level of multi-touch support */ - guac_common_surface_set_multitouch(rdp_client->display->default_surface, + guac_display_layer_set_multitouch(guac_display_default_layer(rdp_client->display), GUAC_RDP_RDPEI_MAX_TOUCHES); guac_client_log(client, GUAC_LOG_DEBUG, "RDPEI channel will be used for " @@ -108,19 +108,19 @@ static void guac_rdp_rdpei_channel_connected(rdpContext* context, * @param context * The rdpContext associated with the active RDP session. * - * @param e + * @param args * 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_disconnected(rdpContext* context, - ChannelDisconnectedEventArgs* e) { + ChannelDisconnectedEventArgs* args) { 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 disconnection event if it's not for the RDPEI channel */ - if (strcmp(e->name, RDPEI_DVC_CHANNEL_NAME) != 0) + if (strcmp(args->name, RDPEI_DVC_CHANNEL_NAME) != 0) return; /* Channel is no longer connected */ diff --git a/src/protocols/rdp/channels/rdpgfx.c b/src/protocols/rdp/channels/rdpgfx.c new file mode 100644 index 000000000..0fae972f6 --- /dev/null +++ b/src/protocols/rdp/channels/rdpgfx.c @@ -0,0 +1,122 @@ +/* + * 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/rdpgfx.h" +#include "plugins/channels.h" +#include "rdp.h" +#include "settings.h" + +#include +#include +#include +#include +#include + +#include +#include + +/** + * Callback which associates handlers specific to Guacamole with the + * RdpgfxClientContext instance allocated by FreeRDP to deal with received + * RDPGFX (Graphics Pipeline) 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 RDPGFX channel. This specific callback is registered with the + * PubSub system of the relevant rdpContext when guac_rdp_rdpgfx_load_plugin() is + * called. + * + * @param context + * The rdpContext associated with the active RDP session. + * + * @param args + * 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_rdpgfx_channel_connected(rdpContext* context, + ChannelConnectedEventArgs* args) { + + guac_client* client = ((rdp_freerdp_context*) context)->client; + + /* Ignore connection event if it's not for the RDPGFX channel */ + if (strcmp(args->name, RDPGFX_DVC_CHANNEL_NAME) != 0) + return; + + /* Init GDI-backed support for the Graphics Pipeline */ + RdpgfxClientContext* rdpgfx = (RdpgfxClientContext*) args->pInterface; + rdpGdi* gdi = context->gdi; + + if (!gdi_graphics_pipeline_init(gdi, rdpgfx)) + guac_client_log(client, GUAC_LOG_WARNING, "Rendering backend for RDPGFX " + "channel could not be loaded. Graphics may not render at all!"); + else + guac_client_log(client, GUAC_LOG_DEBUG, "RDPGFX channel will be used for " + "the RDP Graphics Pipeline Extension."); + +} + +/** + * Callback which handles any RDPGFX cleanup specific to Guacamole. + * + * This function is called whenever a channel disconnects via the PubSub event + * system within FreeRDP, but only has any effect if the disconnected channel + * is the RDPGFX channel. This specific callback is registered with the PubSub + * system of the relevant rdpContext when guac_rdp_rdpgfx_load_plugin() is + * called. + * + * @param context + * The rdpContext associated with the active RDP session. + * + * @param args + * 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_rdpgfx_channel_disconnected(rdpContext* context, + ChannelDisconnectedEventArgs* args) { + + guac_client* client = ((rdp_freerdp_context*) context)->client; + + /* Ignore disconnection event if it's not for the RDPGFX channel */ + if (strcmp(args->name, RDPGFX_DVC_CHANNEL_NAME) != 0) + return; + + /* Un-init GDI-backed support for the Graphics Pipeline */ + RdpgfxClientContext* rdpgfx = (RdpgfxClientContext*) args->pInterface; + rdpGdi* gdi = context->gdi; + gdi_graphics_pipeline_uninit(gdi, rdpgfx); + + guac_client_log(client, GUAC_LOG_DEBUG, "RDPGFX channel support unloaded."); + +} + +void guac_rdp_rdpgfx_load_plugin(rdpContext* context) { + + /* Subscribe to and handle channel connected events */ + PubSub_SubscribeChannelConnected(context->pubSub, + (pChannelConnectedEventHandler) guac_rdp_rdpgfx_channel_connected); + + /* Subscribe to and handle channel disconnected events */ + PubSub_SubscribeChannelDisconnected(context->pubSub, + (pChannelDisconnectedEventHandler) guac_rdp_rdpgfx_channel_disconnected); + + /* Add "rdpgfx" channel */ + guac_freerdp_dynamic_channel_collection_add(context->settings, "rdpgfx", NULL); + +} + diff --git a/src/protocols/rdp/channels/rdpgfx.h b/src/protocols/rdp/channels/rdpgfx.h new file mode 100644 index 000000000..993445ca1 --- /dev/null +++ b/src/protocols/rdp/channels/rdpgfx.h @@ -0,0 +1,49 @@ +/* + * 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_RDPGFX_H +#define GUAC_RDP_CHANNELS_RDPGFX_H + +#include "settings.h" + +#include +#include +#include + +/** + * Adds FreeRDP's "rdpgfx" 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 associated with the guac_rdp_rdpgfx instance pointed to by the + * current guac_rdp_client. The plugin will only be loaded once the "drdynvc" + * plugin is loaded. The "rdpgfx" plugin ultimately adds support for the RDP + * Graphics Pipeline Extension. + * + * If failures occur, messages noting the specifics of those failures will be + * logged. + * + * This MUST be called within the PreConnect callback of the freerdp instance + * for Graphics Pipeline support to be loaded. + * + * @param context + * The rdpContext associated with the active RDP session. + */ +void guac_rdp_rdpgfx_load_plugin(rdpContext* context); + +#endif + diff --git a/src/protocols/rdp/channels/rdpsnd/rdpsnd-messages.c b/src/protocols/rdp/channels/rdpsnd/rdpsnd-messages.c index 1aeb8df99..7c57bc5c5 100644 --- a/src/protocols/rdp/channels/rdpsnd/rdpsnd-messages.c +++ b/src/protocols/rdp/channels/rdpsnd/rdpsnd-messages.c @@ -204,7 +204,7 @@ void guac_rdpsnd_formats_handler(guac_rdp_common_svc* svc, Stream_Write_UINT16(output_stream, rdpsnd->format_count); /* Reposition cursor at end (necessary for message send) */ - Stream_SetPointer(output_stream, output_stream_end); + Stream_SetPosition(output_stream, output_stream_end - Stream_Buffer(output_stream)); /* Send accepted formats */ guac_rdp_common_svc_write(svc, output_stream); @@ -366,4 +366,3 @@ void guac_rdpsnd_close_handler(guac_rdp_common_svc* svc, /* Do nothing */ } - diff --git a/src/protocols/rdp/client.c b/src/protocols/rdp/client.c index 6412e5224..263ab8d20 100644 --- a/src/protocols/rdp/client.c +++ b/src/protocols/rdp/client.c @@ -22,6 +22,7 @@ #include "channels/cliprdr.h" #include "channels/disp.h" #include "channels/pipe-svc.h" +#include "channels/rail.h" #include "config.h" #include "fs.h" #include "log.h" @@ -131,7 +132,7 @@ static int guac_rdp_join_pending_handler(guac_client* client) { /* Synchronize with current display */ if (rdp_client->display != NULL) { - guac_common_display_dup(rdp_client->display, client, broadcast_socket); + guac_display_dup(rdp_client->display, broadcast_socket); guac_socket_flush(broadcast_socket); } diff --git a/src/protocols/rdp/client.h b/src/protocols/rdp/client.h index 943d39abb..7955db364 100644 --- a/src/protocols/rdp/client.h +++ b/src/protocols/rdp/client.h @@ -23,25 +23,14 @@ #include /** - * The maximum duration of a frame in milliseconds. + * The amount of time to wait for new messages from the RDP server before + * moving on to internal matters, in milliseconds. This value must be kept + * reasonably small such that a slow RDP server will not prevent external + * events from being handled (such as the stop signal from guac_client_stop()), + * but large enough that the message handling loop does not eat up CPU + * spinning. */ -#define GUAC_RDP_FRAME_DURATION 60 - -/** - * The amount of time to allow per message read within a frame, in - * milliseconds. If the server is silent for at least this amount of time, the - * frame will be considered finished. - */ -#define GUAC_RDP_FRAME_TIMEOUT 0 - -/** - * The amount of time to wait for a new message from the RDP server when - * beginning a new frame, in milliseconds. This value must be kept reasonably - * small such that a slow RDP server will not prevent external events from - * being handled (such as the stop signal from guac_client_stop()), but large - * enough that the message handling loop does not eat up CPU spinning. - */ -#define GUAC_RDP_FRAME_START_TIMEOUT 250 +#define GUAC_RDP_MESSAGE_CHECK_INTERVAL 1000 /** * The native resolution of most RDP connections. As Windows and other systems diff --git a/src/protocols/rdp/color.c b/src/protocols/rdp/color.c index 964310aff..7733b2c4c 100644 --- a/src/protocols/rdp/color.c +++ b/src/protocols/rdp/color.c @@ -56,7 +56,12 @@ UINT32 guac_rdp_convert_color(rdpContext* context, UINT32 color) { /* Convert provided color into the intermediate representation expected by * FreeRDPConvertColor() */ - UINT32 intermed = ReadColor((BYTE*) &color, src_format); + UINT32 intermed; +#ifdef USE_UPDATED_RW_COLOR_FUNCS + intermed = FreeRDPReadColor((BYTE*) &color, src_format); +#else + intermed = ReadColor((BYTE*) &color, src_format); +#endif /* Convert color from RDP source format to the native format used by Cairo, * still maintaining intermediate representation */ @@ -69,8 +74,12 @@ UINT32 guac_rdp_convert_color(rdpContext* context, UINT32 color) { /* Convert color from intermediate representation to the actual desired * format */ - WriteColor((BYTE*) &color, dst_format, intermed); +#ifdef USE_UPDATED_RW_COLOR_FUNCS + intermed = FreeRDPWriteColor((BYTE*) &color, dst_format, intermed); +#else + intermed = WriteColor((BYTE*) &color, dst_format, intermed); +#endif + return color; } - diff --git a/src/protocols/rdp/fs.c b/src/protocols/rdp/fs.c index be228728c..80bdacdcd 100644 --- a/src/protocols/rdp/fs.c +++ b/src/protocols/rdp/fs.c @@ -361,7 +361,7 @@ int guac_rdp_fs_open(guac_rdp_fs* fs, const char* path, } /* Get file ID, init file */ - file_id = guac_pool_next_int(fs->file_id_pool); + file_id = guac_pool_next_int_below_or_die(fs->file_id_pool, GUAC_RDP_FS_MAX_FILES); file = &(fs->files[file_id]); file->id = file_id; file->fd = fd; diff --git a/src/protocols/rdp/gdi.c b/src/protocols/rdp/gdi.c index 9c61772be..89b785b7c 100644 --- a/src/protocols/rdp/gdi.c +++ b/src/protocols/rdp/gdi.c @@ -17,381 +17,167 @@ * under the License. */ -#include "bitmap.h" #include "color.h" -#include "common/display.h" -#include "common/surface.h" #include "rdp.h" #include "settings.h" #include #include +#include #include #include +#include #include +#include #include #include #include -#include - -guac_transfer_function guac_rdp_rop3_transfer_function(guac_client* client, - int rop3) { - - /* Translate supported ROP3 opcodes into composite modes */ - switch (rop3) { - - /* "DSon" !(src | dest) */ - case 0x11: return GUAC_TRANSFER_BINARY_NOR; - - /* "DSna" !src & dest */ - case 0x22: return GUAC_TRANSFER_BINARY_NSRC_AND; - - /* "Sn" !src */ - case 0x33: return GUAC_TRANSFER_BINARY_NSRC; - - /* "SDna" (src & !dest) */ - case 0x44: return GUAC_TRANSFER_BINARY_NDEST_AND; - - /* "Dn" !dest */ - case 0x55: return GUAC_TRANSFER_BINARY_NDEST; - - /* "SRCINVERT" (src ^ dest) */ - case 0x66: return GUAC_TRANSFER_BINARY_XOR; - - /* "DSan" !(src & dest) */ - case 0x77: return GUAC_TRANSFER_BINARY_NAND; - - /* "SRCAND" (src & dest) */ - case 0x88: return GUAC_TRANSFER_BINARY_AND; - - /* "DSxn" !(src ^ dest) */ - case 0x99: return GUAC_TRANSFER_BINARY_XNOR; - - /* "MERGEPAINT" (!src | dest)*/ - case 0xBB: return GUAC_TRANSFER_BINARY_NSRC_OR; - - /* "SDno" (src | !dest) */ - case 0xDD: return GUAC_TRANSFER_BINARY_NDEST_OR; - - /* "SRCPAINT" (src | dest) */ - case 0xEE: return GUAC_TRANSFER_BINARY_OR; - - /* 0x00 = "BLACKNESS" (0) */ - /* 0xAA = "NOP" (dest) */ - /* 0xCC = "SRCCOPY" (src) */ - /* 0xFF = "WHITENESS" (1) */ - - } - - /* Log warning if ROP3 opcode not supported */ - guac_client_log(client, GUAC_LOG_INFO, "guac_rdp_rop3_transfer_function: " - "UNSUPPORTED opcode = 0x%02X", rop3); - - /* Default to BINARY_SRC */ - return GUAC_TRANSFER_BINARY_SRC; -} - -BOOL guac_rdp_gdi_dstblt(rdpContext* context, const DSTBLT_ORDER* dstblt) { +void guac_rdp_gdi_mark_frame(rdpContext* context, int starting) { guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_common_surface* current_surface = ((guac_rdp_client*) client->data)->current_surface; - - int x = dstblt->nLeftRect; - int y = dstblt->nTopRect; - int w = dstblt->nWidth; - int h = dstblt->nHeight; - - switch (dstblt->bRop) { - - /* Blackness */ - case 0: - - /* Send black rectangle */ - guac_common_surface_set(current_surface, x, y, w, h, - 0x00, 0x00, 0x00, 0xFF); - break; - - /* DSTINVERT */ - case 0x55: - guac_common_surface_transfer(current_surface, x, y, w, h, - GUAC_TRANSFER_BINARY_NDEST, current_surface, x, y); - break; - - /* NOP */ - case 0xAA: - break; - - /* Whiteness */ - case 0xFF: - guac_common_surface_set(current_surface, x, y, w, h, - 0xFF, 0xFF, 0xFF, 0xFF); - break; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - /* Unsupported ROP3 */ - default: - guac_client_log(client, GUAC_LOG_INFO, - "guac_rdp_gdi_dstblt(rop3=0x%x)", dstblt->bRop); + /* A new frame has been received from the RDP server and processed */ + if (!starting) + guac_display_render_thread_notify_frame(rdp_client->render_thread); - } +} +BOOL guac_rdp_gdi_frame_marker(rdpContext* context, const FRAME_MARKER_ORDER* frame_marker) { + guac_rdp_gdi_mark_frame(context, frame_marker->action == FRAME_START); return TRUE; - } -BOOL guac_rdp_gdi_patblt(rdpContext* context, PATBLT_ORDER* patblt) { +BOOL guac_rdp_gdi_surface_frame_marker(rdpContext* context, const SURFACE_FRAME_MARKER* surface_frame_marker) { - /* - * Note that this is not a full implementation of PATBLT. This is a - * fallback implementation which only renders a solid block of background - * color using the specified ROP3 operation, ignoring whatever brush - * was actually specified. - * - * As libguac-client-rdp explicitly tells the server not to send PATBLT, - * well-behaved RDP servers will not use this operation at all, while - * others will at least have a fallback. - */ + guac_rdp_gdi_mark_frame(context, surface_frame_marker->frameAction != SURFACECMD_FRAMEACTION_END); - /* Get client and current layer */ - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_common_surface* current_surface = - ((guac_rdp_client*) client->data)->current_surface; - - int x = patblt->nLeftRect; - int y = patblt->nTopRect; - int w = patblt->nWidth; - int h = patblt->nHeight; - - /* - * Warn that rendering is a fallback, as the server should not be sending - * this order. - */ - guac_client_log(client, GUAC_LOG_INFO, "Using fallback PATBLT (server is ignoring " - "negotiated client capabilities)"); - - /* Render rectangle based on ROP */ - switch (patblt->bRop) { - - /* If blackness, send black rectangle */ - case 0x00: - guac_common_surface_set(current_surface, x, y, w, h, - 0x00, 0x00, 0x00, 0xFF); - break; - - /* If NOP, do nothing */ - case 0xAA: - break; - - /* If operation is just a copy, send foreground only */ - case 0xCC: - case 0xF0: - guac_common_surface_set(current_surface, x, y, w, h, - (patblt->foreColor >> 16) & 0xFF, - (patblt->foreColor >> 8 ) & 0xFF, - (patblt->foreColor ) & 0xFF, - 0xFF); - break; - - /* If whiteness, send white rectangle */ - case 0xFF: - guac_common_surface_set(current_surface, x, y, w, h, - 0xFF, 0xFF, 0xFF, 0xFF); - break; - - /* Otherwise, invert entire rect */ - default: - guac_common_surface_transfer(current_surface, x, y, w, h, - GUAC_TRANSFER_BINARY_NDEST, current_surface, x, y); - - } + int frame_acknowledge; +#ifdef HAVE_SETTERS_GETTERS + frame_acknowledge = freerdp_settings_get_uint32(context->settings, FreeRDP_FrameAcknowledge); +#else + frame_acknowledge = context->settings->FrameAcknowledge; +#endif + + if (frame_acknowledge > 0) + IFCALL(context->update->SurfaceFrameAcknowledge, context, + surface_frame_marker->frameId); return TRUE; } -BOOL guac_rdp_gdi_scrblt(rdpContext* context, const SCRBLT_ORDER* scrblt) { +BOOL guac_rdp_gdi_begin_paint(rdpContext* context) { guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_common_surface* current_surface = ((guac_rdp_client*) client->data)->current_surface; - - int x = scrblt->nLeftRect; - int y = scrblt->nTopRect; - int w = scrblt->nWidth; - int h = scrblt->nHeight; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + rdpGdi* gdi = context->gdi; - int x_src = scrblt->nXSrc; - int y_src = scrblt->nYSrc; + GUAC_ASSERT(rdp_client->current_context == NULL); - guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + /* All potential drawing operations must occur while holding an open context */ + guac_display_layer* default_layer = guac_display_default_layer(rdp_client->display); + guac_display_layer_raw_context* current_context = guac_display_layer_open_raw(default_layer); + rdp_client->current_context = current_context; - /* Copy screen rect to current surface */ - guac_common_surface_copy(rdp_client->display->default_surface, - x_src, y_src, w, h, current_surface, x, y); + /* Resynchronize default layer buffer details with FreeRDP's GDI */ + current_context->buffer = gdi->primary_buffer; + current_context->stride = gdi->stride; + guac_rect_init(¤t_context->bounds, 0, 0, gdi->width, gdi->height); return TRUE; } -BOOL guac_rdp_gdi_memblt(rdpContext* context, MEMBLT_ORDER* memblt) { +BOOL guac_rdp_gdi_end_paint(rdpContext* context) { guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_common_surface* current_surface = ((guac_rdp_client*) client->data)->current_surface; - guac_rdp_bitmap* bitmap = (guac_rdp_bitmap*) memblt->bitmap; - - int x = memblt->nLeftRect; - int y = memblt->nTopRect; - int w = memblt->nWidth; - int h = memblt->nHeight; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + rdpGdi* gdi = context->gdi; - int x_src = memblt->nXSrc; - int y_src = memblt->nYSrc; + guac_display_layer* default_layer = guac_display_default_layer(rdp_client->display); + guac_display_layer_raw_context* current_context = rdp_client->current_context; - /* Make sure that the received bitmap is not NULL before processing */ - if (bitmap == NULL) { - guac_client_log(client, GUAC_LOG_INFO, "NULL bitmap found in memblt instruction."); + /* Handle the case where EndPaint was called without a preceding BeginPaint. + * This can occur during screen resize events in "display-update" mode with + * FreeRDP version 3.8.0 or later, where EndPaint is called to ensure the + * update-lock is released and data is flushed before resizing. See the + * associated FreeRDP PR: https://github.com/FreeRDP/FreeRDP/pull/10488 */ + if (current_context == NULL) return TRUE; - } - switch (memblt->bRop) { + /* Ignore paint if GDI output is suppressed */ + if (gdi->suppressOutput) + goto paint_complete; - /* If blackness, send black rectangle */ - case 0x00: - guac_common_surface_set(current_surface, x, y, w, h, - 0x00, 0x00, 0x00, 0xFF); - break; + /* Ignore paint if nothing has been done (empty rect) */ + if (gdi->primary->hdc->hwnd->invalid->null) + goto paint_complete; - /* If NOP, do nothing */ - case 0xAA: - break; + INT32 x = gdi->primary->hdc->hwnd->invalid->x; + INT32 y = gdi->primary->hdc->hwnd->invalid->y; + UINT32 w = gdi->primary->hdc->hwnd->invalid->w; + UINT32 h = gdi->primary->hdc->hwnd->invalid->h; - /* If operation is just SRC, simply copy */ - case 0xCC: + /* guac_rect uses signed arithmetic for all values. While FreeRDP + * definitely performs its own checks and ensures these values cannot get + * so large as to cause problems with signed arithmetic, it's worth + * checking and bailing out here if an external bug breaks that. */ + GUAC_ASSERT(w <= INT_MAX && h <= INT_MAX); - /* If not cached, cache if necessary */ - if (bitmap->layer == NULL && bitmap->used >= 1) - guac_rdp_cache_bitmap(context, memblt->bitmap); + /* Mark modified region as dirty, but only within the bounds of the + * rendering surface */ + guac_rect dst_rect; + guac_rect_init(&dst_rect, x, y, w, h); + guac_rect_constrain(&dst_rect, ¤t_context->bounds); + guac_rect_extend(¤t_context->dirty, &dst_rect); - /* If not cached, send as PNG */ - if (bitmap->layer == NULL) { - if (memblt->bitmap->data != NULL) { + guac_display_render_thread_notify_modified(rdp_client->render_thread); - /* Create surface from image data */ - cairo_surface_t* surface = cairo_image_surface_create_for_data( - memblt->bitmap->data + 4*(x_src + y_src*memblt->bitmap->width), - CAIRO_FORMAT_RGB24, w, h, 4*memblt->bitmap->width); +paint_complete: - /* Send surface to buffer */ - guac_common_surface_draw(current_surface, x, y, surface); - - /* Free surface */ - cairo_surface_destroy(surface); - - } - } - - /* Otherwise, copy */ - else - guac_common_surface_copy(bitmap->layer->surface, - x_src, y_src, w, h, current_surface, x, y); - - /* Increment usage counter */ - ((guac_rdp_bitmap*) bitmap)->used++; - - break; - - /* If whiteness, send white rectangle */ - case 0xFF: - guac_common_surface_set(current_surface, x, y, w, h, - 0xFF, 0xFF, 0xFF, 0xFF); - break; - - /* Otherwise, use transfer */ - default: - - /* If not available as a surface, make available. */ - if (bitmap->layer == NULL) - guac_rdp_cache_bitmap(context, memblt->bitmap); - - guac_common_surface_transfer(bitmap->layer->surface, - x_src, y_src, w, h, - guac_rdp_rop3_transfer_function(client, memblt->bRop), - current_surface, x, y); - - /* Increment usage counter */ - ((guac_rdp_bitmap*) bitmap)->used++; - - } - - return TRUE; - -} - -BOOL guac_rdp_gdi_opaquerect(rdpContext* context, const OPAQUE_RECT_ORDER* opaque_rect) { - - /* Get client data */ - guac_client* client = ((rdp_freerdp_context*) context)->client; - - UINT32 color = guac_rdp_convert_color(context, opaque_rect->color); - - guac_common_surface* current_surface = ((guac_rdp_client*) client->data)->current_surface; - - int x = opaque_rect->nLeftRect; - int y = opaque_rect->nTopRect; - int w = opaque_rect->nWidth; - int h = opaque_rect->nHeight; - - guac_common_surface_set(current_surface, x, y, w, h, - (color >> 16) & 0xFF, - (color >> 8 ) & 0xFF, - (color ) & 0xFF, - 0xFF); + /* There will be no further drawing operations */ + rdp_client->current_context = NULL; + guac_display_layer_close_raw(default_layer, current_context); return TRUE; } -BOOL guac_rdp_gdi_set_bounds(rdpContext* context, const rdpBounds* bounds) { +BOOL guac_rdp_gdi_desktop_resize(rdpContext* context) { guac_client* client = ((rdp_freerdp_context*) context)->client; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + rdpGdi* gdi = context->gdi; - /* If no bounds given, clear bounding rect */ - if (bounds == NULL) - guac_common_surface_reset_clip(rdp_client->display->default_surface); + int width = guac_rdp_get_width(context->instance); + int height = guac_rdp_get_height(context->instance); - /* Otherwise, set bounding rectangle */ - else - guac_common_surface_clip(rdp_client->display->default_surface, - bounds->left, bounds->top, - bounds->right - bounds->left + 1, - bounds->bottom - bounds->top + 1); + GUAC_ASSERT(rdp_client->current_context == NULL); - return TRUE; + /* All potential drawing operations must occur while holding an open context */ + guac_display_layer* default_layer = guac_display_default_layer(rdp_client->display); + guac_display_layer_raw_context* current_context = guac_display_layer_open_raw(default_layer); -} + /* Resize FreeRDP's GDI buffer */ + BOOL retval = gdi_resize(context->gdi, width, height); + GUAC_ASSERT(gdi->primary_buffer != NULL); -BOOL guac_rdp_gdi_end_paint(rdpContext* context) { - /* IGNORE */ - return TRUE; -} - -BOOL guac_rdp_gdi_desktop_resize(rdpContext* context) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - - guac_common_surface_resize(rdp_client->display->default_surface, - guac_rdp_get_width(context->instance), - guac_rdp_get_height(context->instance)); - - guac_common_surface_reset_clip(rdp_client->display->default_surface); + /* Update our reference to the GDI buffer, as well as any structural + * details, which may now all be different */ + current_context->buffer = gdi->primary_buffer; + current_context->stride = gdi->stride; + guac_rect_init(¤t_context->bounds, 0, 0, gdi->width, gdi->height); + /* Resize layer to match new display dimensions and underlying buffer */ + guac_display_layer_resize(default_layer, gdi->width, gdi->height); guac_client_log(client, GUAC_LOG_DEBUG, "Server resized display to %ix%i", - guac_rdp_get_width(context->instance), - guac_rdp_get_height(context->instance)); + gdi->width, gdi->height); - return TRUE; + guac_display_layer_close_raw(default_layer, current_context); -} + return retval; +} diff --git a/src/protocols/rdp/gdi.h b/src/protocols/rdp/gdi.h index 76735aa5f..c6cfeb62a 100644 --- a/src/protocols/rdp/gdi.h +++ b/src/protocols/rdp/gdi.h @@ -26,138 +26,67 @@ #include /** - * Translates a standard RDP ROP3 value into a guac_composite_mode. Valid - * ROP3 operations indexes are listed in the RDP protocol specifications: - * - * http://msdn.microsoft.com/en-us/library/cc241583.aspx - * - * @param client - * The guac_client associated with the current RDP session. - * - * @param rop3 - * The ROP3 operation index to translate. - * - * @return - * The guac_composite_mode that equates to, or most closely approximates, - * the given ROP3 operation. - */ -guac_composite_mode guac_rdp_rop3_transfer_function(guac_client* client, - int rop3); - -/** - * Handler for the DstBlt Primary Drawing Order. A DstBlt Primary Drawing Order - * paints a rectangle of image data using a raster operation which considers - * the destination only. See: - * - * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpegdi/87ea30df-59d6-438e-a735-83f0225fbf91 + * Notifies the internal GDI implementation that a frame is either starting or + * ending. If the frame is ending and the connected client is ready to receive + * a new frame, a new frame will be flushed to the client. * * @param context * The rdpContext associated with the current RDP session. * - * @param dstblt - * The DSTBLT update to handle. - * - * @return - * TRUE if successful, FALSE otherwise. + * @param starting + * Non-zero if the frame in question is starting, zero if the frame is + * ending. */ -BOOL guac_rdp_gdi_dstblt(rdpContext* context, const DSTBLT_ORDER* dstblt); +void guac_rdp_gdi_mark_frame(rdpContext* context, int starting); /** - * Handler for the PatBlt Primary Drawing Order. A PatBlt Primary Drawing Order - * paints a rectangle of image data, a brush pattern, and a three-way raster - * operation which considers the source data, the destination, AND the brush - * pattern. See: - * - * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpegdi/bd4bf5e7-b988-45f9-8201-3b22cc9aeeb8 + * Handler called when a frame boundary is received from the RDP server in the + * form of a frame marker command. Each frame boundary may be the beginning or + * the end of a frame. * * @param context * The rdpContext associated with the current RDP session. * - * @param patblt - * The PATBLT update to handle. + * @param frame_marker + * The received frame marker. * * @return * TRUE if successful, FALSE otherwise. */ -BOOL guac_rdp_gdi_patblt(rdpContext* context, PATBLT_ORDER* patblt); +BOOL guac_rdp_gdi_frame_marker(rdpContext* context, const FRAME_MARKER_ORDER* frame_marker); /** - * Handler for the ScrBlt Primary Drawing Order. A ScrBlt Primary Drawing Order - * paints a rectangle of image data using a raster operation which considers - * the source and destination. See: - * - * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpegdi/a4e322b0-cd64-4dfc-8e1a-f24dc0edc99d + * Handler called when a frame boundary is received from the RDP server in the + * form of a surface frame marker. Each frame boundary may be the beginning or + * the end of a frame. * * @param context * The rdpContext associated with the current RDP session. * - * @param scrblt - * The SCRBLT update to handle. + * @param surface_frame_marker + * The received frame marker. * * @return * TRUE if successful, FALSE otherwise. */ -BOOL guac_rdp_gdi_scrblt(rdpContext* context, const SCRBLT_ORDER* scrblt); +BOOL guac_rdp_gdi_surface_frame_marker(rdpContext* context, const SURFACE_FRAME_MARKER* surface_frame_marker); /** - * Handler for the MemBlt Primary Drawing Order. A MemBlt Primary Drawing Order - * paints a rectangle of cached image data from a cached surface to the screen - * using a raster operation which considers the source and destination. See: - * - * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpegdi/84c2ec2f-f776-405b-9b48-6894a28b1b14 + * Handler called when a paint operation is beginning. This function is + * expected to be called by the FreeRDP GDI implementation of RemoteFX when a + * new frame has started. * * @param context * The rdpContext associated with the current RDP session. * - * @param memblt - * The MEMBLT update to handle. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_gdi_memblt(rdpContext* context, MEMBLT_ORDER* memblt); - -/** - * Handler for the OpaqueRect Primary Drawing Order. An OpaqueRect Primary - * Drawing Order draws an opaque rectangle of a single solid color. Note that - * support for OpaqueRect cannot be claimed without also supporting PatBlt, as - * both use the same negotiation order number. See: - * - * https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpegdi/1eead7aa-ac63-411a-9f8c-b1b227526877 - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param opaque_rect - * The OPAQUE RECT update to handle. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_gdi_opaquerect(rdpContext* context, - const OPAQUE_RECT_ORDER* opaque_rect); - -/** - * Handler called prior to calling the handlers for specific updates when - * those updates are clipped by a bounding rectangle. This is not a true RDP - * update, but is called by FreeRDP before and after any update involving - * clipping. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param bounds - * The clipping rectangle to set, or NULL to remove any applied clipping - * rectangle. - * * @return * TRUE if successful, FALSE otherwise. */ -BOOL guac_rdp_gdi_set_bounds(rdpContext* context, const rdpBounds* bounds); +BOOL guac_rdp_gdi_begin_paint(rdpContext* context); /** - * Handler called when a paint operation is complete. We don't actually - * use this, but FreeRDP requires it. Calling this function has no effect. + * Handler called when FreeRDP has finished performing updates to the backing + * surface of its GDI (graphics) implementation. * * @param context * The rdpContext associated with the current RDP session. diff --git a/src/protocols/rdp/glyph.c b/src/protocols/rdp/glyph.c deleted file mode 100644 index 7241a385a..000000000 --- a/src/protocols/rdp/glyph.c +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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 "color.h" -#include "common/surface.h" -#include "config.h" -#include "glyph.h" -#include "rdp.h" - -#include -#include -#include -#include - -#include -#include - -/* Define cairo_format_stride_for_width() if missing */ -#ifndef HAVE_CAIRO_FORMAT_STRIDE_FOR_WIDTH -#define cairo_format_stride_for_width(format, width) (width*4) -#endif - -BOOL guac_rdp_glyph_new(rdpContext* context, const rdpGlyph* glyph) { - - int x, y, i; - int stride; - unsigned char* image_buffer; - unsigned char* image_buffer_row; - - unsigned char* data = glyph->aj; - int width = glyph->cx; - int height = glyph->cy; - - /* Init Cairo buffer */ - stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); - image_buffer = guac_mem_alloc(height, stride); - image_buffer_row = image_buffer; - - /* Copy image data from image data to buffer */ - for (y = 0; ysurface = cairo_image_surface_create_for_data( - image_buffer, CAIRO_FORMAT_ARGB32, width, height, stride); - - return TRUE; - -} - -BOOL guac_rdp_glyph_draw(rdpContext* context, const rdpGlyph* glyph, - GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y, - GLYPH_CALLBACK_INT32 w, GLYPH_CALLBACK_INT32 h, - GLYPH_CALLBACK_INT32 sx, GLYPH_CALLBACK_INT32 sy, - BOOL redundant) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - guac_common_surface* current_surface = rdp_client->current_surface; - uint32_t fgcolor = rdp_client->glyph_color; - - /* Paint with glyph as mask */ - guac_common_surface_paint(current_surface, x, y, ((guac_rdp_glyph*) glyph)->surface, - (fgcolor & 0xFF0000) >> 16, - (fgcolor & 0x00FF00) >> 8, - fgcolor & 0x0000FF); - - return TRUE; - -} - -void guac_rdp_glyph_free(rdpContext* context, rdpGlyph* glyph) { - - unsigned char* image_buffer = cairo_image_surface_get_data( - ((guac_rdp_glyph*) glyph)->surface); - - /* Free surface */ - cairo_surface_destroy(((guac_rdp_glyph*) glyph)->surface); - guac_mem_free(image_buffer); - - /* NOTE: FreeRDP-allocated memory for the rdpGlyph will NOT be - * automatically released after this free handler is invoked, thus we must - * do so manually here */ - - free(glyph->aj); - free(glyph); - -} - -BOOL guac_rdp_glyph_begindraw(rdpContext* context, - GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y, - GLYPH_CALLBACK_INT32 width, GLYPH_CALLBACK_INT32 height, - UINT32 fgcolor, UINT32 bgcolor, BOOL redundant) { - - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_rdp_client* rdp_client = - (guac_rdp_client*) client->data; - - /* Fill background with color if specified */ - if (width != 0 && height != 0 && !redundant) { - - /* Convert background color */ - bgcolor = guac_rdp_convert_color(context, bgcolor); - - guac_common_surface_set(rdp_client->current_surface, - x, y, width, height, - (bgcolor & 0xFF0000) >> 16, - (bgcolor & 0x00FF00) >> 8, - (bgcolor & 0x0000FF), - 0xFF); - - } - - /* Convert foreground color */ - rdp_client->glyph_color = guac_rdp_convert_color(context, fgcolor); - - return TRUE; - -} - -BOOL guac_rdp_glyph_enddraw(rdpContext* context, - GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y, - GLYPH_CALLBACK_INT32 width, GLYPH_CALLBACK_INT32 height, - UINT32 fgcolor, UINT32 bgcolor) { - /* IGNORE */ - return TRUE; -} - diff --git a/src/protocols/rdp/glyph.h b/src/protocols/rdp/glyph.h deleted file mode 100644 index b217de02d..000000000 --- a/src/protocols/rdp/glyph.h +++ /dev/null @@ -1,218 +0,0 @@ -/* - * 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_GLYPH_H -#define GUAC_RDP_GLYPH_H - -#include "config.h" - -#include -#include -#include -#include - -#ifdef FREERDP_GLYPH_CALLBACKS_ACCEPT_INT32 -/** - * FreeRDP 2.0.0-rc4 and newer requires INT32 for all integer arguments of - * glyph callbacks. - */ -#define GLYPH_CALLBACK_INT32 INT32 -#else -/** - * FreeRDP 2.0.0-rc3 and older requires UINT32 for all integer arguments of - * glyph callbacks. - */ -#define GLYPH_CALLBACK_INT32 UINT32 -#endif - -/** - * Guacamole-specific rdpGlyph data. - */ -typedef struct guac_rdp_glyph { - - /** - * FreeRDP glyph data - MUST GO FIRST. - */ - rdpGlyph glyph; - - /** - * Cairo surface layer containing cached image data. - */ - cairo_surface_t* surface; - -} guac_rdp_glyph; - -/** - * Caches the given glyph. Note that this caching currently only occurs server- - * side, as it is more efficient to transmit the text as PNG. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param glyph - * The glyph to cache. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_glyph_new(rdpContext* context, const rdpGlyph* glyph); - -/** - * Draws a previously-cached glyph at the given coordinates within the current - * drawing surface. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param glyph - * The cached glyph to draw. - * - * @param x - * The destination X coordinate of the upper-left corner of the glyph. - * - * @param y - * The destination Y coordinate of the upper-left corner of the glyph. - * - * @param w - * The width of the glyph being drawn. - * - * @param h - * The height of the glyph being drawn. - * - * @param sx - * The X coordinate of the upper-left corner of the glyph within the source - * cache surface containing the glyph. - * - * @param sy - * The Y coordinate of the upper-left corner of the glyph within the source - * cache surface containing the glyph. - * - * @param redundant - * Whether the background rectangle specified is redundant (transparent). - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_glyph_draw(rdpContext* context, const rdpGlyph* glyph, - GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y, - GLYPH_CALLBACK_INT32 w, GLYPH_CALLBACK_INT32 h, - GLYPH_CALLBACK_INT32 sx, GLYPH_CALLBACK_INT32 sy, - BOOL redundant); - -/** - * Frees any Guacamole-specific data associated with the given glyph, such that - * it can be safely freed by FreeRDP. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param glyph - * The cached glyph to free. - */ -void guac_rdp_glyph_free(rdpContext* context, rdpGlyph* glyph); - -/** - * Called just prior to rendering a series of glyphs. After this function is - * called, the glyphs will be individually rendered by calls to - * guac_rdp_glyph_draw(). - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param x - * The X coordinate of the upper-left corner of the background rectangle of - * the drawing operation, or 0 if the background is transparent. - * - * @param y - * The Y coordinate of the upper-left corner of the background rectangle of - * the drawing operation, or 0 if the background is transparent. - * - * @param width - * The width of the background rectangle of the drawing operation, or 0 if - * the background is transparent. - * - * @param height - * The height of the background rectangle of the drawing operation, or 0 if - * the background is transparent. - * - * @param fgcolor - * The foreground color of each glyph. This color will be in the colorspace - * of the RDP session, and may even be a palette index, and must be - * translated via guac_rdp_convert_color(). - * - * @param bgcolor - * The background color of the drawing area. This color will be in the - * colorspace of the RDP session, and may even be a palette index, and must - * be translated via guac_rdp_convert_color(). If the background is - * transparent, this value is undefined. - * - * @param redundant - * Whether the background rectangle specified is redundant (transparent). - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_glyph_begindraw(rdpContext* context, - GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y, - GLYPH_CALLBACK_INT32 width, GLYPH_CALLBACK_INT32 height, - UINT32 fgcolor, UINT32 bgcolor, BOOL redundant); - -/** - * Called immediately after rendering a series of glyphs. Unlike - * guac_rdp_glyph_begindraw(), there is no way to detect through any invocation - * of this function whether the background color is opaque or transparent. We - * currently do NOT implement this function. - * - * @param context - * The rdpContext associated with the current RDP session. - * - * @param x - * The X coordinate of the upper-left corner of the background rectangle of - * the drawing operation. - * - * @param y - * The Y coordinate of the upper-left corner of the background rectangle of - * the drawing operation. - * - * @param width - * The width of the background rectangle of the drawing operation. - * - * @param height - * The height of the background rectangle of the drawing operation. - * - * @param fgcolor - * The foreground color of each glyph. This color will be in the colorspace - * of the RDP session, and may even be a palette index, and must be - * translated via guac_rdp_convert_color(). - * - * @param bgcolor - * The background color of the drawing area. This color will be in the - * colorspace of the RDP session, and may even be a palette index, and must - * be translated via guac_rdp_convert_color(). If the background is - * transparent, this value is undefined. - * - * @return - * TRUE if successful, FALSE otherwise. - */ -BOOL guac_rdp_glyph_enddraw(rdpContext* context, - GLYPH_CALLBACK_INT32 x, GLYPH_CALLBACK_INT32 y, - GLYPH_CALLBACK_INT32 width, GLYPH_CALLBACK_INT32 height, - UINT32 fgcolor, UINT32 bgcolor); - -#endif diff --git a/src/protocols/rdp/input.c b/src/protocols/rdp/input.c index 458c12e71..d8890571e 100644 --- a/src/protocols/rdp/input.c +++ b/src/protocols/rdp/input.c @@ -19,9 +19,8 @@ #include "channels/disp.h" #include "channels/rdpei.h" -#include "common/cursor.h" -#include "common/display.h" #include "input.h" +#include "guacamole/display.h" #include "keyboard.h" #include "rdp.h" #include "settings.h" @@ -29,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -48,7 +48,7 @@ int guac_rdp_user_mouse_handler(guac_user* user, int x, int y, int mask) { goto complete; /* Store current mouse location/state */ - guac_common_cursor_update(rdp_client->display->cursor, user, x, y, mask); + guac_display_notify_user_moved_mouse(rdp_client->display, user, x, y, mask); /* Report mouse position within recording */ if (rdp_client->recording != NULL) @@ -57,7 +57,8 @@ int guac_rdp_user_mouse_handler(guac_user* user, int x, int y, int mask) { /* If button mask unchanged, just send move event */ if (mask == rdp_client->mouse_button_mask) { pthread_mutex_lock(&(rdp_client->message_lock)); - rdp_inst->input->MouseEvent(rdp_inst->input, PTR_FLAGS_MOVE, x, y); + GUAC_RDP_CONTEXT(rdp_inst)->input->MouseEvent( + GUAC_RDP_CONTEXT(rdp_inst)->input, PTR_FLAGS_MOVE, x, y); pthread_mutex_unlock(&(rdp_client->message_lock)); } @@ -80,7 +81,8 @@ int guac_rdp_user_mouse_handler(guac_user* user, int x, int y, int mask) { if (released_mask & 0x04) flags |= PTR_FLAGS_BUTTON2; pthread_mutex_lock(&(rdp_client->message_lock)); - rdp_inst->input->MouseEvent(rdp_inst->input, flags, x, y); + GUAC_RDP_CONTEXT(rdp_inst)->input->MouseEvent( + GUAC_RDP_CONTEXT(rdp_inst)->input, flags, x, y); pthread_mutex_unlock(&(rdp_client->message_lock)); } @@ -98,7 +100,8 @@ int guac_rdp_user_mouse_handler(guac_user* user, int x, int y, int mask) { /* Send event */ pthread_mutex_lock(&(rdp_client->message_lock)); - rdp_inst->input->MouseEvent(rdp_inst->input, flags, x, y); + GUAC_RDP_CONTEXT(rdp_inst)->input->MouseEvent( + GUAC_RDP_CONTEXT(rdp_inst)->input, flags, x, y); pthread_mutex_unlock(&(rdp_client->message_lock)); } @@ -109,14 +112,16 @@ int guac_rdp_user_mouse_handler(guac_user* user, int x, int y, int mask) { /* Down */ if (pressed_mask & 0x08) { pthread_mutex_lock(&(rdp_client->message_lock)); - rdp_inst->input->MouseEvent(rdp_inst->input, PTR_FLAGS_WHEEL | 0x78, x, y); + GUAC_RDP_CONTEXT(rdp_inst)->input->MouseEvent( + GUAC_RDP_CONTEXT(rdp_inst)->input, PTR_FLAGS_WHEEL | 0x78, x, y); pthread_mutex_unlock(&(rdp_client->message_lock)); } /* Up */ if (pressed_mask & 0x10) { pthread_mutex_lock(&(rdp_client->message_lock)); - rdp_inst->input->MouseEvent(rdp_inst->input, PTR_FLAGS_WHEEL | PTR_FLAGS_WHEEL_NEGATIVE | 0x88, x, y); + GUAC_RDP_CONTEXT(rdp_inst)->input->MouseEvent( + GUAC_RDP_CONTEXT(rdp_inst)->input, PTR_FLAGS_WHEEL | PTR_FLAGS_WHEEL_NEGATIVE | 0x88, x, y); pthread_mutex_unlock(&(rdp_client->message_lock)); } diff --git a/src/protocols/rdp/keyboard.c b/src/protocols/rdp/keyboard.c index fa93c20ee..3e749bc78 100644 --- a/src/protocols/rdp/keyboard.c +++ b/src/protocols/rdp/keyboard.c @@ -107,7 +107,8 @@ static void guac_rdp_send_key_event(guac_rdp_client* rdp_client, /* Send actual key */ pthread_mutex_lock(&(rdp_client->message_lock)); - rdp_inst->input->KeyboardEvent(rdp_inst->input, flags | pressed_flags, scancode); + GUAC_RDP_CONTEXT(rdp_inst)->input->KeyboardEvent( + GUAC_RDP_CONTEXT(rdp_inst)->input, flags | pressed_flags, scancode); pthread_mutex_unlock(&(rdp_client->message_lock)); } @@ -136,7 +137,8 @@ static void guac_rdp_send_unicode_event(guac_rdp_client* rdp_client, /* Send Unicode event */ pthread_mutex_lock(&(rdp_client->message_lock)); - rdp_inst->input->UnicodeKeyboardEvent(rdp_inst->input, 0, codepoint); + GUAC_RDP_CONTEXT(rdp_inst)->input->UnicodeKeyboardEvent( + GUAC_RDP_CONTEXT(rdp_inst)->input, 0, codepoint); pthread_mutex_unlock(&(rdp_client->message_lock)); } @@ -165,7 +167,8 @@ static void guac_rdp_send_synchronize_event(guac_rdp_client* rdp_client, /* Synchronize lock key states */ pthread_mutex_lock(&(rdp_client->message_lock)); - rdp_inst->input->SynchronizeEvent(rdp_inst->input, flags); + GUAC_RDP_CONTEXT(rdp_inst)->input->SynchronizeEvent( + GUAC_RDP_CONTEXT(rdp_inst)->input, flags); pthread_mutex_unlock(&(rdp_client->message_lock)); } diff --git a/src/protocols/rdp/keymaps/base.keymap b/src/protocols/rdp/keymaps/base.keymap index 5b93fab6d..4d6cb01d9 100644 --- a/src/protocols/rdp/keymaps/base.keymap +++ b/src/protocols/rdp/keymaps/base.keymap @@ -84,7 +84,6 @@ map 0x1D ~ 0xffe3 # Control_L map +ext 0x1D ~ 0xffe4 # Control_R map 0x38 ~ 0xffe9 # Alt_L map +ext 0x38 ~ 0xffea # Alt_R -map +ext 0x38 ~ 0xfe03 # AltGr map +ext 0x5B ~ 0xffe7 # Meta_L map +ext 0x5C ~ 0xffe8 # Meta_R map +ext 0x5B ~ 0xffeb # Super_L diff --git a/src/protocols/rdp/keymaps/base_altgr.keymap b/src/protocols/rdp/keymaps/base_altgr.keymap new file mode 100644 index 000000000..5c4691d07 --- /dev/null +++ b/src/protocols/rdp/keymaps/base_altgr.keymap @@ -0,0 +1,25 @@ +# +# 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. +# + +parent "base" +name "base_altgr" + +# Modifiers +map +ext 0x38 ~ 0xfe03 # AltGr + diff --git a/src/protocols/rdp/keymaps/cs-cz-qwertz.keymap b/src/protocols/rdp/keymaps/cs-cz-qwertz.keymap new file mode 100644 index 000000000..e29037bc7 --- /dev/null +++ b/src/protocols/rdp/keymaps/cs-cz-qwertz.keymap @@ -0,0 +1,79 @@ +# +# 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. +# + +parent "base_altgr" +name "cs-cz-qwertz" +freerdp "KBD_CZECH" + +# +# Basic keys +# + +map -caps -altgr -shift 0x29 0x02..0x0D ~ ";+ěščřžýáíé=´" +map -caps -altgr -shift 0x10..0x1B ~ "qwertzuıopú)" +map -caps -altgr -shift 0x1E..0x28 0x2B ~ "asdfghjklů§¨" +map -caps -altgr -shift 0x2C..0x35 ~ "yxcvbnm,.-" + +map -caps -altgr +shift 0x29 0x02..0x0D ~ "°1234567890%ˇ" +map -caps -altgr +shift 0x10..0x1B ~ "QWERTZUIOP/(" +map -caps -altgr +shift 0x1E..0x28 0x2B ~ "ASDFGHJKL"!'" +map -caps -altgr +shift 0x2C..0x35 ~ "YXCVBNM?:_" + +map +caps -altgr -shift 0x29 0x02..0x0D ~ ";+ĚŠČŘŽÝÁÍÉ=´" +map +caps -altgr -shift 0x10..0x1B ~ "QWERTZUIOPÚ)" +map +caps -altgr -shift 0x1E..0x28 0x2B ~ "ASDFGHJKL٧¨" +map +caps -altgr -shift 0x2C..0x35 ~ "YXCVBNM,.-" + +map +caps -altgr +shift 0x29 0x02..0x0D ~ "°1234567890%ˇ" +map +caps -altgr +shift 0x10..0x1B ~ "qwertzuiop/(" +map +caps -altgr +shift 0x1E..0x28 0x2B ~ "asdfghjkl"!'" +map +caps -altgr +shift 0x2C..0x35 ~ "yxcvbnm?:_" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x02 ~ "~" + +map +altgr -shift 0x10 ~ "\" +map +altgr -shift 0x11 ~ "|" +map +altgr -shift 0x12 ~ "€" +map +altgr -shift 0x1A ~ "÷" +map +altgr -shift 0x1B ~ "×" + +map +altgr -shift 0x1F ~ "đ" +map +altgr -shift 0x20 ~ "Đ" +map +altgr -shift 0x21 ~ "[" +map +altgr -shift 0x22 ~ "]" +map +altgr -shift 0x25 ~ "ł" +map +altgr -shift 0x26 ~ "Ł" +map +altgr -shift 0x27 ~ "$" +map +altgr -shift 0x28 ~ "ß" +map +altgr -shift 0x2B ~ "¤" + +map +altgr -shift 0x2D ~ "#" +map +altgr -shift 0x2E ~ "&" +map +altgr -shift 0x2F ~ "@" +map +altgr -shift 0x30 ~ "{" +map +altgr -shift 0x31 ~ "}" +map +altgr -shift 0x33 ~ "<" +map +altgr -shift 0x34 ~ ">" +map +altgr -shift 0x35 ~ "*" + +# END diff --git a/src/protocols/rdp/keymaps/da_dk_qwerty.keymap b/src/protocols/rdp/keymaps/da_dk_qwerty.keymap index aa6139cf3..0c4bf7eb2 100644 --- a/src/protocols/rdp/keymaps/da_dk_qwerty.keymap +++ b/src/protocols/rdp/keymaps/da_dk_qwerty.keymap @@ -17,7 +17,7 @@ # under the License. # -parent "base" +parent "base_altgr" name "da-dk-qwerty" freerdp "KBD_DANISH" diff --git a/src/protocols/rdp/keymaps/de_ch_qwertz.keymap b/src/protocols/rdp/keymaps/de_ch_qwertz.keymap index c4d80c7a3..b86f8c8da 100644 --- a/src/protocols/rdp/keymaps/de_ch_qwertz.keymap +++ b/src/protocols/rdp/keymaps/de_ch_qwertz.keymap @@ -17,7 +17,7 @@ # under the License. # -parent "base" +parent "base_altgr" name "de-ch-qwertz" freerdp "KBD_SWISS_GERMAN" diff --git a/src/protocols/rdp/keymaps/de_de_qwertz.keymap b/src/protocols/rdp/keymaps/de_de_qwertz.keymap index 787c05595..bc9a373ca 100644 --- a/src/protocols/rdp/keymaps/de_de_qwertz.keymap +++ b/src/protocols/rdp/keymaps/de_de_qwertz.keymap @@ -17,7 +17,7 @@ # under the License. # -parent "base" +parent "base_altgr" name "de-de-qwertz" freerdp "KBD_GERMAN" diff --git a/src/protocols/rdp/keymaps/en_gb_qwerty.keymap b/src/protocols/rdp/keymaps/en_gb_qwerty.keymap index 9f338983d..acf0edc6e 100644 --- a/src/protocols/rdp/keymaps/en_gb_qwerty.keymap +++ b/src/protocols/rdp/keymaps/en_gb_qwerty.keymap @@ -17,7 +17,7 @@ # under the License. # -parent "base" +parent "base_altgr" name "en-gb-qwerty" freerdp "KBD_UNITED_KINGDOM" diff --git a/src/protocols/rdp/keymaps/es_es_qwerty.keymap b/src/protocols/rdp/keymaps/es_es_qwerty.keymap index 93453cff2..66d76e9b1 100644 --- a/src/protocols/rdp/keymaps/es_es_qwerty.keymap +++ b/src/protocols/rdp/keymaps/es_es_qwerty.keymap @@ -17,7 +17,7 @@ # under the License. # -parent "base" +parent "base_altgr" name "es-es-qwerty" freerdp "KBD_SPANISH" diff --git a/src/protocols/rdp/keymaps/es_latam_qwerty.keymap b/src/protocols/rdp/keymaps/es_latam_qwerty.keymap index 4c4ee578a..d69acf6bf 100644 --- a/src/protocols/rdp/keymaps/es_latam_qwerty.keymap +++ b/src/protocols/rdp/keymaps/es_latam_qwerty.keymap @@ -17,7 +17,7 @@ # under the License. # -parent "base" +parent "base_altgr" name "es-latam-qwerty" freerdp "KBD_LATIN_AMERICAN" diff --git a/src/protocols/rdp/keymaps/fr_be_azerty.keymap b/src/protocols/rdp/keymaps/fr_be_azerty.keymap index 35f637ecd..6899ad87b 100644 --- a/src/protocols/rdp/keymaps/fr_be_azerty.keymap +++ b/src/protocols/rdp/keymaps/fr_be_azerty.keymap @@ -17,7 +17,7 @@ # under the License. # -parent "base" +parent "base_altgr" name "fr-be-azerty" freerdp "KBD_BELGIAN_FRENCH" diff --git a/src/protocols/rdp/keymaps/fr_ca_qwerty.keymap b/src/protocols/rdp/keymaps/fr_ca_qwerty.keymap new file mode 100644 index 000000000..da0f4df4a --- /dev/null +++ b/src/protocols/rdp/keymaps/fr_ca_qwerty.keymap @@ -0,0 +1,55 @@ +# +# 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. +# + +parent "base_altgr" +name "fr-ca-qwerty" +freerdp "KBD_CANADIAN_FRENCH" + +# +# Basic keys +# + +map -altgr -shift 0x29 0x02..0x0D ~ "#1234567890-=" +map -altgr -shift 0x10..0x1B 0x2B ~ "qwertyuiop^¸<" +map -altgr -shift 0x1E..0x28 ~ "asdfghjkl;`" +map -altgr -shift 0x2C..0x35 ~ "zxcvbnm,.é" + +map -altgr +shift 0x29 0x02..0x0D ~ "|!"/$%?&*()_+" +map -altgr +shift 0x10..0x1B 0x2B ~ "QWERTYUIOP^¨>" +map -altgr +shift 0x1E..0x28 ~ "ASDFGHJKL:`" +map -altgr +shift 0x2C..0x35 ~ "ZXCVBNM'.É" + +# +# Keys requiring AltGr +# + +map +altgr -shift 0x29 0x02..0x0D ~ "\±@£¢¤¬¦²³¼½¾" +map +altgr -shift 0x18..0x1B 0x2B ~ "§¶[]}" +map +altgr -shift 0x27..0x28 ~ "~{" +map +altgr -shift 0x32..0x33 0x35 ~ "µ¯´" + +# +# Combined accents +# + +map -altgr -shift 0x1A ~ 0x0302 # COMBINING CIRCUMFLEX ACCENT +map -altgr -shift 0x1B ~ 0x0327 # COMBINING CEDILLA +map -altgr +shift 0x1B ~ 0x0308 # COMBINING DIAERESIS +map -altgr -shift 0x28 ~ 0x0300 # COMBINING GRAVE ACCENT +map +altgr -shift 0x35 ~ 0x0301 # COMBINING ACUTE ACCENT diff --git a/src/protocols/rdp/keymaps/fr_ch_qwertz.keymap b/src/protocols/rdp/keymaps/fr_ch_qwertz.keymap index 8864d701f..088355497 100644 --- a/src/protocols/rdp/keymaps/fr_ch_qwertz.keymap +++ b/src/protocols/rdp/keymaps/fr_ch_qwertz.keymap @@ -17,7 +17,7 @@ # under the License. # -parent "base" +parent "base_altgr" name "fr-ch-qwertz" freerdp "KBD_SWISS_FRENCH" diff --git a/src/protocols/rdp/keymaps/fr_fr_azerty.keymap b/src/protocols/rdp/keymaps/fr_fr_azerty.keymap index 7f4f83271..4d29c4c62 100644 --- a/src/protocols/rdp/keymaps/fr_fr_azerty.keymap +++ b/src/protocols/rdp/keymaps/fr_fr_azerty.keymap @@ -17,7 +17,7 @@ # under the License. # -parent "base" +parent "base_altgr" name "fr-fr-azerty" freerdp "KBD_FRENCH" diff --git a/src/protocols/rdp/keymaps/hu_hu_qwertz.keymap b/src/protocols/rdp/keymaps/hu_hu_qwertz.keymap index bf9d8e6db..0d9242d73 100644 --- a/src/protocols/rdp/keymaps/hu_hu_qwertz.keymap +++ b/src/protocols/rdp/keymaps/hu_hu_qwertz.keymap @@ -17,7 +17,7 @@ # under the License. # -parent "base" +parent "base_altgr" name "hu-hu-qwertz" freerdp "KBD_HUNGARIAN" diff --git a/src/protocols/rdp/keymaps/it_it_qwerty.keymap b/src/protocols/rdp/keymaps/it_it_qwerty.keymap index 54f2172ae..9b58331be 100644 --- a/src/protocols/rdp/keymaps/it_it_qwerty.keymap +++ b/src/protocols/rdp/keymaps/it_it_qwerty.keymap @@ -17,7 +17,7 @@ # under the License. # -parent "base" +parent "base_altgr" name "it-it-qwerty" freerdp "KBD_ITALIAN" diff --git a/src/protocols/rdp/keymaps/ja_jp_qwerty.keymap b/src/protocols/rdp/keymaps/ja_jp_qwerty.keymap index 673b10fda..305cb73de 100644 --- a/src/protocols/rdp/keymaps/ja_jp_qwerty.keymap +++ b/src/protocols/rdp/keymaps/ja_jp_qwerty.keymap @@ -34,3 +34,9 @@ map +shift 0x2C..0x35 0x73 ~ "ZXCVBNM<>?_" map -shift 0x29 ~ 0xFF28 map -shift 0x29 ~ 0xFF2A map +shift 0x29 ~ 0xFF29 +map -shift 0x29 ~ 0xFF21 # KanjiMode +map -shift 0x70 ~ 0xFF27 # HiraganaKatakana +map -shift 0x70 ~ 0xFF24 # Romaji +map -shift 0x7B ~ 0xFF22 # NonConvert +map -shift 0x79 ~ 0xFF23 # Convert +map -shift 0x3A ~ 0xFF30 # Alphanumeric diff --git a/src/protocols/rdp/keymaps/no_no_qwerty.keymap b/src/protocols/rdp/keymaps/no_no_qwerty.keymap index 5fbc126b0..62fe1a43d 100644 --- a/src/protocols/rdp/keymaps/no_no_qwerty.keymap +++ b/src/protocols/rdp/keymaps/no_no_qwerty.keymap @@ -17,7 +17,7 @@ # under the License. # -parent "base" +parent "base_altgr" name "no-no-qwerty" freerdp "KBD_NORWEGIAN" diff --git a/src/protocols/rdp/keymaps/pt_br_qwerty.keymap b/src/protocols/rdp/keymaps/pt_br_qwerty.keymap index e0656e9b3..c513d3802 100644 --- a/src/protocols/rdp/keymaps/pt_br_qwerty.keymap +++ b/src/protocols/rdp/keymaps/pt_br_qwerty.keymap @@ -17,7 +17,7 @@ # under the License. # -parent "base" +parent "base_altgr" name "pt-br-qwerty" freerdp "KBD_PORTUGUESE_BRAZILIAN_ABNT2" @@ -64,4 +64,4 @@ map -altgr +shift 0x07 ~ 0xFE57 # Dead diaeresis (umlaut) map -altgr +shift 0x1A ~ 0xFE50 # Dead grave map -altgr -shift 0x1A ~ 0xFE51 # Dead acute map -altgr +shift 0x28 ~ 0xFE52 # Dead circumflex -map -altgr -shift 0x28 ~ 0xFE53 # Dead tilde \ No newline at end of file +map -altgr -shift 0x28 ~ 0xFE53 # Dead tilde diff --git a/src/protocols/rdp/keymaps/pt_pt_qwerty.keymap b/src/protocols/rdp/keymaps/pt_pt_qwerty.keymap new file mode 100644 index 000000000..4703b31c1 --- /dev/null +++ b/src/protocols/rdp/keymaps/pt_pt_qwerty.keymap @@ -0,0 +1,69 @@ +# +# 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. +# + +parent "base_altgr" +name "pt-pt-qwerty" +freerdp "KBD_PORTUGUESE" + +# +# Basic keys +# + +map -caps -shift 0x29 0x02..0x0D ~ "\1234567890'«" +map -caps -shift 0x10..0x1A ~ "qwertyuiop+" +map -caps -shift 0x1E..0x28 ~ "asdfghjklçº" +map -caps -shift 0x56 0x2C..0x35 ~ "ZXCVBNM;:_" + +map +caps -shift 0x29 0x02..0x0D ~ "\1234567890'«" +map +caps -shift 0x10..0x1A ~ "QWERTYUIOP+" +map +caps -shift 0x1E..0x28 ~ "ASDFGHJKLǺ" +map +caps -shift 0x56 0x2C..0x35 ~ "\ZXCVBNM,./" + +map +caps +shift 0x29 0x02..0x0D ~ "|!"#$%&/()=?»" +map +caps +shift 0x10..0x1A ~ "qwertyuiop*" +map +caps +shift 0x1E..0x28 ~ "asdfghjklçª" +map +caps +shift 0x56 0x2C..0x35 ~ ">zxcvbnm;:_" + + +# +# Keys requiring AltGr (unaffected by Caps Lock, but Shift must not be pressed) +# + +map +altgr -shift 0x03..0x0B ~ "@£§½¬{[]}" +map +altgr -shift 0x12 ~ "€" + +# +# Dead keys requiring AltGr (unaffected by Caps Lock, but Shift must not be pressed) +# + +map +altgr -shift 0x1A ~ 0xFE57 # Dead diaeresis + +# +# Dead keys (unaffected by Caps Lock, but AltGr must not be pressed) +# + +map -altgr -shift 0x1B ~ 0xFE51 # Dead acute +map -altgr +shift 0x1B ~ 0xFE50 # Dead grave +map -altgr -shift 0x2B ~ 0xFE53 # Dead tilde +map -altgr +shift 0x2B ~ 0xFE52 # Dead circumflex diff --git a/src/protocols/rdp/keymaps/ro_ro_qwerty.keymap b/src/protocols/rdp/keymaps/ro_ro_qwerty.keymap new file mode 100644 index 000000000..f66641980 --- /dev/null +++ b/src/protocols/rdp/keymaps/ro_ro_qwerty.keymap @@ -0,0 +1,92 @@ +# +# 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. +# + +parent "base_altgr" +name "ro-ro-qwerty" +freerdp "KBD_ROMANIAN" + +# +# Basic keys +# + +map -caps -shift 0x29 0x02..0x0D ~ "`1234567890-=" +map -caps -shift 0x10..0x1B 0x2B ~ "qwertyuiop[]\" +map -caps -shift 0x1E..0x28 ~ "asdfghjkl;'" +map -caps -shift 0x56 0x2C..0x35 ~ "\zxcvbnm,./" + +map -caps +shift 0x29 0x02..0x0D ~ "~!@#$%^&*()_+" +map -caps +shift 0x10..0x1B 0x2B ~ "QWERTYUIOP{}|" +map -caps +shift 0x1E..0x28 ~ "ASDFGHJKL:"" +map -caps +shift 0x56 0x2C..0x35 ~ "|ZXCVBNM<>?" + +map +caps -shift 0x29 0x02..0x0D ~ "`1234567890-=" +map +caps -shift 0x10..0x1B 0x2B ~ "QWERTYUIOP[]\" +map +caps -shift 0x1E..0x28 ~ "ASDFGHJKL;'" +map +caps -shift 0x56 0x2C..0x35 ~ "\ZXCVBNM,./" + +map +caps +shift 0x29 0x02..0x0D ~ "~!@#$%^&*()_+" +map +caps +shift 0x10..0x1B 0x2B ~ "qwertyuiop{}|" +map +caps +shift 0x1E..0x28 ~ "asdfghjkl:"" +map +caps +shift 0x56 0x2C..0x35 ~ "|zxcvbnm<>?" + +# +# Keys requiring AltGr (unaffected by Caps Lock) +# + +map +altgr +shift 0x0C ~ "–" +map +altgr +shift 0x0D ~ "±" +map +altgr -shift 0x10 ~ "â" +map +altgr +shift 0x10 ~ "Â" +map +altgr -shift 0x11 ~ "ß" +map +altgr -shift 0x12 ~ "€" +map +altgr -shift 0x14 ~ "ț" +map +altgr +shift 0x14 ~ "Ț" +map +altgr -shift 0x17 ~ "î" +map +altgr +shift 0x17 ~ "Î" +map +altgr -shift 0x19 ~ "§" +map +altgr -shift 0x1A ~ "„" +map +altgr -shift 0x1B ~ "”" +map +altgr -shift 0x1E ~ "ă" +map +altgr +shift 0x1E ~ "Ă" +map +altgr -shift 0x1F ~ "ș" +map +altgr +shift 0x1F ~ "Ș" +map +altgr -shift 0x20 ~ "đ" +map +altgr +shift 0x20 ~ "Đ" +map +altgr -shift 0x26 ~ "ł" +map +altgr +shift 0x26 ~ "Ł" +map +altgr -shift 0x2E ~ "©" +map +altgr -shift 0x33 ~ "«" +map +altgr -shift 0x34 ~ "»" + +# +# Dead keys requiring AltGr (unaffected by Caps Lock, but Shift must not be pressed) +# + +map +altgr -shift 0x02 ~ 0xFE53 # Dead tilde +map +altgr -shift 0x03 ~ 0xFE5A # Dead caron +map +altgr -shift 0x04 ~ 0xFE52 # Dead circumflex +map +altgr -shift 0x05 ~ 0xFE55 # Dead breve +map +altgr -shift 0x06 ~ 0xFE58 # Dead abovering +map +altgr -shift 0x07 ~ 0xFE5C # Dead ogonek +map +altgr -shift 0x08 ~ 0xFE50 # Dead grave +map +altgr -shift 0x09 ~ 0xFE56 # Dead abovedot +map +altgr -shift 0x0A ~ 0xFE51 # Dead acute +map +altgr -shift 0x0B ~ 0xFE59 # Dead doubleacute +map +altgr -shift 0x0C ~ 0xFE57 # Dead diaeresis +map +altgr -shift 0x0D ~ 0xFE5B # Dead cedilla diff --git a/src/protocols/rdp/keymaps/sv_se_qwerty.keymap b/src/protocols/rdp/keymaps/sv_se_qwerty.keymap index 6ab8b7a50..1a7ce4a40 100644 --- a/src/protocols/rdp/keymaps/sv_se_qwerty.keymap +++ b/src/protocols/rdp/keymaps/sv_se_qwerty.keymap @@ -17,7 +17,7 @@ # under the License. # -parent "base" +parent "base_altgr" name "sv-se-qwerty" freerdp "KBD_SWEDISH" diff --git a/src/protocols/rdp/keymaps/tr_tr_qwerty.keymap b/src/protocols/rdp/keymaps/tr_tr_qwerty.keymap index ff48349a0..707846c0a 100644 --- a/src/protocols/rdp/keymaps/tr_tr_qwerty.keymap +++ b/src/protocols/rdp/keymaps/tr_tr_qwerty.keymap @@ -17,7 +17,7 @@ # under the License. # -parent "base" +parent "base_altgr" name "tr-tr-qwerty" freerdp "KBD_TURKISH_Q" diff --git a/src/protocols/rdp/plugins/channels.c b/src/protocols/rdp/plugins/channels.c index 342ca33b3..3ae3a8551 100644 --- a/src/protocols/rdp/plugins/channels.c +++ b/src/protocols/rdp/plugins/channels.c @@ -136,8 +136,11 @@ void guac_freerdp_dynamic_channel_collection_add(rdpSettings* settings, va_end(args); /* Register plugin with FreeRDP */ +#ifdef HAVE_SETTERS_GETTERS + freerdp_settings_set_bool(settings, FreeRDP_SupportDynamicChannels, TRUE); +#else settings->SupportDynamicChannels = TRUE; - freerdp_dynamic_channel_collection_add(settings, freerdp_args); +#endif + freerdp_dynamic_channel_collection_add(settings, freerdp_args); } - diff --git a/src/protocols/rdp/plugins/channels.h b/src/protocols/rdp/plugins/channels.h index b1207e2a3..2c3a9fbd0 100644 --- a/src/protocols/rdp/plugins/channels.h +++ b/src/protocols/rdp/plugins/channels.h @@ -61,8 +61,8 @@ * The name of the plugin to load. If the plugin is not statically built * into FreeRDP, this name will determine the filename of the library to be * loaded dynamically. For a plugin named "NAME", the library called - * "libNAME-client" will be loaded from the "freerdp2" subdirectory of the - * main directory containing the FreeRDP libraries. + * "libNAME-client" will be loaded from the "freerdp2" or "freerdp3" + * subdirectory of the main directory containing the FreeRDP libraries. * * @param data * Arbitrary data to be passed to the plugin entry point. For most plugins @@ -105,8 +105,8 @@ int guac_freerdp_channels_load_plugin(rdpContext* context, * The name of the plugin to load. If the plugin is not statically built * into FreeRDP, this name will determine the filename of the library to be * loaded dynamically. For a plugin named "NAME", the library called - * "libNAME-client" will be loaded from the "freerdp2" subdirectory of the - * main directory containing the FreeRDP libraries. + * "libNAME-client" will be loaded from the "freerdp2" or "freerdp3" + * subdirectory of the main directory containing the FreeRDP libraries. * * @param ... * Arbitrary arguments to be passed to the plugin entry point. For most @@ -204,4 +204,3 @@ PVIRTUALCHANNELENTRY guac_rdp_plugin_wrap_entry(guac_client* client, PVIRTUALCHANNELENTRY entry); #endif - diff --git a/src/protocols/rdp/plugins/guacai/guacai.c b/src/protocols/rdp/plugins/guacai/guacai.c index 6ab633c05..2abae6524 100644 --- a/src/protocols/rdp/plugins/guacai/guacai.c +++ b/src/protocols/rdp/plugins/guacai/guacai.c @@ -283,7 +283,12 @@ static UINT guac_rdp_ai_terminated(IWTSPlugin* plugin) { UINT DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) { /* Pull guac_client from arguments */ +#ifdef PLUGIN_DATA_CONST + const ADDIN_ARGV* args = pEntryPoints->GetPluginData(pEntryPoints); +#else ADDIN_ARGV* args = pEntryPoints->GetPluginData(pEntryPoints); +#endif + guac_client* client = (guac_client*) guac_rdp_string_to_ptr(args->argv[1]); /* Pull previously-allocated plugin */ @@ -309,4 +314,3 @@ UINT DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints) { return CHANNEL_RC_OK; } - diff --git a/src/protocols/rdp/pointer.c b/src/protocols/rdp/pointer.c index 165cde135..7387b46b8 100644 --- a/src/protocols/rdp/pointer.c +++ b/src/protocols/rdp/pointer.c @@ -18,9 +18,7 @@ */ #include "color.h" -#include "common/cursor.h" -#include "common/display.h" -#include "common/surface.h" +#include "gdi.h" #include "pointer.h" #include "rdp.h" @@ -29,6 +27,8 @@ #include #include #include +#include +#include #include BOOL guac_rdp_pointer_new(rdpContext* context, rdpPointer* pointer) { @@ -37,33 +37,31 @@ BOOL guac_rdp_pointer_new(rdpContext* context, rdpPointer* pointer) { guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; /* Allocate buffer */ - guac_common_display_layer* buffer = guac_common_display_alloc_buffer( - rdp_client->display, pointer->width, pointer->height); + guac_display_layer* buffer = guac_display_alloc_buffer(rdp_client->display, 0); - /* Allocate data for image */ - unsigned char* data = _aligned_malloc(pointer->width * pointer->height * 4, 16); + guac_display_layer_resize(buffer, pointer->width, pointer->height); + guac_display_layer_raw_context* dst_context = guac_display_layer_open_raw(buffer); - cairo_surface_t* surface; + guac_rect dst_rect = { + .left = 0, + .top = 0, + .right = pointer->width, + .bottom = pointer->height + }; + + guac_rect_constrain(&dst_rect, &dst_context->bounds); /* Convert to alpha cursor using mask data */ - freerdp_image_copy_from_pointer_data(data, - guac_rdp_get_native_pixel_format(TRUE), 0, 0, 0, + freerdp_image_copy_from_pointer_data(GUAC_DISPLAY_LAYER_RAW_BUFFER(dst_context, dst_rect), + guac_rdp_get_native_pixel_format(TRUE), dst_context->stride, 0, 0, pointer->width, pointer->height, pointer->xorMaskData, pointer->lengthXorMask, pointer->andMaskData, pointer->lengthAndMask, pointer->xorBpp, &context->gdi->palette); - /* Create surface from image data */ - surface = cairo_image_surface_create_for_data( - data, CAIRO_FORMAT_ARGB32, - pointer->width, pointer->height, 4*pointer->width); - - /* Send surface to buffer */ - guac_common_surface_draw(buffer->surface, 0, 0, surface); + guac_rect_extend(&dst_context->dirty, &dst_rect); - /* Free surface */ - cairo_surface_destroy(surface); - _aligned_free(data); + guac_display_layer_close_raw(buffer, dst_context); /* Remember buffer */ ((guac_rdp_pointer*) pointer)->layer = buffer; @@ -72,28 +70,50 @@ BOOL guac_rdp_pointer_new(rdpContext* context, rdpPointer* pointer) { } -BOOL guac_rdp_pointer_set(rdpContext* context, const rdpPointer* pointer) { +BOOL guac_rdp_pointer_set(rdpContext* context, POINTER_SET_CONST rdpPointer* pointer) { guac_client* client = ((rdp_freerdp_context*) context)->client; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + guac_display_layer* src_layer = ((guac_rdp_pointer*) pointer)->layer; + guac_display_layer_raw_context* src_context = guac_display_layer_open_raw(src_layer); + + guac_display_layer* cursor_layer = guac_display_cursor(rdp_client->display); + + guac_display_layer_resize(cursor_layer, pointer->width, pointer->height); + guac_display_layer_raw_context* dst_context = guac_display_layer_open_raw(cursor_layer); + + guac_rect ptr_rect = { + .left = 0, + .top = 0, + .right = pointer->width, + .bottom = pointer->height + }; + + guac_rect_constrain(&ptr_rect, &src_context->bounds); + guac_rect_constrain(&ptr_rect, &dst_context->bounds); + /* Set cursor */ - guac_common_cursor_set_surface(rdp_client->display->cursor, - pointer->xPos, pointer->yPos, - ((guac_rdp_pointer*) pointer)->layer->surface); + guac_display_layer_raw_context_put(dst_context, &ptr_rect, src_context->buffer, src_context->stride); + dst_context->hint_from = src_layer; + guac_rect_extend(&dst_context->dirty, &ptr_rect); + + guac_display_set_cursor_hotspot(rdp_client->display, pointer->xPos, pointer->yPos); + + guac_display_layer_close_raw(cursor_layer, dst_context); + guac_display_layer_close_raw(src_layer, src_context); + guac_display_render_thread_notify_modified(rdp_client->render_thread); return TRUE; } void guac_rdp_pointer_free(rdpContext* context, rdpPointer* pointer) { - guac_client* client = ((rdp_freerdp_context*) context)->client; - guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; - guac_common_display_layer* buffer = ((guac_rdp_pointer*) pointer)->layer; + guac_display_layer* buffer = ((guac_rdp_pointer*) pointer)->layer; /* Free buffer */ - guac_common_display_free_buffer(rdp_client->display, buffer); + guac_display_free_layer(buffer); /* NOTE: FreeRDP-allocated memory for the rdpPointer will be automatically * released after this free handler is invoked */ @@ -106,8 +126,9 @@ BOOL guac_rdp_pointer_set_null(rdpContext* context) { guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; /* Set cursor to empty/blank graphic */ - guac_common_cursor_set_blank(rdp_client->display->cursor); + guac_display_set_cursor(rdp_client->display, GUAC_DISPLAY_CURSOR_NONE); + guac_display_render_thread_notify_modified(rdp_client->render_thread); return TRUE; } @@ -118,8 +139,8 @@ BOOL guac_rdp_pointer_set_default(rdpContext* context) { guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; /* Set cursor to embedded pointer */ - guac_common_cursor_set_pointer(rdp_client->display->cursor); + guac_display_set_cursor(rdp_client->display, GUAC_DISPLAY_CURSOR_POINTER); + guac_display_render_thread_notify_modified(rdp_client->render_thread); return TRUE; } - diff --git a/src/protocols/rdp/pointer.h b/src/protocols/rdp/pointer.h index b5fa6a236..8e8c21e3e 100644 --- a/src/protocols/rdp/pointer.h +++ b/src/protocols/rdp/pointer.h @@ -20,12 +20,17 @@ #ifndef GUAC_RDP_POINTER_H #define GUAC_RDP_POINTER_H -#include "common/display.h" - #include #include +#include #include +#ifdef RDP_POINTER_SET_REQUIRES_CONST +#define POINTER_SET_CONST const +#else +#define POINTER_SET_CONST +#endif + /** * Guacamole-specific rdpPointer data. */ @@ -39,7 +44,7 @@ typedef struct guac_rdp_pointer { /** * The display layer containing cached image data. */ - guac_common_display_layer* layer; + guac_display_layer* layer; } guac_rdp_pointer; @@ -71,7 +76,7 @@ BOOL guac_rdp_pointer_new(rdpContext* context, rdpPointer* pointer); * @return * TRUE if successful, FALSE otherwise. */ -BOOL guac_rdp_pointer_set(rdpContext* context, const rdpPointer* pointer); +BOOL guac_rdp_pointer_set(rdpContext* context, POINTER_SET_CONST rdpPointer* pointer); /** * Frees all Guacamole-related data associated with the given pointer, allowing diff --git a/src/protocols/rdp/rdp.c b/src/protocols/rdp/rdp.c index 88c57e323..a6e7720d3 100644 --- a/src/protocols/rdp/rdp.c +++ b/src/protocols/rdp/rdp.c @@ -19,7 +19,6 @@ #include "argv.h" #include "beep.h" -#include "bitmap.h" #include "channels/audio-input/audio-buffer.h" #include "channels/audio-input/audio-input.h" #include "channels/cliprdr.h" @@ -28,16 +27,15 @@ #include "channels/rail.h" #include "channels/rdpdr/rdpdr.h" #include "channels/rdpei.h" +#include "channels/rdpgfx.h" #include "channels/rdpsnd/rdpsnd.h" #include "client.h" #include "color.h" -#include "common/cursor.h" -#include "common/display.h" #include "config.h" #include "error.h" #include "fs.h" #include "gdi.h" -#include "glyph.h" +#include "guacamole/display-types.h" #include "keyboard.h" #include "plugins/channels.h" #include "pointer.h" @@ -52,12 +50,6 @@ #endif #include -#include -#include -#include -#include -#include -#include #include #include #include @@ -69,12 +61,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -83,21 +77,29 @@ #include #include -BOOL rdp_freerdp_pre_connect(freerdp* instance) { - - rdpContext* context = instance->context; - rdpGraphics* graphics = context->graphics; - +/** + * Initializes and loads the necessary FreeRDP plugins based on the current + * RDP session settings. This function is designed to work in environments + * where the FreeRDP instance expects a LoadChannels callback to be set + * otherwise it can becalled directly from our pre_connect callback. It + * configures various features such as display resizing, multi-touch support, + * audio input, clipboard synchronization, device redirection, and graphics + * pipeline, by loading their corresponding plugins if they are enabled in the + * session settings. + * + * @param instance + * The FreeRDP instance to be prepared, containing all context and + * settings for the session. + * + * @return + * Always TRUE. + */ +static BOOL rdp_freerdp_load_channels(freerdp* instance) { + rdpContext* context = GUAC_RDP_CONTEXT(instance); guac_client* client = ((rdp_freerdp_context*) context)->client; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; guac_rdp_settings* settings = rdp_client->settings; - /* Push desired settings to FreeRDP */ - guac_rdp_push_settings(client, settings, instance); - - /* Init FreeRDP add-in provider */ - freerdp_register_addin_provider(freerdp_channels_load_static_addin_entry, 0); - /* Load "disp" plugin for display update */ if (settings->resize_method == GUAC_RESIZE_DISPLAY_UPDATE) guac_rdp_disp_load_plugin(context); @@ -111,7 +113,7 @@ BOOL rdp_freerdp_pre_connect(freerdp* instance) { /* Upgrade the lock to write temporarily for setting the newly allocated audio buffer */ guac_rwlock_acquire_write_lock(&(rdp_client->lock)); rdp_client->audio_input = guac_rdp_audio_buffer_alloc(client); - guac_rdp_audio_load_plugin(instance->context); + guac_rdp_audio_load_plugin(GUAC_RDP_CONTEXT(instance)); /* Downgrade the lock to allow for concurrent read access */ guac_rwlock_release_lock(&(rdp_client->lock)); @@ -129,6 +131,53 @@ BOOL rdp_freerdp_pre_connect(freerdp* instance) { guac_rdpsnd_load_plugin(context); } + /* Load "rdpgfx" plugin for Graphics Pipeline Extension */ + if (settings->enable_gfx) + guac_rdp_rdpgfx_load_plugin(context); + + /* Load plugin providing Dynamic Virtual Channel support, if required */ + if (freerdp_settings_get_bool(GUAC_RDP_CONTEXT(instance)->settings, FreeRDP_SupportDynamicChannels) && + guac_freerdp_channels_load_plugin(context, "drdynvc", + GUAC_RDP_CONTEXT(instance)->settings)) { + guac_client_log(client, GUAC_LOG_WARNING, + "Failed to load drdynvc plugin. Display update and audio " + "input support will be disabled."); + } + + return TRUE; +} + +/** + * Prepares the FreeRDP instance for connection by setting up session-specific + * configurations like graphics, plugins, and RDP settings. This involves + * integrating Guacamole's custom rendering handlers (for bitmaps, glyphs, + * and pointers). If using a freerdp instance that does not expect a + * LoadChannels callback then this function manually loads RDP channels. + * + * @param instance + * The FreeRDP instance to be prepared, containing all context and + * settings for the session. + * + * @return + * Returns TRUE if the pre-connection process completes successfully. + * Returns FALSE if an error occurs during the initialization of the + * FreeRDP GDI system. + */ +static BOOL rdp_freerdp_pre_connect(freerdp* instance) { + + rdpContext* context = GUAC_RDP_CONTEXT(instance); + rdpGraphics* graphics = context->graphics; + + guac_client* client = ((rdp_freerdp_context*) context)->client; + guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; + guac_rdp_settings* settings = rdp_client->settings; + + /* Push desired settings to FreeRDP */ + guac_rdp_push_settings(client, settings, instance); + + /* Init FreeRDP add-in provider */ + freerdp_register_addin_provider(freerdp_channels_load_static_addin_entry, 0); + /* Load RAIL plugin if RemoteApp in use */ if (settings->remote_app != NULL) guac_rdp_rail_load_plugin(context); @@ -143,38 +192,10 @@ BOOL rdp_freerdp_pre_connect(freerdp* instance) { } - /* Load plugin providing Dynamic Virtual Channel support, if required */ - if (instance->settings->SupportDynamicChannels && - guac_freerdp_channels_load_plugin(context, "drdynvc", - instance->settings)) { - guac_client_log(client, GUAC_LOG_WARNING, - "Failed to load drdynvc plugin. Display update and audio " - "input support will be disabled."); - } - /* Init FreeRDP internal GDI implementation */ if (!gdi_init(instance, guac_rdp_get_native_pixel_format(FALSE))) return FALSE; - /* Set up bitmap handling */ - rdpBitmap bitmap = *graphics->Bitmap_Prototype; - bitmap.size = sizeof(guac_rdp_bitmap); - bitmap.New = guac_rdp_bitmap_new; - bitmap.Free = guac_rdp_bitmap_free; - bitmap.Paint = guac_rdp_bitmap_paint; - bitmap.SetSurface = guac_rdp_bitmap_setsurface; - graphics_register_bitmap(graphics, &bitmap); - - /* Set up glyph handling */ - rdpGlyph glyph = *graphics->Glyph_Prototype; - glyph.size = sizeof(guac_rdp_glyph); - glyph.New = guac_rdp_glyph_new; - glyph.Free = guac_rdp_glyph_free; - glyph.Draw = guac_rdp_glyph_draw; - glyph.BeginDraw = guac_rdp_glyph_begindraw; - glyph.EndDraw = guac_rdp_glyph_enddraw; - graphics_register_glyph(graphics, &glyph); - /* Set up pointer handling */ rdpPointer pointer = *graphics->Pointer_Prototype; pointer.size = sizeof(guac_rdp_pointer); @@ -186,32 +207,28 @@ BOOL rdp_freerdp_pre_connect(freerdp* instance) { graphics_register_pointer(graphics, &pointer); /* Beep on receipt of Play Sound PDU */ - instance->update->PlaySound = guac_rdp_beep_play_sound; + GUAC_RDP_CONTEXT(instance)->update->PlaySound = guac_rdp_beep_play_sound; /* Automatically synchronize keyboard locks when changed server-side */ - instance->update->SetKeyboardIndicators = guac_rdp_keyboard_set_indicators; + GUAC_RDP_CONTEXT(instance)->update->SetKeyboardIndicators = guac_rdp_keyboard_set_indicators; /* Set up GDI */ - instance->update->DesktopResize = guac_rdp_gdi_desktop_resize; - instance->update->EndPaint = guac_rdp_gdi_end_paint; - instance->update->SetBounds = guac_rdp_gdi_set_bounds; - - rdpPrimaryUpdate* primary = instance->update->primary; - primary->DstBlt = guac_rdp_gdi_dstblt; - primary->PatBlt = guac_rdp_gdi_patblt; - primary->ScrBlt = guac_rdp_gdi_scrblt; - primary->MemBlt = guac_rdp_gdi_memblt; - primary->OpaqueRect = guac_rdp_gdi_opaquerect; - - pointer_cache_register_callbacks(instance->update); - glyph_cache_register_callbacks(instance->update); - brush_cache_register_callbacks(instance->update); - bitmap_cache_register_callbacks(instance->update); - offscreen_cache_register_callbacks(instance->update); - palette_cache_register_callbacks(instance->update); + GUAC_RDP_CONTEXT(instance)->update->DesktopResize = guac_rdp_gdi_desktop_resize; + GUAC_RDP_CONTEXT(instance)->update->BeginPaint = guac_rdp_gdi_begin_paint; + GUAC_RDP_CONTEXT(instance)->update->EndPaint = guac_rdp_gdi_end_paint; - return TRUE; + GUAC_RDP_CONTEXT(instance)->update->SurfaceFrameMarker = guac_rdp_gdi_surface_frame_marker; + GUAC_RDP_CONTEXT(instance)->update->altsec->FrameMarker = guac_rdp_gdi_frame_marker; + /* + * If the freerdp instance does not have a LoadChannels callback for + * loading plugins we use the PreConnect callback to load plugins instead. + */ + #ifndef RDP_INST_HAS_LOAD_CHANNELS + rdp_freerdp_load_channels(instance); + #endif + + return TRUE; } /** @@ -245,7 +262,7 @@ BOOL rdp_freerdp_pre_connect(freerdp* instance) { static BOOL rdp_freerdp_authenticate(freerdp* instance, char** username, char** password, char** domain) { - rdpContext* context = instance->context; + rdpContext* context = GUAC_RDP_CONTEXT(instance); guac_client* client = ((rdp_freerdp_context*) context)->client; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; guac_rdp_settings* settings = rdp_client->settings; @@ -387,7 +404,7 @@ static DWORD rdp_freerdp_verify_certificate(freerdp* instance, const char* fingerprint, BOOL host_mismatch) { #endif - rdpContext* context = instance->context; + rdpContext* context = GUAC_RDP_CONTEXT(instance); guac_client* client = ((rdp_freerdp_context*) context)->client; guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; @@ -423,11 +440,11 @@ static int rdp_guac_client_wait_for_messages(guac_client* client, freerdp* rdp_inst = rdp_client->rdp_inst; HANDLE handles[GUAC_RDP_MAX_FILE_DESCRIPTORS]; - int num_handles = freerdp_get_event_handles(rdp_inst->context, handles, + int num_handles = freerdp_get_event_handles(GUAC_RDP_CONTEXT(rdp_inst), handles, GUAC_RDP_MAX_FILE_DESCRIPTORS); /* Wait for data and construct a reasonable frame */ - int result = WaitForMultipleObjects(num_handles, handles, FALSE, + DWORD result = WaitForMultipleObjects(num_handles, handles, FALSE, timeout_msecs); /* Translate WaitForMultipleObjects() return values */ @@ -448,6 +465,30 @@ static int rdp_guac_client_wait_for_messages(guac_client* client, } +/** + * Handles any queued RDP-related events, including inbound RDP messages that + * have been received, updating the Guacamole display accordingly. + * + * @param rdp_client + * The guac_rdp_client of the RDP connection whose current messages should + * be handled. + * + * @return + * True (non-zero) if messages were handled successfully, false (zero) + * otherwise. + */ +static int guac_rdp_handle_events(guac_rdp_client* rdp_client) { + + /* Actually handle messages (this may result in drawing to the + * guac_display, resizing the display buffer, etc.) */ + pthread_mutex_lock(&(rdp_client->message_lock)); + int retval = freerdp_check_event_handles(GUAC_RDP_CONTEXT(rdp_client->rdp_inst)); + pthread_mutex_unlock(&(rdp_client->message_lock)); + + return retval; + +} + /** * Connects to an RDP server as described by the guac_rdp_settings structure * associated with the given client, allocating and freeing all objects @@ -478,20 +519,29 @@ static int guac_rdp_handle_connection(guac_client* client) { guac_rwlock_acquire_write_lock(&(rdp_client->lock)); /* Create display */ - rdp_client->display = guac_common_display_alloc(client, - rdp_client->settings->width, - rdp_client->settings->height); + rdp_client->display = guac_display_alloc(client); + + guac_display_layer* default_layer = guac_display_default_layer(rdp_client->display); + guac_display_layer_resize(default_layer, rdp_client->settings->width, rdp_client->settings->height); /* Use lossless compression only if requested (otherwise, use default * heuristics) */ - guac_common_display_set_lossless(rdp_client->display, settings->lossless); + guac_display_layer_set_lossless(default_layer, settings->lossless); - rdp_client->current_surface = rdp_client->display->default_surface; + rdp_client->current_surface = default_layer; rdp_client->available_svc = guac_common_list_alloc(); /* Init client */ freerdp* rdp_inst = freerdp_new(); + + /* + * If the freerdp instance has a LoadChannels callback for loading plugins + * we use that instead of the PreConnect callback to load plugins. + */ + #ifdef RDP_INST_HAS_LOAD_CHANNELS + rdp_inst->LoadChannels = rdp_freerdp_load_channels; + #endif rdp_inst->PreConnect = rdp_freerdp_pre_connect; rdp_inst->Authenticate = rdp_freerdp_authenticate; @@ -512,14 +562,14 @@ static int guac_rdp_handle_connection(guac_client* client) { goto fail; } - ((rdp_freerdp_context*) rdp_inst->context)->client = client; + ((rdp_freerdp_context*) GUAC_RDP_CONTEXT(rdp_inst))->client = client; /* Load keymap into client */ rdp_client->keyboard = guac_rdp_keyboard_alloc(client, settings->server_layout); /* Set default pointer */ - guac_common_cursor_set_pointer(rdp_client->display->cursor); + guac_display_set_cursor(rdp_client->display, GUAC_DISPLAY_CURSOR_POINTER); /* * Downgrade the lock to allow for concurrent read access. @@ -542,13 +592,13 @@ static int guac_rdp_handle_connection(guac_client* client) { /* Connection complete */ rdp_client->rdp_inst = rdp_inst; - guac_timestamp last_frame_end = guac_timestamp_current(); - /* Signal that reconnect has been completed */ guac_rdp_disp_reconnect_complete(rdp_client->disp); guac_rwlock_release_lock(&(rdp_client->lock)); + rdp_client->render_thread = guac_display_render_thread_create(rdp_client->display); + /* Handle messages from RDP server while client is running */ while (client->state == GUAC_CLIENT_RUNNING && !guac_rdp_disp_reconnect_needed(rdp_client->disp)) { @@ -557,64 +607,23 @@ static int guac_rdp_handle_connection(guac_client* client) { guac_rdp_disp_update_size(rdp_client->disp, settings, rdp_inst); /* Wait for data and construct a reasonable frame */ - int wait_result = rdp_guac_client_wait_for_messages(client, - GUAC_RDP_FRAME_START_TIMEOUT); - if (wait_result > 0) { - - int processing_lag = guac_client_get_processing_lag(client); - guac_timestamp frame_start = guac_timestamp_current(); - - /* Read server messages until frame is built */ - do { - - guac_timestamp frame_end; - int frame_remaining; - /* Handle any queued FreeRDP events (this may result in RDP - * messages being sent) */ - pthread_mutex_lock(&(rdp_client->message_lock)); - int event_result = freerdp_check_event_handles(rdp_inst->context); - pthread_mutex_unlock(&(rdp_client->message_lock)); - - /* Abort if FreeRDP event handling fails */ - if (!event_result) { - wait_result = -1; - break; - } - - /* Calculate time remaining in frame */ - frame_end = guac_timestamp_current(); - frame_remaining = frame_start + GUAC_RDP_FRAME_DURATION - - frame_end; - - /* Calculate time that client needs to catch up */ - int time_elapsed = frame_end - last_frame_end; - int required_wait = processing_lag - time_elapsed; - - /* Increase the duration of this frame if client is lagging */ - if (required_wait > GUAC_RDP_FRAME_TIMEOUT) - wait_result = rdp_guac_client_wait_for_messages(client, - required_wait); - - /* Wait again if frame remaining */ - else if (frame_remaining > 0) - wait_result = rdp_guac_client_wait_for_messages(client, - GUAC_RDP_FRAME_TIMEOUT); - else - break; - - } while (wait_result > 0); - - /* Record end of frame, excluding server-side rendering time (we - * assume server-side rendering time will be consistent between any - * two subsequent frames, and that this time should thus be - * excluded from the required wait period of the next frame). */ - last_frame_end = frame_start; + int wait_result = rdp_guac_client_wait_for_messages(client, GUAC_RDP_MESSAGE_CHECK_INTERVAL); + if (wait_result < 0) + break; - } + /* Handle any queued FreeRDP events (this may result in RDP messages + * being sent), aborting later if FreeRDP event handling fails */ + if (!guac_rdp_handle_events(rdp_client)) + wait_result = -1; /* Test whether the RDP server is closing the connection */ - int connection_closing = freerdp_shall_disconnect(rdp_inst); + int connection_closing; +#ifdef HAVE_DISCONNECT_CONTEXT + connection_closing = freerdp_shall_disconnect_context(rdp_inst->context); +#else + connection_closing = freerdp_shall_disconnect(rdp_inst); +#endif /* Close connection cleanly if server is disconnecting */ if (connection_closing) @@ -625,13 +634,6 @@ static int guac_rdp_handle_connection(guac_client* client) { guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_UNAVAILABLE, "Connection closed."); - /* Flush frame only if successful */ - else { - guac_common_display_flush(rdp_client->display); - guac_client_end_frame(client); - guac_socket_flush(client->socket); - } - } guac_rwlock_acquire_write_lock(&(rdp_client->lock)); @@ -647,9 +649,26 @@ static int guac_rdp_handle_connection(guac_client* client) { freerdp_disconnect(rdp_inst); pthread_mutex_unlock(&(rdp_client->message_lock)); - /* Clean up FreeRDP internal GDI implementation */ + /* Stop render loop */ + guac_display_render_thread_destroy(rdp_client->render_thread); + rdp_client->render_thread = NULL; + + /* Remove reference to FreeRDP's GDI buffer so that it can be safely freed + * prior to freeing the guac_display */ + guac_display_layer_raw_context* context = guac_display_layer_open_raw(default_layer); + context->buffer = NULL; + guac_display_layer_close_raw(default_layer, context); + + /* Clean up FreeRDP internal GDI implementation (this must be done BEFORE + * freeing the guac_display, as freeing the GDI will free objects like + * rdpPointer that will attempt to free associated guac_display_layer + * instances during cleanup) */ gdi_free(rdp_inst); + /* Free display */ + guac_display_free(rdp_client->display); + rdp_client->display = NULL; + /* Clean up RDP client context */ freerdp_context_free(rdp_inst); @@ -665,10 +684,6 @@ static int guac_rdp_handle_connection(guac_client* client) { guac_rdp_keyboard_free(rdp_client->keyboard); rdp_client->keyboard = NULL; - /* Free display */ - guac_common_display_free(rdp_client->display); - rdp_client->display = NULL; - guac_rwlock_release_lock(&(rdp_client->lock)); /* Client is now disconnected */ @@ -688,19 +703,50 @@ void* guac_rdp_client_thread(void* data) { guac_rdp_client* rdp_client = (guac_rdp_client*) client->data; guac_rdp_settings* settings = rdp_client->settings; - /* If Wake-on-LAN is enabled, try to wake. */ + /* If Wake-on-LAN is enabled, attempt to wake. */ if (settings->wol_send_packet) { - guac_client_log(client, GUAC_LOG_DEBUG, "Sending Wake-on-LAN packet, " - "and pausing for %d seconds.", settings->wol_wait_time); - - /* Send the Wake-on-LAN request. */ - if (guac_wol_wake(settings->wol_mac_addr, settings->wol_broadcast_addr, - settings->wol_udp_port)) + + /** + * If wait time is set, send the wake packet and try to connect to the + * server, failing if the server does not respond. + */ + if (settings->wol_wait_time > 0) { + guac_client_log(client, GUAC_LOG_DEBUG, "Sending Wake-on-LAN packet, " + "and pausing for %d seconds.", settings->wol_wait_time); + + /* char representation of a port should be, at most, 5 digits plus terminator. */ + char* str_port = guac_mem_alloc(6); + if (guac_itoa(str_port, settings->port) < 1) { + guac_client_log(client, GUAC_LOG_ERROR, "Failed to convert port to integer for WOL function."); + guac_mem_free(str_port); + return NULL; + } + + /* Send the Wake-on-LAN request and wait until the server is responsive. */ + if (guac_wol_wake_and_wait(settings->wol_mac_addr, + settings->wol_broadcast_addr, + settings->wol_udp_port, + settings->wol_wait_time, + GUAC_WOL_DEFAULT_CONNECT_RETRIES, + settings->hostname, + (const char *) str_port, + GUAC_WOL_DEFAULT_CONNECTION_TIMEOUT)) { + guac_client_log(client, GUAC_LOG_ERROR, "Failed to send WOL packet, or server failed to wake up."); + guac_mem_free(str_port); + return NULL; + } + + guac_mem_free(str_port); + + } + + /* Just send the packet and continue the connection, or return if failed. */ + else if(guac_wol_wake(settings->wol_mac_addr, + settings->wol_broadcast_addr, + settings->wol_udp_port)) { + guac_client_log(client, GUAC_LOG_ERROR, "Failed to send WOL packet."); return NULL; - - /* If wait time is specified, sleep for that amount of time. */ - if (settings->wol_wait_time > 0) - guac_timestamp_msleep(settings->wol_wait_time * 1000); + } } /* If audio enabled, choose an encoder */ @@ -766,6 +812,33 @@ void* guac_rdp_client_thread(void* data) { return NULL; } + /* Import the public key, if that is specified. */ + if (settings->sftp_public_key != NULL) { + + guac_client_log(client, GUAC_LOG_DEBUG, + "Attempting public key import"); + + /* Attempt to read public key */ + if (guac_common_ssh_user_import_public_key(rdp_client->sftp_user, + settings->sftp_public_key)) { + + /* Public key import fails. */ + guac_client_abort(client, + GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED, + "Failed to import public key: %s", + guac_common_ssh_key_error()); + + guac_common_ssh_destroy_user(rdp_client->sftp_user); + return NULL; + + } + + /* Success */ + guac_client_log(client, GUAC_LOG_INFO, + "Public key successfully imported."); + + } + } /* Otherwise, use specified password */ @@ -782,8 +855,8 @@ void* guac_rdp_client_thread(void* data) { /* Attempt SSH connection */ rdp_client->sftp_session = guac_common_ssh_create_session(client, settings->sftp_hostname, - settings->sftp_port, rdp_client->sftp_user, settings->sftp_server_alive_interval, - settings->sftp_host_key, NULL); + settings->sftp_port, rdp_client->sftp_user, settings->sftp_timeout, + settings->sftp_server_alive_interval, settings->sftp_host_key, NULL); /* Fail if SSH connection does not succeed */ if (rdp_client->sftp_session == NULL) { @@ -831,7 +904,8 @@ void* guac_rdp_client_thread(void* data) { !settings->recording_exclude_output, !settings->recording_exclude_mouse, !settings->recording_exclude_touch, - settings->recording_include_keys); + settings->recording_include_keys, + settings->recording_write_existing); } /* Continue handling connections until error or client disconnect */ diff --git a/src/protocols/rdp/rdp.h b/src/protocols/rdp/rdp.h index 5f0799688..76ca18ca3 100644 --- a/src/protocols/rdp/rdp.h +++ b/src/protocols/rdp/rdp.h @@ -25,9 +25,7 @@ #include "channels/disp.h" #include "channels/rdpei.h" #include "common/clipboard.h" -#include "common/display.h" #include "common/list.h" -#include "common/surface.h" #include "config.h" #include "fs.h" #include "keyboard.h" @@ -42,8 +40,10 @@ #include #include +#include #include #include +#include #include #include #include @@ -51,6 +51,20 @@ #include #include +#ifdef HAVE_WINPR_ALIGNED +#define GUAC_ALIGNED_FREE winpr_aligned_free +#define GUAC_ALIGNED_MALLOC winpr_aligned_malloc +#else +#define GUAC_ALIGNED_FREE _aligned_free +#define GUAC_ALIGNED_MALLOC _aligned_malloc +#endif + +#ifdef FREERDP_HAS_CONTEXT +#define GUAC_RDP_CONTEXT(rdp_instance) ((rdp_instance)->context) +#else +#define GUAC_RDP_CONTEXT(rdp_instance) ((rdp_instance)) +#endif + /** * RDP-specific client data. */ @@ -84,13 +98,27 @@ typedef struct guac_rdp_client { /** * The display. */ - guac_common_display* display; + guac_display* display; /** * The surface that GDI operations should draw to. RDP messages exist which * change this surface to allow drawing to occur off-screen. */ - guac_common_surface* current_surface; + guac_display_layer* current_surface; + + /** + * The current raw context that can be used to draw to Guacamole's default + * layer. This context is obtained prior to FreeRDP manipulation of the GDI + * buffer and closed when FreeRDP is done with the GDI buffer. If no + * drawing to the GDI is currently underway, this will be NULL. + */ + guac_display_layer_raw_context* current_context; + + /** + * The current instance of the guac_display render thread. If the thread + * has not yet been started, this will be NULL. + */ + guac_display_render_thread* render_thread; /** * The current state of the keyboard with respect to the RDP session. @@ -179,6 +207,12 @@ typedef struct guac_rdp_client { */ pthread_mutex_t message_lock; + /** + * A pointer to the RAIL interface provided by the RDP client when rail is + * in use. + */ + RailClientContext* rail_interface; + } guac_rdp_client; /** diff --git a/src/protocols/rdp/settings.c b/src/protocols/rdp/settings.c index fb2fa0f44..f47b1cff8 100644 --- a/src/protocols/rdp/settings.c +++ b/src/protocols/rdp/settings.c @@ -21,6 +21,7 @@ #include "common/defaults.h" #include "common/string.h" #include "config.h" +#include "rdp.h" #include "resolution.h" #include "settings.h" @@ -55,6 +56,7 @@ const char fips_nla_mode_warning[] = ( const char* GUAC_RDP_CLIENT_ARGS[] = { "hostname", "port", + "timeout", GUAC_RDP_ARGV_DOMAIN, GUAC_RDP_ARGV_USERNAME, GUAC_RDP_ARGV_PASSWORD, @@ -77,6 +79,8 @@ const char* GUAC_RDP_CLIENT_ARGS[] = { "server-layout", "security", "ignore-cert", + "cert-tofu", + "cert-fingerprints", "disable-auth", "remote-app", "remote-app-dir", @@ -92,6 +96,7 @@ const char* GUAC_RDP_CLIENT_ARGS[] = { "disable-bitmap-caching", "disable-offscreen-caching", "disable-glyph-caching", + "disable-gfx", "preconnection-id", "preconnection-blob", "timezone", @@ -101,10 +106,12 @@ const char* GUAC_RDP_CLIENT_ARGS[] = { "sftp-hostname", "sftp-host-key", "sftp-port", + "sftp-timeout", "sftp-username", "sftp-password", "sftp-private-key", "sftp-passphrase", + "sftp-public-key", "sftp-directory", "sftp-root-directory", "sftp-server-alive-interval", @@ -119,6 +126,7 @@ const char* GUAC_RDP_CLIENT_ARGS[] = { "recording-exclude-touch", "recording-include-keys", "create-recording-path", + "recording-write-existing", "resize-method", "enable-audio-input", "enable-touch", @@ -159,6 +167,11 @@ enum RDP_ARGS_IDX { */ IDX_PORT, + /** + * The amount of time to wait for the server to respond, in seconds. + */ + IDX_TIMEOUT, + /** * The domain of the user logging in. */ @@ -289,6 +302,21 @@ enum RDP_ARGS_IDX { */ IDX_IGNORE_CERT, + /** + * "true" if a server certificate should be trusted the first time that + * a connection is established, and then subsequently checked for validity, + * or "false" if that behavior should not be forced. Whether or not the + * connection succeeds will be dependent upon other certificate settings, + * like ignore and/or provided fingerprints. + */ + IDX_CERTIFICATE_TOFU, + + /** + * A comma-separate list of fingerprints of certificates that should be + * trusted when establishing this RDP connection. + */ + IDX_CERTIFICATE_FINGERPRINTS, + /** * "true" if authentication should be disabled, "false" or blank otherwise. * This is different from the authentication that takes place when a user @@ -383,6 +411,13 @@ enum RDP_ARGS_IDX { */ IDX_DISABLE_GLYPH_CACHING, + /** + * "true" if the RDP Graphics Pipeline Extension should not be used, and + * traditional RDP graphics should be used instead, "false" or blank if the + * Graphics Pipeline Extension should be used if available. + */ + IDX_DISABLE_GFX, + /** * The preconnection ID to send within the preconnection PDU when * initiating an RDP connection, if any. @@ -428,6 +463,12 @@ enum RDP_ARGS_IDX { */ IDX_SFTP_PORT, + /** + * The number of seconds to attempt to connect to the SSH server before + * timing out. + */ + IDX_SFTP_TIMEOUT, + /** * The username to provide when authenticating with the SSH server for * SFTP. If blank, the username provided for the RDP user will be used. @@ -452,6 +493,12 @@ enum RDP_ARGS_IDX { */ IDX_SFTP_PASSPHRASE, + /** + * The base64-encoded public key to use when authenticating with the SSH + * server for SFTP. + */ + IDX_SFTP_PUBLIC_KEY, + /** * The default location for file uploads within the SSH server. This will * apply only to uploads which do not use the filesystem guac_object (where @@ -539,6 +586,12 @@ enum RDP_ARGS_IDX { */ IDX_CREATE_RECORDING_PATH, + /** + * Whether existing files should be appended to when creating a new recording. + * Disabled by default. + */ + IDX_RECORDING_WRITE_EXISTING, + /** * The method to use to apply screen size changes requested by the user. * Valid values are blank, "display-update", and "reconnect". @@ -701,6 +754,16 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, IDX_IGNORE_CERT, 0); + /* Add new certificates to trust list */ + settings->certificate_tofu = + guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_CERTIFICATE_TOFU, 0); + + /* Fingerprints of certificates that should be trusted */ + settings->certificate_fingerprints = + guac_user_parse_args_string(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_CERTIFICATE_FINGERPRINTS, NULL); + /* Disable authentication */ settings->disable_authentication = guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, @@ -773,6 +836,11 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, guac_user_parse_args_int(user, GUAC_RDP_CLIENT_ARGS, argv, IDX_PORT, settings->security_mode == GUAC_SECURITY_VMCONNECT ? RDP_DEFAULT_VMCONNECT_PORT : RDP_DEFAULT_PORT); + /* Look for timeout settings and parse or set defaults. */ + settings->timeout = + guac_user_parse_args_int(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_TIMEOUT, RDP_DEFAULT_TIMEOUT); + guac_user_log(user, GUAC_LOG_DEBUG, "User resolution is %ix%i at %i DPI", user->info.optimal_width, @@ -935,11 +1003,6 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, GUAC_RDP_CLIENT_ARGS[IDX_DISABLE_GLYPH_CACHING]); } - /* Session color depth */ - settings->color_depth = - guac_user_parse_args_int(user, GUAC_RDP_CLIENT_ARGS, argv, - IDX_COLOR_DEPTH, RDP_DEFAULT_DEPTH); - /* Preconnection ID */ settings->preconnection_id = -1; if (argv[IDX_PRECONNECTION_ID][0] != '\0') { @@ -1049,6 +1112,11 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_RDP_CLIENT_ARGS, argv, IDX_SFTP_PORT, "22"); + /* SFTP timeout */ + settings->sftp_timeout = + guac_user_parse_args_int(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_SFTP_TIMEOUT, RDP_DEFAULT_SFTP_TIMEOUT); + /* Username for SSH/SFTP authentication */ settings->sftp_username = guac_user_parse_args_string(user, GUAC_RDP_CLIENT_ARGS, argv, @@ -1065,11 +1133,16 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_RDP_CLIENT_ARGS, argv, IDX_SFTP_PRIVATE_KEY, NULL); - /* Passphrase for decrypting the SFTP private key (if applicable */ + /* Passphrase for decrypting the SFTP private key (if applicable) */ settings->sftp_passphrase = guac_user_parse_args_string(user, GUAC_RDP_CLIENT_ARGS, argv, IDX_SFTP_PASSPHRASE, ""); + /* Public key for authenticating to SFTP server, if applicable. */ + settings->sftp_public_key = + guac_user_parse_args_string(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_SFTP_PUBLIC_KEY, NULL); + /* Default upload directory */ settings->sftp_directory = guac_user_parse_args_string(user, GUAC_RDP_CLIENT_ARGS, argv, @@ -1131,6 +1204,11 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, IDX_CREATE_RECORDING_PATH, 0); + /* Parse allow write existing file flag */ + settings->recording_write_existing = + guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_RECORDING_WRITE_EXISTING, 0); + /* No resize method */ if (strcmp(argv[IDX_RESIZE_METHOD], "") == 0) { guac_user_log(user, GUAC_LOG_INFO, "Resize method: none"); @@ -1156,6 +1234,16 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user, settings->resize_method = GUAC_RESIZE_NONE; } + /* RDP Graphics Pipeline enable/disable */ + settings->enable_gfx = + !guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_DISABLE_GFX, 0); + + /* Session color depth */ + settings->color_depth = + guac_user_parse_args_int(user, GUAC_RDP_CLIENT_ARGS, argv, + IDX_COLOR_DEPTH, settings->enable_gfx ? RDP_GFX_REQUIRED_DEPTH : RDP_DEFAULT_DEPTH); + /* Multi-touch input enable/disable */ settings->enable_touch = guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv, @@ -1283,6 +1371,7 @@ void guac_rdp_settings_free(guac_rdp_settings* settings) { guac_mem_free(settings->drive_name); guac_mem_free(settings->drive_path); guac_mem_free(settings->hostname); + guac_mem_free(settings->certificate_fingerprints); guac_mem_free(settings->initial_program); guac_mem_free(settings->password); guac_mem_free(settings->preconnection_blob); @@ -1320,6 +1409,7 @@ void guac_rdp_settings_free(guac_rdp_settings* settings) { guac_mem_free(settings->sftp_password); guac_mem_free(settings->sftp_port); guac_mem_free(settings->sftp_private_key); + guac_mem_free(settings->sftp_public_key); guac_mem_free(settings->sftp_username); #endif @@ -1341,18 +1431,6 @@ void guac_rdp_settings_free(guac_rdp_settings* settings) { } -int guac_rdp_get_width(freerdp* rdp) { - return rdp->settings->DesktopWidth; -} - -int guac_rdp_get_height(freerdp* rdp) { - return rdp->settings->DesktopHeight; -} - -int guac_rdp_get_depth(freerdp* rdp) { - return rdp->settings->ColorDepth; -} - /** * Given the settings structure of the Guacamole RDP client, calculates the * standard performance flag value to send to the RDP server. The value of @@ -1398,11 +1476,279 @@ static int guac_rdp_get_performance_flags(guac_rdp_settings* guac_settings) { } +int guac_rdp_get_width(freerdp* rdp) { +#ifdef HAVE_SETTERS_GETTERS + return freerdp_settings_get_uint32(rdp->context->settings, FreeRDP_DesktopWidth); +#else + return rdp->settings->DesktopWidth; +#endif +} + +int guac_rdp_get_height(freerdp* rdp) { +#ifdef HAVE_SETTERS_GETTERS + return freerdp_settings_get_uint32(rdp->context->settings, FreeRDP_DesktopHeight); +#else + return rdp->settings->DesktopHeight; +#endif +} + +int guac_rdp_get_depth(freerdp* rdp) { +#ifdef HAVE_SETTERS_GETTERS + return freerdp_settings_get_uint32(rdp->context->settings, FreeRDP_ColorDepth); +#else + return rdp->settings->ColorDepth; +#endif +} + void guac_rdp_push_settings(guac_client* client, guac_rdp_settings* guac_settings, freerdp* rdp) { - rdpSettings* rdp_settings = rdp->settings; + rdpSettings* rdp_settings = GUAC_RDP_CONTEXT(rdp)->settings; + +#ifdef HAVE_SETTERS_GETTERS + /* Authentication */ + freerdp_settings_set_string(rdp_settings, FreeRDP_Domain, guac_strdup(guac_settings->domain)); + freerdp_settings_set_string(rdp_settings, FreeRDP_Username, guac_strdup(guac_settings->username)); + freerdp_settings_set_string(rdp_settings, FreeRDP_Password, guac_strdup(guac_settings->password)); + + /* Connection */ + freerdp_settings_set_string(rdp_settings, FreeRDP_ServerHostname, guac_strdup(guac_settings->hostname)); + freerdp_settings_set_uint32(rdp_settings, FreeRDP_ServerPort, guac_settings->port); + + /* Session */ + + freerdp_settings_set_uint32(rdp_settings, FreeRDP_DesktopWidth, guac_settings->width); + freerdp_settings_set_uint32(rdp_settings, FreeRDP_DesktopHeight, guac_settings->height); + freerdp_settings_set_uint32(rdp_settings, FreeRDP_ColorDepth, guac_settings->color_depth); + freerdp_settings_set_string(rdp_settings, FreeRDP_AlternateShell, guac_strdup(guac_settings->initial_program)); + freerdp_settings_set_uint32(rdp_settings, FreeRDP_KeyboardLayout, guac_settings->server_layout->freerdp_keyboard_layout); + + + /* Performance flags */ + /* Explicitly set flag value */ + freerdp_settings_set_uint32(rdp_settings, FreeRDP_PerformanceFlags, guac_rdp_get_performance_flags(guac_settings)); + + /* Always request frame markers */ + freerdp_settings_set_bool(rdp_settings, FreeRDP_FrameMarkerCommandEnabled, TRUE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_SurfaceFrameMarkerEnabled, TRUE); + + /* Enable RemoteFX / Graphics Pipeline */ + if (guac_settings->enable_gfx) { + + freerdp_settings_set_bool(rdp_settings, FreeRDP_SupportGraphicsPipeline, TRUE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_RemoteFxCodec, TRUE); + + if (freerdp_settings_get_uint32(rdp_settings, FreeRDP_ColorDepth) != RDP_GFX_REQUIRED_DEPTH) { + guac_client_log(client, GUAC_LOG_WARNING, "Ignoring requested " + "color depth of %i bpp, as the RDP Graphics Pipeline " + "requires %i bpp.", freerdp_settings_get_uint32(rdp_settings, FreeRDP_ColorDepth), RDP_GFX_REQUIRED_DEPTH); + } + + /* Required for RemoteFX / Graphics Pipeline */ + freerdp_settings_set_bool(rdp_settings, FreeRDP_FastPathOutput, TRUE); + freerdp_settings_set_uint32(rdp_settings, FreeRDP_ColorDepth, RDP_GFX_REQUIRED_DEPTH); + freerdp_settings_set_bool(rdp_settings, FreeRDP_SoftwareGdi, TRUE); + + } + + /* Set individual flags - some FreeRDP versions overwrite flags set by guac_rdp_get_performance_flags() above */ + freerdp_settings_set_bool(rdp_settings, FreeRDP_AllowFontSmoothing, guac_settings->font_smoothing_enabled); + freerdp_settings_set_bool(rdp_settings, FreeRDP_DisableWallpaper, guac_settings->wallpaper_enabled); + freerdp_settings_set_bool(rdp_settings, FreeRDP_DisableFullWindowDrag, guac_settings->full_window_drag_enabled); + freerdp_settings_set_bool(rdp_settings, FreeRDP_DisableMenuAnims, guac_settings->menu_animations_enabled); + freerdp_settings_set_bool(rdp_settings, FreeRDP_DisableThemes, guac_settings->theming_enabled); + freerdp_settings_set_bool(rdp_settings, FreeRDP_AllowDesktopComposition, guac_settings->desktop_composition_enabled); + + /* Client name */ + if (guac_settings->client_name != NULL) { + freerdp_settings_set_string(rdp_settings, FreeRDP_ClientHostname, + guac_strndup(guac_settings->client_name, RDP_CLIENT_HOSTNAME_SIZE)); + } + + /* Console */ + freerdp_settings_set_bool(rdp_settings, FreeRDP_ConsoleSession, guac_settings->console); + freerdp_settings_set_bool(rdp_settings, FreeRDP_RemoteConsoleAudio, guac_settings->console_audio); + + /* Audio */ + freerdp_settings_set_bool(rdp_settings, FreeRDP_AudioPlayback, guac_settings->audio_enabled); + + /* Audio capture */ + freerdp_settings_set_bool(rdp_settings, FreeRDP_AudioCapture, guac_settings->enable_audio_input); + + /* Display Update channel */ + freerdp_settings_set_bool(rdp_settings, FreeRDP_SupportDisplayControl, + (guac_settings->resize_method == GUAC_RESIZE_DISPLAY_UPDATE)); + + /* Timezone redirection */ + if (guac_settings->timezone) { + if (setenv("TZ", guac_settings->timezone, 1)) { + guac_client_log(client, GUAC_LOG_WARNING, + "Unable to forward timezone: TZ environment variable " + "could not be set: %s", strerror(errno)); + } + } + + /* Device redirection */ + freerdp_settings_set_bool(rdp_settings, FreeRDP_DeviceRedirection, + (guac_settings->audio_enabled || guac_settings->drive_enabled || guac_settings->printing_enabled)); + + /* Security */ + switch (guac_settings->security_mode) { + + /* Legacy RDP encryption */ + case GUAC_SECURITY_RDP: + freerdp_settings_set_bool(rdp_settings, FreeRDP_RdpSecurity, TRUE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_TlsSecurity, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_NlaSecurity, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_ExtSecurity, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_UseRdpSecurityLayer, TRUE); + freerdp_settings_set_uint32(rdp_settings, FreeRDP_EncryptionLevel, + ENCRYPTION_LEVEL_CLIENT_COMPATIBLE); + freerdp_settings_set_uint32(rdp_settings, FreeRDP_EncryptionMethods, + ENCRYPTION_METHOD_40BIT | ENCRYPTION_METHOD_128BIT | ENCRYPTION_METHOD_FIPS); + break; + + /* TLS encryption */ + case GUAC_SECURITY_TLS: + freerdp_settings_set_bool(rdp_settings, FreeRDP_RdpSecurity, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_TlsSecurity, TRUE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_NlaSecurity, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_ExtSecurity, FALSE); + break; + + /* Network level authentication */ + case GUAC_SECURITY_NLA: + freerdp_settings_set_bool(rdp_settings, FreeRDP_RdpSecurity, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_TlsSecurity, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_NlaSecurity, TRUE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_ExtSecurity, FALSE); + break; + + /* Extended network level authentication */ + case GUAC_SECURITY_EXTENDED_NLA: + freerdp_settings_set_bool(rdp_settings, FreeRDP_RdpSecurity, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_TlsSecurity, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_NlaSecurity, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_ExtSecurity, TRUE); + break; + + /* Hyper-V "VMConnect" negotiation mode */ + case GUAC_SECURITY_VMCONNECT: + freerdp_settings_set_bool(rdp_settings, FreeRDP_RdpSecurity, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_TlsSecurity, TRUE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_NlaSecurity, TRUE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_ExtSecurity, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_VmConnectMode, TRUE); + break; + + /* All security types */ + case GUAC_SECURITY_ANY: + freerdp_settings_set_bool(rdp_settings, FreeRDP_RdpSecurity, TRUE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_TlsSecurity, TRUE); + + /* Explicitly disable NLA if FIPS mode is enabled - it won't work */ + if (guac_fips_enabled()) { + + guac_client_log(client, GUAC_LOG_INFO, + "FIPS mode is enabled. Excluding NLA security mode from security negotiation " + "(see: https://github.com/FreeRDP/FreeRDP/issues/3412)."); + freerdp_settings_set_bool(rdp_settings, FreeRDP_NlaSecurity, FALSE); + + } + + /* NLA mode is allowed if FIPS is not enabled */ + else + freerdp_settings_set_bool(rdp_settings, FreeRDP_NlaSecurity, TRUE); + + freerdp_settings_set_bool(rdp_settings, FreeRDP_ExtSecurity, FALSE); + break; + + } + + /* Security */ + freerdp_settings_set_bool(rdp_settings, FreeRDP_Authentication, !guac_settings->disable_authentication); + freerdp_settings_set_bool(rdp_settings, FreeRDP_IgnoreCertificate, guac_settings->ignore_certificate); + freerdp_settings_set_bool(rdp_settings, FreeRDP_AutoAcceptCertificate, guac_settings->certificate_tofu); + if (guac_settings->certificate_fingerprints != NULL) + freerdp_settings_set_string(rdp_settings, FreeRDP_CertificateAcceptedFingerprints, + guac_strdup(guac_settings->certificate_fingerprints)); + + + /* RemoteApp */ + if (guac_settings->remote_app != NULL) { + freerdp_settings_set_bool(rdp_settings, FreeRDP_Workarea, TRUE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_RemoteApplicationMode, TRUE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_RemoteAppLanguageBarSupported, TRUE); + freerdp_settings_set_string(rdp_settings, FreeRDP_RemoteApplicationProgram, guac_strdup(guac_settings->remote_app)); + freerdp_settings_set_string(rdp_settings, FreeRDP_ShellWorkingDirectory, guac_strdup(guac_settings->remote_app_dir)); + freerdp_settings_set_string(rdp_settings, FreeRDP_RemoteApplicationCmdLine, guac_strdup(guac_settings->remote_app_args)); + } + + /* Preconnection ID */ + if (guac_settings->preconnection_id != -1) { + freerdp_settings_set_bool(rdp_settings, FreeRDP_NegotiateSecurityLayer, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_SendPreconnectionPdu, TRUE); + freerdp_settings_set_uint32(rdp_settings, FreeRDP_PreconnectionId, guac_settings->preconnection_id); + } + + /* Preconnection BLOB */ + if (guac_settings->preconnection_blob != NULL) { + freerdp_settings_set_bool(rdp_settings, FreeRDP_NegotiateSecurityLayer, FALSE); + freerdp_settings_set_bool(rdp_settings, FreeRDP_SendPreconnectionPdu, TRUE); + freerdp_settings_set_string(rdp_settings, FreeRDP_PreconnectionBlob, guac_strdup(guac_settings->preconnection_blob)); + } + + /* Enable use of RD gateway if a gateway hostname is provided */ + if (guac_settings->gateway_hostname != NULL) { + + /* Enable RD gateway */ + freerdp_settings_set_bool(rdp_settings, FreeRDP_GatewayEnabled, TRUE); + + /* RD gateway connection details */ + freerdp_settings_set_string(rdp_settings, FreeRDP_GatewayHostname, guac_strdup(guac_settings->gateway_hostname)); + freerdp_settings_set_uint32(rdp_settings, FreeRDP_GatewayPort, guac_settings->gateway_port); + + /* RD gateway credentials */ + freerdp_settings_set_bool(rdp_settings, FreeRDP_GatewayUseSameCredentials, FALSE); + freerdp_settings_set_string(rdp_settings, FreeRDP_GatewayDomain, guac_strdup(guac_settings->gateway_domain)); + freerdp_settings_set_string(rdp_settings, FreeRDP_GatewayUsername, guac_strdup(guac_settings->gateway_username)); + freerdp_settings_set_string(rdp_settings, FreeRDP_GatewayPassword, guac_strdup(guac_settings->gateway_password)); + + } + /* Store load balance info (and calculate length) if provided */ + if (guac_settings->load_balance_info != NULL) { + freerdp_settings_set_pointer(rdp_settings, FreeRDP_LoadBalanceInfo, (BYTE*) guac_strdup(guac_settings->load_balance_info)); + freerdp_settings_set_uint32(rdp_settings, FreeRDP_LoadBalanceInfoLength, strlen(guac_settings->load_balance_info)); + } + + freerdp_settings_set_bool(rdp_settings, FreeRDP_BitmapCacheEnabled, !guac_settings->disable_bitmap_caching); + freerdp_settings_set_uint32(rdp_settings, FreeRDP_OffscreenSupportLevel, !guac_settings->disable_offscreen_caching); + freerdp_settings_set_uint32(rdp_settings, FreeRDP_GlyphSupportLevel, + (!guac_settings->disable_glyph_caching ? GLYPH_SUPPORT_FULL : GLYPH_SUPPORT_NONE)); + freerdp_settings_set_uint32(rdp_settings, FreeRDP_OsMajorType, OSMAJORTYPE_UNSPECIFIED); + freerdp_settings_set_uint32(rdp_settings, FreeRDP_OsMinorType, OSMINORTYPE_UNSPECIFIED); + freerdp_settings_set_bool(rdp_settings, FreeRDP_DesktopResize, TRUE); + + /* Claim support only for specific updates, independent of FreeRDP defaults */ + BYTE* order_support = freerdp_settings_get_pointer_writable(rdp_settings, FreeRDP_OrderSupport); + if (order_support) { + ZeroMemory(order_support, GUAC_RDP_ORDER_SUPPORT_LENGTH); + order_support[NEG_DSTBLT_INDEX] = TRUE; + order_support[NEG_SCRBLT_INDEX] = TRUE; + order_support[NEG_MEMBLT_INDEX] = !guac_settings->disable_bitmap_caching; + order_support[NEG_MEMBLT_V2_INDEX] = !guac_settings->disable_bitmap_caching; + order_support[NEG_GLYPH_INDEX_INDEX] = !guac_settings->disable_glyph_caching; + order_support[NEG_FAST_INDEX_INDEX] = !guac_settings->disable_glyph_caching; + order_support[NEG_FAST_GLYPH_INDEX] = !guac_settings->disable_glyph_caching; + } + +#ifdef HAVE_RDPSETTINGS_ALLOWUNANOUNCEDORDERSFROMSERVER + /* Do not consider server use of unannounced orders to be a fatal error */ + freerdp_settings_set_bool(rdp_settings, FreeRDP_AllowUnanouncedOrdersFromServer, TRUE); +#endif + +#else /* Authentication */ rdp_settings->Domain = guac_strdup(guac_settings->domain); rdp_settings->Username = guac_strdup(guac_settings->username); @@ -1411,6 +1757,7 @@ void guac_rdp_push_settings(guac_client* client, /* Connection */ rdp_settings->ServerHostname = guac_strdup(guac_settings->hostname); rdp_settings->ServerPort = guac_settings->port; + rdp_settings->TcpAckTimeout = guac_settings->timeout * 1000; /* Session */ rdp_settings->ColorDepth = guac_settings->color_depth; @@ -1423,7 +1770,34 @@ void guac_rdp_push_settings(guac_client* client, /* Explicitly set flag value */ rdp_settings->PerformanceFlags = guac_rdp_get_performance_flags(guac_settings); - /* Set individual flags - some FreeRDP versions overwrite the above */ + /* Always request frame markers */ + rdp_settings->FrameMarkerCommandEnabled = TRUE; + rdp_settings->SurfaceFrameMarkerEnabled = TRUE; + + /* Always handle input events asynchronously (rather than synchronously + * with the rest of FreeRDP's event loop, including graphics) */ + rdp_settings->AsyncInput = TRUE; + + /* Enable RemoteFX / Graphics Pipeline */ + if (guac_settings->enable_gfx) { + + rdp_settings->SupportGraphicsPipeline = TRUE; + rdp_settings->RemoteFxCodec = TRUE; + + if (rdp_settings->ColorDepth != RDP_GFX_REQUIRED_DEPTH) { + guac_client_log(client, GUAC_LOG_WARNING, "Ignoring requested " + "color depth of %i bpp, as the RDP Graphics Pipeline " + "requires %i bpp.", rdp_settings->ColorDepth, RDP_GFX_REQUIRED_DEPTH); + } + + /* Required for RemoteFX / Graphics Pipeline */ + rdp_settings->FastPathOutput = TRUE; + rdp_settings->ColorDepth = RDP_GFX_REQUIRED_DEPTH; + rdp_settings->SoftwareGdi = TRUE; + + } + + /* Set individual flags - some FreeRDP versions overwrite flags set by guac_rdp_get_performance_flags() above */ rdp_settings->AllowFontSmoothing = guac_settings->font_smoothing_enabled; rdp_settings->DisableWallpaper = !guac_settings->wallpaper_enabled; rdp_settings->DisableFullWindowDrag = !guac_settings->full_window_drag_enabled; @@ -1433,7 +1807,8 @@ void guac_rdp_push_settings(guac_client* client, /* Client name */ if (guac_settings->client_name != NULL) { - guac_strlcpy(rdp_settings->ClientHostname, guac_settings->client_name, + free(rdp_settings->ClientHostname); + rdp_settings->ClientHostname = guac_strndup(guac_settings->client_name, RDP_CLIENT_HOSTNAME_SIZE); } @@ -1539,9 +1914,12 @@ void guac_rdp_push_settings(guac_client* client, } - /* Authentication */ + /* Security */ rdp_settings->Authentication = !guac_settings->disable_authentication; rdp_settings->IgnoreCertificate = guac_settings->ignore_certificate; + rdp_settings->AutoAcceptCertificate = guac_settings->certificate_tofu; + if (guac_settings->certificate_fingerprints != NULL) + rdp_settings->CertificateAcceptedFingerprints = guac_strdup(guac_settings->certificate_fingerprints); /* RemoteApp */ if (guac_settings->remote_app != NULL) { @@ -1613,5 +1991,5 @@ void guac_rdp_push_settings(guac_client* client, rdp_settings->AllowUnanouncedOrdersFromServer = TRUE; #endif +#endif } - diff --git a/src/protocols/rdp/settings.h b/src/protocols/rdp/settings.h index daaf3e9c9..253b7628b 100644 --- a/src/protocols/rdp/settings.h +++ b/src/protocols/rdp/settings.h @@ -33,11 +33,21 @@ */ #define RDP_CLIENT_HOSTNAME_SIZE 32 +/** + * The default server response timeout, in seconds. + */ +#define RDP_DEFAULT_TIMEOUT 10 + /** * The default RDP port. */ #define RDP_DEFAULT_PORT 3389 +/** + * The default SFTP connection timeout, in seconds. + */ +#define RDP_DEFAULT_SFTP_TIMEOUT 10 + /** * The default RDP port used by Hyper-V "VMConnect". */ @@ -58,6 +68,11 @@ */ #define RDP_DEFAULT_DEPTH 16 +/** + * The color depth required by the RDPGFX channel, in bits. + */ +#define RDP_GFX_REQUIRED_DEPTH 32 + /** * The filename to use for the screen recording, if not specified. */ @@ -151,6 +166,11 @@ typedef struct guac_rdp_settings { */ int port; + /** + * The timeout, in seconds, to wait for the remote host to respond. + */ + int timeout; + /** * The domain of the user logging in. */ @@ -281,6 +301,18 @@ typedef struct guac_rdp_settings { */ int ignore_certificate; + /** + * Whether or not a certificate should be added to the local trust + * store on first use. + */ + int certificate_tofu; + + /** + * The fingerprints of host certificates that should be trusted for + * this connection. + */ + char* certificate_fingerprints; + /** * Whether authentication should be disabled. This is different from the * authentication that takes place when a user provides their username @@ -435,6 +467,12 @@ typedef struct guac_rdp_settings { */ char* sftp_port; + /** + * The number of seconds to attempt to connect to the SSH server before + * timing out. + */ + int sftp_timeout; + /** * The username to provide when authenticating with the SSH server for * SFTP. @@ -459,6 +497,11 @@ typedef struct guac_rdp_settings { */ char* sftp_passphrase; + /** + * The public key to use when connecting to the SFTP server, if applicable. + */ + char* sftp_public_key; + /** * The default location for file uploads within the SSH server. This will * apply only to uploads which do not use the filesystem guac_object (where @@ -543,6 +586,12 @@ typedef struct guac_rdp_settings { int recording_include_keys; /** + * Non-zero if existing files should be appended to when creating a new + * recording. Disabled by default. + */ + int recording_write_existing; + + /** * The method to apply when the user's display changes size. */ guac_rdp_resize_method resize_method; @@ -552,6 +601,11 @@ typedef struct guac_rdp_settings { */ int enable_audio_input; + /** + * Whether the RDP Graphics Pipeline Extension is enabled. + */ + int enable_gfx; + /** * Whether multi-touch support is enabled. */ diff --git a/src/protocols/rdp/user.c b/src/protocols/rdp/user.c index 5381a9822..5e115ebfd 100644 --- a/src/protocols/rdp/user.c +++ b/src/protocols/rdp/user.c @@ -20,8 +20,6 @@ #include "channels/audio-input/audio-input.h" #include "channels/cliprdr.h" #include "channels/pipe-svc.h" -#include "common/cursor.h" -#include "common/display.h" #include "config.h" #include "input.h" #include "rdp.h" @@ -36,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -151,7 +150,7 @@ int guac_rdp_user_leave_handler(guac_user* user) { /* Update shared cursor state if the display still exists */ if (rdp_client->display != NULL) - guac_common_cursor_remove_user(rdp_client->display->cursor, user); + guac_display_notify_user_left(rdp_client->display, user); /* Free settings if not owner (owner settings will be freed with client) */ if (!user->owner) { diff --git a/src/protocols/ssh/input.c b/src/protocols/ssh/input.c index ba8867b47..f60e1c9fa 100644 --- a/src/protocols/ssh/input.c +++ b/src/protocols/ssh/input.c @@ -19,8 +19,6 @@ #include "config.h" -#include "common/cursor.h" -#include "common/display.h" #include "ssh.h" #include "terminal/terminal.h" diff --git a/src/protocols/ssh/settings.c b/src/protocols/ssh/settings.c index ea890cec7..48e7383bd 100644 --- a/src/protocols/ssh/settings.c +++ b/src/protocols/ssh/settings.c @@ -38,6 +38,7 @@ const char* GUAC_SSH_CLIENT_ARGS[] = { "hostname", "host-key", "port", + "timeout", "username", "password", GUAC_SSH_ARGV_FONT_NAME, @@ -48,6 +49,7 @@ const char* GUAC_SSH_CLIENT_ARGS[] = { "sftp-disable-upload", "private-key", "passphrase", + "public-key", #ifdef ENABLE_SSH_AGENT "enable-agent", #endif @@ -56,12 +58,14 @@ const char* GUAC_SSH_CLIENT_ARGS[] = { "typescript-path", "typescript-name", "create-typescript-path", + "typescript-write-existing", "recording-path", "recording-name", "recording-exclude-output", "recording-exclude-mouse", "recording-include-keys", "create-recording-path", + "recording-write-existing", "read-only", "server-alive-interval", "backspace", @@ -96,6 +100,11 @@ enum SSH_ARGS_IDX { */ IDX_PORT, + /** + * The timeout of the connection attempt, in seconds. Optional. + */ + IDX_TIMEOUT, + /** * The name of the user to login as. Optional. */ @@ -149,6 +158,11 @@ enum SSH_ARGS_IDX { */ IDX_PASSPHRASE, + /** + * The public key to use for authentication, if any. + */ + IDX_PUBLIC_KEY, + #ifdef ENABLE_SSH_AGENT /** * Whether SSH agent forwarding support should be enabled. @@ -191,6 +205,12 @@ enum SSH_ARGS_IDX { */ IDX_CREATE_TYPESCRIPT_PATH, + /** + * Whether existing files should be appended to when creating a new + * typescript. Disabled by default. + */ + IDX_TYPESCRIPT_WRITE_EXISTING, + /** * The full absolute path to the directory in which screen recordings * should be written. @@ -235,6 +255,12 @@ enum SSH_ARGS_IDX { */ IDX_CREATE_RECORDING_PATH, + /** + * Whether existing files should be appended to when creating a new recording. + * Disabled by default. + */ + IDX_RECORDING_WRITE_EXISTING, + /** * "true" if this connection should be read-only (user input should be * dropped), "false" or blank otherwise. @@ -374,6 +400,10 @@ guac_ssh_settings* guac_ssh_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_SSH_CLIENT_ARGS, argv, IDX_PASSPHRASE, NULL); + settings->public_key_base64 = + guac_user_parse_args_string(user, GUAC_SSH_CLIENT_ARGS, argv, + IDX_PUBLIC_KEY, NULL); + /* Read maximum scrollback size */ settings->max_scrollback = guac_user_parse_args_int(user, GUAC_SSH_CLIENT_ARGS, argv, @@ -430,6 +460,11 @@ guac_ssh_settings* guac_ssh_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_SSH_CLIENT_ARGS, argv, IDX_PORT, GUAC_SSH_DEFAULT_PORT); + /* Parse the timeout value. */ + settings->timeout = + guac_user_parse_args_int(user, GUAC_SSH_CLIENT_ARGS, argv, + IDX_TIMEOUT, GUAC_SSH_DEFAULT_TIMEOUT); + /* Read-only mode */ settings->read_only = guac_user_parse_args_boolean(user, GUAC_SSH_CLIENT_ARGS, argv, @@ -455,6 +490,11 @@ guac_ssh_settings* guac_ssh_parse_args(guac_user* user, guac_user_parse_args_boolean(user, GUAC_SSH_CLIENT_ARGS, argv, IDX_CREATE_TYPESCRIPT_PATH, false); + /* Parse allow write existing file flag */ + settings->typescript_write_existing = + guac_user_parse_args_boolean(user, GUAC_SSH_CLIENT_ARGS, argv, + IDX_TYPESCRIPT_WRITE_EXISTING, false); + /* Read recording path */ settings->recording_path = guac_user_parse_args_string(user, GUAC_SSH_CLIENT_ARGS, argv, @@ -485,6 +525,11 @@ guac_ssh_settings* guac_ssh_parse_args(guac_user* user, guac_user_parse_args_boolean(user, GUAC_SSH_CLIENT_ARGS, argv, IDX_CREATE_RECORDING_PATH, false); + /* Parse allow write existing file flag */ + settings->recording_write_existing = + guac_user_parse_args_boolean(user, GUAC_SSH_CLIENT_ARGS, argv, + IDX_RECORDING_WRITE_EXISTING, false); + /* Parse server alive interval */ settings->server_alive_interval = guac_user_parse_args_int(user, GUAC_SSH_CLIENT_ARGS, argv, @@ -568,6 +613,7 @@ void guac_ssh_settings_free(guac_ssh_settings* settings) { guac_mem_free(settings->password); guac_mem_free(settings->key_base64); guac_mem_free(settings->key_passphrase); + guac_mem_free(settings->public_key_base64); /* Free display preferences */ guac_mem_free(settings->font_name); diff --git a/src/protocols/ssh/settings.h b/src/protocols/ssh/settings.h index 103ff0293..654046183 100644 --- a/src/protocols/ssh/settings.h +++ b/src/protocols/ssh/settings.h @@ -32,6 +32,12 @@ */ #define GUAC_SSH_DEFAULT_PORT "22" +/** + * The default number of seconds to attempt a connection to the SSH/SFTP + * server before giving up. + */ +#define GUAC_SSH_DEFAULT_TIMEOUT 10 + /** * The filename to use for the typescript, if not specified. */ @@ -69,6 +75,12 @@ typedef struct guac_ssh_settings { */ char* port; + /** + * The number of seconds to attempt to connect to the SSH server before + * timing out. + */ + int timeout; + /** * The name of the user to login as, if any. If no username is specified, * this will be NULL. @@ -93,6 +105,12 @@ typedef struct guac_ssh_settings { */ char* key_passphrase; + /** + * The public key, encoded as base64, if any. If no public key is specified, + * this will be NULL. + */ + char* public_key_base64; + /** * Whether this connection is read-only, and user input should be dropped. */ @@ -202,6 +220,12 @@ typedef struct guac_ssh_settings { */ bool create_typescript_path; + /** + * Whether existing files should be appended to when creating a new + * typescript. Disabled by default. + */ + bool typescript_write_existing; + /** * The path in which the screen recording should be saved, if enabled. If * no screen recording should be saved, this will be NULL. @@ -245,6 +269,12 @@ typedef struct guac_ssh_settings { */ bool recording_include_keys; + /** + * Whether existing files should be appended to when creating a new recording. + * Disabled by default. + */ + bool recording_write_existing; + /** * The number of seconds between sending server alive messages. */ diff --git a/src/protocols/ssh/ssh.c b/src/protocols/ssh/ssh.c index 7c3ac05cd..199064de6 100644 --- a/src/protocols/ssh/ssh.c +++ b/src/protocols/ssh/ssh.c @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -133,6 +134,33 @@ static guac_common_ssh_user* guac_ssh_get_user(guac_client* client) { guac_client_log(client, GUAC_LOG_INFO, "Auth key successfully imported."); + /* Import public key, if available. */ + if (settings->public_key_base64 != NULL) { + + guac_client_log(client, GUAC_LOG_DEBUG, + "Attempting public key import"); + + /* Attempt to read public key */ + if (guac_common_ssh_user_import_public_key(user, + settings->public_key_base64)) { + + /* Public key import fails. */ + guac_client_abort(client, + GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED, + "Auth public key import failed: %s", + guac_common_ssh_key_error()); + + guac_common_ssh_destroy_user(user); + return NULL; + + } + + /* Success */ + guac_client_log(client, GUAC_LOG_INFO, + "Auth public key successfully imported."); + + } + } /* end if key given */ /* If available, get password from settings */ @@ -207,17 +235,36 @@ void* ssh_client_thread(void* data) { /* If Wake-on-LAN is enabled, attempt to wake. */ if (settings->wol_send_packet) { - guac_client_log(client, GUAC_LOG_DEBUG, "Sending Wake-on-LAN packet, " - "and pausing for %d seconds.", settings->wol_wait_time); - /* Send the Wake-on-LAN request. */ - if (guac_wol_wake(settings->wol_mac_addr, settings->wol_broadcast_addr, - settings->wol_udp_port)) - return NULL; + /** + * If wait time is set, send the wake packet and try to connect to the + * server, failing if the server does not respond. + */ + if (settings->wol_wait_time > 0) { + guac_client_log(client, GUAC_LOG_DEBUG, "Sending Wake-on-LAN packet, " + "and pausing for %d seconds.", settings->wol_wait_time); + + /* Send the Wake-on-LAN request and wait until the server is responsive. */ + if (guac_wol_wake_and_wait(settings->wol_mac_addr, + settings->wol_broadcast_addr, + settings->wol_udp_port, + settings->wol_wait_time, + GUAC_WOL_DEFAULT_CONNECT_RETRIES, + settings->hostname, + settings->port, + settings->timeout)) { + guac_client_log(client, GUAC_LOG_ERROR, "Failed to send WOL packet or connect to remote server."); + return NULL; + } + } - /* If wait time is specified, sleep for that amount of time. */ - if (settings->wol_wait_time > 0) - guac_timestamp_msleep(settings->wol_wait_time * 1000); + /* Just send the packet and continue the connection, or return if failed. */ + else if(guac_wol_wake(settings->wol_mac_addr, + settings->wol_broadcast_addr, + settings->wol_udp_port)) { + guac_client_log(client, GUAC_LOG_ERROR, "Failed to send WOL packet."); + return NULL; + } } /* Init SSH base libraries */ @@ -238,7 +285,8 @@ void* ssh_client_thread(void* data) { !settings->recording_exclude_output, !settings->recording_exclude_mouse, 0, /* Touch events not supported */ - settings->recording_include_keys); + settings->recording_include_keys, + settings->recording_write_existing); } /* Create terminal options with required parameters */ @@ -274,7 +322,8 @@ void* ssh_client_thread(void* data) { guac_terminal_create_typescript(ssh_client->term, settings->typescript_path, settings->typescript_name, - settings->create_typescript_path); + settings->create_typescript_path, + settings->typescript_write_existing); } /* Get user and credentials */ @@ -289,7 +338,8 @@ void* ssh_client_thread(void* data) { /* Open SSH session */ ssh_client->session = guac_common_ssh_create_session(client, - settings->hostname, settings->port, ssh_client->user, settings->server_alive_interval, + settings->hostname, settings->port, ssh_client->user, + settings->timeout, settings->server_alive_interval, settings->host_key, guac_ssh_get_credential); if (ssh_client->session == NULL) { /* Already aborted within guac_common_ssh_create_session() */ @@ -340,8 +390,8 @@ void* ssh_client_thread(void* data) { guac_client_log(client, GUAC_LOG_DEBUG, "Reconnecting for SFTP..."); ssh_client->sftp_session = guac_common_ssh_create_session(client, settings->hostname, - settings->port, ssh_client->user, settings->server_alive_interval, - settings->host_key, NULL); + settings->port, ssh_client->user, settings->timeout, + settings->server_alive_interval, settings->host_key, NULL); if (ssh_client->sftp_session == NULL) { /* Already aborted within guac_common_ssh_create_session() */ return NULL; diff --git a/src/protocols/ssh/user.c b/src/protocols/ssh/user.c index eff0d8b5d..30b8fe593 100644 --- a/src/protocols/ssh/user.c +++ b/src/protocols/ssh/user.c @@ -21,7 +21,6 @@ #include "argv.h" #include "clipboard.h" -#include "common/display.h" #include "input.h" #include "user.h" #include "pipe.h" diff --git a/src/protocols/telnet/settings.c b/src/protocols/telnet/settings.c index 11bddbb22..e55356948 100644 --- a/src/protocols/telnet/settings.c +++ b/src/protocols/telnet/settings.c @@ -38,6 +38,7 @@ const char* GUAC_TELNET_CLIENT_ARGS[] = { "hostname", "port", + "timeout", "username", "username-regex", "password", @@ -48,12 +49,14 @@ const char* GUAC_TELNET_CLIENT_ARGS[] = { "typescript-path", "typescript-name", "create-typescript-path", + "typescript-write-existing", "recording-path", "recording-name", "recording-exclude-output", "recording-exclude-mouse", "recording-include-keys", "create-recording-path", + "recording-write-existing", "read-only", "backspace", "terminal-type", @@ -82,6 +85,11 @@ enum TELNET_ARGS_IDX { */ IDX_PORT, + /** + * The number of seconds to wait for the remote server to respond. Optional. + */ + IDX_TIMEOUT, + /** * The name of the user to login as. Optional. */ @@ -143,6 +151,12 @@ enum TELNET_ARGS_IDX { */ IDX_CREATE_TYPESCRIPT_PATH, + /** + * Whether existing files should be appended to when creating a new + * typescript. Disabled by default. + */ + IDX_TYPESCRIPT_WRITE_EXISTING, + /** * The full absolute path to the directory in which screen recordings * should be written. @@ -187,6 +201,12 @@ enum TELNET_ARGS_IDX { */ IDX_CREATE_RECORDING_PATH, + /** + * Whether existing files should be appended to when creating a new recording. + * Disabled by default. + */ + IDX_RECORDING_WRITE_EXISTING, + /** * "true" if this connection should be read-only (user input should be * dropped), "false" or blank otherwise. @@ -430,6 +450,11 @@ guac_telnet_settings* guac_telnet_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_TELNET_CLIENT_ARGS, argv, IDX_PORT, GUAC_TELNET_DEFAULT_PORT); + /* Read connection timeout */ + settings->timeout = + guac_user_parse_args_int(user, GUAC_TELNET_CLIENT_ARGS, argv, + IDX_TIMEOUT, GUAC_TELNET_DEFAULT_TIMEOUT); + /* Read typescript path */ settings->typescript_path = guac_user_parse_args_string(user, GUAC_TELNET_CLIENT_ARGS, argv, @@ -445,6 +470,11 @@ guac_telnet_settings* guac_telnet_parse_args(guac_user* user, guac_user_parse_args_boolean(user, GUAC_TELNET_CLIENT_ARGS, argv, IDX_CREATE_TYPESCRIPT_PATH, false); + /* Parse allow write existing file flag */ + settings->typescript_write_existing = + guac_user_parse_args_boolean(user, GUAC_TELNET_CLIENT_ARGS, argv, + IDX_TYPESCRIPT_WRITE_EXISTING, false); + /* Read recording path */ settings->recording_path = guac_user_parse_args_string(user, GUAC_TELNET_CLIENT_ARGS, argv, @@ -475,6 +505,11 @@ guac_telnet_settings* guac_telnet_parse_args(guac_user* user, guac_user_parse_args_boolean(user, GUAC_TELNET_CLIENT_ARGS, argv, IDX_CREATE_RECORDING_PATH, false); + /* Parse allow write existing file flag */ + settings->recording_write_existing = + guac_user_parse_args_boolean(user, GUAC_TELNET_CLIENT_ARGS, argv, + IDX_RECORDING_WRITE_EXISTING, false); + /* Parse backspace key code */ settings->backspace = guac_user_parse_args_int(user, GUAC_TELNET_CLIENT_ARGS, argv, diff --git a/src/protocols/telnet/settings.h b/src/protocols/telnet/settings.h index 3c5ba8b94..a28e440fe 100644 --- a/src/protocols/telnet/settings.h +++ b/src/protocols/telnet/settings.h @@ -33,6 +33,12 @@ */ #define GUAC_TELNET_DEFAULT_PORT "23" +/** + * The default number of seconds to wait for a successful connection before + * timing out. + */ +#define GUAC_TELNET_DEFAULT_TIMEOUT 10 + /** * The filename to use for the typescript, if not specified. */ @@ -72,6 +78,11 @@ typedef struct guac_telnet_settings { */ char* port; + /** + * The number of seconds to wait for a connection before timing out. + */ + int timeout; + /** * The name of the user to login as, if any. If no username is specified, * this will be NULL. @@ -185,6 +196,12 @@ typedef struct guac_telnet_settings { */ bool create_typescript_path; + /** + * Whether existing files should be appended to when creating a new + * typescript. Disabled by default. + */ + bool typescript_write_existing; + /** * The path in which the screen recording should be saved, if enabled. If * no screen recording should be saved, this will be NULL. @@ -228,6 +245,12 @@ typedef struct guac_telnet_settings { */ bool recording_include_keys; + /** + * Whether existing files should be appended to when creating a new recording. + * Disabled by default. + */ + bool recording_write_existing; + /** * The ASCII code, as an integer, that the telnet client will use when the * backspace key is pressed. By default, this is 127, ASCII delete, if diff --git a/src/protocols/telnet/telnet.c b/src/protocols/telnet/telnet.c index 13e1b21e5..0ddcd9fc8 100644 --- a/src/protocols/telnet/telnet.c +++ b/src/protocols/telnet/telnet.c @@ -27,7 +27,9 @@ #include #include #include +#include #include +#include #include #include @@ -381,81 +383,10 @@ static void* __guac_telnet_input_thread(void* data) { */ static telnet_t* __guac_telnet_create_session(guac_client* client) { - int retval; - - int fd; - struct addrinfo* addresses; - struct addrinfo* current_address; - - char connected_address[1024]; - char connected_port[64]; - guac_telnet_client* telnet_client = (guac_telnet_client*) client->data; guac_telnet_settings* settings = telnet_client->settings; - struct addrinfo hints = { - .ai_family = AF_UNSPEC, - .ai_socktype = SOCK_STREAM, - .ai_protocol = IPPROTO_TCP - }; - - /* Get socket */ - fd = socket(AF_INET, SOCK_STREAM, 0); - - /* Get addresses connection */ - if ((retval = getaddrinfo(settings->hostname, settings->port, - &hints, &addresses))) { - guac_client_abort(client, GUAC_PROTOCOL_STATUS_SERVER_ERROR, "Error parsing given address or port: %s", - gai_strerror(retval)); - return NULL; - - } - - /* Attempt connection to each address until success */ - current_address = addresses; - while (current_address != NULL) { - - int retval; - - /* Resolve hostname */ - if ((retval = getnameinfo(current_address->ai_addr, - current_address->ai_addrlen, - connected_address, sizeof(connected_address), - connected_port, sizeof(connected_port), - NI_NUMERICHOST | NI_NUMERICSERV))) - guac_client_log(client, GUAC_LOG_DEBUG, "Unable to resolve host: %s", gai_strerror(retval)); - - /* Connect */ - if (connect(fd, current_address->ai_addr, - current_address->ai_addrlen) == 0) { - - guac_client_log(client, GUAC_LOG_DEBUG, "Successfully connected to " - "host %s, port %s", connected_address, connected_port); - - /* Done if successful connect */ - break; - - } - - /* Otherwise log information regarding bind failure */ - else - guac_client_log(client, GUAC_LOG_DEBUG, "Unable to connect to " - "host %s, port %s: %s", - connected_address, connected_port, strerror(errno)); - - current_address = current_address->ai_next; - - } - - /* If unable to connect to anything, fail */ - if (current_address == NULL) { - guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_NOT_FOUND, - "Unable to connect to any addresses."); - return NULL; - } - - /* Free addrinfo */ - freeaddrinfo(addresses); + int fd = guac_tcp_connect(settings->hostname, settings->port, settings->timeout); /* Open telnet session */ telnet_t* telnet = telnet_init(__telnet_options, __guac_telnet_event_handler, 0, client); @@ -564,17 +495,36 @@ void* guac_telnet_client_thread(void* data) { /* If Wake-on-LAN is enabled, attempt to wake. */ if (settings->wol_send_packet) { - guac_client_log(client, GUAC_LOG_DEBUG, "Sending Wake-on-LAN packet, " - "and pausing for %d seconds.", settings->wol_wait_time); - /* Send the Wake-on-LAN request. */ - if (guac_wol_wake(settings->wol_mac_addr, settings->wol_broadcast_addr, - settings->wol_udp_port)) - return NULL; + /** + * If wait time is set, send the wake packet and try to connect to the + * server, failing if the server does not respond. + */ + if (settings->wol_wait_time > 0) { + guac_client_log(client, GUAC_LOG_DEBUG, "Sending Wake-on-LAN packet, " + "and pausing for %d seconds.", settings->wol_wait_time); + + /* Send the Wake-on-LAN request and wait until the server is responsive. */ + if (guac_wol_wake_and_wait(settings->wol_mac_addr, + settings->wol_broadcast_addr, + settings->wol_udp_port, + settings->wol_wait_time, + GUAC_WOL_DEFAULT_CONNECT_RETRIES, + settings->hostname, + settings->port, + settings->timeout)) { + guac_client_log(client, GUAC_LOG_ERROR, "Failed to send WOL packet or connect to remote server."); + return NULL; + } + } - /* If wait time is specified, sleep for that amount of time. */ - if (settings->wol_wait_time > 0) - guac_timestamp_msleep(settings->wol_wait_time * 1000); + /* Just send the packet and continue the connection, or return if failed. */ + else if(guac_wol_wake(settings->wol_mac_addr, + settings->wol_broadcast_addr, + settings->wol_udp_port)) { + guac_client_log(client, GUAC_LOG_ERROR, "Failed to send WOL packet."); + return NULL; + } } /* Set up screen recording, if requested */ @@ -586,7 +536,8 @@ void* guac_telnet_client_thread(void* data) { !settings->recording_exclude_output, !settings->recording_exclude_mouse, 0, /* Touch events not supported */ - settings->recording_include_keys); + settings->recording_include_keys, + settings->recording_write_existing); } /* Create terminal options with required parameters */ @@ -623,7 +574,8 @@ void* guac_telnet_client_thread(void* data) { guac_terminal_create_typescript(telnet_client->term, settings->typescript_path, settings->typescript_name, - settings->create_typescript_path); + settings->create_typescript_path, + settings->typescript_write_existing); } /* Open telnet session */ diff --git a/src/protocols/vnc/client.c b/src/protocols/vnc/client.c index 43ead098f..206dad894 100644 --- a/src/protocols/vnc/client.c +++ b/src/protocols/vnc/client.c @@ -34,6 +34,7 @@ #endif #include +#include #include #include @@ -89,7 +90,7 @@ static int guac_vnc_join_pending_handler(guac_client* client) { /* Synchronize with current display */ if (vnc_client->display != NULL) { - guac_common_display_dup(vnc_client->display, client, broadcast_socket); + guac_display_dup(vnc_client->display, broadcast_socket); guac_socket_flush(broadcast_socket); } @@ -111,6 +112,9 @@ int guac_client_init(guac_client* client) { pthread_mutex_init(&vnc_client->tls_lock, NULL); #endif + /* Initialize the message lock. */ + pthread_mutex_init(&(vnc_client->message_lock), NULL); + /* Init clipboard */ vnc_client->clipboard = guac_common_clipboard_alloc(); @@ -192,7 +196,7 @@ int guac_vnc_client_free_handler(guac_client* client) { /* Free display */ if (vnc_client->display != NULL) - guac_common_display_free(vnc_client->display); + guac_display_free(vnc_client->display); #ifdef ENABLE_PULSE /* If audio enabled, stop streaming */ @@ -209,6 +213,9 @@ int guac_vnc_client_free_handler(guac_client* client) { pthread_mutex_destroy(&(vnc_client->tls_lock)); #endif + /* Clean up the message lock. */ + pthread_mutex_destroy(&(vnc_client->message_lock)); + /* Free generic data struct */ guac_mem_free(client->data); diff --git a/src/protocols/vnc/client.h b/src/protocols/vnc/client.h index eec4bfce4..86fe264ad 100644 --- a/src/protocols/vnc/client.h +++ b/src/protocols/vnc/client.h @@ -23,25 +23,14 @@ #include /** - * The maximum duration of a frame in milliseconds. + * The amount of time to wait for new messages from the VNC server before + * moving on to internal matters, in milliseconds. This value must be kept + * reasonably small such that a slow VNC server will not prevent external + * events from being handled (such as the stop signal from guac_client_stop()), + * but large enough that the message handling loop does not eat up CPU + * spinning. */ -#define GUAC_VNC_FRAME_DURATION 40 - -/** - * The amount of time to allow per message read within a frame, in - * milliseconds. If the server is silent for at least this amount of time, the - * frame will be considered finished. - */ -#define GUAC_VNC_FRAME_TIMEOUT 0 - -/** - * The amount of time to wait for a new message from the VNC server when - * beginning a new frame. This value must be kept reasonably small such that - * a slow VNC server will not prevent external events from being handled (such - * as the stop signal from guac_client_stop()), but large enough that the - * message handling loop does not eat up CPU spinning. - */ -#define GUAC_VNC_FRAME_START_TIMEOUT 1000000 +#define GUAC_VNC_MESSAGE_CHECK_INTERVAL 1000 /** * The number of milliseconds to wait between connection attempts. diff --git a/src/protocols/vnc/cursor.c b/src/protocols/vnc/cursor.c index 449fea382..079ddd36f 100644 --- a/src/protocols/vnc/cursor.c +++ b/src/protocols/vnc/cursor.c @@ -20,13 +20,10 @@ #include "config.h" #include "client.h" -#include "common/cursor.h" -#include "common/display.h" -#include "common/surface.h" #include "vnc.h" -#include #include +#include #include #include #include @@ -34,99 +31,96 @@ #include #include -/* Define cairo_format_stride_for_width() if missing */ -#ifndef HAVE_CAIRO_FORMAT_STRIDE_FOR_WIDTH -#define cairo_format_stride_for_width(format, width) (width*4) -#endif - #include #include #include #include #include -void guac_vnc_cursor(rfbClient* client, int x, int y, int w, int h, int bpp) { +void guac_vnc_cursor(rfbClient* client, int x, int y, int w, int h, int vnc_bpp) { guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY); guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data; - /* Cairo image buffer */ - int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, w); - unsigned char* buffer = guac_mem_alloc(h, stride); - unsigned char* buffer_row_current = buffer; + /* Begin drawing operation directly to cursor layer */ + guac_display_layer* cursor_layer = guac_display_cursor(vnc_client->display); + guac_display_layer_resize(cursor_layer, w, h); + guac_display_set_cursor_hotspot(vnc_client->display, x, y); + guac_display_layer_raw_context* context = guac_display_layer_open_raw(cursor_layer); - /* VNC image buffer */ - unsigned int fb_stride = bpp * w; - unsigned char* fb_row_current = client->rcSource; - unsigned char* fb_mask = client->rcMask; + /* Convert operation coordinates to guac_rect for easier manipulation */ + guac_rect op_bounds; + guac_rect_init(&op_bounds, 0, 0, w, h); - int dx, dy; + /* Ensure draw is within current bounds of the pending frame */ + guac_rect_constrain(&op_bounds, &context->bounds); - /* Copy image data from VNC client to RGBA buffer */ - for (dy = 0; dyrcSource; + unsigned char* vnc_mask = client->rcMask; + size_t vnc_stride = guac_mem_ckd_mul_or_die(vnc_bpp, w); - unsigned int* buffer_current; - unsigned char* fb_current; - - /* Get current buffer row, advance to next */ - buffer_current = (unsigned int*) buffer_row_current; - buffer_row_current += stride; + /* Copy image data from VNC client to RGBA buffer */ + unsigned char* layer_current_row = GUAC_RECT_MUTABLE_BUFFER(op_bounds, context->buffer, context->stride, GUAC_DISPLAY_LAYER_RAW_BPP); + for (int dy = 0; dy < h; dy++) { - /* Get current framebuffer row, advance to next */ - fb_current = fb_row_current; - fb_row_current += fb_stride; + /* Get current Guacamole buffer row, advance to next */ + uint32_t* layer_current_pixel = (uint32_t*) layer_current_row; + layer_current_row += context->stride; - for (dx = 0; dx> client->format.redShift) * 0x100 / (client->format.redMax + 1); - green = (v >> client->format.greenShift) * 0x100 / (client->format.greenMax+ 1); - blue = (v >> client->format.blueShift) * 0x100 / (client->format.blueMax + 1); + uint8_t red = (v >> client->format.redShift) * 0x100 / (client->format.redMax + 1); + uint8_t green = (v >> client->format.greenShift) * 0x100 / (client->format.greenMax + 1); + uint8_t blue = (v >> client->format.blueShift) * 0x100 / (client->format.blueMax + 1); /* Output ARGB */ if (vnc_client->settings->swap_red_blue) - *(buffer_current++) = (alpha << 24) | (blue << 16) | (green << 8) | red; + *(layer_current_pixel++) = (alpha << 24) | (blue << 16) | (green << 8) | red; else - *(buffer_current++) = (alpha << 24) | (red << 16) | (green << 8) | blue; + *(layer_current_pixel++) = (alpha << 24) | (red << 16) | (green << 8) | blue; - /* Next VNC pixel */ - fb_current += bpp; + /* Advance to next pixel in VNC framebuffer */ + vnc_current_pixel += vnc_bpp; } } - /* Update stored cursor information */ - guac_common_cursor_set_argb(vnc_client->display->cursor, x, y, - buffer, w, h, stride); + /* Mark modified region as dirty */ + guac_rect_extend(&context->dirty, &op_bounds); - /* Free surface */ - guac_mem_free(buffer); + /* Draw operation is now complete */ + guac_display_layer_close_raw(cursor_layer, context); + guac_display_render_thread_notify_modified(vnc_client->render_thread); /* libvncclient does not free rcMask as it does rcSource */ if (client->rcMask != NULL) { free(client->rcMask); client->rcMask = NULL; } + } diff --git a/src/protocols/vnc/cursor.h b/src/protocols/vnc/cursor.h index 193b8749b..18fe7e3cd 100644 --- a/src/protocols/vnc/cursor.h +++ b/src/protocols/vnc/cursor.h @@ -49,10 +49,10 @@ * @param h * The height of the cursor image, in pixels. * - * @param bpp + * @param vnc_bpp * The number of bytes in each pixel, which must be either 4, 2, or 1. */ -void guac_vnc_cursor(rfbClient* client, int x, int y, int w, int h, int bpp); +void guac_vnc_cursor(rfbClient* client, int x, int y, int w, int h, int vnc_bpp); #endif diff --git a/src/protocols/vnc/display.c b/src/protocols/vnc/display.c index 8848e8933..ba0c061c8 100644 --- a/src/protocols/vnc/display.c +++ b/src/protocols/vnc/display.c @@ -20,8 +20,8 @@ #include "config.h" #include "client.h" +#include "display.h" #include "common/iconv.h" -#include "common/surface.h" #include "vnc.h" #include @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -48,110 +49,263 @@ void guac_vnc_update(rfbClient* client, int x, int y, int w, int h) { guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY); guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data; + guac_display_layer* default_layer = guac_display_default_layer(vnc_client->display); - int dx, dy; + guac_display_layer_raw_context* context = vnc_client->current_context; + unsigned int vnc_bpp = client->format.bitsPerPixel / 8; + size_t vnc_stride = guac_mem_ckd_mul_or_die(vnc_bpp, client->width); - /* Cairo image buffer */ - int stride; - unsigned char* buffer; - unsigned char* buffer_row_current; - cairo_surface_t* surface; + /* Convert operation coordinates to guac_rect for easier manipulation */ + guac_rect op_bounds; + guac_rect_init(&op_bounds, x, y, w, h); - /* VNC framebuffer */ - unsigned int bpp; - unsigned int fb_stride; - unsigned char* fb_row_current; + /* Ensure operation bounds are within possibly updated bounds of the + * pending frame (now the RFB client framebuffer) */ + guac_rect_constrain(&op_bounds, &context->bounds); - /* Ignore extra update if already handled by copyrect */ + /* NOTE: The guac_display will be pointed directly at the libvncclient + * framebuffer if the pixel format used is identical to that expected by + * guac_display. No need to manually copy anything around in that case. */ + + /* All framebuffer formats must be manually converted if not identical to + * the format used by guac_display */ + if (vnc_bpp != GUAC_DISPLAY_LAYER_RAW_BPP || vnc_client->settings->swap_red_blue) { + + /* Ensure draw is within current bounds of the pending frame */ + guac_rect_constrain(&op_bounds, &context->bounds); + + const unsigned char* vnc_current_row = GUAC_RECT_CONST_BUFFER(op_bounds, client->frameBuffer, vnc_stride, vnc_bpp); + unsigned char* layer_current_row = GUAC_RECT_MUTABLE_BUFFER(op_bounds, context->buffer, context->stride, GUAC_DISPLAY_LAYER_RAW_BPP); + for (int dy = op_bounds.top; dy < op_bounds.bottom; dy++) { + + /* Get current Guacamole buffer row, advance to next */ + uint32_t* layer_current_pixel = (uint32_t*) layer_current_row; + layer_current_row += context->stride; + + /* Get current VNC framebuffer row, advance to next */ + const unsigned char* vnc_current_pixel = vnc_current_row; + vnc_current_row += vnc_stride; + + for (int dx = op_bounds.left; dx < op_bounds.right; dx++) { + + /* Read current VNC pixel value */ + uint32_t v; + switch (vnc_bpp) { + + case 2: + v = *((uint16_t*) vnc_current_pixel); + break; + + default: + v = *((uint8_t*) vnc_current_pixel); + + } + + /* Translate value to 32-bit RGB */ + uint8_t red = (v >> client->format.redShift) * 0x100 / (client->format.redMax + 1); + uint8_t green = (v >> client->format.greenShift) * 0x100 / (client->format.greenMax + 1); + uint8_t blue = (v >> client->format.blueShift) * 0x100 / (client->format.blueMax + 1); + + /* Output RGB */ + if (vnc_client->settings->swap_red_blue) + *(layer_current_pixel++) = 0xFF000000 | (blue << 16) | (green << 8) | red; + else + *(layer_current_pixel++) = 0xFF000000 | (red << 16) | (green << 8) | blue; + + /* Advance to next pixel in VNC framebuffer */ + vnc_current_pixel += vnc_bpp; + + } + } + + } /* end manual convert */ + + /* Mark modified region as dirty */ + guac_rect_extend(&context->dirty, &op_bounds); + + /* Hint at source of copied data if this update involved CopyRect */ if (vnc_client->copy_rect_used) { + context->hint_from = default_layer; vnc_client->copy_rect_used = 0; - return; } - /* Init Cairo buffer */ - stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, w); - buffer = guac_mem_alloc(h, stride); - buffer_row_current = buffer; + guac_display_render_thread_notify_modified(vnc_client->render_thread); - bpp = client->format.bitsPerPixel/8; - fb_stride = bpp * client->width; - fb_row_current = client->frameBuffer + (y * fb_stride) + (x * bpp); +} - /* Copy image data from VNC client to PNG */ - for (dy = y; dydata; - /* Get current framebuffer row, advance to next */ - fb_current = fb_row_current; - fb_row_current += fb_stride; + vnc_client->copy_rect_used = 1; - for (dx = x; dxrfb_GotCopyRect(client, src_x, src_y, w, h, dest_x, dest_y); - unsigned char red, green, blue; - unsigned int v; +} - switch (bpp) { - case 4: - v = *((uint32_t*) fb_current); - break; +#ifdef LIBVNC_HAS_RESIZE_SUPPORT +/** + * This function does the actual work of sending the message to the RFB/VNC + * server to request the resize, and then makes sure that the client frame + * buffer is updated, as well. + * + * @param client + * The remote frame buffer client that is triggering the resize + * request. + * + * @param width + * The updated width of the screen. + * + * @param height + * The updated height of the screen. + * + * @return + * TRUE if the screen update was sent to the server, otherwise false. Note + * that a successful send of the resize message to the server does NOT mean + * that the server has any obligation to resize the display - it only + * indicates that the VNC library has successfully sent the request. + */ +static rfbBool guac_vnc_send_desktop_size(rfbClient* client, int width, int height) { - case 2: - v = *((uint16_t*) fb_current); - break; + /* Get the Guacamole client data */ + guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY); - default: - v = *((uint8_t*) fb_current); - } + if (client->screen.width == 0 || client->screen.height == 0) { + guac_client_log(gc, GUAC_LOG_WARNING, "Screen data has not been initialized, yet."); + return FALSE; + } - /* Translate value to RGB */ - red = (v >> client->format.redShift) * 0x100 / (client->format.redMax + 1); - green = (v >> client->format.greenShift) * 0x100 / (client->format.greenMax+ 1); - blue = (v >> client->format.blueShift) * 0x100 / (client->format.blueMax + 1); + guac_client_log(gc, GUAC_LOG_TRACE, + "Current screen size is %ix%i; setting new size %ix%i\n", + rfbClientSwap16IfLE(client->screen.width), + rfbClientSwap16IfLE(client->screen.height), + width, height); - /* Output RGB */ - if (vnc_client->settings->swap_red_blue) - *(buffer_current++) = (blue << 16) | (green << 8) | red; - else - *(buffer_current++) = (red << 16) | (green << 8) | blue; + /* Don't send an update if the requested dimensions are identical to current dimensions. */ + if (client->screen.width == rfbClientSwap16IfLE(width) && client->screen.height == rfbClientSwap16IfLE(height)) { + guac_client_log(gc, GUAC_LOG_WARNING, "Screen size has not changed, not sending update."); + return FALSE; + } - fb_current += bpp; + /** + * Note: The RFB protocol requires two message types to be sent during a + * resize request - the first for the desktop size (total size of all + * monitors), and then a message for each screen that is attached to the + * remote server. Both libvncclient and Guacamole only support a single + * screen, so we send the desktop resize and screen resize with (nearly) + * identical data, but if one or both of these components is updated in the + * future to support multiple screens, this will need to be re-worked. + */ + + /* Set up the messages. */ + rfbSetDesktopSizeMsg size_msg = { 0 }; + rfbExtDesktopScreen new_screen = { 0 }; + + /* Configure the desktop size update message. */ + size_msg.type = rfbSetDesktopSize; + size_msg.width = rfbClientSwap16IfLE(width); + size_msg.height = rfbClientSwap16IfLE(height); + size_msg.numberOfScreens = 1; + + /* Configure the screen update message. */ + new_screen.id = GUAC_VNC_SCREEN_ID; + new_screen.x = client->screen.x; + new_screen.y = client->screen.y; + new_screen.flags = client->screen.flags; + + new_screen.width = rfbClientSwap16IfLE(width); + new_screen.height = rfbClientSwap16IfLE(height); + + /* Send the resize messages to the remote server. */ + if (!WriteToRFBServer(client, (char *)&size_msg, sz_rfbSetDesktopSizeMsg) + || !WriteToRFBServer(client, (char *)&new_screen, sz_rfbExtDesktopScreen)) { + + guac_client_log(gc, GUAC_LOG_WARNING, + "Failed to send new desktop and screen size to the VNC server."); + return FALSE; - } } - /* Create surface from decoded buffer */ - surface = cairo_image_surface_create_for_data(buffer, CAIRO_FORMAT_RGB24, - w, h, stride); + /* Update the client frame buffer with the requested size. */ + client->screen.width = rfbClientSwap16IfLE(width); + client->screen.height = rfbClientSwap16IfLE(height); + +#ifdef LIBVNC_CLIENT_HAS_REQUESTED_RESIZE + client->requestedResize = FALSE; +#endif // LIBVNC_HAS_REQUESTED_RESIZE + + if (!SendFramebufferUpdateRequest(client, 0, 0, width, height, FALSE)) { + guac_client_log(gc, GUAC_LOG_WARNING, "Failed to request a full screen update."); + } - /* Draw directly to default layer */ - guac_common_surface_draw(vnc_client->display->default_surface, - x, y, surface); +#ifdef LIBVNC_CLIENT_HAS_REQUESTED_RESIZE + client->requestedResize = TRUE; +#endif // LIBVNC_HAS_REQUESTED_RESIZE - /* Free surface */ - cairo_surface_destroy(surface); - guac_mem_free(buffer); + /* Update should be successful. */ + return TRUE; } -void guac_vnc_copyrect(rfbClient* client, int src_x, int src_y, int w, int h, int dest_x, int dest_y) { +void* guac_vnc_display_set_owner_size(guac_user* owner, void* data) { + + /* Pull RFB clients from provided data. */ + rfbClient* rfb_client = (rfbClient*) data; + + guac_user_log(owner, GUAC_LOG_DEBUG, "Sending VNC display size for owner's display."); + /* Set the display size. */ + guac_vnc_display_set_size(rfb_client, owner->info.optimal_width, owner->info.optimal_height); + + /* Always return NULL. */ + return NULL; + +} + +void guac_vnc_display_set_size(rfbClient* client, int requested_width, int requested_height) { + + /* Get the VNC client */ guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY); guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data; - /* Copy specified rectangle within default layer */ - guac_common_surface_copy(vnc_client->display->default_surface, - src_x, src_y, w, h, - vnc_client->display->default_surface, dest_x, dest_y); + guac_rect resize = { + .left = 0, + .top = 0, + .right = requested_width, + .bottom = requested_height + }; + + /* Fit width and height within bounds, maintaining aspect ratio */ + guac_rect_shrink(&resize, GUAC_DISPLAY_MAX_WIDTH, GUAC_DISPLAY_MAX_HEIGHT); + int width = guac_rect_width(&resize); + int height = guac_rect_height(&resize); + + if (width <= 0 || height <= 0) { + guac_client_log(gc, GUAC_LOG_WARNING, "Ignoring request to resize " + "desktop to %ix%i as the resulting display would be completely " + "empty", requested_width, requested_height); + return; + } - vnc_client->copy_rect_used = 1; + /* Acquire the lock for sending messages to server. */ + pthread_mutex_lock(&(vnc_client->message_lock)); + + /* Send the display size update. */ + guac_client_log(gc, GUAC_LOG_TRACE, "Setting VNC display size."); + if (guac_vnc_send_desktop_size(client, width, height)) + guac_client_log(gc, GUAC_LOG_TRACE, "Successfully sent desktop size message."); + + else + guac_client_log(gc, GUAC_LOG_TRACE, "Failed to send desktop size message."); + + /* Release the lock. */ + pthread_mutex_unlock(&(vnc_client->message_lock)); } +#endif // LIBVNC_HAS_RESIZE_SUPPORT void guac_vnc_set_pixel_format(rfbClient* client, int color_depth) { client->format.trueColour = 1; @@ -197,12 +351,8 @@ rfbBool guac_vnc_malloc_framebuffer(rfbClient* rfb_client) { guac_client* gc = rfbClientGetClientData(rfb_client, GUAC_VNC_CLIENT_KEY); guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data; - /* Resize surface */ - if (vnc_client->display != NULL) - guac_common_surface_resize(vnc_client->display->default_surface, - rfb_client->width, rfb_client->height); - - /* Use original, wrapped proc */ + /* Use original, wrapped proc to resize the buffer maintained by + * libvncclient */ return vnc_client->rfb_MallocFrameBuffer(rfb_client); -} +} diff --git a/src/protocols/vnc/display.h b/src/protocols/vnc/display.h index 831f012f5..cd7d27b25 100644 --- a/src/protocols/vnc/display.h +++ b/src/protocols/vnc/display.h @@ -22,12 +22,13 @@ #include "config.h" +#include #include #include /** - * Callback invoked by libVNCServer when it receives a new binary image data. - * the VNC server. The image itself will be stored in the designated sub- + * Callback invoked by libVNCServer when it receives a new binary image data + * from the VNC server. The image itself will be stored in the designated sub- * rectangle of client->framebuffer. * * @param client @@ -84,6 +85,39 @@ void guac_vnc_update(rfbClient* client, int x, int y, int w, int h); void guac_vnc_copyrect(rfbClient* client, int src_x, int src_y, int w, int h, int dest_x, int dest_y); +/** + * A callback for guac_client_for_owner that sets the VNC display size to the + * width and height of the owner's display. + * + * @param owner + * A pointer to the guac_user data structure that contains the owner of + * the current connection. + * + * @param data + * A pointer to the rfbClient data structure that represents the current + * VNC client for the current connection. + * + * @return + * This callback always returns NULL. + */ +void* guac_vnc_display_set_owner_size(guac_user* owner, void* data); + +/** + * Attempts to set the display size of the remote server to the size requested + * by the client, usually as part of a client (browser) resize, if supported by + * both the VNC client and the remote server. + * + * @param display + * The VNC client to which the display size update should be sent. + * + * @param requested_width + * The width that is being requested, in pixels. + * + * @param requested_height + * The height that is being requested, in pixels. + */ +void guac_vnc_display_set_size(rfbClient* client, int requested_width, int requested_height); + /** * Sets the pixel format to request of the VNC server. The request will be made * during the connection handshake with the VNC server using the values diff --git a/src/protocols/vnc/input.c b/src/protocols/vnc/input.c index 584819183..bfb2546d9 100644 --- a/src/protocols/vnc/input.c +++ b/src/protocols/vnc/input.c @@ -19,10 +19,10 @@ #include "config.h" -#include "common/cursor.h" -#include "common/display.h" +#include "display.h" #include "vnc.h" +#include #include #include #include @@ -34,7 +34,7 @@ int guac_vnc_user_mouse_handler(guac_user* user, int x, int y, int mask) { rfbClient* rfb_client = vnc_client->rfb_client; /* Store current mouse location/state */ - guac_common_cursor_update(vnc_client->display->cursor, user, x, y, mask); + guac_display_notify_user_moved_mouse(vnc_client->display, user, x, y, mask); /* Report mouse position within recording */ if (vnc_client->recording != NULL) @@ -64,3 +64,18 @@ int guac_vnc_user_key_handler(guac_user* user, int keysym, int pressed) { return 0; } +#ifdef LIBVNC_HAS_RESIZE_SUPPORT +int guac_vnc_user_size_handler(guac_user* user, int width, int height) { + + guac_user_log(user, GUAC_LOG_TRACE, "Running user size handler."); + + /* Get the Guacamole VNC client */ + guac_vnc_client* vnc_client = (guac_vnc_client*) user->client->data; + + /* Send display update */ + guac_vnc_display_set_size(vnc_client->rfb_client, width, height); + + return 0; + +} +#endif // LIBVNC_HAS_RESIZE_SUPPORT diff --git a/src/protocols/vnc/input.h b/src/protocols/vnc/input.h index ced9f3079..4bfb9e2d5 100644 --- a/src/protocols/vnc/input.h +++ b/src/protocols/vnc/input.h @@ -34,5 +34,9 @@ guac_user_mouse_handler guac_vnc_user_mouse_handler; */ guac_user_key_handler guac_vnc_user_key_handler; -#endif +/** + * Handler for Guacamole user resize events. + */ +guac_user_size_handler guac_vnc_user_size_handler; +#endif // GUAC_VNC_INPUT_H diff --git a/src/protocols/vnc/log.c b/src/protocols/vnc/log.c index ba593e81d..afb93d0e7 100644 --- a/src/protocols/vnc/log.c +++ b/src/protocols/vnc/log.c @@ -21,7 +21,6 @@ #include "client.h" #include "common/iconv.h" -#include "common/surface.h" #include #include diff --git a/src/protocols/vnc/log.h b/src/protocols/vnc/log.h index 65f3dc2d4..3f348605c 100644 --- a/src/protocols/vnc/log.h +++ b/src/protocols/vnc/log.h @@ -24,7 +24,6 @@ #include "client.h" #include "common/iconv.h" -#include "common/surface.h" #include #include diff --git a/src/protocols/vnc/settings.c b/src/protocols/vnc/settings.c index 725e10cb7..5b7df0bac 100644 --- a/src/protocols/vnc/settings.c +++ b/src/protocols/vnc/settings.c @@ -38,6 +38,7 @@ const char* GUAC_VNC_CLIENT_ARGS[] = { "hostname", "port", "read-only", + "disable-display-resize", "encodings", GUAC_VNC_ARGV_USERNAME, GUAC_VNC_ARGV_PASSWORD, @@ -67,10 +68,12 @@ const char* GUAC_VNC_CLIENT_ARGS[] = { "sftp-hostname", "sftp-host-key", "sftp-port", + "sftp-timeout", "sftp-username", "sftp-password", "sftp-private-key", "sftp-passphrase", + "sftp-public-key", "sftp-directory", "sftp-root-directory", "sftp-server-alive-interval", @@ -84,8 +87,10 @@ const char* GUAC_VNC_CLIENT_ARGS[] = { "recording-exclude-mouse", "recording-include-keys", "create-recording-path", + "recording-write-existing", "disable-copy", "disable-paste", + "disable-server-input", "wol-send-packet", "wol-mac-addr", @@ -94,6 +99,8 @@ const char* GUAC_VNC_CLIENT_ARGS[] = { "wol-wait-time", "force-lossless", + "compress-level", + "quality-level", NULL }; @@ -115,6 +122,13 @@ enum VNC_ARGS_IDX { */ IDX_READ_ONLY, + /** + * "true" if the VNC client should disable attempts to resize the remote + * display to the client's size, "false" or blank if those resize messages + * should be sent. + */ + IDX_DISABLE_DISPLAY_RESIZE, + /** * Space-separated list of encodings to use within the VNC session. If not * specified, this will be: @@ -229,6 +243,12 @@ enum VNC_ARGS_IDX { */ IDX_SFTP_PORT, + /** + * The number of seconds to attempt to connect to the SFTP server before + * timing out. + */ + IDX_SFTP_TIMEOUT, + /** * The username to provide when authenticating with the SSH server for * SFTP. @@ -253,6 +273,12 @@ enum VNC_ARGS_IDX { */ IDX_SFTP_PASSPHRASE, + /** + * The base64-encode public key to use when authentication with the SSH + * server for SFTP using key-based authentication. + */ + IDX_SFTP_PUBLIC_KEY, + /** * The default location for file uploads within the SSH server. This will * apply only to uploads which do not use the filesystem guac_object (where @@ -331,6 +357,12 @@ enum VNC_ARGS_IDX { */ IDX_CREATE_RECORDING_PATH, + /** + * Whether existing files should be appended to when creating a new recording. + * Disabled by default. + */ + IDX_RECORDING_WRITE_EXISTING, + /** * Whether outbound clipboard access should be blocked. If set to "true", * it will not be possible to copy data from the remote desktop to the @@ -344,6 +376,12 @@ enum VNC_ARGS_IDX { * using the clipboard. By default, clipboard access is not blocked. */ IDX_DISABLE_PASTE, + + /** + * Whether or not to disable the input on the server side when the VNC client + * is connected. The default is not to disable the input. + */ + IDX_DISABLE_SERVER_INPUT, /** * Whether to send the magic Wake-on-LAN (WoL) packet to wake the remote @@ -382,6 +420,18 @@ enum VNC_ARGS_IDX { */ IDX_FORCE_LOSSLESS, + /** + * The level of compression, on a scale of 0 (no compression) to 9 (maximum + * compression), that the connection will be configured for. + */ + IDX_COMPRESS_LEVEL, + + /** + * The level of display quality, on a scale of 0 (worst quality) to 9 (best + * quality), that the connection will be configured for. + */ + IDX_QUALITY_LEVEL, + VNC_ARGS_COUNT }; @@ -436,6 +486,16 @@ guac_vnc_settings* guac_vnc_parse_args(guac_user* user, guac_user_parse_args_boolean(user, GUAC_VNC_CLIENT_ARGS, argv, IDX_READ_ONLY, false); + /* Disable server input */ + settings->disable_server_input = + guac_user_parse_args_boolean(user, GUAC_VNC_CLIENT_ARGS, argv, + IDX_DISABLE_SERVER_INPUT, false); + + /* Disable display resize */ + settings->disable_display_resize = + guac_user_parse_args_boolean(user, GUAC_VNC_CLIENT_ARGS, argv, + IDX_DISABLE_DISPLAY_RESIZE, false); + /* Parse color depth */ settings->color_depth = guac_user_parse_args_int(user, GUAC_VNC_CLIENT_ARGS, argv, @@ -446,6 +506,16 @@ guac_vnc_settings* guac_vnc_parse_args(guac_user* user, guac_user_parse_args_boolean(user, GUAC_VNC_CLIENT_ARGS, argv, IDX_FORCE_LOSSLESS, false); + /* Compression level */ + settings->compress_level = + guac_user_parse_args_int(user, GUAC_VNC_CLIENT_ARGS, argv, + IDX_COMPRESS_LEVEL, -1); + + /* Display quality */ + settings->quality_level = + guac_user_parse_args_int(user, GUAC_VNC_CLIENT_ARGS, argv, + IDX_QUALITY_LEVEL, -1); + #ifdef ENABLE_VNC_REPEATER /* Set repeater parameters if specified */ settings->dest_host = @@ -520,6 +590,11 @@ guac_vnc_settings* guac_vnc_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_VNC_CLIENT_ARGS, argv, IDX_SFTP_PORT, "22"); + /* SFTP connection timeout */ + settings->sftp_timeout = + guac_user_parse_args_int(user, GUAC_VNC_CLIENT_ARGS, argv, + IDX_SFTP_TIMEOUT, GUAC_VNC_DEFAULT_SFTP_TIMEOUT); + /* Username for SSH/SFTP authentication */ settings->sftp_username = guac_user_parse_args_string(user, GUAC_VNC_CLIENT_ARGS, argv, @@ -540,6 +615,11 @@ guac_vnc_settings* guac_vnc_parse_args(guac_user* user, guac_user_parse_args_string(user, GUAC_VNC_CLIENT_ARGS, argv, IDX_SFTP_PASSPHRASE, ""); + /* Public key for SFTP using key-based authentication. */ + settings->sftp_public_key = + guac_user_parse_args_string(user, GUAC_VNC_CLIENT_ARGS, argv, + IDX_SFTP_PUBLIC_KEY, NULL); + /* Default upload directory */ settings->sftp_directory = guac_user_parse_args_string(user, GUAC_VNC_CLIENT_ARGS, argv, @@ -594,6 +674,11 @@ guac_vnc_settings* guac_vnc_parse_args(guac_user* user, guac_user_parse_args_boolean(user, GUAC_VNC_CLIENT_ARGS, argv, IDX_CREATE_RECORDING_PATH, false); + /* Parse allow write existing file flag */ + settings->recording_write_existing = + guac_user_parse_args_boolean(user, GUAC_VNC_CLIENT_ARGS, argv, + IDX_RECORDING_WRITE_EXISTING, false); + /* Parse clipboard copy disable flag */ settings->disable_copy = guac_user_parse_args_boolean(user, GUAC_VNC_CLIENT_ARGS, argv, @@ -670,6 +755,7 @@ void guac_vnc_settings_free(guac_vnc_settings* settings) { guac_mem_free(settings->sftp_password); guac_mem_free(settings->sftp_port); guac_mem_free(settings->sftp_private_key); + guac_mem_free(settings->sftp_public_key); guac_mem_free(settings->sftp_username); #endif diff --git a/src/protocols/vnc/settings.h b/src/protocols/vnc/settings.h index d1d072635..f433df9bf 100644 --- a/src/protocols/vnc/settings.h +++ b/src/protocols/vnc/settings.h @@ -29,6 +29,11 @@ */ #define GUAC_VNC_DEFAULT_RECORDING_NAME "recording" +/** + * The default number of seconds to attempt to connect to the SFTP server. + */ +#define GUAC_VNC_DEFAULT_SFTP_TIMEOUT 10 + /** * VNC-specific client data. */ @@ -54,6 +59,12 @@ typedef struct guac_vnc_settings { */ char* password; + /** + * Disable the VNC client messages to request that the remote (server) + * display resize to match the client resolution. + */ + bool disable_display_resize; + /** * Space-separated list of encodings to use within the VNC session. */ @@ -82,6 +93,16 @@ typedef struct guac_vnc_settings { */ bool lossless; + /** + * The level of compression to ask the VNC client library to perform. + */ + int compress_level; + + /** + * The quality level to ask the VNC client library to maintain. + */ + int quality_level; + #ifdef ENABLE_VNC_REPEATER /** * The VNC host to connect to, if using a repeater. @@ -172,6 +193,11 @@ typedef struct guac_vnc_settings { */ char* sftp_port; + /** + * The number of seconds to attempt to connect to the SFTP server. + */ + int sftp_timeout; + /** * The username to provide when authenticating with the SSH server for * SFTP. @@ -196,6 +222,12 @@ typedef struct guac_vnc_settings { */ char* sftp_passphrase; + /** + * The base64-encoded public key to use when authenticating with the SSH + * server for SFTP using key-based authentication. + */ + char* sftp_public_key; + /** * The default location for file uploads within the SSH server. This will * apply only to uploads which do not use the filesystem guac_object (where @@ -274,6 +306,12 @@ typedef struct guac_vnc_settings { * as passwords, credit card numbers, etc. */ bool recording_include_keys; + + /** + * Whether existing files should be appended to when creating a new recording. + * Disabled by default. + */ + bool recording_write_existing; /** * Whether or not to send the magic Wake-on-LAN (WoL) packet prior to @@ -305,6 +343,11 @@ typedef struct guac_vnc_settings { */ int wol_wait_time; + /** + * Whether or not to disable the input on the server side. + */ + bool disable_server_input; + } guac_vnc_settings; /** diff --git a/src/protocols/vnc/user.c b/src/protocols/vnc/user.c index 42bae34aa..8605f0714 100644 --- a/src/protocols/vnc/user.c +++ b/src/protocols/vnc/user.c @@ -21,9 +21,6 @@ #include "clipboard.h" #include "input.h" -#include "common/display.h" -#include "common/dot_cursor.h" -#include "common/pointer_cursor.h" #include "user.h" #include "sftp.h" #include "vnc.h" @@ -35,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -91,8 +89,18 @@ int guac_vnc_user_join_handler(guac_user* user, int argc, char** argv) { user->file_handler = guac_vnc_sftp_file_handler; #endif +#ifdef LIBVNC_HAS_RESIZE_SUPPORT + /* If user is owner, set size handler. */ + if (user->owner && !settings->disable_display_resize) + user->size_handler = guac_vnc_user_size_handler; +#else + guac_user_log(user, GUAC_LOG_WARNING, + "The libvncclient library does not support remote resize."); +#endif // LIBVNC_HAS_RESIZE_SUPPORT + } + /** * Update connection parameters if we own the connection. * @@ -121,10 +129,8 @@ int guac_vnc_user_leave_handler(guac_user* user) { guac_vnc_client* vnc_client = (guac_vnc_client*) user->client->data; - if (vnc_client->display) { - /* Update shared cursor state */ - guac_common_cursor_remove_user(vnc_client->display->cursor, user); - } + if (vnc_client->display) + guac_display_notify_user_left(vnc_client->display, user); /* Free settings if not owner (owner settings will be freed with client) */ if (!user->owner) { diff --git a/src/protocols/vnc/vnc.c b/src/protocols/vnc/vnc.c index 3b94d5e75..6ec4f4937 100644 --- a/src/protocols/vnc/vnc.c +++ b/src/protocols/vnc/vnc.c @@ -23,8 +23,6 @@ #include "client.h" #include "clipboard.h" #include "common/clipboard.h" -#include "common/cursor.h" -#include "common/display.h" #include "cursor.h" #include "display.h" #include "log.h" @@ -42,10 +40,14 @@ #endif #include +#include +#include #include #include #include +#include #include +#include #include #include #include @@ -137,6 +139,7 @@ rfbClient* guac_vnc_get_client(guac_client* client) { /* Framebuffer update handler */ rfb_client->GotFrameBufferUpdate = guac_vnc_update; + vnc_client->rfb_GotCopyRect = rfb_client->GotCopyRect; rfb_client->GotCopyRect = guac_vnc_copyrect; #ifdef ENABLE_VNC_TLS_LOCKING @@ -249,20 +252,97 @@ rfbClient* guac_vnc_get_client(guac_client* client) { * The rfbClient to wait for. * * @param timeout - * The maximum amount of time to wait, in microseconds. + * The maximum amount of time to wait, in milliseconds. * * @returns * A positive value if data is available, zero if the timeout elapses * before data becomes available, or a negative value on error. */ -static int guac_vnc_wait_for_messages(rfbClient* rfb_client, int timeout) { +static int guac_vnc_wait_for_messages(rfbClient* rfb_client, int msec_timeout) { /* Do not explicitly wait while data is on the buffer */ if (rfb_client->buffered) return 1; /* If no data on buffer, wait for data on socket */ - return WaitForMessage(rfb_client, timeout); + return WaitForMessage(rfb_client, msec_timeout * 1000); + +} + +/** + * Handles any inbound VNC messages that have been received, updating the + * Guacamole display accordingly. + * + * @param vnc_client + * The guac_vnc_client of the VNC connection whose current messages should + * be handled. + * + * @return + * True (non-zero) if messages were handled successfully, false (zero) + * otherwise. + */ +static rfbBool guac_vnc_handle_messages(guac_client* client) { + + guac_vnc_client* vnc_client = (guac_vnc_client*) client->data; + rfbClient* rfb_client = vnc_client->rfb_client; + guac_display_layer* default_layer = guac_display_default_layer(vnc_client->display); + + /* All potential drawing operations must occur while holding an open context */ + guac_display_layer_raw_context* context = guac_display_layer_open_raw(default_layer); + vnc_client->current_context = context; + + /* Actually handle messages (this may result in drawing to the + * guac_display, resizing the display buffer, etc.) */ + rfbBool retval = HandleRFBServerMessage(rfb_client); + + /* Use the buffer of libvncclient directly if it matches the guac_display + * format */ + unsigned int vnc_bpp = rfb_client->format.bitsPerPixel / 8; + if (vnc_bpp == GUAC_DISPLAY_LAYER_RAW_BPP && !vnc_client->settings->swap_red_blue) { + + context->buffer = rfb_client->frameBuffer; + context->stride = guac_mem_ckd_mul_or_die(vnc_bpp, rfb_client->width); + + /* Update bounds of pending frame to match those of RFB framebuffer */ + guac_rect_init(&context->bounds, 0, 0, rfb_client->width, rfb_client->height); + + } + + /* There will be no further drawing operations */ + guac_display_layer_close_raw(default_layer, context); + vnc_client->current_context = NULL; + +#ifdef LIBVNC_HAS_RESIZE_SUPPORT + // If screen was not previously initialized, check for it and set it. + if (!vnc_client->rfb_screen_initialized + && rfb_client->screen.width > 0 + && rfb_client->screen.height > 0) { + vnc_client->rfb_screen_initialized = true; + guac_client_log(client, GUAC_LOG_DEBUG, "Screen is now initialized."); + } + + /* + * If the screen is now or has been initialized, check to see if the initial + * dimensions have already been sent. If not, and resize is not disabled, + * send the initial size. + */ + if (vnc_client->rfb_screen_initialized) { + guac_vnc_settings* settings = vnc_client->settings; + if (!vnc_client->rfb_initial_resize && !settings->disable_display_resize) { + guac_client_log(client, GUAC_LOG_DEBUG, + "Sending initial screen size to VNC server."); + guac_client_for_owner(client, guac_vnc_display_set_owner_size, rfb_client); + vnc_client->rfb_initial_resize = true; + } + } +#endif // LIBVNC_HAS_RESIZE_SUPPORT + + /* Resize the surface if VNC screen size has changed (this call + * automatically deals with invalid dimensions and is a no-op + * if the size has not changed) */ + guac_display_layer_resize(default_layer, rfb_client->width, rfb_client->height); + + return retval; } @@ -274,17 +354,48 @@ void* guac_vnc_client_thread(void* data) { /* If Wake-on-LAN is enabled, attempt to wake. */ if (settings->wol_send_packet) { - guac_client_log(client, GUAC_LOG_DEBUG, "Sending Wake-on-LAN packet, " - "and pausing for %d seconds.", settings->wol_wait_time); - - /* Send the Wake-on-LAN request. */ - if (guac_wol_wake(settings->wol_mac_addr, settings->wol_broadcast_addr, - settings->wol_udp_port)) + + /** + * If wait time is set, send the wake packet and try to connect to the + * server, failing if the server does not respond. + */ + if (settings->wol_wait_time > 0) { + guac_client_log(client, GUAC_LOG_DEBUG, "Sending Wake-on-LAN packet, " + "and pausing for %d seconds.", settings->wol_wait_time); + + /* char representation of a port should be, at most, 5 characters plus terminator. */ + char* str_port = guac_mem_alloc(6); + if (guac_itoa(str_port, settings->port) < 1) { + guac_client_log(client, GUAC_LOG_ERROR, "Failed to convert port to integer for WOL function."); + guac_mem_free(str_port); + return NULL; + } + + /* Send the Wake-on-LAN request and wait until the server is responsive. */ + if (guac_wol_wake_and_wait(settings->wol_mac_addr, + settings->wol_broadcast_addr, + settings->wol_udp_port, + settings->wol_wait_time, + GUAC_WOL_DEFAULT_CONNECT_RETRIES, + settings->hostname, + (const char *) str_port, + GUAC_WOL_DEFAULT_CONNECTION_TIMEOUT)) { + guac_client_log(client, GUAC_LOG_ERROR, "Failed to send WOL packet or connect to remote system."); + guac_mem_free(str_port); + return NULL; + } + + guac_mem_free(str_port); + + } + + /* Just send the packet and continue the connection, or return if failed. */ + else if(guac_wol_wake(settings->wol_mac_addr, + settings->wol_broadcast_addr, + settings->wol_udp_port)) { + guac_client_log(client, GUAC_LOG_ERROR, "Failed to send WOL packet."); return NULL; - - /* If wait time is specified, sleep for that amount of time. */ - if (settings->wol_wait_time > 0) - guac_timestamp_msleep(settings->wol_wait_time * 1000); + } } /* Configure clipboard encoding */ @@ -363,6 +474,33 @@ void* guac_vnc_client_thread(void* data) { return NULL; } + /* Import the public key, if that is specified. */ + if (settings->sftp_public_key != NULL) { + + guac_client_log(client, GUAC_LOG_DEBUG, + "Attempting public key import"); + + /* Attempt to read public key */ + if (guac_common_ssh_user_import_public_key(vnc_client->sftp_user, + settings->sftp_public_key)) { + + /* Public key import fails. */ + guac_client_abort(client, + GUAC_PROTOCOL_STATUS_CLIENT_UNAUTHORIZED, + "Failed to import public key: %s", + guac_common_ssh_key_error()); + + guac_common_ssh_destroy_user(vnc_client->sftp_user); + return NULL; + + } + + /* Success */ + guac_client_log(client, GUAC_LOG_INFO, + "Public key successfully imported."); + + } + } /* Otherwise, use specified password */ @@ -376,8 +514,8 @@ void* guac_vnc_client_thread(void* data) { /* Attempt SSH connection */ vnc_client->sftp_session = guac_common_ssh_create_session(client, settings->sftp_hostname, - settings->sftp_port, vnc_client->sftp_user, settings->sftp_server_alive_interval, - settings->sftp_host_key, NULL); + settings->sftp_port, vnc_client->sftp_user, settings->sftp_timeout, + settings->sftp_server_alive_interval, settings->sftp_host_key, NULL); /* Fail if SSH connection does not succeed */ if (vnc_client->sftp_session == NULL) { @@ -416,6 +554,26 @@ void* guac_vnc_client_thread(void* data) { } #endif + /* Disable remote console (Server input) */ + if (settings->disable_server_input) { + rfbSetServerInputMsg msg; + msg.type = rfbSetServerInput; + msg.status = 1; + msg.pad = 0; + + /* Acquire lock for writing to server. */ + pthread_mutex_lock(&(vnc_client->message_lock)); + + if (WriteToRFBServer(rfb_client, (char*)&msg, sz_rfbSetServerInputMsg)) + guac_client_log(client, GUAC_LOG_DEBUG, "Successfully sent request to disable server input."); + + else + guac_client_log(client, GUAC_LOG_WARNING, "Failed to send request to disable server input."); + + /* Release lock. */ + pthread_mutex_unlock(&(vnc_client->message_lock)); + } + /* Set remaining client data */ vnc_client->rfb_client = rfb_client; @@ -428,97 +586,71 @@ void* guac_vnc_client_thread(void* data) { !settings->recording_exclude_output, !settings->recording_exclude_mouse, 0, /* Touch events not supported */ - settings->recording_include_keys); + settings->recording_include_keys, + settings->recording_write_existing); } /* Create display */ - vnc_client->display = guac_common_display_alloc(client, - rfb_client->width, rfb_client->height); + vnc_client->display = guac_display_alloc(client); + guac_display_layer_resize(guac_display_default_layer(vnc_client->display), rfb_client->width, rfb_client->height); /* Use lossless compression only if requested (otherwise, use default * heuristics) */ - guac_common_display_set_lossless(vnc_client->display, settings->lossless); + guac_display_layer_set_lossless(guac_display_default_layer(vnc_client->display), + settings->lossless); + + /* If compression and display quality have been configured, set those. */ + if (settings->compress_level >= 0 && settings->compress_level <= 9) + rfb_client->appData.compressLevel = settings->compress_level; + + if (settings->quality_level >= 0 && settings->quality_level <= 9) + rfb_client->appData.qualityLevel = settings->quality_level; /* If not read-only, set an appropriate cursor */ if (settings->read_only == 0) { if (settings->remote_cursor) - guac_common_cursor_set_dot(vnc_client->display->cursor); + guac_display_set_cursor(vnc_client->display, GUAC_DISPLAY_CURSOR_DOT); else - guac_common_cursor_set_pointer(vnc_client->display->cursor); - + guac_display_set_cursor(vnc_client->display, GUAC_DISPLAY_CURSOR_POINTER); } - guac_socket_flush(client->socket); +#ifdef LIBVNC_HAS_RESIZE_SUPPORT + /* Set initial state of the screen and resize flags. */ + vnc_client->rfb_screen_initialized = false; + vnc_client->rfb_initial_resize = false; +#endif // LIBVNC_HAS_RESIZE_SUPPORT + + guac_display_end_frame(vnc_client->display); - guac_timestamp last_frame_end = guac_timestamp_current(); + vnc_client->render_thread = guac_display_render_thread_create(vnc_client->display); /* Handle messages from VNC server while client is running */ while (client->state == GUAC_CLIENT_RUNNING) { - /* Wait for start of frame */ - int wait_result = guac_vnc_wait_for_messages(rfb_client, - GUAC_VNC_FRAME_START_TIMEOUT); + /* Wait for data and construct a reasonable frame */ + int wait_result = guac_vnc_wait_for_messages(rfb_client, GUAC_VNC_MESSAGE_CHECK_INTERVAL); if (wait_result > 0) { - int processing_lag = guac_client_get_processing_lag(client); - guac_timestamp frame_start = guac_timestamp_current(); - - /* Read server messages until frame is built */ - do { - - guac_timestamp frame_end; - int frame_remaining; - - /* Handle any message received */ - if (!HandleRFBServerMessage(rfb_client)) { - guac_client_abort(client, - GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, - "Error handling message from VNC server."); - break; - } - - /* Calculate time remaining in frame */ - frame_end = guac_timestamp_current(); - frame_remaining = frame_start + GUAC_VNC_FRAME_DURATION - - frame_end; - - /* Calculate time that client needs to catch up */ - int time_elapsed = frame_end - last_frame_end; - int required_wait = processing_lag - time_elapsed; - - /* Increase the duration of this frame if client is lagging */ - if (required_wait > GUAC_VNC_FRAME_TIMEOUT) - wait_result = guac_vnc_wait_for_messages(rfb_client, - required_wait*1000); - - /* Wait again if frame remaining */ - else if (frame_remaining > 0) - wait_result = guac_vnc_wait_for_messages(rfb_client, - GUAC_VNC_FRAME_TIMEOUT*1000); - else - break; - - } while (wait_result > 0); - - /* Record end of frame, excluding server-side rendering time (we - * assume server-side rendering time will be consistent between any - * two subsequent frames, and that this time should thus be - * excluded from the required wait period of the next frame). */ - last_frame_end = frame_start; + /* Handle any message received */ + if (!guac_vnc_handle_messages(client)) { + guac_client_abort(client, + GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, + "Error handling message from VNC server."); + break; + } } /* If an error occurs, log it and fail */ - if (wait_result < 0) + else if (wait_result < 0) guac_client_abort(client, GUAC_PROTOCOL_STATUS_UPSTREAM_ERROR, "Connection closed."); - /* Flush frame */ - guac_common_surface_flush(vnc_client->display->default_surface); - guac_client_end_frame(client); - guac_socket_flush(client->socket); - } + /* Stop render loop */ + guac_display_render_thread_destroy(vnc_client->render_thread); + vnc_client->render_thread = NULL; + /* Kill client and finish connection */ guac_client_stop(client); guac_client_log(client, GUAC_LOG_INFO, "Internal VNC client disconnected"); diff --git a/src/protocols/vnc/vnc.h b/src/protocols/vnc/vnc.h index bdc62e6de..dbe857c42 100644 --- a/src/protocols/vnc/vnc.h +++ b/src/protocols/vnc/vnc.h @@ -23,12 +23,12 @@ #include "config.h" #include "common/clipboard.h" -#include "common/display.h" #include "common/iconv.h" -#include "common/surface.h" +#include "display.h" #include "settings.h" #include +#include #include #include @@ -46,6 +46,12 @@ #include +/** + * The ID of the RFB client screen. If multi-screen support is added, more than + * one ID will be needed as well. + */ +#define GUAC_VNC_SCREEN_ID 1 + /** * VNC-specific client data. */ @@ -63,6 +69,11 @@ typedef struct guac_vnc_client { pthread_mutex_t tls_lock; #endif + /** + * Lock which synchronizes messages sent to VNC server. + */ + pthread_mutex_t message_lock; + /** * The underlying VNC client. */ @@ -74,6 +85,12 @@ typedef struct guac_vnc_client { */ MallocFrameBufferProc rfb_MallocFrameBuffer; + /** + * The original CopyRect processing procedure provided by the initialized + * rfbClient. + */ + GotCopyRectProc rfb_GotCopyRect; + /** * Whether copyrect was used to produce the latest update received * by the VNC server. @@ -88,7 +105,19 @@ typedef struct guac_vnc_client { /** * The current display state. */ - guac_common_display* display; + guac_display* display; + + /** + * The context of the current drawing (update) operation, if any. If no + * operation is in progress, this will be NULL. + */ + guac_display_layer_raw_context* current_context; + + /** + * The current instance of the guac_display render thread. If the thread + * has not yet been started, this will be NULL. + */ + guac_display_render_thread* render_thread; /** * Internal clipboard. @@ -135,6 +164,19 @@ typedef struct guac_vnc_client { */ guac_iconv_write* clipboard_writer; +#ifdef LIBVNC_HAS_RESIZE_SUPPORT + /** + * Whether or not the server has sent the required message to initialize + * the screen data in the client. + */ + bool rfb_screen_initialized; + + /** + * Whether or not the client has sent it's starting size to the server. + */ + bool rfb_initial_resize; +#endif + } guac_vnc_client; /** diff --git a/src/terminal/buffer.c b/src/terminal/buffer.c index 59c10209b..995ce3295 100644 --- a/src/terminal/buffer.c +++ b/src/terminal/buffer.c @@ -19,13 +19,90 @@ #include "terminal/buffer.h" #include "terminal/common.h" +#include "terminal/terminal.h" +#include #include +#include #include #include -guac_terminal_buffer* guac_terminal_buffer_alloc(int rows, guac_terminal_char* default_character) { +/** + * The minimum number of columns to allocate for a buffer row, regardless of + * the terminal size. We set a minimum size here to reduce the memory + * reallocation overhead for small rows. + */ +#define GUAC_TERMINAL_BUFFER_ROW_MIN_SIZE 256 + +/** + * A single variable-length row of terminal data. + */ +typedef struct guac_terminal_buffer_row { + + /** + * Array of guac_terminal_char representing the contents of the row. + */ + guac_terminal_char* characters; + + /** + * The length of this row in characters. This is the number of initialized + * characters in the buffer, usually equal to the number of characters + * in the screen width at the time this row was created. + */ + unsigned int length; + + /** + * The number of elements in the characters array. After the length + * equals this value, the array must be resized. + */ + unsigned int available; + + /** + * True if the current row has been wrapped to avoid going off the screen. + * False otherwise. + */ + bool wrapped_row; + +} guac_terminal_buffer_row; + +struct guac_terminal_buffer { + + /** + * The character to assign to newly-allocated cells. + */ + guac_terminal_char default_character; + + /** + * Array of buffer rows. This array functions as a ring buffer. + * When a new row needs to be appended, the top reference is moved down + * and the old top row is replaced. + */ + guac_terminal_buffer_row* rows; + + /** + * The index of the first row in the buffer (the row which represents row 0 + * with respect to the terminal display). This is also the index of the row + * to replace when insufficient space remains in the buffer to add a new + * row. + */ + unsigned int top; + + /** + * The number of rows currently stored in the buffer. + */ + unsigned int length; + + /** + * The number of rows in the buffer. This is the total capacity + * of the buffer. + */ + unsigned int available; + +}; + +guac_terminal_buffer* guac_terminal_buffer_alloc(int rows, + const guac_terminal_char* default_character) { /* Allocate scrollback */ guac_terminal_buffer* buffer = @@ -46,8 +123,9 @@ guac_terminal_buffer* guac_terminal_buffer_alloc(int rows, guac_terminal_char* d for (i=0; iavailable = 256; + row->available = GUAC_TERMINAL_BUFFER_ROW_MIN_SIZE; row->length = 0; + row->wrapped_row = false; row->characters = guac_mem_alloc(sizeof(guac_terminal_char), row->available); /* Next row */ @@ -76,66 +154,231 @@ void guac_terminal_buffer_free(guac_terminal_buffer* buffer) { } -guac_terminal_buffer_row* guac_terminal_buffer_get_row(guac_terminal_buffer* buffer, int row, int width) { +void guac_terminal_buffer_reset(guac_terminal_buffer* buffer) { + buffer->top = 0; + buffer->length = 0; +} - int i; - guac_terminal_char* first; - guac_terminal_buffer_row* buffer_row; +/** + * Returns the row at the given location. The row returned is guaranteed to be at least the given + * width. + * + * @param buffer + * The buffer to retrieve a row from. + * + * @param row + * The index of the row to retrieve, where zero is the top-most row. + * Negative indices represent rows in the scrollback buffer, above the + * top-most row. + * + * @return + * The buffer row at the given location, or NULL if there is no such row. + */ +static guac_terminal_buffer_row* guac_terminal_buffer_get_row(guac_terminal_buffer* buffer, int row) { + + if (abs(row) >= buffer->available) + return NULL; /* Normalize row index into a scrollback buffer index */ - int index = (buffer->top + row) % buffer->available; - if (index < 0) - index += buffer->available; + unsigned int index = (buffer->top + row) % buffer->available; + return &(buffer->rows[index]); - /* Get row */ - buffer_row = &(buffer->rows[index]); +} + +/** + * Rounds the given value up to the nearest possible row length. To avoid + * unnecessary, repeated resizing of rows, each row length is rounded up to the + * nearest power of two. + * + * @param value + * The value to round. + * + * @return + * The power of two that is closest to the given value without exceeding + * that value. + */ +static unsigned int guac_terminal_buffer_row_length(int value) { + + GUAC_ASSERT(value >= 0); + GUAC_ASSERT(value <= GUAC_TERMINAL_MAX_COLUMNS); + + unsigned int rounded = GUAC_TERMINAL_BUFFER_ROW_MIN_SIZE; + while (rounded < value) + rounded <<= 1; + + return rounded; + +} + +/** + * Expands the amount of space allocated for the given row such that it + * may contain at least the given number of characters, if possible. If the row + * cannot be expanded due to buffer size limitations, it will be expanded to + * the greatest size allowed without exceeding those limits. + * + * @param row + * The row to expand. + * + * @param length + * The number of characters that the row must be able to store. + * + * @param default_character + * The character that should fill any newly-allocated character cells. + */ +static void guac_terminal_buffer_row_expand(guac_terminal_buffer_row* row, int length, + const guac_terminal_char* default_character) { + + /* Bail out if no resize/init is necessary */ + if (length <= row->length) + return; - /* If resizing is needed */ - if (width >= buffer_row->length) { + /* Limit maximum possible row size to the limits of the terminal display */ + if (length > GUAC_TERMINAL_MAX_COLUMNS) + length = GUAC_TERMINAL_MAX_COLUMNS; - /* Expand if necessary */ - if (width > buffer_row->available) { - buffer_row->available = guac_mem_ckd_mul_or_die(width, 2); - buffer_row->characters = guac_mem_realloc_or_die(buffer_row->characters, - sizeof(guac_terminal_char), buffer_row->available); + /* Expand allocated memory if there is otherwise insufficient space to fit + * the provided length */ + if (length > row->available) { + row->available = guac_terminal_buffer_row_length(length); + row->characters = guac_mem_realloc_or_die(row->characters, + sizeof(guac_terminal_char), row->available); + } + + /* Initialize new part of row */ + for (int i = row->length; i < row->available; i++) + row->characters[i] = *default_character; + + row->length = length; + +} + +/** + * Enforces a character break at the given edge, ensuring that the left side + * of the edge is the final column of a character, and the right side of the + * edge is the initial column of a DIFFERENT character. + * + * @param buffer + * The buffer containing the character. + * + * @param row + * The row index of the row containing the character. + * + * @param edge + * The relative edge number where a break is required. For a character in + * column N, that character's left edge is N and the right edge is N+1. + */ +static void guac_terminal_buffer_force_break(guac_terminal_buffer* buffer, int row, int edge) { + + guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row); + if (buffer_row == NULL) + return; + + /* Ensure character to left of edge is unbroken */ + if (edge > 0) { + + int end_column = edge - 1; + int start_column = end_column; + + guac_terminal_char* start_char = &(buffer_row->characters[start_column]); + + /* Determine start column */ + while (start_column > 0 && start_char->value == GUAC_CHAR_CONTINUATION) { + start_char--; + start_column--; + } + + /* Advance to start of broken character if necessary */ + if (start_char->value != GUAC_CHAR_CONTINUATION && start_char->width < end_column - start_column + 1) { + start_column += start_char->width; + start_char += start_char->width; } - /* Initialize new part of row */ - first = &(buffer_row->characters[buffer_row->length]); - for (i=buffer_row->length; idefault_character; + /* Clear character if broken */ + if (start_char->value == GUAC_CHAR_CONTINUATION || start_char->width != end_column - start_column + 1) { + + guac_terminal_char cleared_char; + cleared_char.value = ' '; + cleared_char.attributes = start_char->attributes; + cleared_char.width = 1; - buffer_row->length = width; + guac_terminal_buffer_set_columns(buffer, row, start_column, end_column, &cleared_char); + + } } - /* Return found row */ - return buffer_row; + /* Ensure character to right of edge is unbroken */ + if (edge >= 0 && edge < buffer_row->length) { + + int start_column = edge; + int end_column = start_column; + + guac_terminal_char* start_char = &(buffer_row->characters[start_column]); + guac_terminal_char* end_char = &(buffer_row->characters[end_column]); + + /* Determine end column */ + while (end_column+1 < buffer_row->length && (end_char+1)->value == GUAC_CHAR_CONTINUATION) { + end_char++; + end_column++; + } + + /* Advance to start of broken character if necessary */ + if (start_char->value != GUAC_CHAR_CONTINUATION && start_char->width < end_column - start_column + 1) { + start_column += start_char->width; + start_char += start_char->width; + } + + /* Clear character if broken */ + if (start_char->value == GUAC_CHAR_CONTINUATION || start_char->width != end_column - start_column + 1) { + + guac_terminal_char cleared_char; + cleared_char.value = ' '; + cleared_char.attributes = start_char->attributes; + cleared_char.width = 1; + + guac_terminal_buffer_set_columns(buffer, row, start_column, end_column, &cleared_char); + + } + + } } void guac_terminal_buffer_copy_columns(guac_terminal_buffer* buffer, int row, int start_column, int end_column, int offset) { - guac_terminal_char* src; - guac_terminal_char* dst; - /* Get row */ - guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row, end_column + offset + 1); + guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row); + if (buffer_row == NULL) + return; - /* Fit range within bounds */ - start_column = guac_terminal_fit_to_range(start_column, 0, buffer_row->length - 1); - end_column = guac_terminal_fit_to_range(end_column, 0, buffer_row->length - 1); - start_column = guac_terminal_fit_to_range(start_column + offset, 0, buffer_row->length - 1) - offset; - end_column = guac_terminal_fit_to_range(end_column + offset, 0, buffer_row->length - 1) - offset; + guac_terminal_buffer_row_expand(buffer_row, end_column + offset + 1, &buffer->default_character); + GUAC_ASSERT(buffer_row->length >= end_column + offset + 1); + + /* Fit relevant extents of operation within bounds (NOTE: Because this + * operation is relative and represents the destination with an offset, + * there's no need to recalculate the destination region - the offset + * simply remains the same) */ + if (offset >= 0) { + start_column = guac_terminal_fit_to_range(start_column, 0, buffer_row->length - offset - 1); + end_column = guac_terminal_fit_to_range(end_column, start_column, buffer_row->length - offset - 1); + } + else { + start_column = guac_terminal_fit_to_range(start_column, -offset, buffer_row->length - 1); + end_column = guac_terminal_fit_to_range(end_column, start_column, buffer_row->length - 1); + } /* Determine source and destination locations */ - src = &(buffer_row->characters[start_column]); - dst = &(buffer_row->characters[start_column + offset]); + guac_terminal_char* src = &(buffer_row->characters[start_column]); + guac_terminal_char* dst = &(buffer_row->characters[start_column + offset]); /* Copy data */ memmove(dst, src, sizeof(guac_terminal_char) * (end_column - start_column + 1)); + /* Force breaks around destination region */ + guac_terminal_buffer_force_break(buffer, row, start_column + offset); + guac_terminal_buffer_force_break(buffer, row, end_column + offset + 1); + } void guac_terminal_buffer_copy_rows(guac_terminal_buffer* buffer, @@ -160,12 +403,22 @@ void guac_terminal_buffer_copy_rows(guac_terminal_buffer* buffer, for (i = start_row; i <= end_row; i++) { /* Get source and destination rows */ - guac_terminal_buffer_row* src_row = guac_terminal_buffer_get_row(buffer, current_row, 0); - guac_terminal_buffer_row* dst_row = guac_terminal_buffer_get_row(buffer, current_row + offset, src_row->length); + guac_terminal_buffer_row* src_row = guac_terminal_buffer_get_row(buffer, current_row); + guac_terminal_buffer_row* dst_row = guac_terminal_buffer_get_row(buffer, current_row + offset); + + if (src_row == NULL || dst_row == NULL) + continue; + + guac_terminal_buffer_row_expand(dst_row, src_row->length, &buffer->default_character); + GUAC_ASSERT(dst_row->length >= src_row->length); /* Copy data */ - memcpy(dst_row->characters, src_row->characters, sizeof(guac_terminal_char) * src_row->length); + memcpy(dst_row->characters, src_row->characters, guac_mem_ckd_mul_or_die(sizeof(guac_terminal_char), src_row->length)); dst_row->length = src_row->length; + dst_row->wrapped_row = src_row->wrapped_row; + + /* Reset src wrapped_row */ + src_row->wrapped_row = false; /* Next current_row */ current_row += step; @@ -174,40 +427,140 @@ void guac_terminal_buffer_copy_rows(guac_terminal_buffer* buffer, } +void guac_terminal_buffer_scroll_up(guac_terminal_buffer* buffer, int amount) { + + if (amount <= 0) + return; + + buffer->top = (buffer->top + amount) % buffer->available; + + buffer->length += amount; + if (buffer->length > buffer->available) + buffer->length = buffer->available; + +} + +void guac_terminal_buffer_scroll_down(guac_terminal_buffer* buffer, int amount) { + + if (amount <= 0) + return; + + buffer->top = (buffer->top - amount) % buffer->available; + +} + +unsigned int guac_terminal_buffer_get_columns(guac_terminal_buffer* buffer, + guac_terminal_char** characters, bool* is_wrapped, int row) { + + guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row); + if (buffer_row == NULL) + return 0; + + if (characters != NULL) + *characters = buffer_row->characters; + + if (is_wrapped != NULL) + *is_wrapped = buffer_row->wrapped_row; + + return buffer_row->length; + +} + void guac_terminal_buffer_set_columns(guac_terminal_buffer* buffer, int row, int start_column, int end_column, guac_terminal_char* character) { - int i, j; - guac_terminal_char* current; + /* Do nothing if there's nothing to do (glyph is empty) or if nothing + * sanely can be done (row is impossibly large or glyph has an invalid + * width) */ + if (character->width <= 0 || row >= GUAC_TERMINAL_MAX_ROWS || row <= -GUAC_TERMINAL_MAX_ROWS) + return; - /* Do nothing if glyph is empty */ - if (character->width == 0) + /* Do nothing if there is no such row within the buffer (the given row index + * does not refer to an actual row, even considering scrollback) */ + guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row); + if (buffer_row == NULL) return; /* Build continuation char (for multicolumn characters) */ - guac_terminal_char continuation_char; - continuation_char.value = GUAC_CHAR_CONTINUATION; - continuation_char.attributes = character->attributes; - continuation_char.width = 0; /* Not applicable for GUAC_CHAR_CONTINUATION */ + guac_terminal_char continuation_char = { + .value = GUAC_CHAR_CONTINUATION, + .attributes = character->attributes, + .width = 0 /* Not applicable for GUAC_CHAR_CONTINUATION */ + }; - /* Get and expand row */ - guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row, end_column+1); + start_column = guac_terminal_fit_to_range(start_column, 0, GUAC_TERMINAL_MAX_COLUMNS - 1); + end_column = guac_terminal_fit_to_range(end_column, 0, GUAC_TERMINAL_MAX_COLUMNS - 1); - /* Set values */ - current = &(buffer_row->characters[start_column]); - for (i = start_column; i <= end_column; i += character->width) { + guac_terminal_buffer_row_expand(buffer_row, end_column + 1, &buffer->default_character); + GUAC_ASSERT(buffer_row->length >= end_column + 1); - *(current++) = *character; + int remaining_continuation_chars = 0; + for (int i = start_column; i <= end_column; i++) { /* Store any required continuation characters */ - for (j=1; j < character->width; j++) - *(current++) = continuation_char; + if (remaining_continuation_chars > 0) { + buffer_row->characters[i] = continuation_char; + remaining_continuation_chars--; + } + else { + buffer_row->characters[i] = *character; + remaining_continuation_chars = character->width - 1; + } } /* Update length depending on row written */ if (character->value != 0 && row >= buffer->length) - buffer->length = row+1; + buffer->length = row + 1; + + /* Force breaks around destination region */ + guac_terminal_buffer_force_break(buffer, row, start_column); + guac_terminal_buffer_force_break(buffer, row, end_column + 1); + +} + +void guac_terminal_buffer_set_cursor(guac_terminal_buffer* buffer, int row, + int column, bool is_cursor) { + + /* Do if nothing sanely can be done (row is impossibly large) */ + if (row >= GUAC_TERMINAL_MAX_ROWS || row <= -GUAC_TERMINAL_MAX_ROWS) + return; + + /* Do nothing if there is no such row within the buffer (the given row index + * does not refer to an actual row, even considering scrollback) */ + guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row); + if (buffer_row == NULL) + return; + + column = guac_terminal_fit_to_range(column, 0, GUAC_TERMINAL_MAX_COLUMNS - 1); + + guac_terminal_buffer_row_expand(buffer_row, column + 1, &buffer->default_character); + GUAC_ASSERT(buffer_row->length >= column + 1); + + buffer_row->characters[column].attributes.cursor = is_cursor; + +} + +unsigned int guac_terminal_buffer_effective_length(guac_terminal_buffer* buffer, int scrollback) { + + /* If the buffer contains more rows than requested, pretend it only + * contains the requested number of rows */ + unsigned int effective_length = buffer->length; + if (effective_length > scrollback) + effective_length = scrollback; + + return effective_length; + +} + + +void guac_terminal_buffer_set_wrapped(guac_terminal_buffer* buffer, int row, bool wrapped) { + + guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row); + if (buffer_row == NULL) + return; + + buffer_row->wrapped_row = wrapped; } diff --git a/src/terminal/common.c b/src/terminal/common.c index 5f5509dbe..3b83d4c82 100644 --- a/src/terminal/common.c +++ b/src/terminal/common.c @@ -17,13 +17,20 @@ * under the License. */ +#include "terminal/common.h" #include "terminal/types.h" +#include + #include #include int guac_terminal_fit_to_range(int value, int min, int max) { + /* This should never happen outside a logic error in the caller, but best + * to bail out here where debugging will be less onerous */ + GUAC_ASSERT(min <= max); + if (value < min) return min; if (value > max) return max; diff --git a/src/terminal/display.c b/src/terminal/display.c index ad0ec48e3..7d4748dd6 100644 --- a/src/terminal/display.c +++ b/src/terminal/display.c @@ -32,6 +32,7 @@ #include #include +#include #include #include #include @@ -202,6 +203,19 @@ int __guac_terminal_set(guac_terminal_display* display, int row, int col, int co } +/** + * Calculate the size of margins around the terminal based on DPI. + * + * @param dpi + * The resolution of the display in DPI. + * + * @return + * Calculated size of margin in pixels. + */ +static int get_margin_by_dpi(int dpi) { + return dpi * GUAC_TERMINAL_MARGINS / GUAC_TERMINAL_MM_PER_INCH; +} + guac_terminal_display* guac_terminal_display_alloc(guac_client* client, const char* font_name, int font_size, int dpi, guac_terminal_color* foreground, guac_terminal_color* background, @@ -229,6 +243,13 @@ guac_terminal_display* guac_terminal_display_alloc(guac_client* client, guac_protocol_send_move(client->socket, display->select_layer, display->display_layer, 0, 0, 0); + /* Calculate margin size by DPI */ + display->margin = get_margin_by_dpi(dpi); + + /* Offset the Default Layer to make margins even on all sides */ + guac_protocol_send_move(client->socket, display->display_layer, + GUAC_DEFAULT_LAYER, display->margin, display->margin, 0); + display->default_foreground = display->glyph_foreground = *foreground; display->default_background = display->glyph_background = *background; display->default_palette = palette; @@ -328,39 +349,52 @@ int guac_terminal_display_lookup_color(guac_terminal_display* display, void guac_terminal_display_copy_columns(guac_terminal_display* display, int row, int start_column, int end_column, int offset) { - int i; - guac_terminal_operation* src_current; - guac_terminal_operation* current; - /* Ignore operations outside display bounds */ if (row < 0 || row >= display->height) return; - /* Fit range within bounds */ - start_column = guac_terminal_fit_to_range(start_column, 0, display->width - 1); - end_column = guac_terminal_fit_to_range(end_column, 0, display->width - 1); - start_column = guac_terminal_fit_to_range(start_column + offset, 0, display->width - 1) - offset; - end_column = guac_terminal_fit_to_range(end_column + offset, 0, display->width - 1) - offset; + /* Fit relevant extents of operation within bounds (NOTE: Because this + * operation is relative and represents the destination with an offset, + * there's no need to recalculate the destination region - the offset + * simply remains the same) */ + if (offset >= 0) { + start_column = guac_terminal_fit_to_range(start_column, 0, display->width - offset - 1); + end_column = guac_terminal_fit_to_range(end_column, start_column, display->width - offset - 1); + } + else { + start_column = guac_terminal_fit_to_range(start_column, -offset, display->width - 1); + end_column = guac_terminal_fit_to_range(end_column, start_column, display->width - 1); + } + + /* Determine source and destination locations */ + + size_t row_offset = guac_mem_ckd_mul_or_die(row, display->width); + size_t src_offset = guac_mem_ckd_add_or_die(row_offset, start_column); + + size_t dst_offset; + if (offset >= 0) + dst_offset = guac_mem_ckd_add_or_die(src_offset, offset); + else + dst_offset = guac_mem_ckd_sub_or_die(src_offset, -offset); - src_current = &(display->operations[row * display->width + start_column]); - current = &(display->operations[row * display->width + start_column + offset]); + guac_terminal_operation* src = &(display->operations[src_offset]); + guac_terminal_operation* dst = &(display->operations[dst_offset]); - /* Move data */ - memmove(current, src_current, - (end_column - start_column + 1) * sizeof(guac_terminal_operation)); + /* Copy data */ + memmove(dst, src, guac_mem_ckd_mul_or_die(sizeof(guac_terminal_operation), (end_column - start_column + 1))); /* Update operations */ - for (i=start_column; i<=end_column; i++) { + for (int column = start_column; column <= end_column; column++) { /* If no operation here, set as copy */ - if (current->type == GUAC_CHAR_NOP) { - current->type = GUAC_CHAR_COPY; - current->row = row; - current->column = i; + if (dst->type == GUAC_CHAR_NOP) { + dst->type = GUAC_CHAR_COPY; + dst->row = row; + dst->column = column; } /* Next column */ - current++; + dst++; } @@ -369,28 +403,42 @@ void guac_terminal_display_copy_columns(guac_terminal_display* display, int row, void guac_terminal_display_copy_rows(guac_terminal_display* display, int start_row, int end_row, int offset) { - int row, col; - guac_terminal_operation* src_current_row; - guac_terminal_operation* current_row; + /* Fit relevant extents of operation within bounds (NOTE: Because this + * operation is relative and represents the destination with an offset, + * there's no need to recalculate the destination region - the offset + * simply remains the same) */ + if (offset >= 0) { + start_row = guac_terminal_fit_to_range(start_row, 0, display->height - offset - 1); + end_row = guac_terminal_fit_to_range(end_row, start_row, display->height - offset - 1); + } + else { + start_row = guac_terminal_fit_to_range(start_row, -offset, display->height - 1); + end_row = guac_terminal_fit_to_range(end_row, start_row, display->height - 1); + } - /* Fit range within bounds */ - start_row = guac_terminal_fit_to_range(start_row, 0, display->height - 1); - end_row = guac_terminal_fit_to_range(end_row, 0, display->height - 1); - start_row = guac_terminal_fit_to_range(start_row + offset, 0, display->height - 1) - offset; - end_row = guac_terminal_fit_to_range(end_row + offset, 0, display->height - 1) - offset; + /* Determine source and destination locations */ + + size_t dst_start_row; + if (offset >= 0) + dst_start_row = guac_mem_ckd_add_or_die(start_row, offset); + else + dst_start_row = guac_mem_ckd_sub_or_die(start_row, -offset); - src_current_row = &(display->operations[start_row * display->width]); - current_row = &(display->operations[(start_row + offset) * display->width]); + size_t src_offset = guac_mem_ckd_mul_or_die(start_row, display->width); + size_t dst_offset = guac_mem_ckd_mul_or_die(dst_start_row, display->width); - /* Move data */ - memmove(current_row, src_current_row, - (end_row - start_row + 1) * sizeof(guac_terminal_operation) * display->width); + guac_terminal_operation* src = &(display->operations[src_offset]); + guac_terminal_operation* dst = &(display->operations[dst_offset]); + + /* Copy data */ + memmove(dst, src, guac_mem_ckd_mul_or_die(sizeof(guac_terminal_operation), + display->width, (end_row - start_row + 1))); /* Update operations */ - for (row=start_row; row<=end_row; row++) { + for (int row = start_row; row <= end_row; row++) { - guac_terminal_operation* current = current_row; - for (col=0; colwidth; col++) { + guac_terminal_operation* current = dst; + for (int col = 0; col < display->width; col++) { /* If no operation here, set as copy */ if (current->type == GUAC_CHAR_NOP) { @@ -405,7 +453,7 @@ void guac_terminal_display_copy_rows(guac_terminal_display* display, } /* Next row */ - current_row += display->width; + dst += display->width; } @@ -414,9 +462,6 @@ void guac_terminal_display_copy_rows(guac_terminal_display* display, void guac_terminal_display_set_columns(guac_terminal_display* display, int row, int start_column, int end_column, guac_terminal_char* character) { - int i; - guac_terminal_operation* current; - /* Do nothing if glyph is empty */ if (character->width == 0) return; @@ -429,10 +474,16 @@ void guac_terminal_display_set_columns(guac_terminal_display* display, int row, start_column = guac_terminal_fit_to_range(start_column, 0, display->width - 1); end_column = guac_terminal_fit_to_range(end_column, 0, display->width - 1); - current = &(display->operations[row * display->width + start_column]); + size_t start_offset = guac_mem_ckd_add_or_die(guac_mem_ckd_mul_or_die(row, display->width), start_column); + guac_terminal_operation* current = &(display->operations[start_offset]); /* For each column in range */ - for (i = start_column; i <= end_column; i += character->width) { + for (int col = start_column; col <= end_column; col += character->width) { + + /* Flush pending copy operation before adding new SET operation. This + * avoid operation conflicts that cause inconsistent display. */ + if (current->type == GUAC_CHAR_COPY) + guac_terminal_display_flush(display); /* Set operation */ current->type = GUAC_CHAR_SET; @@ -447,8 +498,12 @@ void guac_terminal_display_set_columns(guac_terminal_display* display, int row, void guac_terminal_display_resize(guac_terminal_display* display, int width, int height) { - guac_terminal_operation* current; - int x, y; + /* Resize display only if dimensions have changed */ + if (width == display->width && height == display->height) + return; + + GUAC_ASSERT(width >= 0 && width <= GUAC_TERMINAL_MAX_COLUMNS); + GUAC_ASSERT(height >= 0 && height <= GUAC_TERMINAL_MAX_ROWS); /* Fill with background color */ guac_terminal_char fill = { @@ -469,11 +524,11 @@ void guac_terminal_display_resize(guac_terminal_display* display, int width, int sizeof(guac_terminal_operation)); /* Init each operation buffer row */ - current = display->operations; - for (y=0; yoperations; + for (int y = 0; y < height; y++) { /* Init entire row to NOP */ - for (x=0; xwidth && y < display->height) @@ -827,6 +882,10 @@ void guac_terminal_display_dup( guac_protocol_send_move(socket, display->select_layer, display->display_layer, 0, 0, 0); + /* Offset the Default Layer to make margins even on all sides */ + guac_protocol_send_move(socket, display->display_layer, + GUAC_DEFAULT_LAYER, display->margin, display->margin, 0); + /* Send select layer size */ guac_protocol_send_size(socket, display->select_layer, display->char_width * display->width, @@ -1019,6 +1078,7 @@ int guac_terminal_display_set_font(guac_terminal_display* display, if (new_width != display->width || new_height != display->height) guac_terminal_display_resize(display, new_width, new_height); + return 0; } diff --git a/src/terminal/select.c b/src/terminal/select.c index fdb5a7cfb..406a0c43f 100644 --- a/src/terminal/select.c +++ b/src/terminal/select.c @@ -146,13 +146,14 @@ void guac_terminal_select_redraw(guac_terminal* terminal) { static int guac_terminal_find_char(guac_terminal* terminal, int row, int* column) { + guac_terminal_char* characters; int start_column = *column; - guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(terminal->buffer, row, 0); - if (start_column < buffer_row->length) { + int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, NULL, row); + if (start_column >= 0 && start_column < length) { /* Find beginning of character */ - guac_terminal_char* start_char = &(buffer_row->characters[start_column]); + guac_terminal_char* start_char = &(characters[start_column]); while (start_column > 0 && start_char->value == GUAC_CHAR_CONTINUATION) { start_char--; start_column--; @@ -194,7 +195,7 @@ void guac_terminal_select_update(guac_terminal* terminal, int row, int column) { /* Only update if selection has changed */ if (row != terminal->selection_end_row - || column < terminal->selection_end_column + || column <= terminal->selection_end_column || column >= terminal->selection_end_column + terminal->selection_end_width) { int width = guac_terminal_find_char(terminal, row, &column); @@ -251,21 +252,21 @@ void guac_terminal_select_resume(guac_terminal* terminal, int row, int column) { } /** - * Appends the text within the given subsection of a terminal row to the + * Appends the text within the given array of terminal characters to the * clipboard. The provided coordinates are considered inclusively (the - * characters at the start and end column are included in the copied - * text). Any out-of-bounds coordinates will be automatically clipped within - * the bounds of the given row. + * characters at the start and end column are included in the copied text). Any + * out-of-bounds coordinates will be automatically clipped within the bounds of + * the given array. * * @param terminal * The guac_terminal instance associated with the buffer containing the * text being copied and the clipboard receiving the copied text. * - * @param row - * The row number of the text within the terminal to be copied into the - * clipboard, where the first (top-most) row in the terminal is row 0. Rows - * within the scrollback buffer (above the top-most row of the terminal) - * will be negative. + * @param characters + * The array of characters copied into the clipboard. + * + * @param length + * The number of characters in the provided character array. * * @param start * The first column of the text to be copied from the given row into the @@ -275,38 +276,44 @@ void guac_terminal_select_resume(guac_terminal* terminal, int row, int column) { * @param end * The last column of the text to be copied from the given row into the * clipboard associated with the given terminal, where 0 is the first - * (left-most) column within the row, or a negative value to denote that - * the last column in the row should be used. + * (left-most) column within the row. */ -static void guac_terminal_clipboard_append_row(guac_terminal* terminal, - int row, int start, int end) { +static void guac_terminal_clipboard_append_characters(guac_terminal* terminal, + guac_terminal_char* characters, unsigned int length, int start, int end) { char buffer[1024]; - int i = start; - - guac_terminal_buffer_row* buffer_row = - guac_terminal_buffer_get_row(terminal->buffer, row, 0); + int eol; /* If selection is entirely outside the bounds of the row, then there is * nothing to append */ - if (start < 0 || start > buffer_row->length - 1) + if (start < 0 || end < 0 || start >= length) return; - /* Clip given range to actual bounds of row */ - if (end < 0 || end > buffer_row->length - 1) - end = buffer_row->length - 1; + /* Ensure desired end column is within bounds */ + if (end >= length) + end = length - 1; + + /* Get position of last not null char */ + for (eol = end; eol > start; eol--) { + if (characters[eol].value != 0) + break; + } /* Repeatedly convert chunks of terminal buffer rows until entire specified * region has been appended to clipboard */ - while (i <= end) { + for (int i = start; i <= end;) { int remaining = sizeof(buffer); char* current = buffer; /* Convert as many codepoints within the given range as possible */ - for (i = start; i <= end; i++) { + for (; i <= end; i++) { + + int codepoint = characters[i].value; - int codepoint = buffer_row->characters[i].value; + /* Fill empty with spaces if not at end of line */ + if (codepoint == 0 && i < eol) + codepoint = GUAC_CHAR_SPACE; /* Ignore null (blank) characters */ if (codepoint == 0 || codepoint == GUAC_CHAR_CONTINUATION) @@ -351,29 +358,27 @@ void guac_terminal_select_end(guac_terminal* terminal) { guac_terminal_select_normalized_range(terminal, &start_row, &start_col, &end_row, &end_col); - /* If only one row, simply copy */ - if (end_row == start_row) - guac_terminal_clipboard_append_row(terminal, start_row, start_col, end_col); + guac_terminal_char* characters; + bool last_row_was_wrapped = true; - /* Otherwise, copy multiple rows */ - else { + for (int row = start_row; row <= end_row; row++) { - /* Store first row */ - guac_terminal_clipboard_append_row(terminal, start_row, start_col, -1); - - /* Store all middle rows */ - for (int row = start_row + 1; row < end_row; row++) { + /* Add a newline only if the previous line was not wrapped */ + if (!last_row_was_wrapped) guac_common_clipboard_append(terminal->clipboard, "\n", 1); - guac_terminal_clipboard_append_row(terminal, row, 0, -1); - } - /* Store last row */ - guac_common_clipboard_append(terminal->clipboard, "\n", 1); - guac_terminal_clipboard_append_row(terminal, end_row, 0, end_col); + /* Append next row from desired region, adjusting the start/end column + * to account for selections that start or end in the middle of a row. + * With the exception of the start and end rows, all other rows are + * copied in their entirety. */ + int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &last_row_was_wrapped, row); + guac_terminal_clipboard_append_characters(terminal, characters, length, + (row == start_row) ? start_col : 0, + (row == end_row) ? end_col : length - 1); } - /* Send data */ + /* Broadcast copied data to all connected users only if allowed */ if (!terminal->disable_copy) { guac_common_clipboard_send(terminal->clipboard, client); guac_socket_flush(socket); diff --git a/src/terminal/terminal-handlers.c b/src/terminal/terminal-handlers.c index 1cff2d8d6..cbdf922d1 100644 --- a/src/terminal/terminal-handlers.c +++ b/src/terminal/terminal-handlers.c @@ -31,6 +31,7 @@ #include #include +#include #include #include #include @@ -50,6 +51,107 @@ */ #define GUAC_TERMINAL_OK "\x1B[0n" +/** + * Flag number for the DEC Private Mode Set (DECSET) operation that switches + * from the normal buffer to the alternate buffer. + * + * NOTE: Switching to the alternate buffer is common for text editors that wish + * to present a user interface yet preserve the original contents of the + * terminal. + */ +#define GUAC_TERMINAL_DECSET_USE_ALT_BUFFER 47 + +/** + * Flag number for the DEC Private Mode Set (DECSET) operation that switches + * from the normal buffer to the alternate buffer AND clears the alternate + * buffer (but only if the alternate buffer wasn't already selected). + * + * @see GUAC_TERMINAL_DECSET_USE_ALT_BUFFER + */ +#define GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_CLEAR 1047 + +/** + * Flag number for the DEC Private Mode Set (DECSET) operation that saves the + * current cursor location. The cursor location can later be restored through + * GUAC_TERMINAL_DECRST_RESTORE_CURSOR and related operations. + * + * @see GUAC_TERMINAL_DECRST_RESTORE_CURSOR + * @see GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER_AND_RESTORE_CURSOR + */ +#define GUAC_TERMINAL_DECSET_SAVE_CURSOR 1048 + +/** + * Flag number for the DEC Private Mode Set (DECSET) operation that saves the + * current cursor location, switches to the alternate buffer, AND clears the + * alternate buffer if the alternate buffer was not already selected. + * + * This is effectively a combination of GUAC_TERMINAL_DECSET_SAVE_CURSOR and + * GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_CLEAR. + * + * @see GUAC_TERMINAL_DECSET_SAVE_CURSOR + * @see GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_CLEAR + */ +#define GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_SAVE_CURSOR_AND_CLEAR 1049 + +/** + * Flag number for the DEC Private Mode Reset (DECRST) operation that switches + * from the alternate buffer to the normal buffer. + * + * @see GUAC_TERMINAL_DECSET_USE_ALT_BUFFER + */ +#define GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER 47 + +/** + * Flag number for the DEC Private Mode Reset (DECRST) operation that switches + * from the alternate buffer to the normal buffer AND clears the normal buffer + * (but only if the normal buffer wasn't already selected). + * + * @see GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER + */ +#define GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER_AND_CLEAR 1047 + +/** + * Flag number for the DEC Private Mode Set (DECSET) operation that restores + * the cursor location. The cursor location must have been previously saved + * through GUAC_TERMINAL_DECSET_SAVE_CURSOR or related operations. + * + * @see GUAC_TERMINAL_DECSET_SAVE_CURSOR + */ +#define GUAC_TERMINAL_DECRST_RESTORE_CURSOR 1048 + +/** + * Flag number for the DEC Private Mode Reet (DECRST) operation that and + * restores the previously saved cursor location. The normal buffer is not + * cleared. + * + * @see GUAC_TERMINAL_DECSET_SAVE_CURSOR + * @see GUAC_TERMINAL_DECSET_USE_ALT_BUFFER + */ +#define GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER_AND_RESTORE_CURSOR 1049 + +/** + * Parses a numeric terminal parameter, as may be accepted by console code + * sequences in general. Only integers between 0 and INT_MAX are parsed, + * inclusive. If a value cannot be parsed, 0 is returned. + * + * @param str + * The terminal parameter to parse, as a null-terminated string. + * + * @return + * The integer value of the provided string, or 0 if the string cannot be + * parsed for any reason. + */ +static int guac_terminal_parse_numeric_param(const char* str) { + + errno = 0; + unsigned long value = strtoul(str, NULL, 10); + if (errno || value > INT_MAX) + return 0; + + return value; + +} + /** * Advances the cursor to the next row, scrolling if the cursor would otherwise * leave the scrolling region. If the cursor is already outside the scrolling @@ -57,8 +159,14 @@ * * @param term * The guac_terminal whose cursor should be advanced to the next row. + * + * @param force_wrap + * True if the line wrap was forced, false otherwise */ -static void guac_terminal_linefeed(guac_terminal* term) { +static void guac_terminal_linefeed(guac_terminal* term, bool force_wrap) { + + /* Pass through possible change in whether the current row was wrapped */ + guac_terminal_buffer_set_wrapped(term->current_buffer, term->cursor_row, force_wrap); /* Scroll up if necessary */ if (term->cursor_row == term->scroll_end) @@ -221,7 +329,7 @@ int guac_terminal_echo(guac_terminal* term, unsigned char c) { case 0x0C: /* FF */ /* Advance to next row */ - guac_terminal_linefeed(term); + guac_terminal_linefeed(term, false); /* If automatic carriage return, fall through to CR handler */ if (!term->automatic_carriage_return) @@ -269,8 +377,10 @@ int guac_terminal_echo(guac_terminal* term, unsigned char c) { /* Wrap if necessary */ if (term->cursor_col >= term->term_width) { + + /* New line */ term->cursor_col = 0; - guac_terminal_linefeed(term); + guac_terminal_linefeed(term, true); } /* If insert mode, shift other characters right by 1 */ @@ -340,14 +450,14 @@ int guac_terminal_escape(guac_terminal* term, unsigned char c) { /* Index (IND) */ case 'D': - guac_terminal_linefeed(term); + guac_terminal_linefeed(term, false); term->char_handler = guac_terminal_echo; break; /* Next Line (NEL) */ case 'E': guac_terminal_move_cursor(term, term->cursor_row, 0); - guac_terminal_linefeed(term); + guac_terminal_linefeed(term, false); term->char_handler = guac_terminal_echo; break; @@ -622,7 +732,7 @@ int guac_terminal_csi(guac_terminal* term, unsigned char c) { /* Finish parameter */ argv_buffer[argv_length] = 0; - argv[argc++] = atoi(argv_buffer); + argv[argc++] = guac_terminal_parse_numeric_param(argv_buffer); /* Prepare for next parameter */ argv_length = 0; @@ -835,6 +945,32 @@ int guac_terminal_csi(guac_terminal* term, unsigned char c) { break; + /* S: Scroll Up by amount */ + case 'S': + + /* Get move amount */ + amount = argv[0]; + if (amount == 0) amount = 1; + + /* Scroll up */ + guac_terminal_scroll_up(term, term->scroll_start, + term->scroll_end, amount); + + break; + + /* T: Scroll Down by amount */ + case 'T': + + /* Get move amount */ + amount = argv[0]; + if (amount == 0) amount = 1; + + /* Scroll Down */ + guac_terminal_scroll_down(term, term->scroll_start, + term->scroll_end, amount); + + break; + /* X: Erase characters (no scroll) */ case 'X': @@ -877,20 +1013,91 @@ int guac_terminal_csi(guac_terminal* term, unsigned char c) { break; - /* h: Set Mode */ + /* h: Set Mode (DECSET) */ case 'h': - - /* Look up flag and set */ + + /* Save cursor for later restoration */ + if (argv[0] == GUAC_TERMINAL_DECSET_SAVE_CURSOR + || argv[0] == GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_SAVE_CURSOR_AND_CLEAR) { + term->saved_cursor_row = term->cursor_row; + term->saved_cursor_col = term->cursor_col; + } + + if (term->current_buffer != term->alternate_buffer) { + + /* Switch to alternate buffer */ + if (argv[0] == GUAC_TERMINAL_DECSET_USE_ALT_BUFFER + || argv[0] == GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_CLEAR + || argv[0] == GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_SAVE_CURSOR_AND_CLEAR) { + + term->current_buffer = term->alternate_buffer; + + /* Update scrollbar bounds (the different buffers have differing levels of scrollback) */ + guac_terminal_scrollbar_set_bounds(term->scrollbar, + -guac_terminal_get_available_scroll(term), 0); + + } + + /* Clear alternate buffer only if we were previously using + * the normal buffer */ + if (argv[0] == GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_CLEAR + || argv[0] == GUAC_TERMINAL_DECSET_USE_ALT_BUFFER_AND_SAVE_CURSOR_AND_CLEAR) { + guac_terminal_clear_range(term, + 0, 0, term->term_height - 1, term->term_width - 1); + } + + } + + /* Look up flag and set */ flag = __guac_terminal_get_flag(term, argv[0], private_mode_character); if (flag != NULL) *flag = true; break; - /* l: Reset Mode */ + /* l: Reset Mode (DECRST) */ case 'l': - - /* Look up flag and clear */ + + if (term->current_buffer != term->normal_buffer) { + + /* Switch back to normal buffer */ + if (argv[0] == GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER + || argv[0] == GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER_AND_CLEAR + || argv[0] == GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER_AND_RESTORE_CURSOR) { + + term->current_buffer = term->normal_buffer; + + /* Update scrollbar bounds (the different buffers have differing levels of scrollback) */ + guac_terminal_scrollbar_set_bounds(term->scrollbar, + -guac_terminal_get_available_scroll(term), 0); + + /* Redraw normal buffer content */ + guac_terminal_redraw_default_layer(term); + + /* Clear selection */ + term->text_selected = false; + term->selection_committed = false; + + } + + /* Clear normal buffer only if we were previously using + * the alternate buffer */ + if (argv[0] == GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER_AND_CLEAR) { + guac_terminal_clear_range(term, + 0, 0, term->term_height - 1, term->term_width - 1); + } + + } + + /* Restore previously saved cursor */ + if (argv[0] == GUAC_TERMINAL_DECRST_RESTORE_CURSOR + || argv[0] == GUAC_TERMINAL_DECRST_USE_NORMAL_BUFFER_AND_RESTORE_CURSOR) { + guac_terminal_move_cursor(term, + term->saved_cursor_row, + term->saved_cursor_col); + } + + /* Look up flag and clear */ flag = __guac_terminal_get_flag(term, argv[0], private_mode_character); if (flag != NULL) *flag = false; @@ -1180,7 +1387,7 @@ int guac_terminal_open_pipe_stream(guac_terminal* term, unsigned char c) { length = 0; /* Parse parameter string as integer flags */ - flags |= atoi(param); + flags |= guac_terminal_parse_numeric_param(param); } @@ -1223,7 +1430,7 @@ int guac_terminal_set_scrollback(guac_terminal* term, unsigned char c) { length = 0; /* Assign scrollback size */ - term->requested_scrollback = atoi(param); + term->requested_scrollback = guac_terminal_parse_numeric_param(param); /* Update scrollbar bounds */ guac_terminal_scrollbar_set_bounds(term->scrollbar, diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index 60dff20f5..466baa555 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -19,6 +19,7 @@ #include "common/clipboard.h" #include "common/cursor.h" +#include "common/iconv.h" #include "terminal/buffer.h" #include "terminal/color-scheme.h" #include "terminal/common.h" @@ -44,11 +45,13 @@ #include #include +#include #include #include #include #include #include +#include /** * Sets the given range of columns to the given character. @@ -59,7 +62,7 @@ static void __guac_terminal_set_columns(guac_terminal* terminal, int row, guac_terminal_display_set_columns(terminal->display, row + terminal->scroll_offset, start_column, end_column, character); - guac_terminal_buffer_set_columns(terminal->buffer, row, + guac_terminal_buffer_set_columns(terminal->current_buffer, row, start_column, end_column, character); /* Clear selection if region is modified */ @@ -67,89 +70,6 @@ static void __guac_terminal_set_columns(guac_terminal* terminal, int row, } -/** - * Enforces a character break at the given edge, ensuring that the left side - * of the edge is the final column of a character, and the right side of the - * edge is the initial column of a DIFFERENT character. - * - * For a character in a column N, the left edge number is N, and the right - * edge is N+1. - */ -static void __guac_terminal_force_break(guac_terminal* terminal, int row, int edge) { - - guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(terminal->buffer, row, 0); - - /* Ensure character to left of edge is unbroken */ - if (edge > 0) { - - int end_column = edge - 1; - int start_column = end_column; - - guac_terminal_char* start_char = &(buffer_row->characters[start_column]); - - /* Determine start column */ - while (start_column > 0 && start_char->value == GUAC_CHAR_CONTINUATION) { - start_char--; - start_column--; - } - - /* Advance to start of broken character if necessary */ - if (start_char->value != GUAC_CHAR_CONTINUATION && start_char->width < end_column - start_column + 1) { - start_column += start_char->width; - start_char += start_char->width; - } - - /* Clear character if broken */ - if (start_char->value == GUAC_CHAR_CONTINUATION || start_char->width != end_column - start_column + 1) { - - guac_terminal_char cleared_char; - cleared_char.value = ' '; - cleared_char.attributes = start_char->attributes; - cleared_char.width = 1; - - __guac_terminal_set_columns(terminal, row, start_column, end_column, &cleared_char); - - } - - } - - /* Ensure character to right of edge is unbroken */ - if (edge >= 0 && edge < buffer_row->length) { - - int start_column = edge; - int end_column = start_column; - - guac_terminal_char* start_char = &(buffer_row->characters[start_column]); - guac_terminal_char* end_char = &(buffer_row->characters[end_column]); - - /* Determine end column */ - while (end_column+1 < buffer_row->length && (end_char+1)->value == GUAC_CHAR_CONTINUATION) { - end_char++; - end_column++; - } - - /* Advance to start of broken character if necessary */ - if (start_char->value != GUAC_CHAR_CONTINUATION && start_char->width < end_column - start_column + 1) { - start_column += start_char->width; - start_char += start_char->width; - } - - /* Clear character if broken */ - if (start_char->value == GUAC_CHAR_CONTINUATION || start_char->width != end_column - start_column + 1) { - - guac_terminal_char cleared_char; - cleared_char.value = ' '; - cleared_char.attributes = start_char->attributes; - cleared_char.width = 1; - - __guac_terminal_set_columns(terminal, row, start_column, end_column, &cleared_char); - - } - - } - -} - /** * Returns the number of rows available within the terminal buffer, taking * changes to the desired scrollback size into account. Regardless of the @@ -178,11 +98,7 @@ static int guac_terminal_effective_buffer_length(guac_terminal* term) { /* If the buffer contains more rows than requested, pretend it only * contains the requested number of rows */ - int effective_length = term->buffer->length; - if (effective_length > scrollback) - effective_length = scrollback; - - return effective_length; + return guac_terminal_buffer_effective_length(term->current_buffer, scrollback); } @@ -214,8 +130,7 @@ void guac_terminal_reset(guac_terminal* term) { term->cursor_visible = true; /* Clear scrollback, buffer, and scroll region */ - term->buffer->top = 0; - term->buffer->length = 0; + guac_terminal_buffer_reset(term->current_buffer); term->scroll_start = 0; term->scroll_end = term->term_height - 1; term->scroll_offset = 0; @@ -241,9 +156,10 @@ void guac_terminal_reset(guac_terminal* term) { /* Reset display palette */ guac_terminal_display_reset_palette(term->display); - /* Clear terminal */ + /* Clear terminal with a row length of term_width-1 + * to avoid exceed the size of the display layer */ for (row=0; rowterm_height; row++) - guac_terminal_set_columns(term, row, 0, term->term_width, &(term->default_char)); + guac_terminal_set_columns(term, row, 0, term->term_width-1, &(term->default_char)); } @@ -336,6 +252,97 @@ guac_terminal_options* guac_terminal_options_create( return options; } +/** + * Calculate the available height and width in characters for text display in + * the terminal and store the results in the pointer arguments. + * + * @param terminal + * The terminal provides character width and height for calculations. + * + * @param height + * The outer height of the terminal, in pixels. + * + * @param width + * The outer width of the terminal, in pixels. + * + * @param rows + * Pointer to the calculated height of the terminal for text display, + * in characters. + * + * @param columns + * Pointer to the calculated width of the terminal for text display, + * in characters. + */ +static void calculate_rows_and_columns(guac_terminal* term, + int height, int width, int *rows, int *columns) { + + int margin = term->display->margin; + int char_width = term->display->char_width; + int char_height = term->display->char_height; + + /* Calculate available display area */ + int available_width = width - GUAC_TERMINAL_SCROLLBAR_WIDTH - 2 * margin; + if (available_width < 0) + available_width = 0; + + int available_height = height - 2 * margin; + if (available_height < 0) + available_height = 0; + + /* Calculate dimensions */ + *rows = available_height / char_height; + *columns = available_width / char_width; + + /* Keep height within predefined maximum */ + if (*rows > GUAC_TERMINAL_MAX_ROWS) + *rows = GUAC_TERMINAL_MAX_ROWS; + + /* Keep width within predefined maximum */ + if (*columns > GUAC_TERMINAL_MAX_COLUMNS) + *columns = GUAC_TERMINAL_MAX_COLUMNS; +} + +/** + * Calculate the available height and width in pixels of the terminal for text + * display in the terminal and store the results in the pointer arguments. + * + * @param terminal + * The terminal provides character width and height for calculations. + * + * @param rows + * The available height of the terminal for text display, in characters. + * + * @param columns + * The available width of the terminal for text display, in characters. + * + * @param height + * Pointer to the calculated available height of the terminal for text + * display, in pixels. + * + * @param width + * Pointer to the calculated available width of the terminal for text + * display, in pixels. + */ +static void calculate_height_and_width(guac_terminal* term, + int rows, int columns, int *height, int *width) { + + int margin = term->display->margin; + int char_width = term->display->char_width; + int char_height = term->display->char_height; + + /* Recalculate height if max rows reached */ + if (rows == GUAC_TERMINAL_MAX_ROWS) { + int available_height = GUAC_TERMINAL_MAX_ROWS * char_height; + *height = available_height + 2 * margin; + } + + /* Recalculate width if max columns reached */ + if (columns == GUAC_TERMINAL_MAX_COLUMNS) { + int available_width = GUAC_TERMINAL_MAX_COLUMNS * char_width; + *width = available_width + GUAC_TERMINAL_SCROLLBAR_WIDTH + 2 * margin; + } +} + guac_terminal* guac_terminal_create(guac_client* client, guac_terminal_options* options) { @@ -364,11 +371,6 @@ guac_terminal* guac_terminal_create(guac_client* client, &default_char.attributes.background, default_palette); - /* Calculate available display area */ - int available_width = width - GUAC_TERMINAL_SCROLLBAR_WIDTH; - if (available_width < 0) - available_width = 0; - guac_terminal* term = guac_mem_alloc(sizeof(guac_terminal)); term->started = false; term->client = client; @@ -380,14 +382,8 @@ guac_terminal* guac_terminal_create(guac_client* client, term->font_name = guac_strdup(options->font_name); term->font_size = options->font_size; - /* Set size of available screen area */ - term->outer_width = width; - term->outer_height = height; - /* Init modified flag and conditional */ - term->modified = 0; - pthread_cond_init(&(term->modified_cond), NULL); - pthread_mutex_init(&(term->modified_lock), NULL); + guac_flag_init(&term->modified); /* Maximum and requested scrollback are initially the same */ term->max_scrollback = options->max_scrollback; @@ -399,9 +395,9 @@ guac_terminal* guac_terminal_create(guac_client* client, if (initial_scrollback < GUAC_TERMINAL_MAX_ROWS) initial_scrollback = GUAC_TERMINAL_MAX_ROWS; - /* Init buffer */ - term->buffer = guac_terminal_buffer_alloc(initial_scrollback, - &default_char); + /* Init current and alternate buffer */ + term->current_buffer = term->normal_buffer = guac_terminal_buffer_alloc(initial_scrollback, &default_char); + term->alternate_buffer = guac_terminal_buffer_alloc(GUAC_TERMINAL_MAX_ROWS, &default_char); /* Init display */ term->display = guac_terminal_display_alloc(client, @@ -426,29 +422,27 @@ guac_terminal* guac_terminal_create(guac_client* client, term->clipboard = guac_common_clipboard_alloc(); term->disable_copy = options->disable_copy; - /* Calculate character size */ - int rows = height / term->display->char_height; - int columns = available_width / term->display->char_width; + /* Calculate available text display area by character size */ + int rows, columns; + calculate_rows_and_columns(term, height, width, &rows, &columns); - /* Keep height within predefined maximum */ - if (rows > GUAC_TERMINAL_MAX_ROWS) { - rows = GUAC_TERMINAL_MAX_ROWS; - height = rows * term->display->char_height; - } - - /* Keep width within predefined maximum */ - if (columns > GUAC_TERMINAL_MAX_COLUMNS) { - columns = GUAC_TERMINAL_MAX_COLUMNS; - available_width = columns * term->display->char_width; - width = available_width + GUAC_TERMINAL_SCROLLBAR_WIDTH; - } + /* Calculate available display area in pixels */ + int adjusted_height = height; + int adjusted_width = width; + calculate_height_and_width(term, rows, columns, + &adjusted_height, &adjusted_width); - /* Set pixel size */ - term->width = width; - term->height = height; + /* Set size of available screen area */ + term->outer_height = height; + term->outer_width = width; - term->term_width = columns; + /* Set rows and columns size */ term->term_height = rows; + term->term_width = columns; + + /* Set pixel size */ + term->height = adjusted_height; + term->width = adjusted_width; /* Open STDIN pipe */ if (pipe(term->stdin_pipe_fd)) { @@ -477,7 +471,7 @@ guac_terminal* guac_terminal_create(guac_client* client, /* Allocate scrollbar */ term->scrollbar = guac_terminal_scrollbar_alloc(term->client, GUAC_DEFAULT_LAYER, - width, height, term->term_height); + term->outer_width, term->outer_height, term->term_height); /* Associate scrollbar with this terminal */ term->scrollbar->data = term; @@ -492,6 +486,7 @@ guac_terminal* guac_terminal_create(guac_client* client, /* All keyboard modifiers are released */ term->mod_alt = term->mod_ctrl = + term->mod_meta = term->mod_shift = 0; /* Initialize mouse cursor */ @@ -508,6 +503,10 @@ guac_terminal* guac_terminal_create(guac_client* client, /* Configure backspace */ term->backspace = options->backspace; + /* Initialize mouse latest click time and counter */ + term->click_timer = 0; + term->click_counter = 0; + return term; } @@ -544,14 +543,15 @@ void guac_terminal_free(guac_terminal* term) { /* Close and flush any active typescript */ guac_terminal_typescript_free(term->typescript); + /* Free scrollbar */ + guac_terminal_scrollbar_free(term->scrollbar); + /* Free display */ guac_terminal_display_free(term->display); - /* Free buffer */ - guac_terminal_buffer_free(term->buffer); - - /* Free scrollbar */ - guac_terminal_scrollbar_free(term->scrollbar); + /* Free buffers */ + guac_terminal_buffer_free(term->normal_buffer); + guac_terminal_buffer_free(term->alternate_buffer); /* Free copies of font and color scheme information */ guac_mem_free_const(term->color_scheme); @@ -561,47 +561,11 @@ void guac_terminal_free(guac_terminal* term) { guac_common_clipboard_free(term->clipboard); /* Free the terminal itself */ + pthread_mutex_destroy(&term->lock); guac_mem_free(term); } -/** - * Populate the given timespec with the current time, plus the given offset. - * - * @param ts - * The timespec structure to populate. - * - * @param offset_sec - * The offset from the current time to use when populating the given - * timespec, in seconds. - * - * @param offset_usec - * The offset from the current time to use when populating the given - * timespec, in microseconds. - */ -static void guac_terminal_get_absolute_time(struct timespec* ts, - int offset_sec, int offset_usec) { - - /* Get timeval */ - struct timeval tv; - gettimeofday(&tv, NULL); - - /* Update with offset */ - tv.tv_sec += offset_sec; - tv.tv_usec += offset_usec; - - /* Wrap to next second if necessary */ - if (tv.tv_usec >= 1000000) { - tv.tv_sec++; - tv.tv_usec -= 1000000; - } - - /* Convert to timespec */ - ts->tv_sec = tv.tv_sec; - ts->tv_nsec = tv.tv_usec * 1000; - -} - /** * Waits for the terminal state to be modified, returning only when the * specified timeout has elapsed or a frame flush is desired. Note that the @@ -620,32 +584,15 @@ static void guac_terminal_get_absolute_time(struct timespec* ts, */ static int guac_terminal_wait(guac_terminal* terminal, int msec_timeout) { - int retval = 1; - - pthread_mutex_t* mod_lock = &(terminal->modified_lock); - pthread_cond_t* mod_cond = &(terminal->modified_cond); - - /* Split provided milliseconds into microseconds and whole seconds */ - int secs = msec_timeout / 1000; - int usecs = (msec_timeout % 1000) * 1000; - - /* Calculate absolute timestamp from provided relative timeout */ - struct timespec timeout; - guac_terminal_get_absolute_time(&timeout, secs, usecs); + int retval = guac_flag_timedwait_and_lock(&terminal->modified, + GUAC_TERMINAL_MODIFIED, msec_timeout); - /* Test for terminal modification */ - pthread_mutex_lock(mod_lock); - if (terminal->modified) - goto wait_complete; - - /* If not yet modified, wait for modification condition to be signaled */ - retval = pthread_cond_timedwait(mod_cond, mod_lock, &timeout) != ETIMEDOUT; - -wait_complete: + /* Rest terminal modified state */ + if (retval) { + guac_flag_clear(&terminal->modified, GUAC_TERMINAL_MODIFIED); + guac_flag_unlock(&terminal->modified); + } - /* Terminal is no longer modified */ - terminal->modified = 0; - pthread_mutex_unlock(mod_lock); return retval; } @@ -660,7 +607,7 @@ int guac_terminal_render_frame(guac_terminal* terminal) { wait_result = guac_terminal_wait(terminal, 1000); if (wait_result || !terminal->started) { - guac_timestamp frame_start = guac_timestamp_current(); + guac_timestamp frame_start = client->last_sent_timestamp; do { @@ -697,16 +644,8 @@ int guac_terminal_read_stdin(guac_terminal* terminal, char* c, int size) { void guac_terminal_notify(guac_terminal* terminal) { - pthread_mutex_t* mod_lock = &(terminal->modified_lock); - pthread_cond_t* mod_cond = &(terminal->modified_cond); - - pthread_mutex_lock(mod_lock); - /* Signal modification */ - terminal->modified = 1; - pthread_cond_signal(mod_cond); - - pthread_mutex_unlock(mod_lock); + guac_flag_set(&terminal->modified, GUAC_TERMINAL_MODIFIED); } @@ -814,37 +753,37 @@ int guac_terminal_set(guac_terminal* term, int row, int col, int codepoint) { void guac_terminal_commit_cursor(guac_terminal* term) { - guac_terminal_char* guac_char; - - guac_terminal_buffer_row* row; - /* If no change, done */ if (term->cursor_visible && term->visible_cursor_row == term->cursor_row && term->visible_cursor_col == term->cursor_col) return; /* Clear cursor if it was visible */ if (term->visible_cursor_row != -1 && term->visible_cursor_col != -1) { - /* Get old row with cursor */ - row = guac_terminal_buffer_get_row(term->buffer, term->visible_cursor_row, term->visible_cursor_col+1); - guac_char = &(row->characters[term->visible_cursor_col]); - guac_char->attributes.cursor = false; - guac_terminal_display_set_columns(term->display, term->visible_cursor_row + term->scroll_offset, - term->visible_cursor_col, term->visible_cursor_col, guac_char); + guac_terminal_buffer_set_cursor(term->current_buffer, term->visible_cursor_row, term->visible_cursor_col, false); + + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(term->current_buffer, &characters, NULL, term->visible_cursor_row); + if (term->visible_cursor_col < length) + guac_terminal_display_set_columns(term->display, term->visible_cursor_row + term->scroll_offset, + term->visible_cursor_col, term->visible_cursor_col, &characters[term->visible_cursor_col]); + } /* Set cursor if should be visible */ if (term->cursor_visible) { - /* Get new row with cursor */ - row = guac_terminal_buffer_get_row(term->buffer, term->cursor_row, term->cursor_col+1); - guac_char = &(row->characters[term->cursor_col]); - guac_char->attributes.cursor = true; - guac_terminal_display_set_columns(term->display, term->cursor_row + term->scroll_offset, - term->cursor_col, term->cursor_col, guac_char); + guac_terminal_buffer_set_cursor(term->current_buffer, term->cursor_row, term->cursor_col, true); + + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(term->current_buffer, &characters, NULL, term->cursor_row); + if (term->cursor_col < length) + guac_terminal_display_set_columns(term->display, term->cursor_row + term->scroll_offset, + term->cursor_col, term->cursor_col, &characters[term->cursor_col]); term->visible_cursor_row = term->cursor_row; term->visible_cursor_col = term->cursor_col; + } /* Otherwise set visible position to a sentinel value */ @@ -880,9 +819,15 @@ int guac_terminal_write(guac_terminal* term, const char* buffer, int length) { } -int guac_terminal_scroll_up(guac_terminal* term, +void guac_terminal_scroll_up(guac_terminal* term, int start_row, int end_row, int amount) { + if (amount <= 0) + return; + + if (amount >= end_row - start_row + 1) + amount = end_row - start_row + 1; + /* If scrolling entire display, update scroll offset */ if (start_row == 0 && end_row == term->term_height - 1) { @@ -890,13 +835,7 @@ int guac_terminal_scroll_up(guac_terminal* term, guac_terminal_display_copy_rows(term->display, start_row + amount, end_row, -amount); /* Advance by scroll amount */ - term->buffer->top += amount; - if (term->buffer->top >= term->buffer->available) - term->buffer->top -= term->buffer->available; - - term->buffer->length += amount; - if (term->buffer->length > term->buffer->available) - term->buffer->length = term->buffer->available; + guac_terminal_buffer_scroll_up(term->current_buffer, amount); /* Reset scrollbar bounds */ guac_terminal_scrollbar_set_bounds(term->scrollbar, @@ -924,10 +863,9 @@ int guac_terminal_scroll_up(guac_terminal* term, end_row - amount + 1, 0, end_row, term->term_width - 1); - return 0; } -int guac_terminal_scroll_down(guac_terminal* term, +void guac_terminal_scroll_down(guac_terminal* term, int start_row, int end_row, int amount) { guac_terminal_copy_rows(term, start_row, end_row - amount, amount); @@ -937,7 +875,6 @@ int guac_terminal_scroll_down(guac_terminal* term, start_row, 0, start_row + amount - 1, term->term_width - 1); - return 0; } int guac_terminal_clear_columns(guac_terminal* term, @@ -1074,16 +1011,16 @@ void guac_terminal_scroll_display_down(guac_terminal* terminal, for (row=start_row; row<=end_row; row++) { /* Get row from scrollback */ - guac_terminal_buffer_row* buffer_row = - guac_terminal_buffer_get_row(terminal->buffer, row, 0); + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, NULL, row); /* Clear row */ guac_terminal_display_set_columns(terminal->display, dest_row, 0, terminal->display->width, &(terminal->default_char)); /* Draw row */ - guac_terminal_char* current = buffer_row->characters; - for (column=0; columnlength; column++) { + guac_terminal_char* current = characters; + for (column = 0; column < length; column++) { /* Only draw if not blank */ if (guac_terminal_is_visible(terminal, current)) @@ -1137,16 +1074,16 @@ void guac_terminal_scroll_display_up(guac_terminal* terminal, for (row=start_row; row<=end_row; row++) { /* Get row from scrollback */ - guac_terminal_buffer_row* buffer_row = - guac_terminal_buffer_get_row(terminal->buffer, row, 0); + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, NULL, row); /* Clear row */ guac_terminal_display_set_columns(terminal->display, dest_row, 0, terminal->display->width, &(terminal->default_char)); /* Draw row */ - guac_terminal_char* current = buffer_row->characters; - for (column=0; columnlength; column++) { + guac_terminal_char* current = characters; + for (column = 0; column < length; column++) { /* Only draw if not blank */ if (guac_terminal_is_visible(terminal, current)) @@ -1171,7 +1108,7 @@ void guac_terminal_copy_columns(guac_terminal* terminal, int row, guac_terminal_display_copy_columns(terminal->display, row + terminal->scroll_offset, start_column, end_column, offset); - guac_terminal_buffer_copy_columns(terminal->buffer, row, + guac_terminal_buffer_copy_columns(terminal->current_buffer, row, start_column, end_column, offset); /* Clear selection if region is modified */ @@ -1183,10 +1120,6 @@ void guac_terminal_copy_columns(guac_terminal* terminal, int row, terminal->visible_cursor_col <= end_column) terminal->visible_cursor_col += offset; - /* Force breaks around destination region */ - __guac_terminal_force_break(terminal, row, start_column + offset); - __guac_terminal_force_break(terminal, row, end_column + offset + 1); - } void guac_terminal_copy_rows(guac_terminal* terminal, @@ -1195,7 +1128,7 @@ void guac_terminal_copy_rows(guac_terminal* terminal, guac_terminal_display_copy_rows(terminal->display, start_row + terminal->scroll_offset, end_row + terminal->scroll_offset, offset); - guac_terminal_buffer_copy_rows(terminal->buffer, + guac_terminal_buffer_copy_rows(terminal->current_buffer, start_row, end_row, offset); /* Clear selection if region is modified */ @@ -1228,10 +1161,6 @@ void guac_terminal_set_columns(guac_terminal* terminal, int row, } - /* Force breaks around destination region */ - __guac_terminal_force_break(terminal, row, start_column); - __guac_terminal_force_break(terminal, row, end_column + 1); - } static void __guac_terminal_redraw_rect(guac_terminal* term, int start_row, int start_col, int end_row, int end_col) { @@ -1241,18 +1170,18 @@ static void __guac_terminal_redraw_rect(guac_terminal* term, int start_row, int /* Redraw region */ for (row=start_row; row<=end_row; row++) { - guac_terminal_buffer_row* buffer_row = - guac_terminal_buffer_get_row(term->buffer, row - term->scroll_offset, 0); + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(term->current_buffer, &characters, NULL, row - term->scroll_offset); /* Clear row */ guac_terminal_display_set_columns(term->display, row, start_col, end_col, &(term->default_char)); /* Copy characters */ - for (col=start_col; col <= end_col && col < buffer_row->length; col++) { + for (col=start_col; col <= end_col && col < length; col++) { /* Only redraw if not blank */ - guac_terminal_char* c = &(buffer_row->characters[col]); + guac_terminal_char* c = &characters[col]; if (guac_terminal_is_visible(term, c)) guac_terminal_display_set_columns(term->display, row, col, col, c); @@ -1265,6 +1194,15 @@ static void __guac_terminal_redraw_rect(guac_terminal* term, int start_row, int /** * Internal terminal resize routine. Accepts width/height in CHARACTERS * (not pixels like the public function). + * + * @param term + * The terminal being resized. + * + * @param width + * The new width of the terminal, in characters. + * + * @param height + * The new height of the terminal, in characters. */ static void __guac_terminal_resize(guac_terminal* term, int width, int height) { @@ -1287,7 +1225,7 @@ static void __guac_terminal_resize(guac_terminal* term, int width, int height) { shift_amount, term->display->height - 1, -shift_amount); /* Update buffer top and cursor row based on shift */ - term->buffer->top += shift_amount; + guac_terminal_buffer_scroll_up(term->current_buffer, shift_amount); term->cursor_row -= shift_amount; if (term->visible_cursor_row != -1) term->visible_cursor_row -= shift_amount; @@ -1322,7 +1260,7 @@ static void __guac_terminal_resize(guac_terminal* term, int width, int height) { shift_amount = available_scroll; /* Update buffer top and cursor row based on shift */ - term->buffer->top -= shift_amount; + guac_terminal_buffer_scroll_down(term->current_buffer, shift_amount); term->cursor_row += shift_amount; if (term->visible_cursor_row != -1) term->visible_cursor_row += shift_amount; @@ -1386,55 +1324,39 @@ int guac_terminal_resize(guac_terminal* terminal, int width, int height) { /* Acquire exclusive access to terminal */ guac_terminal_lock(terminal); - /* Set size of available screen area */ - terminal->outer_width = width; - terminal->outer_height = height; - - /* Calculate available display area */ - int available_width = width - GUAC_TERMINAL_SCROLLBAR_WIDTH; - if (available_width < 0) - available_width = 0; - - /* Calculate dimensions */ - int rows = height / display->char_height; - int columns = available_width / display->char_width; + /* Calculate available text display area by character size */ + int rows, columns; + calculate_rows_and_columns(terminal, height, width, &rows, &columns); - /* Keep height within predefined maximum */ - if (rows > GUAC_TERMINAL_MAX_ROWS) { - rows = GUAC_TERMINAL_MAX_ROWS; - height = rows * display->char_height; - } + /* Calculate available display area in pixels */ + int adjusted_height = height; + int adjusted_width = width; + calculate_height_and_width(terminal, rows, columns, + &adjusted_height, &adjusted_width); - /* Keep width within predefined maximum */ - if (columns > GUAC_TERMINAL_MAX_COLUMNS) { - columns = GUAC_TERMINAL_MAX_COLUMNS; - available_width = columns * display->char_width; - width = available_width + GUAC_TERMINAL_SCROLLBAR_WIDTH; - } + /* Set size of available screen area */ + terminal->outer_height = height; + terminal->outer_width = width; - /* Set pixel sizes */ - terminal->width = width; - terminal->height = height; + /* Set pixel size */ + terminal->height = adjusted_height; + terminal->width = adjusted_width; /* Resize default layer to given pixel dimensions */ guac_terminal_repaint_default_layer(terminal, client->socket); /* Resize terminal if row/column dimensions have changed */ if (columns != terminal->term_width || rows != terminal->term_height) { - - guac_client_log(client, GUAC_LOG_DEBUG, - "Resizing terminal to %ix%i", rows, columns); - - /* Resize terminal */ + /* Resize terminal and set the columns and rows on the terminal struct */ __guac_terminal_resize(terminal, columns, rows); /* Reset scroll region */ terminal->scroll_end = rows - 1; - } /* Notify scrollbar of resize */ - guac_terminal_scrollbar_parent_resized(terminal->scrollbar, width, height, rows); + guac_terminal_scrollbar_parent_resized(terminal->scrollbar, + terminal->outer_width, terminal->outer_height, terminal->term_height); guac_terminal_scrollbar_set_bounds(terminal->scrollbar, -guac_terminal_get_available_scroll(terminal), 0); @@ -1509,20 +1431,31 @@ static int __guac_terminal_send_key(guac_terminal* term, int keysym, int pressed } /* Track modifiers */ - if (keysym == 0xFFE3) + if (keysym == 0xFFE3 || keysym == 0xFFE4) term->mod_ctrl = pressed; - else if (keysym == 0xFFE9) + else if (keysym == 0xFFE7 || keysym == 0xFFE8) + term->mod_meta = pressed; + else if (keysym == 0xFFE9 || keysym == 0xFFEA) term->mod_alt = pressed; - else if (keysym == 0xFFE1) + else if (keysym == 0xFFE1 || keysym == 0xFFE2) term->mod_shift = pressed; /* If key pressed */ else if (pressed) { - /* Ctrl+Shift+V shortcut for paste */ - if (keysym == 'V' && term->mod_ctrl) + /* Ctrl+Shift+V or Cmd+v (mac style) shortcuts for paste */ + if ((keysym == 'V' && term->mod_ctrl) || (keysym == 'v' && term->mod_meta)) return guac_terminal_send_data(term, term->clipboard->buffer, term->clipboard->length); + /* + * Ctrl+Shift+C and Cmd+c shortcuts for copying are not handled, as + * selecting text in the terminal automatically copies it. To avoid + * attempts to use these shortcuts causing unexpected results in the + * terminal, these are just ignored. + */ + if ((keysym == 'C' && term->mod_ctrl) || (keysym == 'c' && term->mod_meta)) + return 0; + /* Shift+PgUp / Shift+PgDown shortcuts for scrolling */ if (term->mod_shift) { @@ -1671,6 +1604,120 @@ int guac_terminal_send_key(guac_terminal* term, int keysym, int pressed) { } +/** + * Determines if the given character is part of a word. + * Match these chars :[0-9A-Za-z\$\%\&\-\.\/\:\=\?\\_~] + * This allows a path, URL, variable name or IP address to be treated as a word. + * + * @param ascii_char + * The character to check. + * + * @return + * true if match a "word" char, + * false otherwise. + */ +static bool guac_terminal_is_part_of_word(int ascii_char) { + return ((ascii_char >= '0' && ascii_char <= '9') || + (ascii_char >= 'A' && ascii_char <= 'Z') || + (ascii_char >= 'a' && ascii_char <= 'z') || + (ascii_char == '$') || + (ascii_char == '%') || + (ascii_char == '&') || + (ascii_char == '-') || + (ascii_char == '.') || + (ascii_char == '/') || + (ascii_char == ':') || + (ascii_char == '=') || + (ascii_char == '?') || + (ascii_char == '\\') || + (ascii_char == '_') || + (ascii_char == '~')); +} + +/** + * Determines if the given character is part of blank block. + * + * @param ascii_char + * The character to check. + * + * @return + * true if match space (char 0x20) or NULL (char 0x00), + * false otherwise. + */ +static bool guac_terminal_is_blank(int ascii_char) { + return (ascii_char == '\0' || ascii_char == ' '); +} + +/** + * Selection of a word during a double click event. + * - Fetching the character under the mouse cursor. + * - Determining the type of character : + * Letter, digit, acceptable symbol within a word, + * or space/NULL, + * all other chars are treated as single. + * - Calculating the word boundaries. + * - Visual selection of the found word. + * - Adding it to clipboard. + * + * @param terminal + * The terminal that received a double click event. + * + * @param row + * The row where is the mouse at the double click event. + * + * @param col + * The column where is the mouse at the double click event. + */ +static void guac_terminal_double_click(guac_terminal* terminal, int row, int col) { + + guac_terminal_char* characters; + int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, NULL, row); + + if (col >= length) + return; + + /* (char)10 behind cursor */ + int current_char = characters[col].value; + + /* Position of the word behind cursor. + * Default = col required to select a char if not a word and not blank. */ + + /* The function used to calculate the word borders */ + bool (*is_part_of_word)(int) = NULL; + + /* If selection is on a word, get its borders */ + if (guac_terminal_is_part_of_word(current_char)) + is_part_of_word = guac_terminal_is_part_of_word; + + /* If selection is on a blank, get its borders */ + else if (guac_terminal_is_blank(current_char)) + is_part_of_word = guac_terminal_is_blank; + + int word_head = col; + int word_tail = col; + + if (is_part_of_word != NULL) { + + /* Get word head*/ + for (; word_head - 1 >= 0; word_head--) { + if (!is_part_of_word(characters[word_head - 1].value)) + break; + } + + /* Get word tail */ + for (; word_tail + 1 < terminal->display->width && word_tail + 1 < length; word_tail++) { + if (!is_part_of_word(characters[word_tail + 1].value)) + break; + } + + } + + /* Select and add to clipboard the "word" */ + guac_terminal_select_start(terminal, row, word_head); + guac_terminal_select_update(terminal, row, word_tail); + +} + static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, int x, int y, int mask) { @@ -1703,6 +1750,10 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, } + /* Remove display margin from mouse position without going below 0 */ + y = y >= term->display->margin ? y - term->display->margin : 0; + x = x >= term->display->margin ? x - term->display->margin : 0; + term->mouse_mask = mask; /* Show mouse cursor if not already shown */ @@ -1732,8 +1783,34 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, if (pressed_mask & GUAC_CLIENT_MOUSE_LEFT) { if (term->mod_shift) guac_terminal_select_resume(term, row, col); - else - guac_terminal_select_start(term, row, col); + else { + + /* Reset click counter if last click was 300ms before */ + if (guac_timestamp_current() - term->click_timer > 300) + term->click_counter = 0; + + /* New click time */ + term->click_timer = guac_timestamp_current(); + + switch (term->click_counter++) { + + /* First click = start selection */ + case 0: + guac_terminal_select_start(term, row, col); + break; + + /* Second click = word selection */ + case 1: + guac_terminal_double_click(term, row, col); + break; + + /* third click or more = line selection */ + default: + guac_terminal_select_start(term, row, 0); + guac_terminal_select_update(term, row, term->display->width); + break; + } + } } /* In all other cases, simply update the existing selection as long as @@ -1952,10 +2029,11 @@ void guac_terminal_pipe_stream_close(guac_terminal* term) { } int guac_terminal_create_typescript(guac_terminal* term, const char* path, - const char* name, int create_path) { + const char* name, int create_path, int allow_write_existing) { /* Create typescript */ - term->typescript = guac_terminal_typescript_alloc(path, name, create_path); + term->typescript = guac_terminal_typescript_alloc( + path, name, create_path, allow_write_existing); /* Log failure */ if (term->typescript == NULL) { @@ -2039,10 +2117,7 @@ void guac_terminal_apply_color_scheme(guac_terminal* terminal, display->default_background = default_char->attributes.background; /* Redraw terminal text and background */ - guac_terminal_repaint_default_layer(terminal, client->socket); - __guac_terminal_redraw_rect(terminal, 0, 0, - terminal->term_height - 1, - terminal->term_width - 1); + guac_terminal_redraw_default_layer(terminal); /* Acquire exclusive access to terminal */ guac_terminal_lock(terminal); @@ -2065,7 +2140,6 @@ const char* guac_terminal_get_color_scheme(guac_terminal* terminal) { void guac_terminal_apply_font(guac_terminal* terminal, const char* font_name, int font_size, int dpi) { - guac_client* client = terminal->client; guac_terminal_display* display = terminal->display; if (guac_terminal_display_set_font(display, font_name, font_size, dpi)) @@ -2077,10 +2151,7 @@ void guac_terminal_apply_font(guac_terminal* terminal, const char* font_name, terminal->outer_height); /* Redraw terminal text and background */ - guac_terminal_repaint_default_layer(terminal, client->socket); - __guac_terminal_redraw_rect(terminal, 0, 0, - terminal->term_height - 1, - terminal->term_width - 1); + guac_terminal_redraw_default_layer(terminal); /* Acquire exclusive access to terminal */ guac_terminal_lock(terminal); @@ -2129,7 +2200,16 @@ void guac_terminal_clipboard_reset(guac_terminal* terminal, void guac_terminal_clipboard_append(guac_terminal* terminal, const char* data, int length) { - guac_common_clipboard_append(terminal->clipboard, data, length); + + /* Allocate and clear space for the converted data */ + char output_data[GUAC_COMMON_CLIPBOARD_MAX_LENGTH]; + char* output = output_data; + + /* Convert clipboard contents */ + guac_iconv(GUAC_READ_UTF8_NORMALIZED, &data, length, + GUAC_WRITE_UTF8, &output, GUAC_COMMON_CLIPBOARD_MAX_LENGTH); + + guac_common_clipboard_append(terminal->clipboard, output_data, output - output_data); } void guac_terminal_remove_user(guac_terminal* terminal, guac_user* user) { @@ -2137,3 +2217,12 @@ void guac_terminal_remove_user(guac_terminal* terminal, guac_user* user) { /* Remove the user from the terminal cursor */ guac_common_cursor_remove_user(terminal->cursor, user); } + +void guac_terminal_redraw_default_layer(guac_terminal* terminal) { + + /* Redraw terminal text and background */ + guac_terminal_repaint_default_layer(terminal, terminal->client->socket); + __guac_terminal_redraw_rect(terminal, 0, 0, + terminal->term_height - 1, + terminal->term_width - 1); +} diff --git a/src/terminal/terminal/buffer.h b/src/terminal/terminal/buffer.h index 7c7f29c5c..9f175ba14 100644 --- a/src/terminal/terminal/buffer.h +++ b/src/terminal/terminal/buffer.h @@ -17,87 +17,37 @@ * under the License. */ -#ifndef _GUAC_TERMINAL_BUFFER_H -#define _GUAC_TERMINAL_BUFFER_H +#ifndef GUAC_TERMINAL_BUFFER_H +#define GUAC_TERMINAL_BUFFER_H /** - * Data structures and functions related to the terminal buffer. + * Data structures and functions related to the terminal buffer. The terminal + * buffer represents both the scrollback region and the current active contents + * of the terminal. + * + * NOTE: By design, all functions defined within this header make no + * assumptions about the validity of received coordinates, offsets, and + * lengths. Depending on the function, invalid values will be clamped, ignored, + * or reported as invalid. * * @file buffer.h */ #include "types.h" -/** - * A single variable-length row of terminal data. - */ -typedef struct guac_terminal_buffer_row { - - /** - * Array of guac_terminal_char representing the contents of the row. - */ - guac_terminal_char* characters; - - /** - * The length of this row in characters. This is the number of initialized - * characters in the buffer, usually equal to the number of characters - * in the screen width at the time this row was created. - */ - int length; - - /** - * The number of elements in the characters array. After the length - * equals this value, the array must be resized. - */ - int available; - -} guac_terminal_buffer_row; - /** * A buffer containing a constant number of arbitrary-length rows. * New rows can be appended to the buffer, with the oldest row replaced with * the new row. */ -typedef struct guac_terminal_buffer { - - /** - * The character to assign to newly-allocated cells. - */ - guac_terminal_char default_character; - - /** - * Array of buffer rows. This array functions as a ring buffer. - * When a new row needs to be appended, the top reference is moved down - * and the old top row is replaced. - */ - guac_terminal_buffer_row* rows; - - /** - * The index of the first row in the buffer (the row which represents row 0 - * with respect to the terminal display). This is also the index of the row - * to replace when insufficient space remains in the buffer to add a new - * row. - */ - int top; - - /** - * The number of rows currently stored in the buffer. - */ - int length; - - /** - * The number of rows in the buffer. This is the total capacity - * of the buffer. - */ - int available; - -} guac_terminal_buffer; +typedef struct guac_terminal_buffer guac_terminal_buffer; /** * Allocates a new buffer having the given maximum number of rows. New character cells will * be initialized to the given character. */ -guac_terminal_buffer* guac_terminal_buffer_alloc(int rows, guac_terminal_char* default_character); +guac_terminal_buffer* guac_terminal_buffer_alloc(int rows, + const guac_terminal_char* default_character); /** * Frees the given buffer. @@ -105,10 +55,15 @@ guac_terminal_buffer* guac_terminal_buffer_alloc(int rows, guac_terminal_char* d void guac_terminal_buffer_free(guac_terminal_buffer* buffer); /** - * Returns the row at the given location. The row returned is guaranteed to be at least the given - * width. + * Resets the state of the given buffer such that it effectively no longer + * contains any rows. Space for previous rows, including the data from those + * previous rows, may still be maintained internally to avoid needing to + * reallocate rows again later. + * + * @param buffer + * The buffer to reset. */ -guac_terminal_buffer_row* guac_terminal_buffer_get_row(guac_terminal_buffer* buffer, int row, int width); +void guac_terminal_buffer_reset(guac_terminal_buffer* buffer); /** * Copies the given range of columns to a new location, offset from @@ -124,6 +79,36 @@ void guac_terminal_buffer_copy_columns(guac_terminal_buffer* buffer, int row, void guac_terminal_buffer_copy_rows(guac_terminal_buffer* buffer, int start_row, int end_row, int offset); +/** + * Scrolls the contents of the given buffer up by the given number of rows. + * Here, "scrolling up" refers to moving the row contents upwards within the + * buffer (ie: decreasing the row index of each row), NOT to moving the + * viewport up. + * + * @param buffer + * The buffer to scroll. + * + * @param amount + * The number of rows to scroll upwards. Zero and negative values have no + * effect. + */ +void guac_terminal_buffer_scroll_up(guac_terminal_buffer* buffer, int amount); + +/** + * Scrolls the contents of the given buffer down by the given number of rows. + * Here, "scrolling down" refers to moving the row contents downwards within + * the buffer (ie: increasing the row index of each row), NOT to moving the + * viewport down. + * + * @param buffer + * The buffer to scroll. + * + * @param amount + * The number of rows to scroll downwards. Zero and negative values have no + * effect. + */ +void guac_terminal_buffer_scroll_down(guac_terminal_buffer* buffer, int amount); + /** * Sets the given range of columns within the given row to the given * character. @@ -131,5 +116,75 @@ void guac_terminal_buffer_copy_rows(guac_terminal_buffer* buffer, void guac_terminal_buffer_set_columns(guac_terminal_buffer* buffer, int row, int start_column, int end_column, guac_terminal_char* character); +/** + * Get the char (int ASCII code) at a specific row/col of the display. + * + * @param terminal + * The terminal on which we want to read a character. + * + * @param row + * The row where to read the character. + * + * @param col + * The column where to read the character. + * + * @return + * The ASCII code of the character at the given row/col. + */ +unsigned int guac_terminal_buffer_get_columns(guac_terminal_buffer* buffer, + guac_terminal_char** characters, bool* is_wrapped, int row); + +/** + * Returns the number of rows actually available for rendering within the given + * buffer, taking the scrollback size into account. Regardless of the true + * buffer length, only the number of rows that should be made available will be + * returned. + * + * @param buffer + * The buffer whose effective length should be retrieved. + * + * @param scrollback + * The number of rows currently within the terminal's scrollback buffer. + * + * @return + * The number of rows effectively available within the buffer. + */ +unsigned int guac_terminal_buffer_effective_length(guac_terminal_buffer* buffer, int scrollback); + +/** + * Sets whether the given buffer row was automatically wrapped by the terminal. + * Rows that were not automatically wrapped are lines of text that were printed + * and included an explicit newline character. + * + * @param buffer + * The buffer associated with the row being modified. + * + * @param row + * The row whose wrapped vs. not-wrapped state is being set. + * + * @param wrapped + * Whether the row was automatically wrapped (as opposed to simply ending + * with a newline character). + */ +void guac_terminal_buffer_set_wrapped(guac_terminal_buffer* buffer, int row, bool wrapped); + +/** + * Sets whether the character at the given row and column contains the cursor. + * + * @param buffer + * The buffer associated with character to modify. + * + * @param row + * The row of the character to modify. + * + * @param column + * The column of the character to modify. + * + * @param is_cursor + * Whether the character contains the cursor. + */ +void guac_terminal_buffer_set_cursor(guac_terminal_buffer* buffer, int row, + int column, bool is_cursor); + #endif diff --git a/src/terminal/terminal/display.h b/src/terminal/terminal/display.h index 9fe489f6d..635536088 100644 --- a/src/terminal/terminal/display.h +++ b/src/terminal/terminal/display.h @@ -42,6 +42,17 @@ */ #define GUAC_TERMINAL_MAX_CHAR_WIDTH 2 +/** + * The size of margins between the console text and the border in mm. + */ +#define GUAC_TERMINAL_MARGINS 2 + +/** + * 1 inch is 25.4 millimeters, and we can therefore use the following + * to create a mm to px formula: (mm × dpi) ÷ 25.4 = px. + */ +#define GUAC_TERMINAL_MM_PER_INCH 25.4 + /** * All available terminal operations which affect character cells. */ @@ -121,6 +132,11 @@ typedef struct guac_terminal_display { */ int height; + /** + * The size of margins between the console text and the border in pixels. + */ + int margin; + /** * The description of the font to use for rendering. */ diff --git a/src/terminal/terminal/terminal-priv.h b/src/terminal/terminal/terminal-priv.h index 08e2baea1..52462ce54 100644 --- a/src/terminal/terminal/terminal-priv.h +++ b/src/terminal/terminal/terminal-priv.h @@ -28,6 +28,14 @@ #include "terminal.h" #include "typescript.h" +#include + +/** + * The bitwise flag set on the modified flag of guac_terminal when the terminal + * state has been modified such that it is appropriate to flush a new frame. + */ +#define GUAC_TERMINAL_MODIFIED 1 + /** * Handler for characters printed to the terminal. When a character is printed, * the current char handler for the terminal is called and given that @@ -84,25 +92,13 @@ struct guac_terminal { */ pthread_mutex_t lock; - /** - * The mutex associated with the modified condition and flag, locked - * whenever a thread is waiting on the modified condition, the modified - * condition is being signalled, or the modified flag is being changed. - */ - pthread_mutex_t modified_lock; - /** * Flag set whenever an operation has affected the terminal in a way that - * will require a frame flush. When this flag is set, the modified_cond - * condition will be signalled. The modified_lock will always be - * acquired before this flag is altered. - */ - int modified; - - /** - * Condition which is signalled when the modified flag has been set - */ - pthread_cond_t modified_cond; + * will require a frame flush. + * + * @see GUAC_TERMINAL_MODIFIED + */ + guac_flag modified; /** * Pipe which will be the source of user input. When a terminal code @@ -305,11 +301,31 @@ struct guac_terminal { guac_terminal_display* display; /** - * Current terminal display state. All characters present on the screen - * are within this buffer. This has nothing to do with the display, which - * facilitates transfer of a set of changes to the remote display. + * The default, "normal" buffer containing all characters that should be + * displayed within the terminal emulator while not using the alternate + * buffer. Unless switched to the alternate buffer, all terminal operations + * will involve this buffer. The buffer that is relevant to terminal + * operations is determined by the current value of current_buffer. + */ + guac_terminal_buffer* normal_buffer; + + /** + * The non-default, "alternate" buffer containing all characters that should + * be displayed within the terminal emulator while not using the normal + * buffer. Unless switched to the normal buffer, all terminal operations + * will involve this buffer. The buffer that is relevant to terminal + * operations is determined by the current value of current_buffer. + */ + guac_terminal_buffer* alternate_buffer; + + /** + * Pointer to the buffer representing the current text contents of the + * terminal, including any scrollback. All characters present on the screen + * are within this buffer. The buffer pointed to by this pointer may change + * over the course of the terminal session if console codes switch between + * the normal and alternate buffers. */ - guac_terminal_buffer* buffer; + guac_terminal_buffer* current_buffer; /** * Automatically place a tabstop every N characters. If zero, then no @@ -405,6 +421,11 @@ struct guac_terminal { */ int mod_ctrl; + /** + * Whether the meta (command on Mac) key is currently being held down. + */ + int mod_meta; + /** * Whether the shift key is currently being held down. */ @@ -460,6 +481,16 @@ struct guac_terminal { */ bool disable_copy; + /** + * The time betwen two left clicks. + */ + guac_timestamp click_timer; + + /** + * Counter for left clicks. + */ + int click_counter; + }; /** @@ -521,13 +552,13 @@ int guac_terminal_clear_range(guac_terminal* term, /** * Scrolls the terminal's current scroll region up by one row. */ -int guac_terminal_scroll_up(guac_terminal* term, +void guac_terminal_scroll_up(guac_terminal* term, int start_row, int end_row, int amount); /** * Scrolls the terminal's current scroll region down by one row. */ -int guac_terminal_scroll_down(guac_terminal* term, +void guac_terminal_scroll_down(guac_terminal* term, int start_row, int end_row, int amount); /** @@ -651,5 +682,12 @@ void guac_terminal_copy_rows(guac_terminal* terminal, */ void guac_terminal_flush(guac_terminal* terminal); -#endif +/** + * Redraw default layer text and background. + * + * @param terminal + * The terminal to redraw. + */ +void guac_terminal_redraw_default_layer(guac_terminal* terminal); +#endif diff --git a/src/terminal/terminal/terminal.h b/src/terminal/terminal/terminal.h index e71b8c56a..11c8fb5a3 100644 --- a/src/terminal/terminal/terminal.h +++ b/src/terminal/terminal/terminal.h @@ -747,13 +747,18 @@ void guac_terminal_clipboard_append(guac_terminal* terminal, void guac_terminal_remove_user(guac_terminal* terminal, guac_user* user); /** - * Requests that the terminal write all output to a new pair of typescript - * files within the given path and using the given base name. Terminal output - * will be written to these new files, along with timing information. If the - * create_path flag is non-zero, the given path will be created if it does not - * yet exist. If creation of the typescript files or path fails, error messages - * will automatically be logged, and no typescript will be written. The - * typescript will automatically be closed once the terminal is freed. + * Requests that the terminal write all output to a pair of typescript + * files within the given path and using the given base name. If + * allow_write_existing is non-zero, these may be existing files; otherwise, + * the existing files may not be written to, and a non-zero value will be + * returned. + * + * Terminal output will be written to the files, along with timing information. + * If the create_path flag is non-zero, the given path will be created if it + * does not yet exist. If creation of the typescript files or path fails, + * error messages will automatically be logged, and no typescript will be + * written. The typescript will automatically be closed once the terminal is + * freed. * * @param term * The terminal whose output should be written to a typescript. @@ -771,12 +776,16 @@ void guac_terminal_remove_user(guac_terminal* terminal, guac_user* user); * written, or non-zero if the path should be created if it does not yet * exist. * + * @param allow_write_existing + * Non-zero if writing to existing files should be allowed, or zero + * otherwise. + * * @return * Zero if the typescript files have been successfully created and a * typescript will be written, non-zero otherwise. */ int guac_terminal_create_typescript(guac_terminal* term, const char* path, - const char* name, int create_path); + const char* name, int create_path, int allow_write_existing); /** * Immediately applies the given color scheme to the given terminal, overriding diff --git a/src/terminal/terminal/types.h b/src/terminal/terminal/types.h index 15333ea07..8415737aa 100644 --- a/src/terminal/terminal/types.h +++ b/src/terminal/terminal/types.h @@ -41,6 +41,11 @@ */ #define GUAC_CHAR_CONTINUATION -1 +/** + * The ASCII code of space. + */ +#define GUAC_CHAR_SPACE 32 + /** * Terminal attributes, as can be applied to a single character. */ @@ -49,29 +54,29 @@ typedef struct guac_terminal_attributes { /** * Whether the character should be rendered bold. */ - bool bold; + bool bold : 1; /** * Whether the character should be rendered with half brightness (faint * or low intensity). */ - bool half_bright; + bool half_bright : 1; /** - * Whether the character should be rendered with reversed colors - * (background becomes foreground and vice-versa). + * Whether the associated character is highlighted by the cursor. */ - bool reverse; + bool cursor : 1; /** - * Whether the associated character is highlighted by the cursor. + * Whether the character should be rendered with reversed colors + * (background becomes foreground and vice-versa). */ - bool cursor; + bool reverse : 1; /** * Whether to render the character with underscore. */ - bool underscore; + bool underscore : 1; /** * The foreground color of this character. diff --git a/src/terminal/terminal/typescript.h b/src/terminal/terminal/typescript.h index bbd8fe120..c88ec52bf 100644 --- a/src/terminal/terminal/typescript.h +++ b/src/terminal/terminal/typescript.h @@ -125,11 +125,13 @@ typedef struct guac_terminal_typescript { } guac_terminal_typescript; /** - * Creates a new pair of typescript files within the given path and using the + * Creates a pair of typescript files within the given path and using the * given base name, returning an abstraction which represents those files. - * Terminal output will be written to these new files, along with timing + * Terminal output will be written to these files, along with timing * information. If the create_path flag is non-zero, the given path will be - * created if it does not yet exist. + * created if it does not yet exist. If allow_write_existing is non-zero, + * these may be existing files; otherwise, any existing file will cause this + * function to fail, returning NULL. * * @param path * The full absolute path to a directory in which the typescript files @@ -144,12 +146,16 @@ typedef struct guac_terminal_typescript { * written, or non-zero if the path should be created if it does not yet * exist. * + * @param allow_write_existing + * Non-zero if writing to existing files should be allowed, or zero + * otherwise. + * * @return * A new guac_terminal_typescript representing the typescript files * requested, or NULL if creation of the typescript files failed. */ guac_terminal_typescript* guac_terminal_typescript_alloc(const char* path, - const char* name, int create_path); + const char* name, int create_path, int allow_write_existing); /** * Writes a single byte of terminal data to the typescript, flushing and diff --git a/src/terminal/typescript.c b/src/terminal/typescript.c index abc8486f0..8f0af081a 100644 --- a/src/terminal/typescript.c +++ b/src/terminal/typescript.c @@ -33,12 +33,12 @@ #include /** - * Attempts to open a new typescript data file within the given path and having - * the given name. If such a file already exists, sequential numeric suffixes - * (.1, .2, .3, etc.) are appended until a filename is found which does not - * exist (or until the maximum number of numeric suffixes has been tried). If - * the file absolutely cannot be opened due to an error, -1 is returned and - * errno is set appropriately. + * Attempts to open a typescript data file within the given path and having + * the given name. If such a file already exists and allow_write_existing is + * zero, sequential numeric suffixes (.1, .2, .3, etc.) are appended until a + * filename is found which does not exist (or until the maximum number of + * numeric suffixes has been tried). If the file absolutely cannot be opened + * due to an error, -1 is returned and errno is set appropriately. * * @param path * The full path to the directory in which the data file should be created. @@ -55,12 +55,17 @@ * @param basename_size * The number of bytes available within the provided basename buffer. * + * @param allow_write_existing + * Non-zero if writing to an existing file should be allowed, or zero + * otherwise. + * * @return * The file descriptor of the open data file if open succeeded, or -1 on * failure. */ static int guac_terminal_typescript_open_data_file(const char* path, - const char* name, char* basename, int basename_size) { + const char* name, char* basename, int basename_size, + int allow_write_existing) { int i; @@ -76,10 +81,11 @@ static int guac_terminal_typescript_open_data_file(const char* path, return -1; } + /* Require the file not exist already if allow_write_existing not set */ + int file_flags = O_CREAT | O_WRONLY | (allow_write_existing ? 0 : O_EXCL); + /* Attempt to open typescript data file */ - int data_fd = open(basename, - O_CREAT | O_EXCL | O_WRONLY, - S_IRUSR | S_IWUSR | S_IRGRP); + int data_fd = open(basename, file_flags, S_IRUSR | S_IWUSR | S_IRGRP); /* Continuously retry with alternate names on failure */ if (data_fd == -1) { @@ -96,9 +102,7 @@ static int guac_terminal_typescript_open_data_file(const char* path, sprintf(suffix, "%i", i); /* Retry with newly-suffixed filename */ - data_fd = open(basename, - O_CREAT | O_EXCL | O_WRONLY, - S_IRUSR | S_IWUSR | S_IRGRP); + data_fd = open(basename, file_flags, S_IRUSR | S_IWUSR | S_IRGRP); } @@ -109,7 +113,7 @@ static int guac_terminal_typescript_open_data_file(const char* path, } guac_terminal_typescript* guac_terminal_typescript_alloc(const char* path, - const char* name, int create_path) { + const char* name, int create_path, int allow_write_existing) { /* Create path if it does not exist, fail if impossible */ if (create_path && mkdir(path, S_IRWXU | S_IRGRP | S_IXGRP) @@ -124,7 +128,8 @@ guac_terminal_typescript* guac_terminal_typescript_alloc(const char* path, typescript->data_fd = guac_terminal_typescript_open_data_file( path, name, typescript->data_filename, sizeof(typescript->data_filename) - - sizeof(GUAC_TERMINAL_TYPESCRIPT_TIMING_SUFFIX)); + - sizeof(GUAC_TERMINAL_TYPESCRIPT_TIMING_SUFFIX), + allow_write_existing); if (typescript->data_fd == -1) { guac_mem_free(typescript); return NULL; @@ -139,10 +144,12 @@ guac_terminal_typescript* guac_terminal_typescript_alloc(const char* path, return NULL; } + /* Require the file not exist already if allow_write_existing not set */ + int file_flags = O_CREAT | O_WRONLY | (allow_write_existing ? 0 : O_EXCL); + /* Attempt to open typescript timing file */ typescript->timing_fd = open(typescript->timing_filename, - O_CREAT | O_EXCL | O_WRONLY, - S_IRUSR | S_IWUSR | S_IRGRP); + file_flags, S_IRUSR | S_IWUSR | S_IRGRP); if (typescript->timing_fd == -1) { close(typescript->data_fd); guac_mem_free(typescript);