fish-shell/src/builtin_jobs.cpp

239 lines
7.9 KiB
C++
Raw Normal View History

// Functions for executing the jobs builtin.
#include "config.h" // IWYU pragma: keep
#include <sys/time.h>
#include <cerrno>
#include <cstddef>
#include "builtin.h"
#include "common.h"
#include "fallback.h" // IWYU pragma: keep
#include "io.h"
#include "parser.h"
#include "proc.h"
#include "wgetopt.h"
#include "wutil.h" // IWYU pragma: keep
class parser_t;
/// Print modes for the jobs builtin.
enum {
JOBS_DEFAULT, // print lots of general info
JOBS_PRINT_PID, // print pid of each process in job
JOBS_PRINT_COMMAND, // print command name of each process in job
JOBS_PRINT_GROUP, // print group id of job
JOBS_PRINT_NOTHING, // print nothing (exit status only)
};
/// Calculates the cpu usage (in percent) of the specified job.
static int cpu_use(const job_t *j) {
double u = 0;
for (const process_ptr_t &p : j->processes) {
struct timeval t;
int jiffies;
gettimeofday(&t, nullptr);
jiffies = proc_get_jiffies(p.get());
double t1 = 1000000.0 * p->last_time.tv_sec + p->last_time.tv_usec;
double t2 = 1000000.0 * t.tv_sec + t.tv_usec;
// std::fwprintf( stderr, L"t1 %f t2 %f p1 %d p2 %d\n", t1, t2, jiffies, p->last_jiffies );
u += (static_cast<double>(jiffies - p->last_jiffies)) / (t2 - t1);
}
return u * 1000000;
}
/// Print information about the specified job.
static void builtin_jobs_print(const job_t *j, int mode, int header, io_streams_t &streams) {
switch (mode) {
case JOBS_PRINT_NOTHING: {
break;
}
case JOBS_DEFAULT: {
if (header) {
// Print table header before first job.
streams.out.append(_(L"Job\tGroup\t"));
if (have_proc_stat()) {
streams.out.append(_(L"CPU\t"));
}
streams.out.append(_(L"State\tCommand\n"));
2012-11-19 08:31:03 +00:00
}
Introduce the internal jobs for functions This PR is aimed at improving how job ids are assigned. In particular, previous to this commit, a job id would be consumed by functions (and thus aliases). Since it's usual to use functions as command wrappers this results in awkward job id assignments. For example if the user is like me and just made the jump from vim -> neovim then the user might create the following alias: ``` alias vim=nvim ``` Previous to this commit if the user ran `vim` after setting up this alias, backgrounded (^Z) and ran `jobs` then the output might be: ``` Job Group State Command 2 60267 stopped nvim $argv ``` If the user subsequently opened another vim (nvim) session, backgrounded and ran jobs then they might see what follows: ``` Job Group State Command 4 70542 stopped nvim $argv 2 60267 stopped nvim $argv ``` These job ids feel unnatural, especially when transitioning away from e.g. bash where job ids are sequentially incremented (and aliases/functions don't consume a job id). See #6053 for more details. As @ridiculousfish pointed out in https://github.com/fish-shell/fish-shell/issues/6053#issuecomment-559899400, we want to elide a job's job id if it corresponds to a single function in the foreground. This translates to the following prerequisites: - A job must correspond to a single process (i.e. the job continuation must be empty) - A job must be in the foreground (i.e. `&` wasn't appended) - The job's single process must resolve to a function invocation If all of these conditions are true then we should mark a job as "internal" and somehow remove it from consideration when any infrastructure tries to interact with jobs / job ids. I saw two paths to implement these requirements: - At the time of job creation calculate whether or not a job is "internal" and use a separate list of job ids to track their ids. Additionally introduce a new flag denoting that a job is internal so that e.g. `jobs` doesn't list internal jobs - I started implementing this route but quickly realized I was computing the same information that would be computed later on (e.g. "is this job a single process" and "is this jobs statement a function"). Specifically I was computing data that populate_job_process would end up computing later anyway. Additionally this added some weird complexities to the job system (after the change there were two job id lists AND an additional flag that had to be taken into consideration) - Once a function is about to be executed we release the current jobs job id if the prerequisites are satisfied (which at this point have been fully computed). - I opted for this solution since it seems cleaner. In this implementation "releasing a job id" is done by both calling `release_job_id` and by marking the internal job_id member variable to -1. The former operation allows subsequent child jobs to reuse that same job id (so e.g. the situation described in Motivation doesn't occur), and the latter ensures that no other job / job id infrastructure will interact with these jobs because valid jobs have positive job ids. The second operation causes job_id to become non-const which leads to the list of code changes outside of `exec.c` (i.e. a codemod from `job_t::job_id` -> `job_t::job_id()` and moving the old member variable to a non-const private `job_t::job_id_`) Note: Its very possible I missed something and setting the job id to -1 will break some other infrastructure, please let me know if so! I tried to run `make/ninja lint`, but a bunch of non-relevant issues appeared (e.g. `fatal error: 'config.h' file not found`). I did successfully clang-format (`git clang-format -f`) and run tests, though. This PR closes #6053.
2019-12-29 15:46:07 +00:00
streams.out.append_format(L"%d\t%d\t", j->job_id(), j->pgid);
if (have_proc_stat()) {
streams.out.append_format(L"%d%%\t", cpu_use(j));
}
streams.out.append(j->is_stopped() ? _(L"stopped") : _(L"running"));
streams.out.append(L"\t");
streams.out.append(j->command_wcstr());
streams.out.append(L"\n");
2012-11-19 08:31:03 +00:00
break;
}
case JOBS_PRINT_GROUP: {
if (header) {
// Print table header before first job.
streams.out.append(_(L"Group\n"));
2012-11-19 08:31:03 +00:00
}
streams.out.append_format(L"%d\n", j->pgid);
2012-11-19 08:31:03 +00:00
break;
}
case JOBS_PRINT_PID: {
if (header) {
// Print table header before first job.
streams.out.append(_(L"Process\n"));
2012-11-19 08:31:03 +00:00
}
for (const process_ptr_t &p : j->processes) {
streams.out.append_format(L"%d\n", p->pid);
2012-11-19 08:31:03 +00:00
}
break;
}
case JOBS_PRINT_COMMAND: {
if (header) {
// Print table header before first job.
streams.out.append(_(L"Command\n"));
2012-11-19 08:31:03 +00:00
}
for (const process_ptr_t &p : j->processes) {
streams.out.append_format(L"%ls\n", p->argv0());
2012-11-19 08:31:03 +00:00
}
break;
}
default: {
DIE("unexpected mode");
break;
}
}
}
/// The jobs builtin. Used for printing running jobs. Defined in builtin_jobs.c.
int builtin_jobs(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
2017-06-14 19:26:05 +00:00
wchar_t *cmd = argv[0];
2017-06-10 19:30:09 +00:00
int argc = builtin_count_args(argv);
bool found = false;
int mode = JOBS_DEFAULT;
int print_last = 0;
static const wchar_t *const short_options = L":cghlpq";
static const struct woption long_options[] = {{L"command", no_argument, nullptr, 'c'},
{L"group", no_argument, nullptr, 'g'},
{L"help", no_argument, nullptr, 'h'},
{L"last", no_argument, nullptr, 'l'},
{L"pid", no_argument, nullptr, 'p'},
{L"quiet", no_argument, nullptr, 'q'},
{nullptr, 0, nullptr, 0}};
2017-06-10 19:30:09 +00:00
int opt;
wgetopter_t w;
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) {
switch (opt) {
case 'p': {
mode = JOBS_PRINT_PID;
2012-11-19 08:31:03 +00:00
break;
}
case 'q': {
mode = JOBS_PRINT_NOTHING;
break;
}
case 'c': {
mode = JOBS_PRINT_COMMAND;
2012-11-19 08:31:03 +00:00
break;
}
case 'g': {
mode = JOBS_PRINT_GROUP;
2012-11-19 08:31:03 +00:00
break;
}
case 'l': {
2012-11-19 08:31:03 +00:00
print_last = 1;
break;
}
case 'h': {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
2017-06-30 04:49:57 +00:00
case ':': {
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
2017-06-30 04:49:57 +00:00
return STATUS_INVALID_ARGS;
}
case '?': {
2017-06-14 19:26:05 +00:00
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
return STATUS_INVALID_ARGS;
}
default: {
2017-06-10 19:30:09 +00:00
DIE("unexpected retval from wgetopt_long");
break;
}
}
}
if (print_last) {
// Ignore unconstructed jobs, i.e. ourself.
for (const auto &j : parser.jobs()) {
if (j->is_visible()) {
builtin_jobs_print(j.get(), mode, !streams.out_is_redirected, streams);
return STATUS_CMD_OK;
}
}
return STATUS_CMD_ERROR;
} else {
if (w.woptind < argc) {
int i;
for (i = w.woptind; i < argc; i++) {
const job_t *j = nullptr;
if (argv[i][0] == L'%') {
int jobId = -1;
jobId = fish_wcstoi(argv[i] + 1);
if (errno || jobId < -1) {
streams.err.append_format(_(L"%ls: '%ls' is not a valid job id"), cmd,
argv[i]);
return STATUS_INVALID_ARGS;
}
j = job_t::from_job_id(jobId);
} else {
int pid = fish_wcstoi(argv[i]);
if (errno || pid < 0) {
streams.err.append_format(_(L"%ls: '%ls' is not a valid process id\n"), cmd,
argv[i]);
return STATUS_INVALID_ARGS;
}
j = parser.job_get_from_pid(pid);
}
if (j && !j->is_completed() && j->is_constructed()) {
2015-11-25 13:37:48 +00:00
builtin_jobs_print(j, mode, false, streams);
found = true;
} else {
streams.err.append_format(_(L"%ls: No suitable job: %ls\n"), cmd, argv[i]);
return STATUS_CMD_ERROR;
}
}
} else {
for (const auto &j : parser.jobs()) {
// Ignore unconstructed jobs, i.e. ourself.
if (j->is_visible()) {
builtin_jobs_print(j.get(), mode, !found && !streams.out_is_redirected,
streams);
found = true;
}
}
}
}
if (!found) {
// Do not babble if not interactive.
if (!streams.out_is_redirected && mode != JOBS_PRINT_NOTHING) {
streams.out.append_format(_(L"%ls: There are no jobs\n"), argv[0]);
2015-11-25 13:37:48 +00:00
}
return STATUS_CMD_ERROR;
}
return STATUS_CMD_OK;
}