]> git.ipfire.org Git - thirdparty/gettext.git/commitdiff
libtextstyle: Implement reliable tty control.
authorBruno Haible <bruno@clisp.org>
Sun, 17 Mar 2019 13:26:03 +0000 (14:26 +0100)
committerBruno Haible <bruno@clisp.org>
Sun, 17 Mar 2019 15:44:55 +0000 (16:44 +0100)
* gnulib-local/lib/term-ostream.oo.c (DEBUG_SIGNALS): New macro.
Include <stdio.h>, <sys/stat.h>.
(NOFLSH): Define fallback value.
Include sig-handler.h, same-inode.h.
(ASYNC_SAFE): New macro.
(color_bgr): Mark as ASYNC_SAFE.
(nonintr_tcgetattr, nonintr_tcsetattr): New functions.
(log_message, sprintf_integer, simple_errno_string, simple_signal_string,
log_signal_handler_called): New functions.
(struct term_ostream): Add fields restore_colors, restore_weight,
restore_posture, restore_underline, tty_control, same_as_stderr.
(BLOCK_SIGNALS_DURING_NON_DEFAULT_STYLE_OUTPUT): New macro.
(term_fd): New variable.
(pgrp_status_t): New type.
(pgrp_status): New variable.
(update_pgrp_status): New function.
(out_stream): New variable.
(restore_colors, restore_weight, restore_posture, restore_underline): Remove
variables.
(out_color_change, out_bgcolor_change, out_weight_change, out_posture_change,
out_underline_change): New functions, extracted from out_attr_change.
(out_attr_change): Call these functions.
(restore): Use out_stream.
(tcsetattr_failed): New function.
(orig_lflag_set, orig_lflag): New variables.
(clobber_local_mode, restore_local_mode): New functions.
(job_control_signals): Renamed from stopping_signals. Add SIGCONT.
(num_job_control_signals): Renamed from num_stopping_signals.
(relevant_signal_set): Replaces stopping_signal_set.
(relevant_signal_set_initialized): New variable.
(init_relevant_signal_set): Replaces init_stopping_signal_set.
(block_relevant_signals): Replaces block_stopping_signals.
(unblock_relevant_signals): Replaces unblock_stopping_signals.
(is_ignored): New function.
(show_signal_marker): New function.
(fatal_or_stopping_signal_handler, fatal_signal_handler,
stopping_signal_handler, continuing_signal_handler,
ensure_continuing_signal_handler, ensure_other_signal_handlers): New functions.
(activate_non_default_attr, deactivate_non_default_attr): Rewritten.
(term_ostream::free): Unset term_fd and call update_pgrp_status.
(term_ostream_create): Initialize the fields restore_colors, restore_weight,
restore_posture, restore_underline, tty_control, same_as_stderr. Call
init_relevant_signal_set. Set term_fd and call ensure_continuing_signal_handler
and update_pgrp_status.
* gnulib-local/m4/term-ostream.m4 (gl_TERM_OSTREAM): Check for tcgetattr.
* gnulib-local/modules/term-ostream (Depends-on): Add sigaction, same-inode.
* libtextstyle/NEWS: Mention the change.

gnulib-local/lib/term-ostream.oo.c
gnulib-local/m4/term-ostream.m4
gnulib-local/modules/term-ostream
libtextstyle/.gitignore
libtextstyle/NEWS

index 53eb51911c6b5372c8c2ca6af34744343d086cfe..ad837321baca0a4ae750fa57904a453ded07262f 100644 (file)
@@ -20,6 +20,9 @@
 /* Specification.  */
 #include "term-ostream.h"
 
+/* Set to 1 to get debugging output regarding signals.  */
+#define DEBUG_SIGNALS 0
+
 #include <assert.h>
 #include <errno.h>
 #include <signal.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
-#if HAVE_TCDRAIN
+#if DEBUG_SIGNALS
+# include <stdio.h>
+#endif
+#if HAVE_TCGETATTR || HAVE_TCDRAIN
 # include <termios.h>
+# if !defined NOFLSH            /* QNX */
+#  define NOFLSH 0
+# endif
+#endif
+#if HAVE_TCGETATTR
+# include <sys/stat.h>
 #endif
 
 #include "error.h"
 #include "fatal-signal.h"
+#include "sig-handler.h"
 #include "full-write.h"
 #include "terminfo.h"
+#include "same-inode.h"
 #include "xalloc.h"
 #include "xsize.h"
 #include "gettext.h"
@@ -53,6 +67,12 @@ static char tparambuf[100];
 
 #define SIZEOF(a) (sizeof(a) / sizeof(a[0]))
 
+/* Restriction for code executed in signal handlers:
+   - Such code must be marked ASYNC_SAFE.
+   - Such code must only access memory locations (variables, struct fields) that
+     are marked 'volatile'.  */
+#define ASYNC_SAFE
+
 
 /* =========================== Color primitives =========================== */
 
@@ -311,7 +331,7 @@ rgb_to_color_common8 (int r, int g, int b)
 /* Convert a cm_common8 color in RGB encoding to BGR encoding.
    See the ncurses terminfo(5) manual page, section "Color Handling", for an
    explanation why this is needed.  */
-static inline int
+static ASYNC_SAFE inline int
 color_bgr (term_color_t color)
 {
   return ((color & 4) >> 2) | (color & 2) | ((color & 1) << 2);
@@ -937,15 +957,54 @@ typedef struct
   BITFIELD_TYPE(term_underline_t, unsigned int) underline : 1;
 } attributes_t;
 
+/* Compare two sets of attributes for equality.  */
+static inline bool
+equal_attributes (attributes_t attr1, attributes_t attr2)
+{
+  return (attr1.color == attr2.color
+          && attr1.bgcolor == attr2.bgcolor
+          && attr1.weight == attr2.weight
+          && attr1.posture == attr2.posture
+          && attr1.underline == attr2.underline);
+}
 
-/* ============================ EINTR handling ============================ */
 
-#if HAVE_TCDRAIN
+/* ============================ EINTR handling ============================ */
 
-/* EINTR handling for tcdrain().
-   This function can return -1/EINTR even though we don't have any
+/* EINTR handling for tcgetattr(), tcsetattr(), tcdrain().
+   These functions can return -1/EINTR even when we don't have any
    signal handlers set up, namely when we get interrupted via SIGSTOP.  */
 
