mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-06-08 19:31:14 -03:00
Associate external commands in functions with extant pgrps
When a function is encountered by exec_job, a new context is created for its execution from the ground up, with a new job and all, ultimately resulting in a recursive call to exec_job from the same (main) thread. Since each time exec_job encounters a new job with external commands that needs terminal control it creates a new pgrp and gives it control of the terminal (tcsetpgrp & co), this effectively takes control away from the previously spawned external commands which may be (and likely are) expecting to still have terminal access. This commit attempts to detect when such a situation arises by handling recursive calls to exec_job (which can only happen if the pipeline included a function) by borrowing the pgrp from the (necessarily still active) parent job and spawning new external commands into it. When a parent job spawns new jobs due to the evaluation of a new function (which shouldn't be the case in the first place), we end up with two distinct jobs sharing one pgrp (to fix #3952). This can lead to early termination of a pgrp if finished parent job children are reaped before future processes in either the parent or future child jobs can join it. While the parent job is under construction, require that waitpid(2) calls for the child job be done by process id and not job pgrp. Closes #3952.
This commit is contained in:
64
src/exec.cpp
64
src/exec.cpp
@@ -17,6 +17,7 @@
|
||||
#include <string.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <stack>
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
@@ -610,7 +611,8 @@ static bool handle_builtin_output(job_t *j, process_t *p, io_chain_t *io_chain,
|
||||
if (fork_was_skipped) {
|
||||
p->completed = 1;
|
||||
if (p->is_last_in_job) {
|
||||
debug(4, L"Set status of job %d (%ls) to %d using short circuit", j->job_id, j->preview().c_str(), p->status);
|
||||
debug(4, L"Set status of job %d (%ls) to %d using short circuit", j->job_id,
|
||||
j->preview().c_str(), p->status);
|
||||
|
||||
int status = p->status;
|
||||
proc_set_last_status(j->get_flag(job_flag_t::NEGATE) ? (!status) : status);
|
||||
@@ -996,6 +998,47 @@ void exec_job(parser_t &parser, shared_ptr<job_t> j) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unfortunately `exec_job()` is called recursively when functions are encountered, with a new
|
||||
// job id (and therefore pgrp) each time, but always from the main thread. This breaks terminal
|
||||
// control since new pgrps take terminal control away from commands upstream in a different pgrp.
|
||||
// We try to work around this with a heuristic to determine whether to reuse the same pgrp as the
|
||||
// last-spawned pgrp if part of an existing job pipeline (keeping in mind that new jobs are
|
||||
// recursively started for both foreground and background jobs, and that a function can expand
|
||||
// to more than one external command, one (or more?) of which may want to read from upstream or
|
||||
// write to downstream of a pipe.
|
||||
// By keeping track of (select) "jobs in flight" we can try to marshall newly-created external
|
||||
// processes into existing pgrps. Fixes #3952.
|
||||
// This is a HACK and the correct solution would be to pass the active job through the pipeline
|
||||
// to the newly established parser context so that the funtion as parsed and evaluated can be
|
||||
// directly associated with this job and not a new one, BUT sometimes functions NEED to start a
|
||||
// new job. This HACK seeks a compromise by letting functions trigger the unilateral creation of
|
||||
// a new job, but reusing the "parent" job's existing pgrp in case of terminal control.
|
||||
static std::stack<decltype(j)> active_jobs;
|
||||
// There's an assumption that there's a one-to-one mapping between jobs under job control and
|
||||
// pgrps. When we share a parent job's pgrp, we risk reaping its processes before it is fully
|
||||
// constructed, causing later setpgrp(2) calls to fails (#5219). While the parent job is still
|
||||
// under construction, child jobs have job_flag_t::WAIT_BY_PROCESS set to prevent early repaing.
|
||||
// We store them here until the parent job is constructed, at which point it unsets this flag.
|
||||
static std::stack<decltype(j)> child_jobs;
|
||||
|
||||
auto parent_job = active_jobs.empty() ? nullptr : active_jobs.top();
|
||||
bool job_pushed = false;
|
||||
if (j->get_flag(job_flag_t::TERMINAL) && j->get_flag(job_flag_t::JOB_CONTROL)) {
|
||||
// This will be popped before this job leaves exec_job
|
||||
active_jobs.push(j);
|
||||
job_pushed = true;
|
||||
}
|
||||
|
||||
if (parent_job && j->processes.front()->type == EXTERNAL) {
|
||||
if (parent_job->pgid != INVALID_PID) {
|
||||
j->pgid = parent_job->pgid;
|
||||
j->set_flag(job_flag_t::JOB_CONTROL, true);
|
||||
j->set_flag(job_flag_t::NESTED, true);
|
||||
j->set_flag(job_flag_t::WAIT_BY_PROCESS, true);
|
||||
child_jobs.push(j);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that all IO_BUFFERs are output. We used to support a (single, hacked-in) magical input
|
||||
// IO_BUFFER used by fish_pager, but now the claim is that there are no more clients and it is
|
||||
// removed. This assertion double-checks that.
|
||||
@@ -1053,8 +1096,8 @@ void exec_job(parser_t &parser, shared_ptr<job_t> j) {
|
||||
}
|
||||
pipe_next_read.close();
|
||||
|
||||
|
||||
debug(3, L"Created job %d from command '%ls' with pgrp %d", j->job_id, j->command_wcstr(), j->pgid);
|
||||
debug(3, L"Created job %d from command '%ls' with pgrp %d", j->job_id, j->command_wcstr(),
|
||||
j->pgid);
|
||||
|
||||
j->set_flag(job_flag_t::CONSTRUCTED, true);
|
||||
if (!j->is_foreground()) {
|
||||
@@ -1062,6 +1105,21 @@ void exec_job(parser_t &parser, shared_ptr<job_t> j) {
|
||||
env_set(L"last_pid", ENV_GLOBAL, { to_string(proc_last_bg_pid) });
|
||||
}
|
||||
|
||||
if (job_pushed) {
|
||||
active_jobs.pop();
|
||||
}
|
||||
|
||||
if (!parent_job) {
|
||||
// Unset WAIT_BY_PROCESS on all child jobs. We could leave it, but this speeds up the
|
||||
// execution of `process_mark_finished_children()`.
|
||||
while (!child_jobs.empty()) {
|
||||
auto child = child_jobs.top();
|
||||
child_jobs.pop();
|
||||
|
||||
child->set_flag(job_flag_t::WAIT_BY_PROCESS, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!exec_error) {
|
||||
j->continue_job(false);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user