cmd.c
author Myhailo Danylenko <isbear@ukrpost.net>
Thu, 13 Nov 2014 02:32:01 +0200
changeset 35 7ee07b3cc5f9
parent 34 5b818c7467ad
child 36 57b2c4f84169
permissions -rw-r--r--
Use user-configured option names in module description

/*
 * cmd.c                -- Send shell command output as messages
 *
 * Copyrigth (C) 2009      Myhailo Danylenko <isbear@ukrpost.net>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 */

#include <stdlib.h>
#include <glib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

#include <mcabber/commands.h>
#include <mcabber/logprint.h>
#include <mcabber/utils.h>
#include <mcabber/settings.h>
#include <mcabber/hbuf.h>
#include <mcabber/xmpp.h>
#include <mcabber/roster.h>
#include <mcabber/modules.h>
#include <mcabber/hooks.h>

#include "config.h"

void mcmd_init   (void);
void mcmd_uninit (void);

#define DESCRIPTION ( \
	"Sends output of command to buddy\n" \
	"Recognizes options " OPT_CMD_SHELL ", " OPT_CMD_REDIRECT_STDERR ", " OPT_CMD_HEADER " and " OPT_CMD_HEADER_INLINE "\n" \
	"Provides command /cmd" )

module_info_t info_cmd = {
	.branch          = MCABBER_BRANCH,
	.api             = MCABBER_API_VERSION,
	.version         = PROJECT_VERSION,
	.description     = DESCRIPTION,
	.requires        = NULL,
	.init            = mcmd_init,
	.uninit          = mcmd_uninit,
	.next            = NULL,
};

typedef struct {
	gchar      *jid;
	GString    *input;
	gchar      *subject;
	guint       seq;
	guint       source;
	GIOChannel *channel;
} cmd_cb_t;

#ifdef MCABBER_API_HAVE_CMD_ID
static gpointer mcmd_cmid = NULL;
#endif

static GSList *cmd_channels = NULL;

static gboolean is_room (const gchar *jid)
{
	return roster_find (jid, jidsearch, ROSTER_TYPE_ROOM) ? TRUE : FALSE;
}

static gboolean cmd_send_msg (const gchar *to, const gchar *subject, const gchar *body)
{
	gboolean room    = is_room (to);
	gpointer xep184  = NULL;
	gint     crypted;

	xmpp_send_msg (to, body, room ? ROSTER_TYPE_ROOM : ROSTER_TYPE_USER, room ? NULL : subject, FALSE /* ? */, &crypted, LM_MESSAGE_SUB_TYPE_NOT_SET, &xep184);

	if (crypted == -1) {
		scr_log_print (LPRINT_LOGNORM, "cmd: Encryption error. Message not sent.");
		return FALSE;
	}

	if (!room) {
		if (subject) {
			gchar *hbody = g_strdup_printf ("[%s]\n%s", subject, body);
			hk_message_out (to, NULL, 0, hbody, crypted, xep184);
			g_free (hbody);
		} else
			hk_message_out (to, NULL, 0, body, crypted, xep184);
	}

	return TRUE;
}

static gboolean cmd_reader (GIOChannel *channel, GIOCondition condition, gpointer data)
{
	cmd_cb_t *cb = (cmd_cb_t *) data;

	if (condition & (G_IO_IN|G_IO_PRI)) {
		GIOStatus     chstat;
		static gchar  buf[HBB_BLOCKSIZE];
		gsize         endpos;
		GError       *error              = NULL;

		chstat = g_io_channel_read_chars (channel, buf, HBB_BLOCKSIZE, &endpos, &error);

		if (error) {
			scr_log_print (LPRINT_DEBUG, "cmd: Reading error: %s.", error -> message);
			g_clear_error (&error);
		}

		if (chstat == G_IO_STATUS_ERROR || chstat == G_IO_STATUS_EOF) {

			return FALSE; // XXX
		}
		
		if (endpos) {
			GString *input = cb->input;
			gsize    bread   = 0;
			gsize    written = 0;
			gchar   *utf8    = NULL;

			g_string_append_len (input, buf, endpos);

			if (!xmpp_is_online ()) {
				scr_log_print (LPRINT_LOGNORM, "cmd: Connection is not ready, delaying data");
				return TRUE;
			}

			// usual g_locale_to_utf8 seem to be unable to detect locale charset
			// maybe, proper solution will be to call setlocale on module loading,
			// but mcabber already does this, and I do not want to mess with it
			utf8 = g_convert (input->str, input->len, LocaleCharSet, "UTF-8", &bread, &written, &error);

			if (error && error->code == G_CONVERT_ERROR_ILLEGAL_SEQUENCE && bread) {
				written = 0;
				g_clear_error (&error);
				utf8 = g_convert (input->str, bread, LocaleCharSet, "UTF-8", &bread, &written, &error);
			}
			
			if (written) {
				gsize sent = 0;

				while (sent < written) {
					gsize     len     = 0;
					gchar    *bbuf    = NULL;
					gchar    *subject = NULL;

					if (written - sent > HBB_BLOCKSIZE) {
						gchar *c = utf8 + sent + HBB_BLOCKSIZE;
						c = g_utf8_find_prev_char (utf8 + sent, c);

						if (!c) {
							scr_log_print (LPRINT_LOGNORM, "cmd: Cannot determine utf8 character end! End of data chunk will be discarded!");
							break;
						}

						len  = c - utf8 - sent;
						bbuf = g_strndup (utf8 + sent, len);
					}

					cb -> seq += 1;

					if (cb -> subject)
						subject = g_strdup_printf (cb -> subject, cb -> seq);

					if (!cmd_send_msg (cb->jid, subject, len ? bbuf : (utf8 + sent)))
						scr_log_print (LPRINT_LOGNORM, "cmd: Encryption error. Message not sent.");

					if (subject)
						g_free (subject);

					if (!len)
						break;

					g_free (bbuf);

					sent += len;
				}

				g_free (utf8);

				g_string_erase (input, 0, bread);

			} else {

				scr_log_print (LPRINT_LOGNORM, "cmd: Character conversion error: %s", error->message);
				g_error_free (error);

				return FALSE;
			}
		}

	} else if (condition & (G_IO_ERR|G_IO_NVAL|G_IO_HUP)) {

		return FALSE; // XXX
	}

	return TRUE;
}

