env --chdir=/build FOO=bar timeout 5 true
@end example
+@item --default-signal[=@var{sig}]
+Unblock and reset signal @var{sig} to its default signal handler.
+Without @var{sig} all known signals are unblocked and 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):
+
+@example
+env --default-signal=PIPE,INT seq 1000 | head -n1
+@end example
+
+In the following example, we see how this is not
+possible to do with traditional shells.
+Here 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.
+
+@example
+trap '' PIPE && sh -c 'trap - PIPE ; seq inf | head -n1'
+@end example
+
+Using @option{--default-signal=PIPE} we can
+ensure the signal handling is set to its default behavior:
+
+@example
+trap '' PIPE && sh -c 'env --default-signal=PIPE seq inf | head -n1'
+@end example
+
+
+@item --ignore-signal[=@var{sig}]
+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:
+
+@example
+env --ignore-signal=INT seq inf > /dev/null
+@end example
+
+@samp{SIGCHLD} is special, in that @option{--ignore-signal=CHLD} might have
+no effect (POSIX says it's unspecified).
+
+Most operating systems do not allow ignoring @samp{SIGKILL}, @samp{SIGSTOP}
+(and possibly other signals). Attempting to ignore these signals will fail.
+
+Multiple (and contradictory) @option{--default-signal=SIG} and
+@option{--ignore-signal=SIG} options are processed left-to-right,
+with the latter taking precedence. In the following example, @samp{SIGPIPE} is
+set to default while @samp{SIGINT} is ignored:
+
+@example
+env --default-signal=INT,PIPE --ignore-signal=INT
+@end example
+
+@item --block-signal[=@var{sig}]
+Block signal(s) @var{sig} from being delivered.
+
@item -v
@itemx --debug
@opindex -v
#include <sys/types.h>
#include <getopt.h>
#include <c-ctype.h>
+#include <signal.h>
#include <assert.h>
#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"
static char *varname;
static size_t vnlen;
+/* 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 + 1];
+
+/* Set of signals to block. */
+static sigset_t block_signals;
+
+/* Set of signals to unblock. */
+static sigset_t unblock_signals;
+
+/* Whether signal mask adjustment requested. */
+static bool sig_mask_changed;
+
static char const shortopts[] = "+C:iS: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,
+ BLOCK_SIGNAL_OPTION,
+};
+
static struct option const longopts[] =
{
{"ignore-environment", no_argument, NULL, 'i'},
{"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},
+ {"block-signal", optional_argument, NULL, BLOCK_SIGNAL_OPTION},
{"debug", no_argument, NULL, 'v'},
{"split-string", required_argument, NULL, 'S'},
{GETOPT_HELP_OPTION_DECL},
fputs (_("\
-S, --split-string=S process and split S into separate arguments;\n\
used to pass multiple arguments on shebang lines\n\
+"), stdout);
+ fputs (_("\
+ --block-signal[=SIG] block delivery of SIG signal(s) to COMMAND\n\
+"), stdout);
+ fputs (_("\
+ --default-signal[=SIG] reset handling of SIG signal(s) to the default\n\
+"), stdout);
+ fputs (_("\
+ --ignore-signal[=SIG] set handling of SIG signals(s) to do nothing\n\
+"), stdout);
+ fputs (_("\
-v, --debug print verbose information for each processing step\n\
"), stdout);
fputs (HELP_OPTION_DESCRIPTION, stdout);
fputs (_("\
\n\
A mere - implies -i. If no COMMAND, print the resulting environment.\n\
+"), stdout);
+ fputs (_("\
+\n\
+SIG may be a signal name like 'PIPE', or a signal number like '13'.\n\
+Without SIG, all known signals are included. Multiple signals can be\n\
+comma-separated.\n\
"), stdout);
emit_ancillary_info (PROGRAM_NAME);
}
*orig_optind = 0; /* tell getopt to restart from first argument */
}
+static void
+parse_signal_action_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 = 1 ; 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);
+ /* operand2sig accepts signal 0 (EXIT) - but we reject it. */
+ if (signum == 0)
+ error (0, 0, _("%s: invalid signal"), quote (opt_sig));
+ 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 = 1; i <= SIGNUM_BOUND; i++)
+ {
+ struct sigaction act;
+
+ if (signals[i] == UNCHANGED)
+ continue;
+
+ bool ignore_errors = (signals[i] == DEFAULT_NOERR
+ || signals[i] == IGNORE_NOERR);
+
+ bool set_to_default = (signals[i] == DEFAULT
+ || signals[i] == DEFAULT_NOERR);
+
+ int sig_err = sigaction (i, NULL, &act);
+
+ if (sig_err && !ignore_errors)
+ die (EXIT_CANCELED, errno,
+ _("failed to get signal action for signal %d"), i);
+
+ if (! sig_err)
+ {
+ act.sa_handler = set_to_default ? SIG_DFL : SIG_IGN;
+
+ if ((sig_err = sigaction (i, &act, NULL)) && !ignore_errors)
+ die (EXIT_CANCELED, errno,
+ _("failed to set signal action for signal %d"), i);
+ }
+
+ if (dev_debug)
+ {
+ char signame[SIG2STR_MAX];
+ sig2str (i, signame);
+ devmsg ("Reset signal %s (%d) to %s%s\n",
+ signame, i,
+ set_to_default ? "DEFAULT" : "IGNORE",
+ sig_err ? " (failure ignored)" : "");
+ }
+ }
+}
+
+
+static void
+parse_block_signal_params (const char* optarg, bool block)
+{
+ char signame[SIG2STR_MAX];
+ char *opt_sig;
+ char *optarg_writable;
+
+ if (! optarg)
+ {
+ /* without an argument, reset all signals. */
+ sigfillset (block ? &block_signals : &unblock_signals);
+ sigemptyset (block ? &unblock_signals : &block_signals);
+ }
+ else if (! sig_mask_changed)
+ {
+ /* Initialize the sets. */
+ sigemptyset (&block_signals);
+ sigemptyset (&unblock_signals);
+ }
+
+ sig_mask_changed = true;
+
+ if (! optarg)
+ return;
+
+ optarg_writable = xstrdup (optarg);
+
+ opt_sig = strtok (optarg_writable, ",");
+ while (opt_sig)
+ {
+ int signum = operand2sig (opt_sig, signame);
+ /* operand2sig accepts signal 0 (EXIT) - but we reject it. */
+ if (signum == 0)
+ error (0, 0, _("%s: invalid signal"), quote (opt_sig));
+ if (signum <= 0)
+ usage (exit_failure);
+
+ sigaddset (block ? &block_signals : &unblock_signals, signum);
+ sigdelset (block ? &unblock_signals : &block_signals, signum);
+
+ opt_sig = strtok (NULL, ",");
+ }
+
+ free (optarg_writable);
+}
+
+static void
+set_signal_proc_mask (void)
+{
+ /* Get the existing signal mask */
+ sigset_t set;
+ const char *debug_act;
+
+ sigemptyset (&set);
+
+ if (sigprocmask (0, NULL, &set))
+ die (EXIT_CANCELED, errno, _("failed to get signal process mask"));
+
+ for (int i = 1; i <= SIGNUM_BOUND; i++)
+ {
+ if (sigismember (&block_signals, i))
+ {
+ sigaddset (&set, i);
+ debug_act = "BLOCK";
+ }
+ else if (sigismember (&unblock_signals, i))
+ {
+ sigdelset (&set, i);
+ debug_act = "UNBLOCK";
+ }
+ else
+ {
+ debug_act = NULL;
+ }
+
+ if (dev_debug && debug_act)
+ {
+ char signame[SIG2STR_MAX];
+ sig2str (i, signame);
+ devmsg ("signal %s (%d) mask set to %s\n",
+ signame, i, debug_act);
+ }
+ }
+
+ if (sigprocmask (SIG_SETMASK, &set, NULL))
+ die (EXIT_CANCELED, errno, _("failed to set signal process mask"));
+}
+
int
main (int argc, char **argv)
{
case '0':
opt_nul_terminate_output = true;
break;
+ case DEFAULT_SIGNAL_OPTION:
+ parse_signal_action_params (optarg, true);
+ parse_block_signal_params (optarg, false);
+ break;
+ case IGNORE_SIGNAL_OPTION:
+ parse_signal_action_params (optarg, false);
+ break;
+ case BLOCK_SIGNAL_OPTION:
+ parse_block_signal_params (optarg, true);
+ break;
case 'C':
newdir = optarg;
break;
return EXIT_SUCCESS;
}
+ reset_signal_handlers ();
+ if (sig_mask_changed)
+ set_signal_proc_mask ();
+
if (newdir)
{
devmsg ("chdir: %s\n", quoteaf (newdir));
--- /dev/null
+#!/bin/sh
+# Test env --default-signal=PIPE feature.
+
+# Copyright (C) 2019 Free Software Foundation, Inc.
+
+# 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 <https://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ env seq test timeout
+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; $SHELL -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 \
+ $SHELL -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 (3)
+# -------------------------------------
+# Repeat the previous test, using --default-signal with no signal names,
+# i.e., all signals.
+(trap '' PIPE;
+ env --default-signal \
+ $SHELL -c 'trap - PIPE; seq 999999 2>err4 | head -n1 > out4')
+
+compare exp-out out4 || fail=1
+compare /dev/null err4 || fail=1
+
+# env test - block signal handler
+env --block-signal true || fail=1
+
+# Baseline test - ignore signal handler
+# -------------------------------------
+# Kill 'sleep' 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 \
+ sleep 10 > /dev/null 2>err5
+
+printf "timeout: sending signal INT to command 'sleep'\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 <<EOF
+timeout: sending signal INT to command 'env'
+timeout: sending signal KILL to command 'env'
+EOF
+
+env timeout --verbose --kill-after=.1 --signal=INT .1 \
+ env --ignore-signal=INT \
+ sleep 10 > /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 \
+ sleep 10 > /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