+#if HAVE_TCGETATTR
+
+static inline int
+nonintr_tcgetattr (int fd, struct termios *tcp)
+{
+  int retval;
+
+  do
+    retval = tcgetattr (fd, tcp);
+  while (retval < 0 && errno == EINTR);
+
+  return retval;
+}
+
+static inline int
+nonintr_tcsetattr (int fd, int flush_mode, const struct termios *tcp)
+{
+  int retval;
+
+  do
+    retval = tcsetattr (fd, flush_mode, tcp);
+  while (retval < 0 && errno == EINTR);
+
+  return retval;
+}
+
+#endif
+
+#if HAVE_TCDRAIN
+
 static inline int
 nonintr_tcdrain (int fd)
 {
@@ -961,6 +1020,151 @@ nonintr_tcdrain (int fd)
 #endif
 
 
+/* ========================== Logging primitives ========================== */
+
+/* We need logging, especially for the signal handling, because
+     - Debugging through gdb is hardly possible, because gdb produces output
+       by itself and interferes with the process states.
+     - strace is buggy when it comes to SIGTSTP handling:  By default, it
+       sends the process a SIGSTOP signal instead of SIGTSTP.  It supports
+       an option '-D -I4' to mitigate this, though.  Also, race conditions
+       appear with different probability with and without strace.
+   fprintf(stderr) is not possible within async-safe code, because fprintf()
+   may invoke malloc().  */
+
+#if DEBUG_SIGNALS
+
+/* Log a simple message.  */
+static ASYNC_SAFE void
+log_message (const char *message)
+{
+  full_write (STDERR_FILENO, message, strlen (message));
+}
+
+#else
+
+# define log_message(message)
+
+#endif
+
+#if HAVE_TCGETATTR || DEBUG_SIGNALS
+
+/* Async-safe implementation of sprintf (str, "%d", n).  */
+static ASYNC_SAFE void
+sprintf_integer (char *str, int x)
+{
+  unsigned int y;
+  char buf[20];
+  char *p;
+  size_t n;
+
+  if (x < 0)
+    {
+      *str++ = '-';
+      y = (unsigned int) (-1 - x) + 1;
+    }
+  else
+    y = x;
+
+  p = buf + sizeof (buf);
+  do
+    {
+      *--p = '0' + (y % 10);
+      y = y / 10;
+    }
+  while (y > 0);
+  n = buf + sizeof (buf) - p;
+  memcpy (str, p, n);
+  str[n] = '\0';
+}
+
+#endif
+
+#if HAVE_TCGETATTR
+
+/* Async-safe conversion of errno value to string.  */
+static ASYNC_SAFE void
+simple_errno_string (char *str, int errnum)
+{
+  switch (errnum)
+    {
+    case EBADF:  strcpy (str, "EBADF"); break;
+    case EINTR:  strcpy (str, "EINTR"); break;
+    case EINVAL: strcpy (str, "EINVAL"); break;
+    case EIO:    strcpy (str, "EIO"); break;
+    case ENOTTY: strcpy (str, "ENOTTY"); break;
+    default: sprintf_integer (str, errnum); break;
+    }
+}
+
+#endif
+
+#if DEBUG_SIGNALS
+
+/* Async-safe conversion of signal number to name.  */
+static ASYNC_SAFE void
+simple_signal_string (char *str, int sig)
+{
+  switch (sig)
+    {
+    /* Fatal signals (see fatal-signal.c).  */
+    #ifdef SIGINT
+    case SIGINT:   strcpy (str, "SIGINT"); break;
+    #endif
+    #ifdef SIGTERM
+    case SIGTERM:  strcpy (str, "SIGTERM"); break;
+    #endif
+    #ifdef SIGHUP
+    case SIGHUP:   strcpy (str, "SIGHUP"); break;
+    #endif
+    #ifdef SIGPIPE
+    case SIGPIPE:  strcpy (str, "SIGPIPE"); break;
+    #endif
+    #ifdef SIGXCPU
+    case SIGXCPU:  strcpy (str, "SIGXCPU"); break;
+    #endif
+    #ifdef SIGXFSZ
+    case SIGXFSZ:  strcpy (str, "SIGXFSZ"); break;
+    #endif
+    #ifdef SIGBREAK
+    case SIGBREAK: strcpy (str, "SIGBREAK"); break;
+    #endif
+    /* Stopping signals.  */
+    #ifdef SIGTSTP
+    case SIGTSTP:  strcpy (str, "SIGTSTP"); break;
+    #endif
+    #ifdef SIGTTIN
+    case SIGTTIN:  strcpy (str, "SIGTTIN"); break;
+    #endif
+    #ifdef SIGTTOU
+    case SIGTTOU:  strcpy (str, "SIGTTOU"); break;
+    #endif
+    /* Continuing signals.  */
+    #ifdef SIGCONT
+    case SIGCONT:  strcpy (str, "SIGCONT"); break;
+    #endif
+    default: sprintf_integer (str, sig); break;
+    }
+}
+
+/* Emit a message that a given signal handler is being run.  */
+static ASYNC_SAFE void
+log_signal_handler_called (int sig)
+{
+  char message[100];
+  strcpy (message, "Signal handler for signal ");
+  simple_signal_string (message + strlen (message), sig);
+  strcat (message, " called.\n");
+  log_message (message);
+}
+
+#else
+
+# define log_signal_handler_called(sig)
+
+#endif
+
+
 /* ============================ term_ostream_t ============================ */
 
 struct term_ostream : struct ostream
@@ -973,27 +1177,37 @@ fields:
   char *filename;
   /* Values from the terminal type's terminfo/termcap description.
      See terminfo(5) for details.  */
-                                /* terminfo  termcap */
-  int max_colors;               /* colors    Co */
-  int no_color_video;           /* ncv       NC */
-  char *set_a_foreground;       /* setaf     AF */
-  char *set_foreground;         /* setf      Sf */
-  char *set_a_background;       /* setab     AB */
-  char *set_background;         /* setb      Sb */
-  char *orig_pair;              /* op        op */
-  char *enter_bold_mode;        /* bold      md */
-  char *enter_italics_mode;     /* sitm      ZH */
-  char *exit_italics_mode;      /* ritm      ZR */
-  char *enter_underline_mode;   /* smul      us */
-  char *exit_underline_mode;    /* rmul      ue */
-  char *exit_attribute_mode;    /* sgr0      me */
+                                         /* terminfo  termcap */
+  int max_colors;                        /* colors    Co */
+  int no_color_video;                    /* ncv       NC */
+  char * volatile set_a_foreground;      /* setaf     AF */
+  char * volatile set_foreground;        /* setf      Sf */
+  char * volatile set_a_background;      /* setab     AB */
+  char * volatile set_background;        /* setb      Sb */
+  char *orig_pair;                       /* op        op */
+  char * volatile enter_bold_mode;       /* bold      md */
+  char * volatile enter_italics_mode;    /* sitm      ZH */
+  char *exit_italics_mode;               /* ritm      ZR */
+  char * volatile enter_underline_mode;  /* smul      us */
+  char *exit_underline_mode;             /* rmul      ue */
+  char *exit_attribute_mode;             /* sgr0      me */
   /* Inferred values.  */
-  bool supports_foreground;
-  bool supports_background;
-  colormodel_t colormodel;
-  bool supports_weight;
-  bool supports_posture;
-  bool supports_underline;
+  bool volatile supports_foreground;
+  bool volatile supports_background;
+  colormodel_t volatile colormodel;
+  bool volatile supports_weight;
+  bool volatile supports_posture;
+  bool volatile supports_underline;
+  /* Inferred values for the exit handler and the signal handlers.  */
+  const char * volatile restore_colors;
+  const char * volatile restore_weight;
+  const char * volatile restore_posture;
+  const char * volatile restore_underline;
+  /* Signal handling and tty control.  */
+  ttyctl_t volatile tty_control;
+  #if HAVE_TCGETATTR
+  bool volatile same_as_stderr;
+  #endif
   /* Variable state, representing past output.  */
   attributes_t default_attr;    /* Default simplified attributes of the
                                    terminal.  */
@@ -1043,17 +1257,201 @@ simplify_attributes (term_ostream_t stream, attributes_t attr)
   return attr;
 }
 
