Begin job tagging implementation

This commit is contained in:
cosineblast 2025-04-10 22:13:54 -03:00
parent b0f9cda9b5
commit 71600fd2b6
7 changed files with 114 additions and 9 deletions

View File

@ -37,7 +37,7 @@ impl Command for JobList {
let values = jobs let values = jobs
.iter() .iter()
.map(|(id, job)| { .map(|(id, job)| {
let record = record! { let mut record = record! {
"id" => Value::int(id.get() as i64, head), "id" => Value::int(id.get() as i64, head),
"type" => match job { "type" => match job {
Job::Thread(_) => Value::string("thread", head), Job::Thread(_) => Value::string("thread", head),
@ -52,12 +52,16 @@ impl Command for JobList {
head, head,
), ),
Job::Frozen(FrozenJob { unfreeze }) => { Job::Frozen(FrozenJob { unfreeze, .. }) => {
Value::list(vec![ Value::int(unfreeze.pid() as i64, head) ], head) 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) Value::record(record, head)
}) })
.collect::<Vec<Value>>(); .collect::<Vec<Value>>();

View File

@ -28,6 +28,12 @@ impl Command for JobSpawn {
Signature::build("job spawn") Signature::build("job spawn")
.category(Category::Experimental) .category(Category::Experimental)
.input_output_types(vec![(Type::Nothing, Type::Int)]) .input_output_types(vec![(Type::Nothing, Type::Int)])
.named(
"tag",
SyntaxShape::String,
"An optional description tag for this job",
Some('t'),
)
.required( .required(
"closure", "closure",
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
@ -50,6 +56,8 @@ impl Command for JobSpawn {
let closure: Closure = call.req(engine_state, stack, 0)?; 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(); let mut job_state = engine_state.clone();
job_state.is_interactive = false; job_state.is_interactive = false;
@ -68,7 +76,7 @@ impl Command for JobSpawn {
let mut jobs = jobs.lock().expect("jobs lock is poisoned!"); let mut jobs = jobs.lock().expect("jobs lock is poisoned!");
let id = { 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()); job_state.current_thread_job = Some(thread_job.clone());
jobs.add_job(Job::Thread(thread_job)) jobs.add_job(Job::Thread(thread_job))
}; };

View File

@ -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<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 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<Example> {
vec![Example {
example: "let id = job spawn { sleep 10sec }; job tag $id",
description: "tag a newly spawned job",
result: None,
}]
}
}

View File

@ -112,7 +112,10 @@ fn unfreeze_job(
span, span,
}), }),
Job::Frozen(FrozenJob { unfreeze: handle }) => { Job::Frozen(FrozenJob {
unfreeze: handle,
tag,
}) => {
let pid = handle.pid(); let pid = handle.pid();
if let Some(thread_job) = &state.current_thread_job { if let Some(thread_job) = &state.current_thread_job {
@ -141,7 +144,13 @@ fn unfreeze_job(
Ok(ForegroundWaitStatus::Frozen(handle)) => { Ok(ForegroundWaitStatus::Frozen(handle)) => {
let mut jobs = state.jobs.lock().expect("jobs lock is poisoned!"); 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"); .expect("job was supposed to be removed");
if state.is_interactive { if state.is_interactive {

View File

@ -3,6 +3,7 @@ mod job;
mod job_kill; mod job_kill;
mod job_list; mod job_list;
mod job_spawn; mod job_spawn;
mod job_tag;
#[cfg(all(unix, feature = "os"))] #[cfg(all(unix, feature = "os"))]
mod job_unfreeze; mod job_unfreeze;

View File

@ -332,7 +332,12 @@ impl Command for External {
if let ForegroundWaitStatus::Frozen(unfreeze) = status { if let ForegroundWaitStatus::Frozen(unfreeze) = status {
let mut jobs = jobs.lock().expect("jobs lock is poisoned!"); 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 { if is_interactive {
println!("\nJob {} is frozen", job_id.get()); println!("\nJob {} is frozen", job_id.get());
} }

View File

@ -38,6 +38,10 @@ impl Jobs {
self.jobs.get(&id) 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> { pub fn remove_job(&mut self, id: JobId) -> Option<Job> {
if self.last_frozen_job_id.is_some_and(|last| id == last) { if self.last_frozen_job_id.is_some_and(|last| id == last) {
self.last_frozen_job_id = None; self.last_frozen_job_id = None;
@ -134,13 +138,15 @@ pub enum Job {
pub struct ThreadJob { pub struct ThreadJob {
signals: Signals, signals: Signals,
pids: Arc<Mutex<HashSet<u32>>>, pids: Arc<Mutex<HashSet<u32>>>,
tag: Option<String>,
} }
impl ThreadJob { impl ThreadJob {
pub fn new(signals: Signals) -> Self { pub fn new(signals: Signals, tag: Option<String>) -> Self {
ThreadJob { ThreadJob {
signals, signals,
pids: Arc::new(Mutex::new(HashSet::default())), pids: Arc::new(Mutex::new(HashSet::default())),
tag,
} }
} }
@ -197,10 +203,18 @@ impl Job {
Job::Frozen(frozen_job) => frozen_job.kill(), 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 struct FrozenJob {
pub unfreeze: UnfreezeHandle, pub unfreeze: UnfreezeHandle,
pub tag: Option<String>,
} }
impl FrozenJob { impl FrozenJob {