mxsh 0.1.0

Embeddable POSIX-style shell parser and runtime
Documentation
use std::io;

use super::*;

#[derive(Clone)]
struct BackgroundFinishEvent {
    command_id: String,
    job_id: u32,
    display_pid: Option<u32>,
    status: i32,
}

fn take_background_finish_event(job: &mut ShellJob) -> Option<BackgroundFinishEvent> {
    let JobState::Done(status) = job.state else {
        return None;
    };
    if job.finish_emitted {
        return None;
    }
    job.finish_emitted = true;
    Some(BackgroundFinishEvent {
        command_id: job.command_id.clone(),
        job_id: job.job_id,
        display_pid: job.display_pid,
        status,
    })
}

fn emit_background_finish_events(state: &ShellState, events: &[BackgroundFinishEvent]) {
    for event in events {
        shell_events::emit_background_finish(
            state,
            &event.command_id,
            event.job_id,
            event.display_pid,
            event.status,
        );
    }
}

pub(super) fn process_event_to_job_state(event: sys::ProcessEvent) -> JobState {
    match event {
        sys::ProcessEvent::Running | sys::ProcessEvent::Continued => JobState::Running,
        sys::ProcessEvent::Stopped(sig) => JobState::Stopped(sig),
        sys::ProcessEvent::Exited(code) => JobState::Done(code),
        sys::ProcessEvent::Signaled(sig) => JobState::Done(128 + sig),
    }
}

pub(super) fn refresh_jobs<R: Runtime>(state: &mut ShellState, runtime: &mut R) {
    let mut finished = Vec::new();
    for job in &mut state.jobs {
        if !matches!(job.state, JobState::Running) {
            continue;
        }
        let Ok(event) = runtime.wait_process(job.handle, sys::WaitMode::Poll) else {
            continue;
        };
        if !matches!(event, sys::ProcessEvent::Running) {
            job.state = process_event_to_job_state(event);
            if let Some(event) = take_background_finish_event(job) {
                finished.push(event);
            }
        }
    }
    emit_background_finish_events(state, &finished);
}

pub(super) fn job_list<R: Runtime>(state: &mut ShellState, runtime: &mut R) -> Vec<JobInfo> {
    refresh_jobs(state, runtime);
    state
        .jobs
        .iter()
        .map(|job| JobInfo {
            job_id: job.job_id,
            display_pid: job.display_pid,
            state: job.state.clone(),
        })
        .collect()
}

pub(crate) fn maybe_notify_jobs<R: Runtime>(state: &mut ShellState, runtime: &mut R) {
    let mut jobs = job_list(state, runtime);
    let live: HashSet<u32> = jobs.iter().map(|job| job.job_id).collect();
    state.notified_jobs.retain(|job_id| live.contains(job_id));
    jobs.sort_by_key(|job| job.job_id);
    for job in jobs {
        if state.notified_jobs.contains(&job.job_id) {
            continue;
        }
        let status = match job.state {
            JobState::Done(code) => format!("Done({code})"),
            JobState::Stopped(sig) => format!("Stopped(SIG{sig})"),
            JobState::Running => continue,
        };
        let pid = job
            .display_pid
            .map(|pid| pid.to_string())
            .unwrap_or_else(|| "?".to_string());
        shell_errln(state, &format!("[{}] {} {}", job.job_id, status, pid));
        state.notified_jobs.insert(job.job_id);
    }
}

fn wait_on_job_index<R: Runtime>(state: &mut ShellState, runtime: &mut R, idx: usize) -> i32 {
    if let Some(event) = {
        let job = &mut state.jobs[idx];
        take_background_finish_event(job)
    } {
        shell_events::emit_background_finish(
            state,
            &event.command_id,
            event.job_id,
            event.display_pid,
            event.status,
        );
    }
    if let JobState::Done(status) = state.jobs[idx].state {
        state.jobs.remove(idx);
        return status;
    }
    let handle = state.jobs[idx].handle;
    loop {
        let event = match runtime.wait_process(handle, sys::WaitMode::Block) {
            Ok(event) => event,
            Err(_) => {
                state.jobs.remove(idx);
                return 128;
            }
        };
        match process_event_to_job_state(event) {
            JobState::Running => continue,
            JobState::Stopped(sig) => {
                state.jobs[idx].state = JobState::Stopped(sig);
                return 128 + sig;
            }
            JobState::Done(code) => {
                let event = {
                    state.jobs[idx].state = JobState::Done(code);
                    let job = &mut state.jobs[idx];
                    take_background_finish_event(job)
                };
                if let Some(event) = event {
                    shell_events::emit_background_finish(
                        state,
                        &event.command_id,
                        event.job_id,
                        event.display_pid,
                        event.status,
                    );
                }
                state.jobs.remove(idx);
                return code;
            }
        }
    }
}