-/* While a line is being output, we need to be careful to restore the
-   terminal's settings in case of a fatal signal or an exit() call.  */
+/* There are several situations which can cause garbled output on the terminal's
+   screen:
+   (1) When the program calls exit() after calling flush_to_current_style,
+       the program would terminate and leave the terminal in a non-default
+       state.
+   (2) When the program is interrupted through a fatal signal, the terminal
+       would be left in a non-default state.
+   (3) When the program is stopped through a stopping signal, the terminal
+       would be left (for temporary use by other programs) in a non-default
+       state.
+   (4) When a foreground process receives a SIGINT, the kernel(!) prints '^C'.
+       On Linux, the place where this happens is
+         linux-5.0/drivers/tty/n_tty.c:713..730
+       within a call sequence
+         n_tty_receive_signal_char (n_tty.c:1245..1246)
+         -> commit_echoes (n_tty.c:792)
+         -> __process_echoes (n_tty.c:713..730).
+   (5) When a signal is sent, the output buffer is cleared.
+       On Linux, this output buffer consists of the "echo buffer" in the tty
+       and the "output buffer" in the driver.  The place where this happens is
+         linux-5.0/drivers/tty/n_tty.c:1133..1140
+       within a call
+         isig (n_tty.c:1133..1140).
+
+   How do we mitigate these problems?
+   (1) We install an exit handler that restores the terminal to the default
+       state.
+   (2) If tty_control is TTYCTL_PARTIAL or TTYCTL_FULL:
+       For some of the fatal signals (see gnulib's 'fatal-signal' module for
+       the precise list), we install a handler that attempts to restore the
+       terminal to the default state.  Since the terminal may be in the middle
+       of outputting an escape sequence at this point, the first escape
+       sequence emitted from this handler may have no effect and produce
+       garbled characters instead.  Therefore the handler outputs the cleanup
+       sequence twice.
+       For the other fatal signals, we don't do anything.
+   (3) If tty_control is TTYCTL_PARTIAL or TTYCTL_FULL:
+       For some of the stopping signals (SIGTSTP, SIGTTIN, SIGTTOU), we install
+       a handler that attempts to restore the terminal to the default state.
+       For SIGCONT, we install a handler that does the opposite: it puts the
+       terminal into the desired state again.
+       For SIGSTOP, we cannot do anything.
+   (4) If tty_control is TTYCTL_FULL:
+       The kernel's action depends on L_ECHO(tty) and L_ISIG(tty), that is, on
+       the local modes of the tty (see
+       <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap11.html>
+       section 11.2.5).  We don't want to change L_ISIG; hence we change L_ECHO.
+       So, we disable the ECHO local flag of the tty; the equivalent command is
+       'stty -echo'.
+   (5) If tty_control is TTYCTL_FULL:
+       The kernel's action depends on !L_NOFLSH(tty), that is, again on the
+       local modes of the tty (see
+       <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap11.html>
+       section 11.2.5).  So, we enable the NOFLSH local flag of the tty; the
+       equivalent command is 'stty noflsh'.
+       For terminals with a baud rate < 9600 this is suboptimal.  For this case
+       - where the traditional flushing behaviour makes sense - we would use a
+       technique that involves tcdrain(), TIOCOUTQ, and usleep() when it is OK
+       to disable NOFLSH.
+
+   Regarding (4) and (5), there is a complication: Changing the local modes is
+   done through tcsetattr().  However, when the process is put into the
+   background, tcsetattr() does not operate the same way as when the process is
+   running in the foreground.
+   To test this kind of behaviour, use the 'color-filter' example like this:
+     $ yes | ./filter '.*'
+     <Ctrl-Z>
+     $ bg 1
+   We have three possible implementation options:
+     * If we don't ignore the signal SIGTTOU:
+       If the TOSTOP bit in the terminal's local mode is clear (command
+       equivalent: 'stty -tostop') and the process is put into the background,
+       normal output would continue (per POSIX
+       <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap11.html>
+       section 11.2.5) but tcsetattr() calls would cause it to stop due to
+       a SIGTTOU signal (per POSIX
+       <https://pubs.opengroup.org/onlinepubs/9699919799/functions/tcsetattr.html>).
+       Thus, the program would behave differently with term-ostream than
+       without.
+     * If we ignore the signal SIGTTOU when the TOSTOP bit in the terminal's
+       local mode is clear (i.e. when (tc.c_lflag & TOSTOP) == 0):
+       The tcsetattr() calls do not stop the process, but they don't have the
+       desired effect.
+       On Linux, when I put the process into the background and then kill it with
+       signal SIGINT, I can see that the last operation on the terminal settings
+       (as shown by 'strace') is
+         ioctl(1, TCSETSW, {B38400 opost isig icanon echo ...}) = 0
+       and yet, once the process is terminated, the terminal settings contain
+       '-echo', not 'echo'.
+     * Don't call tcsetattr() if the process is not in the foreground.
+       This approach produces reliable results.
+
+   Blocking some signals while a non-default style is active is *not* useful:
+     - It does not help against (1), since exit() is not a signal.
+     - Signal handlers are the better approach against (2) and (3).
+     - It does not help against (4) and (5), because the kernel's actions happen
+       outside the process.  */
+#define BLOCK_SIGNALS_DURING_NON_DEFAULT_STYLE_OUTPUT 0
+
+/* File descriptor of the currently open term_ostream_t.  */
+static int volatile term_fd = -1;
+
+#if HAVE_TCGETATTR
+
+/* Status of the process group of term_fd.  */
+typedef enum
+{
+  PGRP_UNKNOWN = 0,     /* term_fd < 0.  Unknown status.  */
+  PGRP_NO_TTY,          /* term_fd >= 0 but is not connected to a tty.  */
+  PGRP_IN_FOREGROUND,   /* term_fd >= 0 is a tty.  This process is running in
+                           the foreground.  */
+  PGRP_IN_BACKGROUND    /* term_fd >= 0 is a tty.  This process is running in
+                           the background.  */
+} pgrp_status_t;
+static pgrp_status_t volatile pgrp_status = PGRP_UNKNOWN;
+
+/* Update pgrp_status, depending on term_fd.  */
+static ASYNC_SAFE void
+update_pgrp_status (void)
+{
+  int fd = term_fd;
+  if (fd < 0)
+    {
+      pgrp_status = PGRP_UNKNOWN;
+      log_message ("pgrp_status = PGRP_UNKNOWN\n");
+    }
+  else
+    {
+      pid_t p = tcgetpgrp (fd);
+      if (p < 0)
+        {
+          pgrp_status = PGRP_NO_TTY;
+          log_message ("pgrp_status = PGRP_NO_TTY\n");
+        }
+      else
+        {
+          /* getpgrp () changes when the process gets put into the background
+             by a shell that implements job control.  */
+          if (p == getpgrp ())
+            {
+              pgrp_status = PGRP_IN_FOREGROUND;
+              log_message ("pgrp_status = PGRP_IN_FOREGROUND\n");
+            }
+          else
+            {
+              pgrp_status = PGRP_IN_BACKGROUND;
+              log_message ("pgrp_status = PGRP_IN_BACKGROUND\n");
+            }
+        }
+    }
+}
+
+#else
+
+# define update_pgrp_status()
 
-/* File descriptor to which out_char shall output escape sequences.  */
-static int out_fd = -1;
+#endif
 
-/* Filename of out_fd.  */
-static const char *out_filename;
+/* Stream that contains information about how the various out_* functions shall
+   do output.  */
+static term_ostream_t volatile out_stream;
 
-/* Output a single char to out_fd.  Ignore errors.  */
+/* File descriptor to which out_char and out_char_unchecked shall output escape
+   sequences.
+   Same as (out_stream != NULL ? out_stream->fd : -1).  */
+static int volatile out_fd = -1;
+
+/* Filename of out_fd.
+   Same as (out_stream != NULL ? out_stream->filename : NULL).  */
+static const char * volatile out_filename;
+
+/* Signal error after full_write failed.  */
+static void
+out_error ()
+{
+  error (EXIT_FAILURE, errno, _("error writing to %s"), out_filename);
+}
+
+/* Output a single char to out_fd.  */
 static int
+out_char (int c)
+{
+  char bytes[1];
+
+  bytes[0] = (char)c;
+  /* We have to write directly to the file descriptor, not to a buffer with
+     the same destination, because of the padding and sleeping that tputs()
+     does.  */
+  if (full_write (out_fd, bytes, 1) < 1)
+    out_error ();
+  return 0;
+}
+
+/* Output a single char to out_fd.  Ignore errors.  */
+static ASYNC_SAFE int
 out_char_unchecked (int c)
 {
   char bytes[1];
@@ -1063,112 +1461,677 @@ out_char_unchecked (int c)
   return 0;
 }
 
