Skip to content

Add stop/continue job control - #7

Open
theduardomaciel wants to merge 1 commit into
hex2f:mainfrom
theduardomaciel:feat/job-control
Open

theduardomaciel wants to merge 1 commit into
hex2f:mainfrom
theduardomaciel:feat/job-control

Conversation

@theduardomaciel

Copy link
Copy Markdown

What

Adds the stop/continue subset of job control that Windows can actually support:

  • SIGSTOP (19), SIGTSTP (20) and SIGCONT (18) are recognized by kill and kill -l.
  • kill -STOP / -TSTP suspends an external job or PID via NtSuspendProcess; -CONT resumes it via NtResumeProcess.
  • jobs reports Stopped.
  • bg resumes a stopped job; fg resumes before waiting.

It also fixes an existing footgun: unknown signal names fell back to SIGTERM, so kill -STOP %1 silently killed the job.

Limits

Windows has no SIGTSTP and no console job control. Ctrl+Z cannot suspend a foreground child because the child owns the console. In-process (JobKind::Thread) jobs cannot be suspended and report an error. The feature is still useful for background external jobs and kill -STOP <pid>.

Testing

Built with the release workflow and verified on Windows 11:

/usr/bin/sleep 60 &
kill -STOP %1     # jobs -> Stopped
bg                # jobs -> Running
kill -CONT %1

/usr/bin/sleep 2 &
kill -STOP %1
fg %1             # resumes and waits

kill -l 19        # STOP

Map SIGSTOP, SIGTSTP and SIGCONT to NtSuspendProcess/NtResumeProcess, make
bg resume a stopped job and fg resume before waiting, and report stopped
jobs from jobs.

Only external jobs can be stopped; in-process jobs report an error. Windows
has no SIGTSTP, so Ctrl+Z still cannot suspend a foreground child.

Co-authored-by: DeepSeek V4.1 Flash <noreply@opencode.ai>

@hex2f hex2f left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, it's a useful feature. There are a few problems, though, and some of them change what scripts do. Line comments below.

I merged it onto current main and ran cargo test: all 65 compat scripts still pass. Nothing tests the new code yet.

Stopped jobs outlive the shell. sleep 1 & kill -STOP $!; exit leaves sleep.exe suspended after gash exits, and it stays that way, since on Windows nothing ever resumes or ends it. I reproduced this. Bash warns about stopped jobs on exit, and sends them SIGHUP and SIGCONT. At minimum, gash should resume or terminate the processes it suspended when it exits.

Missing regression test: please add a tests/scripts case, for example: stop a sleep, check jobs, send CONT, then wait and check the status. Keep in mind that Git Bash's non-interactive jobs prints Running for a stopped job, so the expected output has to come from regen_expected.ps1.

Needs a rebase: #13 has been merged. It edits the same job loop in kill, and Job::kill() now takes the signal, so this branch conflicts with main.

Comment thread src/builtins.rs
"10" | "USR1" => "USR1",
"12" | "USR2" => "USR2",
"17" | "CHLD" => "CHLD",
"18" | "CONT" => "CONT",

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These numbers don't match Git Bash. Git Bash uses Cygwin's numbering: STOP=17, TSTP=18, CONT=19, CHLD=20. kill -l in Git Bash 5.3.15 prints those, and this PR uses Linux's. As a result:

  • kill -19 $p stops the job in gash but resumes it in Git Bash.
  • kill -18 $p resumes it in gash but sends TSTP in Git Bash.
  • kill -l 19 prints STOP here and CONT in Git Bash.

Scripts written for Git Bash would do the opposite of what they mean. gash's existing CHLD=17 has the same problem, so this may be worth fixing in one place, in norm_sig/sig_num.

Comment thread src/builtins.rs
let spec = args.first().cloned().unwrap_or_else(|| "%+".into());
match find_job(sh, &spec) {
Some(i) => {
let mut j = sh.jobs.list.remove(i);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The job is lost when resuming fails. The job is removed from sh.jobs.list here, before the resume() check below. If resuming fails, the function returns Ok(1) and the Job is dropped, so it no longer appears in jobs, wait or kill %n. Check (and resume) before removing it.

Comment thread src/builtins.rs
(j.id, j.text.clone(), ok)
};
if !ok {
sh.eprint(&format!("bg: {}: job is not stopped or cannot be resumed", spec));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bg on a running job (nit). Bash prints bg: job 1 already in background and returns 0 when the job is already running. This returns 1 with an error, so bg in a script that doesn't know the job's state now fails.

Comment thread src/sys.rs
}
}

pub fn suspend_pid(pid: u32) -> bool {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only the one process is suspended. NtSuspendProcess suspends a single process, not the processes it started. A job such as a .bat file, cmd /c ..., or an MSYS program that spawns helpers keeps its children running while jobs says Stopped. Putting background jobs in a job object, and suspending every process in it, would come closer to stopping a process group. If that's out of scope, please at least note the limitation in the README.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants