comparison xwd-application.c @ 19:f0accfc74f7b

Port to GLib version 2.48 or later Use GApplication instead of the custom XWritedUnique object for uniqueness. Use GNotification from GIO instead of libnotify. Remove help and debug options from documentation. Replace intltool with GNU gettext (version 0.19 or later required).
author Guido Berhoerster <guido+xwrited@berhoerster.name>
date Sat, 28 Jul 2018 22:02:24 +0200
parents
children 683ebd334b21
comparison
equal deleted inserted replaced
18:4a5330979433 19:f0accfc74f7b
1 /*
2 * Copyright (C) 2018 Guido Berhoerster <guido+xwrited@berhoerster.name>
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining
5 * a copy of this software and associated documentation files (the
6 * "Software"), to deal in the Software without restriction, including
7 * without limitation the rights to use, copy, modify, merge, publish,
8 * distribute, sublicense, and/or sell copies of the Software, and to
9 * permit persons to whom the Software is furnished to do so, subject to
10 * the following conditions:
11 *
12 * The above copyright notice and this permission notice shall be included
13 * in all copies or substantial portions of the Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 */
23
24 #include <errno.h>
25 #include <fcntl.h>
26 #include <glib-unix.h>
27 #include <glib/gi18n.h>
28 #include <stdio.h>
29 #include <stdlib.h>
30 #include <string.h>
31
32 #include "xwd-application.h"
33 #include "xwd-utmp.h"
34
35 #define BUFFER_TIMEOUT 500 /* ms */
36
37 struct _XwdApplication {
38 GApplication parent_instance;
39 gint masterfd;
40 gint slavefd;
41 GIOChannel *master_pty_chan;
42 guint buffer_timeout_id;
43 GString *message_buf;
44 };
45
46 G_DEFINE_TYPE(XwdApplication, xwd_application, G_TYPE_APPLICATION)
47
48 static void xwd_application_quit(GSimpleAction *, GVariant *, gpointer);
49
50 static const GOptionEntry cmd_options[] = {
51 { "debug", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL,
52 N_("Enable debugging messages"), NULL },
53 { "quit", 'q', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL,
54 N_("Quit running instance of xwrited"), NULL },
55 { "version", 'V', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL,
56 N_("Print the version number and quit"), NULL },
57 { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, NULL, NULL, NULL },
58 { NULL }
59 };
60
61 static const GActionEntry xwd_application_actions[] = {
62 { "quit", xwd_application_quit }
63 };
64
65 static void
66 xwd_application_quit(GSimpleAction *simple, GVariant *parameter,
67 gpointer user_data)
68 {
69 XwdApplication *self = user_data;
70
71 g_application_quit(G_APPLICATION(self));
72 }
73
74 static gboolean
75 on_signal(gpointer user_data)
76 {
77 XwdApplication *self = user_data;
78
79 g_debug("received signal, exiting");
80 g_action_group_activate_action(G_ACTION_GROUP(G_APPLICATION(self)),
81 "quit", NULL);
82
83 return (TRUE);
84 }
85
86 static GString *
87 string_copy(GString *dest, GString *src)
88 {
89 g_string_truncate(dest, 0);
90 g_string_append(dest, src->str);
91
92 return (dest);
93 }
94
95 static void
96 string_to_valid_utf8(GString *string)
97 {
98 GString *result;
99 gchar *start = string->str;
100 gchar *end;
101 gsize remaining = string->len;
102
103 result = g_string_sized_new(string->len);
104
105 while (remaining > 0) {
106 if (g_utf8_validate(start, remaining, (const gchar **)&end)) {
107 /* remaining part is valid */
108 g_string_append_len(result, start, remaining);
109 break;
110 }
111
112 /* append valid part */
113 g_string_append_len(result, start, end - start);
114 /*
115 * replace first invalid byte with Unicode "REPLACEMENT
116 * CHARACTER" (U+FFFD)
117 */
118 g_string_append(result, "\357\277\275");
119 remaining -= (end - start) + 1;
120 start = end + 1;
121 }
122
123 string_copy(string, result);
124 g_string_free(result, TRUE);
125 }
126
127 static void
128 string_trim_lines(GString *string)
129 {
130 GString *result;
131 gchar *p = string->str;
132 gchar *q;
133 gsize line_len;
134 gsize remaining = string->len;
135
136 result = g_string_sized_new(string->len);
137
138 while (remaining > 0) {
139 q = memchr(p, '\n', remaining);
140 if (q == NULL) {
141 g_string_append_len(result, p, remaining);
142 break;
143 }
144 line_len = q - p - 1;
145 /* convert \r\n to \n */
146 if ((line_len > 0) && (p[line_len - 1] == '\r')) {
147 line_len--;
148 }
149 /* trim spaces on the right */
150 while ((line_len > 0) && (p[line_len - 1] == ' ')) {
151 line_len--;
152 }
153 g_string_append_len(result, p, line_len);
154 g_string_append_c(result, '\n');
155 remaining -= q + 1 - p;
156 p = q + 1;
157 }
158
159 string_copy(string, result);
160 g_string_free(result, TRUE);
161 }
162
163 static void
164 string_filter_nonprintable(GString *string)
165 {
166 GString *result;
167 const gchar *p;
168 gunichar c;
169
170 result = g_string_sized_new(string->len);
171
172 for (p = string->str; *p != '\0'; p = g_utf8_next_char(p)) {
173 c = g_utf8_get_char(p);
174 if (g_unichar_isprint(c) || g_unichar_isspace(c)) {
175 g_string_append_unichar(result, c);
176 }
177 }
178
179 string_copy(string, result);
180 g_string_free(result, TRUE);
181 }
182
183 static gchar *
184 hexdumpa(const void *mem, gsize n)
185 {
186 const guchar *bytes = mem;
187 GString *string;
188 gsize i;
189 gsize j;
190
191 string = g_string_sized_new((n / 16 + (n % 16 > 0)) * 76);
192
193 for (i = 0; i < n; i += 16) {
194 g_string_append_printf(string, "%08zx ", i);
195
196 for (j = 0; (i + j < n) && (j < 16); j++) {
197 g_string_append_printf(string, " %02x", bytes[i + j]);
198 }
199 for (; j < 16; j++) {
200 g_string_append(string, " ");
201 }
202
203 g_string_append(string, " ");
204 for (j = 0; (i + j < n) && (j < 16); j++) {
205 g_string_append_printf(string, "%c",
206 g_ascii_isprint(bytes[i + j]) ? bytes[i + j] : '.');
207 }
208
209 g_string_append_c(string, '\n');
210 }
211
212 return (g_string_free(string, FALSE));
213 }
214
215 static void
216 display_message(XwdApplication *self)
217 {
218 gboolean enable_debug_logging;
219 gchar *message_dump;
220 GString *message;
221 GIcon *icon;
222 GNotification *notification;
223
224 if (self->message_buf->len == 0) {
225 return;
226 }
227
228 enable_debug_logging = (g_getenv("G_MESSAGES_DEBUG") != NULL);
229 if (enable_debug_logging) {
230 message_dump = hexdumpa(self->message_buf->str,
231 self->message_buf->len);
232 g_debug("raw message:\n%s", message_dump);
233 g_free(message_dump);
234 }
235
236 /*
237 * There is no reliable way to determine the character encoding of the
238 * received message which, depending on the locale of the sender, may
239 * even differ for different messages. A user could even send binary
240 * data. It is thus assumed that messages are in UTF-8 encoding which
241 * should be the most common case on modern systems. Any invalid
242 * sequences are replaced with the Unicode "REPLACEMENT CHARACTER"
243 * (U+FFFD) and non-printable characters are removed. Additionally,
244 * padding typically added by wall(1) implementations is removed in
245 * order to improve readability.
246 */
247 message = g_string_new_len(self->message_buf->str,
248 self->message_buf->len);
249 string_to_valid_utf8(message);
250 string_filter_nonprintable(message);
251 string_trim_lines(message);
252
253 if (enable_debug_logging) {
254 message_dump = hexdumpa(message->str, message->len);
255 g_debug("message:\n%s", message_dump);
256 g_free(message_dump);
257 }
258
259 icon = g_themed_icon_new("utilities-terminal");
260
261 notification = g_notification_new(_("Message received"));
262 g_notification_set_icon(notification, icon);
263 g_notification_set_body(notification, message->str);
264 g_application_send_notification(G_APPLICATION(self), NULL,
265 notification);
266
267 g_object_unref(notification);
268 g_object_unref(icon);
269 g_string_free(message, TRUE);
270 g_string_truncate(self->message_buf, 0);
271 }
272
273 static gboolean
274 on_buffer_timeout(gpointer user_data)
275 {
276 XwdApplication *self = user_data;
277
278 display_message(self);
279
280 self->buffer_timeout_id = 0;
281
282 return (FALSE);
283 }
284
285 static gboolean
286 on_master_pty_readable(GIOChannel *source, GIOCondition cond,
287 gpointer user_data)
288 {
289 XwdApplication *self = user_data;
290 GIOStatus status;
291 gchar buf[BUFSIZ];
292 gsize buf_len = 0;
293 GError *error = NULL;
294
295 if (cond & G_IO_IN) {
296 /* read message data from master pty */
297 memset(buf, 0, sizeof (buf));
298 while ((status = g_io_channel_read_chars(source, (gchar *)&buf,
299 sizeof (buf), &buf_len, &error)) == G_IO_STATUS_NORMAL) {
300 if (buf_len > 0) {
301 g_debug("read %" G_GSSIZE_FORMAT " bytes from "
302 "master pty", buf_len);
303 g_string_append_len(self->message_buf, buf,
304 buf_len);
305 }
306 }
307 if (error != NULL) {
308 g_critical("failed to read from master pty: %s",
309 error->message);
310 g_error_free(error);
311 return (FALSE);
312 }
313
314 /*
315 * a message might be read in several parts and it is not
316 * possible to reliably detect the beginning or end of a
317 * message, so buffer read data until a short timeout is
318 * reached as this works well for a single message which should
319 * be the most common case
320 */
321 if (self->buffer_timeout_id == 0) {
322 self->buffer_timeout_id = g_timeout_add(BUFFER_TIMEOUT,
323 on_buffer_timeout, self);
324 }
325 }
326
327 if (cond & (G_IO_HUP | G_IO_ERR)) {
328 g_critical("connection to master pty broken");
329 return (FALSE);
330 }
331
332 return (TRUE);
333 }
334
335 static void
336 xwd_application_startup(GApplication *application)
337 {
338 XwdApplication *self = XWD_APPLICATION(application);
339 gchar *slave_name;
340 GIOFlags flags;
341 GError *error = NULL;
342
343 G_APPLICATION_CLASS(xwd_application_parent_class)->startup(application);
344
345 /* create actions */
346 g_action_map_add_action_entries(G_ACTION_MAP(self),
347 xwd_application_actions, G_N_ELEMENTS(xwd_application_actions),
348 self);
349
350 /* create signal watchers */
351 g_unix_signal_add(SIGINT, on_signal, self);
352 g_unix_signal_add(SIGTERM, on_signal, self);
353 g_unix_signal_add(SIGHUP, on_signal, self);
354
355 /* open master pty */
356 self->masterfd = posix_openpt(O_RDWR | O_NOCTTY);
357 if (self->masterfd == -1) {
358 g_critical("failed to open master pty: %s", g_strerror(errno));
359 return;
360 }
361
362 /* create slave pty */
363 if ((grantpt(self->masterfd) == -1) ||
364 (unlockpt(self->masterfd) == -1)) {
365 g_critical("failed to create slave pty: %s", g_strerror(errno));
366 return;
367 }
368 slave_name = ptsname(self->masterfd);
369 if (slave_name == NULL) {
370 g_critical("failed to obtain name of slave pty");
371 return;
372 }
373
374 /*
375 * keep an open fd around order to prevent closing the master fd when
376 * receiving an EOF
377 */
378 self->slavefd = open(slave_name, O_RDWR);
379 if (self->slavefd == -1) {
380 g_critical("failed to open slave pty: %s", g_strerror(errno));
381 return;
382 }
383
384 /* create a GIOChannel for monitoring the master pty for messages */
385 self->master_pty_chan = g_io_channel_unix_new(self->masterfd);
386 /* make it non-blocking */
387 flags = g_io_channel_get_flags(self->master_pty_chan);
388 if (g_io_channel_set_flags(self->master_pty_chan,
389 flags | G_IO_FLAG_NONBLOCK, &error) != G_IO_STATUS_NORMAL) {
390 g_critical("failed set flags on the master pty channel: %s",
391 error->message);
392 g_error_free(error);
393 return;
394 }
395 /* make the channel safe for encodings other than UTF-8 */
396 if (g_io_channel_set_encoding(self->master_pty_chan, NULL, &error) !=
397 G_IO_STATUS_NORMAL) {
398 g_critical("failed set encoding on the master pty channel: %s",
399 error->message);
400 g_error_free(error);
401 return;
402 }
403 if (!g_io_add_watch(self->master_pty_chan,
404 G_IO_IN | G_IO_HUP | G_IO_ERR, on_master_pty_readable, self)) {
405 g_critical("failed to add watch on master pty channel");
406 return;
407 }
408
409 xwd_utmp_add_entry(self->masterfd);
410
411 /* keep GApplication running */
412 g_application_hold(application);
413 }
414
415 static void
416 xwd_application_shutdown(GApplication *application)
417 {
418 XwdApplication *self = XWD_APPLICATION(application);
419 GApplicationClass *application_class =
420 G_APPLICATION_CLASS(xwd_application_parent_class);
421
422 /* display any buffered data before exiting */
423 display_message(self);
424
425 if (self->master_pty_chan != NULL) {
426 g_io_channel_shutdown(self->master_pty_chan, FALSE, NULL);
427 g_clear_pointer(&self->master_pty_chan,
428 (GDestroyNotify)g_io_channel_unref);
429 }
430
431 if (self->slavefd != -1) {
432 close(self->slavefd);
433 self->slavefd = -1;
434 }
435
436 if (self->masterfd != -1) {
437 close(self->masterfd);
438 self->masterfd = -1;
439 }
440
441 xwd_utmp_remove_entry(self->masterfd);
442
443 /* remove signal watches and buffer timeout */
444 while (g_source_remove_by_user_data(self)) {
445 continue;
446 }
447 self->buffer_timeout_id = 0;
448
449 application_class->shutdown(application);
450 }
451
452 static gint
453 xwd_application_handle_local_options(GApplication *application,
454 GVariantDict *options)
455 {
456 gchar **args = NULL;
457 gchar *messages_debug;
458 GError *error = NULL;
459
460 /* filename arguments are not allowed */
461 if (g_variant_dict_lookup(options, G_OPTION_REMAINING, "^a&ay",
462 &args)) {
463 g_printerr("invalid argument: \"%s\"\n", args[0]);
464 g_free(args);
465 return (1);
466 }
467
468 if (g_variant_dict_contains(options, "version")) {
469 g_print("%s %s\n", PACKAGE, VERSION);
470
471 /* quit */
472 return (0);
473 }
474
475 if (g_variant_dict_contains(options, "debug")) {
476 /* enable debug logging */
477 messages_debug = g_strjoin(":", G_LOG_DOMAIN,
478 g_getenv("G_MESSAGES_DEBUG"), NULL);
479 g_setenv("G_MESSAGES_DEBUG", messages_debug, TRUE);
480 g_free(messages_debug);
481 }
482
483 /*
484 * register with the session bus so that it is possible to discern
485 * between remote and primary instance and that remote actions can be
486 * invoked, this causes the startup signal to be emitted which, in case
487 * of the primary instance, starts to instantiate the
488 * backend with the given values
489 */
490 if (!g_application_register(application, NULL, &error)) {
491 g_critical("g_application_register: %s", error->message);
492 g_error_free(error);
493 return (1);
494 }
495
496 if (g_variant_dict_contains(options, "quit")) {
497 /* only valid if a remote instance is running */
498 if (!g_application_get_is_remote(application)) {
499 g_printerr("%s is not running\n", g_get_prgname());
500 return (1);
501 }
502
503 /* signal remote instance to quit */
504 g_action_group_activate_action(G_ACTION_GROUP(application),
505 "quit", NULL);
506
507 /* quit local instance */
508 return (0);
509 }
510
511 /* proceed with default command line processing */
512 return (-1);
513 }
514
515 static void
516 xwd_application_activate(GApplication *application) {
517 GApplicationClass *application_class =
518 G_APPLICATION_CLASS(xwd_application_parent_class);
519
520 /* do nothing, implementation required by GApplication */
521
522 application_class->activate(application);
523 }
524
525 static void
526 xwd_application_finalize(GObject *object)
527 {
528 XwdApplication *self = XWD_APPLICATION(object);
529
530 g_string_free(self->message_buf, TRUE);
531
532 G_OBJECT_CLASS(xwd_application_parent_class)->finalize(object);
533 }
534
535 static void
536 xwd_application_class_init(XwdApplicationClass *klass)
537 {
538 GObjectClass *object_class = G_OBJECT_CLASS(klass);
539 GApplicationClass *application_class = G_APPLICATION_CLASS(klass);
540
541 object_class->finalize = xwd_application_finalize;
542
543 application_class->startup = xwd_application_startup;
544 application_class->shutdown = xwd_application_shutdown;
545 application_class->handle_local_options =
546 xwd_application_handle_local_options;
547 application_class->activate = xwd_application_activate;
548 }
549
550 static void
551 xwd_application_init(XwdApplication *self)
552 {
553 g_application_add_main_option_entries(G_APPLICATION(self),
554 cmd_options);
555
556 self->masterfd = -1;
557 self->slavefd = -1;
558
559 self->message_buf = g_string_sized_new(BUFSIZ);
560 }
561
562 XwdApplication *
563 xwd_application_new(void)
564 {
565 return (g_object_new(XWD_TYPE_APPLICATION, "application-id",
566 APPLICATION_ID, NULL));
567 }