-/* State that informs the exit handler what to do.  */
-static const char *restore_colors;
-static const char *restore_weight;
-static const char *restore_posture;
-static const char *restore_underline;
+/* Output escape sequences to switch the foreground color to NEW_COLOR.  */
+static ASYNC_SAFE void
+out_color_change (term_ostream_t stream, term_color_t new_color,
+                  bool async_safe)
+{
+  assert (stream->supports_foreground);
+  assert (new_color != COLOR_DEFAULT);
+  switch (stream->colormodel)
+    {
+    case cm_common8:
+      assert (new_color >= 0 && new_color < 8);
+      if (stream->set_a_foreground != NULL)
+        tputs (tparm (stream->set_a_foreground, color_bgr (new_color)),
+               1, async_safe ? out_char_unchecked : out_char);
+      else
+        tputs (tparm (stream->set_foreground, new_color),
+               1, async_safe ? out_char_unchecked : out_char);
+      break;
+    /* When we are dealing with an xterm, there is no need to go through
+       tputs() because we know there is no padding and sleeping.  */
+    case cm_xterm8:
+      assert (new_color >= 0 && new_color < 8);
+      {
+        char bytes[5];
+        bytes[0] = 0x1B; bytes[1] = '[';
+        bytes[2] = '3'; bytes[3] = '0' + new_color;
+        bytes[4] = 'm';
+        if (full_write (out_fd, bytes, 5) < 5)
+          if (!async_safe)
+            out_error ();
+      }
+      break;
+    case cm_xterm16:
+      assert (new_color >= 0 && new_color < 16);
+      {
+        char bytes[5];
+        bytes[0] = 0x1B; bytes[1] = '[';
+        if (new_color < 8)
+          {
+            bytes[2] = '3'; bytes[3] = '0' + new_color;
+          }
+        else
+          {
+            bytes[2] = '9'; bytes[3] = '0' + (new_color - 8);
+          }
+        bytes[4] = 'm';
+        if (full_write (out_fd, bytes, 5) < 5)
+          if (!async_safe)
+            out_error ();
+      }
+      break;
+    case cm_xterm88:
+      assert (new_color >= 0 && new_color < 88);
+      {
+        char bytes[10];
+        char *p;
+        bytes[0] = 0x1B; bytes[1] = '[';
+        bytes[2] = '3'; bytes[3] = '8'; bytes[4] = ';';
+        bytes[5] = '5'; bytes[6] = ';';
+        p = bytes + 7;
+        if (new_color >= 10)
+          *p++ = '0' + (new_color / 10);
+        *p++ = '0' + (new_color % 10);
+        *p++ = 'm';
+        if (full_write (out_fd, bytes, p - bytes) < p - bytes)
+          if (!async_safe)
+            out_error ();
+      }
+      break;
+    case cm_xterm256:
+      assert (new_color >= 0 && new_color < 256);
+      {
+        char bytes[11];
+        char *p;
+        bytes[0] = 0x1B; bytes[1] = '[';
+        bytes[2] = '3'; bytes[3] = '8'; bytes[4] = ';';
+        bytes[5] = '5'; bytes[6] = ';';
+        p = bytes + 7;
+        if (new_color >= 100)
+          *p++ = '0' + (new_color / 100);
+        if (new_color >= 10)
+          *p++ = '0' + ((new_color % 100) / 10);
+        *p++ = '0' + (new_color % 10);
+        *p++ = 'm';
+        if (full_write (out_fd, bytes, p - bytes) < p - bytes)
+          if (!async_safe)
+            out_error ();
+      }
+      break;
+    default:
+      abort ();
+    }
+}
+
+/* Output escape sequences to switch the background color to NEW_BGCOLOR.  */
+static ASYNC_SAFE void
+out_bgcolor_change (term_ostream_t stream, term_color_t new_bgcolor,
+                    bool async_safe)
+{
+  assert (stream->supports_background);
+  assert (new_bgcolor != COLOR_DEFAULT);
+  switch (stream->colormodel)
+    {
+    case cm_common8:
+      assert (new_bgcolor >= 0 && new_bgcolor < 8);
+      if (stream->set_a_background != NULL)
+        tputs (tparm (stream->set_a_background, color_bgr (new_bgcolor)),
+               1, async_safe ? out_char_unchecked : out_char);
+      else
+        tputs (tparm (stream->set_background, new_bgcolor),
+               1, async_safe ? out_char_unchecked : out_char);
+      break;
+    /* When we are dealing with an xterm, there is no need to go through
+       tputs() because we know there is no padding and sleeping.  */
+    case cm_xterm8:
+      assert (new_bgcolor >= 0 && new_bgcolor < 8);
+      {
+        char bytes[5];
+        bytes[0] = 0x1B; bytes[1] = '[';
+        bytes[2] = '4'; bytes[3] = '0' + new_bgcolor;
+        bytes[4] = 'm';
+        if (full_write (out_fd, bytes, 5) < 5)
+          if (!async_safe)
+            out_error ();
+      }
+      break;
+    case cm_xterm16:
+      assert (new_bgcolor >= 0 && new_bgcolor < 16);
+      {
+        char bytes[6];
+        bytes[0] = 0x1B; bytes[1] = '[';
+        if (new_bgcolor < 8)
+          {
+            bytes[2] = '4'; bytes[3] = '0' + new_bgcolor;
+            bytes[4] = 'm';
+            if (full_write (out_fd, bytes, 5) < 5)
+              if (!async_safe)
+                out_error ();
+          }
+        else
+          {
+            bytes[2] = '1'; bytes[3] = '0';
+            bytes[4] = '0' + (new_bgcolor - 8); bytes[5] = 'm';
+            if (full_write (out_fd, bytes, 6) < 6)
+              if (!async_safe)
+                out_error ();
+          }
+      }
+      break;
+    case cm_xterm88:
+      assert (new_bgcolor >= 0 && new_bgcolor < 88);
+      {
+        char bytes[10];
+        char *p;
+        bytes[0] = 0x1B; bytes[1] = '[';
+        bytes[2] = '4'; bytes[3] = '8'; bytes[4] = ';';
+        bytes[5] = '5'; bytes[6] = ';';
+        p = bytes + 7;
+        if (new_bgcolor >= 10)
+          *p++ = '0' + (new_bgcolor / 10);
+        *p++ = '0' + (new_bgcolor % 10);
+        *p++ = 'm';
+        if (full_write (out_fd, bytes, p - bytes) < p - bytes)
+          if (!async_safe)
+            out_error ();
+      }
+      break;
+    case cm_xterm256:
+      assert (new_bgcolor >= 0 && new_bgcolor < 256);
+      {
+        char bytes[11];
+        char *p;
+        bytes[0] = 0x1B; bytes[1] = '[';
+        bytes[2] = '4'; bytes[3] = '8'; bytes[4] = ';';
+        bytes[5] = '5'; bytes[6] = ';';
+        p = bytes + 7;
+        if (new_bgcolor >= 100)
+          *p++ = '0' + (new_bgcolor / 100);
+        if (new_bgcolor >= 10)
+          *p++ = '0' + ((new_bgcolor % 100) / 10);
+        *p++ = '0' + (new_bgcolor % 10);
+        *p++ = 'm';
+        if (full_write (out_fd, bytes, p - bytes) < p - bytes)
+          if (!async_safe)
+            out_error ();
+      }
+      break;
+    default:
+      abort ();
+    }
+}
+
+/* Output escape sequences to switch the weight to NEW_WEIGHT.  */
+static ASYNC_SAFE void
+out_weight_change (term_ostream_t stream, term_weight_t new_weight,
+                   bool async_safe)
+{
+  assert (stream->supports_weight);
+  assert (new_weight != WEIGHT_DEFAULT);
+  /* This implies:  */
+  assert (new_weight == WEIGHT_BOLD);
+  tputs (stream->enter_bold_mode,
+         1, async_safe ? out_char_unchecked : out_char);
+}
+
+/* Output escape sequences to switch the posture to NEW_POSTURE.  */
+static ASYNC_SAFE void
+out_posture_change (term_ostream_t stream, term_posture_t new_posture,
+                    bool async_safe)
+{
+  assert (stream->supports_posture);
+  assert (new_posture != POSTURE_DEFAULT);
+  /* This implies:  */
+  assert (new_posture == POSTURE_ITALIC);
+  tputs (stream->enter_italics_mode,
+         1, async_safe ? out_char_unchecked : out_char);
+}
+
+/* Output escape sequences to switch the underline to NEW_UNDERLINE.  */
+static ASYNC_SAFE void
+out_underline_change (term_ostream_t stream, term_underline_t new_underline,
+                      bool async_safe)
+{
+  assert (stream->supports_underline);
+  assert (new_underline != UNDERLINE_DEFAULT);
+  /* This implies:  */
+  assert (new_underline == UNDERLINE_ON);
+  tputs (stream->enter_underline_mode,
+         1, async_safe ? out_char_unchecked : out_char);
+}
 
 /* The exit handler.  */
 static void
 restore (void)
 {
-  /* Only do something while some output was interrupted.  */
-  if (out_fd >= 0)
+  /* Only do something while some output was started but not completed.  */
+  if (out_stream != NULL)
     {
-      if (restore_colors != NULL)
-        tputs (restore_colors, 1, out_char_unchecked);
-      if (restore_weight != NULL)
-        tputs (restore_weight, 1, out_char_unchecked);
-      if (restore_posture != NULL)
-        tputs (restore_posture, 1, out_char_unchecked);
-      if (restore_underline != NULL)
-        tputs (restore_underline, 1, out_char_unchecked);
+      if (out_stream->restore_colors != NULL)
+        tputs (out_stream->restore_colors, 1, out_char_unchecked);
+      if (out_stream->restore_weight != NULL)
+        tputs (out_stream->restore_weight, 1, out_char_unchecked);
+      if (out_stream->restore_posture != NULL)
+        tputs (out_stream->restore_posture, 1, out_char_unchecked);
+      if (out_stream->restore_underline != NULL)
+        tputs (out_stream->restore_underline, 1, out_char_unchecked);
+    }
+}
+
+#if HAVE_TCGETATTR
+
+/* Return a failure message after tcsetattr() failed.  */
+static ASYNC_SAFE void
+tcsetattr_failed (char message[100], const char *caller)
+{
+  int errnum = errno;
+  strcpy (message, caller);
+  strcat (message, ": tcsetattr(fd=");
+  sprintf_integer (message + strlen (message), out_fd);
+  strcat (message, ") failed, errno=");
+  simple_errno_string (message + strlen (message), errnum);
+  strcat (message, "\n");
+}
+
+/* True when orig_lflag represents the original tc.c_lflag.  */
+static bool volatile orig_lflag_set;
+static tcflag_t volatile orig_lflag;
+
+/* Modifies the tty's local mode, preparing for non-default terminal state.
+   Used only when the out_stream's tty_control is TTYCTL_FULL.  */
+static ASYNC_SAFE void
+clobber_local_mode (void)
+{
+  /* Here, out_fd == term_fd.  */
+  struct termios tc;
+  if (pgrp_status == PGRP_IN_FOREGROUND
+      && nonintr_tcgetattr (out_fd, &tc) >= 0)
+    {
+      if (!orig_lflag_set)
+        orig_lflag = tc.c_lflag;
+      /* Set orig_lflag_set to true before actually modifying the tty's local
+         mode, because restore_local_mode does nothing if orig_lflag_set is
+         false.  */
+      orig_lflag_set = true;
+      tc.c_lflag &= ~ECHO;
+      tc.c_lflag |= NOFLSH;
+      if (nonintr_tcsetattr (out_fd, TCSANOW, &tc) < 0)
+        {
+          /* Since tcsetattr failed, restore_local_mode does not need to restore
+             anything.  Set orig_lflag_set to false to indicate this.  */
+          orig_lflag_set = false;
+          {
+            char message[100];
+            tcsetattr_failed (message, "term_ostream:"":clobber_local_mode");
+            full_write (STDERR_FILENO, message, strlen (message));
+          }
+        }
+    }
+}
+
+/* Modifies the tty's local mode, once the terminal is back to the default state.
+   Returns true if ECHO was turned off.
+   Used only when the out_stream's tty_control is TTYCTL_FULL.  */
+static ASYNC_SAFE bool
+restore_local_mode (void)
+{
+  /* Here, out_fd == term_fd.  */
+  bool echo_was_off = false;
+  /* Nothing to do if !orig_lflag_set.  */
+  if (orig_lflag_set)
+    {
+      struct termios tc;
+      if (nonintr_tcgetattr (out_fd, &tc) >= 0)
+        {
+          echo_was_off = (tc.c_lflag & ECHO) == 0;
+          tc.c_lflag = orig_lflag;
+          if (nonintr_tcsetattr (out_fd, TCSADRAIN, &tc) < 0)
+            {
+              char message[100];
+              tcsetattr_failed (message, "term_ostream:"":restore_local_mode");
+              full_write (STDERR_FILENO, message, strlen (message));
+            }
+        }
+      orig_lflag_set = false;
     }
+  return echo_was_off;
 }
 
