From: Bruno Haible Date: Sun, 17 Mar 2019 13:26:03 +0000 (+0100) Subject: libtextstyle: Implement reliable tty control. X-Git-Tag: v0.20~121 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0bd5cb030327d74fb9df076bf81dde8db6c98d8e;p=thirdparty%2Fgettext.git libtextstyle: Implement reliable tty control. * gnulib-local/lib/term-ostream.oo.c (DEBUG_SIGNALS): New macro. Include , . (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. --- diff --git a/gnulib-local/lib/term-ostream.oo.c b/gnulib-local/lib/term-ostream.oo.c index 53eb51911..ad837321b 100644 --- a/gnulib-local/lib/term-ostream.oo.c +++ b/gnulib-local/lib/term-ostream.oo.c @@ -20,6 +20,9 @@ /* Specification. */ #include "term-ostream.h" +/* Set to 1 to get debugging output regarding signals. */ +#define DEBUG_SIGNALS 0 + #include #include #include @@ -27,14 +30,25 @@ #include #include #include -#if HAVE_TCDRAIN +#if DEBUG_SIGNALS +# include +#endif +#if HAVE_TCGETATTR || HAVE_TCDRAIN # include +# if !defined NOFLSH /* QNX */ +# define NOFLSH 0 +# endif +#endif +#if HAVE_TCGETATTR +# include #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 + + 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 + + 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 '.*' + + $ 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 + + section 11.2.5) but tcsetattr() calls would cause it to stop due to + a SIGTTOU signal (per POSIX + ). + 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); diff --git a/gnulib-local/m4/term-ostream.m4 b/gnulib-local/m4/term-ostream.m4 index e3906c8cb..c5446e985 100644 --- a/gnulib-local/m4/term-ostream.m4 +++ b/gnulib-local/m4/term-ostream.m4 @@ -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]) ]) diff --git a/gnulib-local/modules/term-ostream b/gnulib-local/modules/term-ostream index 2264b308e..77e893f7d 100644 --- a/gnulib-local/modules/term-ostream +++ b/gnulib-local/modules/term-ostream @@ -11,9 +11,11 @@ ostream error stdlib fatal-signal +sigaction sigprocmask full-write fsync +same-inode gettext-h terminfo-h xalloc diff --git a/libtextstyle/.gitignore b/libtextstyle/.gitignore index ac2e4e0ef..f4196f42f 100644 --- a/libtextstyle/.gitignore +++ b/libtextstyle/.gitignore @@ -122,6 +122,7 @@ /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 diff --git a/libtextstyle/NEWS b/libtextstyle/NEWS index b22a1c408..ddbb01d72 100644 --- a/libtextstyle/NEWS +++ b/libtextstyle/NEWS @@ -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.