[gnome-builder] util: add PTY interceptor
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-builder] util: add PTY interceptor
- Date: Tue, 23 Jan 2018 04:57:55 +0000 (UTC)
commit fb094eb575374bc8712f7ebde8fc150d2ffa808f
Author: Christian Hergert <chergert redhat com>
Date: Mon Jan 22 19:01:44 2018 -0800
util: add PTY interceptor
This adds a new pty_interceptor_t that can be used to intercept output from
a PTY and proxy it along, while also allowing snooping on the content. We
can use this to extract error regexes.
src/libide/util/meson.build | 1 +
src/libide/util/ptyintercept.c | 560 +++++++++++++++++++++++++++++++++++++++++
src/libide/util/ptyintercept.h | 90 +++++++
3 files changed, 651 insertions(+)
---
diff --git a/src/libide/util/meson.build b/src/libide/util/meson.build
index 1a5539543..ddba1c9e8 100644
--- a/src/libide/util/meson.build
+++ b/src/libide/util/meson.build
@@ -32,6 +32,7 @@ util_private_sources = [
'ide-battery-monitor.c',
'ide-doc-seq.c',
'ide-window-settings.c',
+ 'ptyintercept.c',
]
libide_public_headers += files(util_headers)
diff --git a/src/libide/util/ptyintercept.c b/src/libide/util/ptyintercept.c
new file mode 100644
index 000000000..072425784
--- /dev/null
+++ b/src/libide/util/ptyintercept.c
@@ -0,0 +1,560 @@
+/* ptyintercept.c
+ *
+ * Copyright (C) 2018 Christian Hergert
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef _GNU_SOURCE
+# define _GNU_SOURCE
+#endif
+
+#include <errno.h>
+#include <fcntl.h>
+#include <glib-unix.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include "ptyintercept.h"
+
+/*
+ * We really don't need all that much. A PTY on Linux has a some amount of
+ * kernel memory that is non-pageable and therefore small in size. 4k is what
+ * it appears to be. Anything more than that is really just an opportunity for
+ * us to break some deadlock scenarios.
+ */
+#define CHANNEL_BUFFER_SIZE (4096 * 4)
+
+#define PTY_INTERCEPT_MAGIC (0x81723647)
+#define PTY_IS_INTERCEPT(s) ((s) != NULL && (s)->magic == PTY_INTERCEPT_MAGIC)
+
+static void _pty_intercept_side_close (pty_intercept_side_t *side);
+static gboolean _pty_intercept_in_cb (GIOChannel *channel,
+ GIOCondition condition,
+ gpointer user_data);
+static gboolean _pty_intercept_out_cb (GIOChannel *channel,
+ GIOCondition condition,
+ gpointer user_data);
+static void clear_source (guint *source_id);
+
+static gboolean
+_pty_intercept_set_raw (pty_fd_t fd)
+{
+ struct termios t;
+
+ if (tcgetattr (fd, &t) == -1)
+ return FALSE;
+
+ t.c_lflag &= ~(ICANON | ISIG | IEXTEN | ECHO);
+ t.c_iflag &= ~(BRKINT | ICRNL | IGNBRK | IGNCR | INLCR | INPCK | ISTRIP | IXON | PARMRK);
+ t.c_oflag &= ~(OPOST);
+ t.c_cc[VMIN] = 1;
+ t.c_cc[VTIME] = 0;
+
+ if (tcsetattr (fd, TCSAFLUSH, &t) == -1)
+ return FALSE;
+
+ return TRUE;
+}
+
+/**
+ * pty_intercept_create_slave:
+ * @master_fd: a pty master
+ *
+ * This creates a new slave to the PTY master @master_fd.
+ *
+ * This uses grantpt(), unlockpt(), and ptsname() to open a new
+ * PTY slave.
+ *
+ * Returns: a FD for the slave PTY that should be closed with close().
+ * Upon error, %PTY_FD_INVALID (-1) is returned.
+ */
+pty_fd_t
+pty_intercept_create_slave (pty_fd_t master_fd)
+{
+ g_auto(pty_fd_t) ret = PTY_FD_INVALID;
+ gchar name[256];
+
+ g_assert (master_fd != -1);
+
+ if (grantpt (master_fd) != 0)
+ return PTY_FD_INVALID;
+
+ if (unlockpt (master_fd) != 0)
+ return PTY_FD_INVALID;
+
+ if (ptsname_r (master_fd, name, sizeof name - 1) != 0)
+ return PTY_FD_INVALID;
+
+ name[sizeof name - 1] = '\0';
+
+ ret = open (name, O_RDWR | O_CLOEXEC | O_NONBLOCK);
+
+ if (ret == PTY_FD_INVALID && errno == EINVAL)
+ {
+ gint flags;
+
+ ret = open (name, O_RDWR | O_CLOEXEC);
+ if (ret == PTY_FD_INVALID && errno == EINVAL)
+ ret = open (name, O_RDWR | O_CLOEXEC);
+
+ if (ret == PTY_FD_INVALID)
+ return PTY_FD_INVALID;
+
+ flags = fcntl (ret, F_GETFD, 0);
+ flags |= O_NONBLOCK | O_CLOEXEC;
+
+ if (fcntl (ret, F_SETFD, flags) < 0)
+ return PTY_FD_INVALID;
+ }
+
+ return pty_fd_steal (&ret);
+}
+
+/**
+ * pty_intercept_create_master:
+ *
+ * Creates a new PTY master using posix_openpt(). Some fallbacks are
+ * provided for non-Linux systems where O_CLOEXEC and O_NONBLOCK may
+ * not be supported.
+ *
+ * Returns: a FD that should be closed with close() if successful.
+ * Upon error, %PTY_FD_INVALID (-1) is returned.
+ */
+pty_fd_t
+pty_intercept_create_master (void)
+{
+ g_auto(pty_fd_t) master_fd = PTY_FD_INVALID;
+
+ master_fd = posix_openpt (O_RDWR | O_NOCTTY | O_NONBLOCK | O_CLOEXEC);
+
+#ifndef __linux__
+ /* Fallback for operating systems that don't support
+ * O_NONBLOCK and O_CLOEXEC when opening.
+ */
+ if (master_fd == PTY_FD_INVALID && errno == EINVAL)
+ {
+ gint new_flags = O_NONBLOCK;
+ gint flags;
+
+ master_fd = posix_openpt (O_RDWR | O_NOCTTY | O_CLOEXEC);
+
+ if (master_fd == PTY_FD_INVALID && errno == EINVAL)
+ {
+ master_fd = posix_openpt (O_RDWR | O_NOCTTY);
+ new_flags |= O_CLOEXEC;
+ if (master_fd == -1)
+ return PTY_FD_INVALID;
+ }
+
+ flags = fcntl (master_fd, F_GETFD, 0);
+ if (flags < 0)
+ return PTY_FD_INVALID;
+
+ if (fcntl (master_fd, F_SETFD, flags | new_flags) < 0)
+ return PTY_FD_INVALID;
+ }
+#endif
+
+ return pty_fd_steal (&master_fd);
+}
+
+static void
+clear_source (guint *source_id)
+{
+ guint id = *source_id;
+ *source_id = 0;
+ if (id != 0)
+ g_source_remove (id);
+}
+
+static void
+_pty_intercept_side_close (pty_intercept_side_t *side)
+{
+ g_assert (side != NULL);
+
+ clear_source (&side->in_watch);
+ clear_source (&side->out_watch);
+ g_clear_pointer (&side->channel, g_io_channel_unref);
+ g_clear_pointer (&side->out_bytes, g_bytes_unref);
+}
+
+static gboolean
+_pty_intercept_out_cb (GIOChannel *channel,
+ GIOCondition condition,
+ gpointer user_data)
+{
+ pty_intercept_t *self = user_data;
+ pty_intercept_side_t *us, *them;
+ GIOStatus status;
+ const gchar *wrbuf;
+ gsize n_written = 0;
+ gsize len = 0;
+
+ g_assert (channel != NULL);
+ g_assert (condition & (G_IO_ERR | G_IO_HUP | G_IO_OUT));
+
+ if (channel == self->master.channel)
+ {
+ us = &self->master;
+ them = &self->slave;
+ }
+ else
+ {
+ us = &self->slave;
+ them = &self->master;
+ }
+
+ if ((condition & G_IO_OUT) == 0 ||
+ us->out_bytes == NULL ||
+ us->channel == NULL ||
+ them->channel == NULL)
+ goto close_and_cleanup;
+
+ wrbuf = g_bytes_get_data (us->out_bytes, &len);
+ status = g_io_channel_write_chars (us->channel, wrbuf, len, &n_written, NULL);
+ if (status != G_IO_STATUS_NORMAL)
+ goto close_and_cleanup;
+
+ g_assert (n_written > 0);
+ g_assert (them->in_watch == 0);
+
+ /*
+ * If we didn't write all of our data, wait until another G_IO_OUT
+ * condition to write more data.
+ */
+ if (n_written < len)
+ {
+ g_autoptr(GBytes) bytes = g_steal_pointer (&us->out_bytes);
+ us->out_bytes = g_bytes_new_from_bytes (bytes, n_written, len - n_written);
+ return G_SOURCE_CONTINUE;
+ }
+
+ g_clear_pointer (&us->out_bytes, g_bytes_unref);
+
+ /*
+ * We wrote all the data to this side, so now we can wait for more
+ * data from the input peer.
+ */
+ us->out_watch = 0;
+ them->in_watch =
+ g_io_add_watch_full (them->channel,
+ G_PRIORITY_DEFAULT,
+ G_IO_IN | G_IO_ERR | G_IO_HUP,
+ _pty_intercept_in_cb,
+ self, NULL);
+
+ return G_SOURCE_REMOVE;
+
+close_and_cleanup:
+
+ _pty_intercept_side_close (us);
+ _pty_intercept_side_close (them);
+
+ return G_SOURCE_REMOVE;
+}
+
+/*
+ * _pty_intercept_in_cb:
+ *
+ * This function is called when we have received a condition that specifies
+ * the channel has data to read. We read that data and then setup a watch
+ * onto the other other side so that we can write that data.
+ *
+ * If the other-side of the of the connection can write, then we write
+ * that data immediately.
+ *
+ * The in watch is disabled until we have completed the write.
+ */
+static gboolean
+_pty_intercept_in_cb (GIOChannel *channel,
+ GIOCondition condition,
+ gpointer user_data)
+{
+ pty_intercept_t *self = user_data;
+ pty_intercept_side_t *us, *them;
+ GIOStatus status = G_IO_STATUS_AGAIN;
+ gchar buf[4096];
+ gchar *wrbuf = buf;
+ gsize n_read;
+
+ g_assert (channel != NULL);
+ g_assert (condition & (G_IO_ERR | G_IO_HUP | G_IO_IN));
+ g_assert (PTY_IS_INTERCEPT (self));
+
+ if (channel == self->master.channel)
+ {
+ us = &self->master;
+ them = &self->slave;
+ }
+ else
+ {
+ us = &self->slave;
+ them = &self->master;
+ }
+
+ g_assert (us->in_watch != 0);
+ g_assert (them->out_watch == 0);
+
+ if (condition & (G_IO_ERR | G_IO_HUP) || us->channel == NULL || them->channel == NULL)
+ goto close_and_cleanup;
+
+ g_assert (condition & G_IO_IN);
+
+ while (status == G_IO_STATUS_AGAIN)
+ {
+ n_read = 0;
+ status = g_io_channel_read_chars (us->channel, buf, sizeof buf, &n_read, NULL);
+ }
+
+ if (status == G_IO_STATUS_EOF)
+ goto close_and_cleanup;
+
+ if (n_read > 0 && us->callback != NULL)
+ us->callback (self, us, (const guint8 *)buf, n_read, us->callback_data);
+
+ while (n_read > 0)
+ {
+ gsize n_written = 0;
+
+ status = g_io_channel_write_chars (them->channel, buf, n_read, &n_written, NULL);
+
+ wrbuf += n_written;
+ n_read -= n_written;
+
+ if (n_read > 0 && status == G_IO_STATUS_AGAIN)
+ {
+ /* If we get G_IO_STATUS_AGAIN here, then we are in a situation where
+ * the other side is not in a position to handle the data. We need to
+ * setup a G_IO_OUT watch on the FD to wait until things are writeable.
+ *
+ * We'll cancel our G_IO_IN condition, and wait for the out condition
+ * to make forward progress.
+ */
+ them->out_bytes = g_bytes_new (wrbuf, n_read);
+ them->out_watch = g_io_add_watch_full (them->channel,
+ G_PRIORITY_DEFAULT,
+ G_IO_OUT | G_IO_ERR | G_IO_HUP,
+ _pty_intercept_out_cb,
+ self, NULL);
+ us->in_watch = 0;
+
+ return G_SOURCE_REMOVE;
+ }
+
+ if (status != G_IO_STATUS_NORMAL)
+ goto close_and_cleanup;
+
+ g_io_channel_flush (them->channel, NULL);
+ }
+
+ return G_SOURCE_CONTINUE;
+
+close_and_cleanup:
+
+ _pty_intercept_side_close (us);
+ _pty_intercept_side_close (them);
+
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * pty_intercept_set_size:
+ *
+ * Proxies a winsize across to the inferior. If the PTY is the
+ * controlling PTY for the process, then SIGWINCH will be signaled
+ * in the inferior process.
+ *
+ * Since we can't track SIGWINCH cleanly in here, we rely on the
+ * external consuming program to notify us of SIGWINCH so that we
+ * can copy the new size across.
+ */
+gboolean
+pty_intercept_set_size (pty_intercept_t *self,
+ guint rows,
+ guint columns)
+{
+
+ g_return_val_if_fail (PTY_IS_INTERCEPT (self), FALSE);
+
+ if (self->master.channel != NULL)
+ {
+ pty_fd_t fd = g_io_channel_unix_get_fd (self->master.channel);
+ struct winsize ws = {0};
+
+ ws.ws_col = columns;
+ ws.ws_row = rows;
+
+ return ioctl (fd, TIOCSWINSZ, &ws) == 0;
+ }
+
+ return FALSE;
+}
+
+/**
+ * pty_intercept_init:
+ * @self: a location of memory to store a #pty_intercept_t
+ * @fd: the PTY master fd, possibly from a #VtePty
+ * @main_context: (nullable): a #GMainContext or %NULL for thread-default
+ *
+ * Creates a enw #pty_intercept_t using the PTY master fd @fd.
+ *
+ * A new PTY slave is created that will communicate with @fd.
+ * Additionally, a new PTY master is created that can communicate
+ * with another side, and will pass that information to @fd after
+ * extracting any necessary information.
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE
+ */
+gboolean
+pty_intercept_init (pty_intercept_t *self,
+ int fd,
+ GMainContext *main_context)
+{
+ g_auto(pty_fd_t) slave_fd = PTY_FD_INVALID;
+ g_auto(pty_fd_t) master_fd = PTY_FD_INVALID;
+ struct winsize ws;
+
+ g_return_val_if_fail (self != NULL, FALSE);
+ g_return_val_if_fail (fd != -1, FALSE);
+
+ memset (self, 0, sizeof *self);
+ self->magic = PTY_INTERCEPT_MAGIC;
+
+ slave_fd = pty_intercept_create_slave (fd);
+ if (slave_fd == PTY_FD_INVALID)
+ return FALSE;
+
+ /* Do not perform additional processing on the slave_fd created
+ * from the master we were provided. Otherwise, it will be happening
+ * twice instead of just once.
+ */
+ if (!_pty_intercept_set_raw (slave_fd))
+ return FALSE;
+
+ master_fd = pty_intercept_create_master ();
+ if (master_fd == PTY_FD_INVALID)
+ return FALSE;
+
+ /* Copy the win size across */
+ if (ioctl (slave_fd, TIOCGWINSZ, &ws) >= 0)
+ ioctl (master_fd, TIOCSWINSZ, &ws);
+
+ if (main_context == NULL)
+ main_context = g_main_context_get_thread_default ();
+
+ self->master.channel = g_io_channel_unix_new (pty_fd_steal (&master_fd));
+ self->slave.channel = g_io_channel_unix_new (pty_fd_steal (&slave_fd));
+
+ g_io_channel_set_close_on_unref (self->master.channel, TRUE);
+ g_io_channel_set_close_on_unref (self->slave.channel, TRUE);
+
+ g_io_channel_set_encoding (self->master.channel, NULL, NULL);
+ g_io_channel_set_encoding (self->slave.channel, NULL, NULL);
+
+ g_io_channel_set_buffer_size (self->master.channel, CHANNEL_BUFFER_SIZE);
+ g_io_channel_set_buffer_size (self->slave.channel, CHANNEL_BUFFER_SIZE);
+
+ self->master.in_watch =
+ g_io_add_watch_full (self->master.channel,
+ G_PRIORITY_DEFAULT,
+ G_IO_IN | G_IO_ERR | G_IO_HUP,
+ _pty_intercept_in_cb,
+ self, NULL);
+
+ self->slave.in_watch =
+ g_io_add_watch_full (self->slave.channel,
+ G_PRIORITY_DEFAULT,
+ G_IO_IN | G_IO_ERR | G_IO_HUP,
+ _pty_intercept_in_cb,
+ self, NULL);
+
+ return TRUE;
+}
+
+/**
+ * pty_intercept_clear:
+ * @self: a #pty_intercept_t
+ *
+ * Cleans up a #pty_intercept_t previously initialized with
+ * pty_intercept_init().
+ *
+ * This diconnects any #GIOChannel that have been attached and
+ * releases any allocated memory.
+ *
+ * It is invalid to use @self after calling this function.
+ */
+void
+pty_intercept_clear (pty_intercept_t *self)
+{
+ g_return_if_fail (PTY_IS_INTERCEPT (self));
+
+ clear_source (&self->slave.in_watch);
+ clear_source (&self->slave.out_watch);
+ g_clear_pointer (&self->slave.channel, g_io_channel_unref);
+ g_clear_pointer (&self->slave.out_bytes, g_bytes_unref);
+
+ clear_source (&self->master.in_watch);
+ clear_source (&self->master.out_watch);
+ g_clear_pointer (&self->master.channel, g_io_channel_unref);
+ g_clear_pointer (&self->master.out_bytes, g_bytes_unref);
+
+ memset (self, 0, sizeof *self);
+}
+
+/**
+ * pty_intercept_get_fd:
+ * @self: a #pty_intercept_t
+ *
+ * Gets a master PTY fd created by the #pty_intercept_t. This is suitable
+ * to use to create a slave fd which can be passed to a child process.
+ *
+ * Returns: A FD of a PTY master if successful, otherwise -1.
+ */
+pty_fd_t
+pty_intercept_get_fd (pty_intercept_t *self)
+{
+ g_return_val_if_fail (PTY_IS_INTERCEPT (self), PTY_FD_INVALID);
+ g_return_val_if_fail (self->master.channel != NULL, PTY_FD_INVALID);
+
+ return g_io_channel_unix_get_fd (self->master.channel);
+}
+
+/**
+ * pty_intercept_set_callback:
+ * @self: a pty_intercept_t
+ * @side: the side containing the data to watch
+ * @callback: the callback to execute when data is received
+ * @user_data: closure data for @callback
+ *
+ * This sets the callback to execute every time data is received
+ * from a particular side of the intercept.
+ *
+ * You may only set one per side.
+ */
+void
+pty_intercept_set_callback (pty_intercept_t *self,
+ pty_intercept_side_t *side,
+ pty_intercept_callback_t callback,
+ gpointer callback_data)
+{
+ g_return_if_fail (PTY_IS_INTERCEPT (self));
+ g_return_if_fail (side == &self->master || side == &self->slave);
+
+ side->callback = callback;
+ side->callback_data = callback_data;
+}
diff --git a/src/libide/util/ptyintercept.h b/src/libide/util/ptyintercept.h
new file mode 100644
index 000000000..0f41e5dec
--- /dev/null
+++ b/src/libide/util/ptyintercept.h
@@ -0,0 +1,90 @@
+/* ptyintercept.h
+ *
+ * Copyright © 2018 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <unistd.h>
+
+G_BEGIN_DECLS
+
+#define PTY_FD_INVALID (-1)
+
+typedef int pty_fd_t;
+typedef struct _pty_intercept_t pty_intercept_t;
+typedef struct _pty_intercept_side_t pty_intercept_side_t;
+typedef void (*pty_intercept_callback_t) (const pty_intercept_t *intercept,
+ const pty_intercept_side_t *side,
+ const guint8 *data,
+ gsize len,
+ gpointer user_data);
+
+struct _pty_intercept_side_t
+{
+ GIOChannel *channel;
+ guint in_watch;
+ guint out_watch;
+ GBytes *out_bytes;
+ pty_intercept_callback_t callback;
+ gpointer callback_data;
+};
+
+struct _pty_intercept_t
+{
+ gsize magic;
+ pty_intercept_side_t master;
+ pty_intercept_side_t slave;
+};
+
+static inline pty_fd_t
+pty_fd_steal (pty_fd_t *fd)
+{
+ pty_fd_t ret = *fd;
+ *fd = -1;
+ return ret;
+}
+
+static void
+pty_fd_clear (pty_fd_t *fd)
+{
+ if (fd != NULL && *fd != -1)
+ {
+ int rfd = *fd;
+ *fd = -1;
+ close (rfd);
+ }
+}
+
+G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (pty_fd_t, pty_fd_clear)
+
+pty_fd_t pty_intercept_create_master (void);
+pty_fd_t pty_intercept_create_slave (pty_fd_t master_fd);
+gboolean pty_intercept_init (pty_intercept_t *self,
+ pty_fd_t fd,
+ GMainContext *main_context);
+pty_fd_t pty_intercept_get_fd (pty_intercept_t *self);
+gboolean pty_intercept_set_size (pty_intercept_t *self,
+ guint rows,
+ guint columns);
+void pty_intercept_clear (pty_intercept_t *self);
+void pty_intercept_set_callback (pty_intercept_t *self,
+ pty_intercept_side_t *side,
+ pty_intercept_callback_t callback,
+ gpointer user_data);
+
+G_END_DECLS
\ No newline at end of file
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]