#include "config.h" #include #ifdef HAVE_SYS_MMAN_H #include #endif #include #include #include #include #include #include #include #include #ifdef G_OS_UNIX #include #include #endif #include "broadway-server.h" BroadwayServer *server; GList *clients; static guint32 client_id_count = 1; /* Serials: * * Broadway tracks serials for all clients primarily to get the right behaviour wrt * grabs. Each request the client sends gets an increasing per-client serial number, starting * at 1. Thus, the client can now when a mouse event is seen whether the mouse event was * sent before or after the server saw the grab request from the client (as this affects how * the event is handled). * * There is only a single stream of increasing serials sent from the daemon to the web browser * though, called "daemon serials", so we need to map back from the daemon serials to the client * serials when we send an event to a client. So, each client keeps track of the mappings * between its serials and daemon serials for any outstanding requests. * * There is some additional complexity in that there may be multiple consecutive web browser * sessions, so we need to keep track of the last daemon serial used inbetween each web client * connection so that the daemon serials can be strictly increasing. */ typedef struct { guint32 client_serial; guint32 daemon_serial; } BroadwaySerialMapping; typedef struct { guint32 id; GSocketConnection *connection; GInputStream *in; GString *buffer; GSource *source; GSList *serial_mappings; GList *windows; guint disconnect_idle; GList *fds; GHashTable *textures; } BroadwayClient; static void close_fd (void *data) { close (GPOINTER_TO_INT (data)); } static void client_free (BroadwayClient *client) { g_assert (client->windows == NULL); g_assert (client->disconnect_idle == 0); clients = g_list_remove (clients, client); g_object_unref (client->connection); g_object_unref (client->in); g_string_free (client->buffer, TRUE); g_slist_free_full (client->serial_mappings, g_free); g_list_free_full (client->fds, close_fd); g_hash_table_destroy (client->textures); g_free (client); } static void client_disconnected (BroadwayClient *client) { GHashTableIter iter; gpointer key, value; GList *l; if (client->disconnect_idle != 0) { g_source_remove (client->disconnect_idle); client->disconnect_idle = 0; } if (client->source != 0) { g_source_destroy (client->source); client->source = 0; } for (l = client->windows; l != NULL; l = l->next) broadway_server_destroy_window (server, GPOINTER_TO_UINT (l->data)); g_list_free (client->windows); client->windows = NULL; g_hash_table_iter_init (&iter, client->textures); while (g_hash_table_iter_next (&iter, &key, &value)) broadway_server_release_texture (server, GPOINTER_TO_INT (value)); broadway_server_flush (server); client_free (client); } static gboolean disconnect_idle_cb (BroadwayClient *client) { client->disconnect_idle = 0; client_disconnected (client); return G_SOURCE_REMOVE; } static void client_disconnect_in_idle (BroadwayClient *client) { if (client->disconnect_idle == 0) client->disconnect_idle = g_idle_add_full (G_PRIORITY_DEFAULT, (GSourceFunc)disconnect_idle_cb, client, NULL); } static void send_reply (BroadwayClient *client, BroadwayRequest *request, BroadwayReply *reply, gsize size, guint32 type) { GOutputStream *output; reply->base.size = size; reply->base.in_reply_to = request ? request->base.serial : 0; reply->base.type = type; output = g_io_stream_get_output_stream (G_IO_STREAM (client->connection)); if (!g_output_stream_write_all (output, reply, size, NULL, NULL, NULL)) { g_printerr ("can't write to client"); client_disconnect_in_idle (client); } } static void add_client_serial_mapping (BroadwayClient *client, guint32 client_serial, guint32 daemon_serial) { BroadwaySerialMapping *map; GSList *last; last = g_slist_last (client->serial_mappings); if (last != NULL) { map = last->data; /* If we have no web client, don't grow forever */ if (map->daemon_serial == daemon_serial) { map->client_serial = client_serial; return; } } map = g_new0 (BroadwaySerialMapping, 1); map->client_serial = client_serial; map->daemon_serial = daemon_serial; client->serial_mappings = g_slist_append (client->serial_mappings, map); } /* Returns the latest seen client serial at the time we sent a daemon request to the browser with a specific daemon serial */ static guint32 get_client_serial (BroadwayClient *client, guint32 daemon_serial) { BroadwaySerialMapping *map; GSList *l, *found; guint32 client_serial = 0; found = NULL; for (l = client->serial_mappings; l != NULL; l = l->next) { map = l->data; if (map->daemon_serial <= daemon_serial) { found = l; client_serial = map->client_serial; } else break; } /* Remove mappings before the found one, they will never more be used */ while (found != NULL && client->serial_mappings != found) { g_free (client->serial_mappings->data); client->serial_mappings = g_slist_delete_link (client->serial_mappings, client->serial_mappings); } return client_serial; } #define NODE_SIZE_COLOR 1 #define NODE_SIZE_FLOAT 1 #define NODE_SIZE_POINT 2 #define NODE_SIZE_SIZE 2 #define NODE_SIZE_RECT (NODE_SIZE_POINT + NODE_SIZE_SIZE) #define NODE_SIZE_RRECT (NODE_SIZE_RECT + 4 * NODE_SIZE_SIZE) #define NODE_SIZE_COLOR_STOP (NODE_SIZE_FLOAT + NODE_SIZE_COLOR) #define NODE_SIZE_SHADOW (NODE_SIZE_COLOR + 3 * NODE_SIZE_FLOAT) static guint32 rotl (guint32 value, int shift) { if ((shift &= 32 - 1) == 0) return value; return (value << shift) | (value >> (32 - shift)); } static BroadwayNode * decode_nodes (BroadwayClient *client, int len, guint32 data[], int *pos) { BroadwayNode *node; guint32 type; guint32 i, n_stops, n_shadows; guint32 size, n_children; gint32 texture_offset; guint32 hash; g_assert (*pos < len); size = 0; n_children = 0; texture_offset = -1; type = data[(*pos)++]; switch (type) { case BROADWAY_NODE_COLOR: size = NODE_SIZE_RECT + NODE_SIZE_COLOR; break; case BROADWAY_NODE_BORDER: size = NODE_SIZE_RRECT + 4 * NODE_SIZE_FLOAT + 4 * NODE_SIZE_COLOR; break; case BROADWAY_NODE_INSET_SHADOW: case BROADWAY_NODE_OUTSET_SHADOW: size = NODE_SIZE_RRECT + NODE_SIZE_COLOR + 4 * NODE_SIZE_FLOAT; break; case BROADWAY_NODE_TEXTURE: texture_offset = 4; size = 5; break; case BROADWAY_NODE_CONTAINER: size = 1; n_children = data[*pos]; break; case BROADWAY_NODE_ROUNDED_CLIP: size = NODE_SIZE_RRECT; n_children = 1; break; case BROADWAY_NODE_CLIP: size = NODE_SIZE_RECT; n_children = 1; break; case BROADWAY_NODE_LINEAR_GRADIENT: size = NODE_SIZE_RECT + 2 * NODE_SIZE_POINT; n_stops = data[*pos + size++]; size += n_stops * NODE_SIZE_COLOR_STOP; break; case BROADWAY_NODE_SHADOW: size = 1; n_shadows = data[*pos]; size += n_shadows * NODE_SIZE_SHADOW; n_children = 1; break; case BROADWAY_NODE_OPACITY: size = NODE_SIZE_FLOAT; n_children = 1; break; default: g_assert_not_reached (); } node = g_malloc (sizeof(BroadwayNode) + (size - 1) * sizeof(guint32) + n_children * sizeof (BroadwayNode *)); node->type = type; node->n_children = n_children; node->children = (BroadwayNode **)((char *)node + sizeof(BroadwayNode) + (size - 1) * sizeof(guint32)); node->n_data = size; for (i = 0; i < size; i++) { node->data[i] = data[(*pos)++]; if (i == texture_offset) node->data[i] = GPOINTER_TO_INT (g_hash_table_lookup (client->textures, GINT_TO_POINTER (node->data[i]))); } for (i = 0; i < n_children; i++) node->children[i] = decode_nodes (client, len, data, pos); hash = node->type << 16; for (i = 0; i < size; i++) hash ^= rotl (node->data[i], i); for (i = 0; i < n_children; i++) hash ^= rotl (node->children[i]->hash, i); node->hash = hash; return node; } static void client_handle_request (BroadwayClient *client, BroadwayRequest *request) { BroadwayReplyNewWindow reply_new_window; BroadwayReplySync reply_sync; BroadwayReplyQueryMouse reply_query_mouse; BroadwayReplyGrabPointer reply_grab_pointer; BroadwayReplyUngrabPointer reply_ungrab_pointer; guint32 before_serial, now_serial; guint32 global_id; int fd; before_serial = broadway_server_get_next_serial (server); switch (request->base.type) { case BROADWAY_REQUEST_NEW_WINDOW: reply_new_window.id = broadway_server_new_window (server, request->new_window.x, request->new_window.y, request->new_window.width, request->new_window.height, request->new_window.is_temp); client->windows = g_list_prepend (client->windows, GUINT_TO_POINTER (reply_new_window.id)); send_reply (client, request, (BroadwayReply *)&reply_new_window, sizeof (reply_new_window), BROADWAY_REPLY_NEW_WINDOW); break; case BROADWAY_REQUEST_FLUSH: broadway_server_flush (server); break; case BROADWAY_REQUEST_SYNC: broadway_server_flush (server); send_reply (client, request, (BroadwayReply *)&reply_sync, sizeof (reply_sync), BROADWAY_REPLY_SYNC); break; case BROADWAY_REQUEST_ROUNDTRIP: broadway_server_roundtrip (server, request->roundtrip.id, request->roundtrip.tag); break; case BROADWAY_REQUEST_QUERY_MOUSE: broadway_server_query_mouse (server, &reply_query_mouse.toplevel, &reply_query_mouse.root_x, &reply_query_mouse.root_y, &reply_query_mouse.mask); send_reply (client, request, (BroadwayReply *)&reply_query_mouse, sizeof (reply_query_mouse), BROADWAY_REPLY_QUERY_MOUSE); break; case BROADWAY_REQUEST_DESTROY_WINDOW: client->windows = g_list_remove (client->windows, GUINT_TO_POINTER (request->destroy_window.id)); broadway_server_destroy_window (server, request->destroy_window.id); break; case BROADWAY_REQUEST_SHOW_WINDOW: broadway_server_window_show (server, request->show_window.id); break; case BROADWAY_REQUEST_HIDE_WINDOW: broadway_server_window_hide (server, request->hide_window.id); break; case BROADWAY_REQUEST_SET_TRANSIENT_FOR: broadway_server_window_set_transient_for (server, request->set_transient_for.id, request->set_transient_for.parent); break; case BROADWAY_REQUEST_SET_NODES: { gsize array_size = request->base.size - sizeof (BroadwayRequestSetNodes) + sizeof(guint32); int n_data = array_size / sizeof(guint32); int pos = 0; BroadwayNode *node; node = decode_nodes (client, n_data, request->set_nodes.data, &pos); broadway_server_window_set_nodes (server, request->set_nodes.id, node); } break; case BROADWAY_REQUEST_UPLOAD_TEXTURE: if (client->fds == NULL) g_warning ("FD passing mismatch for texture upload %d", request->release_texture.id); else { char *data, *p; gsize to_read; gssize num_read; GBytes *texture; fd = GPOINTER_TO_INT (client->fds->data); client->fds = g_list_delete_link (client->fds, client->fds); data = g_malloc (request->upload_texture.size); to_read = request->upload_texture.size; lseek (fd, request->upload_texture.offset, SEEK_SET); p = data; do { num_read = read (fd, p, to_read); if (num_read == -1 && errno == EAGAIN) continue; if (num_read > 0) { p += num_read; to_read -= num_read; } else { g_warning ("Unexpected short read of texture"); break; } } while (to_read > 0); close (fd); texture = g_bytes_new_take (data, request->upload_texture.size); global_id = broadway_server_upload_texture (server, texture); g_bytes_unref (texture); g_hash_table_replace (client->textures, GINT_TO_POINTER (request->release_texture.id), GINT_TO_POINTER (global_id)); } break; case BROADWAY_REQUEST_RELEASE_TEXTURE: global_id = GPOINTER_TO_INT (g_hash_table_lookup (client->textures, GINT_TO_POINTER (request->release_texture.id))); if (global_id != 0) broadway_server_release_texture (server, global_id); g_hash_table_remove (client->textures, GINT_TO_POINTER (request->release_texture.id)); break; case BROADWAY_REQUEST_MOVE_RESIZE: broadway_server_window_move_resize (server, request->move_resize.id, request->move_resize.with_move, request->move_resize.x, request->move_resize.y, request->move_resize.width, request->move_resize.height); break; case BROADWAY_REQUEST_GRAB_POINTER: reply_grab_pointer.status = broadway_server_grab_pointer (server, client->id, request->grab_pointer.id, request->grab_pointer.owner_events, request->grab_pointer.event_mask, request->grab_pointer.time_); send_reply (client, request, (BroadwayReply *)&reply_grab_pointer, sizeof (reply_grab_pointer), BROADWAY_REPLY_GRAB_POINTER); break; case BROADWAY_REQUEST_UNGRAB_POINTER: reply_ungrab_pointer.status = broadway_server_ungrab_pointer (server, request->ungrab_pointer.time_); send_reply (client, request, (BroadwayReply *)&reply_ungrab_pointer, sizeof (reply_ungrab_pointer), BROADWAY_REPLY_UNGRAB_POINTER); break; case BROADWAY_REQUEST_FOCUS_WINDOW: broadway_server_focus_window (server, request->focus_window.id); break; case BROADWAY_REQUEST_SET_SHOW_KEYBOARD: broadway_server_set_show_keyboard (server, request->set_show_keyboard.show_keyboard); break; default: g_warning ("Unknown request of type %d", request->base.type); } now_serial = broadway_server_get_next_serial (server); /* If we sent a new output request, map that this client serial to that, otherwise update old mapping for previously sent daemon serial */ if (now_serial != before_serial) add_client_serial_mapping (client, request->base.serial, before_serial); else add_client_serial_mapping (client, request->base.serial, before_serial - 1); } #define INPUT_BUFFER_SIZE 8192 static gboolean client_input_cb (GPollableInputStream *stream, gpointer user_data) { BroadwayClient *client = user_data; GSocket *socket = g_socket_connection_get_socket (client->connection); gssize res; gsize old_len; guchar *buffer; gsize buffer_len; GInputVector input_vector; GSocketControlMessage **messages = NULL; int i, num_messages; old_len = client->buffer->len; /* Ensure we have at least INPUT_BUFFER_SIZE extra space */ g_string_set_size (client->buffer, old_len + INPUT_BUFFER_SIZE); g_string_set_size (client->buffer, old_len); input_vector.buffer = client->buffer->str + old_len; input_vector.size = client->buffer->allocated_len - client->buffer->len -1; res = g_socket_receive_message (socket, NULL, &input_vector, 1, &messages, &num_messages, NULL, NULL, NULL); if (res <= 0) { client->source = NULL; client_disconnected (client); return G_SOURCE_REMOVE; } for (i = 0; i < num_messages; i++) { if (G_IS_UNIX_FD_MESSAGE (messages[i])) { int j, n_fds; int *fds = g_unix_fd_message_steal_fds (G_UNIX_FD_MESSAGE (messages[i]), &n_fds); for (j = 0; j < n_fds; j++) { int fd = fds[i]; client->fds = g_list_append (client->fds, GINT_TO_POINTER (fd)); } g_free (fds); } g_object_unref (messages[i]); } g_free (messages); g_string_set_size (client->buffer, old_len + res); buffer = (guchar *)client->buffer->str; buffer_len = client->buffer->len; while (buffer_len >= sizeof (guint32)) { guint32 size; memcpy (&size, buffer, sizeof (guint32)); if (size <= buffer_len) { client_handle_request (client, (BroadwayRequest *)buffer); buffer_len -= size; buffer += size; } else break; } g_string_erase (client->buffer, 0, client->buffer->len - buffer_len); return G_SOURCE_CONTINUE; } static gboolean incoming_client (GSocketService *service, GSocketConnection *connection, GObject *source_object) { BroadwayClient *client; GInputStream *input; BroadwayInputMsg ev = { {0} }; client = g_new0 (BroadwayClient, 1); client->id = client_id_count++; client->connection = g_object_ref (connection); client->textures = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, NULL); input = g_io_stream_get_input_stream (G_IO_STREAM (client->connection)); client->in = input; client->buffer = g_string_sized_new (INPUT_BUFFER_SIZE); client->source = g_pollable_input_stream_create_source (G_POLLABLE_INPUT_STREAM (input), NULL); g_source_set_callback (client->source, (GSourceFunc) client_input_cb, client, NULL); g_source_attach (client->source, NULL); clients = g_list_prepend (clients, client); /* Send initial resize notify */ ev.base.type = BROADWAY_EVENT_SCREEN_SIZE_CHANGED; ev.base.serial = broadway_server_get_next_serial (server) - 1; ev.base.time = broadway_server_get_last_seen_time (server); broadway_server_get_screen_size (server, &ev.screen_resize_notify.width, &ev.screen_resize_notify.height); broadway_events_got_input (&ev, client->id); return TRUE; } int main (int argc, char *argv[]) { GError *error = NULL; GOptionContext *context; GMainLoop *loop; GSocketAddress *address; GSocketService *listener; char *http_address = NULL; char *unixsocket_address = NULL; int http_port = 0; char *ssl_cert = NULL; char *ssl_key = NULL; const char *display; int port = 0; const GOptionEntry entries[] = { { "port", 'p', 0, G_OPTION_ARG_INT, &http_port, "Httpd port", "PORT" }, { "address", 'a', 0, G_OPTION_ARG_STRING, &http_address, "Ip address to bind to ", "ADDRESS" }, #ifdef G_OS_UNIX { "unixsocket", 'u', 0, G_OPTION_ARG_STRING, &unixsocket_address, "Unix domain socket address", "ADDRESS" }, #endif { "cert", 'c', 0, G_OPTION_ARG_STRING, &ssl_cert, "SSL certificate path", "PATH" }, { "key", 'k', 0, G_OPTION_ARG_STRING, &ssl_key, "SSL key path", "PATH" }, { NULL } }; setlocale (LC_ALL, ""); context = g_option_context_new ("[:DISPLAY] - broadway display daemon"); g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); if (!g_option_context_parse (context, &argc, &argv, &error)) { g_printerr ("option parsing failed: %s\n", error->message); exit (1); } display = NULL; if (argc > 1) { if (*argv[1] != ':') { g_printerr ("Usage gtk4-broadwayd [:DISPLAY]\n"); exit (1); } display = argv[1]; } if (display == NULL) display = ":0"; if (display[0] == ':' && g_ascii_isdigit(display[1])) { char *path, *basename; port = strtol (display + strlen (":"), NULL, 10); basename = g_strdup_printf ("broadway%d.socket", port + 1); path = g_build_filename (g_get_user_runtime_dir (), basename, NULL); g_free (basename); unlink (path); g_print ("Listening on %s\n", path); address = g_unix_socket_address_new_with_type (path, -1, G_UNIX_SOCKET_ADDRESS_PATH); g_free (path); } else { g_printerr ("Failed to parse display %s\n", display); exit (1); } if (http_port == 0) http_port = 8080 + port; if (unixsocket_address != NULL) server = broadway_server_on_unix_socket_new (unixsocket_address, &error); else server = broadway_server_new (http_address, http_port, ssl_cert, ssl_key, &error); if (server == NULL) { g_printerr ("%s\n", error->message); return 1; } listener = g_socket_service_new (); if (!g_socket_listener_add_address (G_SOCKET_LISTENER (listener), address, G_SOCKET_TYPE_STREAM, G_SOCKET_PROTOCOL_DEFAULT, G_OBJECT (server), NULL, &error)) { g_printerr ("Can't listen: %s\n", error->message); return 1; } g_object_unref (address); g_signal_connect (listener, "incoming", G_CALLBACK (incoming_client), NULL); g_socket_service_start (G_SOCKET_SERVICE (listener)); loop = g_main_loop_new (NULL, FALSE); g_main_loop_run (loop); return 0; } static gsize get_event_size (int type) { switch (type) { case BROADWAY_EVENT_ENTER: case BROADWAY_EVENT_LEAVE: return sizeof (BroadwayInputCrossingMsg); case BROADWAY_EVENT_POINTER_MOVE: return sizeof (BroadwayInputPointerMsg); case BROADWAY_EVENT_BUTTON_PRESS: case BROADWAY_EVENT_BUTTON_RELEASE: return sizeof (BroadwayInputButtonMsg); case BROADWAY_EVENT_SCROLL: return sizeof (BroadwayInputScrollMsg); case BROADWAY_EVENT_TOUCH: return sizeof (BroadwayInputTouchMsg); case BROADWAY_EVENT_KEY_PRESS: case BROADWAY_EVENT_KEY_RELEASE: return sizeof (BroadwayInputKeyMsg); case BROADWAY_EVENT_GRAB_NOTIFY: case BROADWAY_EVENT_UNGRAB_NOTIFY: return sizeof (BroadwayInputGrabReply); case BROADWAY_EVENT_CONFIGURE_NOTIFY: return sizeof (BroadwayInputConfigureNotify); case BROADWAY_EVENT_ROUNDTRIP_NOTIFY: return sizeof (BroadwayInputRoundtripNotify); case BROADWAY_EVENT_DELETE_NOTIFY: return sizeof (BroadwayInputDeleteNotify); case BROADWAY_EVENT_SCREEN_SIZE_CHANGED: return sizeof (BroadwayInputScreenResizeNotify); case BROADWAY_EVENT_FOCUS: return sizeof (BroadwayInputFocusMsg); default: g_assert_not_reached (); } return 0; } void broadway_events_got_input (BroadwayInputMsg *message, gint32 client_id) { GList *l; BroadwayReplyEvent reply_event; gsize size; guint32 daemon_serial; size = get_event_size (message->base.type); g_assert (sizeof (BroadwayReplyBase) + size <= sizeof (BroadwayReplyEvent)); memset (&reply_event, 0, sizeof (BroadwayReplyEvent)); daemon_serial = message->base.serial; memcpy (&reply_event.msg, message, size); for (l = clients; l != NULL; l = l->next) { BroadwayClient *client = l->data; if (client_id == -1 || client->id == client_id) { reply_event.msg.base.serial = get_client_serial (client, daemon_serial); send_reply (client, NULL, (BroadwayReply *)&reply_event, G_STRUCT_OFFSET (BroadwayReplyEvent, msg) + size, BROADWAY_REPLY_EVENT); } } }