Rename job_tree to job_group

Initially I wanted to pick a different name to avoid confusion with
process groups, but really job trees *are* process groups. So name them
to reflect that fact.

Also rename "placeholder" to "internal" which is clearer.
This commit is contained in:
ridiculousfish 2020-05-30 14:05:07 -07:00
parent b119c4b3bb
commit 7cc99a2d80
13 changed files with 99 additions and 101 deletions

View file

@ -59,7 +59,7 @@ int builtin_eval(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
}
int status = STATUS_CMD_OK;
auto res = parser.eval(new_cmd, ios, streams.job_tree);
auto res = parser.eval(new_cmd, ios, streams.job_group);
if (res.was_empty) {
// Issue #5692, in particular, to catch `eval ""`, `eval "begin; end;"`, etc.
// where we have an argument but nothing is executed.

View file

@ -175,7 +175,7 @@ static void internal_exec(env_stack_t &vars, job_t *j, const io_chain_t &block_i
/// If our pgroup assignment mode wants us to use the first external proc, then apply it here.
/// \returns the job's pgid, which should always be set to something valid after this call.
static pid_t maybe_assign_pgid_from_child(const std::shared_ptr<job_t> &j, pid_t child_pid) {
auto &jt = j->job_tree;
auto &jt = j->group;
if (jt->needs_pgid_assignment()) {
jt->set_pgid(child_pid);
}
@ -309,8 +309,7 @@ bool blocked_signals_for_job(const job_t &job, sigset_t *sigmask) {
static bool fork_child_for_process(const std::shared_ptr<job_t> &job, process_t *p,
const dup2_list_t &dup2s, const char *fork_type,
const std::function<void()> &child_action) {
assert(!job->job_tree->is_placeholder() &&
"Placeholders are for internal functions, they should never fork");
assert(!job->group->is_internal() && "Internal groups should never need to fork");
pid_t pid = execute_fork();
if (pid == 0) {
// This is the child process. Setup redirections, print correct output to
@ -637,15 +636,15 @@ static proc_performer_t get_performer_for_process(process_t *p, job_t *job,
const io_chain_t &io_chain) {
assert((p->type == process_type_t::function || p->type == process_type_t::block_node) &&
"Unexpected process type");
// We want to capture the job tree.
job_tree_ref_t job_tree = job->job_tree;
// We want to capture the job group.
job_group_ref_t job_group = job->group;
if (p->type == process_type_t::block_node) {
const parsed_source_ref_t &source = p->block_node_source;
tnode_t<grammar::statement> node = p->internal_block_node;
assert(source && node && "Process is missing node info");
return [=](parser_t &parser) {
return parser.eval_node(source, node, io_chain, job_tree).status;
return parser.eval_node(source, node, io_chain, job_group).status;
};
} else {
assert(p->type == process_type_t::function);
@ -659,7 +658,7 @@ static proc_performer_t get_performer_for_process(process_t *p, job_t *job,
// Pull out the job list from the function.
tnode_t<grammar::job_list> body = props->func_node.child<1>();
const block_t *fb = function_prepare_environment(parser, *argv, *props);
auto res = parser.eval_node(props->parsed_source, body, io_chain, job_tree);
auto res = parser.eval_node(props->parsed_source, body, io_chain, job_group);
function_restore_environment(parser, fb);
// If the function did not execute anything, treat it as success.
@ -832,7 +831,7 @@ static bool exec_process_in_job(parser_t &parser, process_t *p, const std::share
case process_type_t::builtin: {
io_streams_t builtin_io_streams{stdout_read_limit};
builtin_io_streams.job_tree = j->job_tree;
builtin_io_streams.job_group = j->group;
if (!exec_internal_builtin_proc(parser, p, pipe_read.get(), process_net_io_chain,
builtin_io_streams)) {
return false;
@ -1066,14 +1065,14 @@ static void populate_subshell_output(wcstring_list_t *lst, const io_buffer_t &bu
/// Execute \p cmd in a subshell in \p parser. If \p lst is not null, populate it with the output.
/// Return $status in \p out_status.
/// If \p job_tree is set, any spawned commands should join that job tree.
/// If \p job_group is set, any spawned commands should join that job group.
/// If \p apply_exit_status is false, then reset $status back to its original value.
/// \p is_subcmd controls whether we apply a read limit.
/// \p break_expand is used to propagate whether the result should be "expansion breaking" in the
/// sense that subshells used during string expansion should halt that expansion. \return the value
/// of $status.
static int exec_subshell_internal(const wcstring &cmd, parser_t &parser,
const job_tree_ref_t &job_tree, wcstring_list_t *lst,
const job_group_ref_t &job_group, wcstring_list_t *lst,
bool *break_expand, bool apply_exit_status, bool is_subcmd) {
ASSERT_IS_MAIN_THREAD();
auto &ld = parser.libdata();
@ -1097,7 +1096,7 @@ static int exec_subshell_internal(const wcstring &cmd, parser_t &parser,
*break_expand = true;
return STATUS_CMD_ERROR;
}
eval_res_t eval_res = parser.eval(cmd, io_chain_t{bufferfill}, job_tree, block_type_t::subst);
eval_res_t eval_res = parser.eval(cmd, io_chain_t{bufferfill}, job_group, block_type_t::subst);
std::shared_ptr<io_buffer_t> buffer = io_bufferfill_t::finish(std::move(bufferfill));
if (buffer->buffer().discarded()) {
*break_expand = true;
@ -1116,11 +1115,11 @@ static int exec_subshell_internal(const wcstring &cmd, parser_t &parser,
return eval_res.status.status_value();
}
int exec_subshell_for_expand(const wcstring &cmd, parser_t &parser, const job_tree_ref_t &job_tree,
wcstring_list_t &outputs) {
int exec_subshell_for_expand(const wcstring &cmd, parser_t &parser,
const job_group_ref_t &job_group, wcstring_list_t &outputs) {
ASSERT_IS_MAIN_THREAD();
bool break_expand = false;
int ret = exec_subshell_internal(cmd, parser, job_tree, &outputs, &break_expand, true, true);
int ret = exec_subshell_internal(cmd, parser, job_group, &outputs, &break_expand, true, true);
// Only return an error code if we should break expansion.
return break_expand ? ret : STATUS_CMD_OK;
}

View file

@ -31,8 +31,8 @@ int exec_subshell(const wcstring &cmd, parser_t &parser, wcstring_list_t &output
/// "success" (even though the command may have failed), a non-zero return means that we should
/// halt expansion. If the \p pgid is supplied, then any spawned external commands should join that
/// pgroup.
int exec_subshell_for_expand(const wcstring &cmd, parser_t &parser, const job_tree_ref_t &job_tree,
wcstring_list_t &outputs);
int exec_subshell_for_expand(const wcstring &cmd, parser_t &parser,
const job_group_ref_t &job_group, wcstring_list_t &outputs);
/// Loops over close until the syscall was run without being interrupted.
void exec_close(int fd);

View file

@ -622,7 +622,7 @@ static expand_result_t expand_cmdsubst(wcstring input, const operation_context_t
wcstring_list_t sub_res;
const wcstring subcmd(paren_begin + 1, paren_end - paren_begin - 1);
int subshell_status = exec_subshell_for_expand(subcmd, *ctx.parser, ctx.job_tree, sub_res);
int subshell_status = exec_subshell_for_expand(subcmd, *ctx.parser, ctx.job_group, sub_res);
if (subshell_status != 0) {
// TODO: Ad-hoc switch, how can we enumerate the possible errors more safely?
const wchar_t *err;

View file

@ -21,7 +21,7 @@
using std::shared_ptr;
class job_tree_t;
class job_group_t;
/// A simple set of FDs.
struct fd_set_t {
@ -464,10 +464,10 @@ struct io_streams_t {
// Actual IO redirections. This is only used by the source builtin. Unowned.
const io_chain_t *io_chain{nullptr};
// The job tree of the job, if any. This enables builtins which run more code like eval() to
// The job group of the job, if any. This enables builtins which run more code like eval() to
// share pgid.
// TODO: this is awkwardly placed.
std::shared_ptr<job_tree_t> job_tree{};
// FIXME: this is awkwardly placed.
std::shared_ptr<job_group_t> job_group{};
// io_streams_t cannot be copied.
io_streams_t(const io_streams_t &) = delete;

View file

@ -7,7 +7,7 @@
class environment_t;
class parser_t;
class job_tree_t;
class job_group_t;
/// A common helper which always returns false.
bool no_cancel();
@ -30,10 +30,10 @@ class operation_context_t {
// context itself.
const environment_t &vars;
/// The job tree of the parental job.
/// The job group of the parental job.
/// This is used only when expanding command substitutions. If this is set, any jobs created by
/// the command substitions should use this tree.
std::shared_ptr<job_tree_t> job_tree{};
std::shared_ptr<job_group_t> job_group{};
// A function which may be used to poll for cancellation.
cancel_checker_t cancel_checker;

View file

@ -1254,7 +1254,7 @@ end_execution_reason_t parse_execution_context_t::run_1_job(tnode_t<g::job> job_
bool wants_job_control =
(job_control_mode == job_control_t::all) ||
((job_control_mode == job_control_t::interactive) && parser->is_interactive()) ||
(ctx.job_tree && ctx.job_tree->wants_job_control());
(ctx.job_group && ctx.job_group->wants_job_control());
job_t::properties_t props{};
props.wants_terminal = wants_job_control && !ld.is_event;
@ -1287,9 +1287,9 @@ end_execution_reason_t parse_execution_context_t::run_1_job(tnode_t<g::job> job_
// Clean up the job on failure or cancellation.
if (pop_result == end_execution_reason_t::ok) {
// Set the pgroup assignment mode and job tree, now that the job is populated.
job_tree_t::populate_tree_for_job(job.get(), ctx.job_tree);
assert(job->job_tree && "Should have a job tree");
// Set the pgroup assignment mode and job group, now that the job is populated.
job_group_t::populate_tree_for_job(job.get(), ctx.job_group);
assert(job->group && "Should have a job group");
// Success. Give the job to the parser - it will clean it up.
parser->job_add(job);

View file

@ -145,7 +145,7 @@ class parse_execution_context_t {
public:
/// Construct a context in preparation for evaluating a node in a tree, with the given block_io.
/// The execution context may access the parser and job_tree through ctx.
/// The execution context may access the parser and group through ctx.
parse_execution_context_t(parsed_source_ref_t pstree, const operation_context_t &ctx,
io_chain_t block_io);

View file

@ -632,12 +632,12 @@ profile_item_t *parser_t::create_profile_item() {
return result;
}
eval_res_t parser_t::eval(const wcstring &cmd, const io_chain_t &io, const job_tree_ref_t &job_tree,
enum block_type_t block_type) {
eval_res_t parser_t::eval(const wcstring &cmd, const io_chain_t &io,
const job_group_ref_t &job_group, enum block_type_t block_type) {
// Parse the source into a tree, if we can.
parse_error_list_t error_list;
if (parsed_source_ref_t ps = parse_source(cmd, parse_flag_none, &error_list)) {
return this->eval(ps, io, job_tree, block_type);
return this->eval(ps, io, job_group, block_type);
} else {
// Get a backtrace. This includes the message.
wcstring backtrace_and_desc;
@ -654,12 +654,12 @@ eval_res_t parser_t::eval(const wcstring &cmd, const io_chain_t &io, const job_t
}
eval_res_t parser_t::eval(const parsed_source_ref_t &ps, const io_chain_t &io,
const job_tree_ref_t &job_tree, enum block_type_t block_type) {
const job_group_ref_t &job_group, enum block_type_t block_type) {
assert(block_type == block_type_t::top || block_type == block_type_t::subst);
if (!ps->tree.empty()) {
// Execute the first node.
tnode_t<grammar::job_list> start{&ps->tree, &ps->tree.front()};
return this->eval_node(ps, start, io, job_tree, block_type);
return this->eval_node(ps, start, io, job_group, block_type);
} else {
auto status = proc_status_t::from_exit_code(get_last_status());
bool break_expand = false;
@ -670,7 +670,7 @@ eval_res_t parser_t::eval(const parsed_source_ref_t &ps, const io_chain_t &io,
template <typename T>
eval_res_t parser_t::eval_node(const parsed_source_ref_t &ps, tnode_t<T> node,
const io_chain_t &block_io, const job_tree_ref_t &job_tree,
const io_chain_t &block_io, const job_group_ref_t &job_group,
block_type_t block_type) {
static_assert(
std::is_same<T, grammar::statement>::value || std::is_same<T, grammar::job_list>::value,
@ -695,8 +695,8 @@ eval_res_t parser_t::eval_node(const parsed_source_ref_t &ps, tnode_t<T> node,
operation_context_t op_ctx = this->context();
block_t *scope_block = this->push_block(block_t::scope_block(block_type));
// Propogate our job tree.
op_ctx.job_tree = job_tree;
// Propogate our job group.
op_ctx.job_group = job_group;
// Create and set a new execution context.
using exc_ctx_ref_t = std::unique_ptr<parse_execution_context_t>;
@ -726,9 +726,9 @@ eval_res_t parser_t::eval_node(const parsed_source_ref_t &ps, tnode_t<T> node,
// Explicit instantiations. TODO: use overloads instead?
template eval_res_t parser_t::eval_node(const parsed_source_ref_t &, tnode_t<grammar::statement>,
const io_chain_t &, const job_tree_ref_t &, block_type_t);
const io_chain_t &, const job_group_ref_t &, block_type_t);
template eval_res_t parser_t::eval_node(const parsed_source_ref_t &, tnode_t<grammar::job_list>,
const io_chain_t &, const job_tree_ref_t &, block_type_t);
const io_chain_t &, const job_group_ref_t &, block_type_t);
void parser_t::get_backtrace(const wcstring &src, const parse_error_list_t &errors,
wcstring &output) const {

View file

@ -282,27 +282,28 @@ class parser_t : public std::enable_shared_from_this<parser_t> {
///
/// \param cmd the string to evaluate
/// \param io io redirections to perform on all started jobs
/// \param job_tree if set, the job tree to give to spawned jobs.
/// \param job_group if set, the job group to give to spawned jobs.
/// \param block_type The type of block to push on the block stack, which must be either 'top'
/// or 'subst'.
/// \param break_expand If not null, return by reference whether the error ought to be an expand
/// error. This includes nested expand errors, and command-not-found.
///
/// \return the result of evaluation.
eval_res_t eval(const wcstring &cmd, const io_chain_t &io, const job_tree_ref_t &job_tree = {},
eval_res_t eval(const wcstring &cmd, const io_chain_t &io,
const job_group_ref_t &job_group = {},
block_type_t block_type = block_type_t::top);
/// Evaluate the parsed source ps.
/// Because the source has been parsed, a syntax error is impossible.
eval_res_t eval(const parsed_source_ref_t &ps, const io_chain_t &io,
const job_tree_ref_t &job_tree = {},
const job_group_ref_t &job_group = {},
block_type_t block_type = block_type_t::top);
/// Evaluates a node.
/// The node type must be grammar::statement or grammar::job_list.
template <typename T>
eval_res_t eval_node(const parsed_source_ref_t &ps, tnode_t<T> node, const io_chain_t &block_io,
const job_tree_ref_t &job_tree,
const job_group_ref_t &job_group,
block_type_t block_type = block_type_t::top);
/// Evaluate line as a list of parameters, i.e. tokenize it and perform parameter expansion and

View file

@ -205,11 +205,11 @@ bool fork_actions_make_spawn_properties(posix_spawnattr_t *attr,
// desired_pgid tracks the pgroup for the process. If it is none, the pgroup is left unchanged.
// If it is zero, create a new pgroup from the pid. If it is >0, join that pgroup.
maybe_t<pid_t> desired_pgid = none();
if (auto job_pgid = j->job_tree->get_pgid()) {
if (auto job_pgid = j->group->get_pgid()) {
desired_pgid = *job_pgid;
} else {
assert(j->job_tree->needs_pgid_assignment() && "We should be expecting a pgid");
// We are the first external proc in the job tree. Set the desired_pgid to 0 to indicate we
assert(j->group->needs_pgid_assignment() && "We should be expecting a pgid");
// We are the first external proc in the job group. Set the desired_pgid to 0 to indicate we
// should creating a new process group.
desired_pgid = 0;
}

View file

@ -164,7 +164,7 @@ bool job_t::should_report_process_exits() const {
return false;
}
bool job_t::job_chain_is_fully_constructed() const { return job_tree->is_root_constructed(); }
bool job_t::job_chain_is_fully_constructed() const { return group->is_root_constructed(); }
bool job_t::signal(int signal) {
// Presumably we are distinguishing between the two cases below because we do
@ -241,38 +241,38 @@ void print_exit_warning_for_jobs(const job_list_t &jobs) {
fputws(_(L"Use 'disown PID' to remove jobs from the list without terminating them.\n"), stdout);
}
job_tree_t::job_tree_t(bool job_control, bool placeholder)
job_group_t::job_group_t(bool job_control, bool internal)
: job_control_(job_control),
is_placeholder_(placeholder),
job_id_(placeholder ? -1 : acquire_job_id()) {}
is_internal_(internal),
job_id_(internal ? -1 : acquire_job_id()) {}
job_tree_t::~job_tree_t() {
job_group_t::~job_group_t() {
if (job_id_ > 0) {
release_job_id(job_id_);
}
}
void job_tree_t::set_pgid(pid_t pgid) {
// Note we need not be concerned about thread safety. job_trees are intended to be shared across
// threads, but their pgid should always have been set beforehand.
void job_group_t::set_pgid(pid_t pgid) {
// Note we need not be concerned about thread safety. job_groups are intended to be shared
// across threads, but their pgid should always have been set beforehand.
assert(needs_pgid_assignment() && "We should not be setting a pgid");
assert(pgid >= 0 && "Invalid pgid");
pgid_ = pgid;
}
maybe_t<pid_t> job_tree_t::get_pgid() const { return pgid_; }
maybe_t<pid_t> job_group_t::get_pgid() const { return pgid_; }
void job_tree_t::populate_tree_for_job(job_t *job, const job_tree_ref_t &proposed) {
void job_group_t::populate_tree_for_job(job_t *job, const job_group_ref_t &proposed) {
// Note there's three cases to consider:
// nullptr -> this is a root job, there is no inherited job tree
// placeholder -> the parent is running as part of a simple function execution
// We may need to create a new job tree if we are going to fork.
// non-placeholder -> we are running as part of a real pipeline
// Decide if this job can use the placeholder tree.
// nullptr -> this is a root job, there is no inherited job group
// internal -> the parent is running as part of a simple function execution
// We may need to create a new job group if we are going to fork.
// non-internal -> we are running as part of a real pipeline
// Decide if this job can use an internal tree.
// This is true if it's a simple foreground execution of an internal proc.
bool first_proc_internal = job->processes.front()->is_internal();
bool can_use_placeholder = !job->is_initially_background() && job->processes.size() == 1 &&
job->processes.front()->is_internal();
bool can_use_internal = !job->is_initially_background() && job->processes.size() == 1 &&
job->processes.front()->is_internal();
bool needs_new_tree = false;
if (!proposed) {
@ -281,8 +281,8 @@ void job_tree_t::populate_tree_for_job(job_t *job, const job_tree_ref_t &propose
} else if (!job->is_foreground()) {
// Background jobs always get a new tree.
needs_new_tree = true;
} else if (proposed->is_placeholder() && !can_use_placeholder) {
// We cannot use the placeholder tree for this job.
} else if (proposed->is_internal() && !can_use_internal) {
// We cannot use the internal tree for this job.
needs_new_tree = true;
}
@ -290,18 +290,18 @@ void job_tree_t::populate_tree_for_job(job_t *job, const job_tree_ref_t &propose
bool job_control = job->wants_job_control();
if (!needs_new_tree) {
job->job_tree = proposed;
} else if (can_use_placeholder) {
job->job_tree.reset(new job_tree_t(job_control, true));
job->group = proposed;
} else if (can_use_internal) {
job->group.reset(new job_group_t(job_control, true));
} else {
job->job_tree.reset(new job_tree_t(job_control, false));
job->group.reset(new job_group_t(job_control, false));
// Perhaps this job should immediately live in fish's pgroup.
// There's two reasons why it may be so:
// 1. The job doesn't need job control.
// 2. The first process in the job is internal to fish; this needs to own the tty.
if (!job_control || first_proc_internal) {
job->job_tree->set_pgid(getpgrp());
job->group->set_pgid(getpgrp());
}
}
}
@ -390,7 +390,7 @@ void job_t::mark_constructed() {
assert(!is_constructed() && "Job was already constructed");
mut_flags().constructed = true;
if (flags().is_tree_root) {
job_tree->mark_root_constructed();
group->mark_root_constructed();
}
}

View file

@ -160,26 +160,25 @@ using job_id_t = int;
job_id_t acquire_job_id(void);
void release_job_id(job_id_t jid);
/// job_tree_t is conceptually similar to the idea of a process group. It represents data which
/// job_group_t is conceptually similar to the idea of a process group. It represents data which
/// is shared among all of the "subjobs" that may be spawned by a single job.
/// For example, two fish functions in a pipeline may themselves spawn multiple jobs, but all will
/// share the same job tree.
/// There is also a notion of a "placeholder" job tree. Placeholders are used when executing a
/// foreground function or block. These are not jobs as the user understands them - they do not
/// consume a job ID, they do not show up in job lists, and they do not have a pgid because they
/// contain no external procs.
/// Note that job_tree_t is intended to eventually be shared between threads, and so must be thread
/// safe.
/// share the same job group.
/// There is also a notion of a "internal" job group. Internal groups are used when executing a
/// foreground function or block with no pipeline. These are not jobs as the user understands them -
/// they do not consume a job ID, they do not show up in job lists, and they do not have a pgid
/// because they contain no external procs. Note that job_group_t is intended to eventually be
/// shared between threads, and so must be thread safe.
class job_t;
class job_tree_t;
using job_tree_ref_t = std::shared_ptr<job_tree_t>;
class job_group_t;
using job_group_ref_t = std::shared_ptr<job_group_t>;
class job_tree_t {
class job_group_t {
public:
/// Set the pgid for this job tree, latching it to this value.
/// Set the pgid for this job group, latching it to this value.
/// The pgid should not already have been set.
/// Of course this does not keep the pgid alive by itself.
/// The placeholder job tree does not have a pgid and it is an error to set it.
/// An internal job group does not have a pgid and it is an error to set it.
void set_pgid(pid_t pgid);
/// Get the pgid, or none() if it has not been set.
@ -188,12 +187,12 @@ class job_tree_t {
/// \return whether we want job control
bool wants_job_control() const { return job_control_; }
/// \return whether this is a placeholder.
bool is_placeholder() const { return is_placeholder_; }
/// \return whether this is an internal group.
bool is_internal() const { return is_internal_; }
/// \return whether this job tree is awaiting a pgid.
/// This is true for non-placeholder trees that don't already have a pgid.
bool needs_pgid_assignment() const { return !is_placeholder_ && !pgid_.has_value(); }
/// \return whether this job group is awaiting a pgid.
/// This is true for non-internal trees that don't already have a pgid.
bool needs_pgid_assignment() const { return !is_internal_ && !pgid_.has_value(); }
/// \return the job ID, or -1 if none.
job_id_t get_id() const { return job_id_; }
@ -204,20 +203,20 @@ class job_tree_t {
void mark_root_constructed() { root_constructed_ = true; };
bool is_root_constructed() const { return root_constructed_; }
/// Given a job and a proposed job tree (possibly null), populate the job's tree.
/// Given a job and a proposed job group (possibly null), populate the job's tree.
/// The proposed tree is the tree from the parent job, or null if this is a root.
static void populate_tree_for_job(job_t *job, const job_tree_ref_t &proposed_tree);
static void populate_tree_for_job(job_t *job, const job_group_ref_t &proposed_tree);
~job_tree_t();
~job_group_t();
private:
maybe_t<pid_t> pgid_{};
const bool job_control_;
const bool is_placeholder_;
const bool is_internal_;
const job_id_t job_id_;
relaxed_atomic_bool_t root_constructed_{};
explicit job_tree_t(bool job_control, bool placeholder);
explicit job_group_t(bool job_control, bool internal);
};
/// A structure representing a single fish process. Contains variables for tracking process state
@ -347,7 +346,6 @@ using job_id_t = int;
job_id_t acquire_job_id(void);
void release_job_id(job_id_t jid);
/// A struct representing a job. A job is a pipeline of one or more processes.
class job_t {
public:
@ -439,22 +437,22 @@ class job_t {
/// All the processes in this job.
process_list_t processes;
// The tree containing this job.
// The group containing this job.
// This is never null and not changed after construction.
job_tree_ref_t job_tree{};
job_group_ref_t group{};
/// Process group ID for the process group that this job is running in.
/// Set to a nonexistent, non-return-value of getpgid() integer by the constructor
// pid_t pgid{INVALID_PID};
/// \return the pgid for the job, based on the job tree.
/// \return the pgid for the job, based on the job group.
/// This may be none if the job consists of just internal fish functions or builtins.
/// This may also be fish itself.
maybe_t<pid_t> get_pgid() const { return job_tree->get_pgid(); }
maybe_t<pid_t> get_pgid() const { return group->get_pgid(); }
/// The id of this job.
/// This is user-visible, is recycled, and may be -1.
job_id_t job_id() const { return job_tree->get_id(); }
job_id_t job_id() const { return group->get_id(); }
/// A non-user-visible, never-recycled job ID.
const internal_job_id_t internal_job_id;