diff options
Diffstat (limited to 'libgsystem/gsystem-console.c')
-rw-r--r-- | libgsystem/gsystem-console.c | 443 |
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 +} |