This commit is contained in:
Renan Ribeiro 2025-04-13 03:05:12 +00:00 committed by GitHub
commit a1cec17723
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 184 additions and 10 deletions

View File

@ -452,6 +452,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
JobSpawn,
JobList,
JobKill,
JobTag,
Job,
};

View File

@ -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::<Vec<Value>>();

View File

@ -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<String> = 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))
};

View File

@ -0,0 +1,75 @@
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)])
.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<PipelineData, ShellError> {
let head = call.head;
let id_arg: Spanned<i64> = 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<String> = 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<Example> {
vec![Example {
example: "let id = job spawn { sleep 10sec }; job tag $id abc ",
description: "tag a newly spawned job",
result: None,
}]
}
}

View File

@ -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,7 +144,13 @@ 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 }))
jobs.add_job_with_id(
old_id,
Job::Frozen(FrozenJob {
unfreeze: handle,
tag,
}),
)
.expect("job was supposed to be removed");
if state.is_interactive {

View File

@ -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;

View File

@ -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());
}

View File

@ -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, "");
}

View File

@ -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<Job> {
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<Mutex<HashSet<u32>>>,
tag: Option<String>,
}
impl ThreadJob {
pub fn new(signals: Signals) -> Self {
pub fn new(signals: Signals, tag: Option<String>) -> 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<String>) {
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<String>,
}
impl FrozenJob {