/// Returns control of the terminal to the shell, and saves the terminal attribute state to the job, /// so that we can restore the terminal ownership to the job at a later time. static int terminal_return_from_job(job_t *j) { if (tcsetpgrp(STDIN_FILENO, getpgrp()) == -1) { if (errno == ENOTTY) redirect_tty_output(); debug(1, _(L"Could not return shell to foreground")); wperror(L"tcsetpgrp"); return 0; } // Save jobs terminal modes. if (tcgetattr(STDIN_FILENO, &j->tmodes)) { if (errno == ENOTTY) redirect_tty_output(); debug(1, _(L"Could not return shell to foreground")); wperror(L"tcgetattr"); return 0; } // Disabling this per // https://github.com/adityagodbole/fish-shell/commit/9d229cd18c3e5c25a8bd37e9ddd3b67ddc2d1b72 On // Linux, 'cd . ; ftp' prevents you from typing into the ftp prompt. See // https://github.com/fish-shell/fish-shell/issues/121 #if 0 // Restore the shell's terminal modes. if (tcsetattr(STDIN_FILENO, TCSADRAIN, &shell_modes)) { debug(1, _(L"Could not return shell to foreground")); wperror(L"tcsetattr"); return 0; } #endif return 1; }
/// Give ownership of the terminal to the specified job. /// /// \param j The job to give the terminal to. /// \param cont If this variable is set, we are giving back control to a job that has previously /// been stopped. In that case, we need to set the terminal attributes to those saved in the job. static bool terminal_give_to_job(job_t *j, int cont) { if (tcsetpgrp(STDIN_FILENO, j->pgid) == -1) { if (errno == ENOTTY) redirect_tty_output(); debug(1, _(L"Could not send job %d ('%ls') to foreground"), j->job_id, j->command_wcstr()); wperror(L"tcsetpgrp"); return false; } if (cont && tcsetattr(STDIN_FILENO, TCSADRAIN, &j->tmodes)) { if (errno == ENOTTY) redirect_tty_output(); debug(1, _(L"Could not send job %d ('%ls') to foreground"), j->job_id, j->command_wcstr()); wperror(L"tcsetattr"); return false; } return true; }
/// Give ownership of the terminal to the specified job. /// /// \param j The job to give the terminal to. /// \param cont If this variable is set, we are giving back control to a job that has previously /// been stopped. In that case, we need to set the terminal attributes to those saved in the job. bool terminal_give_to_job(job_t *j, int cont) { errno = 0; if (j->pgid == 0) { debug(2, "terminal_give_to_job() returning early due to no process group"); return true; } signal_block(); // It may not be safe to call tcsetpgrp if we've already done so, as at that point we are no // longer the controlling process group for the terminal and no longer have permission to set // the process group that is in control, causing tcsetpgrp to return EPERM, even though that's // not the documented behavior in tcsetpgrp(3), which instead says other bad things will happen // (it says SIGTTOU will be sent to all members of the background *calling* process group, but // it's more complicated than that, SIGTTOU may or may not be sent depending on the TTY // configuration and whether or not signal handlers for SIGTTOU are installed. Read: // http://curiousthing.org/sigttin-sigttou-deep-dive-linux In all cases, our goal here was just // to hand over control of the terminal to this process group, which is a no-op if it's already // been done. if (tcgetpgrp(STDIN_FILENO) == j->pgid) { debug(4, L"Process group %d already has control of terminal\n", j->pgid); } else { debug(4, L"Attempting to bring process group to foreground via tcsetpgrp for job->pgid %d\n", j->pgid); // The tcsetpgrp(2) man page says that EPERM is thrown if "pgrp has a supported value, but // is not the process group ID of a process in the same session as the calling process." // Since we _guarantee_ that this isn't the case (the child calls setpgid before it calls // SIGSTOP, and the child was created in the same session as us), it seems that EPERM is // being thrown because of an caching issue - the call to tcsetpgrp isn't seeing the // newly-created process group just yet. On this developer's test machine (WSL running Linux // 4.4.0), EPERM does indeed disappear on retry. The important thing is that we can // guarantee the process isn't going to exit while we wait (which would cause us to possibly // block indefinitely). while (tcsetpgrp(STDIN_FILENO, j->pgid) != 0) { bool pgroup_terminated = false; if (errno == EINTR) { ; // Always retry on EINTR, see comments in tcsetattr EINTR code below. } else if (errno == EINVAL) { // OS X returns EINVAL if the process group no longer lives. Probably other OSes, // too. Unlike EPERM below, EINVAL can only happen if the process group has // terminated. pgroup_terminated = true; } else if (errno == EPERM) { // Retry so long as this isn't because the process group is dead. int wait_result = waitpid(-1 * j->pgid, &wait_result, WNOHANG); if (wait_result == -1) { // Note that -1 is technically an "error" for waitpid in the sense that an // invalid argument was specified because no such process group exists any // longer. This is the observed behavior on Linux 4.4.0. a "success" result // would mean processes from the group still exist but is still running in some // state or the other. pgroup_terminated = true; } else { // Debug the original tcsetpgrp error (not the waitpid errno) to the log, and // then retry until not EPERM or the process group has exited. debug(2, L"terminal_give_to_job(): EPERM.\n", j->pgid); } } else { if (errno == ENOTTY) redirect_tty_output(); debug(1, _(L"Could not send job %d ('%ls') with pgid %d to foreground"), j->job_id, j->command_wcstr(), j->pgid); wperror(L"tcsetpgrp"); signal_unblock(); return false; } if (pgroup_terminated) { // All processes in the process group has exited. Since we force all child procs to // SIGSTOP on startup, the only way that can happen is if the very last process in // the group terminated, and didn't need to access the terminal, otherwise it would // have hung waiting for terminal IO (SIGTTIN). We can ignore this. debug(3, L"tcsetpgrp called but process group %d has terminated.\n", j->pgid); break; } } } if (cont) { int result = -1; // TODO: Remove this EINTR loop since we have blocked all signals and thus cannot be // interrupted. I'm leaving it in place because all of the logic involving controlling // terminal management is more than a little opaque and smacks of voodoo programming. errno = EINTR; while (result == -1 && errno == EINTR) { result = tcsetattr(STDIN_FILENO, TCSADRAIN, &j->tmodes); } if (result == -1) { if (errno == ENOTTY) redirect_tty_output(); debug(1, _(L"Could not send job %d ('%ls') to foreground"), j->job_id, j->command_wcstr()); wperror(L"tcsetattr"); signal_unblock(); return false; } } signal_unblock(); return true; }
/// Handle output from a builtin, by printing the contents of builtin_io_streams to the redirections /// given in io_chain. static bool handle_builtin_output(job_t *j, process_t *p, io_chain_t *io_chain, const io_streams_t &builtin_io_streams) { assert(p->type == INTERNAL_BUILTIN && "Process is not a builtin"); // Handle output from builtin commands. In the general case, this means forking of a // worker process, that will write out the contents of the stdout and stderr buffers // to the correct file descriptor. Since forking is expensive, fish tries to avoid // it when possible. bool fork_was_skipped = false; const shared_ptr<io_data_t> stdout_io = io_chain->get_io_for_fd(STDOUT_FILENO); const shared_ptr<io_data_t> stderr_io = io_chain->get_io_for_fd(STDERR_FILENO); const output_stream_t &stdout_stream = builtin_io_streams.out; const output_stream_t &stderr_stream = builtin_io_streams.err; // If we are outputting to a file, we have to actually do it, even if we have no // output, so that we can truncate the file. Does not apply to /dev/null. bool must_fork = redirection_is_to_real_file(stdout_io.get()) || redirection_is_to_real_file(stderr_io.get()); if (!must_fork && p->is_last_in_job) { // We are handling reads directly in the main loop. Note that we may still end // up forking. const bool stdout_is_to_buffer = stdout_io && stdout_io->io_mode == IO_BUFFER; const bool no_stdout_output = stdout_stream.empty(); const bool no_stderr_output = stderr_stream.empty(); const bool stdout_discarded = stdout_stream.buffer().discarded(); if (!stdout_discarded && no_stdout_output && no_stderr_output) { // The builtin produced no output and is not inside of a pipeline. No // need to fork or even output anything. debug(4, L"Skipping fork: no output for internal builtin '%ls'", p->argv0()); fork_was_skipped = true; } else if (no_stderr_output && stdout_is_to_buffer) { // The builtin produced no stderr, and its stdout is going to an // internal buffer. There is no need to fork. This helps out the // performance quite a bit in complex completion code. // TODO: we're sloppy about handling explicitly separated output. // Theoretically we could have explicitly separated output on stdout and // also stderr output; in that case we ought to thread the exp-sep output // through to the io buffer. We're getting away with this because the only // thing that can output exp-sep output is `string split0` which doesn't // also produce stderr. debug(4, L"Skipping fork: buffered output for internal builtin '%ls'", p->argv0()); io_buffer_t *io_buffer = static_cast<io_buffer_t *>(stdout_io.get()); io_buffer->append_from_stream(stdout_stream); fork_was_skipped = true; } else if (stdout_io.get() == NULL && stderr_io.get() == NULL) { // We are writing to normal stdout and stderr. Just do it - no need to fork. debug(4, L"Skipping fork: ordinary output for internal builtin '%ls'", p->argv0()); const std::string outbuff = wcs2string(stdout_stream.contents()); const std::string errbuff = wcs2string(stderr_stream.contents()); bool builtin_io_done = do_builtin_io(outbuff.data(), outbuff.size(), errbuff.data(), errbuff.size()); if (!builtin_io_done && errno != EPIPE) { redirect_tty_output(); // workaround glibc bug debug(0, "!builtin_io_done and errno != EPIPE"); show_stackframe(L'E'); } if (stdout_discarded) p->status = STATUS_READ_TOO_MUCH; fork_was_skipped = true; } } 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); int status = p->status; proc_set_last_status(j->get_flag(job_flag_t::NEGATE) ? (!status) : status); } } else { // Ok, unfortunately, we have to do a real fork. Bummer. We work hard to make // sure we don't have to wait for all our threads to exit, by arranging things // so that we don't have to allocate memory or do anything except system calls // in the child. // // These strings may contain embedded nulls, so don't treat them as C strings. const std::string outbuff_str = wcs2string(stdout_stream.contents()); const char *outbuff = outbuff_str.data(); size_t outbuff_len = outbuff_str.size(); const std::string errbuff_str = wcs2string(stderr_stream.contents()); const char *errbuff = errbuff_str.data(); size_t errbuff_len = errbuff_str.size(); fflush(stdout); fflush(stderr); if (!fork_child_for_process(j, p, *io_chain, false, "internal builtin", [&] { do_builtin_io(outbuff, outbuff_len, errbuff, errbuff_len); exit_without_destructors(p->status); })) { return false; } } return true; }