1 #include "config.h"
2 #include <string.h>
3 #ifdef HAVE_SYS_MMAN_H
4 #include <sys/mman.h>
5 #endif
6 #include <sys/stat.h>
7 #include <fcntl.h>
8 #include <stdlib.h>
9 #include <stdio.h>
10 #include <locale.h>
11 #include <errno.h>
12 
13 #include <glib.h>
14 #include <gio/gio.h>
15 #ifdef G_OS_UNIX
16 #include <gio/gunixsocketaddress.h>
17 #include <gio/gunixfdmessage.h>
18 #endif
19 
20 #include "broadway-server.h"
21 
22 BroadwayServer *server;
23 GList *clients;
24 
25 static guint32 client_id_count = 1;
26 
27 /* Serials:
28  *
29  * Broadway tracks serials for all clients primarily to get the right behaviour wrt
30  * grabs. Each request the client sends gets an increasing per-client serial number, starting
31  * at 1. Thus, the client can now when a mouse event is seen whether the mouse event was
32  * sent before or after the server saw the grab request from the client (as this affects how
33  * the event is handled).
34  *
35  * There is only a single stream of increasing serials sent from the daemon to the web browser
36  * though, called "daemon serials", so we need to map back from the daemon serials to the client
37  * serials when we send an event to a client. So, each client keeps track of the mappings
38  * between its serials and daemon serials for any outstanding requests.
39  *
40  * There is some additional complexity in that there may be multiple consecutive web browser
41  * sessions, so we need to keep track of the last daemon serial used in between each web client
42  * connection so that the daemon serials can be strictly increasing.
43  */
44 
45 typedef struct {
46   guint32 client_serial;
47   guint32 daemon_serial;
48 } BroadwaySerialMapping;
49 
50 typedef struct  {
51   guint32 id;
52   GSocketConnection *connection;
53   GInputStream *in;
54   GString *buffer;
55   GSource *source;
56   GSList *serial_mappings;
57   GList *surfaces;
58   guint disconnect_idle;
59   GList *fds;
60   GHashTable *textures;
61 } BroadwayClient;
62 
63 static void
close_fd(void * data)64 close_fd (void *data)
65 {
66   close (GPOINTER_TO_INT (data));
67 }
68 
69 static void
client_free(BroadwayClient * client)70 client_free (BroadwayClient *client)
71 {
72   g_assert (client->surfaces == NULL);
73   g_assert (client->disconnect_idle == 0);
74   clients = g_list_remove (clients, client);
75   g_object_unref (client->connection);
76   g_object_unref (client->in);
77   g_string_free (client->buffer, TRUE);
78   g_slist_free_full (client->serial_mappings, g_free);
79   g_list_free_full (client->fds, close_fd);
80   g_hash_table_destroy (client->textures);
81   g_free (client);
82 }
83 
84 static void
client_disconnected(BroadwayClient * client)85 client_disconnected (BroadwayClient *client)
86 {
87   GHashTableIter iter;
88   gpointer key, value;
89   GList *l;
90 
91   if (client->disconnect_idle != 0)
92     {
93       g_source_remove (client->disconnect_idle);
94       client->disconnect_idle = 0;
95     }
96 
97   if (client->source != 0)
98     {
99       g_source_destroy (client->source);
100       client->source = 0;
101     }
102 
103   for (l = client->surfaces; l != NULL; l = l->next)
104     broadway_server_destroy_surface (server,
105                                      GPOINTER_TO_UINT (l->data));
106   g_list_free (client->surfaces);
107   client->surfaces = NULL;
108 
109   g_hash_table_iter_init (&iter, client->textures);
110   while (g_hash_table_iter_next (&iter, &key, &value))
111     broadway_server_release_texture (server, GPOINTER_TO_INT (value));
112 
113   broadway_server_flush (server);
114 
115   client_free (client);
116 }
117 
118 static gboolean
disconnect_idle_cb(BroadwayClient * client)119 disconnect_idle_cb (BroadwayClient *client)
120 {
121   client->disconnect_idle = 0;
122   client_disconnected (client);
123   return G_SOURCE_REMOVE;
124 }
125 
126 static void
client_disconnect_in_idle(BroadwayClient * client)127 client_disconnect_in_idle (BroadwayClient *client)
128 {
129   if (client->disconnect_idle == 0)
130     client->disconnect_idle =
131       g_idle_add_full (G_PRIORITY_DEFAULT, (GSourceFunc)disconnect_idle_cb, client, NULL);
132 }
133 
134 static void
send_reply(BroadwayClient * client,BroadwayRequest * request,BroadwayReply * reply,gsize size,guint32 type)135 send_reply (BroadwayClient *client,
136             BroadwayRequest *request,
137             BroadwayReply *reply,
138             gsize size,
139             guint32 type)
140 {
141   GOutputStream *output;
142 
143   reply->base.size = size;
144   reply->base.in_reply_to = request ? request->base.serial : 0;
145   reply->base.type = type;
146 
147   output = g_io_stream_get_output_stream (G_IO_STREAM (client->connection));
148   if (!g_output_stream_write_all (output, reply, size, NULL, NULL, NULL))
149     {
150       g_printerr ("can't write to client");
151       client_disconnect_in_idle (client);
152     }
153 }
154 
155 static void
add_client_serial_mapping(BroadwayClient * client,guint32 client_serial,guint32 daemon_serial)156 add_client_serial_mapping (BroadwayClient *client,
157                            guint32 client_serial,
158                            guint32 daemon_serial)
159 {
160   BroadwaySerialMapping *map;
161   GSList *last;
162 
163   last = g_slist_last (client->serial_mappings);
164 
165   if (last != NULL)
166     {
167       map = last->data;
168 
169       /* If we have no web client, don't grow forever */
170       if (map->daemon_serial == daemon_serial)
171         {
172           map->client_serial = client_serial;
173           return;
174         }
175     }
176 
177   map = g_new0 (BroadwaySerialMapping, 1);
178   map->client_serial = client_serial;
179   map->daemon_serial = daemon_serial;
180   client->serial_mappings = g_slist_append (client->serial_mappings, map);
181 }
182 
183 /* Returns the latest seen client serial at the time we sent
184    a daemon request to the browser with a specific daemon serial */
185 static guint32
get_client_serial(BroadwayClient * client,guint32 daemon_serial)186 get_client_serial (BroadwayClient *client, guint32 daemon_serial)
187 {
188   BroadwaySerialMapping *map;
189   GSList *l, *found;
190   guint32 client_serial = 0;
191 
192   found = NULL;
193   for (l = client->serial_mappings;  l != NULL; l = l->next)
194     {
195       map = l->data;
196 
197       if (map->daemon_serial <= daemon_serial)
198         {
199           found = l;
200           client_serial = map->client_serial;
201         }
202       else
203         break;
204     }
205 
206   /* Remove mappings before the found one, they will never more be used */
207   while (found != NULL &&
208          client->serial_mappings != found)
209     {
210       g_free (client->serial_mappings->data);
211       client->serial_mappings =
212         g_slist_delete_link (client->serial_mappings, client->serial_mappings);
213     }
214 
215   return client_serial;
216 }
217 
218 static void
client_handle_request(BroadwayClient * client,BroadwayRequest * request)219 client_handle_request (BroadwayClient *client,
220                        BroadwayRequest *request)
221 {
222   BroadwayReplyNewSurface reply_new_surface;
223   BroadwayReplySync reply_sync;
224   BroadwayReplyQueryMouse reply_query_mouse;
225   BroadwayReplyGrabPointer reply_grab_pointer;
226   BroadwayReplyUngrabPointer reply_ungrab_pointer;
227   guint32 before_serial, now_serial;
228   guint32 global_id;
229   int fd;
230 
231   before_serial = broadway_server_get_next_serial (server);
232 
233   switch (request->base.type)
234     {
235     case BROADWAY_REQUEST_NEW_SURFACE:
236       reply_new_surface.id =
237         broadway_server_new_surface (server, client->id,
238                                      request->new_surface.x,
239                                      request->new_surface.y,
240                                      request->new_surface.width,
241                                      request->new_surface.height);
242       client->surfaces =
243         g_list_prepend (client->surfaces,
244                         GUINT_TO_POINTER (reply_new_surface.id));
245 
246       send_reply (client, request, (BroadwayReply *)&reply_new_surface, sizeof (reply_new_surface),
247                   BROADWAY_REPLY_NEW_SURFACE);
248       break;
249     case BROADWAY_REQUEST_FLUSH:
250       broadway_server_flush (server);
251       break;
252     case BROADWAY_REQUEST_SYNC:
253       broadway_server_flush (server);
254       send_reply (client, request, (BroadwayReply *)&reply_sync, sizeof (reply_sync),
255                   BROADWAY_REPLY_SYNC);
256       break;
257     case BROADWAY_REQUEST_ROUNDTRIP:
258       broadway_server_roundtrip (server,
259                                  request->roundtrip.id,
260                                  request->roundtrip.tag);
261       break;
262     case BROADWAY_REQUEST_QUERY_MOUSE:
263       broadway_server_query_mouse (server,
264                                    &reply_query_mouse.surface,
265                                    &reply_query_mouse.root_x,
266                                    &reply_query_mouse.root_y,
267                                    &reply_query_mouse.mask);
268       send_reply (client, request, (BroadwayReply *)&reply_query_mouse, sizeof (reply_query_mouse),
269                   BROADWAY_REPLY_QUERY_MOUSE);
270       break;
271     case BROADWAY_REQUEST_DESTROY_SURFACE:
272       client->surfaces =
273         g_list_remove (client->surfaces,
274                        GUINT_TO_POINTER (request->destroy_surface.id));
275       broadway_server_destroy_surface (server, request->destroy_surface.id);
276       break;
277     case BROADWAY_REQUEST_SHOW_SURFACE:
278       broadway_server_surface_show (server, request->show_surface.id);
279       break;
280     case BROADWAY_REQUEST_HIDE_SURFACE:
281       broadway_server_surface_hide (server, request->hide_surface.id);
282       break;
283     case BROADWAY_REQUEST_SET_TRANSIENT_FOR:
284       broadway_server_surface_set_transient_for (server,
285                                                  request->set_transient_for.id,
286                                                  request->set_transient_for.parent);
287       break;
288     case BROADWAY_REQUEST_SET_NODES:
289       {
290         gsize array_size = request->base.size - sizeof (BroadwayRequestSetNodes) + sizeof(guint32);
291         int n_data = array_size / sizeof(guint32);
292 
293         broadway_server_surface_update_nodes (server, request->set_nodes.id,
294                                               request->set_nodes.data, n_data,
295                                               client->textures);
296       }
297       break;
298     case BROADWAY_REQUEST_UPLOAD_TEXTURE:
299       if (client->fds == NULL)
300         g_warning ("FD passing mismatch for texture upload %d", request->release_texture.id);
301       else
302         {
303           char *data, *p;
304           gsize to_read;
305           gssize num_read;
306           GBytes *texture;
307 
308           fd = GPOINTER_TO_INT (client->fds->data);
309           client->fds = g_list_delete_link (client->fds, client->fds);
310 
311           data = g_malloc (request->upload_texture.size);
312           to_read = request->upload_texture.size;
313           lseek (fd, request->upload_texture.offset, SEEK_SET);
314 
315           p = data;
316           do
317             {
318               num_read = read (fd, p, to_read);
319               if (num_read == -1 && errno == EAGAIN)
320                 continue;
321 
322               if (num_read > 0)
323                 {
324                   p += num_read;
325                   to_read -= num_read;
326                 }
327               else
328                 {
329                   g_warning ("Unexpected short read of texture");
330                   break;
331                 }
332             }
333           while (to_read > 0);
334           close (fd);
335 
336           texture = g_bytes_new_take (data, request->upload_texture.size);
337           global_id = broadway_server_upload_texture (server, texture);
338           g_bytes_unref (texture);
339 
340           g_hash_table_replace (client->textures,
341                                 GINT_TO_POINTER (request->release_texture.id),
342                                 GINT_TO_POINTER (global_id));
343         }
344       break;
345     case BROADWAY_REQUEST_RELEASE_TEXTURE:
346       global_id = GPOINTER_TO_INT (g_hash_table_lookup (client->textures,
347                                                         GINT_TO_POINTER (request->release_texture.id)));
348       if (global_id != 0)
349         broadway_server_release_texture (server, global_id);
350       g_hash_table_remove (client->textures,
351                            GINT_TO_POINTER (request->release_texture.id));
352 
353       break;
354     case BROADWAY_REQUEST_MOVE_RESIZE:
355       broadway_server_surface_move_resize (server,
356                                            request->move_resize.id,
357                                            request->move_resize.with_move,
358                                            request->move_resize.x,
359                                            request->move_resize.y,
360                                            request->move_resize.width,
361                                            request->move_resize.height);
362       break;
363     case BROADWAY_REQUEST_GRAB_POINTER:
364       reply_grab_pointer.status =
365         broadway_server_grab_pointer (server,
366                                       client->id,
367                                       request->grab_pointer.id,
368                                       request->grab_pointer.owner_events,
369                                       request->grab_pointer.event_mask,
370                                       request->grab_pointer.time_);
371       send_reply (client, request, (BroadwayReply *)&reply_grab_pointer, sizeof (reply_grab_pointer),
372                   BROADWAY_REPLY_GRAB_POINTER);
373       break;
374     case BROADWAY_REQUEST_UNGRAB_POINTER:
375       reply_ungrab_pointer.status =
376         broadway_server_ungrab_pointer (server,
377                                         request->ungrab_pointer.time_);
378       send_reply (client, request, (BroadwayReply *)&reply_ungrab_pointer, sizeof (reply_ungrab_pointer),
379                   BROADWAY_REPLY_UNGRAB_POINTER);
380       break;
381     case BROADWAY_REQUEST_FOCUS_SURFACE:
382       broadway_server_focus_surface (server, request->focus_surface.id);
383       break;
384     case BROADWAY_REQUEST_SET_SHOW_KEYBOARD:
385       broadway_server_set_show_keyboard (server, request->set_show_keyboard.show_keyboard);
386       break;
387     default:
388       g_warning ("Unknown request of type %d", request->base.type);
389     }
390 
391 
392   now_serial = broadway_server_get_next_serial (server);
393 
394   /* If we sent a new output request, map that this client serial to that, otherwise
395      update old mapping for previously sent daemon serial */
396   if (now_serial != before_serial)
397     add_client_serial_mapping (client,
398                                request->base.serial,
399                                before_serial);
400   else
401     add_client_serial_mapping (client,
402                                request->base.serial,
403                                before_serial - 1);
404 }
405 
406 #define INPUT_BUFFER_SIZE 8192
407 
408 static gboolean
client_input_cb(GPollableInputStream * stream,gpointer user_data)409 client_input_cb (GPollableInputStream *stream,
410                  gpointer              user_data)
411 {
412   BroadwayClient *client = user_data;
413   GSocket *socket = g_socket_connection_get_socket (client->connection);
414   gssize res;
415   gsize old_len;
416   guchar *buffer;
417   gsize buffer_len;
418   GInputVector input_vector;
419   GSocketControlMessage **messages = NULL;
420   int i, num_messages;
421 
422   old_len = client->buffer->len;
423 
424   /* Ensure we have at least INPUT_BUFFER_SIZE extra space */
425   g_string_set_size (client->buffer, old_len + INPUT_BUFFER_SIZE);
426   g_string_set_size (client->buffer, old_len);
427 
428   input_vector.buffer = client->buffer->str + old_len;
429   input_vector.size = client->buffer->allocated_len - client->buffer->len -1;
430 
431   res = g_socket_receive_message (socket, NULL,
432                                   &input_vector, 1,
433                                   &messages, &num_messages,
434                                   NULL, NULL, NULL);
435   if (res <= 0)
436     {
437       client->source = NULL;
438       client_disconnected (client);
439       return G_SOURCE_REMOVE;
440     }
441 
442   for (i = 0; i < num_messages; i++)
443     {
444       if (G_IS_UNIX_FD_MESSAGE (messages[i]))
445         {
446           int j, n_fds;
447           int *fds = g_unix_fd_message_steal_fds (G_UNIX_FD_MESSAGE (messages[i]), &n_fds);
448           for (j = 0; j < n_fds; j++)
449             {
450               int fd = fds[i];
451               client->fds = g_list_append (client->fds, GINT_TO_POINTER (fd));
452             }
453           g_free (fds);
454         }
455       g_object_unref (messages[i]);
456     }
457   g_free (messages);
458 
459   g_string_set_size (client->buffer, old_len + res);
460 
461   buffer = (guchar *)client->buffer->str;
462   buffer_len = client->buffer->len;
463 
464   while (buffer_len >= sizeof (guint32))
465     {
466       guint32 size;
467 
468       memcpy (&size, buffer, sizeof (guint32));
469       if (size <= buffer_len)
470         {
471           client_handle_request (client, (BroadwayRequest *)buffer);
472 
473           buffer_len -= size;
474           buffer += size;
475         }
476       else
477         break;
478     }
479 
480   g_string_erase (client->buffer, 0, client->buffer->len - buffer_len);
481 
482   return G_SOURCE_CONTINUE;
483 }
484 
485 static gboolean
incoming_client(GSocketService * service,GSocketConnection * connection,GObject * source_object)486 incoming_client (GSocketService    *service,
487                  GSocketConnection *connection,
488                  GObject           *source_object)
489 {
490   BroadwayClient *client;
491   GInputStream *input;
492   BroadwayInputMsg ev = { {0} };
493 
494   client = g_new0 (BroadwayClient, 1);
495   client->id = client_id_count++;
496   client->connection = g_object_ref (connection);
497   client->textures = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, NULL);
498 
499   input = g_io_stream_get_input_stream (G_IO_STREAM (client->connection));
500   client->in = input;
501   client->buffer = g_string_sized_new (INPUT_BUFFER_SIZE);
502   client->source = g_pollable_input_stream_create_source (G_POLLABLE_INPUT_STREAM (input), NULL);
503 
504   g_source_set_callback (client->source, (GSourceFunc) client_input_cb, client, NULL);
505   g_source_attach (client->source, NULL);
506 
507   clients = g_list_prepend (clients, client);
508 
509   /* Send initial resize notify */
510   ev.base.type = BROADWAY_EVENT_SCREEN_SIZE_CHANGED;
511   ev.base.serial = broadway_server_get_next_serial (server) - 1;
512   ev.base.time = broadway_server_get_last_seen_time (server);
513   broadway_server_get_screen_size (server,
514                                    &ev.screen_resize_notify.width,
515                                    &ev.screen_resize_notify.height,
516                                    &ev.screen_resize_notify.scale);
517 
518   broadway_events_got_input (&ev,
519                              client->id);
520 
521   return TRUE;
522 }
523 
524 
525 int
main(int argc,char * argv[])526 main (int argc, char *argv[])
527 {
528   GError *error = NULL;
529   GOptionContext *context;
530   GMainLoop *loop;
531   GSocketAddress *address;
532   GSocketService *listener;
533   char *http_address = NULL;
534   char *unixsocket_address = NULL;
535   int http_port = 0;
536   char *ssl_cert = NULL;
537   char *ssl_key = NULL;
538   const char *display;
539   int port = 0;
540   const GOptionEntry entries[] = {
541     { "port", 'p', 0, G_OPTION_ARG_INT, &http_port, "Httpd port", "PORT" },
542     { "address", 'a', 0, G_OPTION_ARG_STRING, &http_address, "Ip address to bind to ", "ADDRESS" },
543 #ifdef G_OS_UNIX
544     { "unixsocket", 'u', 0, G_OPTION_ARG_STRING, &unixsocket_address, "Unix domain socket address", "ADDRESS" },
545 #endif
546     { "cert", 'c', 0, G_OPTION_ARG_STRING, &ssl_cert, "SSL certificate path", "PATH" },
547     { "key", 'k', 0, G_OPTION_ARG_STRING, &ssl_key, "SSL key path", "PATH" },
548     { NULL }
549   };
550 
551   setlocale (LC_ALL, "");
552 
553   context = g_option_context_new ("[:DISPLAY] - broadway display daemon");
554   g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE);
555   if (!g_option_context_parse (context, &argc, &argv, &error))
556     {
557       g_printerr ("option parsing failed: %s\n", error->message);
558       exit (1);
559     }
560 
561   display = NULL;
562   if (argc > 1)
563     {
564       if (*argv[1] != ':')
565         {
566           g_printerr ("Usage gtk4-broadwayd [:DISPLAY]\n");
567           exit (1);
568         }
569       display = argv[1];
570     }
571 
572   if (display == NULL)
573     display = ":0";
574 
575   if (display[0] == ':' && g_ascii_isdigit(display[1]))
576     {
577       char *path, *basename;
578 
579       port = strtol (display + strlen (":"), NULL, 10);
580       basename = g_strdup_printf ("broadway%d.socket", port + 1);
581       path = g_build_filename (g_get_user_runtime_dir (), basename, NULL);
582       g_free (basename);
583 
584       unlink (path);
585 
586       g_print ("Listening on %s\n", path);
587       address = g_unix_socket_address_new_with_type (path, -1,
588                                                      G_UNIX_SOCKET_ADDRESS_PATH);
589       g_free (path);
590     }
591   else
592     {
593       g_printerr ("Failed to parse display %s\n", display);
594       exit (1);
595     }
596 
597   if (http_port == 0)
598     http_port = 8080 + port;
599 
600   if (unixsocket_address != NULL)
601     server = broadway_server_on_unix_socket_new (unixsocket_address, &error);
602   else
603     server = broadway_server_new (http_address,
604                                   http_port,
605                                   ssl_cert,
606                                   ssl_key,
607                                   &error);
608 
609   if (server == NULL)
610     {
611       g_printerr ("%s\n", error->message);
612       return 1;
613     }
614 
615   listener = g_socket_service_new ();
616   if (!g_socket_listener_add_address (G_SOCKET_LISTENER (listener),
617                                       address,
618                                       G_SOCKET_TYPE_STREAM,
619                                       G_SOCKET_PROTOCOL_DEFAULT,
620                                       G_OBJECT (server),
621                                       NULL,
622                                       &error))
623     {
624       g_printerr ("Can't listen: %s\n", error->message);
625       return 1;
626     }
627   g_object_unref (address);
628   g_signal_connect (listener, "incoming", G_CALLBACK (incoming_client), NULL);
629 
630   g_socket_service_start (G_SOCKET_SERVICE (listener));
631 
632   loop = g_main_loop_new (NULL, FALSE);
633   g_main_loop_run (loop);
634 
635   return 0;
636 }
637 
638 static gsize
get_event_size(int type)639 get_event_size (int type)
640 {
641   switch (type)
642     {
643     case BROADWAY_EVENT_ENTER:
644     case BROADWAY_EVENT_LEAVE:
645       return sizeof (BroadwayInputCrossingMsg);
646     case BROADWAY_EVENT_POINTER_MOVE:
647       return sizeof (BroadwayInputPointerMsg);
648     case BROADWAY_EVENT_BUTTON_PRESS:
649     case BROADWAY_EVENT_BUTTON_RELEASE:
650       return sizeof (BroadwayInputButtonMsg);
651     case BROADWAY_EVENT_SCROLL:
652       return sizeof (BroadwayInputScrollMsg);
653     case BROADWAY_EVENT_TOUCH:
654       return sizeof (BroadwayInputTouchMsg);
655     case BROADWAY_EVENT_KEY_PRESS:
656     case BROADWAY_EVENT_KEY_RELEASE:
657       return  sizeof (BroadwayInputKeyMsg);
658     case BROADWAY_EVENT_GRAB_NOTIFY:
659     case BROADWAY_EVENT_UNGRAB_NOTIFY:
660       return sizeof (BroadwayInputGrabReply);
661     case BROADWAY_EVENT_CONFIGURE_NOTIFY:
662       return  sizeof (BroadwayInputConfigureNotify);
663     case BROADWAY_EVENT_ROUNDTRIP_NOTIFY:
664       return  sizeof (BroadwayInputRoundtripNotify);
665     case BROADWAY_EVENT_SCREEN_SIZE_CHANGED:
666       return sizeof (BroadwayInputScreenResizeNotify);
667     case BROADWAY_EVENT_FOCUS:
668       return sizeof (BroadwayInputFocusMsg);
669     default:
670       g_assert_not_reached ();
671     }
672   return 0;
673 }
674 
675 void
broadway_events_got_input(BroadwayInputMsg * message,gint32 client_id)676 broadway_events_got_input (BroadwayInputMsg *message,
677                            gint32 client_id)
678 {
679   GList *l;
680   BroadwayReplyEvent reply_event;
681   gsize size;
682   guint32 daemon_serial;
683 
684   size = get_event_size (message->base.type);
685   g_assert (sizeof (BroadwayReplyBase) + size <= sizeof (BroadwayReplyEvent));
686 
687   memset (&reply_event, 0, sizeof (BroadwayReplyEvent));
688   daemon_serial = message->base.serial;
689 
690   memcpy (&reply_event.msg, message, size);
691 
692   for (l = clients; l != NULL; l = l->next)
693     {
694       BroadwayClient *client = l->data;
695 
696       if (client_id == -1 ||
697           client->id == client_id)
698         {
699           reply_event.msg.base.serial = get_client_serial (client, daemon_serial);
700 
701           send_reply (client, NULL, (BroadwayReply *)&reply_event,
702                       G_STRUCT_OFFSET (BroadwayReplyEvent, msg) + size,
703                       BROADWAY_REPLY_EVENT);
704         }
705     }
706 }
707