summaryrefslogtreecommitdiff
path: root/libgsystem/gsystem-console.c
diff options
context:
space:
mode:
Diffstat (limited to 'libgsystem/gsystem-console.c')
-rw-r--r--libgsystem/gsystem-console.c443
1 files changed, 443 insertions, 0 deletions
diff --git a/libgsystem/gsystem-console.c b/libgsystem/gsystem-console.c
new file mode 100644
index 000000000..35477ebaf
--- /dev/null
+++ b/libgsystem/gsystem-console.c
@@ -0,0 +1,443 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2013 Colin Walters <walters@verbum.org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#define _GSYSTEM_NO_LOCAL_ALLOC
+#include "libgsystem.h"
+
+/**
+ * SECTION:gsconsole
+ * @title: GSConsole
+ * @short_description: Interact with standard input/output as well as terminal
+ *
+ * First, this class offers API to access the standard input and
+ * output/error, streams as #GInputStream and #GOutputStream
+ * respectively.
+ *
+ * In the case where the process is connected to a controlling
+ * terminal, the gs_console_get() API is available, which exposes a
+ * number of additional features such as no-echo password reading.
+ *
+ * Since: 2.36
+ */
+
+#include "config.h"
+
+#include "gsystem-console.h"
+
+#include <string.h>
+#ifdef G_OS_UNIX
+#include <gio/gunixoutputstream.h>
+#include <gio/gfiledescriptorbased.h>
+#include <gio/gunixinputstream.h>
+#include <glib-unix.h>
+#include <unistd.h>
+#include <termios.h>
+#endif
+#include <fcntl.h>
+
+typedef GObjectClass GSConsoleClass;
+
+struct _GSConsole
+{
+ GObject parent;
+
+ gboolean in_status_line;
+ gssize last_line_written;
+};
+
+G_DEFINE_TYPE (GSConsole, gs_console, G_TYPE_OBJECT);
+
+static void
+gs_console_init (GSConsole *self)
+{
+ self->last_line_written = -1;
+}
+
+static void
+gs_console_class_init (GSConsoleClass *class)
+{
+}
+
+/**
+ * gs_console_get:
+ *
+ * If the current process has an interactive console, return the
+ * singleton #GSConsole instance. On Unix, this is equivalent to
+ * isatty(). For all other cases, such as pipes, sockets, /dev/null,
+ * this function will return %NULL.
+ *
+ * Returns: (transfer none): The console instance, or %NULL if not interactive
+ *
+ * Since: 2.36
+ */
+GSConsole *
+gs_console_get (void)
+{
+ static gsize checked = 0;
+ static GSConsole *instance = NULL;
+
+ if (g_once_init_enter (&checked))
+ {
+#ifdef G_OS_UNIX
+ if (isatty (0) && isatty (1))
+ instance = g_object_new (GS_TYPE_CONSOLE, NULL);
+#endif
+ g_once_init_leave (&checked, 1);
+ }
+
+ return (GSConsole*) instance;
+}
+
+/**
+ * gs_console_get_stdin:
+ *
+ * Returns: (transfer none): The singleton stream connected to standard input
+ */
+GInputStream *
+gs_console_get_stdin (void)
+{
+#ifdef G_OS_UNIX
+ static gsize instance = 0;
+
+ if (g_once_init_enter (&instance))
+ g_once_init_leave (&instance, (gsize) g_unix_input_stream_new (0, FALSE));
+
+ return (GInputStream*) instance;
+#else
+ g_error ("not implemented");
+#endif
+}
+
+/**
+ * gs_console_get_stdout:
+ *
+ * Returns: (transfer none): The singleton stream connected to standard output
+ */
+GOutputStream *
+gs_console_get_stdout (void)
+{
+#ifdef G_OS_UNIX
+ static gsize instance = 0;
+
+ if (g_once_init_enter (&instance))
+ g_once_init_leave (&instance, (gsize) g_unix_output_stream_new (1, FALSE));
+
+ return (GOutputStream*) instance;
+#else
+ g_error ("not implemented");
+#endif
+}
+
+/**
+ * gs_console_get_stderr:
+ *
+ * Returns: (transfer none): The singleton stream connected to standard error
+ */
+GOutputStream *
+gs_console_get_stderr (void)
+{
+#ifdef G_OS_UNIX
+ static gsize instance = 0;
+
+ if (g_once_init_enter (&instance))
+ g_once_init_leave (&instance, (gsize) g_unix_output_stream_new (2, FALSE));
+
+ return (GOutputStream*) instance;
+#else
+ g_error ("not implemented");
+#endif
+}
+
+#ifdef G_OS_UNIX
+static inline void
+_set_error_from_errno (GError **error)
+{
+ int errsv = errno;
+ g_set_error_literal (error, G_IO_ERROR, g_io_error_from_errno (errsv),
+ g_strerror (errsv));
+}
+#endif
+
+/**
+ * gs_console_read_password:
+ * @console: the #GSConsole
+ * @prompt: A string to output before reading the password
+ * @error: a #GError
+ *
+ * Write @prompt to standard output, then switch output echo off, read
+ * a result string, then switch output echo back on.
+ *
+ * Returns: A string, or %NULL on error
+ */
+char *
+gs_console_read_password (GSConsole *console,
+ const char *prompt,
+ GCancellable *cancellable,
+ GError **error)
+{
+#ifdef G_OS_UNIX
+ gboolean ret = FALSE;
+ /* This code is modified from that found in
+ * polkit/src/polkittextagentlistener.c, reused under the LGPL v2.1
+ */
+ int res;
+ struct termios ts, ots;
+ GInputStream *in;
+ GOutputStream *out;
+ GString *str = NULL;
+ gsize bytes_written;
+ gboolean reset_terminal = FALSE;
+
+ in = gs_console_get_stdin ();
+ out = gs_console_get_stdout ();
+
+ if (!g_output_stream_write_all (out, prompt, strlen (prompt), &bytes_written,
+ cancellable, error))
+ goto out;
+ if (!g_output_stream_flush (out, cancellable, error))
+ goto out;
+
+ /* TODO: We really ought to block SIGINT and STGSTP (and probably
+ * other signals too) so we can restore the terminal (since we
+ * turn off echoing). See e.g. Advanced Programming in the
+ * UNIX Environment 2nd edition (Steves and Rago) section
+ * 18.10, pg 660 where this is suggested. See also various
+ * getpass(3) implementations
+ *
+ * However, since we are a library routine the user could have
+ * multiple threads - in fact, typical usage of
+ * PolkitAgentTextListener is to run it in a thread. And
+ * unfortunately threads and POSIX signals is a royal PITA.
+ *
+ * Maybe we could fork(2) and ask for the password in the
+ * child and send it back to the parent over a pipe? (we are
+ * guaranteed that there is only one thread in the child
+ * process).
+ *
+ * (Side benefit of doing this in a child process is that we
+ * could avoid blocking the thread where the
+ * PolkitAgentTextListener object is being serviced from. But
+ * since this class is normally used in a dedicated thread
+ * it doesn't really matter *anyway*.)
+ *
+ * Anyway, On modern Linux not doing this doesn't seem to be a
+ * problem - looks like modern shells restore echoing anyway
+ * on the first input. So maybe it's not even worth solving
+ * the problem.
+ */
+
+ do
+ res = tcgetattr (1, &ts);
+ while (G_UNLIKELY (res == -1 && errno == EINTR));
+ if (res == -1)
+ {
+ _set_error_from_errno (error);
+ goto out;
+ }
+ ots = ts;
+ ts.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);
+ do
+ res = tcsetattr (1, TCSAFLUSH, &ts);
+ while (G_UNLIKELY (res == -1 && errno == EINTR));
+ if (res == -1)
+ {
+ _set_error_from_errno (error);
+ goto out;
+ }
+
+ /* After this point, we'll need to clean up the terminal in case of
+ * error.
+ */
+ reset_terminal = TRUE;
+
+ str = g_string_new (NULL);
+ while (TRUE)
+ {
+ gssize bytes_read;
+ guint8 buf[1];
+
+ /* FIXME - we should probably be converting from the system
+ * codeset, in case it's not UTF-8.
+ */
+ bytes_read = g_input_stream_read (in, buf, sizeof (buf),
+ cancellable, error);
+ if (bytes_read < 0)
+ goto out;
+ else if (bytes_read == 0)
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_CLOSED,
+ "End of stream while reading password");
+ goto out;
+ }
+ else if (buf[0] == '\n')
+ {
+ break;
+ }
+ else
+ {
+ g_string_append_c (str, buf[0]);
+ }
+ }
+
+ ret = TRUE;
+ out:
+ if (reset_terminal)
+ {
+ do
+ res = tcsetattr (1, TCSAFLUSH, &ots);
+ while (G_UNLIKELY (res == -1 && errno == EINTR));
+ if (res == -1)
+ {
+ _set_error_from_errno (error);
+ g_string_free (str, TRUE);
+ return NULL;
+ }
+ }
+
+ if (!ret)
+ {
+ g_string_free (str, TRUE);
+ return NULL;
+ }
+ else
+ {
+ return g_string_free (str, FALSE);
+ }
+#else
+ g_error ("not implemented");
+#endif
+}
+
+/**
+ * gs_console_begin_status_line:
+ * @console: the #GSConsole
+ * @line: String to output
+ *
+ * The primary use case for this function is to output periodic
+ * "status" or "progress" information. The first time this function
+ * is called, @line will be output normally. Subsequent invocations
+ * will overwrite the previous.
+ *
+ * You must invoke gs_console_end_status_line() to return the console
+ * to normal mode. In particular, concurrent use of this function and
+ * the stream returned by gs_console_get_stdout() results in undefined
+ * behavior.
+ */
+gboolean
+gs_console_begin_status_line (GSConsole *console,
+ const char *line,
+ GCancellable *cancellable,
+ GError **error)
+{
+#ifdef G_OS_UNIX
+ gboolean ret = FALSE;
+ gsize linelen;
+ GOutputStream *out;
+ gsize bytes_written;
+
+ out = gs_console_get_stdout ();
+
+ if (!console->in_status_line)
+ {
+ guint8 buf[3] = { (guint8)'\n', 0x1B, 0x37 };
+ if (!g_output_stream_write_all (out, buf, sizeof (buf), &bytes_written,
+ cancellable, error))
+ goto out;
+ console->in_status_line = TRUE;
+ console->last_line_written = -1;
+ }
+
+ {
+ guint8 buf[2] = { 0x1B, 0x38 };
+ if (!g_output_stream_write_all (out, buf, sizeof (buf), &bytes_written,
+ cancellable, error))
+ goto out;
+ }
+
+ linelen = strlen (line);
+ if (!g_output_stream_write_all (out, line, linelen, &bytes_written,
+ cancellable, error))
+ goto out;
+
+ /* Now we need to pad with spaces enough to overwrite our last line
+ */
+ if (console->last_line_written >= 0
+ && linelen < (gsize) console->last_line_written)
+ {
+ gsize towrite = console->last_line_written - linelen;
+ const char c = ' ';
+ while (towrite > 0)
+ {
+ if (!g_output_stream_write_all (out, &c, 1, &bytes_written,
+ cancellable, error))
+ goto out;
+ towrite--;
+ }
+ }
+
+ console->last_line_written = linelen;
+
+ ret = TRUE;
+ out:
+ return ret;
+#else
+ g_error ("not implemented");
+#endif
+}
+
+/**
+ * gs_console_end_status_line:
+ * @console: the #GSConsole
+ *
+ * Complete a series of invocations of gs_console_begin_status_line(),
+ * returning the stream to normal mode. The last printed status line
+ * remains on the console; if this is not desired, print an empty
+ * string to clear it before invoking this function.
+ */
+gboolean
+gs_console_end_status_line (GSConsole *console,
+ GCancellable *cancellable,
+ GError **error)
+{
+#ifdef G_OS_UNIX
+ gboolean ret = FALSE;
+ GOutputStream *out;
+ gsize bytes_written;
+ char c = '\n';
+
+ g_return_val_if_fail (console->in_status_line, FALSE);
+
+ out = gs_console_get_stdout ();
+
+ if (!g_output_stream_write_all (out, &c, 1, &bytes_written,
+ cancellable, error))
+ goto out;
+
+ console->in_status_line = FALSE;
+
+ ret = TRUE;
+ out:
+ return ret;
+#else
+ g_error ("not implemented");
+#endif
+}