Commit a8e3547b for guacamole.apache.org

commit a8e3547b779fdd9ea718b75134e8de92ef61144d
Author: Michael Jumper <mjumper@apache.org>
Date:   Fri Sep 25 18:08:51 2026 -0700

    GUACAMOLE-2118: Monitor all operations which must not block with watchdog.

diff --git a/src/libguac/Makefile.am b/src/libguac/Makefile.am
index af9e7fed..cb575f43 100644
--- a/src/libguac/Makefile.am
+++ b/src/libguac/Makefile.am
@@ -132,6 +132,7 @@ libguac_la_SOURCES =          \
     argv.c                    \
     audio.c                   \
     client.c                  \
+    client-watchdog.c         \
     display.c                 \
     display-builtin-cursors.c \
     display-cursor.c          \
diff --git a/src/libguac/client-watchdog.c b/src/libguac/client-watchdog.c
new file mode 100644
index 00000000..13849379
--- /dev/null
+++ b/src/libguac/client-watchdog.c
@@ -0,0 +1,374 @@
+/*
+ * 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/client.h"
+#include "guacamole/error.h"
+#include "guacamole/flag.h"
+#include "guacamole/mem.h"
+#include "guacamole/proctitle.h"
+#include "guacamole/timestamp.h"
+
+#include <errno.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+/**
+ * Flag set within the state of a watchdog once teardown of the connection has
+ * begun. This flag is never cleared. Once set, the connection being monitored
+ * must finish cleaning up within the cleanup timeout given to
+ * guac_client_watchdog_start(). If the connection does not finish cleaning up
+ * within this time, it will be forcibly killed.
+ */
+#define GUAC_CLIENT_WATCHDOG_TEARDOWN_STARTED 1
+
+/**
+ * Flag set within the state of a watchdog once teardown of the connection has
+ * completed. This flag is never cleared.
+ */
+#define GUAC_CLIENT_WATCHDOG_TEARDOWN_ENDED 2
+
+/**
+ * Flag set within the state of a watchdog once all users have left the
+ * connection. Once set, this flag is never cleared, and no further users are
+ * permitted to join.
+ */
+#define GUAC_CLIENT_WATCHDOG_USERS_GONE 4
+
+/**
+ * The polling interval of the watchdog's state checks, in milliseconds.
+ */
+#define GUAC_CLIENT_WATCHDOG_POLLING_INTERVAL 1000
+
+struct guac_client_watchdog {
+
+    /**
+     * The state of the watchdog. This guac_flag also functions as the lock for
+     * other state-related members of this structure.
+     *
+     * @see GUAC_CLIENT_WATCHDOG_TEARDOWN_STARTED
+     * @see GUAC_CLIENT_WATCHDOG_TEARDOWN_ENDED
+     * @see GUAC_CLIENT_WATCHDOG_USERS_GONE
+     */
+    guac_flag state;
+
+    /**
+     * The client whose connection this watchdog observes.
+     *
+     * @important
+     *     This pointer MUST NOT be dereferenced once
+     *     GUAC_CLIENT_WATCHDOG_TEARDOWN_STARTED has been set.
+     *
+     * @important
+     *     This value must only be read or modified while holding the state
+     *     lock.
+     */
+    guac_client* client;
+
+    /**
+     * The number of milliseconds allowed for a timed operation before the
+     * connection process is considered unresponsive. Within the scope of client
+     * plugins, and with the sole exception of the free_handler covered by
+     * cleanup_timeout, the handlers of guac_client and guac_user are considered
+     * timed operations. This value may safely be read without acquiring the
+     * state lock.
+     */
+    unsigned const int op_timeout;
+
+    /**
+     * The maximum number of milliseconds the process may take to clean up
+     * before it is forcibly killed. This value may safely be read without
+     * acquiring the state lock.
+     */
+    unsigned const int cleanup_timeout;
+
+    /**
+     * Whether the connection being monitored has been requested to stop. This
+     * value may safely be modified or checked without acquiring the state lock.
+     *
+     * @see guac_client_watchdog_stop_connection().
+     */
+    volatile sig_atomic_t stop_requested;
+
+    /**
+     * Whether users are currently present on the connection.
+     *
+     * @note
+     *     This value is maintained as a consistent rough count. It increases
+     *     as users join, decreases as users leave, is non-zero when at least
+     *     one user is present, and is exactly zero once all users have left.
+     *     An individual user may be counted multiple times so long as that
+     *     same number is decremented from this value when that user leaves.
+     *
+     * @important
+     *     This value must only be read or modified while holding the state
+     *     lock.
+     */
+    unsigned int users;
+
+    /**
+     * The number of timed operations currently in progress across all users of
+     * the connection.
+     *
+     * @important
+     *     This value must only be read or modified while holding the state
+     *     lock.
+     */
+    unsigned int active_operations;
+
+    /**
+     * The number of times any timed operation has started or ended, across all
+     * users of the connection.
+     *
+     * @important
+     *     This value must only be read or modified while holding the state
+     *     lock.
+     */
+    unsigned long total_operation_events;
+
+};
+
+int guac_client_watchdog_notify_user_joined(guac_client* client) {
+
+    guac_client_watchdog* watchdog = client->__watchdog;
+    if (watchdog == NULL)
+        return 0;
+
+    guac_flag_lock(&watchdog->state);
+
+    /* No further users are accepted after the last user has left */
+    if (watchdog->state.value & GUAC_CLIENT_WATCHDOG_USERS_GONE) {
+        guac_flag_unlock(&watchdog->state);
+        return 1;
+    }
+
+    watchdog->users++;
+    guac_flag_unlock(&watchdog->state);
+
+    return 0;
+
+}
+
+void guac_client_watchdog_notify_user_left(guac_client* client) {
+
+    guac_client_watchdog* watchdog = client->__watchdog;
+    if (watchdog == NULL)
+        return;
+
+    guac_flag_lock(&watchdog->state);
+
+    if (watchdog->users && --watchdog->users == 0)
+        guac_flag_set(&watchdog->state, GUAC_CLIENT_WATCHDOG_USERS_GONE);
+
+    guac_flag_unlock(&watchdog->state);
+
+}
+
+void guac_client_watchdog_notify_operation_start(guac_client* client) {
+
+    guac_client_watchdog* watchdog = client->__watchdog;
+    if (watchdog == NULL)
+        return;
+
+    guac_flag_lock(&watchdog->state);
+    watchdog->active_operations++;
+    watchdog->total_operation_events++;
+    guac_flag_unlock(&watchdog->state);
+
+}
+
+void guac_client_watchdog_notify_operation_end(guac_client* client) {
+
+    guac_client_watchdog* watchdog = client->__watchdog;
+    if (watchdog == NULL)
+        return;
+
+    guac_flag_lock(&watchdog->state);
+
+    if (watchdog->active_operations)
+        watchdog->active_operations--;
+
+    watchdog->total_operation_events++;
+    guac_flag_unlock(&watchdog->state);
+
+}
+
+void guac_client_watchdog_stop_connection(guac_client_watchdog* watchdog) {
+    watchdog->stop_requested = 1;
+}
+
+void guac_client_watchdog_notify_teardown_start(guac_client_watchdog* watchdog) {
+
+    guac_flag_set_and_lock(&watchdog->state, GUAC_CLIENT_WATCHDOG_TEARDOWN_STARTED);
+
+    if (watchdog->users == 0)
+        guac_flag_set(&watchdog->state, GUAC_CLIENT_WATCHDOG_USERS_GONE);
+
+    guac_flag_unlock(&watchdog->state);
+
+}
+
+void guac_client_watchdog_await_teardown(guac_client_watchdog* watchdog) {
+    guac_flag_wait_and_lock(&watchdog->state, GUAC_CLIENT_WATCHDOG_TEARDOWN_STARTED);
+    guac_flag_unlock(&watchdog->state);
+}
+
+void guac_client_watchdog_notify_teardown_end(guac_client_watchdog* watchdog) {
+
+    guac_flag_lock(&watchdog->state);
+
+    if (watchdog->state.value & GUAC_CLIENT_WATCHDOG_TEARDOWN_STARTED)
+        guac_flag_set(&watchdog->state, GUAC_CLIENT_WATCHDOG_TEARDOWN_ENDED);
+
+    guac_flag_unlock(&watchdog->state);
+
+}
+
+void guac_client_watchdog_await_all_users(guac_client_watchdog* watchdog) {
+    guac_flag_wait_and_lock(&watchdog->state, GUAC_CLIENT_WATCHDOG_USERS_GONE);
+    guac_flag_unlock(&watchdog->state);
+}
+
+/**
+ * Thread which guarantees the process serving a client does not continue
+ * past that client's connection.
+ *
+ * @param data
+ *     The guac_client_watchdog describing the watchdog.
+ *
+ * @return
+ *     Always NULL.
+ */
+static void* guac_client_watchdog_thread(void* data) {
+
+    guac_thread_name_set("watchdog");
+
+    guac_client_watchdog* watchdog = (guac_client_watchdog*) data;
+
+    /* Do not allow the signal handlers of any client plugin (or its libraries)
+     * to run on this thread. A signal handler that preempts this thread and
+     * blocks could prevent the cleanup deadline from being enforced. */
+    sigset_t all_signals;
+    sigfillset(&all_signals);
+    pthread_sigmask(SIG_BLOCK, &all_signals, NULL);
+
+    unsigned long last_operation_events = 0;
+    guac_timestamp last_progress = guac_timestamp_current();
+
+    /* Watch connection state until teardown begins */
+    for (;;) {
+
+        if (!guac_flag_timedwait_and_lock(&watchdog->state,
+                    GUAC_CLIENT_WATCHDOG_TEARDOWN_STARTED | GUAC_CLIENT_WATCHDOG_USERS_GONE,
+                    GUAC_CLIENT_WATCHDOG_POLLING_INTERVAL)) {
+            guac_flag_lock(&watchdog->state);
+        }
+
+        /* Teardown may have begun while the lock was being acquired. If it has,
+         * it's no longer safe to dereference the client, and we must stop
+         * monitoring here */
+        if (watchdog->state.value & GUAC_CLIENT_WATCHDOG_TEARDOWN_STARTED) {
+            guac_flag_unlock(&watchdog->state);
+            break;
+        }
+
+        guac_timestamp current = guac_timestamp_current();
+
+        /* Track general operation progress, stopping the client if at least one
+         * operation must have exceeded the timeout */
+        if (watchdog->active_operations == 0
+                || watchdog->total_operation_events != last_operation_events) {
+            last_operation_events = watchdog->total_operation_events;
+            last_progress = current;
+        }
+        else if (current - last_progress >= watchdog->op_timeout) {
+            guac_client_watchdog_stop_connection(watchdog);
+        }
+
+        /* Switch from watching connection behavior to watching termination
+         * progress if any termination condition is met (stop forced/requested
+         * or all users have left) */
+        if (watchdog->stop_requested
+                || watchdog->client->state != GUAC_CLIENT_RUNNING
+                || (watchdog->state.value & GUAC_CLIENT_WATCHDOG_USERS_GONE)) {
+            guac_client_watchdog_notify_teardown_start(watchdog);
+            guac_flag_unlock(&watchdog->state);
+            break;
+        }
+
+        guac_flag_unlock(&watchdog->state);
+
+    }
+
+    /* Teardown must complete within the cleanup timeout */
+    if (guac_flag_timedwait_and_lock(&watchdog->state,
+                GUAC_CLIENT_WATCHDOG_TEARDOWN_ENDED,
+                watchdog->cleanup_timeout)) {
+        guac_flag_unlock(&watchdog->state);
+        return NULL;
+    }
+
+    /* If we reach here, teardown did not complete in time. NOTE: We do not log
+     * here for the reason noted above - logging would mean invoking
+     * guac_client_log() and there's no guarantee that the client has not yet
+     * been freed. */
+    kill(0, SIGKILL);
+
+    /* We should never reach here so long as SIGKILL delivery is possible */
+    _exit(EXIT_FAILURE);
+
+}
+
+guac_client_watchdog* guac_client_watchdog_start(guac_client* client,
+        unsigned int op_timeout, unsigned int cleanup_timeout) {
+
+    /* NOTE: Once started successfully, the watchdog allocated here is never
+     * freed. Its struct continues to be available and its thread continues to
+     * run until the process exits. */
+
+    guac_client_watchdog* watchdog = guac_mem_zalloc(sizeof(guac_client_watchdog));
+    if (watchdog == NULL) {
+        guac_error = GUAC_STATUS_NO_MEMORY;
+        guac_error_message = "Insufficient memory to allocate client watchdog";
+        return NULL;
+    }
+
+    guac_flag_init(&watchdog->state);
+    watchdog->client = client;
+    *((unsigned int*) &watchdog->op_timeout) = op_timeout;
+    *((unsigned int*) &watchdog->cleanup_timeout) = cleanup_timeout;
+
+    pthread_t watchdog_thread;
+    int result = pthread_create(&watchdog_thread, NULL, guac_client_watchdog_thread, watchdog);
+    if (result) {
+        guac_flag_destroy(&watchdog->state);
+        guac_mem_free(watchdog);
+        guac_error = GUAC_STATUS_SEE_ERRNO;
+        guac_error_message = "Unable to start client watchdog thread";
+        errno = result;
+        return NULL;
+    }
+
+    pthread_detach(watchdog_thread);
+
+    client->__watchdog = watchdog;
+    return watchdog;
+
+}
diff --git a/src/libguac/client.c b/src/libguac/client.c
index b2919c44..416fc7a9 100644
--- a/src/libguac/client.c
+++ b/src/libguac/client.c
@@ -173,16 +173,19 @@ static void guac_client_promote_pending_users(guac_client* client) {
     /* Run the pending join handler, if one is defined */
     if (client->join_pending_handler) {

-        /* If an error occurs in the pending handler */
-        if(client->join_pending_handler(client)) {
+        guac_client_watchdog_notify_operation_start(client);
+        int join_pending_failed = client->join_pending_handler(client);
+        guac_client_watchdog_notify_operation_end(client);

-            /* Log a warning and abort the promotion of the pending users */
+        /* Log a warning and abort the promotion of the pending users if an error
+         * occurs in the pending handler */
+        if (join_pending_failed) {
             guac_client_log(client, GUAC_LOG_WARNING,
                     "join_pending_handler did not successfully complete;"
                     " any pending users have not been promoted.\n");
-
             goto promotion_complete;
         }
+
     }

     /* The first pending user in the list, if any */
@@ -470,8 +473,11 @@ int guac_client_add_user(guac_client* client, guac_user* user, int argc, char**
     int retval = 0;

     /* Call handler, if defined */
-    if (client->join_handler)
+    if (client->join_handler) {
+        guac_client_watchdog_notify_operation_start(client);
         retval = client->join_handler(user, argc, argv);
+        guac_client_watchdog_notify_operation_end(client);
+    }

     if (retval == 0) {

@@ -526,10 +532,16 @@ void guac_client_remove_user(guac_client* client, guac_user* user) {
         guac_client_owner_notify_leave(client, user);

     /* Call handler, if defined */
-    if (user->leave_handler)
+    if (user->leave_handler) {
+        guac_client_watchdog_notify_operation_start(client);
         user->leave_handler(user);
-    else if (client->leave_handler)
+        guac_client_watchdog_notify_operation_end(client);
+    }
+    else if (client->leave_handler) {
+        guac_client_watchdog_notify_operation_start(client);
         client->leave_handler(user);
+        guac_client_watchdog_notify_operation_end(client);
+    }

 }

@@ -641,6 +653,9 @@ int guac_client_end_multiple_frames(guac_client* client, int frames) {

 int guac_client_load_plugin(guac_client* client, const char* protocol) {

+    int retval = -1;
+    guac_client_watchdog_notify_operation_start(client);
+
     /* Reference to dlopen()'d plugin */
     void* client_plugin_handle;

@@ -661,7 +676,7 @@ int guac_client_load_plugin(guac_client* client, const char* protocol) {
                 sizeof(protocol_lib)) >= sizeof(protocol_lib)) {
         guac_error = GUAC_STATUS_NO_MEMORY;
         guac_error_message = "Protocol name is too long";
-        return -1;
+        goto finished;
     }

     /* Load client plugin */
@@ -669,7 +684,7 @@ int guac_client_load_plugin(guac_client* client, const char* protocol) {
     if (!client_plugin_handle) {
         guac_error = GUAC_STATUS_NOT_FOUND;
         guac_error_message = dlerror();
-        return -1;
+        goto finished;
     }

     dlerror(); /* Clear errors */
@@ -682,13 +697,17 @@ int guac_client_load_plugin(guac_client* client, const char* protocol) {
         guac_error = GUAC_STATUS_INTERNAL_ERROR;
         guac_error_message = dlerror();
         dlclose(client_plugin_handle);
-        return -1;
+        goto finished;
     }

     /* Init client */
     client->__plugin_handle = client_plugin_handle;

-    return alias.client_init(client);
+    retval = alias.client_init(client);
+
+finished:
+    guac_client_watchdog_notify_operation_end(client);
+    return retval;

 }

diff --git a/src/libguac/guacamole/client-types.h b/src/libguac/guacamole/client-types.h
index 1caa6bcb..c6d642bf 100644
--- a/src/libguac/guacamole/client-types.h
+++ b/src/libguac/guacamole/client-types.h
@@ -34,6 +34,14 @@
  */
 typedef struct guac_client guac_client;

+/**
+ * Watchdog guarding resource consumption and termination behavior of
+ * guac_client. The watchdog is intended to guarantee proper cleanup of the
+ * per-connection process and any child processes, as well as to recognize cases
+ * where a connection process has stopped responding and must be terminated.
+ */
+typedef struct guac_client_watchdog guac_client_watchdog;
+
 /**
  * Possible current states of the Guacamole client. Currently, the only
  * two states are GUAC_CLIENT_RUNNING and GUAC_CLIENT_STOPPING.
diff --git a/src/libguac/guacamole/client.h b/src/libguac/guacamole/client.h
index 01ecbc84..27587b64 100644
--- a/src/libguac/guacamole/client.h
+++ b/src/libguac/guacamole/client.h
@@ -311,6 +311,12 @@ struct guac_client {
      */
     void* __plugin_handle;

+    /**
+     * The watchdog for this client, or NULL if guac_client_watchdog_start() has
+     * not yet been called.
+     */
+    guac_client_watchdog* __watchdog;
+
 };

 /**
@@ -393,6 +399,153 @@ void guac_client_abort(guac_client* client, guac_protocol_status status,
 void vguac_client_abort(guac_client* client, guac_protocol_status status,
         const char* format, va_list ap);

+/**
+ * Allocates and starts the watchdog for the given client. If the watchdog could
+ * not be allocated or started, NULL is returned and guac_error is set
+ * appropriately.
+ *
+ * The watchdog observes the behavior of the connection and guarantees the
+ * connection process cleans itself up in a timely manner upon disconnect. If
+ * the connection process fails to clean up in a timely manner, or stops
+ * responding, it is forcibly killed.
+ *
+ * @important
+ *     The client monitored by the watchdog MUST NOT be freed until after
+ *     guac_client_watchdog_notify_teardown_start() has been called and
+ *     no further users remain connected.
+ *
+ * @see guac_client_watchdog_notify_teardown_start()
+ * @see guac_client_watchdog_await_all_users()
+ *
+ * @param client
+ *     The client that the watchdog should observe.
+ *
+ * @param op_timeout
+ *     The number of milliseconds allowed for a timed operation before the
+ *     connection process is considered unresponsive. Within the scope of client
+ *     plugins, and with the sole exception of the free_handler covered by
+ *     cleanup_timeout, the handlers of guac_client and guac_user are considered
+ *     timed operations.
+ *
+ * @param cleanup_timeout
+ *     The maximum number of milliseconds the process may take to clean up
+ *     before it is forcibly killed.
+ *
+ * @return
+ *     The newly started watchdog, or NULL if the watchdog could not be started.
+ */
+guac_client_watchdog* guac_client_watchdog_start(guac_client* client,
+        unsigned int op_timeout, unsigned int cleanup_timeout);
+
+/**
+ * Requests that the given watchdog terminate the connection it is monitoring.
+ *
+ * @note
+ *     This function is intentionally async-signal-safe and may be called at
+ *     any time from any thread, even after the relevant guac_client has been
+ *     freed.
+ *
+ * @param watchdog
+ *     The watchdog of the client whose connection should terminate.
+ */
+void guac_client_watchdog_stop_connection(guac_client_watchdog* watchdog);
+
+/**
+ * Notifies the given watchdog that teardown has begun for the client that the
+ * watchdog is monitoring. Any thread waiting on
+ * guac_client_watchdog_await_teardown() is unblocked. If teardown has already
+ * been notified, this function has no effect.
+ *
+ * @param watchdog
+ *     The watchdog of the client whose connection is being torn down.
+ */
+void guac_client_watchdog_notify_teardown_start(guac_client_watchdog* watchdog);
+
+/**
+ * Notifies the given watchdog that teardown has completed for the client that
+ * the watchdog is monitoring. This function may only be called after
+ * guac_client_watchdog_notify_teardown_start().
+ *
+ * @param watchdog
+ *     The watchdog of the client whose connection has been torn down.
+ */
+void guac_client_watchdog_notify_teardown_end(guac_client_watchdog* watchdog);
+
+/**
+ * Notifies the watchdog of the given client that a new user is joining the
+ * connection. Once all users of a connection have left, any further attempts to
+ * join the connection are refused. If the client does not have an associated
+ * watchdog, this function has no effect.
+ *
+ * @param client
+ *     The client the user has joined.
+ *
+ * @return
+ *     Zero if the user may be accepted, non-zero if all users have left and
+ *     this user's attempt to join should be refused.
+ */
+int guac_client_watchdog_notify_user_joined(guac_client* client);
+
+/**
+ * Notifies the watchdog of the given client that an existing user is leaving
+ * the connection. This function may be called multiple times per user, but
+ * MUST be called exactly once for each call to
+ * guac_client_watchdog_notify_user_joined(). If the client does not have an
+ * associated watchdog, this function has no effect.
+ *
+ * @param client
+ *     The client the user has left.
+ */
+void guac_client_watchdog_notify_user_left(guac_client* client);
+
+/**
+ * Notifies the watchdog of the given client that an operation is beginning that
+ * must complete within a bounded amount of time. The general amount of time
+ * that should be permitted for timed operations is provided when the watchdog
+ * was originally started. Every call must be balanced by a call to
+ * guac_client_watchdog_notify_operation_end(), even if the operation fails.
+ * Calls may safely overlap and nest. If the client does not have an associated
+ * watchdog, this function has no effect.
+ *
+ * @see guac_client_watchdog_start()
+ * @see guac_client_watchdog_notify_operation_end()
+ *
+ * @param client
+ *     The client on whose behalf the operation is about to be performed.
+ */
+void guac_client_watchdog_notify_operation_start(guac_client* client);
+
+/**
+ * Notifies the watchdog of the given client that an operation associated with a
+ * call to guac_client_watchdog_notify_operation_start() has completed. If the
+ * client does not have an associated watchdog, this function has no effect.
+ *
+ * @param client
+ *     The client on whose behalf the operation was performed.
+ */
+void guac_client_watchdog_notify_operation_end(guac_client* client);
+
+/**
+ * Blocks until teardown of the connection monitored by the given watchdog has
+ * begun, whether because the watchdog observed that the connection terminated
+ * or because guac_client_watchdog_notify_teardown_start() was called.
+ *
+ * @see guac_client_watchdog_notify_teardown_start()
+ *
+ * @param watchdog
+ *     The watchdog to wait on.
+ */
+void guac_client_watchdog_await_teardown(guac_client_watchdog* watchdog);
+
+/**
+ * Waits until no users of the connection monitored by the given watchdog remain
+ * connected.
+ *
+ * @param watchdog
+ *     The watchdog to wait on.
+ */
+void guac_client_watchdog_await_all_users(guac_client_watchdog* watchdog);
+
 /**
  * Allocates a new buffer (invisible layer). An arbitrary index is
  * automatically assigned if no existing buffer is available for use.
diff --git a/src/libguac/user-handshake.c b/src/libguac/user-handshake.c
index 4b79ccae..e3492c44 100644
--- a/src/libguac/user-handshake.c
+++ b/src/libguac/user-handshake.c
@@ -166,8 +166,12 @@ static void* guac_user_input_thread(void* data) {
         guac_error_message = NULL;

         /* Call handler, stop on error */
-        if (__guac_user_call_opcode_handler(__guac_instruction_handler_map,
-                user, parser->opcode, parser->argc, parser->argv)) {
+        guac_client_watchdog_notify_operation_start(client);
+        int handler_failed = __guac_user_call_opcode_handler(__guac_instruction_handler_map,
+                user, parser->opcode, parser->argc, parser->argv);
+        guac_client_watchdog_notify_operation_end(client);
+
+        if (handler_failed) {

             /* Log error */
             guac_user_log_guac_error(user, GUAC_LOG_WARNING,
@@ -269,9 +273,13 @@ static int __guac_user_handshake(guac_user* user, guac_parser* parser,
         guac_user_log(user, GUAC_LOG_DEBUG, "Processing instruction: %s",
                 parser->opcode);

-        /* Run instruction handler for opcode with arguments. */
-        if (__guac_user_call_opcode_handler(__guac_handshake_handler_map, user,
-                parser->opcode, parser->argc, parser->argv)) {
+        /* Run instruction handler for opcode with arguments */
+        guac_client_watchdog_notify_operation_start(user->client);
+        int handler_failed = __guac_user_call_opcode_handler(__guac_handshake_handler_map,
+                user, parser->opcode, parser->argc, parser->argv);
+        guac_client_watchdog_notify_operation_end(user->client);
+
+        if (handler_failed) {

             guac_user_log_handshake_failure(user);
             guac_user_log_guac_error(user, GUAC_LOG_DEBUG,
@@ -279,7 +287,6 @@ static int __guac_user_handshake(guac_user* user, guac_parser* parser,
             guac_user_log(user, GUAC_LOG_DEBUG, "Failed opcode: %s",
                     parser->opcode);

-            guac_parser_free(parser);
             return 1;

         }
@@ -294,9 +301,17 @@ static int __guac_user_handshake(guac_user* user, guac_parser* parser,

 int guac_user_handle_connection(guac_user* user, int usec_timeout) {

+    int retval = 1;
+
     guac_socket* socket = user->socket;
     guac_client* client = user->client;
-
+    guac_parser* parser = NULL;
+
+    /* Immediately report change in user count, denying the attempt if all users
+     * have left */
+    if (guac_client_watchdog_notify_user_joined(client))
+        return 1;
+
     user->info.audio_mimetypes = NULL;
     user->info.image_mimetypes = NULL;
     user->info.video_mimetypes = NULL;
@@ -316,16 +331,14 @@ int guac_user_handle_connection(guac_user* user, int usec_timeout) {
         guac_user_log_guac_error(user, GUAC_LOG_DEBUG,
                 "Error sending \"args\" to new user");

-        return 1;
+        goto finished;
     }

-    guac_parser* parser = guac_parser_alloc();
+    parser = guac_parser_alloc();

     /* Perform the handshake with the client. */
-    if (__guac_user_handshake(user, parser, usec_timeout)) {
-        guac_parser_free(parser);
-        return 1;
-    }
+    if (__guac_user_handshake(user, parser, usec_timeout))
+        goto finished;

     /* Acknowledge connection availability */
     guac_protocol_send_ready(socket, client->connection_id);
@@ -335,7 +348,7 @@ int guac_user_handle_connection(guac_user* user, int usec_timeout) {
     if (parser->argc != (num_args + 1)) {
         guac_client_log(client, GUAC_LOG_ERROR, "Client did not return the "
                 "expected number of arguments.");
-        return 1;
+        goto finished;
     }

     /* Attempt to join user to connection. */
@@ -378,11 +391,17 @@ int guac_user_handle_connection(guac_user* user, int usec_timeout) {
     /* Free name and timezone info. */
     guac_mem_free_const(user->info.name);
     guac_mem_free_const(user->info.timezone);
-
-    guac_parser_free(parser);

     /* Successful disconnect */
-    return 0;
+    retval = 0;
+
+finished:
+
+    if (parser != NULL)
+        guac_parser_free(parser);
+
+    guac_client_watchdog_notify_user_left(client);
+    return retval;

 }