use std::{future::Future, sync::Arc, time::Duration}; use anyhow::Result; use futures::{FutureExt, future::BoxFuture}; use parking_lot::Mutex; use tokio::{select, sync::{mpsc::{self, UnboundedReceiver}, oneshot}, task::JoinHandle}; use yazi_config::{YAZI, plugin::{Fetcher, Preloader}}; use yazi_dds::Pump; use yazi_parser::{app::PluginOpt, tasks::ProcessOpenOpt}; use yazi_proxy::TasksProxy; use yazi_shared::{Id, Throttle, scheme::SchemeLike, url::{UrlBuf, UrlLike}}; use yazi_vfs::{must_be_dir, provider, unique_name}; use super::{Ongoing, TaskOp}; use crate::{HIGH, LOW, NORMAL, TaskIn, TaskOps, TaskOut, file::{File, FileInDelete, FileInDownload, FileInHardlink, FileInLink, FileInPaste, FileInTrash, FileInUpload, FileOutDelete, FileOutDownload, FileOutHardlink, FileOutPaste, FileOutUpload, FileProgDelete, FileProgDownload, FileProgHardlink, FileProgLink, FileProgPaste, FileProgTrash, FileProgUpload}, plugin::{Plugin, PluginInEntry, PluginProgEntry}, prework::{Prework, PreworkInFetch, PreworkInLoad, PreworkInSize, PreworkProgFetch, PreworkProgLoad, PreworkProgSize}, process::{Process, ProcessInBg, ProcessInBlock, ProcessInOrphan, ProcessOutBg, ProcessOutBlock, ProcessOutOrphan, ProcessProgBg, ProcessProgBlock, ProcessProgOrphan}}; pub struct Scheduler { file: Arc, plugin: Arc, pub prework: Arc, process: Arc, ops: TaskOps, micro: async_priority_channel::Sender, u8>, handles: Vec>, pub ongoing: Arc>, } impl Scheduler { pub fn serve() -> Self { let (op_tx, op_rx) = mpsc::unbounded_channel(); let (micro_tx, micro_rx) = async_priority_channel::unbounded(); let (macro_tx, macro_rx) = async_priority_channel::unbounded(); let mut scheduler = Self { file: Arc::new(File::new(&op_tx, ¯o_tx)), plugin: Arc::new(Plugin::new(&op_tx, ¯o_tx)), prework: Arc::new(Prework::new(&op_tx, ¯o_tx)), process: Arc::new(Process::new(&op_tx)), ops: TaskOps(op_tx), micro: micro_tx, handles: Vec::with_capacity( YAZI.tasks.micro_workers as usize + YAZI.tasks.macro_workers as usize + 1, ), ongoing: Default::default(), }; for _ in 0..YAZI.tasks.micro_workers { scheduler.handles.push(scheduler.schedule_micro(micro_rx.clone())); } for _ in 0..YAZI.tasks.macro_workers { scheduler.handles.push(scheduler.schedule_macro(micro_rx.clone(), macro_rx.clone())); } scheduler.handle_ops(op_rx); scheduler } pub fn cancel(&self, id: Id) -> bool { let mut ongoing = self.ongoing.lock(); if let Some(hook) = ongoing.hooks.pop(id) && let Some(fut) = hook.call(true) { self.micro.try_send(fut, HIGH).ok(); return false; } ongoing.all.remove(&id).is_some() } pub fn shutdown(&self) { for handle in &self.handles { handle.abort(); } } pub fn file_cut(&self, from: UrlBuf, mut to: UrlBuf, force: bool) { let mut ongoing = self.ongoing.lock(); let id = ongoing.add::(format!("Cut {} to {}", from.display(), to.display())); if to.starts_with(&from) && !to.covariant(&from) { return self.ops.out(id, FileOutPaste::Fail("Cannot cut directory into itself".to_owned())); } ongoing.hooks.add_async(id, { let ops = self.ops.clone(); let (from, to) = (from.clone(), to.clone()); move |canceled| async move { if !canceled { provider::remove_dir_clean(&from).await.ok(); Pump::push_move(from, to); } ops.out(id, FileOutPaste::Clean); } }); let file = self.file.clone(); self.send_micro(id, LOW, async move { if !force { to = unique_name(to, must_be_dir(&from)).await?; } file.paste(FileInPaste { id, from, to, cha: None, cut: true, follow: false, retry: 0 }).await }); } pub fn file_copy(&self, from: UrlBuf, mut to: UrlBuf, force: bool, follow: bool) { let id = self.ongoing.lock().add::(format!( "Copy {} to {}", from.display(), to.display() )); if to.starts_with(&from) && !to.covariant(&from) { return self.ops.out(id, FileOutPaste::Fail("Cannot copy directory into itself".to_owned())); } let file = self.file.clone(); self.send_micro(id, LOW, async move { if !force { to = unique_name(to, must_be_dir(&from)).await?; } file.paste(FileInPaste { id, from, to, cha: None, cut: false, follow, retry: 0 }).await }); } pub fn file_link(&self, from: UrlBuf, mut to: UrlBuf, relative: bool, force: bool) { let id = self.ongoing.lock().add::(format!( "Link {} to {}", from.display(), to.display() )); let file = self.file.clone(); self.send_micro(id, LOW, async move { if !force { to = unique_name(to, must_be_dir(&from)).await?; } file.link(FileInLink { id, from, to, cha: None, resolve: false, relative, delete: false }) }); } pub fn file_hardlink(&self, from: UrlBuf, mut to: UrlBuf, force: bool, follow: bool) { let id = self.ongoing.lock().add::(format!( "Hardlink {} to {}", from.display(), to.display() )); if to.starts_with(&from) && !to.covariant(&from) { return self .ops .out(id, FileOutHardlink::Fail("Cannot hardlink directory into itself".to_owned())); } let file = self.file.clone(); self.send_micro(id, LOW, async move { if !force { to = unique_name(to, must_be_dir(&from)).await?; } file.hardlink(FileInHardlink { id, from, to, cha: None, follow }).await }); } pub fn file_delete(&self, target: UrlBuf) { let mut ongoing = self.ongoing.lock(); let id = ongoing.add::(format!("Delete {}", target.display())); ongoing.hooks.add_async(id, { let ops = self.ops.clone(); let target = target.clone(); move |canceled| async move { if !canceled { provider::remove_dir_all(&target).await.ok(); TasksProxy::update_succeed(&target); Pump::push_delete(target); } ops.out(id, FileOutDelete::Clean); } }); let file = self.file.clone(); self.send_micro( id, LOW, async move { file.delete(FileInDelete { id, target, length: 0 }).await }, ); } pub fn file_trash(&self, target: UrlBuf) { let mut ongoing = self.ongoing.lock(); let id = ongoing.add::(format!("Trash {}", target.display())); ongoing.hooks.add_sync(id, { let target = target.clone(); move |canceled| { if !canceled { TasksProxy::update_succeed(&target); Pump::push_trash(target); } } }); let file = self.file.clone(); self.send_micro(id, LOW, async move { file.trash(FileInTrash { id, target }) }) } pub fn file_download(&self, url: UrlBuf, done: Option>) { let mut ongoing = self.ongoing.lock(); let id = ongoing.add::(format!("Download {}", url.display())); if let Some(tx) = done { ongoing.hooks.add_sync(id, move |canceled| _ = tx.send(canceled)); } if !url.scheme.is_remote() { return self.ops.out(id, FileOutDownload::Fail("Cannot download non-remote file".to_owned())); }; let file = self.file.clone(); self.send_micro(id, LOW, async move { file.download(FileInDownload { id, url, cha: None, retry: 0 }).await }); } pub fn file_upload(&self, url: UrlBuf) { let mut ongoing = self.ongoing.lock(); let id = ongoing.add::(format!("Upload {}", url.display())); if !url.scheme.is_remote() { return self.ops.out(id, FileOutUpload::Fail("Cannot upload non-remote file".to_owned())); }; let file = self.file.clone(); self.send_micro(id, LOW, async move { file.upload(FileInUpload { id, url }).await }); } pub fn plugin_micro(&self, opt: PluginOpt) { let id = self.ongoing.lock().add::(format!("Run micro plugin `{}`", opt.id)); let plugin = self.plugin.clone(); self.send_micro(id, NORMAL, async move { plugin.micro(PluginInEntry { id, opt }).await }); } pub fn plugin_macro(&self, opt: PluginOpt) { let id = self.ongoing.lock().add::(format!("Run macro plugin `{}`", opt.id)); self.plugin.r#macro(PluginInEntry { id, opt }).ok(); } pub fn fetch_paged( &self, fetcher: &'static Fetcher, targets: Vec, done: Option>, ) { let mut ongoing = self.ongoing.lock(); let id = ongoing.add::(format!( "Run fetcher `{}` with {} target(s)", fetcher.run.name, targets.len() )); if let Some(tx) = done { ongoing.hooks.add_sync(id, move |canceled| _ = tx.send(canceled)); } let prework = self.prework.clone(); self.send_micro(id, NORMAL, async move { prework.fetch(PreworkInFetch { id, plugin: fetcher, targets }).await }); } pub async fn fetch_mimetype(&self, targets: Vec) -> bool { let mut wg = vec![]; for (fetcher, targets) in YAZI.plugin.mime_fetchers(targets) { let (tx, rx) = oneshot::channel(); self.fetch_paged(fetcher, targets, Some(tx)); wg.push(rx); } for rx in wg { if rx.await != Ok(false) { return false; // Canceled or error } } true } pub fn preload_paged(&self, preloader: &'static Preloader, target: &yazi_fs::File) { let id = self.ongoing.lock().add::(format!("Run preloader `{}`", preloader.run.name)); let target = target.clone(); let prework = self.prework.clone(); self.send_micro(id, NORMAL, async move { prework.load(PreworkInLoad { id, plugin: preloader, target }).await }); } pub fn prework_size(&self, targets: Vec<&UrlBuf>) { let throttle = Arc::new(Throttle::new(targets.len(), Duration::from_millis(300))); let mut ongoing = self.ongoing.lock(); for target in targets { let id = ongoing.add::(format!("Calculate the size of {}", target.display())); let target = target.clone(); let throttle = throttle.clone(); let prework = self.prework.clone(); self.send_micro(id, NORMAL, async move { prework.size(PreworkInSize { id, target, throttle }).await }); } } pub fn process_open(&self, opt: ProcessOpenOpt) { let name = { let args = opt.args.iter().map(|a| a.display().to_string()).collect::>().join(" "); if args.is_empty() { format!("Run {:?}", opt.cmd) } else { format!("Run {:?} with `{args}`", opt.cmd) } }; let mut ongoing = self.ongoing.lock(); let (id, clean): (_, TaskOut) = if opt.block { (ongoing.add::(name), ProcessOutBlock::Clean.into()) } else if opt.orphan { (ongoing.add::(name), ProcessOutOrphan::Clean.into()) } else { (ongoing.add::(name), ProcessOutBg::Clean.into()) }; let ops = self.ops.clone(); let (cancel_tx, cancel_rx) = mpsc::channel(1); ongoing.hooks.add_async(id, move |canceled| async move { if canceled { cancel_tx.send(()).await.ok(); cancel_tx.closed().await; } if let Some(tx) = opt.done { tx.send(()).ok(); } ops.out(id, clean); }); let process = self.process.clone(); self.send_micro::<_, TaskOut>(id, NORMAL, async move { if opt.block { process.block(ProcessInBlock { id, cwd: opt.cwd, cmd: opt.cmd, args: opt.args }).await?; } else if opt.orphan { process.orphan(ProcessInOrphan { id, cwd: opt.cwd, cmd: opt.cmd, args: opt.args }).await?; } else { process .bg(ProcessInBg { id, cwd: opt.cwd, cmd: opt.cmd, args: opt.args, cancel: cancel_rx }) .await?; } Ok(()) }); } fn schedule_micro( &self, rx: async_priority_channel::Receiver, u8>, ) -> JoinHandle<()> { tokio::spawn(async move { loop { if let Ok((fut, _)) = rx.recv().await { fut.await; } } }) } fn schedule_macro( &self, micro: async_priority_channel::Receiver, u8>, r#macro: async_priority_channel::Receiver, ) -> JoinHandle<()> { let file = self.file.clone(); let plugin = self.plugin.clone(); let prework = self.prework.clone(); let ops = self.ops.clone(); let ongoing = self.ongoing.clone(); tokio::spawn(async move { loop { select! { Ok((fut, _)) = micro.recv() => { fut.await; } Ok((r#in, _)) = r#macro.recv() => { let id = r#in.id(); if !ongoing.lock().exists(id) { continue; } let result: Result<_, TaskOut> = match r#in { // File TaskIn::FilePaste(r#in) => file.paste_do(r#in).await.map_err(Into::into), TaskIn::FileLink(r#in) => file.link_do(r#in).await.map_err(Into::into), TaskIn::FileHardlink(r#in) => file.hardlink_do(r#in).await.map_err(Into::into), TaskIn::FileDelete(r#in) => file.delete_do(r#in).await.map_err(Into::into), TaskIn::FileTrash(r#in) => file.trash_do(r#in).await.map_err(Into::into), TaskIn::FileDownload(r#in) => file.download_do(r#in).await.map_err(Into::into), TaskIn::FileUploadDo(r#in) => file.upload_do(r#in).await.map_err(Into::into), // Plugin TaskIn::PluginEntry(r#in) => plugin.macro_do(r#in).await.map_err(Into::into), // Prework TaskIn::PreworkFetch(r#in) => prework.fetch_do(r#in).await.map_err(Into::into), TaskIn::PreworkLoad(r#in) => prework.load_do(r#in).await.map_err(Into::into), TaskIn::PreworkSize(r#in) => prework.size_do(r#in).await.map_err(Into::into), }; if let Err(out) = result { ops.out(id, out); } } } } }) } fn handle_ops(&self, mut rx: UnboundedReceiver) -> JoinHandle<()> { let micro = self.micro.clone(); let ongoing = self.ongoing.clone(); tokio::spawn(async move { while let Some(op) = rx.recv().await { let mut ongoing = ongoing.lock(); let Some(task) = ongoing.get_mut(op.id) else { continue }; op.out.reduce(task); if !task.prog.success() && !task.prog.cleaned() { continue; } else if let Some(hook) = ongoing.hooks.pop(op.id) && let Some(fut) = hook.call(false) { micro.try_send(fut, LOW).ok(); } else { ongoing.all.remove(&op.id); } } }) } fn send_micro(&self, id: Id, priority: u8, f: F) where F: Future> + Send + 'static, E: Into, { let ops = self.ops.clone(); _ = self.micro.try_send( async move { if let Err(out) = f.await { ops.out(id, out); } } .boxed(), priority, ); } }