-/* The list of signals whose default behaviour is to stop the program.  */
-static int stopping_signals[] =
+#endif
+
+#if defined SIGCONT
+
+/* The list of signals whose default behaviour is to stop or continue the
+   program.  */
+static int const job_control_signals[] =
   {
-#ifdef SIGTSTP
+    #ifdef SIGTSTP
     SIGTSTP,
-#endif
-#ifdef SIGTTIN
+    #endif
+    #ifdef SIGTTIN
     SIGTTIN,
-#endif
-#ifdef SIGTTOU
+    #endif
+    #ifdef SIGTTOU
     SIGTTOU,
-#endif
+    #endif
+    #ifdef SIGCONT
+    SIGCONT,
+    #endif
     0
   };
 
-#define num_stopping_signals (SIZEOF (stopping_signals) - 1)
+# define num_job_control_signals (SIZEOF (job_control_signals) - 1)
+
+#endif
+
+/* The following signals are relevant because they do tputs invocations:
+     - fatal signals,
+     - stopping signals,
+     - continuing signals (SIGCONT).  */
 
-static sigset_t stopping_signal_set;
+static sigset_t relevant_signal_set;
+static bool relevant_signal_set_initialized = false;
 
 static void
-init_stopping_signal_set ()
+init_relevant_signal_set ()
 {
-  static bool stopping_signal_set_initialized = false;
-  if (!stopping_signal_set_initialized)
+  if (!relevant_signal_set_initialized)
     {
+      int fatal_signals[64];
+      size_t num_fatal_signals;
       size_t i;
 
-      sigemptyset (&stopping_signal_set);
-      for (i = 0; i < num_stopping_signals; i++)
-        sigaddset (&stopping_signal_set, stopping_signals[i]);
+      num_fatal_signals = get_fatal_signals (fatal_signals);
 
-      stopping_signal_set_initialized = true;
+      sigemptyset (&relevant_signal_set);
+      for (i = 0; i < num_fatal_signals; i++)
+        sigaddset (&relevant_signal_set, fatal_signals[i]);
+      #if defined SIGCONT
+      for (i = 0; i < num_job_control_signals; i++)
+        sigaddset (&relevant_signal_set, job_control_signals[i]);
+      #endif
+
+      relevant_signal_set_initialized = true;
     }
 }
 
-/* Temporarily delay the stopping signals.  */
-static inline void
-block_stopping_signals ()
+/* Temporarily delay the relevant signals.  */
+static ASYNC_SAFE inline void
+block_relevant_signals ()
 {
-  init_stopping_signal_set ();
-  sigprocmask (SIG_BLOCK, &stopping_signal_set, NULL);
+  /* The caller must ensure that init_relevant_signal_set () was already
+     called.  */
+  if (!relevant_signal_set_initialized)
+    abort ();
+
+  sigprocmask (SIG_BLOCK, &relevant_signal_set, NULL);
 }
 
-/* Stop delaying the stopping signals.  */
-static inline void
-unblock_stopping_signals ()
+/* Stop delaying the relevant signals.  */
+static ASYNC_SAFE inline void
+unblock_relevant_signals ()
 {
-  init_stopping_signal_set ();
-  sigprocmask (SIG_UNBLOCK, &stopping_signal_set, NULL);
+  sigprocmask (SIG_UNBLOCK, &relevant_signal_set, NULL);
 }
 
-/* Compare two sets of attributes for equality.  */
-static inline bool
-equal_attributes (attributes_t attr1, attributes_t attr2)
+/* Determines whether a signal is ignored.  */
+static ASYNC_SAFE bool
+is_ignored (int sig)
 {
-  return (attr1.color == attr2.color
-          && attr1.bgcolor == attr2.bgcolor
-          && attr1.weight == attr2.weight
-          && attr1.posture == attr2.posture
-          && attr1.underline == attr2.underline);
+  struct sigaction action;
+
+  return (sigaction (sig, NULL, &action) >= 0
+          && get_handler (&action) == SIG_IGN);
 }
 
