view 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
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_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.
	 */
	message = g_string_new_len(self->message_buf->str,
	    self->message_buf->len);
	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));
}