diff --git a/meli/src/command.rs b/meli/src/command.rs index e19ccec5..284d0a13 100644 --- a/meli/src/command.rs +++ b/meli/src/command.rs @@ -876,6 +876,17 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str } ) }, + { tags: ["manage-jobs"], + desc: "view and manage jobs", + tokens: &[One(Literal("manage-jobs"))], + parser:( + fn manage_jobs(input: &[u8]) -> IResult<&[u8], Action> { + let (input, _) = tag("manage-jobs")(input.trim())?; + let (input, _) = eof(input)?; + Ok((input, Tab(ManageJobs))) + } + ) + }, { tags: ["quit"], desc: "quit meli", tokens: &[One(Literal("quit"))], @@ -978,6 +989,10 @@ fn view(input: &[u8]) -> IResult<&[u8], Action> { ))(input) } +fn new_tab(input: &[u8]) -> IResult<&[u8], Action> { + alt((manage_mailboxes, manage_jobs, compose_action))(input) +} + pub fn parse_command(input: &[u8]) -> Result { alt(( goto, @@ -989,13 +1004,12 @@ pub fn parse_command(input: &[u8]) -> Result { setenv, printenv, view, - compose_action, create_mailbox, sub_mailbox, unsub_mailbox, delete_mailbox, rename_mailbox, - manage_mailboxes, + new_tab, account_action, print_setting, toggle_mouse, diff --git a/meli/src/command/actions.rs b/meli/src/command/actions.rs index 2c553615..24d88aa0 100644 --- a/meli/src/command/actions.rs +++ b/meli/src/command/actions.rs @@ -62,6 +62,7 @@ pub enum TabAction { Kill(ComponentId), New(Option>), ManageMailboxes, + ManageJobs, } #[derive(Debug)] diff --git a/meli/src/components.rs b/meli/src/components.rs index 2f5053be..9377ccd1 100644 --- a/meli/src/components.rs +++ b/meli/src/components.rs @@ -34,18 +34,21 @@ use crate::{ }; pub mod mail; -pub use crate::mail::*; +pub use mail::*; pub mod notifications; pub mod utilities; -pub use self::utilities::*; +pub use utilities::*; pub mod contacts; -pub use crate::contacts::*; +pub use contacts::*; pub mod mailbox_management; -pub use self::mailbox_management::*; +pub use mailbox_management::*; + +pub mod jobs_view; +pub use jobs_view::*; #[cfg(feature = "svgscreenshot")] pub mod svg; diff --git a/meli/src/components/jobs_view.rs b/meli/src/components/jobs_view.rs new file mode 100644 index 00000000..ec3bc7e9 --- /dev/null +++ b/meli/src/components/jobs_view.rs @@ -0,0 +1,446 @@ +/* + * meli + * + * Copyright 2019 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use std::{borrow::Cow, cmp}; + +use super::*; +use crate::{ + jobs::{JobId, JobMetadata}, + melib::utils::datetime::{self, formats::RFC3339_DATETIME_AND_SPACE}, +}; + +#[derive(Debug)] +pub struct JobManager { + cursor_pos: usize, + new_cursor_pos: usize, + length: usize, + data_columns: DataColumns<5>, + entries: IndexMap, + + initialized: bool, + theme_default: ThemeAttribute, + highlight_theme: ThemeAttribute, + + dirty: bool, + + movement: Option, + id: ComponentId, +} + +impl fmt::Display for JobManager { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "jobs") + } +} + +impl JobManager { + pub fn new(context: &Context) -> Self { + let theme_default = crate::conf::value(context, "theme_default"); + let mut data_columns = DataColumns::default(); + data_columns.theme_config.set_single_theme(theme_default); + Self { + cursor_pos: 0, + new_cursor_pos: 0, + entries: IndexMap::default(), + length: 0, + data_columns, + theme_default, + highlight_theme: crate::conf::value(context, "highlight"), + initialized: false, + dirty: true, + movement: None, + id: ComponentId::default(), + } + } + + fn initialize(&mut self, context: &mut Context) { + self.entries = (*context.main_loop_handler.job_executor.jobs.lock().unwrap()).clone(); + self.length = self.entries.len(); + self.entries.sort_by(|_, a, _, b| a.started.cmp(&b.started)); + + self.set_dirty(true); + let mut min_width = ( + "id".len(), + "desc".len(), + "started".len(), + "finished".len(), + "succeeded".len(), + 0, + ); + + for c in self.entries.values() { + /* title */ + min_width.0 = cmp::max(min_width.0, c.id.to_string().len()); + /* desc */ + min_width.1 = cmp::max(min_width.1, c.desc.len()); + } + min_width.2 = "1970-01-01 00:00:00".len(); + min_width.3 = min_width.2; + + /* name column */ + self.data_columns.columns[0] = + CellBuffer::new_with_context(min_width.0, self.length, None, context); + /* path column */ + self.data_columns.columns[1] = + CellBuffer::new_with_context(min_width.1, self.length, None, context); + /* size column */ + self.data_columns.columns[2] = + CellBuffer::new_with_context(min_width.2, self.length, None, context); + /* subscribed column */ + self.data_columns.columns[3] = + CellBuffer::new_with_context(min_width.3, self.length, None, context); + self.data_columns.columns[4] = + CellBuffer::new_with_context(min_width.4, self.length, None, context); + + for (idx, e) in self.entries.values().enumerate() { + write_string_to_grid( + &e.id.to_string(), + &mut self.data_columns.columns[0], + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + ((0, idx), (min_width.0, idx)), + None, + ); + + write_string_to_grid( + &e.desc, + &mut self.data_columns.columns[1], + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + ((0, idx), (min_width.1, idx)), + None, + ); + + write_string_to_grid( + &datetime::timestamp_to_string(e.started, Some(RFC3339_DATETIME_AND_SPACE), true), + &mut self.data_columns.columns[2], + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + ((0, idx), (min_width.2, idx)), + None, + ); + + write_string_to_grid( + &if let Some(t) = e.finished { + Cow::Owned(datetime::timestamp_to_string( + t, + Some(RFC3339_DATETIME_AND_SPACE), + true, + )) + } else { + Cow::Borrowed("null") + }, + &mut self.data_columns.columns[3], + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + ((0, idx), (min_width.3, idx)), + None, + ); + + write_string_to_grid( + &if e.finished.is_some() { + Cow::Owned(format!("{:?}", e.succeeded)) + } else { + Cow::Borrowed("-") + }, + &mut self.data_columns.columns[4], + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + ((0, idx), (min_width.4, idx)), + None, + ); + } + + if self.length == 0 { + let message = "No mailboxes.".to_string(); + self.data_columns.columns[0] = + CellBuffer::new_with_context(message.len(), self.length, None, context); + write_string_to_grid( + &message, + &mut self.data_columns.columns[0], + self.theme_default.fg, + self.theme_default.bg, + self.theme_default.attrs, + ((0, 0), (message.len() - 1, 0)), + None, + ); + } + } + + fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { + let (upper_left, bottom_right) = area; + + if self.length == 0 { + clear_area(grid, area, self.theme_default); + copy_area( + grid, + &self.data_columns.columns[0], + area, + ((0, 0), pos_dec(self.data_columns.columns[0].size(), (1, 1))), + ); + context.dirty_areas.push_back(area); + return; + } + let rows = get_y(bottom_right) - get_y(upper_left) + 1; + + if let Some(mvm) = self.movement.take() { + match mvm { + PageMovement::Up(amount) => { + self.new_cursor_pos = self.new_cursor_pos.saturating_sub(amount); + } + PageMovement::PageUp(multiplier) => { + self.new_cursor_pos = self.new_cursor_pos.saturating_sub(rows * multiplier); + } + PageMovement::Down(amount) => { + if self.new_cursor_pos + amount < self.length { + self.new_cursor_pos += amount; + } else { + self.new_cursor_pos = self.length - 1; + } + } + PageMovement::PageDown(multiplier) => { + #[allow(clippy::comparison_chain)] + if self.new_cursor_pos + rows * multiplier < self.length { + self.new_cursor_pos += rows * multiplier; + } else if self.new_cursor_pos + rows * multiplier > self.length { + self.new_cursor_pos = self.length - 1; + } else { + self.new_cursor_pos = (self.length / rows) * rows; + } + } + PageMovement::Right(_) | PageMovement::Left(_) => {} + PageMovement::Home => { + self.new_cursor_pos = 0; + } + PageMovement::End => { + self.new_cursor_pos = self.length - 1; + } + } + } + + let prev_page_no = (self.cursor_pos).wrapping_div(rows); + let page_no = (self.new_cursor_pos).wrapping_div(rows); + + let top_idx = page_no * rows; + + if self.length >= rows { + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::Update { + id: self.id, + context: ScrollContext { + shown_lines: top_idx + rows, + total_lines: self.length, + has_more_lines: false, + }, + }, + ))); + } else { + context + .replies + .push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate( + ScrollUpdate::End(self.id), + ))); + } + + /* If cursor position has changed, remove the highlight from the previous + * position and apply it in the new one. */ + if self.cursor_pos != self.new_cursor_pos && prev_page_no == page_no { + let old_cursor_pos = self.cursor_pos; + self.cursor_pos = self.new_cursor_pos; + for &(idx, highlight) in &[(old_cursor_pos, false), (self.new_cursor_pos, true)] { + if idx >= self.length { + continue; //bounds check + } + let new_area = nth_row_area(area, idx % rows); + self.data_columns + .draw(grid, idx, self.cursor_pos, grid.bounds_iter(new_area)); + let row_attr = if highlight { + self.highlight_theme + } else { + self.theme_default + }; + change_colors(grid, new_area, row_attr.fg, row_attr.bg); + context.dirty_areas.push_back(new_area); + } + return; + } else if self.cursor_pos != self.new_cursor_pos { + self.cursor_pos = self.new_cursor_pos; + } + if self.new_cursor_pos >= self.length { + self.new_cursor_pos = self.length - 1; + self.cursor_pos = self.new_cursor_pos; + } + /* Page_no has changed, so draw new page */ + _ = self + .data_columns + .recalc_widths((width!(area), height!(area)), top_idx); + clear_area(grid, area, self.theme_default); + /* copy table columns */ + self.data_columns + .draw(grid, top_idx, self.cursor_pos, grid.bounds_iter(area)); + + /* highlight cursor */ + change_colors( + grid, + nth_row_area(area, self.cursor_pos % rows), + self.highlight_theme.fg, + self.highlight_theme.bg, + ); + + /* clear gap if available height is more than count of entries */ + if top_idx + rows > self.length { + clear_area( + grid, + ( + pos_inc(upper_left, (0, self.length - top_idx)), + bottom_right, + ), + self.theme_default, + ); + } + context.dirty_areas.push_back(area); + } +} + +impl Component for JobManager { + fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { + if !self.is_dirty() { + return; + } + if !self.initialized { + self.initialize(context); + } + + self.draw_list(grid, area, context); + self.dirty = false; + } + + fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { + if let UIEvent::ConfigReload { old_settings: _ } = event { + self.theme_default = crate::conf::value(context, "theme_default"); + self.initialized = false; + self.set_dirty(true); + } + + let shortcuts = self.shortcuts(context); + match event { + UIEvent::StatusEvent( + StatusEvent::JobFinished(_) | StatusEvent::JobCanceled(_) | StatusEvent::NewJob(_), + ) => { + self.initialized = false; + self.set_dirty(true); + return false; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) => + { + let amount = 1; + self.movement = Some(PageMovement::Up(amount)); + self.set_dirty(true); + return true; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) + && self.cursor_pos < self.length.saturating_sub(1) => + { + let amount = 1; + self.set_dirty(true); + self.movement = Some(PageMovement::Down(amount)); + return true; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::GENERAL]["prev_page"]) => + { + let mult = 1; + self.set_dirty(true); + self.movement = Some(PageMovement::PageUp(mult)); + return true; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::GENERAL]["next_page"]) => + { + let mult = 1; + self.set_dirty(true); + self.movement = Some(PageMovement::PageDown(mult)); + return true; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) => + { + self.set_dirty(true); + self.movement = Some(PageMovement::Home); + return true; + } + UIEvent::Input(ref key) + if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) => + { + self.set_dirty(true); + self.movement = Some(PageMovement::End); + return true; + } + _ => {} + } + false + } + + fn is_dirty(&self) -> bool { + self.dirty + } + + fn set_dirty(&mut self, value: bool) { + self.dirty = value; + } + + fn kill(&mut self, uuid: ComponentId, context: &mut Context) { + debug_assert!(uuid == self.id); + context.replies.push_back(UIEvent::Action(Tab(Kill(uuid)))); + } + + fn shortcuts(&self, context: &Context) -> ShortcutMaps { + let mut map = ShortcutMaps::default(); + + map.insert( + Shortcuts::GENERAL, + context.settings.shortcuts.general.key_values(), + ); + + map + } + + fn id(&self) -> ComponentId { + self.id + } + + fn can_quit_cleanly(&mut self, _context: &Context) -> bool { + true + } + + fn status(&self, _context: &Context) -> String { + format!("{} entries", self.entries.len()) + } +} diff --git a/meli/src/components/mail/compose.rs b/meli/src/components/mail/compose.rs index b17221f6..cd5ec1e9 100644 --- a/meli/src/components/mail/compose.rs +++ b/meli/src/components/mail/compose.rs @@ -1233,7 +1233,10 @@ impl Component for Composer { Flag::SEEN, ) { Ok(job) => { - let handle = context.main_loop_handler.job_executor.spawn_blocking(job); + let handle = context + .main_loop_handler + .job_executor + .spawn_blocking("compose::submit".into(), job); context .replies .push_back(UIEvent::StatusEvent(StatusEvent::NewJob( diff --git a/meli/src/components/mail/compose/gpg.rs b/meli/src/components/mail/compose/gpg.rs index 6bb6f959..01e40bf0 100644 --- a/meli/src/components/mail/compose/gpg.rs +++ b/meli/src/components/mail/compose/gpg.rs @@ -66,7 +66,7 @@ impl KeySelection { let handle = context .main_loop_handler .job_executor - .spawn_specialized(job); + .spawn_specialized("gpg::keylist".into(), job); let mut progress_spinner = ProgressSpinner::new(8, context); progress_spinner.start(); Ok(KeySelection::LoadingKeys { diff --git a/meli/src/components/mail/listing.rs b/meli/src/components/mail/listing.rs index 3753b42e..8551f624 100644 --- a/meli/src/components/mail/listing.rs +++ b/meli/src/components/mail/listing.rs @@ -479,7 +479,7 @@ pub trait MailListingTrait: ListingTrait { let handle = account .main_loop_handler .job_executor - .spawn_specialized(fut); + .spawn_specialized("set_seen".into(), fut); account .insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle }); } @@ -501,7 +501,7 @@ pub trait MailListingTrait: ListingTrait { let handle = account .main_loop_handler .job_executor - .spawn_specialized(fut); + .spawn_specialized("set_unseen".into(), fut); account .insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle }); } @@ -523,7 +523,7 @@ pub trait MailListingTrait: ListingTrait { let handle = account .main_loop_handler .job_executor - .spawn_specialized(fut); + .spawn_specialized("remove_tag".into(), fut); account .insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle }); } @@ -545,7 +545,7 @@ pub trait MailListingTrait: ListingTrait { let handle = account .main_loop_handler .job_executor - .spawn_specialized(fut); + .spawn_specialized("add_tag".into(), fut); account .insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle }); } @@ -567,7 +567,7 @@ pub trait MailListingTrait: ListingTrait { let handle = account .main_loop_handler .job_executor - .spawn_specialized(fut); + .spawn_specialized("delete".into(), fut); account.insert_job( handle.job_id, JobRequest::DeleteMessages { env_hashes, handle }, @@ -595,7 +595,7 @@ pub trait MailListingTrait: ListingTrait { let handle = account .main_loop_handler .job_executor - .spawn_specialized(fut); + .spawn_specialized("copy_to_mailbox".into(), fut); account.insert_job( handle.job_id, JobRequest::Generic { @@ -635,7 +635,7 @@ pub trait MailListingTrait: ListingTrait { let handle = account .main_loop_handler .job_executor - .spawn_specialized(fut); + .spawn_specialized("move_to_mailbox".into(), fut); account.insert_job( handle.job_id, JobRequest::Generic { @@ -714,7 +714,10 @@ pub trait MailListingTrait: ListingTrait { let _ = sender.send(r); Ok(()) }); - let handle = account.main_loop_handler.job_executor.spawn_blocking(fut); + let handle = account + .main_loop_handler + .job_executor + .spawn_blocking("export_to_mbox".into(), fut); let path = path.to_path_buf(); account.insert_job( handle.job_id, @@ -1230,6 +1233,7 @@ impl Component for Listing { )); } } + return true; } #[cfg(feature = "debug-tracing")] UIEvent::IntraComm { @@ -2026,6 +2030,13 @@ impl Component for Listing { .push_back(UIEvent::Action(Tab(New(Some(Box::new(mgr)))))); return true; } + UIEvent::Action(Action::Tab(ManageJobs)) => { + let mgr = JobManager::new(context); + context + .replies + .push_back(UIEvent::Action(Tab(New(Some(Box::new(mgr)))))); + return true; + } UIEvent::Action(Action::Compose(ComposeAction::Mailto(ref mailto))) => { let account_hash = context.accounts[self.cursor_pos.0].hash(); let mut composer = Composer::with_account(account_hash, context); diff --git a/meli/src/components/mail/listing/compact.rs b/meli/src/components/mail/listing/compact.rs index 5c1bae98..76e3df99 100644 --- a/meli/src/components/mail/listing/compact.rs +++ b/meli/src/components/mail/listing/compact.rs @@ -1984,7 +1984,7 @@ impl Component for CompactListing { let handle = context.accounts[&self.cursor_pos.0] .main_loop_handler .job_executor - .spawn_specialized(job); + .spawn_specialized("search".into(), job); self.search_job = Some((filter_term.to_string(), handle)); } Err(err) => { @@ -2007,7 +2007,7 @@ impl Component for CompactListing { let mut handle = context.accounts[&self.cursor_pos.0] .main_loop_handler .job_executor - .spawn_specialized(job); + .spawn_specialized("select_by_search".into(), job); if let Ok(Some(search_result)) = try_recv_timeout!(&mut handle.chan) { self.select(search_term, search_result, context); } else { diff --git a/meli/src/components/mail/listing/conversations.rs b/meli/src/components/mail/listing/conversations.rs index 09909702..904663f9 100644 --- a/meli/src/components/mail/listing/conversations.rs +++ b/meli/src/components/mail/listing/conversations.rs @@ -1508,7 +1508,7 @@ impl Component for ConversationsListing { let handle = context.accounts[&self.cursor_pos.0] .main_loop_handler .job_executor - .spawn_specialized(job); + .spawn_specialized("search".into(), job); self.search_job = Some((filter_term.to_string(), handle)); } Err(err) => { diff --git a/meli/src/components/mail/listing/plain.rs b/meli/src/components/mail/listing/plain.rs index 4d808eaa..ac1270be 100644 --- a/meli/src/components/mail/listing/plain.rs +++ b/meli/src/components/mail/listing/plain.rs @@ -1581,7 +1581,7 @@ impl Component for PlainListing { let handle = context.accounts[&self.cursor_pos.0] .main_loop_handler .job_executor - .spawn_specialized(job); + .spawn_specialized("search".into(), job); self.search_job = Some((filter_term.to_string(), handle)); } Err(err) => { diff --git a/meli/src/components/mail/listing/thread.rs b/meli/src/components/mail/listing/thread.rs index ded2f342..cd3a4438 100644 --- a/meli/src/components/mail/listing/thread.rs +++ b/meli/src/components/mail/listing/thread.rs @@ -1508,7 +1508,7 @@ impl Component for ThreadListing { let handle = context.accounts[&self.cursor_pos.0] .main_loop_handler .job_executor - .spawn_specialized(job); + .spawn_specialized("search".into(), job); self.search_job = Some((filter_term.to_string(), handle)); } Err(err) => { diff --git a/meli/src/components/mail/view.rs b/meli/src/components/mail/view.rs index 48ab5439..fd08a65b 100644 --- a/meli/src/components/mail/view.rs +++ b/meli/src/components/mail/view.rs @@ -121,7 +121,7 @@ impl MailView { let mut handle = account .main_loop_handler .job_executor - .spawn_specialized(fut); + .spawn_specialized("fetch_envelopes".into(), fut); let job_id = handle.job_id; pending_action = if let MailViewState::Init { ref mut pending_action, @@ -302,7 +302,7 @@ impl Component for MailView { let handle = account .main_loop_handler .job_executor - .spawn_specialized(fut); + .spawn_specialized("set_flags".into(), fut); account.insert_job( handle.job_id, JobRequest::SetFlags { @@ -532,12 +532,12 @@ impl Component for MailView { context .main_loop_handler .job_executor - .spawn_specialized(bytes_job) + .spawn_specialized("fetch_envelope".into(), bytes_job) } else { context .main_loop_handler .job_executor - .spawn_blocking(bytes_job) + .spawn_blocking("fetch_envelope".into(), bytes_job) }; context.accounts[&account_hash].insert_job( handle.job_id, diff --git a/meli/src/components/mail/view/envelope.rs b/meli/src/components/mail/view/envelope.rs index ef2c89b9..f242dc86 100644 --- a/meli/src/components/mail/view/envelope.rs +++ b/meli/src/components/mail/view/envelope.rs @@ -255,8 +255,9 @@ impl EnvelopeView { { if view_settings.auto_verify_signatures { let verify_fut = crate::components::mail::pgp::verify(a.clone()); - let handle = - main_loop_handler.job_executor.spawn_specialized(verify_fut); + let handle = main_loop_handler + .job_executor + .spawn_specialized("gpg::verify_sig".into(), verify_fut); active_jobs.insert(handle.job_id); main_loop_handler.send(ThreadEvent::UIEvent(UIEvent::StatusEvent( StatusEvent::NewJob(handle.job_id), @@ -317,7 +318,7 @@ impl EnvelopeView { crate::components::mail::pgp::decrypt(a.raw().to_vec()); let handle = main_loop_handler .job_executor - .spawn_specialized(decrypt_fut); + .spawn_specialized("gpg::decrypt".into(), decrypt_fut); active_jobs.insert(handle.job_id); main_loop_handler.send(ThreadEvent::UIEvent( UIEvent::StatusEvent(StatusEvent::NewJob(handle.job_id)), diff --git a/meli/src/conf/accounts.rs b/meli/src/conf/accounts.rs index e3a53505..8afe0a09 100644 --- a/meli/src/conf/accounts.rs +++ b/meli/src/conf/accounts.rs @@ -515,11 +515,11 @@ impl Account { let handle = if backend.capabilities().is_async { main_loop_handler .job_executor - .spawn_specialized(online_job.then(|_| mailboxes_job)) + .spawn_specialized("mailboxes".into(), online_job.then(|_| mailboxes_job)) } else { main_loop_handler .job_executor - .spawn_blocking(online_job.then(|_| mailboxes_job)) + .spawn_blocking("mailboxes".into(), online_job.then(|_| mailboxes_job)) }; let job_id = handle.job_id; active_jobs.insert(job_id, JobRequest::Mailboxes { handle }); @@ -716,11 +716,11 @@ impl Account { let handle = if self.backend_capabilities.is_async { self.main_loop_handler .job_executor - .spawn_specialized(mailbox_job) + .spawn_specialized("fetch-mailbox".into(), mailbox_job) } else { self.main_loop_handler .job_executor - .spawn_blocking(mailbox_job) + .spawn_blocking("fetch-mailbox".into(), mailbox_job) }; let job_id = handle.job_id; self.main_loop_handler @@ -790,8 +790,10 @@ impl Account { ); } Ok(job) => { - let handle = - self.main_loop_handler.job_executor.spawn_blocking(job); + let handle = self + .main_loop_handler + .job_executor + .spawn_blocking("sqlite3::update".into(), job); self.insert_job( handle.job_id, JobRequest::Generic { @@ -837,8 +839,10 @@ impl Account { ) }) { Ok(job) => { - let handle = - self.main_loop_handler.job_executor.spawn_blocking(job); + let handle = self + .main_loop_handler + .job_executor + .spawn_blocking("sqlite3::remove".into(), job); self.insert_job( handle.job_id, JobRequest::Generic { @@ -890,8 +894,10 @@ impl Account { ); } Ok(job) => { - let handle = - self.main_loop_handler.job_executor.spawn_blocking(job); + let handle = self + .main_loop_handler + .job_executor + .spawn_blocking("sqlite3::rename".into(), job); self.insert_job( handle.job_id, JobRequest::Generic { @@ -932,6 +938,7 @@ impl Account { #[cfg(feature = "sqlite3")] if self.settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 { let handle = self.main_loop_handler.job_executor.spawn_blocking( + "sqlite3::insert".into(), crate::sqlite3::insert( (*envelope).clone(), self.backend.clone(), @@ -1096,11 +1103,11 @@ impl Account { let handle = if self.backend_capabilities.is_async { self.main_loop_handler .job_executor - .spawn_specialized(refresh_job) + .spawn_specialized("refresh".into(), refresh_job) } else { self.main_loop_handler .job_executor - .spawn_blocking(refresh_job) + .spawn_blocking("refresh".into(), refresh_job) }; self.insert_job( handle.job_id, @@ -1122,9 +1129,13 @@ impl Account { match self.backend.read().unwrap().watch() { Ok(fut) => { let handle = if self.backend_capabilities.is_async { - self.main_loop_handler.job_executor.spawn_specialized(fut) + self.main_loop_handler + .job_executor + .spawn_specialized("watch".into(), fut) } else { - self.main_loop_handler.job_executor.spawn_blocking(fut) + self.main_loop_handler + .job_executor + .spawn_blocking("watch".into(), fut) }; self.active_jobs .insert(handle.job_id, JobRequest::Watch { handle }); @@ -1202,11 +1213,11 @@ impl Account { let handle = if self.backend_capabilities.is_async { self.main_loop_handler .job_executor - .spawn_specialized(mailbox_job) + .spawn_specialized("mailbox_fetch".into(), mailbox_job) } else { self.main_loop_handler .job_executor - .spawn_blocking(mailbox_job) + .spawn_blocking("mailbox_fetch".into(), mailbox_job) }; self.insert_job( handle.job_id, @@ -1294,9 +1305,13 @@ impl Account { .save(bytes.to_vec(), mailbox_hash, flags)?; let handle = if self.backend_capabilities.is_async { - self.main_loop_handler.job_executor.spawn_specialized(job) + self.main_loop_handler + .job_executor + .spawn_specialized("save".into(), job) } else { - self.main_loop_handler.job_executor.spawn_blocking(job) + self.main_loop_handler + .job_executor + .spawn_blocking("save".into(), job) }; self.insert_job( handle.job_id, @@ -1362,14 +1377,14 @@ impl Account { } #[cfg(feature = "smtp")] SendMail::Smtp(conf) => { - let handle = self - .main_loop_handler - .job_executor - .spawn_specialized(async move { + let handle = self.main_loop_handler.job_executor.spawn_specialized( + "smtp".into(), + async move { let mut smtp_connection = melib::smtp::SmtpConnection::new_connection(conf).await?; smtp_connection.mail_transaction(&message, None).await - }); + }, + ); if complete_in_background { self.insert_job(handle.job_id, JobRequest::SendMessageBackground { handle }); return Ok(None); @@ -1387,9 +1402,13 @@ impl Account { .submit(message.into_bytes(), None, None)?; let handle = if self.backend_capabilities.is_async { - self.main_loop_handler.job_executor.spawn_specialized(job) + self.main_loop_handler + .job_executor + .spawn_specialized("server_submission".into(), job) } else { - self.main_loop_handler.job_executor.spawn_blocking(job) + self.main_loop_handler + .job_executor + .spawn_blocking("server_submission".into(), job) }; self.insert_job(handle.job_id, JobRequest::SendMessageBackground { handle }); return Ok(None); @@ -1508,9 +1527,13 @@ impl Account { .unwrap() .create_mailbox(path.to_string())?; let handle = if self.backend_capabilities.is_async { - self.main_loop_handler.job_executor.spawn_specialized(job) + self.main_loop_handler + .job_executor + .spawn_specialized("create_mailbox".into(), job) } else { - self.main_loop_handler.job_executor.spawn_blocking(job) + self.main_loop_handler + .job_executor + .spawn_blocking("create_mailbox".into(), job) }; self.insert_job(handle.job_id, JobRequest::CreateMailbox { path, handle }); Ok(()) @@ -1523,9 +1546,13 @@ impl Account { let mailbox_hash = self.mailbox_by_path(&path)?; let job = self.backend.write().unwrap().delete_mailbox(mailbox_hash)?; let handle = if self.backend_capabilities.is_async { - self.main_loop_handler.job_executor.spawn_specialized(job) + self.main_loop_handler + .job_executor + .spawn_specialized("delete_mailbox".into(), job) } else { - self.main_loop_handler.job_executor.spawn_blocking(job) + self.main_loop_handler + .job_executor + .spawn_blocking("delete_mailbox".into(), job) }; self.insert_job( handle.job_id, @@ -1544,9 +1571,13 @@ impl Account { .unwrap() .set_mailbox_subscription(mailbox_hash, true)?; let handle = if self.backend_capabilities.is_async { - self.main_loop_handler.job_executor.spawn_specialized(job) + self.main_loop_handler + .job_executor + .spawn_specialized("subscribe_mailbox".into(), job) } else { - self.main_loop_handler.job_executor.spawn_blocking(job) + self.main_loop_handler + .job_executor + .spawn_blocking("subscribe_mailbox".into(), job) }; self.insert_job( handle.job_id, @@ -1566,9 +1597,13 @@ impl Account { .unwrap() .set_mailbox_subscription(mailbox_hash, false)?; let handle = if self.backend_capabilities.is_async { - self.main_loop_handler.job_executor.spawn_specialized(job) + self.main_loop_handler + .job_executor + .spawn_specialized("unsubscribe_mailbox".into(), job) } else { - self.main_loop_handler.job_executor.spawn_blocking(job) + self.main_loop_handler + .job_executor + .spawn_blocking("unsubscribe_mailbox".into(), job) }; self.insert_job( handle.job_id, @@ -1641,30 +1676,28 @@ impl Account { use melib::utils::futures::sleep; let handle = match (wait, self.backend_capabilities.is_async) { - (Some(wait), true) => { - self.main_loop_handler - .job_executor - .spawn_specialized(async move { - sleep(wait).await; - online_fut.await - }) - } + (Some(wait), true) => self.main_loop_handler.job_executor.spawn_specialized( + "is_online".into(), + async move { + sleep(wait).await; + online_fut.await + }, + ), (None, true) => self .main_loop_handler .job_executor - .spawn_specialized(online_fut), - (Some(wait), false) => { - self.main_loop_handler - .job_executor - .spawn_blocking(async move { - sleep(wait).await; - online_fut.await - }) - } + .spawn_specialized("is_online".into(), online_fut), + (Some(wait), false) => self.main_loop_handler.job_executor.spawn_blocking( + "is_online".into(), + async move { + sleep(wait).await; + online_fut.await + }, + ), (None, false) => self .main_loop_handler .job_executor - .spawn_blocking(online_fut), + .spawn_blocking("is_online".into(), online_fut), }; self.insert_job(handle.job_id, JobRequest::IsOnline { handle }); } @@ -1724,6 +1757,7 @@ impl Account { ))); if let Some(mut job) = self.active_jobs.remove(job_id) { + let job_id = *job_id; match job { JobRequest::Mailboxes { ref mut handle } => { if let Ok(Some(mailboxes)) = handle.chan.try_recv() { @@ -1740,6 +1774,9 @@ impl Account { self.main_loop_handler.send(ThreadEvent::UIEvent( UIEvent::AccountStatusChange(self.hash, None), )); + self.main_loop_handler + .job_executor + .set_job_success(job_id, false); return true; } let mailboxes_job = self.backend.read().unwrap().mailboxes(); @@ -1747,11 +1784,11 @@ impl Account { let handle = if self.backend_capabilities.is_async { self.main_loop_handler .job_executor - .spawn_specialized(mailboxes_job) + .spawn_specialized("mailboxes_list".into(), mailboxes_job) } else { self.main_loop_handler .job_executor - .spawn_blocking(mailboxes_job) + .spawn_blocking("mailboxes_list".into(), mailboxes_job) }; self.insert_job(handle.job_id, JobRequest::Mailboxes { handle }); }; @@ -1773,6 +1810,9 @@ impl Account { log::trace!("got payload in status for {}", mailbox_hash); match handle.chan.try_recv() { Err(_) => { + self.main_loop_handler + .job_executor + .set_job_success(job_id, false); /* canceled */ return true; } @@ -1792,6 +1832,9 @@ impl Account { return true; } Ok(Some((Some(Err(err)), _))) => { + self.main_loop_handler + .job_executor + .set_job_success(job_id, false); self.main_loop_handler.send(ThreadEvent::UIEvent( UIEvent::Notification( Some(format!("{}: could not fetch mailbox", &self.name)), @@ -1813,11 +1856,11 @@ impl Account { let handle = if self.backend_capabilities.is_async { self.main_loop_handler .job_executor - .spawn_specialized(rest.into_future()) + .spawn_specialized("rest_fetch".into(), rest.into_future()) } else { self.main_loop_handler .job_executor - .spawn_blocking(rest.into_future()) + .spawn_blocking("rest_fetch".into(), rest.into_future()) }; self.insert_job( handle.job_id, @@ -1870,11 +1913,11 @@ impl Account { let handle = if self.backend_capabilities.is_async { self.main_loop_handler .job_executor - .spawn_specialized(online_job) + .spawn_specialized("is_online".into(), online_job) } else { self.main_loop_handler .job_executor - .spawn_blocking(online_job) + .spawn_blocking("is_online".into(), online_job) }; self.insert_job(handle.job_id, JobRequest::IsOnline { handle }); }; @@ -1894,6 +1937,9 @@ impl Account { )); } Ok(Some(Err(err))) => { + self.main_loop_handler + .job_executor + .set_job_success(job_id, false); self.is_online.set_err(err); _ = self.is_online(); self.main_loop_handler.send(ThreadEvent::UIEvent( @@ -1904,6 +1950,9 @@ impl Account { } JobRequest::SetFlags { ref mut handle, .. } => { if let Ok(Some(Err(err))) = handle.chan.try_recv() { + self.main_loop_handler + .job_executor + .set_job_success(job_id, false); self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::Notification( Some(format!("{}: could not set flag", &self.name)), @@ -1918,6 +1967,9 @@ impl Account { .. } => { if let Ok(Some(Err(err))) = handle.chan.try_recv() { + self.main_loop_handler + .job_executor + .set_job_success(job_id, false); log::error!("Could not save message: {err}"); let file = crate::types::create_temp_file(bytes, None, None, Some("eml"), false); @@ -1940,6 +1992,9 @@ impl Account { JobRequest::SendMessage => {} JobRequest::SendMessageBackground { ref mut handle, .. } => { if let Ok(Some(Err(err))) = handle.chan.try_recv() { + self.main_loop_handler + .job_executor + .set_job_success(job_id, false); self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::Notification( Some("Could not send message".to_string()), @@ -1950,6 +2005,9 @@ impl Account { } JobRequest::DeleteMessages { ref mut handle, .. } => { if let Ok(Some(Err(err))) = handle.chan.try_recv() { + self.main_loop_handler + .job_executor + .set_job_success(job_id, false); self.main_loop_handler .send(ThreadEvent::UIEvent(UIEvent::Notification( Some(format!("{}: could not delete message", &self.name)), @@ -1966,6 +2024,9 @@ impl Account { if let Ok(Some(r)) = handle.chan.try_recv() { match r { Err(err) => { + self.main_loop_handler + .job_executor + .set_job_success(job_id, false); self.main_loop_handler.send(ThreadEvent::UIEvent( UIEvent::Notification( Some(format!( @@ -2051,6 +2112,9 @@ impl Account { Err(_) => { /* canceled */ } Ok(None) => {} Ok(Some(Err(err))) => { + self.main_loop_handler + .job_executor + .set_job_success(job_id, false); self.main_loop_handler.send(ThreadEvent::UIEvent( UIEvent::Notification( Some(format!("{}: could not delete mailbox", &self.name)), @@ -2120,6 +2184,9 @@ impl Account { Err(_) => { /* canceled */ } Ok(None) => {} Ok(Some(Err(err))) => { + self.main_loop_handler + .job_executor + .set_job_success(job_id, false); self.main_loop_handler.send(ThreadEvent::UIEvent( UIEvent::Notification( Some(format!( @@ -2154,6 +2221,9 @@ impl Account { Err(_) => { /* canceled */ } Ok(None) => {} Ok(Some(Err(err))) => { + self.main_loop_handler + .job_executor + .set_job_success(job_id, false); self.main_loop_handler.send(ThreadEvent::UIEvent( UIEvent::Notification( Some(format!( @@ -2196,6 +2266,9 @@ impl Account { if err.kind.is_timeout() { self.watch(); } else { + self.main_loop_handler + .job_executor + .set_job_success(job_id, false); // [ref:TODO]: relaunch watch job with ratelimit for failure self.main_loop_handler.send(ThreadEvent::UIEvent( UIEvent::Notification( @@ -2215,6 +2288,9 @@ impl Account { } => { match handle.chan.try_recv() { Ok(Some(Err(err))) => { + self.main_loop_handler + .job_executor + .set_job_success(job_id, false); self.main_loop_handler.send(ThreadEvent::UIEvent( UIEvent::Notification( Some(format!("{}: {} failed", &self.name, name,)), diff --git a/meli/src/jobs.rs b/meli/src/jobs.rs index 22279772..71593e70 100644 --- a/meli/src/jobs.rs +++ b/meli/src/jobs.rs @@ -22,7 +22,7 @@ //! Async job executor thread pool use std::{ - collections::HashMap, + borrow::Cow, future::Future, iter, panic::catch_unwind, @@ -37,7 +37,8 @@ use crossbeam::{ sync::{Parker, Unparker}, }; pub use futures::channel::oneshot; -use melib::{log, smol, uuid::Uuid}; +use indexmap::IndexMap; +use melib::{log, smol, utils::datetime, uuid::Uuid, UnixTimestamp}; use crate::types::{ThreadEvent, UIEvent}; @@ -101,16 +102,29 @@ uuid_hash_type!(TimerId); pub struct MeliTask { task: AsyncTask, id: JobId, + desc: Cow<'static, str>, timer: bool, } +#[derive(Debug, Clone)] +/// A spawned future's metadata for book-keeping. +pub struct JobMetadata { + pub id: JobId, + pub desc: Cow<'static, str>, + pub timer: bool, + pub started: UnixTimestamp, + pub finished: Option, + pub succeeded: bool, +} + #[derive(Debug)] pub struct JobExecutor { global_queue: Arc>, workers: Vec>, sender: Sender, parkers: Vec, - timers: Arc>>, + timers: Arc>>, + pub jobs: Arc>>, } #[derive(Debug, Default)] @@ -163,7 +177,8 @@ impl JobExecutor { workers: vec![], parkers: vec![], sender, - timers: Arc::new(Mutex::new(HashMap::default())), + timers: Arc::new(Mutex::new(IndexMap::default())), + jobs: Arc::new(Mutex::new(IndexMap::default())), }; let mut workers = vec![]; for _ in 0..num_cpus::get().max(1) { @@ -194,13 +209,18 @@ impl JobExecutor { parker.park_timeout(Duration::from_millis(100)); let task = find_task(&local, &global, stealers.as_slice()); if let Some(meli_task) = task { - let MeliTask { task, id, timer } = meli_task; + let MeliTask { + task, + id, + timer, + desc, + } = meli_task; if !timer { - log::trace!("Worker {} got task {:?}", i, id); + log::trace!("Worker {} got task {:?} {:?}", i, desc, id); } let _ = catch_unwind(|| task.run()); if !timer { - log::trace!("Worker {} returned after {:?}", i, id); + log::trace!("Worker {} returned after {:?} {:?}", i, desc, id); } } }) @@ -210,7 +230,7 @@ impl JobExecutor { } /// Spawns a future with a generic return value `R` - pub fn spawn_specialized(&self, future: F) -> JoinHandle + pub fn spawn_specialized(&self, desc: Cow<'static, str>, future: F) -> JoinHandle where F: Future + Send + 'static, R: Send + 'static, @@ -221,6 +241,19 @@ impl JobExecutor { let injector = self.global_queue.clone(); let cancel = Arc::new(Mutex::new(false)); let cancel2 = cancel.clone(); + + self.jobs.lock().unwrap().insert( + job_id, + JobMetadata { + id: job_id, + desc: desc.clone(), + started: datetime::now(), + finished: None, + succeeded: true, + timer: false, + }, + ); + // Create a task and schedule it for execution. let (handle, task) = async_task::spawn( async move { @@ -234,9 +267,11 @@ impl JobExecutor { if *cancel.lock().unwrap() { return; } + let desc = desc.clone(); injector.push(MeliTask { task, id: job_id, + desc, timer: false, }) }, @@ -256,12 +291,15 @@ impl JobExecutor { /// Spawns a future with a generic return value `R` that might block on a /// new thread - pub fn spawn_blocking(&self, future: F) -> JoinHandle + pub fn spawn_blocking(&self, desc: Cow<'static, str>, future: F) -> JoinHandle where F: Future + Send + 'static, R: Send + 'static, { - self.spawn_specialized(smol::unblock(move || futures::executor::block_on(future))) + self.spawn_specialized( + desc, + smol::unblock(move || futures::executor::block_on(future)), + ) } pub fn create_timer(self: Arc, interval: Duration, value: Duration) -> Timer { @@ -327,6 +365,7 @@ impl JobExecutor { injector.push(MeliTask { task, id: job_id, + desc: "timer".into(), timer: true, }) }, @@ -356,6 +395,18 @@ impl JobExecutor { timer.interval = new_val; } } + + pub fn set_job_finished(&self, id: JobId) { + self.jobs.lock().unwrap().entry(id).and_modify(|entry| { + entry.finished = Some(datetime::now()); + }); + } + + pub fn set_job_success(&self, id: JobId, value: bool) { + self.jobs.lock().unwrap().entry(id).and_modify(|entry| { + entry.succeeded = value; + }); + } } pub type JobChannel = oneshot::Receiver; diff --git a/meli/src/main.rs b/meli/src/main.rs index 4fe06d60..b54c211d 100644 --- a/meli/src/main.rs +++ b/meli/src/main.rs @@ -294,6 +294,7 @@ fn run_app(opt: Opt) -> Result<()> { }, ThreadEvent::JobFinished(id) => { log::trace!("Job finished {}", id); + state.context.main_loop_handler.job_executor.set_job_finished(id); for account in state.context.accounts.values_mut() { if account.process_event(&id) { break; diff --git a/meli/src/state.rs b/meli/src/state.rs index b3f71276..2a43594d 100644 --- a/meli/src/state.rs +++ b/meli/src/state.rs @@ -923,7 +923,7 @@ impl State { .context .main_loop_handler .job_executor - .spawn_blocking(job); + .spawn_blocking("sqlite3::index".into(), job); self.context.accounts[account_index].active_jobs.insert( handle.job_id, crate::conf::accounts::JobRequest::Generic {