>From 3542f1762c9f14e2275fe5e61d5d7f6275b420a9 Mon Sep 17 00:00:00 2001 From: Assaf Gordon Date: Fri, 15 Feb 2019 12:31:48 -0700 Subject: [PATCH] env: new options -p/--default-signal=SIG/--ignore-signal=SIG New options to set signal handlers to default (SIG_DFL) or ignore (SIG_IGN) This is useful to overcome POSIX limitation that shell must not override inherited signal state, e.g. the second 'trap' here is a no-op: trap '' PIPE && sh -c 'trap - PIPE ; seq inf | head -n1' Instead use: trap '' PIPE && sh -c 'env -p seq inf | head -n1' Similarly, the following will prevent CTRL-C from terminating the program: env --ignore-signal=INT seq inf > /dev/null See https://bugs.gnu.org/34488#8 . * NEWS: Mention new options. * doc/coreutils.texi (env invocation): Document new options. * man/env.x: Add example of --default-signal=SIG usage. * src/env.c (signals): New global variable. (shortopts,longopts): Add new options. (usage): Print new options. (parse_signal_params): Parse comma-separated list of signals, store in signals variable. (reset_signal_handlers): Set each signal to SIG_DFL/SIG_IGN. (main): Process new options. * src/local.mk (src_env_SOURCES): Add operand2sig.c. * tests/misc/env-signal-handler.sh: New test. * tests/local.mk (all_tests): Add new test. --- NEWS | 3 + doc/coreutils.texi | 43 ++++++++++++ man/env.x | 35 ++++++++++ src/env.c | 127 +++++++++++++++++++++++++++++++++- src/local.mk | 1 + tests/local.mk | 1 + tests/misc/env-signal-handler.sh | 146 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 355 insertions(+), 1 deletion(-) create mode 100755 tests/misc/env-signal-handler.sh diff --git a/NEWS b/NEWS index fdde47593..5a8e8a3de 100644 --- a/NEWS +++ b/NEWS @@ -67,6 +67,9 @@ GNU coreutils NEWS -*- outline -*- test now supports the '-N FILE' unary operator (like e.g. bash) to check whether FILE exists and has been modified since it was last read. + env now supports '--default-singal[=SIG]' and '--ignore-signal[=SIG]' + options to set signal handlers before executing a program. + ** New commands basenc is added to complement existing base64,base32 commands, diff --git a/doc/coreutils.texi b/doc/coreutils.texi index be35de490..57b209e07 100644 --- a/doc/coreutils.texi +++ b/doc/coreutils.texi @@ -17227,6 +17227,49 @@ chroot /chroot env --chdir=/srv true env --chdir=/build FOO=bar timeout 5 true @end example address@hidden address@hidden +Reset signal @var{sig} to its default signal handler. Without @var{sig} all +known signals are reset to their defaults. Multiple signals can be +comma-separated. The following command runs @command{seq} with SIGINT and +SIGPIPE set to their default (which is to terminate the program): + address@hidden +env --default-signal=PIPE,INT seq 1000 | head -n1 address@hidden example + +In the following example: + address@hidden +trap '' PIPE && sh -c 'trap - PIPE ; seq inf | head -n1' address@hidden example + +The first trap command sets SIGPIPE to ignore. The second trap command +ostensibly sets it back to its default, but POSIX mandates that the shell +must not change inherited state of the signal - so it is a no-op. + +Using @option{--default-signal=PIPE} (or its shortcut @option{-p}) can be +used to force the signal to its default behavior: + address@hidden +trap '' PIPE && sh -c "env -p seq inf | head -n1' address@hidden example + + address@hidden address@hidden +Ignore signal @var{sig} when running a program. Without @var{sig} all +known signals are set to ignore. Multiple signals can be +comma-separated. The following command runs @command{seq} with SIGINT set +to be ignored - pressing @kbd{Ctrl-C} will not terminate it: + address@hidden +env --ignore-signal=INT seq inf > /dev/null address@hidden example + + address@hidden -p +Equivalent to @option{--default-signal=PIPE} - sets SIGPIPE to its default +behavior (terminate a program upon SIGPIPE). + @item -v @itemx --debug @opindex -v diff --git a/man/env.x b/man/env.x index 8eea79655..5e0ef975e 100644 --- a/man/env.x +++ b/man/env.x @@ -37,3 +37,38 @@ parameter the script will likely fail with: .RE .PP See the full documentation for more details. +.PP +.SS "\-\-default-signal[=SIG]" to 'untrap' a singal +This option allows setting a signal handler to its default +action. This is useful to reset a signal after setting it +to 'ignore' using the shell's trap command. + +In the following example: +.PP +.RS +.nf +trap '' PIPE && sh \-c 'trap \- PIPE ; seq inf | head \-n1' +.fi +.RE +.PP +The first +.B trap +command sets SIGPIPE to ignore. +The second +.B trap +command ostensibly sets it back to its default, but POSIX mandates that the +shell must not change inherited state of the signal - so it is a no-op. +.PP +Using +.B \-\-default-signal=PIPE +(or its shortcut +.B \-p +) +can be used to force the signal to its default behavior: +.PP +.RS +.nf +trap '' PIPE && sh \-c "env \-p seq inf | head \-n1' +.fi +.RE +.PP diff --git a/src/env.c b/src/env.c index 3a1a3869e..6c20ac40c 100644 --- a/src/env.c +++ b/src/env.c @@ -21,12 +21,15 @@ #include #include #include +#include #include #include "system.h" #include "die.h" #include "error.h" +#include "operand2sig.h" #include "quote.h" +#include "sig2str.h" /* The official name of this program (e.g., no 'g' prefix). */ #define PROGRAM_NAME "env" @@ -48,7 +51,27 @@ static bool dev_debug; static char *varname; static size_t vnlen; -static char const shortopts[] = "+C:iS:u:v0 \t"; +/* Possible actions on each signal. */ +enum SIGNAL_MODE { + UNCHANGED = 0, + DEFAULT, /* Set to default handler (SIG_DFL). */ + DEFAULT_NOERR, /* ditto, but ignore sigaction(2) errors. */ + IGNORE, /* Set to ignore (SIG_IGN). */ + IGNORE_NOERR /* ditto, but ignore sigaction(2) errors. */ +}; +static enum SIGNAL_MODE signals[SIGNUM_BOUND]; + + + +static char const shortopts[] = "+C:ipS:u:v0 \t"; + +/* For long options that have no equivalent short option, use a + non-character as a pseudo short option, starting with CHAR_MAX + 1. */ +enum +{ + DEFAULT_SIGNAL_OPTION = CHAR_MAX + 1, + IGNORE_SIGNAL_OPTION +}; static struct option const longopts[] = { @@ -56,6 +79,8 @@ static struct option const longopts[] = {"null", no_argument, NULL, '0'}, {"unset", required_argument, NULL, 'u'}, {"chdir", required_argument, NULL, 'C'}, + {"default-signal", optional_argument, NULL, DEFAULT_SIGNAL_OPTION}, + {"ignore-signal", optional_argument, NULL, IGNORE_SIGNAL_OPTION}, {"debug", no_argument, NULL, 'v'}, {"split-string", required_argument, NULL, 'S'}, {GETOPT_HELP_OPTION_DECL}, @@ -88,8 +113,23 @@ Set each NAME to VALUE in the environment and run COMMAND.\n\ -C, --chdir=DIR change working directory to DIR\n\ "), stdout); fputs (_("\ + --default-signal[=SIG] reset signal SIG to its default signal handler.\n\ + without SIG, all known signals are included.\n\ + multiple signals can be comma-separated.\n\ +"), stdout); + fputs (_("\ + --ignore-signal[=SIG] set signal SIG to be IGNORED.\n\ + without SIG, all known signals are included.\n\ + multiple signals can be comma-separated.\n\ +"), stdout); + fputs (_("\ -S, --split-string=S process and split S into separate arguments;\n\ used to pass multiple arguments on shebang lines\n\ +"), stdout); + fputs (_("\ + -p same as --default-signal=PIPE\n\ +"), stdout); + fputs (_("\ -v, --debug print verbose information for each processing step\n\ "), stdout); fputs (HELP_OPTION_DESCRIPTION, stdout); @@ -525,6 +565,79 @@ parse_split_string (const char* str, int /*out*/ *orig_optind, *orig_optind = 0; /* tell getopt to restart from first argument */ } +static void +parse_signal_params (const char* optarg, bool set_default) +{ + char signame[SIG2STR_MAX]; + char *opt_sig; + char *optarg_writable; + + if (!optarg) + { + /* without an argument, reset all signals. + Some signals cannot be set to ignore or default (e.g., SIGKILL, + SIGSTOP on most OSes, and SIGCONT on AIX.) - so ignore errors. */ + for (int i = 0 ; i < SIGNUM_BOUND; ++i) + if (sig2str (i, signame) == 0) + signals[i] = set_default ? DEFAULT_NOERR : IGNORE_NOERR; + return; + } + + optarg_writable = xstrdup (optarg); + + opt_sig = strtok (optarg_writable, ","); + while (opt_sig) + { + int signum = operand2sig (opt_sig, signame); + if (signum < 0) + usage (exit_failure); + + signals[signum] = set_default ? DEFAULT : IGNORE; + + opt_sig = strtok (NULL, ","); + } + + free (optarg_writable); +} + +static void +reset_signal_handlers (void) +{ + for (int i=0; i. + +. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src +print_ver_ seq +print_ver_ timeout +print_ver_ test +trap_sigpipe_or_skip_ + +# Paraphrasing http://bugs.gnu.org/34488#8: +# POSIX requires that sh started with an inherited ignored SIGPIPE must +# silently ignore all attempts from within the shell to restore SIGPIPE +# handling to child processes of the shell: +# +# $ (trap '' PIPE; bash -c 'trap - PIPE; seq inf | head -n1') +# 1 +# seq: write error: Broken pipe +# +# With 'env --default-signal=PIPE', the signal handler can be reset to its +# default. + +# Baseline Test - default signal handler +# -------------------------------------- +# Ensure this results in a "broken pipe" error (the first 'trap' +# sets SIGPIPE to ignore, and the second 'trap' becomes a no-op instead +# of resetting SIGPIPE to its default). Upon a SIGPIPE 'seq' will not be +# terminated, instead its write(2) call will return an error. +(trap '' PIPE; sh -c 'trap - PIPE; seq 999999 2>err1t | head -n1 > out1') + +# The exact broken pipe message depends on the operating system, just ensure +# there was a 'write error' message in stderr: +sed 's/^\(seq: write error:\) .*/\1/' err1t > err1 || framework_failure_ + +printf "1\n" > exp-out || framework_failure_ +printf "seq: write error:\n" > exp-err1 || framework_failure_ + +compare exp-out out1 || framework_failure_ +compare exp-err1 err1 || framework_failure_ + + +# env test - default signal handler +# --------------------------------- +# With env resetting the signal handler to its defaults, there should be no +# error message (because the default SIGPIPE action is to terminate the +# 'seq' program): +(trap '' PIPE; + env --default-signal=PIPE \ + sh -c 'trap - PIPE; seq 999999 2>err2 | head -n1 > out2') + +compare exp-out out2 || fail=1 +compare /dev/null err2 || fail=1 + +# env test - default signal handler (2) +# ------------------------------------- +# Repeat the previous test, using "-p" (shortcut for --default-signal=PIPE): +(trap '' PIPE; + env -p \ + sh -c 'trap - PIPE; seq 999999 2>err3 | head -n1 > out3') + +compare exp-out out3 || fail=1 +compare /dev/null err3 || fail=1 + +# env test - default signal handler (3) +# ------------------------------------- +# Repeat the previous test, using --default-signal with no signal names, +# i.e., all signals. +(trap '' PIPE; + env --default-signal \ + sh -c 'trap - PIPE; seq 999999 2>err4 | head -n1 > out4') + +compare exp-out out4 || fail=1 +compare /dev/null err4 || fail=1 + + + + + + +# Baseline test - ignore signal handler +# ------------------------------------- +# Kill 'seq' after 1 second with SIGINT - it should terminate (as SIGINT's +# default action is to terminate a program). +# (The first 'env' is just to ensure timeout is not the shell's built-in.) +env timeout --verbose --kill-after=1 --signal=INT 1 \ + seq inf > /dev/null 2>err5 + +printf "timeout: sending signal INT to command 'seq'\n" > exp-err5 \ + || framework_failure_ + +compare exp-err5 err5 || fail=1 + + +# env test - ignore signal handler +# -------------------------------- +# Use env to silence (ignore) SIGINT - "seq" should continue running +# after timeout sends SIGINT, and be killed after 1 second using SIGKILL. + +cat>exp-err6 < /dev/null 2>err6t + +# check only the first two lines from stderr, which are printed by timeout. +# (operating systems might add more messages, like "killed"). +sed -n '1,2p' err6t > err6 || framework_failure_ + +compare exp-err6 err6 || fail=1 + + +# env test - ignore signal handler (2) +# ------------------------------------ +# Repeat the previous test with "--ignore-signals" and no signal names, +# i.e., all signals. + +env timeout --verbose --kill-after=1 --signal=INT 1 \ + env --ignore-signal \ + seq inf > /dev/null 2>err7t + +# check only the first two lines from stderr, which are printed by timeout. +# (operating systems might add more messages, like "killed"). +sed -n '1,2p' err7t > err7 || framework_failure_ + +compare exp-err6 err7 || fail=1 + + + +Exit $fail -- 2.11.0