diff --git a/src/libguac/Makefile.am b/src/libguac/Makefile.am index 46d4e589..781f8622 100644 --- a/src/libguac/Makefile.am +++ b/src/libguac/Makefile.am @@ -70,26 +70,27 @@ noinst_HEADERS = \ user-handlers.h \ raw_encoder.h -libguac_la_SOURCES = \ - audio.c \ - client.c \ - encode-jpeg.c \ - encode-png.c \ - error.c \ - hash.c \ - id.c \ - palette.c \ - parser.c \ - pool.c \ - protocol.c \ - raw_encoder.c \ - socket.c \ - socket-fd.c \ - socket-nest.c \ - socket-tee.c \ - timestamp.c \ - unicode.c \ - user.c \ +libguac_la_SOURCES = \ + audio.c \ + client.c \ + encode-jpeg.c \ + encode-png.c \ + error.c \ + hash.c \ + id.c \ + palette.c \ + parser.c \ + pool.c \ + protocol.c \ + raw_encoder.c \ + socket.c \ + socket-broadcast.c \ + socket-fd.c \ + socket-nest.c \ + socket-tee.c \ + timestamp.c \ + unicode.c \ + user.c \ user-handlers.c # Compile WebP support if available diff --git a/src/libguac/client.c b/src/libguac/client.c index 2bf54718..a88de74d 100644 --- a/src/libguac/client.c +++ b/src/libguac/client.c @@ -52,23 +52,6 @@ guac_layer __GUAC_DEFAULT_LAYER = { const guac_layer* GUAC_DEFAULT_LAYER = &__GUAC_DEFAULT_LAYER; -/** - * Single chunk of data, to be broadcast to all users. - */ -typedef struct __write_chunk { - - /** - * The buffer to write. - */ - const void* buffer; - - /** - * The number of bytes in the buffer. - */ - size_t length; - -} __write_chunk; - guac_layer* guac_client_alloc_layer(guac_client* client) { /* Init new layer */ @@ -143,251 +126,6 @@ void guac_client_free_stream(guac_client* client, guac_stream* stream) { } -/** - * Callback which handles read requests on the broadcast socket. This callback - * always fails, as the broadcast socket is write-only; it cannot be read. - * - * @param socket - * The broadcast socket to read from. - * - * @param buf - * The buffer into which data should be read. - * - * @param count - * The number of bytes to attempt to read. - * - * @return - * The number of bytes read, or -1 if an error occurs. This implementation - * always returns -1, as the broadcast socket is write-only and cannot be - * read. - */ -static ssize_t __guac_socket_broadcast_read_handler(guac_socket* socket, - void* buf, size_t count) { - - /* Broadcast socket reads are not allowed */ - return -1; - -} - -/** - * Callback invoked by guac_client_foreach_user() which write a given chunk of - * data to that user's socket. If the write attempt fails, the user is - * signalled to stop with guac_user_stop(). - * - * @param user - * The user that the chunk of data should be written to. - * - * @param data - * A pointer to a __write_chunk which describes the data to be written. - * - * @return - * Always NULL. - */ -static void* __write_chunk_callback(guac_user* user, void* data) { - - __write_chunk* chunk = (__write_chunk*) data; - - /* Attempt write, disconnect on failure */ - if (guac_socket_write(user->socket, chunk->buffer, chunk->length)) - guac_user_stop(user); - - return NULL; - -} - -/** - * Socket write handler which operates on each of the sockets of all connected - * users. This write handler will always succeed, but any failing user-specific - * writes will invoke guac_user_stop() on the failing user. - * - * @param socket - * The socket to which the given data must be written. - * - * @param buf - * The buffer containing the data to write. - * - * @param count - * The number of bytes to attempt to write from the given buffer. - * - * @return - * The number of bytes written, or -1 if an error occurs. This handler will - * always succeed, and thus will always return the exact number of bytes - * specified by count. - */ -static ssize_t __guac_socket_broadcast_write_handler(guac_socket* socket, - const void* buf, size_t count) { - - guac_client* client = (guac_client*) socket->data; - - /* Build chunk */ - __write_chunk chunk; - chunk.buffer = buf; - chunk.length = count; - - /* Broadcast chunk to all users */ - guac_client_foreach_user(client, __write_chunk_callback, &chunk); - - return count; - -} - -/** - * Callback which is invoked by guac_client_foreach_user() to flush all - * pending data on the given user's socket. If an error occurs while flushing - * a user's socket, that user is signalled to stop with guac_user_stop(). - * - * @param user - * The user whose socket should be flushed. - * - * @param data - * Arbitrary data passed to guac_client_foreach_user(). This is not needed - * by this callback, and should be left as NULL. - * - * @return - * Always NULL. - */ -static void* __flush_callback(guac_user* user, void* data) { - - /* Attempt flush, disconnect on failure */ - if (guac_socket_flush(user->socket)) - guac_user_stop(user); - - return NULL; - -} - -/** - * Socket flush handler which operates on each of the sockets of all connected - * users. This flush handler will always succeed, but any failing user-specific - * flush will invoke guac_user_stop() on the failing user. - * - * @param socket - * The broadcast socket to flush. - * - * @return - * Zero if the flush operation succeeds, non-zero if the operation fails. - * This handler will always succeed, and thus will always return zero. - */ -static ssize_t __guac_socket_broadcast_flush_handler(guac_socket* socket) { - - guac_client* client = (guac_client*) socket->data; - - /* Flush all users */ - guac_client_foreach_user(client, __flush_callback, NULL); - - return 0; - -} - -/** - * Callback which is invoked by guac_client_foreach_user() to lock the given - * user's socket in preparation for the beginning of a Guacamole protocol - * instruction. - * - * @param user - * The user whose socket should be locked. - * - * @param data - * Arbitrary data passed to guac_client_foreach_user(). This is not needed - * by this callback, and should be left as NULL. - * - * @return - * Always NULL. - */ -static void* __lock_callback(guac_user* user, void* data) { - - /* Lock socket */ - guac_socket_instruction_begin(user->socket); - - return NULL; - -} - -/** - * Socket lock handler which acquires the socket locks of all connected users. - * Socket-level locks are acquired in preparation for the beginning of a new - * Guacamole instruction to ensure that parallel writes are only interleaved at - * instruction boundaries. - * - * @param socket - * The broadcast socket to lock. - */ -static void __guac_socket_broadcast_lock_handler(guac_socket* socket) { - - guac_client* client = (guac_client*) socket->data; - - /* Lock sockets of all users */ - guac_client_foreach_user(client, __lock_callback, NULL); - -} - -/** - * Callback which is invoked by guac_client_foreach_user() to unlock the given - * user's socket at the end of a Guacamole protocol instruction. - * - * @param user - * The user whose socket should be unlocked. - * - * @param data - * Arbitrary data passed to guac_client_foreach_user(). This is not needed - * by this callback, and should be left as NULL. - * - * @return - * Always NULL. - */ -static void* __unlock_callback(guac_user* user, void* data) { - - /* Unlock socket */ - guac_socket_instruction_end(user->socket); - - return NULL; - -} - -/** - * Socket unlock handler which releases the socket locks of all connected users. - * Socket-level locks are released after a Guacamole instruction has finished - * being written. - * - * @param socket - * The broadcast socket to unlock. - */ -static void __guac_socket_broadcast_unlock_handler(guac_socket* socket) { - - guac_client* client = (guac_client*) socket->data; - - /* Unlock sockets of all users */ - guac_client_foreach_user(client, __unlock_callback, NULL); - -} - -/** - * Callback which handles select operations on the broadcast socket, waiting - * for data to become available such that the next read operation will not - * block. This callback always fails, as the broadcast socket is write-only; it - * cannot be read. - * - * @param socket - * The broadcast socket to wait for. - * - * @param usec_timeout - * The maximum amount of time to wait for data, in microseconds, or -1 to - * potentially wait forever. - * - * @return - * A positive value on success, zero if the timeout elapsed and no data is - * available, or a negative value if an error occurs. This implementation - * always returns -1, as the broadcast socket is write-only and cannot be - * read. - */ -static int __guac_socket_broadcast_select_handler(guac_socket* socket, - int usec_timeout) { - - /* Selecting the broadcast socket is not possible */ - return -1; - -} - guac_client* guac_client_alloc() { int i; @@ -437,16 +175,7 @@ guac_client* guac_client_alloc() { pthread_rwlock_init(&(client->__users_lock), &lock_attributes); /* Set up socket to broadcast to all users */ - guac_socket* socket = guac_socket_alloc(); - client->socket = socket; - socket->data = client; - - socket->read_handler = __guac_socket_broadcast_read_handler; - socket->write_handler = __guac_socket_broadcast_write_handler; - socket->select_handler = __guac_socket_broadcast_select_handler; - socket->flush_handler = __guac_socket_broadcast_flush_handler; - socket->lock_handler = __guac_socket_broadcast_lock_handler; - socket->unlock_handler = __guac_socket_broadcast_unlock_handler; + client->socket = guac_socket_broadcast(client); return client; @@ -550,12 +279,12 @@ int guac_client_add_user(guac_client* client, guac_user* user, int argc, char** int retval = 0; - pthread_rwlock_wrlock(&(client->__users_lock)); - /* Call handler, if defined */ if (client->join_handler) retval = client->join_handler(user, argc, argv); + pthread_rwlock_wrlock(&(client->__users_lock)); + /* Add to list if join was successful */ if (retval == 0) { @@ -584,12 +313,6 @@ void guac_client_remove_user(guac_client* client, guac_user* user) { pthread_rwlock_wrlock(&(client->__users_lock)); - /* Call handler, if defined */ - if (user->leave_handler) - user->leave_handler(user); - else if (client->leave_handler) - client->leave_handler(user); - /* Update prev / head */ if (user->__prev != NULL) user->__prev->__next = user->__next; @@ -608,6 +331,12 @@ void guac_client_remove_user(guac_client* client, guac_user* user) { pthread_rwlock_unlock(&(client->__users_lock)); + /* Call handler, if defined */ + if (user->leave_handler) + user->leave_handler(user); + else if (client->leave_handler) + client->leave_handler(user); + } void guac_client_foreach_user(guac_client* client, guac_user_callback* callback, void* data) { diff --git a/src/libguac/guacamole/client.h b/src/libguac/guacamole/client.h index ccb49cf1..2d01573c 100644 --- a/src/libguac/guacamole/client.h +++ b/src/libguac/guacamole/client.h @@ -53,10 +53,6 @@ struct guac_client { * provide their own mechanism of I/O for their protocol. The guac_socket * structure is used only to communicate conveniently with the Guacamole * web-client. - * - * Because this socket broadcasts to all connected users, this socket MUST - * NOT be used within the same thread as a "leave" or "join" handler. Doing - * so results in undefined behavior, including possible segfaults. */ guac_socket* socket; @@ -437,10 +433,6 @@ void guac_client_remove_user(guac_client* client, guac_user* user); * MAY invoke guac_client_foreach_user(), doing so should not be necessary, and * may indicate poor design choices. * - * Because this function loops through all connected users, this function MUST - * NOT be invoked within the same thread as a "leave" or "join" handler. Doing - * so results in undefined behavior, including possible segfaults. - * * @param client * The client whose users should be iterated. * @@ -465,11 +457,6 @@ void guac_client_foreach_user(guac_client* client, * This function is reentrant, but the user list MUST NOT be manipulated * within the same thread as a callback to this function. * - * Because this function depends on a consistent list of connected users, this - * function MUST NOT be invoked within the same thread as a "leave" or "join" - * handler. Doing so results in undefined behavior, including possible - * segfaults. - * * @param client * The client to retrieve the owner from. * @@ -498,11 +485,6 @@ void* guac_client_for_owner(guac_client* client, guac_user_callback* callback, * This function is reentrant, but the user list MUST NOT be manipulated * within the same thread as a callback to this function. * - * Because this function depends on a consistent list of connected users, this - * function MUST NOT be invoked within the same thread as a "leave" or "join" - * handler. Doing so results in undefined behavior, including possible - * segfaults. - * * @param client * The client that the given user is expected to be associated with. * diff --git a/src/libguac/guacamole/socket.h b/src/libguac/guacamole/socket.h index 1290c34a..e83a4ebc 100644 --- a/src/libguac/guacamole/socket.h +++ b/src/libguac/guacamole/socket.h @@ -26,6 +26,7 @@ * @file socket.h */ +#include "client-types.h" #include "socket-constants.h" #include "socket-fntypes.h" #include "socket-types.h" @@ -220,6 +221,32 @@ guac_socket* guac_socket_nest(guac_socket* parent, int index); */ guac_socket* guac_socket_tee(guac_socket* primary, guac_socket* secondary); +/** + * Allocates and initializes a new guac_socket which duplicates all + * instructions written across the sockets of each connected user of the given + * guac_client. The returned socket is a write-only socket. Attempts to read + * from the socket will fail. If a write occurs while no users are connected, + * that write will simply be dropped. + * + * Return values (error codes) from each user's socket will not affect the + * in-progress write, but each failing user will be forcibly stopped with + * guac_user_stop(). + * + * If an error occurs while allocating the guac_socket object, NULL is returned, + * and guac_error is set appropriately. + * + * @param client + * The client associated with the group of connected users across which + * duplicates of all instructions should be written. + * + * @return + * A write-only guac_socket object which broadcasts copies of all + * instructions written across all connected users of the given + * guac_client, or NULL if an error occurs while allocating the guac_socket + * object. + */ +guac_socket* guac_socket_broadcast(guac_client* client); + /** * Writes the given unsigned int to the given guac_socket object. The data * written may be buffered until the buffer is flushed automatically or diff --git a/src/libguac/socket-broadcast.c b/src/libguac/socket-broadcast.c new file mode 100644 index 00000000..a3f17fd0 --- /dev/null +++ b/src/libguac/socket-broadcast.c @@ -0,0 +1,377 @@ +/* + * 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 "client.h" +#include "error.h" +#include "socket.h" +#include "user.h" + +#include +#include + +/** + * Data associated with an open socket which writes to all connected users of + * a particular guac_client. + */ +typedef struct guac_socket_broadcast_data { + + /** + * The guac_client whose connected users should receive all instructions + * written to this socket. + */ + guac_client* client; + + /** + * Lock which is acquired when an instruction is being written, and + * released when the instruction is finished being written. + */ + pthread_mutex_t socket_lock; + +} guac_socket_broadcast_data; + +/** + * Single chunk of data, to be broadcast to all users. + */ +typedef struct __write_chunk { + + /** + * The buffer to write. + */ + const void* buffer; + + /** + * The number of bytes in the buffer. + */ + size_t length; + +} __write_chunk; + +/** + * Callback which handles read requests on the broadcast socket. This callback + * always fails, as the broadcast socket is write-only; it cannot be read. + * + * @param socket + * The broadcast socket to read from. + * + * @param buf + * The buffer into which data should be read. + * + * @param count + * The number of bytes to attempt to read. + * + * @return + * The number of bytes read, or -1 if an error occurs. This implementation + * always returns -1, as the broadcast socket is write-only and cannot be + * read. + */ +static ssize_t __guac_socket_broadcast_read_handler(guac_socket* socket, + void* buf, size_t count) { + + /* Broadcast socket reads are not allowed */ + return -1; + +} + +/** + * Callback invoked by guac_client_foreach_user() which write a given chunk of + * data to that user's socket. If the write attempt fails, the user is + * signalled to stop with guac_user_stop(). + * + * @param user + * The user that the chunk of data should be written to. + * + * @param data + * A pointer to a __write_chunk which describes the data to be written. + * + * @return + * Always NULL. + */ +static void* __write_chunk_callback(guac_user* user, void* data) { + + __write_chunk* chunk = (__write_chunk*) data; + + /* Attempt write, disconnect on failure */ + if (guac_socket_write(user->socket, chunk->buffer, chunk->length)) + guac_user_stop(user); + + return NULL; + +} + +/** + * Socket write handler which operates on each of the sockets of all connected + * users. This write handler will always succeed, but any failing user-specific + * writes will invoke guac_user_stop() on the failing user. + * + * @param socket + * The socket to which the given data must be written. + * + * @param buf + * The buffer containing the data to write. + * + * @param count + * The number of bytes to attempt to write from the given buffer. + * + * @return + * The number of bytes written, or -1 if an error occurs. This handler will + * always succeed, and thus will always return the exact number of bytes + * specified by count. + */ +static ssize_t __guac_socket_broadcast_write_handler(guac_socket* socket, + const void* buf, size_t count) { + + guac_socket_broadcast_data* data = + (guac_socket_broadcast_data*) socket->data; + + /* Build chunk */ + __write_chunk chunk; + chunk.buffer = buf; + chunk.length = count; + + /* Broadcast chunk to all users */ + guac_client_foreach_user(data->client, __write_chunk_callback, &chunk); + + return count; + +} + +/** + * Callback which is invoked by guac_client_foreach_user() to flush all + * pending data on the given user's socket. If an error occurs while flushing + * a user's socket, that user is signalled to stop with guac_user_stop(). + * + * @param user + * The user whose socket should be flushed. + * + * @param data + * Arbitrary data passed to guac_client_foreach_user(). This is not needed + * by this callback, and should be left as NULL. + * + * @return + * Always NULL. + */ +static void* __flush_callback(guac_user* user, void* data) { + + /* Attempt flush, disconnect on failure */ + if (guac_socket_flush(user->socket)) + guac_user_stop(user); + + return NULL; + +} + +/** + * Socket flush handler which operates on each of the sockets of all connected + * users. This flush handler will always succeed, but any failing user-specific + * flush will invoke guac_user_stop() on the failing user. + * + * @param socket + * The broadcast socket to flush. + * + * @return + * Zero if the flush operation succeeds, non-zero if the operation fails. + * This handler will always succeed, and thus will always return zero. + */ +static ssize_t __guac_socket_broadcast_flush_handler(guac_socket* socket) { + + guac_socket_broadcast_data* data = + (guac_socket_broadcast_data*) socket->data; + + /* Flush all users */ + guac_client_foreach_user(data->client, __flush_callback, NULL); + + return 0; + +} + +/** + * Callback which is invoked by guac_client_foreach_user() to lock the given + * user's socket in preparation for the beginning of a Guacamole protocol + * instruction. + * + * @param user + * The user whose socket should be locked. + * + * @param data + * Arbitrary data passed to guac_client_foreach_user(). This is not needed + * by this callback, and should be left as NULL. + * + * @return + * Always NULL. + */ +static void* __lock_callback(guac_user* user, void* data) { + + /* Lock socket */ + guac_socket_instruction_begin(user->socket); + + return NULL; + +} + +/** + * Socket lock handler which acquires the socket locks of all connected users. + * Socket-level locks are acquired in preparation for the beginning of a new + * Guacamole instruction to ensure that parallel writes are only interleaved at + * instruction boundaries. + * + * @param socket + * The broadcast socket to lock. + */ +static void __guac_socket_broadcast_lock_handler(guac_socket* socket) { + + guac_socket_broadcast_data* data = + (guac_socket_broadcast_data*) socket->data; + + /* Acquire exclusive access to socket */ + pthread_mutex_lock(&(data->socket_lock)); + + /* Lock sockets of all users */ + guac_client_foreach_user(data->client, __lock_callback, NULL); + +} + +/** + * Callback which is invoked by guac_client_foreach_user() to unlock the given + * user's socket at the end of a Guacamole protocol instruction. + * + * @param user + * The user whose socket should be unlocked. + * + * @param data + * Arbitrary data passed to guac_client_foreach_user(). This is not needed + * by this callback, and should be left as NULL. + * + * @return + * Always NULL. + */ +static void* __unlock_callback(guac_user* user, void* data) { + + /* Unlock socket */ + guac_socket_instruction_end(user->socket); + + return NULL; + +} + +/** + * Socket unlock handler which releases the socket locks of all connected users. + * Socket-level locks are released after a Guacamole instruction has finished + * being written. + * + * @param socket + * The broadcast socket to unlock. + */ +static void __guac_socket_broadcast_unlock_handler(guac_socket* socket) { + + guac_socket_broadcast_data* data = + (guac_socket_broadcast_data*) socket->data; + + /* Unlock sockets of all users */ + guac_client_foreach_user(data->client, __unlock_callback, NULL); + + /* Relinquish exclusive access to socket */ + pthread_mutex_unlock(&(data->socket_lock)); + +} + +/** + * Callback which handles select operations on the broadcast socket, waiting + * for data to become available such that the next read operation will not + * block. This callback always fails, as the broadcast socket is write-only; it + * cannot be read. + * + * @param socket + * The broadcast socket to wait for. + * + * @param usec_timeout + * The maximum amount of time to wait for data, in microseconds, or -1 to + * potentially wait forever. + * + * @return + * A positive value on success, zero if the timeout elapsed and no data is + * available, or a negative value if an error occurs. This implementation + * always returns -1, as the broadcast socket is write-only and cannot be + * read. + */ +static int __guac_socket_broadcast_select_handler(guac_socket* socket, + int usec_timeout) { + + /* Selecting the broadcast socket is not possible */ + return -1; + +} + +/** + * Frees all implementation-specific data associated with the given socket, but + * not the socket object itself. + * + * @param socket + * The guac_socket whose associated data should be freed. + * + * @return + * Zero if the data was successfully freed, non-zero otherwise. This + * implementation always succeeds, and will always return zero. + */ +static int __guac_socket_broadcast_free_handler(guac_socket* socket) { + + guac_socket_broadcast_data* data = + (guac_socket_broadcast_data*) socket->data; + + /* Destroy locks */ + pthread_mutex_destroy(&(data->socket_lock)); + + free(data); + return 0; + +} + +guac_socket* guac_socket_broadcast(guac_client* client) { + + pthread_mutexattr_t lock_attributes; + + /* Allocate socket and associated data */ + guac_socket* socket = guac_socket_alloc(); + guac_socket_broadcast_data* data = + malloc(sizeof(guac_socket_broadcast_data)); + + /* Store client as socket data */ + data->client = client; + socket->data = data; + + pthread_mutexattr_init(&lock_attributes); + pthread_mutexattr_setpshared(&lock_attributes, PTHREAD_PROCESS_SHARED); + + /* Init lock */ + pthread_mutex_init(&(data->socket_lock), &lock_attributes); + + /* Set read/write handlers */ + socket->read_handler = __guac_socket_broadcast_read_handler; + socket->write_handler = __guac_socket_broadcast_write_handler; + socket->select_handler = __guac_socket_broadcast_select_handler; + socket->flush_handler = __guac_socket_broadcast_flush_handler; + socket->lock_handler = __guac_socket_broadcast_lock_handler; + socket->unlock_handler = __guac_socket_broadcast_unlock_handler; + socket->free_handler = __guac_socket_broadcast_free_handler; + + return socket; + +} +