diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index d4cd7e22f5..d803225c36 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -452,6 +452,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { JobSpawn, JobList, JobKill, + JobTag, Job, }; diff --git a/crates/nu-command/src/env/config/config_.rs b/crates/nu-command/src/env/config/config_.rs index 2d53245591..11fe86ee72 100644 --- a/crates/nu-command/src/env/config/config_.rs +++ b/crates/nu-command/src/env/config/config_.rs @@ -122,7 +122,7 @@ pub(super) fn start_editor( ) })?; - let post_wait_callback = PostWaitCallback::for_job_control(engine_state, None); + let post_wait_callback = PostWaitCallback::for_job_control(engine_state, None, None); // Wrap the output into a `PipelineData::ByteStream`. let child = nu_protocol::process::ChildProcess::new( diff --git a/crates/nu-command/src/experimental/job_list.rs b/crates/nu-command/src/experimental/job_list.rs index 28c0979aca..440e0aca64 100644 --- a/crates/nu-command/src/experimental/job_list.rs +++ b/crates/nu-command/src/experimental/job_list.rs @@ -37,7 +37,7 @@ impl Command for JobList { let values = jobs .iter() .map(|(id, job)| { - let record = record! { + let mut record = record! { "id" => Value::int(id.get() as i64, head), "type" => match job { Job::Thread(_) => Value::string("thread", head), @@ -52,12 +52,16 @@ impl Command for JobList { head, ), - Job::Frozen(FrozenJob { unfreeze }) => { + Job::Frozen(FrozenJob { unfreeze, .. }) => { Value::list(vec![ Value::int(unfreeze.pid() as i64, head) ], head) } - } + }, }; + if let Some(tag) = job.tag() { + record.push("tag", Value::string(tag, head)); + } + Value::record(record, head) }) .collect::>(); diff --git a/crates/nu-command/src/experimental/job_spawn.rs b/crates/nu-command/src/experimental/job_spawn.rs index 60f30024be..37203aafb9 100644 --- a/crates/nu-command/src/experimental/job_spawn.rs +++ b/crates/nu-command/src/experimental/job_spawn.rs @@ -28,6 +28,12 @@ impl Command for JobSpawn { Signature::build("job spawn") .category(Category::Experimental) .input_output_types(vec![(Type::Nothing, Type::Int)]) + .named( + "tag", + SyntaxShape::String, + "An optional description tag for this job", + Some('t'), + ) .required( "closure", SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), @@ -50,6 +56,8 @@ impl Command for JobSpawn { let closure: Closure = call.req(engine_state, stack, 0)?; + let tag: Option = call.get_flag(engine_state, stack, "tag")?; + let mut job_state = engine_state.clone(); job_state.is_interactive = false; @@ -68,7 +76,7 @@ impl Command for JobSpawn { let mut jobs = jobs.lock().expect("jobs lock is poisoned!"); let id = { - let thread_job = ThreadJob::new(job_signals); + let thread_job = ThreadJob::new(job_signals, tag); job_state.current_thread_job = Some(thread_job.clone()); jobs.add_job(Job::Thread(thread_job)) }; diff --git a/crates/nu-command/src/experimental/job_tag.rs b/crates/nu-command/src/experimental/job_tag.rs new file mode 100644 index 0000000000..651e687736 --- /dev/null +++ b/crates/nu-command/src/experimental/job_tag.rs @@ -0,0 +1,81 @@ +use nu_engine::command_prelude::*; +use nu_protocol::JobId; + +#[derive(Clone)] +pub struct JobTag; + +impl Command for JobTag { + fn name(&self) -> &str { + "job tag" + } + + fn description(&self) -> &str { + "Add a description tag to a background job." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("job tag") + .category(Category::Experimental) + .required("id", SyntaxShape::Int, "The id of the job to tag.") + .required( + "tag", + SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::Nothing]), + "The tag to assign to the job.", + ) + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["describe", "desc"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let head = call.head; + + let id_arg: Spanned = call.req(engine_state, stack, 0)?; + + if id_arg.item < 0 { + return Err(ShellError::NeedsPositiveValue { span: id_arg.span }); + } + + let id: JobId = JobId::new(id_arg.item as usize); + + let tag: Option = call.req(engine_state, stack, 1)?; + + let mut jobs = engine_state.jobs.lock().expect("jobs lock is poisoned!"); + + match jobs.lookup_mut(id) { + None => { + return Err(ShellError::JobNotFound { + id: id.get(), + span: head, + }); + } + + Some(job) => job.assign_tag(tag), + } + + Ok(Value::nothing(head).into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "let id = job spawn { sleep 10sec }; job tag $id abc ", + description: "Tag a newly spawned job", + result: None, + }, + Example { + example: "let id = job spawn { sleep 10sec }; job tag $id abc; job tag $id null", + description: "Remove the tag of a job", + result: None, + }, + ] + } +} diff --git a/crates/nu-command/src/experimental/job_unfreeze.rs b/crates/nu-command/src/experimental/job_unfreeze.rs index 46ee5b46f5..67fb3c96a1 100644 --- a/crates/nu-command/src/experimental/job_unfreeze.rs +++ b/crates/nu-command/src/experimental/job_unfreeze.rs @@ -112,7 +112,10 @@ fn unfreeze_job( span, }), - Job::Frozen(FrozenJob { unfreeze: handle }) => { + Job::Frozen(FrozenJob { + unfreeze: handle, + tag, + }) => { let pid = handle.pid(); if let Some(thread_job) = &state.current_thread_job { @@ -141,8 +144,14 @@ fn unfreeze_job( Ok(ForegroundWaitStatus::Frozen(handle)) => { let mut jobs = state.jobs.lock().expect("jobs lock is poisoned!"); - jobs.add_job_with_id(old_id, Job::Frozen(FrozenJob { unfreeze: handle })) - .expect("job was supposed to be removed"); + jobs.add_job_with_id( + old_id, + Job::Frozen(FrozenJob { + unfreeze: handle, + tag, + }), + ) + .expect("job was supposed to be removed"); if state.is_interactive { println!("\nJob {} is re-frozen", old_id.get()); diff --git a/crates/nu-command/src/experimental/mod.rs b/crates/nu-command/src/experimental/mod.rs index ff4f6b0399..f98d123218 100644 --- a/crates/nu-command/src/experimental/mod.rs +++ b/crates/nu-command/src/experimental/mod.rs @@ -3,6 +3,7 @@ mod job; mod job_kill; mod job_list; mod job_spawn; +mod job_tag; #[cfg(all(unix, feature = "os"))] mod job_unfreeze; @@ -11,8 +12,8 @@ pub use is_admin::IsAdmin; pub use job::Job; pub use job_kill::JobKill; pub use job_list::JobList; - pub use job_spawn::JobSpawn; +pub use job_tag::JobTag; #[cfg(all(unix, feature = "os"))] pub use job_unfreeze::JobUnfreeze; diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index 007bfb553b..841a2047b2 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -147,7 +147,7 @@ impl Command for External { }; // Create the command. - let mut command = std::process::Command::new(executable); + let mut command = std::process::Command::new(&executable); // Configure PWD. command.current_dir(cwd); @@ -322,6 +322,11 @@ impl Command for External { Some(PostWaitCallback::for_job_control( engine_state, Some(child_pid), + executable + .as_path() + .file_name() + .and_then(|it| it.to_str()) + .map(|it| it.to_string()), )), )?; diff --git a/crates/nu-command/tests/commands/job.rs b/crates/nu-command/tests/commands/job.rs index 9044564ef3..10b8346edd 100644 --- a/crates/nu-command/tests/commands/job.rs +++ b/crates/nu-command/tests/commands/job.rs @@ -215,3 +215,53 @@ fn job_extern_into_pipe_is_not_silent() { assert_eq!(actual.out, "11"); assert_eq!(actual.err, ""); } + +#[test] +fn job_list_returns_no_tag_when_job_is_untagged() { + let actual = nu!(r#" + job spawn { sleep 10sec } + job spawn { sleep 10sec } + job spawn { sleep 10sec } + + ('tag' in (job list | columns)) | to nuon"#); + + assert_eq!(actual.out, "false"); + assert_eq!(actual.err, ""); +} + +#[test] +fn job_list_returns_tag_when_job_is_spawned_with_tag() { + let actual = nu!(r#" + job spawn { sleep 10sec } --tag abc + job list | where id == 1 | get tag.0 + "#); + + assert_eq!(actual.out, "abc"); + assert_eq!(actual.err, ""); +} + +#[test] +fn job_tag_modifies_untagged_job_tag() { + let actual = nu!(r#" + job spawn { sleep 10sec } + + job tag 1 beep + + job list | where id == 1 | get tag.0"#); + + assert_eq!(actual.out, "beep"); + assert_eq!(actual.err, ""); +} + +#[test] +fn job_tag_modifies_tagged_job_tag() { + let actual = nu!(r#" + job spawn { sleep 10sec } --tag abc + + job tag 1 beep + + job list | where id == 1 | get tag.0"#); + + assert_eq!(actual.out, "beep"); + assert_eq!(actual.err, ""); +} diff --git a/crates/nu-protocol/src/engine/jobs.rs b/crates/nu-protocol/src/engine/jobs.rs index 7d0b4c4b56..8e64e46f7f 100644 --- a/crates/nu-protocol/src/engine/jobs.rs +++ b/crates/nu-protocol/src/engine/jobs.rs @@ -38,6 +38,10 @@ impl Jobs { self.jobs.get(&id) } + pub fn lookup_mut(&mut self, id: JobId) -> Option<&mut Job> { + self.jobs.get_mut(&id) + } + pub fn remove_job(&mut self, id: JobId) -> Option { if self.last_frozen_job_id.is_some_and(|last| id == last) { self.last_frozen_job_id = None; @@ -134,13 +138,15 @@ pub enum Job { pub struct ThreadJob { signals: Signals, pids: Arc>>, + tag: Option, } impl ThreadJob { - pub fn new(signals: Signals) -> Self { + pub fn new(signals: Signals, tag: Option) -> Self { ThreadJob { signals, pids: Arc::new(Mutex::new(HashSet::default())), + tag, } } @@ -197,10 +203,25 @@ impl Job { Job::Frozen(frozen_job) => frozen_job.kill(), } } + + pub fn tag(&self) -> Option<&String> { + match self { + Job::Thread(thread_job) => thread_job.tag.as_ref(), + Job::Frozen(frozen_job) => frozen_job.tag.as_ref(), + } + } + + pub fn assign_tag(&mut self, tag: Option) { + match self { + Job::Thread(thread_job) => thread_job.tag = tag, + Job::Frozen(frozen_job) => frozen_job.tag = tag, + } + } } pub struct FrozenJob { pub unfreeze: UnfreezeHandle, + pub tag: Option, } impl FrozenJob { diff --git a/crates/nu-protocol/src/process/child.rs b/crates/nu-protocol/src/process/child.rs index 7903c03e03..069cb71204 100644 --- a/crates/nu-protocol/src/process/child.rs +++ b/crates/nu-protocol/src/process/child.rs @@ -182,12 +182,18 @@ impl PostWaitCallback { PostWaitCallback(Box::new(f)) } - /// Creates a PostWaitCallback that checks creates a frozen job in the job table + /// Creates a PostWaitCallback that creates a frozen job in the job table /// if the incoming wait status indicates that the job was frozen. /// /// If `child_pid` is provided, the returned callback will also remove /// it from the pid list of the current running job. - pub fn for_job_control(engine_state: &EngineState, child_pid: Option) -> Self { + /// + /// The given `tag` argument will be used as the tag for the newly created job table entry. + pub fn for_job_control( + engine_state: &EngineState, + child_pid: Option, + tag: Option, + ) -> Self { let this_job = engine_state.current_thread_job.clone(); let jobs = engine_state.jobs.clone(); let is_interactive = engine_state.is_interactive; @@ -200,7 +206,7 @@ impl PostWaitCallback { if let ForegroundWaitStatus::Frozen(unfreeze) = status { let mut jobs = jobs.lock().expect("jobs lock is poisoned!"); - let job_id = jobs.add_job(Job::Frozen(FrozenJob { unfreeze })); + let job_id = jobs.add_job(Job::Frozen(FrozenJob { unfreeze, tag })); if is_interactive { println!("\nJob {} is frozen", job_id.get());