Implement cancel groups

This concerns how "internal job groups" know to stop executing when an
external command receives a "cancel signal" (SIGINT or SIGQUIT). For
example:

    while true
        sleep 1
    end

The intent is that if any 'sleep' exits from a cancel signal, then so would
the while loop. This is why you can hit control-C to end the loop even
if the SIGINT is delivered to sleep and not fish.

Here the 'while' loop is considered an "internal job group" (no separate
pgid, bash would not fork) while each 'sleep' is a separate external
command with its own job group, pgroup, etc. Prior to this change, after
running each 'sleep', parse_execution_context_t would check to see if its
exit status was a cancel signal, and if so, stash it into an int that the
cancel checker would check. But this became unwieldy: now there were three
sources of cancellation signals (that int, the job group, and fish itself).

Introduce the notion of a "cancellation group" which is a set of job
groups that should cancel together. Even though the while loop and sleep
are in different job groups, they are in the same cancellation group. When
any job gets a SIGINT or SIGQUIT, it marks that signal in its cancellation
group, which prevents running new jobs in that group.

This reduces the number of signals to check from 3 to 2; eventually we can
teach cancellation groups how to check fish's own signals and then it will
just be 1.
This commit is contained in:
ridiculousfish
2020-09-02 15:06:05 -07:00
parent 760b6e76cc
commit 3062994645
7 changed files with 79 additions and 42 deletions

View File

@@ -659,6 +659,10 @@ eval_res_t parser_t::eval_node(const parsed_source_ref_t &ps, const T &node,
}
}
// If we are provided a cancellation group, use it; otherwise create one.
cancellation_group_ref_t cancel_group =
job_group ? job_group->cancel_group : cancellation_group_t::create();
// A helper to detect if we got a signal.
// This includes both signals sent to fish (user hit control-C while fish is foreground) and
// signals from the job group (e.g. some external job terminated with SIGQUIT).
@@ -666,7 +670,7 @@ eval_res_t parser_t::eval_node(const parsed_source_ref_t &ps, const T &node,
// Did fish itself get a signal?
int sig = signal_check_cancel();
// Has this job group been cancelled?
if (!sig && job_group) sig = job_group->get_cancel_signal();
if (!sig) sig = cancel_group->get_cancel_signal();
return sig;
};
@@ -689,8 +693,8 @@ eval_res_t parser_t::eval_node(const parsed_source_ref_t &ps, const T &node,
// Create and set a new execution context.
using exc_ctx_ref_t = std::unique_ptr<parse_execution_context_t>;
scoped_push<exc_ctx_ref_t> exc(&execution_context,
make_unique<parse_execution_context_t>(ps, op_ctx, block_io));
scoped_push<exc_ctx_ref_t> exc(&execution_context, make_unique<parse_execution_context_t>(
ps, op_ctx, cancel_group, block_io));
// Check the exec count so we know if anything got executed.
const size_t prev_exec_count = libdata().exec_count;
@@ -699,22 +703,12 @@ eval_res_t parser_t::eval_node(const parsed_source_ref_t &ps, const T &node,
const size_t new_exec_count = libdata().exec_count;
const size_t new_status_count = libdata().status_count;
// Check if the execution context stopped due to a signal from a job it created.
// This may come about if the context created a new job group.
// TODO: there are way too many signals flying around, we need to rationalize this.
int signal_from_exec = execution_context->get_cancel_signal();
exc.restore();
this->pop_block(scope_block);
job_reap(*this, false); // reap again
if (signal_from_exec) {
// A job spawned by the execution context got SIGINT or SIGQUIT, which stopped all
// execution.
return proc_status_t::from_signal(signal_from_exec);
} else if (int sig = check_cancel_signal()) {
// We were signalled.
if (int sig = check_cancel_signal()) {
return proc_status_t::from_signal(sig);
} else {
auto status = proc_status_t::from_exit_code(this->get_last_status());