pub(super) fn wait_job<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    job_id: u32,
) -> Option<i32> {
    refresh_jobs(state, runtime);
    let idx = state.jobs.iter().position(|job| job.job_id == job_id)?;
    Some(wait_on_job_index(state, runtime, idx))
}

pub(super) fn wait_pid<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    pid: u32,
) -> Result<Option<i32>, io::Error> {
    refresh_jobs(state, runtime);
    if let Some(idx) = state
        .jobs
        .iter()
        .position(|job| job.display_pid == Some(pid))
    {
        return Ok(Some(wait_on_job_index(state, runtime, idx)));
    }
    match runtime.wait_display_pid(pid) {
        Ok(Some(status)) => Ok(Some(status)),
        Ok(None) => Ok(None),
        Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
        Err(err) => Err(err),
    }
}

pub(super) fn wait_all_jobs<R: Runtime>(state: &mut ShellState, runtime: &mut R) -> i32 {
    refresh_jobs(state, runtime);
    let mut last_status = 0;
    for i in (0..state.jobs.len()).rev() {
        last_status = wait_on_job_index(state, runtime, i);
    }
    last_status
}

pub(super) fn continue_job_background<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    job_id: u32,
) -> bool {
    refresh_jobs(state, runtime);
    let Some(job) = state.jobs.iter_mut().find(|job| job.job_id == job_id) else {
        return false;
    };
    if runtime
        .signal_process_group(job.handle, sys::RuntimeSignal::Continue)
        .is_ok()
    {
        job.state = JobState::Running;
        true
    } else {
        false
    }
}

pub(super) fn continue_job_foreground<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    job_id: u32,
) -> Option<i32> {
    refresh_jobs(state, runtime);
    let handle = state
        .jobs
        .iter()
        .find(|job| job.job_id == job_id)
        .map(|job| job.handle)?;
    if state.has_option(OPT_MONITOR) && state.stdin_fd.is_valid() {
        let guard = runtime.claim_foreground(handle, state.stdin_fd).ok()?;
        let _ = runtime.signal_process_group(handle, sys::RuntimeSignal::Continue);
        let status = wait_job(state, runtime, job_id);
        let _ = runtime.release_foreground(guard);
        return status;
    }
    let _ = runtime.signal_process_group(handle, sys::RuntimeSignal::Continue);
    wait_job(state, runtime, job_id)
}

#[allow(dead_code)]
pub(super) fn pause_job<R: Runtime>(state: &mut ShellState, runtime: &mut R, job_id: u32) -> bool {
    refresh_jobs(state, runtime);
    let Some(job) = state.jobs.iter_mut().find(|job| job.job_id == job_id) else {
        return false;
    };
    if runtime
        .signal_process_group(job.handle, sys::RuntimeSignal::Stop)
        .is_ok()
    {
        job.state = JobState::Stopped(libc::SIGSTOP);
        true
    } else {
        false
    }
}

pub(super) fn run_background<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    aol: &AndOrList,
) -> Result<Option<u32>, i32> {
    let command_id = shell_events::new_command_id();
    let canonical_command = shell_events::canonical_command_list_text(aol, true);
    let spawned =
        super::machine::spawn_background_job(state, runtime, aol, &command_id).map_err(|err| {
            let status = if err.starts_with("failed to spawn background job:") {
                126
            } else {
                2
            };
            shell_errln(state, &state.prefixed_message(err));
            status
        })?;
    let job_id = state.next_job_id;
    state.next_job_id += 1;
    state.jobs.push(ShellJob {
        job_id,
        handle: spawned.handle,
        display_pid: spawned.display_pid,
        state: JobState::Running,
        command_id: command_id.clone(),
        finish_emitted: false,
    });
    shell_events::emit_background_start(
        state,
        &command_id,
        Some(&canonical_command),
        &canonical_command,
        job_id,
        spawned.display_pid,
    );
    Ok(spawned.display_pid)
}