Mercurial > projects > xwrited
view xwd-application.c @ 23:98b5d974cea5 default tip
Added tag version-3 for changeset 9d00c0e07c47
author | Guido Berhoerster <guido+xwrited@berhoerster.name> |
---|---|
date | Sat, 30 Jun 2018 23:09:12 +0200 |
parents | 683ebd334b21 |
children |
line wrap: on
line source
/* * Copyright (C) 2018 Guido Berhoerster <guido+xwrited@berhoerster.name> * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include <errno.h> #include <fcntl.h> #include <glib-unix.h> #include <glib/gi18n.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include "xwd-application.h" #include "xwd-utmp.h" #define BUFFER_TIMEOUT 500 /* ms */ struct _XwdApplication { GApplication parent_instance; gint masterfd; gint slavefd; GIOChannel *master_pty_chan; guint buffer_timeout_id; GString *message_buf; }; G_DEFINE_TYPE(XwdApplication, xwd_application, G_TYPE_APPLICATION) static void xwd_application_quit(GSimpleAction *, GVariant *, gpointer); static const GOptionEntry cmd_options[] = { { "debug", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, N_("Enable debugging messages"), NULL }, { "quit", 'q', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, N_("Quit running instance of xwrited"), NULL }, { "version", 'V', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, N_("Print the version number and quit"), NULL }, { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, NULL, NULL, NULL }, { NULL } }; static const GActionEntry xwd_application_actions[] = { { "quit", xwd_application_quit } }; static void xwd_application_quit(GSimpleAction *simple, GVariant *parameter, gpointer user_data) { XwdApplication *self = user_data; g_application_quit(G_APPLICATION(self)); } static gboolean on_signal(gpointer user_data) { XwdApplication *self = user_data; g_debug("received signal, exiting"); g_action_group_activate_action(G_ACTION_GROUP(G_APPLICATION(self)), "quit", NULL); return (TRUE); } static GString * string_copy(GString *dest, GString *src) { g_string_truncate(dest, 0); g_string_append(dest, src->str); return (dest); } static void string_decode_octal(GString *string) { GString *result; gsize remaining = string->len; gchar *p = string->str; gchar oct[4] = { '\0' }; guint64 num; char *end; result = g_string_sized_new(string->len); while (remaining > 0) { if ((remaining >= 4) && (*p == '\\')) { /* put octal number in NUL-terminated buffer */ memcpy(oct, p + 1, 3); /* convert valid octal number to byte */ num = g_ascii_strtoull(oct, &end, 8); if ((*end == '\0') && (num <= 0xff)) { /* conversion succeeded */ p += 4; remaining -= 4; g_string_append_c(result, num); continue; } } g_string_append_c(result, *p++); remaining--; } string_copy(string, result); g_string_free(result, TRUE); } static void string_decode_meta_ascii(GString *string) { GString *result; gchar *p = string->str; gsize remaining = string->len; result = g_string_sized_new(string->len); while (remaining > 0) { if ((remaining >= 3) && (*p == 'M') && (*(p + 1) == '-') && ((*(p + 2) & (1 << 7)) == 0)) { /* restore 8th bit */ g_string_append_c(result, *(p + 2) | (1 << 7)); remaining -= 3; p += 3; continue; } g_string_append_c(result, *p++); remaining--; } string_copy(string, result); g_string_free(result, TRUE); } static void string_to_valid_utf8(GString *string) { GString *result; gchar *start = string->str; gchar *end; gsize remaining = string->len; result = g_string_sized_new(string->len); while (remaining > 0) { if (g_utf8_validate(start, remaining, (const gchar **)&end)) { /* remaining part is valid */ g_string_append_len(result, start, remaining); break; } /* append valid part */ g_string_append_len(result, start, end - start); /* * replace first invalid byte with Unicode "REPLACEMENT * CHARACTER" (U+FFFD) */ g_string_append(result, "\357\277\275"); remaining -= (end - start) + 1; start = end + 1; } string_copy(string, result); g_string_free(result, TRUE); } static void string_trim_lines(GString *string) { GString *result; gchar *p = string->str; gchar *q; gsize line_len; gsize remaining = string->len; result = g_string_sized_new(string->len); while (remaining > 0) { q = memchr(p, '\n', remaining); if (q == NULL) { g_string_append_len(result, p, remaining); break; } line_len = q - p - 1; /* convert \r\n to \n */ if ((line_len > 0) && (p[line_len - 1] == '\r')) { line_len--; } /* trim spaces on the right */ while ((line_len > 0) && (p[line_len - 1] == ' ')) { line_len--; } g_string_append_len(result, p, line_len); g_string_append_c(result, '\n'); remaining -= q + 1 - p; p = q + 1; } string_copy(string, result); g_string_free(result, TRUE); } static void string_filter_nonprintable(GString *string) { GString *result; const gchar *p; gunichar c; result = g_string_sized_new(string->len); for (p = string->str; *p != '\0'; p = g_utf8_next_char(p)) { c = g_utf8_get_char(p); if (g_unichar_isprint(c) || g_unichar_isspace(c)) { g_string_append_unichar(result, c); } } string_copy(string, result); g_string_free(result, TRUE); } static gchar * hexdumpa(const void *mem, gsize n) { const guchar *bytes = mem; GString *string; gsize i; gsize j; string = g_string_sized_new((n / 16 + (n % 16 > 0)) * 76); for (i = 0; i < n; i += 16) { g_string_append_printf(string, "%08zx ", i); for (j = 0; (i + j < n) && (j < 16); j++) { g_string_append_printf(string, " %02x", bytes[i + j]); } for (; j < 16; j++) { g_string_append(string, " "); } g_string_append(string, " "); for (j = 0; (i + j < n) && (j < 16); j++) { g_string_append_printf(string, "%c", g_ascii_isprint(bytes[i + j]) ? bytes[i + j] : '.'); } g_string_append_c(string, '\n'); } return (g_string_free(string, FALSE)); } static void display_message(XwdApplication *self) { gboolean enable_debug_logging; gchar *message_dump; GString *message; GIcon *icon; GNotification *notification; if (self->message_buf->len == 0) { return; } enable_debug_logging = (g_getenv("G_MESSAGES_DEBUG") != NULL); if (enable_debug_logging) { message_dump = hexdumpa(self->message_buf->str, self->message_buf->len); g_debug("raw message:\n%s", message_dump); g_free(message_dump); } /* * There is no reliable way to determine the character encoding of the * received message which, depending on the locale of the sender, may * even differ for different messages. A user could even send binary * data. It is thus assumed that messages are in UTF-8 encoding which * should be the most common case on modern systems. Any invalid * sequences are replaced with the Unicode "REPLACEMENT CHARACTER" * (U+FFFD) and non-printable characters are removed. Additionally, * padding typically added by wall(1) implementations is removed in * order to improve readability. * Some write(1) and wall(1) implementations encode non-ASCII * characters, in particular UTF-8 sequences, by prefixing them with * "M-" and clearing the 8th bit while others (e.g. util-linux) use * octal escape sequences. These encodings are reversed before messages * are processed further. However some implementations such as NetBSD * write(1) uncoditionally process each byte with toascii(3) which * makes it impossible to restore the original value. */ message = g_string_new_len(self->message_buf->str, self->message_buf->len); string_decode_octal(message); string_decode_meta_ascii(message); string_to_valid_utf8(message); string_filter_nonprintable(message); string_trim_lines(message); if (enable_debug_logging) { message_dump = hexdumpa(message->str, message->len); g_debug("message:\n%s", message_dump); g_free(message_dump); } icon = g_themed_icon_new("utilities-terminal"); notification = g_notification_new(_("Message received")); g_notification_set_icon(notification, icon); g_notification_set_body(notification, message->str); g_application_send_notification(G_APPLICATION(self), NULL, notification); g_object_unref(notification); g_object_unref(icon); g_string_free(message, TRUE); g_string_truncate(self->message_buf, 0); } static gboolean on_buffer_timeout(gpointer user_data) { XwdApplication *self = user_data; display_message(self); self->buffer_timeout_id = 0; return (FALSE); } static gboolean on_master_pty_readable(GIOChannel *source, GIOCondition cond, gpointer user_data) { XwdApplication *self = user_data; GIOStatus status; gchar buf[BUFSIZ]; gsize buf_len = 0; GError *error = NULL; if (cond & G_IO_IN) { /* read message data from master pty */ memset(buf, 0, sizeof (buf)); while ((status = g_io_channel_read_chars(source, (gchar *)&buf, sizeof (buf), &buf_len, &error)) == G_IO_STATUS_NORMAL) { if (buf_len > 0) { g_debug("read %" G_GSSIZE_FORMAT " bytes from " "master pty", buf_len); g_string_append_len(self->message_buf, buf, buf_len); } } if (error != NULL) { g_critical("failed to read from master pty: %s", error->message); g_error_free(error); return (FALSE); } /* * a message might be read in several parts and it is not * possible to reliably detect the beginning or end of a * message, so buffer read data until a short timeout is * reached as this works well for a single message which should * be the most common case */ if (self->buffer_timeout_id == 0) { self->buffer_timeout_id = g_timeout_add(BUFFER_TIMEOUT, on_buffer_timeout, self); } } if (cond & (G_IO_HUP | G_IO_ERR)) { g_critical("connection to master pty broken"); return (FALSE); } return (TRUE); } static void xwd_application_startup(GApplication *application) { XwdApplication *self = XWD_APPLICATION(application); gchar *slave_name; GIOFlags flags; GError *error = NULL; G_APPLICATION_CLASS(xwd_application_parent_class)->startup(application); /* create actions */ g_action_map_add_action_entries(G_ACTION_MAP(self), xwd_application_actions, G_N_ELEMENTS(xwd_application_actions), self); /* create signal watchers */ g_unix_signal_add(SIGINT, on_signal, self); g_unix_signal_add(SIGTERM, on_signal, self); g_unix_signal_add(SIGHUP, on_signal, self); /* open master pty */ self->masterfd = posix_openpt(O_RDWR | O_NOCTTY); if (self->masterfd == -1) { g_critical("failed to open master pty: %s", g_strerror(errno)); return; } /* create slave pty */ if ((grantpt(self->masterfd) == -1) || (unlockpt(self->masterfd) == -1)) { g_critical("failed to create slave pty: %s", g_strerror(errno)); return; } slave_name = ptsname(self->masterfd); if (slave_name == NULL) { g_critical("failed to obtain name of slave pty"); return; } /* * keep an open fd around order to prevent closing the master fd when * receiving an EOF */ self->slavefd = open(slave_name, O_RDWR); if (self->slavefd == -1) { g_critical("failed to open slave pty: %s", g_strerror(errno)); return; } /* create a GIOChannel for monitoring the master pty for messages */ self->master_pty_chan = g_io_channel_unix_new(self->masterfd); /* make it non-blocking */ flags = g_io_channel_get_flags(self->master_pty_chan); if (g_io_channel_set_flags(self->master_pty_chan, flags | G_IO_FLAG_NONBLOCK, &error) != G_IO_STATUS_NORMAL) { g_critical("failed set flags on the master pty channel: %s", error->message); g_error_free(error); return; } /* make the channel safe for encodings other than UTF-8 */ if (g_io_channel_set_encoding(self->master_pty_chan, NULL, &error) != G_IO_STATUS_NORMAL) { g_critical("failed set encoding on the master pty channel: %s", error->message); g_error_free(error); return; } if (!g_io_add_watch(self->master_pty_chan, G_IO_IN | G_IO_HUP | G_IO_ERR, on_master_pty_readable, self)) { g_critical("failed to add watch on master pty channel"); return; } xwd_utmp_add_entry(self->masterfd); /* keep GApplication running */ g_application_hold(application); } static void xwd_application_shutdown(GApplication *application) { XwdApplication *self = XWD_APPLICATION(application); GApplicationClass *application_class = G_APPLICATION_CLASS(xwd_application_parent_class); /* display any buffered data before exiting */ display_message(self); if (self->master_pty_chan != NULL) { g_io_channel_shutdown(self->master_pty_chan, FALSE, NULL); g_clear_pointer(&self->master_pty_chan, (GDestroyNotify)g_io_channel_unref); } if (self->slavefd != -1) { close(self->slavefd); self->slavefd = -1; } if (self->masterfd != -1) { close(self->masterfd); self->masterfd = -1; } xwd_utmp_remove_entry(self->masterfd); /* remove signal watches and buffer timeout */ while (g_source_remove_by_user_data(self)) { continue; } self->buffer_timeout_id = 0; application_class->shutdown(application); } static gint xwd_application_handle_local_options(GApplication *application, GVariantDict *options) { gchar **args = NULL; gchar *messages_debug; GError *error = NULL; /* filename arguments are not allowed */ if (g_variant_dict_lookup(options, G_OPTION_REMAINING, "^a&ay", &args)) { g_printerr("invalid argument: \"%s\"\n", args[0]); g_free(args); return (1); } if (g_variant_dict_contains(options, "version")) { g_print("%s %s\n", PACKAGE, VERSION); /* quit */ return (0); } if (g_variant_dict_contains(options, "debug")) { /* enable debug logging */ messages_debug = g_strjoin(":", G_LOG_DOMAIN, g_getenv("G_MESSAGES_DEBUG"), NULL); g_setenv("G_MESSAGES_DEBUG", messages_debug, TRUE); g_free(messages_debug); } /* * register with the session bus so that it is possible to discern * between remote and primary instance and that remote actions can be * invoked, this causes the startup signal to be emitted which, in case * of the primary instance, starts to instantiate the * backend with the given values */ if (!g_application_register(application, NULL, &error)) { g_critical("g_application_register: %s", error->message); g_error_free(error); return (1); } if (g_variant_dict_contains(options, "quit")) { /* only valid if a remote instance is running */ if (!g_application_get_is_remote(application)) { g_printerr("%s is not running\n", g_get_prgname()); return (1); } /* signal remote instance to quit */ g_action_group_activate_action(G_ACTION_GROUP(application), "quit", NULL); /* quit local instance */ return (0); } /* proceed with default command line processing */ return (-1); } static void xwd_application_activate(GApplication *application) { GApplicationClass *application_class = G_APPLICATION_CLASS(xwd_application_parent_class); /* do nothing, implementation required by GApplication */ application_class->activate(application); } static void xwd_application_finalize(GObject *object) { XwdApplication *self = XWD_APPLICATION(object); g_string_free(self->message_buf, TRUE); G_OBJECT_CLASS(xwd_application_parent_class)->finalize(object); } static void xwd_application_class_init(XwdApplicationClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GApplicationClass *application_class = G_APPLICATION_CLASS(klass); object_class->finalize = xwd_application_finalize; application_class->startup = xwd_application_startup; application_class->shutdown = xwd_application_shutdown; application_class->handle_local_options = xwd_application_handle_local_options; application_class->activate = xwd_application_activate; } static void xwd_application_init(XwdApplication *self) { g_application_add_main_option_entries(G_APPLICATION(self), cmd_options); self->masterfd = -1; self->slavefd = -1; self->message_buf = g_string_sized_new(BUFSIZ); } XwdApplication * xwd_application_new(void) { return (g_object_new(XWD_TYPE_APPLICATION, "application-id", APPLICATION_ID, NULL)); }