Mercurial > projects > xwrited
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 } |