Skip to main content

nu_protocol/engine/
jobs.rs

1use std::{
2    collections::{BTreeMap, BTreeSet, HashMap, HashSet, hash_map::Entry},
3    sync::{
4        Arc, Mutex,
5        mpsc::{Receiver, RecvTimeoutError, Sender, TryRecvError},
6    },
7};
8
9#[cfg(not(target_family = "wasm"))]
10use nu_utils::time::Instant;
11#[cfg(not(target_family = "wasm"))]
12use std::time::Duration;
13
14use nu_system::{UnfreezeHandle, kill_by_pid};
15
16use crate::{PipelineData, Signals, shell_error};
17
18use crate::JobId;
19
20#[derive(Debug)]
21pub struct Jobs {
22    next_job_id: usize,
23
24    // this is the ID of the most recently added frozen job in the jobs table.
25    // the methods of this struct must ensure the invariant of this always
26    // being None or pointing to a valid job in the table
27    last_frozen_job_id: Option<JobId>,
28    jobs: HashMap<JobId, Job>,
29}
30
31impl Default for Jobs {
32    fn default() -> Self {
33        Self {
34            next_job_id: 1,
35            last_frozen_job_id: None,
36            jobs: HashMap::default(),
37        }
38    }
39}
40
41impl Jobs {
42    pub fn iter(&self) -> impl Iterator<Item = (JobId, &Job)> {
43        self.jobs.iter().map(|(k, v)| (*k, v))
44    }
45
46    /// Whether there are no tracked jobs (running or frozen).
47    pub fn is_empty(&self) -> bool {
48        self.jobs.is_empty()
49    }
50
51    pub fn lookup(&self, id: JobId) -> Option<&Job> {
52        self.jobs.get(&id)
53    }
54
55    pub fn lookup_mut(&mut self, id: JobId) -> Option<&mut Job> {
56        self.jobs.get_mut(&id)
57    }
58
59    pub fn remove_job(&mut self, id: JobId) -> Option<Job> {
60        if self.last_frozen_job_id.is_some_and(|last| id == last) {
61            self.last_frozen_job_id = None;
62        }
63
64        self.jobs.remove(&id)
65    }
66
67    fn assign_last_frozen_id_if_frozen(&mut self, id: JobId, job: &Job) {
68        if let Job::Frozen(_) = job {
69            self.last_frozen_job_id = Some(id);
70        }
71    }
72
73    pub fn add_job(&mut self, job: Job) -> JobId {
74        let this_id = JobId::new(self.next_job_id);
75
76        self.assign_last_frozen_id_if_frozen(this_id, &job);
77
78        self.jobs.insert(this_id, job);
79        self.next_job_id += 1;
80
81        this_id
82    }
83
84    pub fn most_recent_frozen_job_id(&mut self) -> Option<JobId> {
85        self.last_frozen_job_id
86    }
87
88    // this is useful when you want to remove a job from the list and add it back later
89    pub fn add_job_with_id(&mut self, id: JobId, job: Job) -> Result<(), &'static str> {
90        self.assign_last_frozen_id_if_frozen(id, &job);
91
92        if let Entry::Vacant(e) = self.jobs.entry(id) {
93            e.insert(job);
94            Ok(())
95        } else {
96            Err("job already exists")
97        }
98    }
99
100    /// This function tries to forcefully kill a job from this job table,
101    /// removes it from the job table. It always succeeds in removing the job
102    /// from the table, but may fail in killing the job's active processes.
103    pub fn kill_and_remove(&mut self, id: JobId) -> shell_error::io::Result<()> {
104        if let Some(job) = self.jobs.get(&id) {
105            let err = job.kill();
106
107            self.remove_job(id);
108
109            err?
110        }
111
112        Ok(())
113    }
114
115    /// This function tries to forcefully kill all the background jobs and
116    /// removes all of them from the job table.
117    ///
118    /// It returns an error if any of the job killing attempts fails, but always
119    /// succeeds in removing the jobs from the table.
120    pub fn kill_all(&mut self) -> shell_error::io::Result<()> {
121        self.last_frozen_job_id = None;
122
123        let first_err = self
124            .iter()
125            .map(|(_, job)| job.kill().err())
126            .fold(None, |acc, x| acc.or(x));
127
128        self.jobs.clear();
129
130        if let Some(err) = first_err {
131            Err(err)
132        } else {
133            Ok(())
134        }
135    }
136}
137
138#[derive(Debug)]
139pub enum Job {
140    Thread(ThreadJob),
141    Frozen(FrozenJob),
142}
143
144// A thread job represents a job that is currently executing as a background thread in nushell.
145// This is an Arc-y type, cloning it does not uniquely clone the information of this particular
146// job.
147
148// Although rust's documentation does not document the acquire-release semantics of Mutex, this
149// is a direct undocumentented requirement of its soundness, and is thus assumed by this
150// implementaation.
151// see issue https://github.com/rust-lang/rust/issues/126239.
152#[derive(Clone, Debug)]
153pub struct ThreadJob {
154    signals: Signals,
155    pids: Arc<Mutex<HashSet<u32>>>,
156    description: Option<String>,
157    pub sender: Sender<Mail>,
158}
159
160impl ThreadJob {
161    pub fn new(signals: Signals, description: Option<String>, sender: Sender<Mail>) -> Self {
162        ThreadJob {
163            signals,
164            pids: Arc::new(Mutex::new(HashSet::default())),
165            sender,
166            description,
167        }
168    }
169
170    /// Tries to add the provided pid to the active pid set of the current job.
171    ///
172    /// Returns true if the pid was added successfully, or false if the
173    /// current job is interrupted.
174    pub fn try_add_pid(&self, pid: u32) -> bool {
175        let mut pids = self.pids.lock().expect("PIDs lock was poisoned");
176
177        // note: this signals check must occur after the pids lock has been locked.
178        if self.signals.interrupted() {
179            false
180        } else {
181            pids.insert(pid);
182            true
183        }
184    }
185
186    pub fn collect_pids(&self) -> Vec<u32> {
187        let lock = self.pids.lock().expect("PID lock was poisoned");
188
189        lock.iter().copied().collect()
190    }
191
192    pub fn kill(&self) -> shell_error::io::Result<()> {
193        // it's okay to make this interrupt outside of the mutex, since it has acquire-release
194        // semantics.
195
196        self.signals.trigger();
197
198        let mut pids = self.pids.lock().expect("PIDs lock was poisoned");
199
200        for pid in pids.iter() {
201            kill_by_pid((*pid).into())?;
202        }
203
204        pids.clear();
205
206        Ok(())
207    }
208
209    pub fn remove_pid(&self, pid: u32) {
210        let mut pids = self.pids.lock().expect("PID lock was poisoned");
211
212        pids.remove(&pid);
213    }
214}
215
216impl Job {
217    pub fn kill(&self) -> shell_error::io::Result<()> {
218        match self {
219            Job::Thread(thread_job) => thread_job.kill(),
220            Job::Frozen(frozen_job) => frozen_job.kill(),
221        }
222    }
223
224    pub fn description(&self) -> Option<&String> {
225        match self {
226            Job::Thread(thread_job) => thread_job.description.as_ref(),
227            Job::Frozen(frozen_job) => frozen_job.description.as_ref(),
228        }
229    }
230
231    pub fn assign_description(&mut self, description: Option<String>) {
232        match self {
233            Job::Thread(thread_job) => thread_job.description = description,
234            Job::Frozen(frozen_job) => frozen_job.description = description,
235        }
236    }
237}
238
239#[derive(Debug)]
240pub struct FrozenJob {
241    pub unfreeze: UnfreezeHandle,
242    pub description: Option<String>,
243}
244
245impl FrozenJob {
246    pub fn kill(&self) -> shell_error::io::Result<()> {
247        #[cfg(unix)]
248        {
249            Ok(kill_by_pid(self.unfreeze.pid() as i64)?)
250        }
251
252        // it doesn't happen outside unix.
253        #[cfg(not(unix))]
254        {
255            Ok(())
256        }
257    }
258}
259
260/// Stores the information about the background job currently being executed by this thread, if any
261#[derive(Clone, Debug)]
262pub struct CurrentJob {
263    pub id: JobId,
264
265    // The background thread job associated with this thread.
266    // If None, it indicates this thread is currently the main job
267    pub background_thread_job: Option<ThreadJob>,
268
269    // note: although the mailbox is Mutex'd, it is only ever accessed
270    // by the current job's threads
271    pub mailbox: Arc<Mutex<Mailbox>>,
272}
273
274// The storage for unread messages
275//
276// Messages are initially sent over a mpsc channel,
277// and may then be stored in a IgnoredMail struct when
278// filtered out by a message tag.
279#[derive(Debug)]
280pub struct Mailbox {
281    receiver: Receiver<Mail>,
282    ignored_mail: IgnoredMail,
283}
284
285impl Mailbox {
286    pub fn new(receiver: Receiver<Mail>) -> Self {
287        Mailbox {
288            receiver,
289            ignored_mail: IgnoredMail::default(),
290        }
291    }
292
293    #[cfg(not(target_family = "wasm"))]
294    pub fn recv_timeout(
295        &mut self,
296        filter_tag: Option<FilterTag>,
297        timeout: Duration,
298    ) -> Result<PipelineData, RecvTimeoutError> {
299        if let Some(value) = self.ignored_mail.pop(filter_tag) {
300            Ok(value)
301        } else {
302            let mut waited_so_far = Duration::ZERO;
303            let mut before = Instant::now();
304
305            while waited_so_far < timeout {
306                let (tag, value) = self
307                    .receiver
308                    .recv_timeout(timeout.checked_sub(waited_so_far).unwrap_or(Duration::ZERO))?;
309
310                if filter_tag.is_none() || filter_tag == tag {
311                    return Ok(value);
312                } else {
313                    self.ignored_mail.add((tag, value));
314                    let now = Instant::now();
315                    waited_so_far += now - before;
316                    before = now;
317                }
318            }
319
320            Err(RecvTimeoutError::Timeout)
321        }
322    }
323
324    #[cfg(not(target_family = "wasm"))]
325    pub fn try_recv(
326        &mut self,
327        filter_tag: Option<FilterTag>,
328    ) -> Result<PipelineData, TryRecvError> {
329        if let Some(value) = self.ignored_mail.pop(filter_tag) {
330            Ok(value)
331        } else {
332            loop {
333                let (tag, value) = self.receiver.try_recv()?;
334
335                if filter_tag.is_none() || filter_tag == tag {
336                    return Ok(value);
337                } else {
338                    self.ignored_mail.add((tag, value));
339                }
340            }
341        }
342    }
343
344    pub fn clear(&mut self) {
345        self.ignored_mail.clear();
346
347        while self.receiver.try_recv().is_ok() {}
348    }
349}
350
351// A data structure used to store messages which were received, but currently ignored by a tag filter
352// messages are added and popped in a first-in-first-out matter.
353#[derive(Default, Debug)]
354struct IgnoredMail {
355    next_id: usize,
356    messages: BTreeMap<usize, Mail>,
357    by_tag: HashMap<FilterTag, BTreeSet<usize>>,
358}
359
360pub type FilterTag = u64;
361pub type Mail = (Option<FilterTag>, PipelineData);
362
363impl IgnoredMail {
364    pub fn add(&mut self, (tag, value): Mail) {
365        let id = self.next_id;
366        self.next_id += 1;
367
368        self.messages.insert(id, (tag, value));
369
370        if let Some(tag) = tag {
371            self.by_tag.entry(tag).or_default().insert(id);
372        }
373    }
374
375    pub fn pop(&mut self, tag: Option<FilterTag>) -> Option<PipelineData> {
376        if let Some(tag) = tag {
377            self.pop_oldest_with_tag(tag)
378        } else {
379            self.pop_oldest()
380        }
381    }
382
383    pub fn clear(&mut self) {
384        self.messages.clear();
385        self.by_tag.clear();
386    }
387
388    fn pop_oldest(&mut self) -> Option<PipelineData> {
389        let (id, (tag, value)) = self.messages.pop_first()?;
390
391        if let Some(tag) = tag
392            && let Entry::Occupied(mut occupied_entry) = self.by_tag.entry(tag)
393        {
394            occupied_entry.get_mut().remove(&id);
395            if occupied_entry.get().is_empty() {
396                occupied_entry.remove();
397            }
398        }
399
400        Some(value)
401    }
402
403    fn pop_oldest_with_tag(&mut self, tag: FilterTag) -> Option<PipelineData> {
404        let ids = self.by_tag.get_mut(&tag)?;
405
406        let id = ids.pop_first()?;
407
408        if ids.is_empty() {
409            self.by_tag.remove(&tag);
410        }
411
412        Some(self.messages.remove(&id)?.1)
413    }
414}