mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-29 06:13:20 +00:00
1877 lines
66 KiB
Rust
1877 lines
66 KiB
Rust
//! Utilities for keeping track of jobs, processes and subshells, as well as signal handling
|
|
//! functions for tracking children. These functions do not themselves launch new processes,
|
|
//! the exec library will call proc to create representations of the running jobs as needed.
|
|
|
|
use crate::ast;
|
|
use crate::common::{
|
|
charptr2wcstring, escape, fputws, redirect_tty_output, scoped_push_replacer, timef, Timepoint,
|
|
};
|
|
use crate::compat::cur_term;
|
|
use crate::env::Statuses;
|
|
use crate::event::{self, Event};
|
|
use crate::flog::{FLOG, FLOGF};
|
|
use crate::global_safety::RelaxedAtomicBool;
|
|
use crate::io::IoChain;
|
|
use crate::job_group::{JobGroup, MaybeJobId};
|
|
use crate::parse_tree::ParsedSourceRef;
|
|
use crate::parser::{Block, Parser};
|
|
use crate::reader::{fish_is_unwinding_for_exit, reader_schedule_prompt_repaint};
|
|
use crate::redirection::RedirectionSpecList;
|
|
use crate::signal::{signal_set_handlers_once, Signal};
|
|
use crate::topic_monitor::{topic_monitor_principal, topic_t, GenerationsList};
|
|
use crate::wait_handle::{InternalJobId, WaitHandle, WaitHandleRef, WaitHandleStore};
|
|
use crate::wchar::{wstr, WString, L};
|
|
use crate::wchar_ext::ToWString;
|
|
use crate::wutil::{perror, wbasename, wgettext, wperror};
|
|
use libc::{
|
|
EBADF, EINVAL, ENOTTY, EPERM, EXIT_SUCCESS, SIGABRT, SIGBUS, SIGCONT, SIGFPE, SIGHUP, SIGILL,
|
|
SIGINT, SIGPIPE, SIGQUIT, SIGSEGV, SIGSYS, SIGTTOU, SIG_DFL, SIG_IGN, STDIN_FILENO,
|
|
STDOUT_FILENO, WCONTINUED, WEXITSTATUS, WIFCONTINUED, WIFEXITED, WIFSIGNALED, WIFSTOPPED,
|
|
WNOHANG, WTERMSIG, WUNTRACED, _SC_CLK_TCK,
|
|
};
|
|
use once_cell::sync::Lazy;
|
|
use printf_compat::sprintf;
|
|
use std::cell::{Cell, Ref, RefCell, RefMut};
|
|
use std::fs;
|
|
use std::io::{Read, Write};
|
|
use std::os::fd::RawFd;
|
|
use std::rc::Rc;
|
|
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, AtomicU8, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
use widestring_suffix::widestrs;
|
|
|
|
/// Types of processes.
|
|
#[derive(Default, Eq, PartialEq)]
|
|
pub enum ProcessType {
|
|
/// A regular external command.
|
|
#[default]
|
|
external,
|
|
/// A builtin command.
|
|
builtin,
|
|
/// A shellscript function.
|
|
function,
|
|
/// A block of commands, represented as a node.
|
|
block_node,
|
|
/// The exec builtin.
|
|
exec,
|
|
}
|
|
|
|
#[repr(u8)]
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
pub enum JobControl {
|
|
all,
|
|
interactive,
|
|
none,
|
|
}
|
|
|
|
impl TryFrom<&wstr> for JobControl {
|
|
type Error = ();
|
|
|
|
fn try_from(value: &wstr) -> Result<Self, Self::Error> {
|
|
if value == "full" {
|
|
Ok(JobControl::all)
|
|
} else if value == "interactive" {
|
|
Ok(JobControl::interactive)
|
|
} else if value == "none" {
|
|
Ok(JobControl::none)
|
|
} else {
|
|
Err(())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A number of clock ticks.
|
|
pub type ClockTicks = u64;
|
|
|
|
/// \return clock ticks in seconds, or 0 on failure.
|
|
/// This uses sysconf(_SC_CLK_TCK) to convert to seconds.
|
|
pub fn clock_ticks_to_seconds(ticks: ClockTicks) -> f64 {
|
|
let clock_ticks_per_sec = unsafe { libc::sysconf(_SC_CLK_TCK) };
|
|
if clock_ticks_per_sec > 0 {
|
|
return ticks as f64 / clock_ticks_per_sec as f64;
|
|
}
|
|
0.0
|
|
}
|
|
|
|
pub type JobGroupRef = Arc<JobGroup>;
|
|
|
|
/// A proc_status_t is a value type that encapsulates logic around exited vs stopped vs signaled,
|
|
/// etc.
|
|
///
|
|
/// It contains two fields packed into an AtomicU64 to allow interior mutability, `status: i32` and
|
|
/// `empty: bool`.
|
|
#[derive(Default)]
|
|
pub struct ProcStatus {
|
|
value: AtomicU64,
|
|
}
|
|
|
|
impl Clone for ProcStatus {
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
value: AtomicU64::new(self.value.load(Ordering::Relaxed)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ProcStatus {
|
|
fn new(status: i32, empty: bool) -> Self {
|
|
ProcStatus {
|
|
value: Self::to_u64(status, empty).into(),
|
|
}
|
|
}
|
|
|
|
/// Returns the raw `i32` status value.
|
|
fn status(&self) -> i32 {
|
|
Self::from_u64(self.value.load(Ordering::Relaxed)).0
|
|
}
|
|
|
|
/// Returns the `empty` field.
|
|
///
|
|
/// If `empty` is `true` then there is no actual status to report (e.g. background or variable
|
|
/// assignment).
|
|
pub fn is_empty(&self) -> bool {
|
|
Self::from_u64(self.value.load(Ordering::Relaxed)).1
|
|
}
|
|
|
|
/// Replace the current `ProcStatus` with that of `other`.
|
|
pub fn update(&self, other: &ProcStatus) {
|
|
self.value
|
|
.store(other.value.load(Ordering::Relaxed), Ordering::Relaxed);
|
|
}
|
|
|
|
fn set_status(&self, status: i32) {
|
|
let value = Self::to_u64(status, self.is_empty());
|
|
self.value.store(value, Ordering::Relaxed);
|
|
}
|
|
|
|
fn set_empty(&self, empty: bool) {
|
|
let value = Self::to_u64(self.status(), empty);
|
|
self.value.store(value, Ordering::Relaxed);
|
|
}
|
|
|
|
fn to_u64(status: i32, empty: bool) -> u64 {
|
|
(u64::from(empty) << 32) | u64::from(status as u32)
|
|
}
|
|
|
|
fn from_u64(bits: u64) -> (i32, bool) {
|
|
let status = bits as u32 as i32;
|
|
let empty = (bits >> 32) != 0;
|
|
(status, empty)
|
|
}
|
|
|
|
/// Encode a return value \p ret and signal \p sig into a status value like waitpid() does.
|
|
const fn w_exitcode(ret: i32, sig: i32) -> i32 {
|
|
#[cfg(HAVE_WAITSTATUS_SIGNAL_RET)]
|
|
// It's encoded signal and then status
|
|
// The return status is in the lower byte.
|
|
return (sig << 8) | ret;
|
|
#[cfg(not(HAVE_WAITSTATUS_SIGNAL_RET))]
|
|
// The status is encoded in the upper byte.
|
|
// This should be W_EXITCODE(ret, sig) but that's not available everywhere.
|
|
return (ret << 8) | sig;
|
|
}
|
|
|
|
/// Construct from a status returned from a waitpid call.
|
|
pub fn from_waitpid(status: i32) -> ProcStatus {
|
|
ProcStatus::new(status, false)
|
|
}
|
|
|
|
/// Construct directly from an exit code.
|
|
pub fn from_exit_code(ret: i32) -> ProcStatus {
|
|
assert!(
|
|
ret >= 0,
|
|
"trying to create proc_status_t from failed waitid()/waitpid() call \
|
|
or invalid builtin exit code!"
|
|
);
|
|
|
|
// Some paranoia.
|
|
const zerocode: i32 = ProcStatus::w_exitcode(0, 0);
|
|
const _: () = assert!(
|
|
WIFEXITED(zerocode),
|
|
"Synthetic exit status not reported as exited"
|
|
);
|
|
|
|
assert!(ret < 256);
|
|
ProcStatus::new(Self::w_exitcode(ret, 0 /* sig */), false)
|
|
}
|
|
|
|
/// Construct directly from a signal.
|
|
pub fn from_signal(signal: Signal) -> ProcStatus {
|
|
ProcStatus::new(Self::w_exitcode(0 /* ret */, signal.code()), false)
|
|
}
|
|
|
|
/// Construct an empty status_t (e.g. `set foo bar`).
|
|
pub fn empty() -> ProcStatus {
|
|
let empty = true;
|
|
ProcStatus::new(0, empty)
|
|
}
|
|
|
|
/// \return if we are stopped (as in SIGSTOP).
|
|
pub fn stopped(&self) -> bool {
|
|
WIFSTOPPED(self.status())
|
|
}
|
|
|
|
/// \return if we are continued (as in SIGCONT).
|
|
pub fn continued(&self) -> bool {
|
|
WIFCONTINUED(self.status())
|
|
}
|
|
|
|
/// \return if we exited normally (not a signal).
|
|
pub fn normal_exited(&self) -> bool {
|
|
WIFEXITED(self.status())
|
|
}
|
|
|
|
/// \return if we exited because of a signal.
|
|
pub fn signal_exited(&self) -> bool {
|
|
WIFSIGNALED(self.status())
|
|
}
|
|
|
|
/// \return the signal code, given that we signal exited.
|
|
pub fn signal_code(&self) -> libc::c_int {
|
|
assert!(self.signal_exited(), "Process is not signal exited");
|
|
WTERMSIG(self.status())
|
|
}
|
|
|
|
/// \return the exit code, given that we normal exited.
|
|
pub fn exit_code(&self) -> libc::c_int {
|
|
assert!(self.normal_exited(), "Process is not normal exited");
|
|
WEXITSTATUS(self.status())
|
|
}
|
|
|
|
/// \return if this status represents success.
|
|
pub fn is_success(&self) -> bool {
|
|
self.normal_exited() && self.exit_code() == EXIT_SUCCESS
|
|
}
|
|
|
|
/// \return the value appropriate to populate $status.
|
|
pub fn status_value(&self) -> i32 {
|
|
if self.signal_exited() {
|
|
128 + self.signal_code()
|
|
} else if self.normal_exited() {
|
|
self.exit_code()
|
|
} else {
|
|
panic!("Process is not exited")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A structure representing a "process" internal to fish. This is backed by a pthread instead of a
|
|
/// separate process.
|
|
pub struct InternalProc {
|
|
/// An identifier for internal processes.
|
|
/// This is used for logging purposes only.
|
|
internal_proc_id: u64,
|
|
|
|
/// Whether the process has exited.
|
|
exited: AtomicBool,
|
|
|
|
/// If the process has exited, its status code.
|
|
status: ProcStatus,
|
|
}
|
|
|
|
impl InternalProc {
|
|
pub fn new() -> Self {
|
|
static NEXT_PROC_ID: AtomicU64 = AtomicU64::new(0);
|
|
Self {
|
|
internal_proc_id: NEXT_PROC_ID.fetch_add(1, Ordering::SeqCst),
|
|
exited: AtomicBool::new(false),
|
|
status: ProcStatus::default(),
|
|
}
|
|
}
|
|
|
|
/// \return if this process has exited.
|
|
pub fn exited(&self) -> bool {
|
|
self.exited.load(Ordering::Acquire)
|
|
}
|
|
|
|
/// Mark this process as having exited with the given `status`.
|
|
pub fn mark_exited(&self, status: &ProcStatus) {
|
|
assert!(!self.exited(), "Process is already exited");
|
|
self.status.update(status);
|
|
self.exited.store(true, Ordering::Release);
|
|
topic_monitor_principal().post(topic_t::internal_exit);
|
|
FLOG!(
|
|
proc_internal_proc,
|
|
"Internal proc",
|
|
self.internal_proc_id,
|
|
"exited with status",
|
|
status.status_value()
|
|
);
|
|
}
|
|
|
|
pub fn get_status(&self) -> ProcStatus {
|
|
assert!(self.exited(), "Process is not exited");
|
|
self.status.clone()
|
|
}
|
|
|
|
pub fn get_id(&self) -> u64 {
|
|
self.internal_proc_id
|
|
}
|
|
}
|
|
|
|
/// 0 should not be used; although it is not a valid PGID in userspace,
|
|
/// the Linux kernel will use it for kernel processes.
|
|
/// -1 should not be used; it is a possible return value of the getpgid()
|
|
/// function
|
|
pub const INVALID_PID: i32 = -2;
|
|
|
|
// Allows transferring the tty to a job group, while it runs.
|
|
#[derive(Default)]
|
|
pub struct TtyTransfer {
|
|
// The job group which owns the tty, or empty if none.
|
|
owner: Option<JobGroupRef>,
|
|
}
|
|
|
|
impl TtyTransfer {
|
|
pub fn new() -> Self {
|
|
Default::default()
|
|
}
|
|
/// Transfer to the given job group, if it wants to own the terminal.
|
|
#[allow(clippy::wrong_self_convention)]
|
|
pub fn to_job_group(&mut self, jg: &JobGroupRef) {
|
|
assert!(self.owner.is_none(), "Terminal already transferred");
|
|
if TtyTransfer::try_transfer(jg) {
|
|
self.owner = Some(jg.clone());
|
|
}
|
|
}
|
|
|
|
/// Reclaim the tty if we transferred it.
|
|
pub fn reclaim(&mut self) {
|
|
if self.owner.is_some() {
|
|
FLOG!(proc_pgroup, "fish reclaiming terminal");
|
|
if unsafe { libc::tcsetpgrp(STDIN_FILENO, libc::getpgrp()) } == -1 {
|
|
FLOG!(warning, "Could not return shell to foreground");
|
|
perror("tcsetpgrp");
|
|
}
|
|
self.owner = None;
|
|
}
|
|
}
|
|
|
|
/// Save the current tty modes into the owning job group, if we are transferred.
|
|
pub fn save_tty_modes(&mut self) {
|
|
if let Some(ref mut owner) = self.owner {
|
|
let mut tmodes: libc::termios = unsafe { std::mem::zeroed() };
|
|
if unsafe { libc::tcgetattr(STDIN_FILENO, &mut tmodes) } == 0 {
|
|
owner.tmodes.replace(Some(tmodes));
|
|
} else if errno::errno().0 != ENOTTY {
|
|
perror("tcgetattr");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn try_transfer(jg: &JobGroup) -> bool {
|
|
if !jg.wants_terminal() {
|
|
// The job doesn't want the terminal.
|
|
return false;
|
|
}
|
|
|
|
// Get the pgid; we must have one if we want the terminal.
|
|
let pgid = jg.get_pgid().unwrap();
|
|
assert!(pgid >= 0, "Invalid pgid");
|
|
|
|
// It should never be fish's pgroup.
|
|
let fish_pgrp = unsafe { libc::getpgrp() };
|
|
assert!(pgid != fish_pgrp, "Job should not have fish's pgroup");
|
|
|
|
// Ok, we want to transfer to the child.
|
|
// Note it is important to be very careful about calling tcsetpgrp()!
|
|
// fish ignores SIGTTOU which means that it has the power to reassign the tty even if it doesn't
|
|
// own it. This means that other processes may get SIGTTOU and become zombies.
|
|
// Check who own the tty now. There's four cases of interest:
|
|
// 1. There is no tty at all (tcgetpgrp() returns -1). For example running from a pure script.
|
|
// Of course do not transfer it in that case.
|
|
// 2. The tty is owned by the process. This comes about often, as the process will call
|
|
// tcsetpgrp() on itself between fork ane exec. This is the essential race inherent in
|
|
// tcsetpgrp(). In this case we want to reclaim the tty, but do not need to transfer it
|
|
// ourselves since the child won the race.
|
|
// 3. The tty is owned by a different process. This may come about if fish is running in the
|
|
// background with job control enabled. Do not transfer it.
|
|
// 4. The tty is owned by fish. In that case we want to transfer the pgid.
|
|
let current_owner = unsafe { libc::tcgetpgrp(STDIN_FILENO) };
|
|
if current_owner < 0 {
|
|
// Case 1.
|
|
return false;
|
|
} else if current_owner == pgid {
|
|
// Case 2.
|
|
return true;
|
|
} else if current_owner != pgid && current_owner != fish_pgrp {
|
|
// Case 3.
|
|
return false;
|
|
}
|
|
// Case 4 - we do want to transfer it.
|
|
|
|
// 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 unsafe { libc::tcsetpgrp(STDIN_FILENO, pgid) } != 0 {
|
|
FLOGF!(proc_termowner, "tcsetpgrp failed: %d", errno::errno().0);
|
|
|
|
// Before anything else, make sure that it's even necessary to call tcsetpgrp.
|
|
// Since it usually _is_ necessary, we only check in case it fails so as to avoid the
|
|
// unnecessary syscall and associated context switch, which profiling has shown to have
|
|
// a significant cost when running process groups in quick succession.
|
|
let getpgrp_res = unsafe { libc::tcgetpgrp(STDIN_FILENO) };
|
|
if getpgrp_res < 0 {
|
|
match errno::errno().0 {
|
|
ENOTTY => {
|
|
// stdin is not a tty. This may come about if job control is enabled but we are
|
|
// not a tty - see #6573.
|
|
return false;
|
|
}
|
|
EBADF => {
|
|
// stdin has been closed. Workaround a glibc bug - see #3644.
|
|
redirect_tty_output();
|
|
return false;
|
|
}
|
|
_ => {
|
|
perror("tcgetpgrp");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
if getpgrp_res == pgid {
|
|
FLOGF!(
|
|
proc_termowner,
|
|
"Process group %d already has control of terminal",
|
|
pgid
|
|
);
|
|
return true;
|
|
}
|
|
|
|
let pgroup_terminated;
|
|
if errno::errno().0 == 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::errno().0 == EPERM {
|
|
// Retry so long as this isn't because the process group is dead.
|
|
let mut result: libc::c_int = 0;
|
|
let wait_result = unsafe { libc::waitpid(-pgid, &mut 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.
|
|
FLOGF!(
|
|
proc_termowner,
|
|
"terminal_give_to_job(): EPERM with pgid %d.",
|
|
pgid
|
|
);
|
|
continue;
|
|
}
|
|
} else if errno::errno().0 == ENOTTY {
|
|
// stdin is not a TTY. In general we expect this to be caught via the tcgetpgrp
|
|
// call's EBADF handler above.
|
|
return false;
|
|
} else {
|
|
FLOGF!(
|
|
warning,
|
|
"Could not send job %d ('%ls') with pgid %d to foreground",
|
|
jg.job_id.to_wstring(),
|
|
jg.command,
|
|
pgid
|
|
);
|
|
perror("tcsetpgrp");
|
|
return false;
|
|
}
|
|
|
|
if pgroup_terminated {
|
|
// All processes in the process group has exited.
|
|
// Since we delay reaping any processes in a process group until all members of that
|
|
// job/group have been started, the only way this 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 safely ignore this.
|
|
FLOGF!(
|
|
proc_termowner,
|
|
"tcsetpgrp called but process group %d has terminated.\n",
|
|
pgid
|
|
);
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
}
|
|
true
|
|
}
|
|
}
|
|
|
|
/// The destructor will assert if reclaim() has not been called.
|
|
impl Drop for TtyTransfer {
|
|
fn drop(&mut self) {
|
|
assert!(self.owner.is_none(), "Forgot to reclaim() the tty");
|
|
}
|
|
}
|
|
|
|
/// A structure representing a single fish process. Contains variables for tracking process state
|
|
/// and the process argument list. Actually, a fish process can be either a regular external
|
|
/// process, an internal builtin which may or may not spawn a fake IO process during execution, a
|
|
/// shellscript function or a block of commands to be evaluated by calling eval. Lastly, this
|
|
/// process can be the result of an exec command. The role of this process_t is determined by the
|
|
/// type field, which can be one of process_type_t::external, process_type_t::builtin,
|
|
/// process_type_t::function, process_type_t::exec.
|
|
///
|
|
/// The process_t contains information on how the process should be started, such as command name
|
|
/// and arguments, as well as runtime information on the status of the actual physical process which
|
|
/// represents it. Shellscript functions, builtins and blocks of code may all need to spawn an
|
|
/// external process that handles the piping and redirecting of IO for them.
|
|
///
|
|
/// If the process is of type process_type_t::external or process_type_t::exec, argv is the argument
|
|
/// array and actual_cmd is the absolute path of the command to execute.
|
|
///
|
|
/// If the process is of type process_type_t::builtin, argv is the argument vector, and argv[0] is
|
|
/// the name of the builtin command.
|
|
///
|
|
/// If the process is of type process_type_t::function, argv is the argument vector, and argv[0] is
|
|
/// the name of the shellscript function.
|
|
#[derive(Default)]
|
|
pub struct Process {
|
|
/// Note whether we are the first and/or last in the job
|
|
pub is_first_in_job: bool,
|
|
pub is_last_in_job: bool,
|
|
|
|
/// Type of process.
|
|
pub typ: ProcessType,
|
|
|
|
/// For internal block processes only, the node of the statement.
|
|
/// This is always either block, ifs, or switchs, never boolean or decorated.
|
|
pub block_node_source: Option<ParsedSourceRef>,
|
|
pub internal_block_node: Option<std::ptr::NonNull<ast::Statement>>,
|
|
|
|
/// The expanded variable assignments for this process, as specified by the `a=b cmd` syntax.
|
|
pub variable_assignments: Vec<ConcreteAssignment>,
|
|
|
|
/// Actual command to pass to exec in case of process_type_t::external or process_type_t::exec.
|
|
pub actual_cmd: WString,
|
|
|
|
/// Generation counts for reaping.
|
|
pub gens: GenerationsList,
|
|
|
|
/// Process ID, represented as an AtomicI32. This is actually an Option<AtomicNonZeroI32> with a
|
|
/// value of zero representing `None`.
|
|
pub pid: AtomicI32,
|
|
|
|
/// If we are an "internal process," that process.
|
|
pub internal_proc: RefCell<Option<Arc<InternalProc>>>,
|
|
|
|
/// File descriptor that pipe output should bind to.
|
|
pub pipe_write_fd: RawFd,
|
|
|
|
/// True if process has completed.
|
|
pub completed: RelaxedAtomicBool,
|
|
|
|
/// True if process has stopped.
|
|
pub stopped: RelaxedAtomicBool,
|
|
|
|
/// If set, this process is (or will become) the pgroup leader.
|
|
/// This is only meaningful for external processes.
|
|
pub leads_pgrp: bool,
|
|
|
|
/// Whether we have generated a proc_exit event.
|
|
pub posted_proc_exit: RelaxedAtomicBool,
|
|
|
|
/// Reported status value.
|
|
pub status: ProcStatus,
|
|
|
|
pub last_times: Cell<ProcTimes>,
|
|
|
|
argv: Vec<WString>,
|
|
proc_redirection_specs: RedirectionSpecList,
|
|
|
|
// The wait handle. This is constructed lazily, and cached.
|
|
// This may be null.
|
|
wait_handle: RefCell<Option<WaitHandleRef>>,
|
|
}
|
|
|
|
#[derive(Default, Clone, Copy)]
|
|
pub struct ProcTimes {
|
|
/// Last time of cpu time check, in seconds (per timef).
|
|
pub time: Timepoint,
|
|
/// Number of jiffies spent in process at last cpu time check.
|
|
pub jiffies: ClockTicks,
|
|
}
|
|
|
|
pub struct ConcreteAssignment {
|
|
pub variable_name: WString,
|
|
pub values: Vec<WString>,
|
|
}
|
|
|
|
impl ConcreteAssignment {
|
|
pub fn new(variable_name: WString, values: Vec<WString>) -> Self {
|
|
Self {
|
|
variable_name,
|
|
values,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Process {
|
|
pub fn new() -> Self {
|
|
Default::default()
|
|
}
|
|
|
|
/// Retrieves the associated [`libc::pid_t`] or panics if no pid has been set (not yet set or
|
|
/// process does not get a pid).
|
|
///
|
|
/// See [`Process::has_pid()]` to safely check if the process has a pid.
|
|
pub fn pid(&self) -> libc::pid_t {
|
|
let value = self.pid.load(Ordering::Relaxed);
|
|
assert!(value != 0, "Process::pid() called but pid not set!");
|
|
#[allow(clippy::useless_conversion)]
|
|
value.into()
|
|
}
|
|
|
|
pub fn has_pid(&self) -> bool {
|
|
let value = self.pid.load(Ordering::Relaxed);
|
|
value != 0
|
|
}
|
|
|
|
/// Sets the process' pid. Panics if a pid has already been set.
|
|
pub fn set_pid(&self, pid: libc::pid_t) {
|
|
assert!(pid != 0, "Invalid pid of 0 passed to Process::set_pid()");
|
|
assert!(pid >= 0);
|
|
let old = self.pid.swap(pid, Ordering::Relaxed);
|
|
assert!(old == 0, "Process::set_pid() called more than once!");
|
|
}
|
|
|
|
/// Sets argv.
|
|
pub fn set_argv(&mut self, argv: Vec<WString>) {
|
|
self.argv = argv;
|
|
}
|
|
|
|
/// Returns argv.
|
|
pub fn argv(&self) -> &Vec<WString> {
|
|
&self.argv
|
|
}
|
|
|
|
/// Returns argv[0].
|
|
pub fn argv0(&self) -> Option<&wstr> {
|
|
self.argv.get(0).map(|s| s.as_utfstr())
|
|
}
|
|
|
|
/// Redirection list getter and setter.
|
|
pub fn redirection_specs(&self) -> &RedirectionSpecList {
|
|
&self.proc_redirection_specs
|
|
}
|
|
pub fn redirection_specs_mut(&mut self) -> &mut RedirectionSpecList {
|
|
&mut self.proc_redirection_specs
|
|
}
|
|
pub fn set_redirection_specs(&mut self, specs: RedirectionSpecList) {
|
|
self.proc_redirection_specs = specs;
|
|
}
|
|
|
|
/// Store the current topic generations. That is, right before the process is launched, record
|
|
/// the generations of all topics; then we can tell which generation values have changed after
|
|
/// launch. This helps us avoid spurious waitpid calls.
|
|
pub fn check_generations_before_launch(&self) {
|
|
self.gens
|
|
.update(&topic_monitor_principal().current_generations());
|
|
}
|
|
|
|
/// Mark that this process was part of a pipeline which was aborted.
|
|
/// The process was never successfully launched; give it a status of EXIT_FAILURE.
|
|
pub fn mark_aborted_before_launch(&self) {
|
|
self.completed.store(true);
|
|
// The status may have already been set to e.g. STATUS_NOT_EXECUTABLE.
|
|
// Only stomp a successful status.
|
|
if self.status.is_success() {
|
|
self.status
|
|
.set_status(ProcStatus::from_exit_code(libc::EXIT_FAILURE).status())
|
|
}
|
|
}
|
|
|
|
/// \return whether this process type is internal (block, function, or builtin).
|
|
pub fn is_internal(&self) -> bool {
|
|
match self.typ {
|
|
ProcessType::builtin | ProcessType::function | ProcessType::block_node => true,
|
|
ProcessType::external | ProcessType::exec => false,
|
|
}
|
|
}
|
|
|
|
/// \return the wait handle for the process, if it exists.
|
|
pub fn get_wait_handle(&self) -> Option<WaitHandleRef> {
|
|
self.wait_handle.borrow().clone()
|
|
}
|
|
|
|
pub fn is_stopped(&self) -> bool {
|
|
self.stopped.load()
|
|
}
|
|
|
|
pub fn is_completed(&self) -> bool {
|
|
self.completed.load()
|
|
}
|
|
|
|
/// Create a wait handle for the process.
|
|
/// As a process does not know its job id, we pass it in.
|
|
/// Note this will return null if the process is not waitable (has no pid).
|
|
pub fn make_wait_handle(&self, jid: InternalJobId) -> Option<WaitHandleRef> {
|
|
if self.typ != ProcessType::external || self.pid.load(Ordering::Relaxed) <= 0 {
|
|
// Not waitable.
|
|
None
|
|
} else {
|
|
if self.wait_handle.borrow().is_none() {
|
|
self.wait_handle.replace(Some(WaitHandle::new(
|
|
self.pid(),
|
|
jid,
|
|
wbasename(&self.actual_cmd.clone()).to_owned(),
|
|
)));
|
|
}
|
|
self.get_wait_handle()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub type ProcessPtr = Box<Process>;
|
|
pub type ProcessList = Vec<ProcessPtr>;
|
|
|
|
/// A set of jobs properties. These are immutable: they do not change for the lifetime of the
|
|
/// job.
|
|
#[derive(Default, Clone, Copy)]
|
|
pub struct JobProperties {
|
|
/// Whether the specified job is a part of a subshell, event handler or some other form of
|
|
/// special job that should not be reported.
|
|
pub skip_notification: bool,
|
|
|
|
/// Whether the job had the background ampersand when constructed, e.g. /bin/echo foo &
|
|
/// Note that a job may move between foreground and background; this just describes what the
|
|
/// initial state should be.
|
|
pub initial_background: bool,
|
|
|
|
/// Whether the job has the 'time' prefix and so we should print timing for this job.
|
|
pub wants_timing: bool,
|
|
|
|
/// Whether this job was created as part of an event handler.
|
|
pub from_event_handler: bool,
|
|
}
|
|
|
|
/// Flags associated with the job.
|
|
#[derive(Default)]
|
|
pub struct JobFlags {
|
|
/// Whether the specified job is completely constructed: every process in the job has been
|
|
/// forked, etc.
|
|
pub constructed: bool,
|
|
|
|
/// Whether the user has been notified that this job is stopped (if it is).
|
|
pub notified_of_stop: bool,
|
|
|
|
/// Whether the exit status should be negated. This flag can only be set by the not builtin.
|
|
/// Two "not" prefixes on a single job cancel each other out.
|
|
pub negate: bool,
|
|
|
|
/// This job is disowned, and should be removed from the active jobs list.
|
|
pub disown_requested: bool,
|
|
|
|
// Indicates that we are the "group root." Any other jobs using this tree are nested.
|
|
pub is_group_root: bool,
|
|
}
|
|
|
|
/// A struct representing a job. A job is a pipeline of one or more processes.
|
|
#[derive(Default)]
|
|
pub struct Job {
|
|
/// Set of immutable job properties.
|
|
properties: JobProperties,
|
|
|
|
/// The original command which led to the creation of this job. It is used for displaying
|
|
/// messages about job status on the terminal.
|
|
command_str: WString,
|
|
|
|
/// All the processes in this job.
|
|
pub processes: ProcessList,
|
|
|
|
// The group containing this job.
|
|
// This is never cleared.
|
|
pub group: Option<JobGroupRef>,
|
|
|
|
/// A non-user-visible, never-recycled job ID.
|
|
pub internal_job_id: InternalJobId,
|
|
|
|
/// Flags associated with the job.
|
|
pub job_flags: RefCell<JobFlags>,
|
|
}
|
|
|
|
impl Job {
|
|
pub fn new(properties: JobProperties, command_str: WString) -> Self {
|
|
static NEXT_INTERNAL_JOB_ID: AtomicU64 = AtomicU64::new(0);
|
|
Job {
|
|
properties,
|
|
command_str,
|
|
internal_job_id: NEXT_INTERNAL_JOB_ID.fetch_add(1, Ordering::Relaxed),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
pub fn group(&self) -> &JobGroup {
|
|
self.group.as_ref().unwrap()
|
|
}
|
|
|
|
/// Returns the command.
|
|
pub fn command(&self) -> &wstr {
|
|
&self.command_str
|
|
}
|
|
|
|
/// Borrow the job's process list. Only read-only or interior mutability actions may be
|
|
/// performed on the processes in the list.
|
|
pub fn processes(&self) -> &ProcessList {
|
|
&self.processes
|
|
}
|
|
|
|
/// Get mutable access to the job's process list.
|
|
/// Only available with a mutable reference `&mut Job`.
|
|
pub fn processes_mut(&mut self) -> &mut ProcessList {
|
|
&mut self.processes
|
|
}
|
|
|
|
/// \return whether it is OK to reap a given process. Sometimes we want to defer reaping a
|
|
/// process if it is the group leader and the job is not yet constructed, because then we might
|
|
/// also reap the process group and then we cannot add new processes to the group.
|
|
pub fn can_reap(&self, p: &ProcessPtr) -> bool {
|
|
!(
|
|
// Can't reap twice.
|
|
p.is_completed() ||
|
|
// Can't reap the group leader in an under-construction job.
|
|
(p.has_pid() && !self.is_constructed() && self.get_pgid() == Some(p.pid()))
|
|
)
|
|
}
|
|
|
|
/// Returns a truncated version of the job string. Used when a message has already been emitted
|
|
/// containing the full job string and job id, but using the job id alone would be confusing
|
|
/// due to reuse of freed job ids. Prevents overloading the debug comments with the full,
|
|
/// untruncated job string when we don't care what the job is, only which of the currently
|
|
/// running jobs it is.
|
|
#[widestrs]
|
|
pub fn preview(&self) -> WString {
|
|
if self.processes().is_empty() {
|
|
return ""L.to_owned();
|
|
}
|
|
// Note argv0 may be empty in e.g. a block process.
|
|
let procs = self.processes();
|
|
let result = procs.first().unwrap().argv0().unwrap_or("null"L);
|
|
result.to_owned() + "..."L
|
|
}
|
|
|
|
/// \return our pgid, or none if we don't have one, or are internal to fish
|
|
/// This never returns fish's own pgroup.
|
|
pub fn get_pgid(&self) -> Option<libc::pid_t> {
|
|
self.group().get_pgid()
|
|
}
|
|
|
|
/// \return the pid of the last external process in the job.
|
|
/// This may be none if the job consists of just internal fish functions or builtins.
|
|
/// This will never be fish's own pid.
|
|
pub fn get_last_pid(&self) -> Option<libc::pid_t> {
|
|
self.processes()
|
|
.iter()
|
|
.rev()
|
|
.find(|proc| proc.has_pid())
|
|
.map(|proc| proc.pid())
|
|
}
|
|
|
|
/// The id of this job.
|
|
/// This is user-visible, is recycled, and may be -1.
|
|
pub fn job_id(&self) -> MaybeJobId {
|
|
self.group().job_id
|
|
}
|
|
|
|
/// Access the job flags.
|
|
pub fn flags(&self) -> Ref<JobFlags> {
|
|
self.job_flags.borrow()
|
|
}
|
|
|
|
/// Access mutable job flags.
|
|
pub fn mut_flags(&self) -> RefMut<JobFlags> {
|
|
self.job_flags.borrow_mut()
|
|
}
|
|
|
|
// \return whether we should print timing information.
|
|
pub fn wants_timing(&self) -> bool {
|
|
self.properties.wants_timing
|
|
}
|
|
|
|
/// \return if we want job control.
|
|
pub fn wants_job_control(&self) -> bool {
|
|
self.group().wants_job_control()
|
|
}
|
|
|
|
/// \return whether this job is initially going to run in the background, because & was
|
|
/// specified.
|
|
pub fn is_initially_background(&self) -> bool {
|
|
self.properties.initial_background
|
|
}
|
|
|
|
/// Mark this job as constructed. The job must not have previously been marked as constructed.
|
|
pub fn mark_constructed(&self) {
|
|
assert!(!self.is_constructed(), "Job was already constructed");
|
|
self.mut_flags().constructed = true;
|
|
}
|
|
|
|
/// \return whether we have internal or external procs, respectively.
|
|
/// Internal procs are builtins, blocks, and functions.
|
|
/// External procs include exec and external.
|
|
pub fn has_external_proc(&self) -> bool {
|
|
self.processes().iter().any(|p| !p.is_internal())
|
|
}
|
|
|
|
/// \return whether this job, when run, will want a job ID.
|
|
/// Jobs that are only a single internal block do not get a job ID.
|
|
pub fn wants_job_id(&self) -> bool {
|
|
self.processes().len() > 1
|
|
|| !self.processes()[0].is_internal()
|
|
|| self.is_initially_background()
|
|
}
|
|
|
|
// Helper functions to check presence of flags on instances of jobs
|
|
/// The job has been fully constructed, i.e. all its member processes have been launched
|
|
pub fn is_constructed(&self) -> bool {
|
|
self.flags().constructed
|
|
}
|
|
/// The job is complete, i.e. all its member processes have been reaped
|
|
/// Return true if all processes in the job have completed.
|
|
pub fn is_completed(&self) -> bool {
|
|
assert!(!self.processes().is_empty());
|
|
self.processes().iter().all(|p| p.is_completed())
|
|
}
|
|
/// The job is in a stopped state
|
|
/// Return true if all processes in the job are stopped or completed, and there is at least one
|
|
/// stopped process.
|
|
pub fn is_stopped(&self) -> bool {
|
|
let mut has_stopped = false;
|
|
for p in self.processes().iter() {
|
|
if !p.is_completed() && !p.is_stopped() {
|
|
return false;
|
|
}
|
|
has_stopped |= p.is_stopped();
|
|
}
|
|
has_stopped
|
|
}
|
|
/// The job is OK to be externally visible, e.g. to the user via `jobs`
|
|
pub fn is_visible(&self) -> bool {
|
|
!self.is_completed() && self.is_constructed() && !self.flags().disown_requested
|
|
}
|
|
pub fn skip_notification(&self) -> bool {
|
|
self.properties.skip_notification
|
|
}
|
|
#[allow(clippy::wrong_self_convention)]
|
|
pub fn from_event_handler(&self) -> bool {
|
|
self.properties.from_event_handler
|
|
}
|
|
|
|
/// \return whether this job's group is in the foreground.
|
|
pub fn is_foreground(&self) -> bool {
|
|
self.group().is_foreground()
|
|
}
|
|
|
|
/// \return whether we should post job_exit events.
|
|
pub fn posts_job_exit_events(&self) -> bool {
|
|
// Only report root job exits.
|
|
// For example in `ls | begin ; cat ; end` we don't need to report the cat sub-job.
|
|
if !self.flags().is_group_root {
|
|
return false;
|
|
}
|
|
|
|
// Only jobs with external processes post job_exit events.
|
|
self.has_external_proc()
|
|
}
|
|
|
|
/// Run ourselves. Returning once we complete or stop.
|
|
pub fn continue_job(&self, parser: &Parser) {
|
|
FLOGF!(
|
|
proc_job_run,
|
|
"Run job %d (%ls), %ls, %ls",
|
|
self.job_id(),
|
|
self.command(),
|
|
if self.is_completed() {
|
|
"COMPLETED"
|
|
} else {
|
|
"UNCOMPLETED"
|
|
},
|
|
if parser.libdata().pods.is_interactive {
|
|
"INTERACTIVE"
|
|
} else {
|
|
"NON-INTERACTIVE"
|
|
}
|
|
);
|
|
|
|
// Wait for the status of our own job to change.
|
|
while !fish_is_unwinding_for_exit() && !self.is_stopped() && !self.is_completed() {
|
|
process_mark_finished_children(parser, true);
|
|
}
|
|
if self.is_completed() {
|
|
// Set $status only if we are in the foreground and the last process in the job has
|
|
// finished.
|
|
let procs = self.processes();
|
|
let p = procs.last().unwrap();
|
|
if p.status.normal_exited() || p.status.signal_exited() {
|
|
if let Some(statuses) = self.get_statuses() {
|
|
parser.set_last_statuses(statuses);
|
|
parser.libdata_mut().pods.status_count += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Prepare to resume a stopped job by sending SIGCONT and clearing the stopped flag.
|
|
/// \return true on success, false if we failed to send the signal.
|
|
pub fn resume(&self) -> bool {
|
|
self.mut_flags().notified_of_stop = false;
|
|
if !self.signal(SIGCONT) {
|
|
FLOGF!(
|
|
proc_pgroup,
|
|
"Failed to send SIGCONT to procs in job %ls",
|
|
self.command()
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// Reset the status of each process instance
|
|
for p in self.processes.iter() {
|
|
p.stopped.store(false);
|
|
}
|
|
true
|
|
}
|
|
|
|
/// Send the specified signal to all processes in this job.
|
|
/// \return true on success, false on failure.
|
|
pub fn signal(&self, signal: i32) -> bool {
|
|
if let Some(pgid) = self.group().get_pgid() {
|
|
if unsafe { libc::killpg(pgid, signal) } == -1 {
|
|
let strsignal = unsafe { libc::strsignal(signal) };
|
|
let strsignal = if strsignal.is_null() {
|
|
L!("(nil)").to_owned()
|
|
} else {
|
|
charptr2wcstring(strsignal)
|
|
};
|
|
wperror(&sprintf!("killpg(%d, %s)", pgid, strsignal));
|
|
return false;
|
|
}
|
|
} else {
|
|
// This job lives in fish's pgroup and we need to signal procs individually.
|
|
for p in self.processes().iter() {
|
|
if !p.is_completed() && p.has_pid() && unsafe { libc::kill(p.pid(), signal) } == -1
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
/// \returns the statuses for this job.
|
|
pub fn get_statuses(&self) -> Option<Statuses> {
|
|
let mut st = Statuses::default();
|
|
let mut has_status = false;
|
|
let mut laststatus = 0;
|
|
st.pipestatus.resize(self.processes().len(), 0);
|
|
for (i, p) in self.processes().iter().enumerate() {
|
|
let status = &p.status;
|
|
if status.is_empty() {
|
|
// Corner case for if a variable assignment is part of a pipeline.
|
|
// e.g. `false | set foo bar | true` will push 1 in the second spot,
|
|
// for a complete pipestatus of `1 1 0`.
|
|
st.pipestatus[i] = laststatus;
|
|
continue;
|
|
}
|
|
if status.signal_exited() {
|
|
st.kill_signal = Some(Signal::new(status.signal_code()));
|
|
}
|
|
laststatus = status.status_value();
|
|
has_status = true;
|
|
st.pipestatus[i] = status.status_value();
|
|
}
|
|
if !has_status {
|
|
return None;
|
|
}
|
|
st.status = if self.flags().negate {
|
|
if laststatus == 0 {
|
|
1
|
|
} else {
|
|
0
|
|
}
|
|
} else {
|
|
laststatus
|
|
};
|
|
Some(st)
|
|
}
|
|
}
|
|
|
|
pub type JobRef = Rc<Job>;
|
|
|
|
/// Whether this shell is attached to a tty.
|
|
pub fn is_interactive_session() -> bool {
|
|
IS_INTERACTIVE_SESSION.load()
|
|
}
|
|
pub fn set_interactive_session(flag: bool) {
|
|
IS_INTERACTIVE_SESSION.store(flag)
|
|
}
|
|
static IS_INTERACTIVE_SESSION: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
|
|
|
|
/// Whether we are a login shell.
|
|
pub fn get_login() -> bool {
|
|
IS_LOGIN.load()
|
|
}
|
|
pub fn mark_login() {
|
|
IS_LOGIN.store(true)
|
|
}
|
|
static IS_LOGIN: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
|
|
|
|
/// If this flag is set, fish will never fork or run execve. It is used to put fish into a syntax
|
|
/// verifier mode where fish tries to validate the syntax of a file but doesn't actually do
|
|
/// anything.
|
|
pub fn no_exec() -> bool {
|
|
IS_NO_EXEC.load()
|
|
}
|
|
pub fn mark_no_exec() {
|
|
IS_NO_EXEC.store(true)
|
|
}
|
|
static IS_NO_EXEC: RelaxedAtomicBool = RelaxedAtomicBool::new(false);
|
|
|
|
// List of jobs.
|
|
pub type JobList = Vec<JobRef>;
|
|
|
|
/// The current job control mode.
|
|
///
|
|
/// Must be one of job_control_t::all, job_control_t::interactive and job_control_t::none.
|
|
pub fn get_job_control_mode() -> JobControl {
|
|
unsafe { std::mem::transmute(JOB_CONTROL_MODE.load(Ordering::Relaxed)) }
|
|
}
|
|
pub fn set_job_control_mode(mode: JobControl) {
|
|
JOB_CONTROL_MODE.store(mode as u8, Ordering::Relaxed);
|
|
|
|
// HACK: when fish (or any shell) launches a job with job control, it will put the job into its
|
|
// own pgroup and call tcsetpgrp() to allow that pgroup to own the terminal (making fish a
|
|
// background process). When the job finishes, fish will try to reclaim the terminal via
|
|
// tcsetpgrp(), but as fish is now a background process it will receive SIGTTOU and stop! Ensure
|
|
// that doesn't happen by ignoring SIGTTOU.
|
|
// Note that if we become interactive, we also ignore SIGTTOU.
|
|
if mode == JobControl::all {
|
|
unsafe {
|
|
libc::signal(SIGTTOU, SIG_IGN);
|
|
}
|
|
}
|
|
}
|
|
static JOB_CONTROL_MODE: AtomicU8 = AtomicU8::new(JobControl::interactive as u8);
|
|
|
|
/// Notify the user about stopped or terminated jobs, and delete completed jobs from the job list.
|
|
/// If \p interactive is set, allow removing interactive jobs; otherwise skip them.
|
|
/// \return whether text was printed to stdout.
|
|
pub fn job_reap(parser: &Parser, allow_interactive: bool) -> bool {
|
|
parser.assert_can_execute();
|
|
|
|
// Early out for the common case that there are no jobs.
|
|
if parser.jobs().is_empty() {
|
|
return false;
|
|
}
|
|
|
|
process_mark_finished_children(parser, false /* not block_ok */);
|
|
process_clean_after_marking(parser, allow_interactive)
|
|
}
|
|
|
|
/// \return the list of background jobs which we should warn the user about, if the user attempts to
|
|
/// exit. An empty result (common) means no such jobs.
|
|
pub fn jobs_requiring_warning_on_exit(parser: &Parser) -> JobList {
|
|
let mut result = vec![];
|
|
for job in parser.jobs().iter() {
|
|
if !job.is_foreground() && job.is_constructed() && !job.is_completed() {
|
|
result.push(job.clone());
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
/// Print the exit warning for the given jobs, which should have been obtained via
|
|
/// jobs_requiring_warning_on_exit().
|
|
#[widestrs]
|
|
pub fn print_exit_warning_for_jobs(jobs: &JobList) {
|
|
fputws(wgettext!("There are still jobs active:\n"), STDOUT_FILENO);
|
|
fputws(wgettext!("\n PID Command\n"), STDOUT_FILENO);
|
|
for j in jobs {
|
|
fwprintf!(
|
|
STDOUT_FILENO,
|
|
"%6d %ls\n",
|
|
j.processes()[0].pid(),
|
|
j.command()
|
|
);
|
|
}
|
|
fputws("\n"L, STDOUT_FILENO);
|
|
fputws(
|
|
wgettext!("A second attempt to exit will terminate them.\n"),
|
|
STDOUT_FILENO,
|
|
);
|
|
fputws(
|
|
wgettext!("Use 'disown PID' to remove jobs from the list without terminating them.\n"),
|
|
STDOUT_FILENO,
|
|
);
|
|
reader_schedule_prompt_repaint();
|
|
}
|
|
|
|
/// Use the procfs filesystem to look up how many jiffies of cpu time was used by a given pid. This
|
|
/// function is only available on systems with the procfs file entry 'stat', i.e. Linux.
|
|
pub fn proc_get_jiffies(inpid: libc::pid_t) -> ClockTicks {
|
|
if inpid <= 0 || !have_proc_stat() {
|
|
return 0;
|
|
}
|
|
|
|
let filename = format!("/proc/{}/stat", inpid);
|
|
let Ok(mut f) = fs::File::open(filename) else {
|
|
return 0;
|
|
};
|
|
|
|
let mut buf = vec![];
|
|
if f.read_to_end(&mut buf).is_err() {
|
|
return 0;
|
|
}
|
|
|
|
let mut timesstrs = buf.split(|c| *c == b' ').skip(13);
|
|
let mut sum = 0;
|
|
for _ in 0..4 {
|
|
let Some(timestr) = timesstrs.next() else {
|
|
return 0;
|
|
};
|
|
let Ok(timestr) = std::str::from_utf8(timestr) else {
|
|
return 0;
|
|
};
|
|
let Ok(time) = str::parse::<u64>(timestr) else {
|
|
return 0;
|
|
};
|
|
sum += time;
|
|
}
|
|
sum
|
|
}
|
|
|
|
/// Update process time usage for all processes by calling the proc_get_jiffies function for every
|
|
/// process of every job.
|
|
pub fn proc_update_jiffies(parser: &Parser) {
|
|
for job in parser.jobs().iter() {
|
|
for p in job.processes.iter() {
|
|
p.last_times.replace(ProcTimes {
|
|
time: timef(),
|
|
jiffies: proc_get_jiffies(p.pid.load(Ordering::Relaxed)),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Initializations.
|
|
pub fn proc_init() {
|
|
signal_set_handlers_once(false);
|
|
}
|
|
|
|
/// Set the status of \p proc to \p status.
|
|
fn handle_child_status(job: &Job, proc: &Process, status: &ProcStatus) {
|
|
proc.status.update(status);
|
|
if status.stopped() {
|
|
proc.stopped.store(true);
|
|
} else if status.continued() {
|
|
proc.stopped.store(false);
|
|
} else {
|
|
proc.completed.store(true);
|
|
}
|
|
|
|
// If the child was killed by SIGINT or SIGQUIT, then cancel the entire group if interactive. If
|
|
// not interactive, we have historically re-sent the signal to ourselves; however don't do that
|
|
// if the signal is trapped (#6649).
|
|
// Note the asymmetry: if the fish process gets SIGINT we will run SIGINT handlers. If a child
|
|
// process gets SIGINT we do not run SIGINT handlers; we just don't exit. This should be
|
|
// rationalized.
|
|
if status.signal_exited() {
|
|
let sig = status.signal_code();
|
|
if [SIGINT, SIGQUIT].contains(&sig) {
|
|
if is_interactive_session() {
|
|
// Mark the job group as cancelled.
|
|
job.group().cancel_with_signal(Signal::new(sig));
|
|
} else if !event::is_signal_observed(sig) {
|
|
// Deliver the SIGINT or SIGQUIT signal to ourself since we're not interactive.
|
|
let mut act: libc::sigaction = unsafe { std::mem::zeroed() };
|
|
unsafe { libc::sigemptyset(&mut act.sa_mask) };
|
|
act.sa_flags = 0;
|
|
act.sa_sigaction = SIG_DFL;
|
|
unsafe {
|
|
libc::sigaction(sig, &act, std::ptr::null_mut());
|
|
libc::kill(libc::getpid(), sig);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Wait for any process finishing, or receipt of a signal.
|
|
pub fn proc_wait_any(parser: &Parser) {
|
|
process_mark_finished_children(parser, true /*block_ok*/);
|
|
let is_interactive = parser.libdata().pods.is_interactive;
|
|
process_clean_after_marking(parser, is_interactive);
|
|
}
|
|
|
|
/// Send SIGHUP to the list \p jobs, excepting those which are in fish's pgroup.
|
|
pub fn hup_jobs(jobs: &JobList) {
|
|
let fish_pgrp = unsafe { libc::getpgrp() };
|
|
for j in jobs {
|
|
let Some(pgid) = j.get_pgid() else { continue };
|
|
if pgid != fish_pgrp && !j.is_completed() {
|
|
if j.is_stopped() {
|
|
j.signal(SIGCONT);
|
|
}
|
|
j.signal(SIGHUP);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Add a job to the list of PIDs/PGIDs we wait on even though they are not associated with any
|
|
/// jobs. Used to avoid zombie processes after disown.
|
|
pub fn add_disowned_job(j: &Job) {
|
|
let mut disowned_pids = unsafe { DISOWNED_PIDS.lock().unwrap() };
|
|
for process in j.processes().iter() {
|
|
if process.has_pid() {
|
|
disowned_pids.push(process.pid());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reap any pids in our disowned list that have exited. This is used to avoid zombies.
|
|
fn reap_disowned_pids() {
|
|
let mut disowned_pids = unsafe { DISOWNED_PIDS.lock().unwrap() };
|
|
// waitpid returns 0 iff the PID/PGID in question has not changed state; remove the pid/pgid
|
|
// if it has changed or an error occurs (presumably ECHILD because the child does not exist)
|
|
disowned_pids.retain(|pid| {
|
|
let mut status: libc::c_int = 0;
|
|
let ret = unsafe { libc::waitpid(*pid, &mut status, WNOHANG) };
|
|
if ret > 0 {
|
|
FLOGF!(proc_reap_external, "Reaped disowned PID or PGID %d", pid);
|
|
}
|
|
ret == 0
|
|
});
|
|
}
|
|
|
|
/// A list of pids that have been disowned. They are kept around until either they exit or
|
|
/// we exit. Poll these from time-to-time to prevent zombie processes from happening (#5342).
|
|
static mut DISOWNED_PIDS: Mutex<Vec<libc::pid_t>> = Mutex::new(vec![]);
|
|
|
|
/// See if any reapable processes have exited, and mark them accordingly.
|
|
/// \param block_ok if no reapable processes have exited, block until one is (or until we receive a
|
|
/// signal).
|
|
fn process_mark_finished_children(parser: &Parser, block_ok: bool) {
|
|
parser.assert_can_execute();
|
|
|
|
// Get the exit and signal generations of all reapable processes.
|
|
// The exit generation tells us if we have an exit; the signal generation allows for detecting
|
|
// SIGHUP and SIGINT.
|
|
// Go through each process and figure out if and how it wants to be reaped.
|
|
let mut reapgens = GenerationsList::invalid();
|
|
for j in parser.jobs().iter() {
|
|
for proc in j.processes().iter() {
|
|
if !j.can_reap(proc) {
|
|
continue;
|
|
}
|
|
|
|
if proc.has_pid() {
|
|
// Reaps with a pid.
|
|
reapgens.set_min_from(topic_t::sigchld, &proc.gens);
|
|
reapgens.set_min_from(topic_t::sighupint, &proc.gens);
|
|
}
|
|
if proc.internal_proc.borrow().is_some() {
|
|
// Reaps with an internal process.
|
|
reapgens.set_min_from(topic_t::internal_exit, &proc.gens);
|
|
reapgens.set_min_from(topic_t::sighupint, &proc.gens);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now check for changes, optionally waiting.
|
|
if !topic_monitor_principal().check(&reapgens, block_ok) {
|
|
// Nothing changed.
|
|
return;
|
|
}
|
|
|
|
// We got some changes. Since we last checked we received SIGCHLD, and or HUP/INT.
|
|
// Update the hup/int generations and reap any reapable processes.
|
|
// We structure this as two loops for some simplicity.
|
|
// First reap all pids.
|
|
for j in parser.jobs().iter() {
|
|
for proc in j.processes.iter() {
|
|
// Does this proc have a pid that is reapable?
|
|
if proc.pid.load(Ordering::Relaxed) <= 0 || !j.can_reap(proc) {
|
|
continue;
|
|
}
|
|
|
|
// Always update the signal hup/int gen.
|
|
proc.gens.sighupint.set(reapgens.sighupint.get());
|
|
|
|
// Nothing to do if we did not get a new sigchld.
|
|
if proc.gens.sigchld == reapgens.sigchld {
|
|
continue;
|
|
}
|
|
proc.gens.sigchld.set(reapgens.sigchld.get());
|
|
|
|
// Ok, we are reapable. Run waitpid()!
|
|
let mut statusv: libc::c_int = -1;
|
|
let pid = unsafe {
|
|
libc::waitpid(proc.pid(), &mut statusv, WNOHANG | WUNTRACED | WCONTINUED)
|
|
};
|
|
assert!(pid <= 0 || pid == proc.pid(), "Unexpected waitpid() return");
|
|
if pid <= 0 {
|
|
continue;
|
|
}
|
|
|
|
// The process has stopped or exited! Update its status.
|
|
let status = ProcStatus::from_waitpid(statusv);
|
|
handle_child_status(j, proc, &status);
|
|
if status.stopped() {
|
|
j.group().set_is_foreground(false);
|
|
}
|
|
if status.continued() {
|
|
j.mut_flags().notified_of_stop = false;
|
|
}
|
|
if status.normal_exited() || status.signal_exited() {
|
|
FLOGF!(
|
|
proc_reap_external,
|
|
"Reaped external process '%ls' (pid %d, status %d)",
|
|
proc.argv0().unwrap(),
|
|
pid,
|
|
proc.status.status_value()
|
|
);
|
|
} else {
|
|
assert!(status.stopped() || status.continued());
|
|
FLOGF!(
|
|
proc_reap_external,
|
|
"External process '%ls' (pid %d, %s)",
|
|
proc.argv0().unwrap(),
|
|
proc.pid(),
|
|
if proc.status.stopped() {
|
|
"stopped"
|
|
} else {
|
|
"continued"
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// We are done reaping pids.
|
|
// Reap internal processes.
|
|
for j in parser.jobs().iter() {
|
|
for proc in j.processes.iter() {
|
|
// Does this proc have an internal process that is reapable?
|
|
if proc.internal_proc.borrow().is_none() || !j.can_reap(proc) {
|
|
continue;
|
|
}
|
|
|
|
// Always update the signal hup/int gen.
|
|
proc.gens.sighupint.set(reapgens.sighupint.get());
|
|
|
|
// Nothing to do if we did not get a new internal exit.
|
|
if proc.gens.internal_exit == reapgens.internal_exit {
|
|
continue;
|
|
}
|
|
proc.gens.internal_exit.set(reapgens.internal_exit.get());
|
|
|
|
// Keep the borrow so we don't keep borrowing again and again and unwrapping again and
|
|
// again below.
|
|
let borrow = proc.internal_proc.borrow();
|
|
let internal_proc = borrow.as_ref().unwrap();
|
|
// Has the process exited?
|
|
if !internal_proc.exited() {
|
|
continue;
|
|
}
|
|
|
|
// The process gets the status from its internal proc.
|
|
let status = internal_proc.get_status();
|
|
handle_child_status(j, proc, &status);
|
|
FLOGF!(
|
|
proc_reap_internal,
|
|
"Reaped internal process '%ls' (id %llu, status %d)",
|
|
proc.argv0().unwrap(),
|
|
internal_proc.get_id(),
|
|
proc.status.status_value(),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Remove any zombies.
|
|
reap_disowned_pids();
|
|
}
|
|
|
|
/// Generate process_exit events for any completed processes in \p j.
|
|
fn generate_process_exit_events(j: &Job, out_evts: &mut Vec<Event>) {
|
|
// Historically we have avoided generating events for foreground jobs from event handlers, as an
|
|
// event handler may itself produce a new event.
|
|
if !j.from_event_handler() || !j.is_foreground() {
|
|
for p in j.processes().iter() {
|
|
if p.has_pid() && p.is_completed() && !p.posted_proc_exit.load() {
|
|
p.posted_proc_exit.store(true);
|
|
out_evts.push(Event::process_exit(p.pid(), p.status.status_value()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Given a job that has completed, generate job_exit and caller_exit events.
|
|
fn generate_job_exit_events(j: &Job, out_evts: &mut Vec<Event>) {
|
|
// Generate proc and job exit events, except for foreground jobs originating in event handlers.
|
|
if !j.from_event_handler() || !j.is_foreground() {
|
|
// job_exit events.
|
|
if j.posts_job_exit_events() {
|
|
if let Some(last_pid) = j.get_last_pid() {
|
|
out_evts.push(Event::job_exit(last_pid, j.internal_job_id));
|
|
}
|
|
}
|
|
}
|
|
// Generate caller_exit events.
|
|
out_evts.push(Event::caller_exit(j.internal_job_id, j.job_id()));
|
|
}
|
|
|
|
/// \return whether to emit a fish_job_summary call for a process.
|
|
fn proc_wants_summary(j: &Job, p: &Process) -> bool {
|
|
// Are we completed with a pid?
|
|
if !p.is_completed() || !p.has_pid() {
|
|
return false;
|
|
}
|
|
|
|
// Did we die due to a signal other than SIGPIPE?
|
|
let s = &p.status;
|
|
if !s.signal_exited() || s.signal_code() == SIGPIPE {
|
|
return false;
|
|
}
|
|
|
|
// Does the job want to suppress notifications?
|
|
// Note we always report crashes.
|
|
if j.skip_notification() && !CRASHSIGNALS.contains(&s.signal_code()) {
|
|
return false;
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
/// \return whether to emit a fish_job_summary call for a job as a whole. We may also emit this for
|
|
/// its individual processes.
|
|
fn job_wants_summary(j: &Job) -> bool {
|
|
// Do we just skip notifications?
|
|
if j.skip_notification() {
|
|
return false;
|
|
}
|
|
|
|
// Do we have a single process which will also report? If so then that suffices for us.
|
|
if j.processes().len() == 1 && proc_wants_summary(j, &j.processes()[0]) {
|
|
return false;
|
|
}
|
|
|
|
// Are we foreground?
|
|
// The idea here is to not print status messages for jobs that execute in the foreground (i.e.
|
|
// without & and without being `bg`).
|
|
if j.is_foreground() {
|
|
return false;
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
/// \return whether we want to emit a fish_job_summary call for a job or any of its processes.
|
|
fn job_or_proc_wants_summary(j: &Job) -> bool {
|
|
job_wants_summary(j) || j.processes().iter().any(|p| proc_wants_summary(j, p))
|
|
}
|
|
|
|
/// Invoke the fish_job_summary function by executing the given command.
|
|
fn call_job_summary(parser: &Parser, cmd: &wstr) {
|
|
let event = Event::generic(L!("fish_job_summary").to_owned());
|
|
let b = parser.push_block(Block::event_block(event));
|
|
let saved_status = parser.get_last_statuses();
|
|
parser.eval(cmd, &IoChain::new());
|
|
parser.set_last_statuses(saved_status);
|
|
parser.pop_block(b);
|
|
}
|
|
|
|
// \return a command which invokes fish_job_summary.
|
|
// The process pointer may be null, in which case it represents the entire job.
|
|
// Note this implements the arguments which fish_job_summary expects.
|
|
#[widestrs]
|
|
fn summary_command(j: &Job, p: Option<&Process>) -> WString {
|
|
let mut buffer = "fish_job_summary"L.to_owned();
|
|
|
|
// Job id.
|
|
buffer += &sprintf!(" %s", j.job_id().to_wstring())[..];
|
|
|
|
// 1 if foreground, 0 if background.
|
|
buffer += &sprintf!(" %d", if j.is_foreground() { 1 } else { 0 })[..];
|
|
|
|
// Command.
|
|
buffer.push(' ');
|
|
buffer += &escape(j.command())[..];
|
|
|
|
match p {
|
|
None => {
|
|
// No process, we are summarizing the whole job.
|
|
buffer += if j.is_stopped() {
|
|
" STOPPED"L
|
|
} else {
|
|
" ENDED"L
|
|
};
|
|
}
|
|
Some(p) => {
|
|
// We are summarizing a process which exited with a signal.
|
|
// Arguments are the signal name and description.
|
|
let sig = Signal::new(p.status.signal_code());
|
|
buffer.push(' ');
|
|
buffer += &escape(&sig.name())[..];
|
|
|
|
buffer.push(' ');
|
|
buffer += &escape(&sig.desc())[..];
|
|
|
|
// If we have multiple processes, we also append the pid and argv.
|
|
if j.processes().len() > 1 {
|
|
buffer += &sprintf!(" %d", p.pid())[..];
|
|
|
|
buffer.push(' ');
|
|
buffer += &escape(p.argv0().unwrap())[..];
|
|
}
|
|
}
|
|
}
|
|
buffer
|
|
}
|
|
|
|
// Summarize a list of jobs, by emitting calls to fish_job_summary.
|
|
// Note the given list must NOT be the parser's own job list, since the call to fish_job_summary
|
|
// could modify it.
|
|
fn summarize_jobs(parser: &Parser, jobs: &[JobRef]) -> bool {
|
|
if jobs.is_empty() {
|
|
return false;
|
|
}
|
|
|
|
for j in jobs {
|
|
if j.is_stopped() {
|
|
call_job_summary(parser, &summary_command(j, None));
|
|
} else {
|
|
// Completed job.
|
|
for p in j.processes().iter() {
|
|
if proc_wants_summary(j, p) {
|
|
call_job_summary(parser, &summary_command(j, Some(p)));
|
|
}
|
|
}
|
|
|
|
// Overall status for the job.
|
|
if job_wants_summary(j) {
|
|
call_job_summary(parser, &summary_command(j, None));
|
|
}
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
/// Remove all disowned jobs whose job chain is fully constructed (that is, do not erase disowned
|
|
/// jobs that still have an in-flight parent job). Note we never print statuses for such jobs.
|
|
fn remove_disowned_jobs(jobs: &mut JobList) {
|
|
jobs.retain(|j| !j.flags().disown_requested || !j.is_constructed());
|
|
}
|
|
|
|
/// Given that a job has completed, check if it may be wait'ed on; if so add it to the wait handle
|
|
/// store. Then mark all wait handles as complete.
|
|
fn save_wait_handle_for_completed_job(job: &Job, store: &mut WaitHandleStore) {
|
|
assert!(job.is_completed(), "Job not completed");
|
|
// Are we a background job?
|
|
if !job.is_foreground() {
|
|
for proc in job.processes().iter() {
|
|
if let Some(wh) = proc.make_wait_handle(job.internal_job_id) {
|
|
store.add(wh);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mark all wait handles as complete (but don't create just for this).
|
|
for proc in job.processes().iter() {
|
|
if let Some(wh) = proc.get_wait_handle() {
|
|
wh.set_status_and_complete(proc.status.status_value());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Remove completed jobs from the job list, printing status messages as appropriate.
|
|
/// \return whether something was printed.
|
|
fn process_clean_after_marking(parser: &Parser, allow_interactive: bool) -> bool {
|
|
parser.assert_can_execute();
|
|
|
|
// This function may fire an event handler, we do not want to call ourselves recursively (to
|
|
// avoid infinite recursion).
|
|
if parser.libdata().pods.is_cleaning_procs {
|
|
return false;
|
|
}
|
|
|
|
let _cleaning = scoped_push_replacer(
|
|
|new_value| std::mem::replace(&mut parser.libdata_mut().pods.is_cleaning_procs, new_value),
|
|
true,
|
|
);
|
|
|
|
// This may be invoked in an exit handler, after the TERM has been torn down
|
|
// Don't try to print in that case (#3222)
|
|
let interactive = allow_interactive && cur_term();
|
|
|
|
// Remove all disowned jobs.
|
|
remove_disowned_jobs(&mut parser.jobs_mut());
|
|
|
|
// Accumulate exit events into a new list, which we fire after the list manipulation is
|
|
// complete.
|
|
let mut exit_events = vec![];
|
|
|
|
// Defer processing under-construction jobs or jobs that want a message when we are not
|
|
// interactive.
|
|
let should_process_job = |j: &Job| {
|
|
// Do not attempt to process jobs which are not yet constructed.
|
|
// Do not attempt to process jobs that need to print a status message,
|
|
// unless we are interactive, in which case printing is OK.
|
|
j.is_constructed() && (interactive || !job_or_proc_wants_summary(j))
|
|
};
|
|
|
|
// The list of jobs to summarize. Some of these jobs are completed and are removed from the
|
|
// parser's job list, others are stopped and remain in the list.
|
|
let mut jobs_to_summarize = vec![];
|
|
|
|
// Handle stopped jobs. These stay in our list.
|
|
for j in parser.jobs().iter() {
|
|
if j.is_stopped()
|
|
&& !j.flags().notified_of_stop
|
|
&& should_process_job(j)
|
|
&& job_wants_summary(j)
|
|
{
|
|
j.mut_flags().notified_of_stop = true;
|
|
jobs_to_summarize.push(j.clone());
|
|
}
|
|
}
|
|
|
|
// Generate process_exit events for finished processes.
|
|
for j in parser.jobs().iter() {
|
|
generate_process_exit_events(j, &mut exit_events);
|
|
}
|
|
|
|
// Remove completed, processable jobs from our job list.
|
|
let mut completed_jobs = vec![];
|
|
parser.jobs_mut().retain(|j| {
|
|
if !should_process_job(j) || !j.is_completed() {
|
|
return true;
|
|
}
|
|
// We are committed to removing this job.
|
|
// Remember it for summary later, generate exit events, maybe save its wait handle if it
|
|
// finished in the background.
|
|
if job_or_proc_wants_summary(j) {
|
|
jobs_to_summarize.push(j.clone());
|
|
}
|
|
generate_job_exit_events(j, &mut exit_events);
|
|
completed_jobs.push(j.clone());
|
|
false
|
|
});
|
|
for j in completed_jobs {
|
|
save_wait_handle_for_completed_job(&j, &mut parser.mut_wait_handles());
|
|
}
|
|
|
|
// Emit calls to fish_job_summary.
|
|
let printed = summarize_jobs(parser, &jobs_to_summarize);
|
|
|
|
// Post pending exit events.
|
|
for evt in exit_events {
|
|
event::fire(parser, evt);
|
|
}
|
|
|
|
if printed {
|
|
let _ = std::io::stdout().lock().flush();
|
|
}
|
|
|
|
printed
|
|
}
|
|
|
|
pub fn have_proc_stat() -> bool {
|
|
// Check for /proc/self/stat to see if we are running with Linux-style procfs.
|
|
static HAVE_PROC_STAT_RESULT: Lazy<bool> =
|
|
Lazy::new(|| fs::metadata("/proc/self/stat").is_ok());
|
|
*HAVE_PROC_STAT_RESULT
|
|
}
|
|
|
|
/// The signals that signify crashes to us.
|
|
const CRASHSIGNALS: [libc::c_int; 6] = [SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGSYS];
|
|
|
|
pub struct JobRefFfi(JobRef);
|
|
|
|
unsafe impl cxx::ExternType for JobRefFfi {
|
|
type Id = cxx::type_id!("JobRefFfi");
|
|
type Kind = cxx::kind::Opaque;
|
|
}
|
|
|
|
pub struct JobGroupRefFfi(JobGroupRef);
|
|
|
|
unsafe impl cxx::ExternType for JobGroupRefFfi {
|
|
type Id = cxx::type_id!("JobGroupRefFfi");
|
|
type Kind = cxx::kind::Opaque;
|
|
}
|
|
|
|
#[cxx::bridge]
|
|
mod proc_ffi {
|
|
extern "C++" {
|
|
include!("parser.h");
|
|
type Parser = crate::parser::Parser;
|
|
}
|
|
extern "Rust" {
|
|
fn set_interactive_session(flag: bool);
|
|
fn get_login() -> bool;
|
|
fn mark_login();
|
|
fn mark_no_exec();
|
|
fn proc_init();
|
|
|
|
type JobRefFfi;
|
|
type JobGroupRefFfi;
|
|
|
|
fn job_reap(parser: &Parser, allow_interactive: bool) -> bool;
|
|
fn is_interactive_session() -> bool;
|
|
fn have_proc_stat() -> bool;
|
|
fn proc_update_jiffies(parser: &Parser);
|
|
}
|
|
extern "Rust" {
|
|
type TtyTransfer;
|
|
fn new_tty_transfer() -> Box<TtyTransfer>;
|
|
#[cxx_name = "to_job_group"]
|
|
fn to_job_group_ffi(&mut self, jg: &JobGroupRefFfi);
|
|
fn save_tty_modes(&mut self);
|
|
fn reclaim(&mut self);
|
|
fn no_exec() -> bool;
|
|
}
|
|
extern "Rust" {
|
|
type JobListFFI;
|
|
#[cxx_name = "jobs_requiring_warning_on_exit"]
|
|
fn jobs_requiring_warning_on_exit_ffi(parser: &Parser) -> Box<JobListFFI>;
|
|
#[cxx_name = "print_exit_warning_for_jobs"]
|
|
fn print_exit_warning_for_jobs_ffi(jobs: &JobListFFI);
|
|
fn empty(&self) -> bool;
|
|
#[cxx_name = "hup_jobs"]
|
|
fn hup_jobs_ffi(parser: &Parser);
|
|
}
|
|
}
|
|
struct JobListFFI(JobList);
|
|
fn jobs_requiring_warning_on_exit_ffi(parser: &Parser) -> Box<JobListFFI> {
|
|
Box::new(JobListFFI(jobs_requiring_warning_on_exit(parser)))
|
|
}
|
|
fn print_exit_warning_for_jobs_ffi(jobs: &JobListFFI) {
|
|
print_exit_warning_for_jobs(&jobs.0)
|
|
}
|
|
|
|
fn hup_jobs_ffi(parser: &Parser) {
|
|
hup_jobs(&parser.jobs())
|
|
}
|
|
impl JobListFFI {
|
|
fn empty(&self) -> bool {
|
|
self.0.is_empty()
|
|
}
|
|
}
|
|
|
|
fn new_tty_transfer() -> Box<TtyTransfer> {
|
|
Box::new(TtyTransfer::new())
|
|
}
|
|
|
|
impl TtyTransfer {
|
|
#[allow(clippy::wrong_self_convention)]
|
|
fn to_job_group_ffi(&mut self, jg: &JobGroupRefFfi) {
|
|
self.to_job_group(&jg.0);
|
|
}
|
|
}
|