>From 61171aa7682c8429de0455e94bc3d6dd593ab9a6 Mon Sep 17 00:00:00 2001 From: Bruno Haible Subject: [PATCH 1/2] term-style-control: New module. * lib/term-style-control.h: New file, based on libtextstyle's term-ostream.oo.h and term-ostream.oo.c. * lib/term-style-control.c: New file, based on libtextstyle's term-ostream.oo.c. * modules/term-style-control: New file. --- ChangeLog | 9 + lib/term-style-control.c | 1013 ++++++++++++++++++++++++++++++++++++++++++++ lib/term-style-control.h | 182 ++++++++ modules/term-style-control | 30 ++ 4 files changed, 1234 insertions(+) create mode 100644 lib/term-style-control.c create mode 100644 lib/term-style-control.h create mode 100644 modules/term-style-control diff --git a/ChangeLog b/ChangeLog index 58b47dc..481db9d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +2019-03-24 Bruno Haible + + term-style-control: New module. + * lib/term-style-control.h: New file, based on libtextstyle's + term-ostream.oo.h and term-ostream.oo.c. + * lib/term-style-control.c: New file, based on libtextstyle's + term-ostream.oo.c. + * modules/term-style-control: New file. + 2019-03-22 Akim Demaille _Noreturn: beware of C's _Noreturn in C++ pre C++11. diff --git a/lib/term-style-control.c b/lib/term-style-control.c new file mode 100644 index 0000000..4d2b8ee --- /dev/null +++ b/lib/term-style-control.c @@ -0,0 +1,1013 @@ +/* Terminal control for outputting styled text to a terminal. + Copyright (C) 2006-2008, 2017, 2019 Free Software Foundation, Inc. + Written by Bruno Haible , 2019. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +#include + +/* Specification. */ +#include "term-style-control.h" + +/* Set to 1 to get debugging output regarding signals. */ +#define DEBUG_SIGNALS 0 + +#include +#include +#include +#include +#include +#include +#if DEBUG_SIGNALS +# include +#endif +#if HAVE_TCGETATTR +# include +# if !defined NOFLSH /* QNX */ +# define NOFLSH 0 +# endif +#endif +#if HAVE_TCGETATTR +# include +#endif + +#include "fatal-signal.h" +#include "sig-handler.h" +#include "full-write.h" +#include "same-inode.h" + +#define SIZEOF(a) (sizeof(a) / sizeof(a[0])) + + +/* ============================ EINTR handling ============================ */ + +/* EINTR handling for tcgetattr(), tcsetattr(). + 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 + + +/* ========================== 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 _GL_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 _GL_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 _GL_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 _GL_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 _GL_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 + + +/* ============================ Signal handling ============================ */ + +/* 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-style-control 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 active 'struct term_style_controller' and + 'struct term_style_user_data'. */ +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 _GL_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() + +#endif + +/* Controller and its user_data that contain information about how to do + output. */ +static const struct term_style_controller * volatile active_controller; +static struct term_style_user_data * volatile active_user_data; + +/* The 'struct term_style_control_data' embedded in active_user_data. + Same as + (active_controller != NULL + ? active_controller->get_control_data (active_user_data) + : NULL). */ +static struct term_style_control_data * volatile active_control_data; + +/* The fd contained in active_control_data. + Same as + (active_controller != NULL + ? active_control_data->fd + : -1). */ +static int volatile active_fd = -1; + +/* The exit handler. */ +static void +atexit_handler (void) +{ + /* Only do something while some output was started but not completed. */ + if (active_controller != NULL) + { + active_controller->restore (active_user_data); + deactivate_term_non_default_mode (active_controller, active_user_data); + #if 0 /* not needed */ + deactivate_term_style_controller (active_controller, active_user_data); + #endif + } +} + +#if HAVE_TCGETATTR + +/* Return a failure message after tcsetattr() failed. */ +static _GL_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), active_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 active_control_data's tty_control is TTYCTL_FULL. */ +static _GL_ASYNC_SAFE void +clobber_local_mode (void) +{ + /* Here, active_fd == term_fd. */ + if (pgrp_status == PGRP_IN_FOREGROUND) + { + struct termios tc; + if (nonintr_tcgetattr (active_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 (active_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-style-control: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 active_control_data's tty_control is TTYCTL_FULL. */ +static _GL_ASYNC_SAFE bool +restore_local_mode (void) +{ + /* Here, active_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 (active_fd, &tc) >= 0) + { + echo_was_off = (tc.c_lflag & ECHO) == 0; + tc.c_lflag = orig_lflag; + if (nonintr_tcsetattr (active_fd, TCSADRAIN, &tc) < 0) + { + char message[100]; + tcsetattr_failed (message, + "term-style-control:restore_local_mode"); + full_write (STDERR_FILENO, message, strlen (message)); + } + } + orig_lflag_set = false; + } + return echo_was_off; +} + +#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 + SIGTSTP, + #endif + #ifdef SIGTTIN + SIGTTIN, + #endif + #ifdef SIGTTOU + SIGTTOU, + #endif + #ifdef SIGCONT + SIGCONT, + #endif + 0 + }; + +# define num_job_control_signals (SIZEOF (job_control_signals) - 1) + +#endif + +/* The following signals are relevant because they output escape sequences to + the terminal: + - fatal signals, + - stopping signals, + - continuing signals (SIGCONT). */ + +static sigset_t relevant_signal_set; +static bool relevant_signal_set_initialized = false; + +static void +init_relevant_signal_set () +{ + if (!relevant_signal_set_initialized) + { + int fatal_signals[64]; + size_t num_fatal_signals; + size_t i; + + num_fatal_signals = get_fatal_signals (fatal_signals); + + 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 relevant signals. */ +static _GL_ASYNC_SAFE inline void +block_relevant_signals () +{ + /* 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 relevant signals. */ +static _GL_ASYNC_SAFE inline void +unblock_relevant_signals () +{ + sigprocmask (SIG_UNBLOCK, &relevant_signal_set, NULL); +} + +#if defined SIGCONT + +/* Determines whether a signal is ignored. */ +static _GL_ASYNC_SAFE bool +is_ignored (int sig) +{ + struct sigaction action; + + return (sigaction (sig, NULL, &action) >= 0 + && get_handler (&action) == SIG_IGN); +} + +#endif + +#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 active_control_data->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 'test-term-style-control-yes' example + like this: + $ ./test-term-style-control-yes + */ +static _GL_ASYNC_SAFE void +show_signal_marker (int sig) +{ + /* Write to stderr, not to active_control_data->fd, because + active_control_data->fd is often logged or used with 'less -R'. */ + if (active_controller != NULL && active_control_data->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 _GL_ASYNC_SAFE void +fatal_or_stopping_signal_handler (int sig) +{ + #if HAVE_TCGETATTR + bool echo_was_off = false; + #endif + /* Only do something while some output was interrupted. */ + if (active_controller != NULL + && active_control_data->tty_control != TTYCTL_NONE) + { + unsigned int i; + + /* Block the relevant signals. This is needed, because the output + of escape sequences below (usually through tputs invocations) is + not reentrant. */ + block_relevant_signals (); + + /* Restore the terminal to the default state. */ + for (i = 0; i < 2; i++) + active_controller->async_restore (active_user_data); + #if HAVE_TCGETATTR + if (active_control_data->tty_control == TTYCTL_FULL) + { + /* Restore the local mode, once the escape sequences output 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 _GL_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 _GL_ASYNC_SAFE void +stopping_signal_handler (int sig) +{ + int saved_errno = errno; + + 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); + } + errno = saved_errno; + raise (sig); +} + +/* The signal handler for SIGCONT. + It is reentrant. */ +static _GL_ASYNC_SAFE void +continuing_signal_handler (int sig) +{ + int saved_errno = errno; + + log_signal_handler_called (sig); + update_pgrp_status (); + /* Only do something while some output was interrupted. */ + if (active_controller != NULL + && active_control_data->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 output of + escape sequences done inside the async_set_attributes_from_default + call below is not reentrant. */ + block_relevant_signals (); + + #if HAVE_TCGETATTR + if (active_control_data->tty_control == TTYCTL_FULL) + { + /* Modify the local mode. */ + clobber_local_mode (); + } + #endif + /* Set the terminal attributes. */ + active_controller->async_set_attributes_from_default (active_user_data); + + /* Unblock the relevant signals. */ + unblock_relevant_signals (); + } + + errno = saved_errno; +} + +/* 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 active_controller to tell the + signal handler whether it has something to do or not. */ + +static void +ensure_continuing_signal_handler (void) +{ + 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; + } +} + +#endif + +static void +ensure_other_signal_handlers (void) +{ + static bool signal_handlers_installed = false; + + 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; + } +} + + +/* ============================== Public API ============================== */ + +void +activate_term_non_default_mode (const struct term_style_controller *controller, + struct term_style_user_data *user_data) +{ + struct term_style_control_data *control_data = + controller->get_control_data (user_data); + + if (!control_data->non_default_active) + { + if (control_data->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. + 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 (active_controller != NULL) + { + /* We can't support two active controllers 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 active_controller to a non-NULL value only after the memory + locations active_user_data, active_control_data, active_fd have been + filled. */ + active_fd = control_data->fd; + active_control_data = control_data; + active_user_data = user_data; + active_controller = controller; + + #if HAVE_TCGETATTR + /* Now that the signal handlers are effective, modify the tty. */ + if (active_control_data->tty_control == TTYCTL_FULL) + { + /* Modify the local mode. */ + clobber_local_mode (); + } + #endif + + control_data->non_default_active = true; + } +} + +void +deactivate_term_non_default_mode (const struct term_style_controller *controller, + struct term_style_user_data *user_data) +{ + struct term_style_control_data *control_data = + controller->get_control_data (user_data); + + if (control_data->non_default_active) + { + #if HAVE_TCGETATTR + /* Before we make the signal handlers ineffective, modify the tty. */ + if (active_control_data->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 active_user_data, active_control_data, active_fd only after + the memory location active_controller has been cleared. */ + active_controller = NULL; + active_user_data = NULL; + active_control_data = NULL; + active_fd = -1; + + #if BLOCK_SIGNALS_DURING_NON_DEFAULT_STYLE_OUTPUT + /* Unblock the relevant signals. */ + unblock_relevant_signals (); + #endif + + control_data->non_default_active = false; + } +} + +void +activate_term_style_controller (const struct term_style_controller *controller, + struct term_style_user_data *user_data, + int fd, ttyctl_t tty_control) +{ + struct term_style_control_data *control_data = + controller->get_control_data (user_data); + + control_data->fd = fd; + + /* Prepare tty control. */ + if (tty_control == TTYCTL_AUTO) + tty_control = TTYCTL_FULL; + control_data->tty_control = tty_control; + if (control_data->tty_control != TTYCTL_NONE) + init_relevant_signal_set (); + #if HAVE_TCGETATTR + if (control_data->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))) + control_data->same_as_stderr = true; + else + control_data->same_as_stderr = false; + } + else + /* This value is actually not used. */ + control_data->same_as_stderr = false; + #endif + + control_data->non_default_active = false; + + /* Start keeping track of the process group status. */ + term_fd = fd; + #if defined SIGCONT + ensure_continuing_signal_handler (); + #endif + update_pgrp_status (); + + /* Register an exit handler. */ + { + static bool registered = false; + if (!registered) + { + atexit (atexit_handler); + registered = true; + } + } +} + +void +deactivate_term_style_controller (const struct term_style_controller *controller, + struct term_style_user_data *user_data) +{ + struct term_style_control_data *control_data = + controller->get_control_data (user_data); + + /* Verify that the non-default attributes mode is turned off. */ + if (control_data->non_default_active) + abort (); + + term_fd = -1; + update_pgrp_status (); +} diff --git a/lib/term-style-control.h b/lib/term-style-control.h new file mode 100644 index 0000000..e341931 --- /dev/null +++ b/lib/term-style-control.h @@ -0,0 +1,182 @@ +/* Terminal control for outputting styled text to a terminal. + Copyright (C) 2019 Free Software Foundation, Inc. + Written by Bruno Haible , 2019. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +#ifndef _TERM_STYLE_CONTROL_H +#define _TERM_STYLE_CONTROL_H + +#include + +/* The user of this file will define a macro 'term_style_user_data', such that + 'struct term_style_user_data' is a user-defined struct. */ + + +/* The amount of control to take over the underlying tty in order to avoid + garbled output on the screen, due to interleaved output of escape sequences + and output from the kernel (such as when the kernel echoes user's input + or when the kernel prints '^C' after the user pressed Ctrl-C). */ +typedef enum +{ + TTYCTL_AUTO = 0, /* Automatic best-possible choice. */ + TTYCTL_NONE, /* No control. + Result: Garbled output can occur, and the terminal can + be left in any state when the program is interrupted. */ + TTYCTL_PARTIAL, /* Signal handling. + Result: Garbled output can occur, but the terminal will + be left in the default state when the program is + interrupted. */ + TTYCTL_FULL /* Signal handling and disabling echo and flush-upon-signal. + Result: No garbled output, and the the terminal will + be left in the default state when the program is + interrupted. */ +} ttyctl_t; + +/* This struct contains data, used by implementation of this module. + You should not access the members of this struct; they may be renamed or + removed without notice. */ +struct term_style_control_data +{ + int volatile fd; + ttyctl_t volatile tty_control; /* Signal handling and tty control. */ + #if HAVE_TCGETATTR + bool volatile same_as_stderr; + #endif + bool non_default_active; /* True if activate_term_non_default_mode() + is in effect. */ +}; + +/* Forward declaration. */ +struct term_style_user_data; + +/* This struct contains function pointers. You implement these functions + in your application; this module invokes them when it needs to. */ +struct term_style_controller +{ + /* This function returns a pointer to the embedded + 'struct term_style_control_data' contained in a + 'struct term_style_user_data'. */ + struct term_style_control_data * (*get_control_data) (struct term_style_user_data *); + + /* This function brings the terminal's state back to the default state + (no styling attributes set). It is invoked when the process terminates + through exit(). */ + void (*restore) (struct term_style_user_data *); + + /* This function brings the terminal's state back to the default state + (no styling attributes set). It is async-safe (see gnulib-common.m4 for + the precise definition). It is invoked when the process receives a fatal + or stopping signal. */ + _GL_ASYNC_SAFE void (*async_restore) (struct term_style_user_data *); + + /* This function brings the terminal's state, from the default state, back + to the state where it has the desired attributes set. It is async-safe + (see gnulib-common.m4 for the precise definition). It is invoked when + the process receives a SIGCONT signal. */ + _GL_ASYNC_SAFE void (*async_set_attributes_from_default) (struct term_style_user_data *); +}; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* This module is used as follows: + 1. You fill a 'struct term_style_controller' with function pointers. + You create a 'struct term_style_user_data' that contains, among other + members, a 'struct term_style_control_data'. + You will pass these two objects to all API functions below. + 2. You call activate_term_style_controller to activate this controller. + Activation of the controller is the prerequisite for activating + the non-default mode, which in turn is the prerequisite for changing + the terminal's attributes. + When you are done with the styled output, you may deactivate the + controller. This is not required before exiting the program, but is + required before activating a different controller. + You cannot have more than one controller activated at the same time. + 3. Once the controller is activated, you may turn on the non-default mode. + The non-default mode is the prerequisite for changing the terminal's + attributes. Once the terminal's attributes are in the default state + again, you may turn off the non-default mode again. + In other words: + - In the default mode, the terminal's attributes MUST be in the default + state; no styled output is possible. + - In the non-default mode, the terminal's attributes MAY switch among + the default state and other states. + This module exercises a certain amount of control over the terminal + during the non-default mode phases; see above (ttyctl_t) for details. + You may switch between the default and the non-default modes any number + of times. + The idea is that you switch back to the default mode before doing large + amounts of output of unstyled text. However, this is not a requirement: + You may leave the non-default mode turned on all the time until the + the program exits. + 3. Once the non-default mode is activated, you may change the attributes + (foreground color, background color, font weight, font posture, underline + decoration, etc.) of the terminal. On Unix, this is typically done by + outputting appropriate escape sequences. + 4. Once attributes are set, text output to the terminal will be rendered + with these attributes. + Note: You MUST return the terminal to the default state before outputting + a newline. + */ + +/* Activates a controller. The CONTROLLER and its USER_DATA controls the + terminal associated with FD. FD is usually STDOUT_FILENO. + TTY_CONTROL specifies the amount of control to take over the underlying tty. + The effects of this functions are undone by calling + deactivate_term_style_controller. + You cannot have more than one controller activated at the same time. + You must not close FD while the controller is active. */ +extern void + activate_term_style_controller (const struct term_style_controller *controller, + struct term_style_user_data *user_data, + int fd, ttyctl_t tty_control); + +/* Activates the non-default mode. + CONTROLLER and its USER_DATA must be a currently active controller. + This function fiddles with the signals of the current process and with + the underlying tty, to an extent described by TTY_CONTROL. + This function is idempotent: When you call it twice in a row, the second + invocation does nothing. + The effects of this function are undone by calling + deactivate_term_non_default_mode. */ +extern void + activate_term_non_default_mode (const struct term_style_controller *controller, + struct term_style_user_data *user_data); + +/* Deactivates the non-default mode. + CONTROLLER and its USER_DATA must be a currently active controller. + This function is idempotent: When you call it twice in a row, the second + invocation does nothing. + Before invoking this function, you must put the terminal's attributes in + the default state. */ +extern void + deactivate_term_non_default_mode (const struct term_style_controller *controller, + struct term_style_user_data *user_data); + +/* Deactivates a controller. + CONTROLLER and its USER_DATA must be a currently active controller. + Before invoking this function, you must ensure that the non-default mode + is deactivated. */ +extern void + deactivate_term_style_controller (const struct term_style_controller *controller, + struct term_style_user_data *user_data); + +#ifdef __cplusplus +} +#endif + +#endif /* _TERM_STYLE_CONTROL_H */ diff --git a/modules/term-style-control b/modules/term-style-control new file mode 100644 index 0000000..fb2d098 --- /dev/null +++ b/modules/term-style-control @@ -0,0 +1,30 @@ +Description: +Terminal control for outputting styled text to a terminal. + +Files: +lib/term-style-control.h +lib/term-style-control.c + +Depends-on: +stdbool +fatal-signal +sigaction +sigprocmask +full-write +same-inode + +configure.ac: +AC_REQUIRE([AC_C_INLINE]) +AC_CHECK_FUNCS_ONCE([tcgetattr]) + +Makefile.am: +lib_SOURCES += term-style-control.c + +Include: +"term-style-control.h" + +License: +GPL + +Maintainer: +Bruno Haible -- 2.7.4