static void cmd_destroy_data (gpointer data)
{
	cmd_cb_t *cb = (cmd_cb_t *) data;

	cmd_channels = g_slist_remove (cmd_channels, data);

	if (cb -> subject)
		g_free (cb -> subject);
	g_free (cb->jid);
	g_slice_free (cmd_cb_t, cb);
}

static void do_cmd (char *arg)
{
	int         fd[2];
	const char *jid = CURRENT_JID;

	if (!*arg)
		return;
	
	if (!jid) {
		scr_log_print (LPRINT_LOGNORM, "Unsuitable buddy selected.");
		return;
	}
	
	if (pipe (fd)) {
		scr_log_print (LPRINT_LOGNORM, "Cannot create pipe: %s.", strerror (errno));
		return;
	}

	{
		GIOChannel *channel;
		int         res     = fork ();

		if (!res) {
			
			close (fd[0]);
			dup2 (fd[1], STDOUT_FILENO);
			if (settings_opt_get_int ( OPT_CMD_REDIRECT_STDERR ))
				dup2 (fd[1], STDERR_FILENO);
			else
				close (STDERR_FILENO);
			close (STDIN_FILENO);
			close (fd[1]);

			{
				const char *shell = settings_opt_get ( OPT_CMD_SHELL );
				if (!shell)
					shell = getenv ("SHELL");
				if (!shell)
					shell = "sh";
				execl (shell, shell, "-c", arg, NULL);
			}
		}

		if (res == -1) {
			scr_log_print (LPRINT_NORMAL, "Cannot fork child: %s.", strerror (errno));
			close (fd[0]);
			close (fd[1]);
			return;
		}

		close (fd[1]);

		{
			GError *error = NULL;

			channel = g_io_channel_unix_new (fd[0]);

			g_io_channel_set_encoding (channel, NULL, &error);
			if (error) {
				scr_log_print (LPRINT_DEBUG, "cmd: Cannot unset channel encoding: %s.", error -> message);
				g_clear_error (&error);
			}
			g_io_channel_set_buffered (channel, FALSE);
			g_io_channel_set_flags (channel, G_IO_FLAG_NONBLOCK, &error);
			if (error) {
				scr_log_print (LPRINT_DEBUG, "cmd: Cannot set nonblocking flag on channel: %s.", error -> message);
				g_error_free (error);
			}
			g_io_channel_set_close_on_unref (channel, TRUE);
		}

		{
			cmd_cb_t *cb = g_slice_new (cmd_cb_t);

			if (settings_opt_get_int ( OPT_CMD_HEADER )) {
				if (settings_opt_get_int ( OPT_CMD_HEADER_INLINE ) || is_room (jid)) {
					gchar *mesg = g_strdup_printf ("$ %s", arg);
					gchar *utf  = to_utf8 (mesg);
					g_free (mesg);
					cmd_send_msg (jid, NULL, utf);
					g_free (utf);
					cb -> subject = NULL;
				} else {
					gchar *header = g_strdup_printf ("[%%02d] $ %s", arg);
					cb -> subject = to_utf8 (header);
					g_free (header);
				}
			}

			cb -> jid     = g_strdup (jid);
			cb -> input   = g_string_new (NULL);
			cb -> seq     = 0;
			cb -> channel = channel;
			
			cmd_channels  = g_slist_append (cmd_channels, cb);

			cb -> source  = g_io_add_watch_full (channel, 0, G_IO_IN|G_IO_ERR|G_IO_HUP|G_IO_NVAL,
			                                     cmd_reader, (gpointer) cb, cmd_destroy_data);

			g_io_channel_unref ( channel );

		}
	}
}

void mcmd_init (void)
{
#ifndef MCABBER_API_HAVE_CMD_ID
	cmd_add ("cmd", "", 0, 0, do_cmd, NULL);
#else
	mcmd_cmid = cmd_add ("cmd", "", 0, 0, do_cmd, NULL);
#endif
}

void mcmd_uninit (void)
{
	GSList *sel;

#ifndef MCABBER_API_HAVE_CMD_ID
	cmd_del ("cmd");
#else
	if (mcmd_cmid)
		cmd_del (mcmd_cmid);
#endif

	for (sel = cmd_channels; sel; ) {
		cmd_cb_t *cb = (cmd_cb_t *) sel->data;
		sel = sel -> next;
		if (cb->source)
			g_source_remove (cb->source);
	}
}

/* vim: se ts=4 sw=4: */