-/* Signal error after full_write failed.  */
+#if HAVE_TCGETATTR
+
+/* Write the same signal marker that the kernel would have printed if ECHO had
+   been turned on.  See (4) above.
+   This is a makeshift and is not perfect:
+     - When stderr refers to a different target than out_stream->fd, it is too
+       hairy to write the signal marker.
+     - In some cases, when the signal was generated right before and delivered
+       right after a clobber_local_mode invocation, the result is that the
+       marker appears twice, e.g. ^C^C.  This occurs only with a small
+       probability.
+     - In some cases, when the signal was generated right before and delivered
+       right after a restore_local_mode invocation, the result is that the
+       marker does not appear at all.  This occurs only with a small
+       probability.
+   To test this kind of behaviour, use the 'color-filter' example like this:
+     $ yes | ./filter '.*'
+ */
+static ASYNC_SAFE void
+show_signal_marker (int sig)
+{
+  /* Write to stderr, not to out_stream->fd, because out_stream->fd is often
+     logged or used with 'less -R'.  */
+  if (out_stream != NULL && out_stream->same_as_stderr)
+    switch (sig)
+      {
+      /* The kernel's action when the user presses the INTR key.  */
+      case SIGINT:
+        full_write (STDERR_FILENO, "^C", 2); break;
+      /* The kernel's action when the user presses the SUSP key.  */
+      case SIGTSTP:
+        full_write (STDERR_FILENO, "^Z", 2); break;
+      /* The kernel's action when the user presses the QUIT key.  */
+      case SIGQUIT:
+        full_write (STDERR_FILENO, "^\\", 2); break;
+      default: break;
+      }
+}
+
+#endif
+
+/* The main code of the signal handler for fatal signals and stopping signals.
+   It is reentrant.  */
+static ASYNC_SAFE void
+fatal_or_stopping_signal_handler (int sig)
+{
+  bool echo_was_off = false;
+  /* Only do something while some output was interrupted.  */
+  if (out_stream != NULL && out_stream->tty_control != TTYCTL_NONE)
+    {
+      unsigned int i;
+
+      /* Block the relevant signals.  This is needed, because the tputs
+         invocations below are not reentrant.  */
+      block_relevant_signals ();
+
+      /* Restore the terminal to the default state.  */
+      for (i = 0; i < 2; i++)
+        {
+          if (out_stream->restore_colors != NULL)
+            tputs (out_stream->restore_colors, 1, out_char_unchecked);
+          if (out_stream->restore_weight != NULL)
+            tputs (out_stream->restore_weight, 1, out_char_unchecked);
+          if (out_stream->restore_posture != NULL)
+            tputs (out_stream->restore_posture, 1, out_char_unchecked);
+          if (out_stream->restore_underline != NULL)
+            tputs (out_stream->restore_underline, 1, out_char_unchecked);
+        }
+      #if HAVE_TCGETATTR
+      if (out_stream->tty_control == TTYCTL_FULL)
+        {
+          /* Restore the local mode, once the tputs calls above have reached
+             their destination.  */
+          echo_was_off = restore_local_mode ();
+        }
+      #endif
+
+      /* Unblock the relevant signals.  */
+      unblock_relevant_signals ();
+    }
+
+  #if HAVE_TCGETATTR
+  if (echo_was_off)
+    show_signal_marker (sig);
+  #endif
+}
+
+/* The signal handler for fatal signals.
+   It is reentrant.  */
+static ASYNC_SAFE void
+fatal_signal_handler (int sig)
+{
+  log_signal_handler_called (sig);
+  fatal_or_stopping_signal_handler (sig);
+}
+
+#if defined SIGCONT
+
+/* The signal handler for stopping signals.
+   It is reentrant.  */
+static ASYNC_SAFE void
+stopping_signal_handler (int sig)
+{
+  log_signal_handler_called (sig);
+  fatal_or_stopping_signal_handler (sig);
+
+  /* Now execute the signal's default action.
+     We reinstall the handler later, during the SIGCONT handler.  */
+  {
+    struct sigaction action;
+    action.sa_handler = SIG_DFL;
+    action.sa_flags = SA_NODEFER;
+    sigemptyset (&action.sa_mask);
+    sigaction (sig, &action, NULL);
+  }
+  raise (sig);
+}
+
+/* The signal handler for SIGCONT.
+   It is reentrant.  */
+static ASYNC_SAFE void
+continuing_signal_handler (int sig)
+{
+  log_signal_handler_called (sig);
+  update_pgrp_status ();
+  /* Only do something while some output was interrupted.  */
+  if (out_stream != NULL && out_stream->tty_control != TTYCTL_NONE)
+    {
+      /* Reinstall the signals handlers removed in stopping_signal_handler.  */
+      {
+        unsigned int i;
+
+        for (i = 0; i < num_job_control_signals; i++)
+          {
+            int sig = job_control_signals[i];
+
+            if (sig != SIGCONT && !is_ignored (sig))
+              {
+                struct sigaction action;
+                action.sa_handler = &stopping_signal_handler;
+                /* If we get a stopping or continuing signal while executing
+                   stopping_signal_handler or continuing_signal_handler, enter
+                   it recursively, since it is reentrant.
+                   Hence no SA_RESETHAND.  */
+                action.sa_flags = SA_NODEFER;
+                sigemptyset (&action.sa_mask);
+                sigaction (sig, &action, NULL);
+              }
+          }
+      }
+
+      /* Block the relevant signals.  This is needed, because the tputs
+         invocations done inside the out_* calls below are not reentrant.  */
+      block_relevant_signals ();
+
+      #if HAVE_TCGETATTR
+      if (out_stream->tty_control == TTYCTL_FULL)
+        {
+          /* Modify the local mode.  */
+          clobber_local_mode ();
+        }
+      #endif
+      /* Set the terminal attributes.  */
+      attributes_t new_attr = out_stream->active_attr;
+      if (new_attr.color != COLOR_DEFAULT)
+        out_color_change (out_stream, new_attr.color, true);
+      if (new_attr.bgcolor != COLOR_DEFAULT)
+        out_bgcolor_change (out_stream, new_attr.bgcolor, true);
+      if (new_attr.weight != WEIGHT_DEFAULT)
+        out_weight_change (out_stream, new_attr.weight, true);
+      if (new_attr.posture != POSTURE_DEFAULT)
+        out_posture_change (out_stream, new_attr.posture, true);
+      if (new_attr.underline != UNDERLINE_DEFAULT)
+        out_underline_change (out_stream, new_attr.underline, true);
+
+      /* Unblock the relevant signals.  */
+      unblock_relevant_signals ();
+    }
+}
+
+/* Ensure the signal handlers are installed.
+   Once they are installed, we leave them installed.  It's not worth
+   installing and uninstalling them each time we switch the terminal to a
+   non-default state and back; instead we set out_stream to tell the signal
+   handler whether it has something to do or not.  */
+
 static void
-out_error ()
+ensure_continuing_signal_handler (void)
 {
-  error (EXIT_FAILURE, errno, _("error writing to %s"), out_filename);
+  static bool signal_handler_installed = false;
+
+  if (!signal_handler_installed)
+    {
+      int sig = SIGCONT;
+      struct sigaction action;
+      action.sa_handler = &continuing_signal_handler;
+      /* If we get a stopping or continuing signal while executing
+         continuing_signal_handler, enter it recursively, since it is
+         reentrant.  Hence no SA_RESETHAND.  */
+      action.sa_flags = SA_NODEFER;
+      sigemptyset (&action.sa_mask);
+      sigaction (sig, &action, NULL);
+
+      signal_handler_installed = true;
+    }
 }
 
