diff --git a/Cargo.lock b/Cargo.lock index a0ab6f82..a4f693e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,27 +155,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" -dependencies = [ - "autocfg", - "cfg-if 1.0.0", - "lazy_static", -] - [[package]] name = "deflate" version = "0.8.6" @@ -789,7 +768,6 @@ dependencies = [ "bitflags", "byteorder", "chrono", - "crossbeam-channel", "downcast-rs", "entities", "filetime", diff --git a/Cargo.toml b/Cargo.toml index 71b5d977..06d796e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,10 +74,6 @@ version = "0.4.19" version = "0.34.3" optional = true -[dependencies.crossbeam-channel] -version = "0.5.0" -optional = true - [dependencies.signal-hook] version = "0.3.1" optional = true @@ -85,4 +81,4 @@ optional = true [features] importer = ["getopts"] emulator = ["sdl2"] -fetcher = ["reqwest", "crossbeam-channel", "signal-hook"] +fetcher = ["reqwest", "signal-hook"] diff --git a/doc/HOOKS.md b/doc/HOOKS.md index 26f3c5ef..9622ff99 100644 --- a/doc/HOOKS.md +++ b/doc/HOOKS.md @@ -7,22 +7,27 @@ Here's an example hook, that launches the default article fetcher included in path = "Articles" program = "bin/article_fetcher/article_fetcher" sort-method = "added" +first-column = "title-and-author" second-column = "progress" ``` The above chunk needs to be added after one of the `[[libraries]]` section. -The `path` key is the path of the directory that will trigger the hook. The -`sort-method` and `second-column` keys are optional. +`path` is the path of the directory that will trigger the hook. `program` is +the path to the executable associated with this hook. The `sort-method`, +`first-column` and `second-column` keys are optional. -The *Toogle Select* sub-menu of the library menu can be used to trigger a hook when the -corresponding directory doesn't exit yet. Otherwise, you can just tap -the directory label in the navigation bar. When the hook is triggered, the -associated `program` is spawned. It will receive the directory path, wifi and -online statuses (*true* or *false*) as arguments. +The *Toogle Select* sub-menu of the library menu can be used to trigger a hook +when there's no imported documents in `path`. Otherwise, you can just tap the +directory in the navigation bar. When the hook is triggered, the associated +`program` is spawned. It will receive the directory path, wifi and online +statuses (*true* or *false*) as arguments. -A fetcher can send events to *Plato* through its standard output. -Each event is a JSON object with a required `type` key: +A fetcher can use its standard output (resp. standard input) to send events to +(resp. receive events from) *Plato*. An event is a JSON object with a required +`type` key. Events are read and written line by line, one per line. + +The events that can be written to standard output are: ``` // Display a notification message. @@ -32,16 +37,22 @@ Each event is a JSON object with a required `type` key: {"type": "addDocument", "info": OBJECT} // Enable or disable the WiFi. {"type": "setWifi", "enable": BOOL} +// Search for books matching the given query. +{"type": "search", "query": STRING} // Import new entries and update existing entries in the current library. {"type": "import"} // Remove entries with dangling paths from the current library. {"type": "cleanUp"} ``` -On *Plato*'s side, the events are read line by line, one event per line. +The events that can be read from standard input are: -When the network becomes operational, *Plato* will send the `SIGUSR1` signal to -all the fetchers. +``` +// Sent in response to `search`. `results` is an array of *Info* objects. +{"type": "search": "results": ARRAY} +// Sent to all the fetchers when the network becomes available. +{"type": "network", "status": "up"} +``` -When the associated directory is deselected, *Plato* will send the `SIGTERM` -signal to the corresponding fetcher. +When a directory is deselected, *Plato* will send the `SIGTERM` signal to all +the matching fetchers. diff --git a/src/fetcher.rs b/src/fetcher.rs index c961d95e..41599e0a 100644 --- a/src/fetcher.rs +++ b/src/fetcher.rs @@ -1,9 +1,11 @@ mod helpers; +use std::io; use std::env; -use std::thread; use std::fs::{self, File}; use std::path::PathBuf; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use reqwest::blocking::Client; use serde_json::json; use chrono::{Duration, Utc, Local, DateTime}; @@ -16,11 +18,6 @@ const SETTINGS_PATH: &str = "Settings.toml"; const SESSION_PATH: &str = ".session.json"; // Nearly RFC 3339 const DATE_FORMAT: &str = "%FT%T%z"; -const LISTENED_SIGNALS: &[libc::c_int] = &[ - signal_hook::consts::SIGINT, signal_hook::consts::SIGHUP, - signal_hook::consts::SIGQUIT, signal_hook::consts::SIGTERM, - signal_hook::consts::SIGUSR1, signal_hook::consts::SIGUSR2, -]; #[derive(Default, Debug, Clone, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case")] @@ -64,19 +61,6 @@ impl Default for Session { } } -fn signal_receiver(signals: &[libc::c_int]) -> Result, Error> { - let (s, r) = crossbeam_channel::bounded(4); - let mut signals = signal_hook::iterator::Signals::new(signals)?; - thread::spawn(move || { - for signal in signals.forever() { - if s.send(signal).is_err() { - break; - } - } - }); - Ok(r) -} - fn update_token(client: &Client, session: &mut Session, settings: &Settings) -> Result<(), Error> { let query = json!({ "grant_type": "password", @@ -120,15 +104,17 @@ fn main() -> Result<(), Error> { .with_context(|| format!("Can't load settings from {}", SETTINGS_PATH))?; let mut session = load_json::(SESSION_PATH) .unwrap_or_default(); - let signals = signal_receiver(LISTENED_SIGNALS)?; if !online { - let event = json!({ - "type": "setWifi", - "enable": true, - }); - println!("{}", event); - signals.recv()?; + if !wifi { + let event = json!({ + "type": "setWifi", + "enable": true, + }); + println!("{}", event); + } + let mut line = String::new(); + io::stdin().read_line(&mut line)?; } if !save_path.exists() { @@ -147,6 +133,9 @@ fn main() -> Result<(), Error> { let since = session.since; let url = format!("{}/api/entries", &settings.base_url); + let sigterm = Arc::new(AtomicBool::new(false)); + signal_hook::flag::register(signal_hook::consts::SIGTERM, Arc::clone(&sigterm))?; + 'outer: loop { let query = json!({ "since": since, @@ -191,10 +180,8 @@ fn main() -> Result<(), Error> { if let Some(items) = entries.pointer("/_embedded/items").and_then(|v| v.as_array()) { for element in items { - if let Ok(sig) = signals.try_recv() { - if sig != signal_hook::consts::SIGUSR1 { - break 'outer; - } + if sigterm.load(Ordering::Relaxed) { + break 'outer } let id = element.get("id") @@ -309,11 +296,13 @@ fn main() -> Result<(), Error> { println!("{}", event); } - let event = json!({ - "type": "setWifi", - "enable": wifi, - }); - println!("{}", event); + if !wifi { + let event = json!({ + "type": "setWifi", + "enable": false, + }); + println!("{}", event); + } save_json(&session, SESSION_PATH).context("Can't save session.")?; Ok(()) diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 7ca06b90..6e1428f7 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -230,7 +230,7 @@ pub enum SecondColumn { #[serde(default, rename_all = "kebab-case")] pub struct Hook { pub path: PathBuf, - pub program: Option, + pub program: PathBuf, pub sort_method: Option, pub first_column: Option, pub second_column: Option, @@ -240,7 +240,7 @@ impl Default for Hook { fn default() -> Self { Hook { path: PathBuf::default(), - program: None, + program: PathBuf::default(), sort_method: None, first_column: None, second_column: None, diff --git a/src/view/home/mod.rs b/src/view/home/mod.rs index 06790894..d76bd897 100644 --- a/src/view/home/mod.rs +++ b/src/view/home/mod.rs @@ -10,12 +10,13 @@ mod bottom_bar; use std::fs; use std::mem; use std::thread; +use std::io::Write; use std::path::{Path, PathBuf}; use std::process::{Command, Child, Stdio}; use std::io::{BufRead, BufReader}; use fxhash::FxHashMap; use rand_core::RngCore; -use serde_json::Value as JsonValue; +use serde_json::{json, Value as JsonValue}; use anyhow::{Error, format_err}; use crate::library::Library; use crate::framebuffer::{Framebuffer, UpdateMode}; @@ -64,12 +65,13 @@ pub struct Home { reverse_order: bool, visible_books: Metadata, current_directory: PathBuf, - background_fetchers: FxHashMap, + background_fetchers: FxHashMap, } #[derive(Debug)] struct Fetcher { - process: Option, + path: PathBuf, + process: Child, sort_method: Option, first_column: Option, second_column: Option, @@ -1132,12 +1134,10 @@ impl Home { } fn terminate_fetchers(&mut self, path: &Path, hub: &Hub) { - self.background_fetchers.retain(|dir, fetcher| { - if dir == path { - if let Some(process) = fetcher.process.as_mut() { - unsafe { libc::kill(process.id() as libc::pid_t, libc::SIGTERM) }; - process.wait().ok(); - } + self.background_fetchers.retain(|id, fetcher| { + if fetcher.path == path { + unsafe { libc::kill(*id as libc::pid_t, libc::SIGTERM) }; + fetcher.process.wait().ok(); if let Some(sort_method) = fetcher.sort_method { hub.send(Event::Select(EntryId::Sort(sort_method))).ok(); } @@ -1155,26 +1155,28 @@ impl Home { } fn insert_fetcher(&mut self, hook: &Hook, hub: &Hub, context: &Context) { - let mut sort_method = hook.sort_method; - let mut first_column = hook.first_column; - let mut second_column = hook.second_column; - if let Some(sort_method) = sort_method.replace(self.sort_method) { - hub.send(Event::Select(EntryId::Sort(sort_method))).ok(); - } - let selected_library = context.settings.selected_library; - if let Some(first_column) = first_column.replace(context.settings.libraries[selected_library].first_column) { - hub.send(Event::Select(EntryId::FirstColumn(first_column))).ok(); - } - if let Some(second_column) = second_column.replace(context.settings.libraries[selected_library].second_column) { - hub.send(Event::Select(EntryId::SecondColumn(second_column))).ok(); + let dir = context.library.home.join(&hook.path); + match self.spawn_child(&dir, &hook.program, context.settings.wifi, context.online, hub) { + Ok(process) => { + let mut sort_method = hook.sort_method; + let mut first_column = hook.first_column; + let mut second_column = hook.second_column; + if let Some(sort_method) = sort_method.replace(self.sort_method) { + hub.send(Event::Select(EntryId::Sort(sort_method))).ok(); + } + let selected_library = context.settings.selected_library; + if let Some(first_column) = first_column.replace(context.settings.libraries[selected_library].first_column) { + hub.send(Event::Select(EntryId::FirstColumn(first_column))).ok(); + } + if let Some(second_column) = second_column.replace(context.settings.libraries[selected_library].second_column) { + hub.send(Event::Select(EntryId::SecondColumn(second_column))).ok(); + } + self.background_fetchers.insert(process.id(), + Fetcher { path: hook.path.clone(), process, + sort_method, first_column, second_column }); + }, + Err(e) => eprintln!("Can't spawn child: {}.", e), } - let process = hook.program.as_ref().and_then(|p| { - let dir = context.library.home.join(&hook.path); - self.spawn_child(&dir, p, context.settings.wifi, context.online, hub) - .map_err(|e| eprintln!("Can't spawn child: {}.", e)).ok() - }); - self.background_fetchers.insert(hook.path.clone(), - Fetcher { process, sort_method, first_column, second_column }); } fn spawn_child(&mut self, dir: &Path, program: &PathBuf, wifi: bool, online: bool, hub: &Hub) -> Result { @@ -1186,6 +1188,7 @@ impl Home { .arg(dir) .arg(wifi.to_string()) .arg(online.to_string()) + .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?; let stdout = process.stdout.take() @@ -1214,6 +1217,13 @@ impl Home { hub2.send(Event::SetWifi(enable)).ok(); } }, + Some("search") => { + let path = event.get("path").and_then(JsonValue::as_str) + .map(PathBuf::from); + let query = event.get("query").and_then(JsonValue::as_str) + .map(String::from); + hub2.send(Event::FetcherSearch(id, path, query)).ok(); + }, Some("cleanUp") => { hub2.send(Event::Select(EntryId::CleanUp)).ok(); }, @@ -1529,26 +1539,33 @@ impl View for Home { true }, Event::Device(DeviceEvent::NetUp) => { - for fetcher in self.background_fetchers.values() { - if let Some(process) = fetcher.process.as_ref() { - unsafe { libc::kill(process.id() as libc::pid_t, libc::SIGUSR1) }; + for fetcher in self.background_fetchers.values_mut() { + if let Some(stdin) = fetcher.process.stdin.as_mut() { + writeln!(stdin, "{}", json!({"type": "network", "status": "up"})).ok(); + } + } + true + }, + Event::FetcherSearch(id, ref path, ref text) => { + let path = path.as_ref().unwrap_or_else(|| &context.library.home); + let query = text.as_ref().and_then(|text| BookQuery::new(text)); + let (files, _) = context.library.list(path, query.as_ref(), false); + if let Some(fetcher) = self.background_fetchers.get_mut(&id) { + if let Some(stdin) = fetcher.process.stdin.as_mut() { + writeln!(stdin, "{}", json!({"type": "search", "results": &files})).ok(); } } true }, Event::CheckFetcher(id) => { - for (path, fetcher) in &mut self.background_fetchers { - if let Some(process) = fetcher.process.as_mut() - .filter(|process| process.id() == id) { - if let Ok(exit_status) = process.wait() { - if !exit_status.success() { - let msg = format!("{}: abnormal process termination.", path.display()); - let notif = Notification::new(ViewId::FetcherFailure, - msg, hub, rq, context); - self.children.push(Box::new(notif) as Box); - } + if let Some(fetcher) = self.background_fetchers.get_mut(&id) { + if let Ok(exit_status) = fetcher.process.wait() { + if !exit_status.success() { + let msg = format!("{}: abnormal process termination.", fetcher.path.display()); + let notif = Notification::new(ViewId::FetcherFailure, + msg, hub, rq, context); + self.children.push(Box::new(notif) as Box); } - break; } } true diff --git a/src/view/mod.rs b/src/view/mod.rs index 59632671..17c26498 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -55,7 +55,7 @@ use downcast_rs::{Downcast, impl_downcast}; use crate::font::Fonts; use crate::document::{Location, TextLocation}; use crate::settings::{ButtonScheme, FirstColumn, SecondColumn, RotationLock}; -use crate::metadata::{Info, ZoomMode, SortMethod, TextAlign, SimpleStatus, PageScheme, Margin}; +use crate::metadata::{Info, BookQuery, ZoomMode, SortMethod, TextAlign, SimpleStatus, PageScheme, Margin}; use crate::geom::{LinearDir, CycleDir, Rectangle, Boundary}; use crate::framebuffer::{Framebuffer, UpdateMode}; use crate::input::{DeviceEvent, FingerStatus}; @@ -315,6 +315,7 @@ pub enum Event { CloseSub(ViewId), Search(String), SearchResult(usize, Vec), + FetcherSearch(u32, Option, Option), CheckFetcher(u32), EndOfSearch, Finished,