From 71600fd2b64c40b36faf08835d8e963f9a41bff6 Mon Sep 17 00:00:00 2001 From: cosineblast <55855728+cosineblast@users.noreply.github.com> Date: Thu, 10 Apr 2025 22:13:54 -0300 Subject: [PATCH 1/2] Begin job tagging implementation --- .../nu-command/src/experimental/job_list.rs | 10 ++- .../nu-command/src/experimental/job_spawn.rs | 10 ++- crates/nu-command/src/experimental/job_tag.rs | 64 +++++++++++++++++++ .../src/experimental/job_unfreeze.rs | 15 ++++- crates/nu-command/src/experimental/mod.rs | 1 + crates/nu-command/src/system/run_external.rs | 7 +- crates/nu-protocol/src/engine/jobs.rs | 16 ++++- 7 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 crates/nu-command/src/experimental/job_tag.rs 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..c98654ccd2 --- /dev/null +++ b/crates/nu-command/src/experimental/job_tag.rs @@ -0,0 +1,64 @@ +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 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.") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .allow_variants_without_examples(true) + } + + 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 mut jobs = engine_state.jobs.lock().expect("jobs lock is poisoned!"); + + if jobs.lookup(id).is_none() { + return Err(ShellError::JobNotFound { + id: id.get(), + span: head, + }); + } + + Ok(Value::nothing(head).into_pipeline_data()) + } + + fn examples(&self) -> Vec { + vec![Example { + example: "let id = job spawn { sleep 10sec }; job tag $id", + description: "tag a newly spawned 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..94125fc5bb 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; diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index 03e2d3de02..c3d0d9ea23 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -332,7 +332,12 @@ impl Command for External { 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 })); + // TODO: use name of process as a tag + let job_id = jobs.add_job(Job::Frozen(FrozenJob { + unfreeze, + tag: None, + })); + if is_interactive { println!("\nJob {} is frozen", job_id.get()); } diff --git a/crates/nu-protocol/src/engine/jobs.rs b/crates/nu-protocol/src/engine/jobs.rs index 7d0b4c4b56..cdde13a16a 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,18 @@ 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 struct FrozenJob { pub unfreeze: UnfreezeHandle, + pub tag: Option, } impl FrozenJob { From 27baea4bbeb2bae8f0bea6505dc03a988ca56ee7 Mon Sep 17 00:00:00 2001 From: Renan Ribeiro <55855728+cosineblast@users.noreply.github.com> Date: Sun, 13 Apr 2025 00:04:54 -0300 Subject: [PATCH 2/2] Implement job tag command --- crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/experimental/job_tag.rs | 25 +++++++--- crates/nu-command/src/experimental/mod.rs | 2 +- crates/nu-command/tests/commands/job.rs | 50 +++++++++++++++++++ crates/nu-protocol/src/engine/jobs.rs | 7 +++ 5 files changed, 77 insertions(+), 8 deletions(-) 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/experimental/job_tag.rs b/crates/nu-command/src/experimental/job_tag.rs index c98654ccd2..40a843b51b 100644 --- a/crates/nu-command/src/experimental/job_tag.rs +++ b/crates/nu-command/src/experimental/job_tag.rs @@ -10,13 +10,18 @@ impl Command for JobTag { } fn description(&self) -> &str { - "Add a tag to a background job." + "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)]) .allow_variants_without_examples(true) } @@ -42,13 +47,19 @@ impl Command for JobTag { 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!"); - if jobs.lookup(id).is_none() { - return Err(ShellError::JobNotFound { - id: id.get(), - span: head, - }); + 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()) @@ -56,7 +67,7 @@ impl Command for JobTag { fn examples(&self) -> Vec { vec![Example { - example: "let id = job spawn { sleep 10sec }; job tag $id", + example: "let id = job spawn { sleep 10sec }; job tag $id abc ", description: "tag a newly spawned job", result: None, }] diff --git a/crates/nu-command/src/experimental/mod.rs b/crates/nu-command/src/experimental/mod.rs index 94125fc5bb..f98d123218 100644 --- a/crates/nu-command/src/experimental/mod.rs +++ b/crates/nu-command/src/experimental/mod.rs @@ -12,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/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 cdde13a16a..8e64e46f7f 100644 --- a/crates/nu-protocol/src/engine/jobs.rs +++ b/crates/nu-protocol/src/engine/jobs.rs @@ -210,6 +210,13 @@ impl Job { 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 {