use std::os::unix::fs::MetadataExt;
use std::sync::atomic::{AtomicU64, Ordering};
use std::{env, io};
use super::*;
use crate::embed::{OutcomeCaptureHandle, ShellDiagnostic, TraceEvent};
use crate::status::normalize_exit_status;
fn next_trap_owner_id() -> u64 {
static NEXT_TRAP_OWNER_ID: AtomicU64 = AtomicU64::new(1);
NEXT_TRAP_OWNER_ID.fetch_add(1, Ordering::Relaxed)
}
fn same_directory(left: &Path, right: &Path) -> bool {
let (Ok(left), Ok(right)) = (fs::metadata(left), fs::metadata(right)) else {
return false;
};
left.dev() == right.dev() && left.ino() == right.ino()
}
fn trusted_logical_cwd(path: PathBuf, reference: &Path) -> Option<PathBuf> {
if path.is_absolute() && same_directory(&path, reference) {
Some(path)
} else {
None
}
}
fn startup_cwd() -> PathBuf {
env::var_os("PWD")
.map(PathBuf::from)
.and_then(|path| trusted_logical_cwd(path, Path::new(".")))
.unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| PathBuf::from("/")))
}
fn pwd_for_shell_cwd(existing: Option<&str>, cwd: &Path) -> String {
existing
.map(PathBuf::from)
.and_then(|path| trusted_logical_cwd(path, cwd))
.unwrap_or_else(|| cwd.to_path_buf())
.display()
.to_string()
}
fn ambient_open_fds() -> HashMap<i32, sys::FileDescriptor> {
let mut fds = HashMap::new();
for fd in ambient_fd_candidates() {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
if flags < 0 || (flags & libc::FD_CLOEXEC) != 0 {
continue;
}
fds.insert(fd, sys::FileDescriptor::new(fd));
}
fds
}
fn ambient_fd_candidates() -> Vec<i32> {
let mut candidates = Vec::new();
let mut listed_fd_dir = false;
for fd_dir in ["/proc/self/fd", "/dev/fd"] {
let Ok(entries) = fs::read_dir(fd_dir) else {
continue;
};
listed_fd_dir = true;
for entry in entries.flatten() {
let name = entry.file_name();
let Some(name) = name.to_str() else {
continue;
};
if let Ok(fd) = name.parse::<i32>()
&& fd >= 3
{
candidates.push(fd);
}
}
}
if !listed_fd_dir {
candidates.extend(3..ambient_fd_scan_limit());
}
candidates.sort_unstable();
candidates.dedup();
candidates
}
fn ambient_fd_scan_limit() -> i32 {
let max_fd = unsafe { libc::sysconf(libc::_SC_OPEN_MAX) };
if max_fd > 0 {
max_fd.min(i32::MAX as libc::c_long) as i32
} else {
256
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(super) enum ControlFlow {
None,
Break(u32),
Continue(u32),
Return(i32),
Exit(i32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(super) struct CommandOutcome {
pub(super) status: i32,
pub(super) control: ControlFlow,
}
impl CommandOutcome {
pub(super) fn status(status: i32) -> Self {
Self {
status,
control: ControlFlow::None,
}
}
pub(super) fn from_state(status: i32, state: &ShellState) -> Self {
Self {
status,
control: state.control_flow,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(super) enum ErrexitContext {
IfCondition,
LoopCondition,
AndOrLeft,
InvertedPipeline,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum ExecutionContextKind {
TopLevel,
Function,
DotScript,
Subshell,
CommandSubstitution,
PipelineStage,
BackgroundJob,
DetachedSession,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(super) enum JobScope {
Owned,
Empty,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(super) enum TrapScope {
Owned,
Child,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(super) enum FdScope {
Owned,
Inherited,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(super) enum ProcessGlobalScope {
Owned,
Isolated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(super) struct ExecutionContext {
pub(super) kind: ExecutionContextKind,
pub(super) logical_shell_pid: u32,
pub(super) job_scope: JobScope,
pub(super) trap_scope: TrapScope,
pub(super) fd_scope: FdScope,
pub(super) process_global_scope: ProcessGlobalScope,
pub(super) errexit_exempt: bool,
}
impl ExecutionContext {
fn top_level() -> Self {
Self {
kind: ExecutionContextKind::TopLevel,
logical_shell_pid: std::process::id(),
job_scope: JobScope::Owned,
trap_scope: TrapScope::Owned,
fd_scope: FdScope::Owned,
process_global_scope: ProcessGlobalScope::Owned,
errexit_exempt: false,
}
}
fn child(kind: ExecutionContextKind, logical_shell_pid: u32) -> Self {
let process_global_scope = match kind {
ExecutionContextKind::TopLevel
| ExecutionContextKind::Function
| ExecutionContextKind::DotScript => ProcessGlobalScope::Owned,
ExecutionContextKind::Subshell
| ExecutionContextKind::CommandSubstitution
| ExecutionContextKind::PipelineStage
| ExecutionContextKind::BackgroundJob
| ExecutionContextKind::DetachedSession => ProcessGlobalScope::Isolated,
};
let job_scope = match kind {
ExecutionContextKind::TopLevel
| ExecutionContextKind::Function
| ExecutionContextKind::DotScript => JobScope::Owned,
ExecutionContextKind::Subshell
| ExecutionContextKind::CommandSubstitution
| ExecutionContextKind::PipelineStage
| ExecutionContextKind::BackgroundJob
| ExecutionContextKind::DetachedSession => JobScope::Empty,
};
Self {
kind,
logical_shell_pid,
job_scope,
trap_scope: TrapScope::Child,
fd_scope: FdScope::Inherited,
process_global_scope,
errexit_exempt: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Variable {
pub value: String,
pub attrib: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ShellFunction {
pub(super) body: AstCommand,
pub(super) definition_redirects: Vec<IoRedirect>,
}
#[derive(Debug, Clone)]
pub(crate) struct VariableStore {
pub(crate) frame: Vec<String>,
pub(crate) vars: HashMap<String, Variable>,
pub(crate) unset_var_attribs: HashMap<String, u32>,
pub(crate) getopts_cursor: Option<usize>,
pub(crate) getopts_index: Option<usize>,
}
impl VariableStore {
fn new() -> Self {
Self {
frame: Vec::new(),
vars: HashMap::new(),
unset_var_attribs: HashMap::new(),
getopts_cursor: None,
getopts_index: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct VariableStoreCheckpoint {
frame: Vec<String>,
vars: HashMap<String, Variable>,
unset_var_attribs: HashMap<String, u32>,
getopts_cursor: Option<usize>,
getopts_index: Option<usize>,
}
impl VariableStoreCheckpoint {
fn into_store(self) -> VariableStore {
VariableStore {
frame: self.frame,
vars: self.vars,
unset_var_attribs: self.unset_var_attribs,
getopts_cursor: self.getopts_cursor,
getopts_index: self.getopts_index,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct DefinitionStore {
pub(crate) functions: HashMap<String, ShellFunction>,
pub(crate) aliases: Arc<HashMap<String, String>>,
}
impl DefinitionStore {
fn new() -> Self {
Self {
functions: HashMap::new(),
aliases: Arc::new(HashMap::new()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct DefinitionStoreCheckpoint {
functions: HashMap<String, ShellFunction>,
aliases: HashMap<String, String>,
}
impl DefinitionStoreCheckpoint {
fn into_store(self) -> DefinitionStore {
DefinitionStore {
functions: self.functions,
aliases: Arc::new(self.aliases),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct PathState {
pub(crate) cwd: PathBuf,
}
#[derive(Debug, Clone)]
pub(crate) struct TrapTable {
pub(crate) traps: HashMap<i32, String>,
pub(crate) owner_id: u64,
pub(crate) inherited_exit_trap: bool,
}
impl TrapTable {
fn new() -> Self {
Self {
traps: HashMap::new(),
owner_id: next_trap_owner_id(),
inherited_exit_trap: false,
}
}
}
#[derive(Debug, Clone)]
pub(super) enum JobState {
Running,
Stopped(i32),
Done(i32),
}
#[derive(Debug, Clone)]
pub(super) struct JobInfo {
pub(super) job_id: u32,
pub(super) display_pid: Option<u32>,
pub(super) state: JobState,
}
#[derive(Debug, Clone)]
pub(crate) struct ShellJob {
pub(super) job_id: u32,
pub(super) handle: sys::ProcessHandle,
pub(super) display_pid: Option<u32>,
pub(super) state: JobState,
pub(super) command_id: String,
pub(super) finish_emitted: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct JobTable {
pub(crate) last_bg_pid: Option<u32>,
pub(crate) notified_jobs: HashSet<u32>,
pub(crate) next_job_id: u32,
pub(crate) jobs: Vec<ShellJob>,
}
impl JobTable {
fn new() -> Self {
Self {
last_bg_pid: None,
notified_jobs: HashSet::new(),
next_job_id: 1,
jobs: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub(super) struct FdTable {
pub(super) raw_fds: HashMap<i32, sys::FileDescriptor>,
pub(super) named_fds: HashMap<String, i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct FdTableCheckpoint {
raw_fds: HashMap<i32, i32>,
named_fds: HashMap<String, i32>,
}
impl FdTableCheckpoint {
fn into_table(self) -> FdTable {
FdTable {
raw_fds: self
.raw_fds
.into_iter()
.map(|(child, parent)| (child, sys::FileDescriptor::new(parent)))
.collect(),
named_fds: self.named_fds,
}
}
}
#[derive(Debug)]
pub struct ShellState {
pub exit_code: i32,
pub options: u32,
pub interactive: bool,
pub last_status: i32,
pub(crate) variable_store: VariableStore,
pub(crate) definition_store: DefinitionStore,
pub(crate) path_state: PathState,
pub(super) control_flow: ControlFlow,
pub(super) loop_depth: u32,
pub(super) function_depth: u32,
pub(super) dot_script_depth: u32,
pub(crate) trap_table: TrapTable,
pub(crate) job_table: JobTable,
pub(super) warned_vi_unsupported: bool,
pub(crate) manage_signals: bool,
pub(super) monitor_signals: MonitorSignalState,
pub(super) monitor_mode_active: bool,
pub(super) expansion_error: Option<i32>,
pub(super) command_substitution_status: Option<i32>,
pub(super) process_isolated: bool,
pub(super) execution_context: ExecutionContext,
pub(super) errexit_contexts: Vec<ErrexitContext>,
pub(super) next_status_errexit_exempt: Option<ErrexitContext>,
pub(super) current_source: Option<String>,
pub(super) active_command_id: Option<String>,
pub stdin_fd: sys::FileDescriptor,
pub stdout_fd: sys::FileDescriptor,
pub stderr_fd: sys::FileDescriptor,
pub(super) fd_table: FdTable,
pub(crate) outcome_captures: Vec<OutcomeCaptureHandle>,
pub(crate) definition: Arc<crate::embed::ShellDefinition>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct BackgroundStateCheckpoint {
exit_code: i32,
options: u32,
interactive: bool,
last_status: i32,
variables: VariableStoreCheckpoint,
cwd: PathBuf,
definitions: DefinitionStoreCheckpoint,
loop_depth: u32,
function_depth: u32,
dot_script_depth: u32,
last_bg_pid: Option<u32>,
traps: HashMap<i32, String>,
warned_vi_unsupported: bool,
expansion_error: Option<i32>,
current_source: Option<String>,
fd_table: FdTableCheckpoint,
logical_shell_pid: u32,
control_flow: ControlFlow,
}
#[derive(Debug, Clone)]
pub(crate) struct DetachedStateCheckpoint {
options: u32,
interactive: bool,
last_status: i32,
variables: VariableStoreCheckpoint,
cwd: PathBuf,
definitions: DefinitionStoreCheckpoint,
last_bg_pid: Option<u32>,
traps: HashMap<i32, String>,
warned_vi_unsupported: bool,
fd_table: FdTableCheckpoint,
logical_shell_pid: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct BackgroundMachinePayload {
pub(super) definition: crate::embed::PortableShellDefinition,
pub(super) state: BackgroundStateCheckpoint,
pub(super) and_or_list: AndOrList,
pub(super) command_id: String,
}
impl Default for ShellState {
fn default() -> Self {
Self::new()
}
}
impl ShellState {
pub fn new() -> Self {
let cwd = startup_cwd();
Self {
exit_code: -1,
options: 0,
interactive: false,
last_status: 0,
variable_store: VariableStore::new(),
definition_store: DefinitionStore::new(),
path_state: PathState { cwd },
control_flow: ControlFlow::None,
loop_depth: 0,
function_depth: 0,
dot_script_depth: 0,
trap_table: TrapTable::new(),
job_table: JobTable::new(),
warned_vi_unsupported: false,
manage_signals: false,
monitor_signals: MonitorSignalState::default(),
monitor_mode_active: false,
expansion_error: None,
command_substitution_status: None,
process_isolated: false,
execution_context: ExecutionContext::top_level(),
errexit_contexts: Vec::new(),
next_status_errexit_exempt: None,
current_source: None,
active_command_id: None,
stdin_fd: sys::FileDescriptor::STDIN,
stdout_fd: sys::FileDescriptor::STDOUT,
stderr_fd: sys::FileDescriptor::STDERR,
fd_table: FdTable {
raw_fds: HashMap::new(),
named_fds: HashMap::new(),
},
outcome_captures: Vec::new(),
definition: Arc::new(crate::embed::ShellDefinition::default()),
}
}
pub(crate) fn fork_for(&self, kind: ExecutionContextKind) -> Self {
let mut traps = self.trap_table.traps.clone();
if matches!(
kind,
ExecutionContextKind::BackgroundJob | ExecutionContextKind::DetachedSession
) {
traps.remove(&TRAP_EXIT);
}
let detached = matches!(kind, ExecutionContextKind::DetachedSession);
let inherited_exit_trap = matches!(
kind,
ExecutionContextKind::Subshell
| ExecutionContextKind::CommandSubstitution
| ExecutionContextKind::PipelineStage
) && traps.contains_key(&TRAP_EXIT);
Self {
exit_code: self.exit_code,
options: self.options,
interactive: self.interactive,
last_status: self.last_status,
variable_store: VariableStore {
frame: self.variable_store.frame.clone(),
vars: self.variable_store.vars.clone(),
unset_var_attribs: self.variable_store.unset_var_attribs.clone(),
getopts_cursor: self.variable_store.getopts_cursor,
getopts_index: self.variable_store.getopts_index,
},
definition_store: DefinitionStore {
functions: self.definition_store.functions.clone(),
aliases: Arc::clone(&self.definition_store.aliases),
},
path_state: PathState {
cwd: self.path_state.cwd.clone(),
},
control_flow: ControlFlow::None,
loop_depth: self.loop_depth,
function_depth: self.function_depth,
dot_script_depth: self.dot_script_depth,
trap_table: TrapTable {
traps,
owner_id: next_trap_owner_id(),
inherited_exit_trap,
},
job_table: JobTable {
last_bg_pid: self.job_table.last_bg_pid,
notified_jobs: HashSet::new(),
next_job_id: 1,
jobs: Vec::new(),
},
warned_vi_unsupported: self.warned_vi_unsupported,
manage_signals: false,
monitor_signals: MonitorSignalState::default(),
monitor_mode_active: false,
expansion_error: self.expansion_error,
command_substitution_status: None,
process_isolated: true,
execution_context: ExecutionContext::child(
kind,
self.execution_context.logical_shell_pid,
),
errexit_contexts: self.errexit_contexts.clone(),
next_status_errexit_exempt: None,
current_source: self.current_source.clone(),
active_command_id: if detached {
None
} else {
self.active_command_id.clone()
},
stdin_fd: self.stdin_fd,
stdout_fd: self.stdout_fd,
stderr_fd: self.stderr_fd,
fd_table: self.fd_table.clone(),
outcome_captures: if detached {
Vec::new()
} else {
self.outcome_captures.clone()
},
definition: Arc::clone(&self.definition),
}
}
pub fn last_background_pid(&self) -> Option<u32> {
self.job_table.last_bg_pid
}
pub(crate) fn shell_name(&self) -> &str {
self.definition.identity.name()
}
pub(super) fn active_command_id(&self) -> Option<&str> {
self.active_command_id.as_deref()
}
pub(super) fn set_active_command_id(&mut self, command_id: Option<String>) {
self.active_command_id = command_id;
}
pub(crate) fn has_outcome_capture(&self) -> bool {
!self.outcome_captures.is_empty()
}
pub(crate) fn push_outcome_capture(&mut self, capture: OutcomeCaptureHandle) {
self.outcome_captures.push(capture);
}
pub(crate) fn pop_outcome_capture(&mut self) -> Option<OutcomeCaptureHandle> {
self.outcome_captures.pop()
}
pub(crate) fn record_diagnostic(&self, diagnostic: ShellDiagnostic) {
for capture in &self.outcome_captures {
capture
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.diagnostics
.push(diagnostic.clone());
}
}
pub(crate) fn record_trace_event(&self, event: TraceEvent) {
for capture in &self.outcome_captures {
capture
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.trace
.push(event.clone());
}
}
pub(crate) fn prefixed_message(&self, message: impl AsRef<str>) -> String {
format!("{}: {}", self.shell_name(), message.as_ref())
}
pub(crate) fn note_stdout_write_error(&mut self, err: &io::Error) {
if (err.raw_os_error() == Some(libc::EPIPE) || err.kind() == io::ErrorKind::BrokenPipe)
&& matches!(
self.execution_context.kind,
ExecutionContextKind::PipelineStage
)
{
self.set_exit_code_if_unset(128 + libc::SIGPIPE);
}
}
pub(super) fn background_checkpoint(&self) -> BackgroundStateCheckpoint {
let mut traps = self.trap_table.traps.clone();
traps.remove(&TRAP_EXIT);
BackgroundStateCheckpoint {
exit_code: self.exit_code,
options: self.options,
interactive: self.interactive,
last_status: self.last_status,
variables: self.variable_store_checkpoint(),
cwd: self.path_state.cwd.clone(),
definitions: self.definition_store_checkpoint(),
loop_depth: self.loop_depth,
function_depth: self.function_depth,
dot_script_depth: self.dot_script_depth,
last_bg_pid: self.job_table.last_bg_pid,
traps,
warned_vi_unsupported: self.warned_vi_unsupported,
expansion_error: self.expansion_error,
current_source: self.current_source.clone(),
fd_table: self.background_fd_table_checkpoint(),
logical_shell_pid: self.execution_context.logical_shell_pid,
control_flow: self.control_flow,
}
}
pub(crate) fn detached_session_checkpoint(&self) -> DetachedStateCheckpoint {
let mut traps = self.trap_table.traps.clone();
traps.remove(&TRAP_EXIT);
DetachedStateCheckpoint {
options: self.options,
interactive: self.interactive,
last_status: self.last_status,
variables: self.variable_store_checkpoint(),
cwd: self.path_state.cwd.clone(),
definitions: self.definition_store_checkpoint(),
last_bg_pid: self.job_table.last_bg_pid,
traps,
warned_vi_unsupported: self.warned_vi_unsupported,
fd_table: self.fd_table_checkpoint(),
logical_shell_pid: self.execution_context.logical_shell_pid,
}
}
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
pub(super) fn from_background_checkpoint(
definition: Arc<crate::embed::ShellDefinition>,
checkpoint: BackgroundStateCheckpoint,
stdin_fd: sys::FileDescriptor,
stdout_fd: sys::FileDescriptor,
stderr_fd: sys::FileDescriptor,
outcome_captures: Vec<OutcomeCaptureHandle>,
) -> Self {
let mut state = Self::new();
state.exit_code = checkpoint.exit_code;
state.options = checkpoint.options;
state.interactive = checkpoint.interactive;
state.last_status = checkpoint.last_status;
state.variable_store = checkpoint.variables.into_store();
state.variable_store.getopts_cursor = None;
state.variable_store.getopts_index = None;
state.path_state.cwd = checkpoint.cwd;
state.definition_store = checkpoint.definitions.into_store();
state.control_flow = checkpoint.control_flow;
state.loop_depth = checkpoint.loop_depth;
state.function_depth = checkpoint.function_depth;
state.dot_script_depth = checkpoint.dot_script_depth;
state.trap_table.traps = checkpoint.traps;
state.trap_table.owner_id = next_trap_owner_id();
state.trap_table.inherited_exit_trap = false;
state.job_table.last_bg_pid = checkpoint.last_bg_pid;
state.warned_vi_unsupported = checkpoint.warned_vi_unsupported;
state.manage_signals = false;
state.monitor_signals = MonitorSignalState::default();
state.monitor_mode_active = false;
state.expansion_error = checkpoint.expansion_error;
state.command_substitution_status = None;
state.process_isolated = false;
state.execution_context = ExecutionContext {
kind: ExecutionContextKind::BackgroundJob,
logical_shell_pid: checkpoint.logical_shell_pid,
job_scope: JobScope::Owned,
trap_scope: TrapScope::Owned,
fd_scope: FdScope::Owned,
process_global_scope: ProcessGlobalScope::Owned,
errexit_exempt: false,
};
state.errexit_contexts = Vec::new();
state.next_status_errexit_exempt = None;
state.current_source = checkpoint.current_source;
state.active_command_id = None;
state.stdin_fd = stdin_fd;
state.stdout_fd = stdout_fd;
state.stderr_fd = stderr_fd;
state.fd_table = checkpoint.fd_table.into_table();
state.outcome_captures = outcome_captures;
state.definition = definition;
state
}
pub(crate) fn from_detached_session_checkpoint(
definition: Arc<crate::embed::ShellDefinition>,
checkpoint: DetachedStateCheckpoint,
stdin_fd: sys::FileDescriptor,
stdout_fd: sys::FileDescriptor,
stderr_fd: sys::FileDescriptor,
) -> Self {
let mut state = Self::new();
state.options = checkpoint.options;
state.interactive = checkpoint.interactive;
state.last_status = checkpoint.last_status;
state.variable_store = checkpoint.variables.into_store();
state.path_state.cwd = checkpoint.cwd;
state.definition_store = checkpoint.definitions.into_store();
state.trap_table.traps = checkpoint.traps;
state.trap_table.owner_id = next_trap_owner_id();
state.trap_table.inherited_exit_trap = false;
state.job_table.last_bg_pid = checkpoint.last_bg_pid;
state.warned_vi_unsupported = checkpoint.warned_vi_unsupported;
state.manage_signals = false;
state.monitor_signals = MonitorSignalState::default();
state.monitor_mode_active = false;
state.expansion_error = None;
state.command_substitution_status = None;
state.process_isolated = true;
state.execution_context = ExecutionContext::child(
ExecutionContextKind::DetachedSession,
checkpoint.logical_shell_pid,
);
state.errexit_contexts = Vec::new();
state.next_status_errexit_exempt = None;
state.current_source = None;
state.active_command_id = None;
state.stdin_fd = stdin_fd;
state.stdout_fd = stdout_fd;
state.stderr_fd = stderr_fd;
state.fd_table = checkpoint.fd_table.into_table();
state.outcome_captures = Vec::new();
state.definition = definition;
state
}
fn variable_store_checkpoint(&self) -> VariableStoreCheckpoint {
VariableStoreCheckpoint {
frame: self.variable_store.frame.clone(),
vars: self.variable_store.vars.clone(),
unset_var_attribs: self.variable_store.unset_var_attribs.clone(),
getopts_cursor: self.variable_store.getopts_cursor,
getopts_index: self.variable_store.getopts_index,
}
}
fn definition_store_checkpoint(&self) -> DefinitionStoreCheckpoint {
DefinitionStoreCheckpoint {
functions: self.definition_store.functions.clone(),
aliases: self.definition_store.aliases.as_ref().clone(),
}
}
fn fd_table_checkpoint(&self) -> FdTableCheckpoint {
FdTableCheckpoint {
raw_fds: self
.fd_table
.raw_fds
.iter()
.map(|(&child, parent)| (child, parent.as_i32()))
.collect(),
named_fds: self.fd_table.named_fds.clone(),
}
}
fn background_fd_table_checkpoint(&self) -> FdTableCheckpoint {
FdTableCheckpoint {
raw_fds: self
.fd_table
.raw_fds
.iter()
.map(|(&child, parent)| {
let checkpoint_fd = if parent.is_open() {
child
} else {
parent.as_i32()
};
(child, checkpoint_fd)
})
.collect(),
named_fds: self.fd_table.named_fds.clone(),
}
}
pub(crate) fn untracked_ambient_fds(&self) -> Vec<sys::FileDescriptor> {
let mut fds: Vec<_> = ambient_open_fds()
.into_iter()
.filter_map(|(fd, descriptor)| {
(!self.fd_table.raw_fds.contains_key(&fd)).then_some(descriptor)
})
.collect();
fds.sort_by_key(|fd| fd.as_i32());
fds
}
pub fn env_set(&mut self, key: &str, value: String, attrib: u32) -> bool {
let attrib = match self.variable_store.vars.get(key) {
Some(var) if (var.attrib & VAR_READONLY) != 0 => return false,
Some(var) => var.attrib | attrib,
None => {
let pending = self
.variable_store
.unset_var_attribs
.get(key)
.copied()
.unwrap_or(0);
if (pending & VAR_READONLY) != 0 {
return false;
}
pending | attrib
}
};
self.variable_store
.vars
.insert(key.to_string(), Variable { value, attrib });
self.variable_store.unset_var_attribs.remove(key);
self.reset_getopts_state_for(key);
true
}
pub(crate) fn env_set_internal(&mut self, key: &str, value: String, attrib: u32) {
self.variable_store
.vars
.insert(key.to_string(), Variable { value, attrib });
self.variable_store.unset_var_attribs.remove(key);
self.reset_getopts_state_for(key);
}
pub fn env_unset(&mut self, key: &str) -> bool {
if self
.variable_store
.vars
.get(key)
.is_some_and(|var| (var.attrib & VAR_READONLY) != 0)
|| self
.variable_store
.unset_var_attribs
.get(key)
.is_some_and(|attrib| (attrib & VAR_READONLY) != 0)
{
return false;
}
self.variable_store.vars.remove(key);
self.variable_store.unset_var_attribs.remove(key);
self.reset_getopts_state_for(key);
true
}
pub fn env_get(&self, key: &str) -> Option<&str> {
self.variable_store.vars.get(key).map(|v| v.value.as_str())
}
fn reset_getopts_state_for(&mut self, key: &str) {
if key == "OPTIND" {
self.variable_store.getopts_cursor = None;
self.variable_store.getopts_index = None;
}
}
pub fn exported_env(&self) -> Vec<(String, String)> {
self.variable_store
.vars
.iter()
.filter(|(_, v)| (v.attrib & VAR_EXPORT) != 0)
.map(|(k, v)| (k.clone(), v.value.clone()))
.collect()
}
pub(crate) fn inherited_fd_entries(&self) -> Vec<(i32, sys::FileDescriptor)> {
let mut inherited: Vec<_> = self
.fd_table
.raw_fds
.iter()
.filter_map(|(&child, &parent)| parent.is_open().then_some((child, parent)))
.collect();
inherited.sort_by_key(|(child, _)| *child);
inherited
}
#[cfg(feature = "frontend")]
pub(crate) fn import_ambient_fds(&mut self) {
for (child_fd, parent_fd) in ambient_open_fds() {
self.fd_table.raw_fds.entry(child_fd).or_insert(parent_fd);
}
}
pub(crate) fn named_inherited_fd_entries(&self) -> Vec<crate::embed::NamedFileDescriptor> {
let mut inherited: Vec<_> = self
.fd_table
.named_fds
.iter()
.filter_map(|(name, &child)| {
self.fd_table
.raw_fds
.get(&child)
.copied()
.filter(|parent| parent.is_open())
.map(|parent| {
crate::embed::NamedFileDescriptor::new(name.clone(), child, parent)
})
})
.collect();
inherited.sort_by(|left, right| left.name.cmp(&right.name));
inherited
}
pub(crate) fn named_inherited_fd(
&self,
name: &str,
) -> Option<crate::embed::NamedFileDescriptor> {
let child_fd = *self.fd_table.named_fds.get(name)?;
let parent_fd = *self.fd_table.raw_fds.get(&child_fd)?;
if !parent_fd.is_open() {
return None;
}
Some(crate::embed::NamedFileDescriptor::new(
name, child_fd, parent_fd,
))
}
pub(crate) fn set_inherited_fd(
&mut self,
child_fd: i32,
parent_fd: sys::FileDescriptor,
) -> Option<sys::FileDescriptor> {
self.fd_table.raw_fds.insert(child_fd, parent_fd)
}
pub(crate) fn set_named_inherited_fd(
&mut self,
named_fd: crate::embed::NamedFileDescriptor,
) -> Option<crate::embed::NamedFileDescriptor> {
let crate::embed::NamedFileDescriptor {
name,
child_fd,
parent_fd,
} = named_fd;
let previous = self.named_inherited_fd(&name);
self.fd_table.named_fds.insert(name, child_fd);
let _ = self.set_inherited_fd(child_fd, parent_fd);
if let Some(ref previous) = previous
&& previous.child_fd != child_fd
&& !self
.fd_table
.named_fds
.values()
.any(|&named_child| named_child == previous.child_fd)
{
let _ = self.fd_table.raw_fds.remove(&previous.child_fd);
}
previous
}
pub(crate) fn aliases(&self) -> Vec<(String, String)> {
let mut aliases: Vec<_> = self
.definition_store
.aliases
.iter()
.map(|(name, value)| (name.clone(), value.clone()))
.collect();
aliases.sort_by(|left, right| left.0.cmp(&right.0));
aliases
}
pub(crate) fn alias(&self, name: &str) -> Option<&str> {
self.definition_store.aliases.get(name).map(String::as_str)
}
pub(crate) fn set_alias(&mut self, name: impl Into<String>, value: impl Into<String>) {
Arc::make_mut(&mut self.definition_store.aliases).insert(name.into(), value.into());
}
pub(crate) fn functions(&self) -> Vec<(String, AstCommand)> {
let mut functions: Vec<_> = self
.definition_store
.functions
.iter()
.map(|(name, function)| (name.clone(), function.body.clone()))
.collect();
functions.sort_by(|left, right| left.0.cmp(&right.0));
functions
}
pub(crate) fn function(&self, name: &str) -> Option<&AstCommand> {
self.definition_store
.functions
.get(name)
.map(|function| &function.body)
}
pub(crate) fn set_function(&mut self, name: impl Into<String>, body: AstCommand) {
self.definition_store.functions.insert(
name.into(),
ShellFunction {
body,
definition_redirects: Vec::new(),
},
);
}
pub(super) fn set_function_definition(
&mut self,
name: impl Into<String>,
body: AstCommand,
definition_redirects: Vec<IoRedirect>,
) {
self.definition_store.functions.insert(
name.into(),
ShellFunction {
body,
definition_redirects,
},
);
}
pub(super) fn function_definition(&self, name: &str) -> Option<&ShellFunction> {
self.definition_store.functions.get(name)
}
pub(crate) fn argv0(&self) -> Option<&str> {
self.variable_store.frame.first().map(String::as_str)
}
pub(crate) fn positional_parameters(&self) -> &[String] {
self.variable_store.frame.get(1..).unwrap_or(&[])
}
pub(crate) fn initialize_defaults(&mut self) {
self.env_set("IFS", " \t\n".to_string(), 0);
self.env_set("PPID", sys::parent_pid().to_string(), 0);
let pwd = pwd_for_shell_cwd(self.env_get("PWD"), &self.path_state.cwd);
self.env_set_internal("PWD", pwd, VAR_EXPORT);
self.env_set("OPTIND", "1".to_string(), 0);
}
pub fn populate_env(&mut self) {
for (k, v) in env::vars_os() {
let Ok(k) = k.into_string() else {
eprintln!("{}: ignoring non-UTF-8 environment key", self.shell_name());
continue;
};
let Ok(v) = v.into_string() else {
eprintln!(
"{}: ignoring non-UTF-8 environment value for {k}",
self.shell_name()
);
continue;
};
self.env_set(&k, v, VAR_EXPORT);
}
self.initialize_defaults();
}
pub(super) fn has_option(&self, opt: u32) -> bool {
(self.options & opt) != 0
}
pub(super) fn aliases_snapshot(&self) -> Arc<HashMap<String, String>> {
Arc::clone(&self.definition_store.aliases)
}
pub(crate) fn set_last_status(&mut self, status: i32) {
self.last_status = normalize_exit_status(status);
}
pub(crate) fn set_exit_code(&mut self, code: i32) {
self.exit_code = normalize_exit_status(code);
}
pub(super) fn set_exit_code_if_unset(&mut self, code: i32) {
if self.exit_code < 0 {
self.exit_code = normalize_exit_status(code);
}
}
pub(super) fn set_last_background_pid(&mut self, pid: Option<u32>) {
self.job_table.last_bg_pid = pid;
}
pub(super) fn set_current_source(&mut self, source: Option<String>) {
self.current_source = source;
}
pub(super) fn record_expansion_error(&mut self, status: i32) {
if self.expansion_error.is_none() {
self.expansion_error = Some(normalize_exit_status(status));
}
}
pub(super) fn take_expansion_error(&mut self) -> Option<i32> {
self.expansion_error.take()
}
pub(super) fn clear_command_substitution_status(&mut self) {
self.command_substitution_status = None;
}
pub(super) fn record_command_substitution_status(&mut self, status: i32) {
self.command_substitution_status = Some(normalize_exit_status(status));
}
pub(super) fn take_command_substitution_status(&mut self) -> Option<i32> {
self.command_substitution_status.take()
}
pub(super) fn logical_shell_pid(&self) -> u32 {
self.execution_context.logical_shell_pid
}
pub(super) fn enter_local_execution_context(
&mut self,
kind: ExecutionContextKind,
) -> ExecutionContext {
let previous = self.execution_context;
self.execution_context = ExecutionContext { kind, ..previous };
previous
}
pub(super) fn restore_execution_context(&mut self, context: ExecutionContext) {
self.execution_context = context;
}
pub(super) fn push_errexit_context(&mut self, context: ErrexitContext) {
self.errexit_contexts.push(context);
}
pub(super) fn pop_errexit_context(&mut self) {
let _ = self.errexit_contexts.pop();
}
pub(super) fn errexit_is_exempt(&self) -> bool {
!self.errexit_contexts.is_empty() || self.execution_context.errexit_exempt
}
pub(super) fn mark_next_status_errexit_exempt(&mut self, context: ErrexitContext) {
self.next_status_errexit_exempt = Some(context);
}
pub(super) fn take_next_status_errexit_exempt(&mut self) -> bool {
self.next_status_errexit_exempt.take().is_some()
}
}
pub(super) struct ProcessGlobalGuard {
umask: Option<u32>,
resource_limits: Vec<sys::CapturedResourceLimit>,
}
impl ProcessGlobalGuard {
pub(super) fn capture_for(state: &ShellState) -> Self {
let resource_limits = if state.process_isolated {
sys::capture_resource_limits()
} else {
Vec::new()
};
Self {
umask: state.process_isolated.then(sys::get_umask),
resource_limits,
}
}
}
impl Drop for ProcessGlobalGuard {
fn drop(&mut self) {
if let Some(umask) = self.umask {
sys::set_umask(umask);
}
sys::restore_resource_limits(&self.resource_limits);
}
}