Inter-Job direct messaging (#15253)

# Description

This PR implements an experimental inter-job communication model,
through direct message passing, aka "mail"ing or "dm"ing:



- `job send <id>`: Sends a message the job with the given id, the root
job has id 0. Messages are stored in the recipient's "mailbox"
- `job recv`: Returns a stored message, blocks if the mailbox is empty
- `job flush`: Clear all messages from mailbox

Additionally, messages can be sent with a numeric tag, which can then be
filtered with `mail recv --tag`.
This is useful for spawning jobs and receiving messages specifically
from those jobs.

This PR is mostly a proof of concept for how inter-job communication
could look like, so people can provide feedback and suggestions

Closes  #15199

May close #15220 since now jobs can access their own id.

# User-Facing Changes

Adds, `job id`, `job send`, `job recv` and `job flush`  commands.

# Tests + Formatting

[X] TODO:  Implement tests
[X] Consider rewriting some of the job-related tests to use this, to
make them a bit less fragile.

# After Submitting
This commit is contained in:
Renan Ribeiro 2025-04-26 12:24:35 -03:00 committed by GitHub
parent 0389815137
commit 2d868323b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 853 additions and 47 deletions

View File

@ -452,10 +452,18 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
JobSpawn,
JobList,
JobKill,
JobId,
JobTag,
Job,
};
#[cfg(not(target_family = "wasm"))]
bind_command! {
JobSend,
JobRecv,
JobFlush,
}
#[cfg(all(unix, feature = "os"))]
bind_command! {
JobUnfreeze,

View File

@ -0,0 +1,58 @@
use nu_engine::command_prelude::*;
#[derive(Clone)]
pub struct JobFlush;
impl Command for JobFlush {
fn name(&self) -> &str {
"job flush"
}
fn description(&self) -> &str {
"Clear this job's mailbox."
}
fn extra_description(&self) -> &str {
r#"
This command removes all messages in the mailbox of the current job.
If a message is received while this command is executing, it may also be discarded.
"#
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("job flush")
.category(Category::Experimental)
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
.allow_variants_without_examples(true)
}
fn search_terms(&self) -> Vec<&str> {
vec![]
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let mut mailbox = engine_state
.current_job
.mailbox
.lock()
.expect("failed to acquire lock");
mailbox.clear();
Ok(Value::nothing(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![Example {
example: "job flush",
description: "Clear the mailbox of the current job.",
result: None,
}]
}
}

View File

@ -0,0 +1,50 @@
use nu_engine::command_prelude::*;
#[derive(Clone)]
pub struct JobId;
impl Command for JobId {
fn name(&self) -> &str {
"job id"
}
fn description(&self) -> &str {
"Get id of current job."
}
fn extra_description(&self) -> &str {
"This command returns the job id for the current background job.
The special id 0 indicates that this command was not called from a background job thread, and
was instead spawned by main nushell execution thread."
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("job id")
.category(Category::Experimental)
.input_output_types(vec![(Type::Nothing, Type::Int)])
}
fn search_terms(&self) -> Vec<&str> {
vec!["self", "this", "my-id", "this-id"]
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
Ok(Value::int(engine_state.current_job.id.get() as i64, head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![Example {
example: "job id",
description: "Get id of current job",
result: None,
}]
}
}

View File

@ -0,0 +1,181 @@
use std::{
sync::mpsc::{RecvTimeoutError, TryRecvError},
time::{Duration, Instant},
};
use nu_engine::command_prelude::*;
use nu_protocol::{
engine::{FilterTag, Mailbox},
Signals,
};
#[derive(Clone)]
pub struct JobRecv;
const CTRL_C_CHECK_INTERVAL: Duration = Duration::from_millis(100);
impl Command for JobRecv {
fn name(&self) -> &str {
"job recv"
}
fn description(&self) -> &str {
"Read a message from the mailbox."
}
fn extra_description(&self) -> &str {
r#"When messages are sent to the current process, they get stored in what is called the "mailbox".
This commands reads and returns a message from the mailbox, in a first-in-first-out fashion.
j
Messages may have numeric flags attached to them. This commands supports filtering out messages that do not satisfy a given tag, by using the `tag` flag.
If no tag is specified, this command will accept any message.
If no message with the specified tag (if any) is available in the mailbox, this command will block the current thread until one arrives.
By default this command block indefinitely until a matching message arrives, but a timeout duration can be specified.
If a timeout duration of zero is specified, it will succeed only if there already is a message in the mailbox.
Note: When using par-each, only one thread at a time can utilize this command.
In the case of two or more threads running this command, they will wait until other threads are done using it,
in no particular order, regardless of the specified timeout parameter.
"#
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("job recv")
.category(Category::Experimental)
.named("tag", SyntaxShape::Int, "A tag for the message", None)
.named(
"timeout",
SyntaxShape::Duration,
"The maximum time duration to wait for.",
None,
)
.input_output_types(vec![(Type::Nothing, Type::Any)])
.allow_variants_without_examples(true)
}
fn search_terms(&self) -> Vec<&str> {
vec!["receive"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let tag_arg: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "tag")?;
if let Some(tag) = tag_arg {
if tag.item < 0 {
return Err(ShellError::NeedsPositiveValue { span: tag.span });
}
}
let tag = tag_arg.map(|it| it.item as FilterTag);
let duration: Option<i64> = call.get_flag(engine_state, stack, "timeout")?;
let timeout = duration.map(|it| Duration::from_nanos(it as u64));
let mut mailbox = engine_state
.current_job
.mailbox
.lock()
.expect("failed to acquire lock");
if let Some(timeout) = timeout {
if timeout == Duration::ZERO {
recv_instantly(&mut mailbox, tag, head)
} else {
recv_with_time_limit(&mut mailbox, tag, engine_state.signals(), head, timeout)
}
} else {
recv_without_time_limit(&mut mailbox, tag, engine_state.signals(), head)
}
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
example: "job recv",
description: "Block the current thread while no message arrives",
result: None,
},
Example {
example: "job recv --timeout 10sec",
description: "Receive a message, wait for at most 10 seconds.",
result: None,
},
Example {
example: "job recv --timeout 0sec",
description: "Get a message or fail if no message is available immediately",
result: None,
},
]
}
}
fn recv_without_time_limit(
mailbox: &mut Mailbox,
tag: Option<FilterTag>,
signals: &Signals,
span: Span,
) -> Result<PipelineData, ShellError> {
loop {
if signals.interrupted() {
return Err(ShellError::Interrupted { span });
}
match mailbox.recv_timeout(tag, CTRL_C_CHECK_INTERVAL) {
Ok(value) => return Ok(value),
Err(RecvTimeoutError::Timeout) => {} // try again
Err(RecvTimeoutError::Disconnected) => return Err(ShellError::Interrupted { span }),
}
}
}
fn recv_instantly(
mailbox: &mut Mailbox,
tag: Option<FilterTag>,
span: Span,
) -> Result<PipelineData, ShellError> {
match mailbox.try_recv(tag) {
Ok(value) => Ok(value),
Err(TryRecvError::Empty) => Err(ShellError::RecvTimeout { span }),
Err(TryRecvError::Disconnected) => Err(ShellError::Interrupted { span }),
}
}
fn recv_with_time_limit(
mailbox: &mut Mailbox,
tag: Option<FilterTag>,
signals: &Signals,
span: Span,
timeout: Duration,
) -> Result<PipelineData, ShellError> {
let deadline = Instant::now() + timeout;
loop {
if signals.interrupted() {
return Err(ShellError::Interrupted { span });
}
let time_until_deadline = deadline.saturating_duration_since(Instant::now());
let time_to_sleep = time_until_deadline.min(CTRL_C_CHECK_INTERVAL);
match mailbox.recv_timeout(tag, time_to_sleep) {
Ok(value) => return Ok(value),
Err(RecvTimeoutError::Timeout) => {} // try again
Err(RecvTimeoutError::Disconnected) => return Err(ShellError::Interrupted { span }),
}
if time_until_deadline.is_zero() {
return Err(ShellError::RecvTimeout { span });
}
}
}

View File

@ -0,0 +1,112 @@
use nu_engine::command_prelude::*;
use nu_protocol::{engine::FilterTag, JobId};
#[derive(Clone)]
pub struct JobSend;
impl Command for JobSend {
fn name(&self) -> &str {
"job send"
}
fn description(&self) -> &str {
"Send a message to the mailbox of a job."
}
fn extra_description(&self) -> &str {
r#"
This command sends a message to a background job, which can then read sent messages
in a first-in-first-out fashion with `job recv`. When it does so, it may additionally specify a numeric filter tag,
in which case it will only read messages sent with the exact same filter tag.
In particular, the id 0 refers to the main/initial nushell thread.
A message can be any nushell value, and streams are always collected before being sent.
This command never blocks.
"#
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("job send")
.category(Category::Experimental)
.required(
"id",
SyntaxShape::Int,
"The id of the job to send the message to.",
)
.named("tag", SyntaxShape::Int, "A tag for the message", None)
.input_output_types(vec![(Type::Any, Type::Nothing)])
.allow_variants_without_examples(true)
}
fn search_terms(&self) -> Vec<&str> {
vec![]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let id_arg: Spanned<i64> = call.req(engine_state, stack, 0)?;
let tag_arg: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "tag")?;
let id = id_arg.item;
if id < 0 {
return Err(ShellError::NeedsPositiveValue { span: id_arg.span });
}
if let Some(tag) = tag_arg {
if tag.item < 0 {
return Err(ShellError::NeedsPositiveValue { span: tag.span });
}
}
let tag = tag_arg.map(|it| it.item as FilterTag);
if id == 0 {
engine_state
.root_job_sender
.send((tag, input))
.expect("this should NEVER happen.");
} else {
let jobs = engine_state.jobs.lock().expect("failed to acquire lock");
if let Some(job) = jobs.lookup(JobId::new(id as usize)) {
match job {
nu_protocol::engine::Job::Thread(thread_job) => {
// it is ok to send this value while holding the lock, because
// mail channels are always unbounded, so this send never blocks
let _ = thread_job.sender.send((tag, input));
}
nu_protocol::engine::Job::Frozen(_) => {
return Err(ShellError::JobIsFrozen {
id: id as usize,
span: id_arg.span,
});
}
}
} else {
return Err(ShellError::JobNotFound {
id: id as usize,
span: id_arg.span,
});
}
}
Ok(Value::nothing(head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
vec![Example {
example: "let id = job spawn { job recv | save sent.txt }; 'hi' | job send $id",
description: "Send a message to a newly spawned job",
result: None,
}]
}
}

View File

@ -1,14 +1,14 @@
use std::{
sync::{
atomic::{AtomicBool, AtomicU32},
Arc,
mpsc, Arc, Mutex,
},
thread,
};
use nu_engine::{command_prelude::*, ClosureEvalOnce};
use nu_protocol::{
engine::{Closure, Job, Redirection, ThreadJob},
engine::{Closure, CurrentJob, Job, Mailbox, Redirection, ThreadJob},
report_shell_error, OutDest, Signals,
};
@ -57,12 +57,11 @@ impl Command for JobSpawn {
let closure: Closure = call.req(engine_state, stack, 0)?;
let tag: Option<String> = call.get_flag(engine_state, stack, "tag")?;
let job_stack = stack.clone();
let mut job_state = engine_state.clone();
job_state.is_interactive = false;
let job_stack = stack.clone();
// the new job should have its ctrl-c independent of foreground
let job_signals = Signals::new(Arc::new(AtomicBool::new(false)));
job_state.set_signals(job_signals.clone());
@ -75,10 +74,20 @@ impl Command for JobSpawn {
let jobs = job_state.jobs.clone();
let mut jobs = jobs.lock().expect("jobs lock is poisoned!");
let (send, recv) = mpsc::channel();
let id = {
let thread_job = ThreadJob::new(job_signals, tag);
job_state.current_thread_job = Some(thread_job.clone());
jobs.add_job(Job::Thread(thread_job))
let thread_job = ThreadJob::new(job_signals, tag, send);
let id = jobs.add_job(Job::Thread(thread_job.clone()));
job_state.current_job = CurrentJob {
id,
background_thread_job: Some(thread_job),
mailbox: Arc::new(Mutex::new(Mailbox::new(recv))),
};
id
};
let result = thread::Builder::new()

View File

@ -118,7 +118,7 @@ fn unfreeze_job(
}) => {
let pid = handle.pid();
if let Some(thread_job) = &state.current_thread_job {
if let Some(thread_job) = &state.current_thread_job() {
if !thread_job.try_add_pid(pid) {
kill_by_pid(pid.into()).map_err(|err| {
ShellError::Io(IoError::new_internal(
@ -136,7 +136,7 @@ fn unfreeze_job(
.then(|| state.pipeline_externals_state.clone()),
);
if let Some(thread_job) = &state.current_thread_job {
if let Some(thread_job) = &state.current_thread_job() {
thread_job.remove_pid(pid);
}

View File

@ -1,5 +1,6 @@
mod is_admin;
mod job;
mod job_id;
mod job_kill;
mod job_list;
mod job_spawn;
@ -8,12 +9,27 @@ mod job_tag;
#[cfg(all(unix, feature = "os"))]
mod job_unfreeze;
#[cfg(not(target_family = "wasm"))]
mod job_flush;
#[cfg(not(target_family = "wasm"))]
mod job_recv;
#[cfg(not(target_family = "wasm"))]
mod job_send;
pub use is_admin::IsAdmin;
pub use job::Job;
pub use job_id::JobId;
pub use job_kill::JobKill;
pub use job_list::JobList;
pub use job_spawn::JobSpawn;
pub use job_tag::JobTag;
#[cfg(not(target_family = "wasm"))]
pub use job_flush::JobFlush;
#[cfg(not(target_family = "wasm"))]
pub use job_recv::JobRecv;
#[cfg(not(target_family = "wasm"))]
pub use job_send::JobSend;
#[cfg(all(unix, feature = "os"))]
pub use job_unfreeze::JobUnfreeze;

View File

@ -285,7 +285,7 @@ impl Command for External {
)
})?;
if let Some(thread_job) = &engine_state.current_thread_job {
if let Some(thread_job) = engine_state.current_thread_job() {
if !thread_job.try_add_pid(child.pid()) {
kill_by_pid(child.pid().into()).map_err(|err| {
ShellError::Io(IoError::new_internal(

View File

@ -1,22 +1,188 @@
use nu_test_support::{nu, playground::Playground};
use nu_test_support::nu;
#[test]
fn jobs_do_run() {
Playground::setup("job_test_1", |dirs, sandbox| {
sandbox.with_files(&[]);
fn job_send_root_job_works() {
let actual = nu!(r#"
job spawn { 'beep' | job send 0 }
job recv --timeout 10sec"#);
let actual = nu!(
cwd: dirs.root(),
r#"
rm -f a.txt;
job spawn { sleep 200ms; 'a' | save a.txt };
let before = 'a.txt' | path exists;
sleep 400ms;
let after = 'a.txt' | path exists;
[$before, $after] | to nuon"#
);
assert_eq!(actual.out, "[false, true]");
})
assert_eq!(actual.out, "beep");
}
#[test]
fn job_send_background_job_works() {
let actual = nu!(r#"
let job = job spawn { job recv | job send 0 }
'boop' | job send $job
job recv --timeout 10sec"#);
assert_eq!(actual.out, "boop");
}
#[test]
fn job_send_to_self_works() {
let actual = nu!(r#"
"meep" | job send 0
job recv"#);
assert_eq!(actual.out, "meep");
}
#[test]
fn job_send_to_self_from_background_works() {
let actual = nu!(r#"
job spawn {
'beep' | job send (job id)
job recv | job send 0
}
job recv --timeout 10sec"#);
assert_eq!(actual.out, "beep");
}
#[test]
fn job_id_of_root_job_is_zero() {
let actual = nu!(r#"job id"#);
assert_eq!(actual.out, "0");
}
#[test]
fn job_id_of_background_jobs_works() {
let actual = nu!(r#"
let job1 = job spawn { job id | job send 0 }
let id1 = job recv --timeout 5sec
let job2 = job spawn { job id | job send 0 }
let id2 = job recv --timeout 5sec
let job3 = job spawn { job id | job send 0 }
let id3 = job recv --timeout 5sec
[($job1 == $id1) ($job2 == $id2) ($job3 == $id3)] | to nuon
"#);
assert_eq!(actual.out, "[true, true, true]");
}
#[test]
fn untagged_job_recv_accepts_tagged_messages() {
let actual = nu!(r#"
job spawn { "boop" | job send 0 --tag 123 }
job recv --timeout 10sec
"#);
assert_eq!(actual.out, "boop");
}
#[test]
fn tagged_job_recv_filters_untagged_messages() {
let actual = nu!(r#"
job spawn { "boop" | job send 0 }
job recv --tag 123 --timeout 1sec
"#);
assert_eq!(actual.out, "");
assert!(actual.err.contains("timeout"));
}
#[test]
fn tagged_job_recv_filters_badly_tagged_messages() {
let actual = nu!(r#"
job spawn { "boop" | job send 0 --tag 321 }
job recv --tag 123 --timeout 1sec
"#);
assert_eq!(actual.out, "");
assert!(actual.err.contains("timeout"));
}
#[test]
fn tagged_job_recv_accepts_properly_tagged_messages() {
let actual = nu!(r#"
job spawn { "boop" | job send 0 --tag 123 }
job recv --tag 123 --timeout 5sec
"#);
assert_eq!(actual.out, "boop");
}
#[test]
fn filtered_messages_are_not_erased() {
let actual = nu!(r#"
"msg1" | job send 0 --tag 123
"msg2" | job send 0 --tag 456
"msg3" | job send 0 --tag 789
let first = job recv --tag 789 --timeout 5sec
let second = job recv --timeout 1sec
let third = job recv --timeout 1sec
[($first) ($second) ($third)] | to nuon
"#);
assert_eq!(actual.out, r#"["msg3", "msg1", "msg2"]"#);
}
#[test]
fn job_recv_timeout_works() {
let actual = nu!(r#"
job spawn {
sleep 2sec
"boop" | job send 0
}
job recv --timeout 1sec
"#);
assert_eq!(actual.out, "");
assert!(actual.err.contains("timeout"));
}
#[test]
fn job_recv_timeout_zero_works() {
let actual = nu!(r#"
"hi there" | job send 0
job recv --timeout 0sec
"#);
assert_eq!(actual.out, "hi there");
}
#[test]
fn job_flush_clears_messages() {
let actual = nu!(r#"
"SALE!!!" | job send 0
"[HYPERLINK BLOCKED]" | job send 0
job flush
job recv --timeout 1sec
"#);
assert_eq!(actual.out, "");
assert!(actual.err.contains("timeout"));
}
#[test]
fn job_flush_clears_filtered_messages() {
let actual = nu!(r#"
"msg1" | job send 0 --tag 123
"msg2" | job send 0 --tag 456
"msg3" | job send 0 --tag 789
job recv --tag 789 --timeout 1sec
job flush
job recv --timeout 1sec
"#);
assert_eq!(actual.out, "");
assert!(actual.err.contains("timeout"));
}
#[test]
@ -31,11 +197,11 @@ fn job_list_adds_jobs_correctly() {
let actual = nu!(format!(
r#"
let list0 = job list | get id;
let job1 = job spawn {{ sleep 20ms }};
let job1 = job spawn {{ job recv }};
let list1 = job list | get id;
let job2 = job spawn {{ sleep 20ms }};
let job2 = job spawn {{ job recv }};
let list2 = job list | get id;
let job3 = job spawn {{ sleep 20ms }};
let job3 = job spawn {{ job recv }};
let list3 = job list | get id;
[({}), ({}), ({}), ({})] | to nuon
"#,
@ -52,11 +218,13 @@ fn job_list_adds_jobs_correctly() {
fn jobs_get_removed_from_list_after_termination() {
let actual = nu!(format!(
r#"
let job = job spawn {{ sleep 0.5sec }};
let job = job spawn {{ job recv }};
let list0 = job list | get id;
sleep 1sec
"die!" | job send $job
sleep 0.2sec
let list1 = job list | get id;
@ -68,6 +236,8 @@ fn jobs_get_removed_from_list_after_termination() {
assert_eq!(actual.out, "[true, true]");
}
// TODO: find way to communicate between process in windows
// so these tests can fail less often
#[test]
fn job_list_shows_pids() {
let actual = nu!(format!(
@ -89,9 +259,9 @@ fn job_list_shows_pids() {
fn killing_job_removes_it_from_table() {
let actual = nu!(format!(
r#"
let job1 = job spawn {{ sleep 100ms }}
let job2 = job spawn {{ sleep 100ms }}
let job3 = job spawn {{ sleep 100ms }}
let job1 = job spawn {{ job recv }}
let job2 = job spawn {{ job recv }}
let job3 = job spawn {{ job recv }}
let list_before = job list | get id

View File

@ -8,9 +8,9 @@ use crate::{
},
eval_const::create_nu_constant,
shell_error::io::IoError,
BlockId, Category, Config, DeclId, FileId, GetSpan, Handlers, HistoryConfig, Module, ModuleId,
OverlayId, ShellError, SignalAction, Signals, Signature, Span, SpanId, Type, Value, VarId,
VirtualPathId,
BlockId, Category, Config, DeclId, FileId, GetSpan, Handlers, HistoryConfig, JobId, Module,
ModuleId, OverlayId, ShellError, SignalAction, Signals, Signature, Span, SpanId, Type, Value,
VarId, VirtualPathId,
};
use fancy_regex::Regex;
use lru::LruCache;
@ -22,6 +22,8 @@ use std::{
path::PathBuf,
sync::{
atomic::{AtomicBool, AtomicU32, Ordering},
mpsc::channel,
mpsc::Sender,
Arc, Mutex, MutexGuard, PoisonError,
},
};
@ -31,7 +33,7 @@ type PoisonDebuggerError<'a> = PoisonError<MutexGuard<'a, Box<dyn Debugger>>>;
#[cfg(feature = "plugin")]
use crate::{PluginRegistryFile, PluginRegistryItem, RegisteredPlugin};
use super::{Jobs, ThreadJob};
use super::{CurrentJob, Jobs, Mail, Mailbox, ThreadJob};
#[derive(Clone, Debug)]
pub enum VirtualPath {
@ -117,7 +119,9 @@ pub struct EngineState {
pub jobs: Arc<Mutex<Jobs>>,
// The job being executed with this engine state, or None if main thread
pub current_thread_job: Option<ThreadJob>,
pub current_job: CurrentJob,
pub root_job_sender: Sender<Mail>,
// When there are background jobs running, the interactive behavior of `exit` changes depending on
// the value of this flag:
@ -141,6 +145,8 @@ pub const UNKNOWN_SPAN_ID: SpanId = SpanId::new(0);
impl EngineState {
pub fn new() -> Self {
let (send, recv) = channel::<Mail>();
Self {
files: vec![],
virtual_paths: vec![],
@ -196,7 +202,12 @@ impl EngineState {
is_debugging: IsDebugging::new(false),
debugger: Arc::new(Mutex::new(Box::new(NoopDebugger))),
jobs: Arc::new(Mutex::new(Jobs::default())),
current_thread_job: None,
current_job: CurrentJob {
id: JobId::new(0),
background_thread_job: None,
mailbox: Arc::new(Mutex::new(Mailbox::new(recv))),
},
root_job_sender: send,
exit_warning_given: Arc::new(AtomicBool::new(false)),
}
}
@ -1081,7 +1092,12 @@ impl EngineState {
// Determines whether the current state is being held by a background job
pub fn is_background_job(&self) -> bool {
self.current_thread_job.is_some()
self.current_job.background_thread_job.is_some()
}
// Gets the thread job entry
pub fn current_thread_job(&self) -> Option<&ThreadJob> {
self.current_job.background_thread_job.as_ref()
}
}

View File

@ -1,11 +1,17 @@
use std::{
collections::{HashMap, HashSet},
sync::{Arc, Mutex},
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
sync::{
mpsc::{Receiver, RecvTimeoutError, Sender, TryRecvError},
Arc, Mutex,
},
};
#[cfg(not(target_family = "wasm"))]
use std::time::{Duration, Instant};
use nu_system::{kill_by_pid, UnfreezeHandle};
use crate::Signals;
use crate::{PipelineData, Signals};
use crate::JobId;
@ -139,13 +145,15 @@ pub struct ThreadJob {
signals: Signals,
pids: Arc<Mutex<HashSet<u32>>>,
tag: Option<String>,
pub sender: Sender<Mail>,
}
impl ThreadJob {
pub fn new(signals: Signals, tag: Option<String>) -> Self {
pub fn new(signals: Signals, tag: Option<String>, sender: Sender<Mail>) -> Self {
ThreadJob {
signals,
pids: Arc::new(Mutex::new(HashSet::default())),
sender,
tag,
}
}
@ -238,3 +246,160 @@ impl FrozenJob {
}
}
}
/// Stores the information about the background job currently being executed by this thread, if any
#[derive(Clone)]
pub struct CurrentJob {
pub id: JobId,
// The background thread job associated with this thread.
// If None, it indicates this thread is currently the main job
pub background_thread_job: Option<ThreadJob>,
// note: although the mailbox is Mutex'd, it is only ever accessed
// by the current job's threads
pub mailbox: Arc<Mutex<Mailbox>>,
}
// The storage for unread messages
//
// Messages are initially sent over a mpsc channel,
// and may then be stored in a IgnoredMail struct when
// filtered out by a tag.
pub struct Mailbox {
receiver: Receiver<Mail>,
ignored_mail: IgnoredMail,
}
impl Mailbox {
pub fn new(receiver: Receiver<Mail>) -> Self {
Mailbox {
receiver,
ignored_mail: IgnoredMail::default(),
}
}
#[cfg(not(target_family = "wasm"))]
pub fn recv_timeout(
&mut self,
filter_tag: Option<FilterTag>,
timeout: Duration,
) -> Result<PipelineData, RecvTimeoutError> {
if let Some(value) = self.ignored_mail.pop(filter_tag) {
Ok(value)
} else {
let mut waited_so_far = Duration::ZERO;
let mut before = Instant::now();
while waited_so_far < timeout {
let (tag, value) = self.receiver.recv_timeout(timeout - waited_so_far)?;
if filter_tag.is_none() || filter_tag == tag {
return Ok(value);
} else {
self.ignored_mail.add((tag, value));
let now = Instant::now();
waited_so_far += now - before;
before = now;
}
}
Err(RecvTimeoutError::Timeout)
}
}
#[cfg(not(target_family = "wasm"))]
pub fn try_recv(
&mut self,
filter_tag: Option<FilterTag>,
) -> Result<PipelineData, TryRecvError> {
if let Some(value) = self.ignored_mail.pop(filter_tag) {
Ok(value)
} else {
loop {
let (tag, value) = self.receiver.try_recv()?;
if filter_tag.is_none() || filter_tag == tag {
return Ok(value);
} else {
self.ignored_mail.add((tag, value));
}
}
}
}
pub fn clear(&mut self) {
self.ignored_mail.clear();
while self.receiver.try_recv().is_ok() {}
}
}
// A data structure used to store messages which were received, but currently ignored by a tag filter
// messages are added and popped in a first-in-first-out matter.
#[derive(Default)]
struct IgnoredMail {
next_id: usize,
messages: BTreeMap<usize, Mail>,
by_tag: HashMap<FilterTag, BTreeSet<usize>>,
}
pub type FilterTag = u64;
pub type Mail = (Option<FilterTag>, PipelineData);
impl IgnoredMail {
pub fn add(&mut self, (tag, value): Mail) {
let id = self.next_id;
self.next_id += 1;
self.messages.insert(id, (tag, value));
if let Some(tag) = tag {
self.by_tag.entry(tag).or_default().insert(id);
}
}
pub fn pop(&mut self, tag: Option<FilterTag>) -> Option<PipelineData> {
if let Some(tag) = tag {
self.pop_oldest_with_tag(tag)
} else {
self.pop_oldest()
}
}
pub fn clear(&mut self) {
self.messages.clear();
self.by_tag.clear();
}
fn pop_oldest(&mut self) -> Option<PipelineData> {
let (id, (tag, value)) = self.messages.pop_first()?;
if let Some(tag) = tag {
let needs_cleanup = if let Some(ids) = self.by_tag.get_mut(&tag) {
ids.remove(&id);
ids.is_empty()
} else {
false
};
if needs_cleanup {
self.by_tag.remove(&tag);
}
}
Some(value)
}
fn pop_oldest_with_tag(&mut self, tag: FilterTag) -> Option<PipelineData> {
let ids = self.by_tag.get_mut(&tag)?;
let id = ids.pop_first()?;
if ids.is_empty() {
self.by_tag.remove(&tag);
}
Some(self.messages.remove(&id)?.1)
}
}

View File

@ -1370,7 +1370,7 @@ On Windows, this would be %USERPROFILE%\AppData\Roaming"#
#[error("Job {id} is not frozen")]
#[diagnostic(
code(nu::shell::os_disabled),
code(nu::shell::job_not_frozen),
help("You tried to unfreeze a job which is not frozen")
)]
JobNotFrozen {
@ -1379,6 +1379,27 @@ On Windows, this would be %USERPROFILE%\AppData\Roaming"#
span: Span,
},
#[error("The job {id} is frozen")]
#[diagnostic(
code(nu::shell::job_is_frozen),
help("This operation cannot be performed because the job is frozen")
)]
JobIsFrozen {
id: usize,
#[label = "This job is frozen"]
span: Span,
},
#[error("No message was received in the requested time interval")]
#[diagnostic(
code(nu::shell::recv_timeout),
help("No message arrived within the specified time limit")
)]
RecvTimeout {
#[label = "timeout"]
span: Span,
},
#[error(transparent)]
#[diagnostic(transparent)]
ChainedError(ChainedError),

View File

@ -194,7 +194,7 @@ impl PostWaitCallback {
child_pid: Option<u32>,
tag: Option<String>,
) -> Self {
let this_job = engine_state.current_thread_job.clone();
let this_job = engine_state.current_thread_job().cloned();
let jobs = engine_state.jobs.clone();
let is_interactive = engine_state.is_interactive;