-/* Output a single char to out_fd.  */
-static int
-out_char (int c)
+#endif
+
+static void
+ensure_other_signal_handlers (void)
 {
-  char bytes[1];
+  static bool signal_handlers_installed = false;
 
-  bytes[0] = (char)c;
-  /* We have to write directly to the file descriptor, not to a buffer with
-     the same destination, because of the padding and sleeping that tputs()
-     does.  */
-  if (full_write (out_fd, bytes, 1) < 1)
-    out_error ();
-  return 0;
+  if (!signal_handlers_installed)
+    {
+      unsigned int i;
+
+      /* Install the handlers for the fatal signals.  */
+      at_fatal_signal (fatal_signal_handler);
+
+      #if defined SIGCONT
+
+      /* Install the handlers for the stopping and continuing signals.  */
+      for (i = 0; i < num_job_control_signals; i++)
+        {
+          int sig = job_control_signals[i];
+
+          if (sig == SIGCONT)
+            /* Already handled in ensure_continuing_signal_handler.  */
+            ;
+          else if (!is_ignored (sig))
+            {
+              struct sigaction action;
+              action.sa_handler = &stopping_signal_handler;
+              /* If we get a stopping or continuing signal while executing
+                 stopping_signal_handler, enter it recursively, since it is
+                 reentrant.  Hence no SA_RESETHAND.  */
+              action.sa_flags = SA_NODEFER;
+              sigemptyset (&action.sa_mask);
+              sigaction (sig, &action, NULL);
+            }
+          #if DEBUG_SIGNALS
+          else
+            {
+              fprintf (stderr, "Signal %d is ignored. Not installing a handler!\n",
+                       sig);
+              fflush (stderr);
+            }
+          #endif
+        }
+
+      #endif
+
+      signal_handlers_installed = true;
+    }
 }
 
 /* Output escape sequences to switch from STREAM->ACTIVE_ATTR to NEW_ATTR,
@@ -1252,209 +2215,27 @@ out_attr_change (term_ostream_t stream, attributes_t new_attr)
   if (new_attr.color != old_attr.color
       || (cleared_attributes && new_attr.color != COLOR_DEFAULT))
     {
-      assert (stream->supports_foreground);
-      assert (new_attr.color != COLOR_DEFAULT);
-      switch (stream->colormodel)
-        {
-        case cm_common8:
-          assert (new_attr.color >= 0 && new_attr.color < 8);
-          if (stream->set_a_foreground != NULL)
-            tputs (tparm (stream->set_a_foreground,
-                          color_bgr (new_attr.color)),
-                   1, out_char);
-          else
-            tputs (tparm (stream->set_foreground, new_attr.color),
-                   1, out_char);
-          break;
-        /* When we are dealing with an xterm, there is no need to go through
-           tputs() because we know there is no padding and sleeping.  */
-        case cm_xterm8:
-          assert (new_attr.color >= 0 && new_attr.color < 8);
-          {
-            char bytes[5];
-            bytes[0] = 0x1B; bytes[1] = '[';
-            bytes[2] = '3'; bytes[3] = '0' + new_attr.color;
-            bytes[4] = 'm';
-            if (full_write (out_fd, bytes, 5) < 5)
-              out_error ();
-          }
-          break;
-        case cm_xterm16:
-          assert (new_attr.color >= 0 && new_attr.color < 16);
-          {
-            char bytes[5];
-            bytes[0] = 0x1B; bytes[1] = '[';
-            if (new_attr.color < 8)
-              {
-                bytes[2] = '3'; bytes[3] = '0' + new_attr.color;
-              }
-            else
-              {
-                bytes[2] = '9'; bytes[3] = '0' + (new_attr.color - 8);
-              }
-            bytes[4] = 'm';
-            if (full_write (out_fd, bytes, 5) < 5)
-              out_error ();
-          }
-          break;
-        case cm_xterm88:
-          assert (new_attr.color >= 0 && new_attr.color < 88);
-          {
-            char bytes[10];
-            char *p;
-            bytes[0] = 0x1B; bytes[1] = '[';
-            bytes[2] = '3'; bytes[3] = '8'; bytes[4] = ';';
-            bytes[5] = '5'; bytes[6] = ';';
-            p = bytes + 7;
-            if (new_attr.color >= 10)
-              *p++ = '0' + (new_attr.color / 10);
-            *p++ = '0' + (new_attr.color % 10);
-            *p++ = 'm';
-            if (full_write (out_fd, bytes, p - bytes) < p - bytes)
-              out_error ();
-          }
-          break;
-        case cm_xterm256:
-          assert (new_attr.color >= 0 && new_attr.color < 256);
-          {
-            char bytes[11];
-            char *p;
-            bytes[0] = 0x1B; bytes[1] = '[';
-            bytes[2] = '3'; bytes[3] = '8'; bytes[4] = ';';
-            bytes[5] = '5'; bytes[6] = ';';
-            p = bytes + 7;
-            if (new_attr.color >= 100)
-              *p++ = '0' + (new_attr.color / 100);
-            if (new_attr.color >= 10)
-              *p++ = '0' + ((new_attr.color % 100) / 10);
-            *p++ = '0' + (new_attr.color % 10);
-            *p++ = 'm';
-            if (full_write (out_fd, bytes, p - bytes) < p - bytes)
-              out_error ();
-          }
-          break;
-        default:
-          abort ();
-        }
+      out_color_change (stream, new_attr.color, false);
     }
   if (new_attr.bgcolor != old_attr.bgcolor
       || (cleared_attributes && new_attr.bgcolor != COLOR_DEFAULT))
     {
-      assert (stream->supports_background);
-      assert (new_attr.bgcolor != COLOR_DEFAULT);
-      switch (stream->colormodel)
-        {
-        case cm_common8:
-          assert (new_attr.bgcolor >= 0 && new_attr.bgcolor < 8);
-          if (stream->set_a_background != NULL)
-            tputs (tparm (stream->set_a_background,
-                          color_bgr (new_attr.bgcolor)),
-                   1, out_char);
-          else
-            tputs (tparm (stream->set_background, new_attr.bgcolor),
-                   1, out_char);
-          break;
-        /* When we are dealing with an xterm, there is no need to go through
-           tputs() because we know there is no padding and sleeping.  */
-        case cm_xterm8:
-          assert (new_attr.bgcolor >= 0 && new_attr.bgcolor < 8);
-          {
-            char bytes[5];
-            bytes[0] = 0x1B; bytes[1] = '[';
-            bytes[2] = '4'; bytes[3] = '0' + new_attr.bgcolor;
-            bytes[4] = 'm';
-            if (full_write (out_fd, bytes, 5) < 5)
-              out_error ();
-          }
-          break;
-        case cm_xterm16:
-          assert (new_attr.bgcolor >= 0 && new_attr.bgcolor < 16);
-          {
-            char bytes[6];
-            bytes[0] = 0x1B; bytes[1] = '[';
-            if (new_attr.bgcolor < 8)
-              {
-                bytes[2] = '4'; bytes[3] = '0' + new_attr.bgcolor;
-                bytes[4] = 'm';
-                if (full_write (out_fd, bytes, 5) < 5)
-                  out_error ();
-              }
-            else
-              {
-                bytes[2] = '1'; bytes[3] = '0';
-                bytes[4] = '0' + (new_attr.bgcolor - 8); bytes[5] = 'm';
-                if (full_write (out_fd, bytes, 6) < 6)
-                  out_error ();
-              }
-          }
-          break;
-        case cm_xterm88:
-          assert (new_attr.bgcolor >= 0 && new_attr.bgcolor < 88);
-          {
-            char bytes[10];
-            char *p;
-            bytes[0] = 0x1B; bytes[1] = '[';
-            bytes[2] = '4'; bytes[3] = '8'; bytes[4] = ';';
-            bytes[5] = '5'; bytes[6] = ';';
-            p = bytes + 7;
-            if (new_attr.bgcolor >= 10)
-              *p++ = '0' + (new_attr.bgcolor / 10);
-            *p++ = '0' + (new_attr.bgcolor % 10);
-            *p++ = 'm';
-            if (full_write (out_fd, bytes, p - bytes) < p - bytes)
-              out_error ();
-          }
-          break;
-        case cm_xterm256:
-          assert (new_attr.bgcolor >= 0 && new_attr.bgcolor < 256);
-          {
-            char bytes[11];
-            char *p;
-            bytes[0] = 0x1B; bytes[1] = '[';
-            bytes[2] = '4'; bytes[3] = '8'; bytes[4] = ';';
-            bytes[5] = '5'; bytes[6] = ';';
-            p = bytes + 7;
-            if (new_attr.bgcolor >= 100)
-              *p++ = '0' + (new_attr.bgcolor / 100);
-            if (new_attr.bgcolor >= 10)
-              *p++ = '0' + ((new_attr.bgcolor % 100) / 10);
-            *p++ = '0' + (new_attr.bgcolor % 10);
-            *p++ = 'm';
-            if (full_write (out_fd, bytes, p - bytes) < p - bytes)
-              out_error ();
-          }
-          break;
-        default:
-          abort ();
-        }
+      out_bgcolor_change (stream, new_attr.bgcolor, false);
     }
-
   if (new_attr.weight != old_attr.weight
       || (cleared_attributes && new_attr.weight != WEIGHT_DEFAULT))
     {
-      assert (stream->supports_weight);
-      assert (new_attr.weight != WEIGHT_DEFAULT);
-      /* This implies:  */
-      assert (new_attr.weight == WEIGHT_BOLD);
-      tputs (stream->enter_bold_mode, 1, out_char);
+      out_weight_change (stream, new_attr.weight, false);
     }
   if (new_attr.posture != old_attr.posture
       || (cleared_attributes && new_attr.posture != POSTURE_DEFAULT))
     {
-      assert (stream->supports_posture);
-      assert (new_attr.posture != POSTURE_DEFAULT);
-      /* This implies:  */
-      assert (new_attr.posture == POSTURE_ITALIC);
-      tputs (stream->enter_italics_mode, 1, out_char);
+      out_posture_change (stream, new_attr.posture, false);
     }
   if (new_attr.underline != old_attr.underline
       || (cleared_attributes && new_attr.underline != UNDERLINE_DEFAULT))
     {
-      assert (stream->supports_underline);
-      assert (new_attr.underline != UNDERLINE_DEFAULT);
-      /* This implies:  */
-      assert (new_attr.underline == UNDERLINE_ON);
-      tputs (stream->enter_underline_mode, 1, out_char);
+      out_underline_change (stream, new_attr.underline, false);
     }
 
   /* Keep track of the active attributes.  */
@@ -1468,33 +2249,39 @@ activate_non_default_attr (term_ostream_t stream)
 {
   if (!stream->non_default_active)
     {
+      if (stream->tty_control != TTYCTL_NONE)
+        ensure_other_signal_handlers ();
+
+      #if BLOCK_SIGNALS_DURING_NON_DEFAULT_STYLE_OUTPUT
       /* Block fatal signals, so that a SIGINT or similar doesn't interrupt
-         us without the possibility of restoring the terminal's state.  */
-      block_fatal_signals ();
-      /* Likewise for SIGTSTP etc.  */
-      block_stopping_signals ();
-
-      /* Enable the exit handler for restoring the terminal's state.  */
-      restore_colors =
-        (stream->supports_foreground || stream->supports_background
-         ? stream->orig_pair
-         : NULL);
-      restore_weight =
-        (stream->supports_weight ? stream->exit_attribute_mode : NULL);
-      restore_posture =
-        (stream->supports_posture
-         ? (stream->exit_italics_mode != NULL
-            ? stream->exit_italics_mode
-            : stream->exit_attribute_mode)
-         : NULL);
-      restore_underline =
-        (stream->supports_underline
-         ? (stream->exit_underline_mode != NULL
-            ? stream->exit_underline_mode
-            : stream->exit_attribute_mode)
-         : NULL);
+         us without the possibility of restoring the terminal's state.
+         Likewise for SIGTSTP etc.  */
+      block_relevant_signals ();
+      #endif
+
+      /* Enable the exit handler for restoring the terminal's state,
+         and make the signal handlers effective.  */
+      if (out_stream != NULL)
+        {
+          /* We can't support two term_ostream_t active with non-default
+             attributes at the same time.  */
+          abort ();
+        }
+      /* The uses of 'volatile' (and ISO C 99 section 5.1.2.3.(5)) ensure that
+         we set out_stream to a non-NULL value only after the memory locations
+         out_fd and out_filename have been filled.  */
       out_fd = stream->fd;
       out_filename = stream->filename;
+      out_stream = stream;
+
+      #if HAVE_TCGETATTR
+      /* Now that the signal handlers are effective, modify the tty.  */
+      if (stream->tty_control == TTYCTL_FULL)
+        {
+          /* Modify the local mode.  */
+          clobber_local_mode ();
+        }
+      #endif
 
       stream->non_default_active = true;
     }
@@ -1507,13 +2294,28 @@ deactivate_non_default_attr (term_ostream_t stream)
 {
   if (stream->non_default_active)
     {
-      /* Disable the exit handler.  */
+      #if HAVE_TCGETATTR
+      /* Before we make the signal handlers ineffective, modify the tty.  */
+      if (out_stream->tty_control == TTYCTL_FULL)
+        {
+          /* Restore the local mode, once the tputs calls from out_attr_change
+             have reached their destination.  */
+          restore_local_mode ();
+        }
+      #endif
+
+      /* Disable the exit handler, and make the signal handlers ineffective.  */
+      /* The uses of 'volatile' (and ISO C 99 section 5.1.2.3.(5)) ensure that
+         we reset out_fd and out_filename only after the memory location
+         out_stream has been cleared.  */
+      out_stream = NULL;
       out_fd = -1;
       out_filename = NULL;
 
-      /* Unblock fatal and stopping signals.  */
-      unblock_stopping_signals ();
-      unblock_fatal_signals ();
+      #if BLOCK_SIGNALS_DURING_NON_DEFAULT_STYLE_OUTPUT
+      /* Unblock the relevant signals.  */
+      unblock_relevant_signals ();
+      #endif
 
       stream->non_default_active = false;
     }
@@ -1705,6 +2507,8 @@ term_ostream::free (term_ostream_t stream)
   /* Verify that the non-default attributes mode is turned off.  */
   if (stream->non_default_active)
     abort ();
+  term_fd = -1;
+  update_pgrp_status ();
   free (stream->filename);
   if (stream->set_a_foreground != NULL)
     free (stream->set_a_foreground);
@@ -2005,6 +2809,57 @@ term_ostream_create (int fd, const char *filename, ttyctl_t tty_control)
      && (stream->exit_underline_mode != NULL
          || stream->exit_attribute_mode != NULL));
 
+  /* Infer the restore strings.  */
+  stream->restore_colors =
+    (stream->supports_foreground || stream->supports_background
+     ? stream->orig_pair
+     : NULL);
+  stream->restore_weight =
+    (stream->supports_weight ? stream->exit_attribute_mode : NULL);
+  stream->restore_posture =
+    (stream->supports_posture
+     ? (stream->exit_italics_mode != NULL
+        ? stream->exit_italics_mode
+        : stream->exit_attribute_mode)
+     : NULL);
+  stream->restore_underline =
+    (stream->supports_underline
+     ? (stream->exit_underline_mode != NULL
+        ? stream->exit_underline_mode
+        : stream->exit_attribute_mode)
+     : NULL);
+
+  /* Prepare tty control.  */
+  if (tty_control == TTYCTL_AUTO)
+    tty_control = TTYCTL_FULL;
+  stream->tty_control = tty_control;
+  if (stream->tty_control != TTYCTL_NONE)
+    init_relevant_signal_set ();
+  #if HAVE_TCGETATTR
+  if (stream->tty_control == TTYCTL_FULL)
+    {
+      struct stat statbuf1;
+      struct stat statbuf2;
+      if (fd == STDERR_FILENO
+          || (fstat (fd, &statbuf1) >= 0
+              && fstat (STDERR_FILENO, &statbuf2) >= 0
+              && SAME_INODE (statbuf1, statbuf2)))
+        stream->same_as_stderr = true;
+      else
+        stream->same_as_stderr = false;
+    }
+  else
+    /* This value is actually not used.  */
+    stream->same_as_stderr = false;
+  #endif
+
+  /* Start keeping track of the process group status.  */
+  term_fd = fd;
+  #if defined SIGCONT
+  ensure_continuing_signal_handler ();
+  #endif
+  update_pgrp_status ();
+
   /* Initialize the buffer.  */
   stream->allocated = 120;
   stream->buffer = XNMALLOC (stream->allocated, char);
index e3906c8cbdbf1bee8f66ce745e434ba761a741ae..c5446e98598befc8d09742195903932e6bb08779 100644 (file)
@@ -1,4 +1,4 @@
-# term-ostream.m4 serial 2
+# term-ostream.m4 serial 3
 dnl Copyright (C) 2006, 2019 Free Software Foundation, Inc.
 dnl This file is free software; the Free Software Foundation
 dnl gives unlimited permission to copy and/or distribute it,
@@ -7,5 +7,5 @@ dnl with or without modifications, as long as this notice is preserved.
 AC_DEFUN([gl_TERM_OSTREAM],
 [
   AC_REQUIRE([AC_C_INLINE])
-  AC_CHECK_FUNCS_ONCE([tcdrain])
+  AC_CHECK_FUNCS_ONCE([tcgetattr tcdrain])
 ])
index 2264b308eb9c8bb718f09234d676e415dff29e8e..77e893f7dc821acf0af6d194b323bfd56aea1670 100644 (file)
@@ -11,9 +11,11 @@ ostream
 error
 stdlib
 fatal-signal
+sigaction
 sigprocmask
 full-write
 fsync
+same-inode
 gettext-h
 terminfo-h
 xalloc
index ac2e4e0ef6d11b4d37ace9907fc49bdb762d9ead..f4196f42fba21c8c8b2c316a6695d83be2002b6b 100644 (file)
 /lib/safe-read.h
 /lib/safe-write.c
 /lib/safe-write.h
+/lib/same-inode.h
 /lib/sig-handler.c
 /lib/sig-handler.h
 /lib/sigaction.c
index b22a1c4089b39955ff1a21bc81943902d5696675..ddbb01d72fcae62f7c1014b408f76789c33fb8d4 100644 (file)
@@ -1,3 +1,6 @@
+New in 0.7:
+* Reliable tty control, as described by the ttyctl_t enum, is now implemented.
+
 New in 0.6:
 * The ostream_t operation 'flush' now takes an additional argument, of type
   ostream_flush_scope_t.