From fff6ddb15a8a5e7c698b795b23d461e7cedd58e9 Mon Sep 17 00:00:00 2001 From: Bruce Leidl Date: Tue, 2 Apr 2019 15:22:55 -0400 Subject: [PATCH] citadel-desktopd replaced with citadel-desktop-sync util --- citadel-desktopd/Cargo.toml | 17 -- citadel-desktopd/conf/citadel-desktopd.conf | 6 - .../conf/citadel-desktopd.service | 5 - citadel-desktopd/src/config.rs | 120 ---------- citadel-desktopd/src/desktop_file_sync.rs | 120 ---------- citadel-desktopd/src/main.rs | 52 ----- citadel-desktopd/src/monitor.rs | 211 ------------------ .../src/sync/desktop_file.rs | 13 +- citadel-tool/src/sync/icon_cache.rs | 91 ++++++++ citadel-tool/src/sync/icons.rs | 121 ++++++++++ citadel-tool/src/sync/mod.rs | 27 +++ .../src => citadel-tool/src/sync}/parser.rs | 6 +- citadel-tool/src/sync/sync.rs | 166 ++++++++++++++ systemd/citadel-current-watcher.path | 9 + systemd/citadel-current-watcher.service | 8 + systemd/citadel-desktop-watcher.path | 7 + systemd/citadel-desktop-watcher.service | 7 + 17 files changed, 446 insertions(+), 540 deletions(-) delete mode 100644 citadel-desktopd/Cargo.toml delete mode 100644 citadel-desktopd/conf/citadel-desktopd.conf delete mode 100644 citadel-desktopd/conf/citadel-desktopd.service delete mode 100644 citadel-desktopd/src/config.rs delete mode 100644 citadel-desktopd/src/desktop_file_sync.rs delete mode 100644 citadel-desktopd/src/main.rs delete mode 100644 citadel-desktopd/src/monitor.rs rename citadel-desktopd/src/desktop.rs => citadel-tool/src/sync/desktop_file.rs (96%) create mode 100644 citadel-tool/src/sync/icon_cache.rs create mode 100644 citadel-tool/src/sync/icons.rs create mode 100644 citadel-tool/src/sync/mod.rs rename {citadel-desktopd/src => citadel-tool/src/sync}/parser.rs (99%) create mode 100644 citadel-tool/src/sync/sync.rs create mode 100644 systemd/citadel-current-watcher.path create mode 100644 systemd/citadel-current-watcher.service create mode 100644 systemd/citadel-desktop-watcher.path create mode 100644 systemd/citadel-desktop-watcher.service diff --git a/citadel-desktopd/Cargo.toml b/citadel-desktopd/Cargo.toml deleted file mode 100644 index 0c3eaf8..0000000 --- a/citadel-desktopd/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "citadel-desktopd" -version = "0.1.0" -authors = ["brl@subgraph.com"] -homepage = "https://github.com/subgraph/citadel" -edition = "2018" - -[dependencies] -failure = "0.1.1" -lazy_static = "1.0" -log = "0.4.0" -env_logger = "0.5.3" -inotify = "0.5" -toml = "0.4.5" -serde_derive = "1.0.27" -serde = "1.0.27" -nix = "0.10.0" diff --git a/citadel-desktopd/conf/citadel-desktopd.conf b/citadel-desktopd/conf/citadel-desktopd.conf deleted file mode 100644 index 1e7cc02..0000000 --- a/citadel-desktopd/conf/citadel-desktopd.conf +++ /dev/null @@ -1,6 +0,0 @@ -[options] -exec_prefix="/usr/bin/realms run --" -target_directory="/home/citadel/.local/share/applications" - -[[sources]] -path="/run/realms/current.realm/rootfs/usr/share/applications" diff --git a/citadel-desktopd/conf/citadel-desktopd.service b/citadel-desktopd/conf/citadel-desktopd.service deleted file mode 100644 index a367e4e..0000000 --- a/citadel-desktopd/conf/citadel-desktopd.service +++ /dev/null @@ -1,5 +0,0 @@ -[Unit] -Description=Desktop Integration Manager - -[Service] -ExecStart=/usr/libexec/citadel-desktopd /usr/share/citadel/citadel-desktopd.conf diff --git a/citadel-desktopd/src/config.rs b/citadel-desktopd/src/config.rs deleted file mode 100644 index bf668c4..0000000 --- a/citadel-desktopd/src/config.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::path::{Path,PathBuf}; -use std::fs::File; -use std::io::Read; -use crate::Result; -use toml; - -#[derive(Clone)] -pub struct Config { - exec_prefix: String, - target_directory: PathBuf, - source_paths: Vec, -} - - -impl Config { - pub fn from_path>(path: P) -> Result { - let toml = ConfigToml::from_path(path)?; - let config = toml.to_config()?; - Ok(config) - } - - pub fn exec_prefix(&self) -> &str { - self.exec_prefix.as_ref() - } - - pub fn target_directory(&self) -> &Path { - self.target_directory.as_ref() - } - - pub fn source_paths(&self) -> &Vec { - self.source_paths.as_ref() - } -} - -#[derive(Deserialize)] -struct ConfigToml { - options: Option, - sources: Option>, -} - -#[derive(Deserialize)] -struct Options { - exec_prefix: Option, - target_directory: Option, -} - -impl Options { - fn exec_prefix(&self) -> Result<&str> { - match self.exec_prefix { - Some(ref s) => Ok(s.as_str()), - None => Err(format_err!("missing 'exec_prefix=' field")), - } - } - - fn target_directory(&self) -> Result<&str> { - match self.target_directory { - Some(ref s) => Ok(s.as_str()), - None => Err(format_err!("missing 'target_directory=' field")), - } - } -} - -#[derive(Deserialize,Clone)] -struct Source { - path: Option, -} - -impl Source { - fn path(&self) -> Result<&str> { - match self.path { - Some(ref s) => Ok(s.as_str()), - None => Err(format_err!("missing 'path=' field")), - } - } -} - -fn load_as_string(path: &Path) -> Result { - let mut f = File::open(path)?; - let mut buffer = String::new(); - f.read_to_string(&mut buffer)?; - Ok(buffer) -} - -impl ConfigToml { - fn from_path>(path: P) -> Result { - let s = load_as_string(path.as_ref())?; - let config = toml::from_str::(&s)?; - Ok(config) - } - - fn options(&self) -> Result<&Options> { - self.options.as_ref() - .ok_or(format_err!("missing '[options]' section")) - } - - fn sources(&self) -> Result> { - match self.sources { - Some(ref srcs) => Ok(srcs.clone()), - None => Err(format_err!("missing '[[sources]]' section(s)")), - } - } - - fn to_config(&self) -> Result { - let options = self.options()?; - let exec_prefix = options.exec_prefix()?.to_string() + " "; - let target_path = options.target_directory()?.to_string(); - let target_directory = PathBuf::from(target_path); - - let mut source_paths = Vec::new(); - for src in self.sources()? { - let path = src.path()?; - source_paths.push(PathBuf::from(path)); - } - Ok(Config{ - exec_prefix, - target_directory, - source_paths, - }) - } -} diff --git a/citadel-desktopd/src/desktop_file_sync.rs b/citadel-desktopd/src/desktop_file_sync.rs deleted file mode 100644 index a12185e..0000000 --- a/citadel-desktopd/src/desktop_file_sync.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::sync::{Arc,Mutex}; -use std::path::Path; -use std::fs; - -use failure::ResultExt; - -use crate::monitor::{DirectoryMonitor,MonitorEventHandler}; -use crate::parser::DesktopFileParser; -use crate::config::Config; -use crate::Result; - - -pub struct DesktopFileSync { - config: Config, - monitor: DirectoryMonitor, -} - -impl DesktopFileSync { - pub fn new(config: Config) -> DesktopFileSync { - let handler = Arc::new(Mutex::new(SyncHandler::new(config.clone()))); - let monitor = DirectoryMonitor::new(handler.clone()); - DesktopFileSync { - config, monitor - } - } - - pub fn clear_target_directory(&self) -> Result<()> { - let entries = fs::read_dir(self.config.target_directory())?; - - for entry in entries { - let path = entry?.path(); - if is_desktop_file(&path) { - fs::remove_file(&path).context(format!("remove_file({:?})", path))?; - } - } - Ok(()) - } - - pub fn sync_source(&mut self, src: &Path) -> Result<()> { - self.clear_target_directory()?; - self.sync_source_directory(src)?; - self.monitor.set_monitor_sources(&[src.to_path_buf()]); - Ok(()) - } - - fn sync_source_directory(&self, src: &Path) -> Result<()> { - let entries = fs::read_dir(src)?; - for entry in entries { - let path = entry?.path(); - if is_desktop_file(path.as_path()) { - if let Err(e) = sync_desktop_file(path.as_path(), &self.config) { - info!("error syncing desktop file {:?}: {}", path, e); - } - } - } - Ok(()) - } -} - -struct SyncHandler { - config: Config, -} - -impl SyncHandler { - fn new(config: Config) -> SyncHandler { - SyncHandler { config } - } -} - -impl MonitorEventHandler for SyncHandler { - fn file_added(&self, path: &Path) -> Result<()> { - info!("file_added: {:?}", path); - if is_desktop_file(path) { - sync_desktop_file(path, &self.config)?; - } - Ok(()) - } - - fn file_removed(&self, path: &Path) -> Result<()> { - info!("file_removed: {:?}", path); - let filename = filename_from_path(path)?; - let target_path = self.config.target_directory().join(filename); - if target_path.exists() { - fs::remove_file(target_path.as_path())?; - } - Ok(()) - } -} - -fn sync_desktop_file(source: &Path, config: &Config) -> Result<()> { - if !is_desktop_file(source) { - return Err(format_err!("source path [{:?}] is not desktop file", source)); - } - let df = DesktopFileParser::parse_from_path(source, config.exec_prefix())?; - if df.is_showable() { - df.write_to_dir(config.target_directory())?; - } else { - info!("ignoring {} as not showable", df.filename()); - } - Ok(()) -} - -fn is_desktop_file(path: &Path) -> bool { - if let Some(ext) = path.extension() { - return ext == "desktop" - } - false -} - -fn filename_from_path(path: &Path) -> Result<&str> { - let filename = match path.file_name() { - Some(name) => name, - None => return Err(format_err!("Path {:?} has no filename component", path)), - }; - match filename.to_str() { - Some(s) => Ok(s), - None => Err(format_err!("Filename has invalid utf8 encoding")), - } -} - diff --git a/citadel-desktopd/src/main.rs b/citadel-desktopd/src/main.rs deleted file mode 100644 index de3dce8..0000000 --- a/citadel-desktopd/src/main.rs +++ /dev/null @@ -1,52 +0,0 @@ -#[macro_use] extern crate failure; -#[macro_use] extern crate lazy_static; -#[macro_use] extern crate log; -#[macro_use] extern crate serde_derive; - -mod desktop; -mod parser; -mod config; -mod monitor; -mod desktop_file_sync; - -use std::result; -use std::process; -use failure::Error; -use crate::desktop_file_sync::DesktopFileSync; - -use crate::config::Config; - -pub type Result = result::Result; - - -fn main() { - std::env::set_var("RUST_LOG", "info"); - env_logger::init(); - - let mut args = std::env::args(); - args.next(); - if args.len() != 1 { - println!("expected config file argument"); - process::exit(1); - } - - let config_path = args.next().unwrap(); - let config = match Config::from_path(&config_path) { - Err(e) => { - warn!("Failed to load configuration file: {}", e); - process::exit(1); - }, - Ok(config) => config, - }; - - let src = config.source_paths().first().unwrap().clone(); - - let mut dfs = DesktopFileSync::new(config.clone()); - if let Err(e) = dfs.sync_source(src.as_path()) { - warn!("error calling sync_source: {}", e); - } - loop { - std::thread::sleep(std::time::Duration::new(120, 0)); - } - -} diff --git a/citadel-desktopd/src/monitor.rs b/citadel-desktopd/src/monitor.rs deleted file mode 100644 index 3bf0b6b..0000000 --- a/citadel-desktopd/src/monitor.rs +++ /dev/null @@ -1,211 +0,0 @@ -use std::collections::HashMap; -use std::path::{Path,PathBuf}; -use std::sync::{Arc,Mutex,Once,ONCE_INIT}; -use std::sync::atomic::{AtomicBool,Ordering}; -use std::thread::{self,JoinHandle}; -use std::os::unix::thread::JoinHandleExt; -use std::io::ErrorKind; -use std::ffi::OsStr; - -use nix::libc; -use nix::sys::signal; - -use inotify::{Events,Inotify,EventMask,WatchMask,Event,WatchDescriptor}; - -use crate::Result; - -pub trait MonitorEventHandler: Send+Sync { - fn file_added(&self, path: &Path) -> Result<()> { let _ = path; Ok(()) } - fn file_removed(&self, path: &Path) -> Result<()> { let _ = path; Ok(()) } - fn directory_added(&self, path: &Path) -> Result<()> { let _ = path; Ok(()) } - fn directory_removed(&self, path: &Path) -> Result<()> { let _ = path; Ok(()) } -} - -pub struct DirectoryMonitor { - event_handler: Arc>, - worker_handle: Option, -} - -impl DirectoryMonitor { - pub fn new(handler: Arc>) -> DirectoryMonitor { - initialize(); - DirectoryMonitor { - event_handler: handler, - worker_handle: None, - } - } - - pub fn set_monitor_sources(&mut self, sources: &[PathBuf]) { - if let Some(handle) = self.worker_handle.take() { - handle.stop(); - handle.wait(); - } - let sources = Vec::from(sources); - let h = MonitorWorker::start_worker(sources, self.event_handler.clone()); - self.worker_handle = Some(h); - } -} - -struct MonitorWorker { - descriptors: HashMap, - inotify: Inotify, - exit_flag: Arc, - watch_paths: Vec, - handler: Arc>, -} - - -impl MonitorWorker { - fn start_worker(watch_paths: Vec, handler: Arc>) -> WorkerHandle { - let exit_flag = Arc::new(AtomicBool::new(false)); - let flag_clone = exit_flag.clone(); - let jhandle = thread::spawn(move || { - - let mut worker = match MonitorWorker::new(watch_paths, flag_clone, handler) { - Ok(worker) => worker, - Err(e) => { - info!("failed to initialize inotify handle: {}", e); - return; - } - }; - if let Err(e) = worker.run() { - info!("error returned from worker thread: {}", e); - } - }); - WorkerHandle::new(jhandle, exit_flag) - } - - fn new(watch_paths: Vec, exit_flag: Arc, handler: Arc>) -> Result { - Ok(MonitorWorker { - descriptors: HashMap::new(), - inotify: Inotify::init()?, - exit_flag, - watch_paths, - handler, - }) - } - - fn add_watches(&mut self) -> Result<()> { - let watch_flags = WatchMask::CREATE | WatchMask::DELETE | WatchMask::MOVED_TO | - WatchMask::DONT_FOLLOW | WatchMask::ONLYDIR; - for p in &self.watch_paths { - let wd = self.inotify.add_watch(p, watch_flags)?; - self.descriptors.insert(wd, p.clone()); - } - Ok(()) - } - - fn read_events<'a>(&mut self, buffer: &'a mut [u8]) -> Result>> { - if self.exit_flag.load(Ordering::Relaxed) { - return Ok(None); - } - - match self.inotify.read_events_blocking(buffer) { - Ok(events) => Ok(Some(events)), - Err(e) => { - if e.kind() == ErrorKind::Interrupted { - Ok(None) - } else { - Err(e.into()) - } - } - } - } - - fn process_events(&self, events: Events) { - for ev in events { - if let Err(e) = self.handle_event(&ev) { - info!("error handling inotify event: {}", e); - } - } - } - - fn run(&mut self) -> Result<()> { - info!("running monitor event loop"); - self.add_watches()?; - let mut buffer = [0u8; 4096]; - loop { - match self.read_events(&mut buffer)? { - Some(events) => self.process_events(events), - None => break, - } - } - Ok(()) - } - - fn full_event_path(&self, ev: &Event<&OsStr>) -> Result { - let filename = ev.name - .ok_or(format_err!("inotify event received without a filename"))?; - let path = self.descriptors.get(&ev.wd) - .ok_or(format_err!("Failed to find descriptor for received inotify event"))?; - Ok(path.join(filename)) - } - - fn handle_event(&self, ev: &Event<&OsStr>) -> Result<()> { - let handler = self.handler.lock().unwrap(); - let pb = self.full_event_path(ev)?; - let path = pb.as_path(); - let is_create = ev.mask.intersects(EventMask::CREATE|EventMask::MOVED_TO); - if !is_create && !ev.mask.contains(EventMask::DELETE) { - return Err(format_err!("Unexpected mask value for inotify event: {:?}", ev.mask)); - } - - if ev.mask.contains(EventMask::ISDIR) { - if is_create { - handler.directory_added(path)?; - } else { - handler.directory_removed(path)?; - } - - } else { - if is_create { - handler.file_added(path)?; - } else { - handler.file_removed(path)?; - } - } - Ok(()) - } -} - -pub struct WorkerHandle { - join_handle: JoinHandle<()>, - exit_flag: Arc, -} - -impl WorkerHandle { - fn new(join_handle: JoinHandle<()>, exit_flag: Arc) -> WorkerHandle { - WorkerHandle { join_handle, exit_flag } - } - - pub fn stop(&self) { - info!("calling stop on monitor"); - let tid = self.join_handle.as_pthread_t(); - self.exit_flag.store(true, Ordering::Relaxed); - unsafe { - libc::pthread_kill(tid, signal::SIGUSR1 as libc::c_int); - } - } - - pub fn wait(self) { - if let Err(e) = self.join_handle.join() { - warn!("monitor thread panic with '{:?}'", e); - } - } -} - -static INITIALIZE_ONCE: Once = ONCE_INIT; - -fn initialize() { - INITIALIZE_ONCE.call_once(|| { - let h = signal::SigHandler::Handler(sighandler); - let sa = signal::SigAction::new(h, signal::SaFlags::empty(), signal::SigSet::empty()); - if let Err(e) = unsafe { signal::sigaction(signal::SIGUSR1, &sa) } { - warn!("Error setting signal handler: {}", e); - } - }); -} - -extern fn sighandler(_: libc::c_int) { - // do nothing, signal is only used to EINTR blocking inotify call -} \ No newline at end of file diff --git a/citadel-desktopd/src/desktop.rs b/citadel-tool/src/sync/desktop_file.rs similarity index 96% rename from citadel-desktopd/src/desktop.rs rename to citadel-tool/src/sync/desktop_file.rs index 96a8ffa..7c5d34f 100644 --- a/citadel-desktopd/src/desktop.rs +++ b/citadel-tool/src/sync/desktop_file.rs @@ -2,7 +2,8 @@ use std::io::Write; use std::fs::File; use std::path::Path; use std::collections::HashMap; -use crate::Result; + +use libcitadel::Result; pub struct DesktopFile { @@ -18,8 +19,7 @@ pub struct DesktopFile { impl DesktopFile { pub fn write_to_dir>(&self, directory: P) -> Result<()> { - let mut path = directory.as_ref().to_path_buf(); - path.push(self.filename.as_str()); + let path = directory.as_ref().join(&self.filename); let f = File::create(&path)?; self.write_to(f)?; Ok(()) @@ -61,6 +61,10 @@ impl DesktopFile { false } + pub fn icon(&self) -> Option<&str> { + self.get_key_val("Icon") + } + fn show_in_gnome(&self) -> bool { if self.key_exists("NotShowIn") && self.key_value_contains("NotShowIn", "GNOME") { return false; @@ -105,7 +109,6 @@ impl DesktopFile { None } - pub fn new(filename: &str) -> DesktopFile { DesktopFile { filename: filename.to_string(), @@ -131,8 +134,6 @@ impl DesktopFile { } self.lines.push(line); } - - } diff --git a/citadel-tool/src/sync/icon_cache.rs b/citadel-tool/src/sync/icon_cache.rs new file mode 100644 index 0000000..f2c34ba --- /dev/null +++ b/citadel-tool/src/sync/icon_cache.rs @@ -0,0 +1,91 @@ +use std::fs::File; +use std::path::Path; +use std::str; +use std::os::unix::fs::FileExt; + +use byteorder::{ByteOrder,BE}; +use libcitadel::Result; + +pub struct IconCache { + file: File, +} + +impl IconCache { + + pub fn open>(path: P) -> Result { + let file = File::open(path.as_ref())?; + Ok(IconCache { file }) + } + + pub fn find_image(&self, icon_name: &str) -> Result { + let hash_offset = self.read_offset(4)?; + let nbuckets = self.read_u32(hash_offset)?; + + let hash = Self::icon_name_hash(icon_name) % nbuckets; + let mut chain_offset = self.read_offset(hash_offset + 4 + (4 * hash as usize))?; + while chain_offset != u32::max_value() as usize { + let name_offset = self.read_offset(chain_offset + 4)?; + chain_offset = self.read_offset(chain_offset)?; + let name = self.read_string(name_offset)?; + if name == icon_name { + return Ok(true); + } + } + Ok(false) + } + + fn icon_name_hash(key: &str) -> u32 { + key.bytes().fold(0u32, |h, b| + (h << 5) + .wrapping_sub(h) + .wrapping_add(b as u32)) + } + + fn read_string(&self, offset: usize) -> Result { + let mut buf = [0u8; 128]; + let mut output = String::new(); + let mut nread = 0; + loop { + let n = self.file.read_at(&mut buf, (offset + nread)as u64 )?; + if n == 0 { + return Ok(output); + } + nread += n; + + if let Some(idx) = Self::null_index(&buf[0..n]) { + if let Ok(s) = str::from_utf8(&buf[..idx]) { + output.push_str(s); + } + return Ok(output) + } + output.push_str(str::from_utf8(&buf).unwrap()); + } + } + + fn null_index(buffer: &[u8]) -> Option { + buffer.iter().enumerate().find(|(_,b)| **b == 0).map(|(idx,_)| idx) + } + + fn read_offset(&self, offset: usize) -> Result { + let offset = self.read_u32(offset)? as usize; + Ok(offset as usize) + } + + fn read_u32(&self, offset: usize) -> Result { + let mut buf = [0u8; 4]; + self.read_exact_at(&mut buf, offset)?; + Ok(BE::read_u32(&buf)) + } + + fn read_exact_at(&self, buf: &mut [u8], offset: usize) -> Result<()> { + let mut nread = 0; + while nread < buf.len() { + let sz = self.file.read_at(&mut buf[nread..], (offset + nread) as u64)?; + nread += sz; + if sz == 0 { + bail!("bad offset"); + } + } + Ok(()) + } +} diff --git a/citadel-tool/src/sync/icons.rs b/citadel-tool/src/sync/icons.rs new file mode 100644 index 0000000..3978a6a --- /dev/null +++ b/citadel-tool/src/sync/icons.rs @@ -0,0 +1,121 @@ +use crate::sync::icon_cache::IconCache; +use std::collections::HashSet; +use std::fs; +use std::path::Path; + +use libcitadel::{Result, Realms}; +use std::cell::{RefCell, Cell}; + +pub struct IconSync { + cache: IconCache, + known: RefCell>, + known_changed: Cell, +} + +impl IconSync { + const CITADEL_ICONS: &'static str = "/home/citadel/.local/share/icons"; + const KNOWN_ICONS_FILE: &'static str = "/home/citadel/.local/share/icons/known.cache"; + const PAPER_ICON_CACHE: &'static str = "/usr/share/icons/Paper/icon-theme.cache"; + + pub fn new() -> Result { + let cache = IconCache::open(Self::PAPER_ICON_CACHE)?; + let known = Self::read_known_cache()?; + let known = RefCell::new(known); + let known_changed = Cell::new(false); + Ok(IconSync { cache, known, known_changed }) + } + + pub fn sync_icon(&self, icon_name: &str) -> Result<()> { + if self.is_known(icon_name) { + return Ok(()) + } + if self.cache.find_image(icon_name)? { + debug!("found {} in cache", icon_name); + self.add_known(icon_name); + return Ok(()); + } + + if !self.search("rootfs/usr/share/icons/hicolor", icon_name)? { + self.search("home/.local/share/icons/hicolor", icon_name)?; + } + Ok(()) + } + + fn add_known(&self, icon_name: &str) { + self.known.borrow_mut().insert(icon_name.to_string()); + self.known_changed.set(true); + } + + fn is_known(&self, icon_name: &str) -> bool { + self.known.borrow().contains(icon_name) + } + + pub fn write_known_cache(&self) -> Result<()> { + if !self.known_changed.get() { + return Ok(()) + } + let mut names: Vec = self.known.borrow().iter().map(|s| s.to_string()).collect(); + names.sort_unstable(); + let out = names.join("\n") + "\n"; + fs::write(Self::KNOWN_ICONS_FILE, out)?; + Ok(()) + } + + fn read_known_cache() -> Result> { + let target = Path::new(Self::KNOWN_ICONS_FILE); + if target.exists() { + let content = fs::read_to_string(target)?; + Ok(content.lines().map(|s| s.to_string()).collect()) + } else { + Ok(HashSet::new()) + } + } + + fn search(&self, subdir: impl AsRef, icon_name: &str) -> Result { + let base = Realms::current_realm_symlink().join(subdir.as_ref()); + if !base.exists() { + return Ok(false) + } + let mut found = false; + for entry in fs::read_dir(&base)? { + let entry = entry?; + let apps = entry.path().join("apps"); + if apps.exists() { + if self.search_subdirectory(&base, &apps, icon_name)? { + found = true; + } + } + } + if found { + self.add_known(icon_name); + } + Ok(found) + } + + fn search_subdirectory(&self, base: &Path, subdir: &Path, icon_name: &str) -> Result { + let mut found = false; + for entry in fs::read_dir(subdir)? { + let entry = entry?; + let path = entry.path(); + if let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) { + if stem == icon_name { + self.copy_icon_file(base, &path)?; + found = true; + } + } + } + Ok(found) + } + + fn copy_icon_file(&self, base: &Path, icon_path: &Path) -> Result<()> { + verbose!("copy icon file {}", icon_path.display()); + let stripped = icon_path.strip_prefix(base)?; + let target = Path::new(Self::CITADEL_ICONS).join("hicolor").join(stripped); + let parent = target.parent().unwrap(); + if !parent.exists() { + fs::create_dir_all(parent)?; + } + fs::copy(icon_path, target)?; + Ok(()) + } +} \ No newline at end of file diff --git a/citadel-tool/src/sync/mod.rs b/citadel-tool/src/sync/mod.rs new file mode 100644 index 0000000..2aa0631 --- /dev/null +++ b/citadel-tool/src/sync/mod.rs @@ -0,0 +1,27 @@ +use libcitadel::{Result, Logger, LogLevel}; + +mod desktop_file; +mod parser; +mod sync; +mod icons; +mod icon_cache; + +use self::sync::DesktopFileSync; + +pub fn main(args: Vec) { + + Logger::set_log_level(LogLevel::Debug); + let clear = args.len() > 1 && args[1].as_str() == "--clear"; + + if let Err(e) = sync(clear) { + println!("Desktop file sync failed: {}", e); + } +} + +fn sync(clear: bool) -> Result<()> { + if let Some(mut sync) = DesktopFileSync::new_current() { + sync.run_sync(clear) + } else { + DesktopFileSync::clear_target_files() + } +} \ No newline at end of file diff --git a/citadel-desktopd/src/parser.rs b/citadel-tool/src/sync/parser.rs similarity index 99% rename from citadel-desktopd/src/parser.rs rename to citadel-tool/src/sync/parser.rs index ba0f27f..ce65e55 100644 --- a/citadel-desktopd/src/parser.rs +++ b/citadel-tool/src/sync/parser.rs @@ -1,10 +1,10 @@ - use std::io::Read; use std::fs::File; use std::path::Path; use std::collections::HashSet; -use crate::desktop::{DesktopFile,Line}; -use crate::Result; + +use libcitadel::Result; +use crate::sync::desktop_file::{DesktopFile,Line}; lazy_static! { // These are the keys which are copied into the translated .desktop files diff --git a/citadel-tool/src/sync/sync.rs b/citadel-tool/src/sync/sync.rs new file mode 100644 index 0000000..d7815df --- /dev/null +++ b/citadel-tool/src/sync/sync.rs @@ -0,0 +1,166 @@ +use std::collections::HashSet; +use std::ffi::{OsStr,OsString}; +use std::fs; +use std::path::{Path,PathBuf}; +use std::time::SystemTime; + +use libcitadel::{Realm,Realms,Result}; +use crate::sync::parser::DesktopFileParser; +use std::fs::DirEntry; +use crate::sync::icons::IconSync; + +/// Synchronize dot-desktop files from active realm to a target directory in Citadel. +pub struct DesktopFileSync { + realm: Realm, + items: HashSet, + icons: Option, +} + +#[derive(Eq,PartialEq,Hash)] +struct DesktopItem { + path: PathBuf, + mtime: SystemTime, +} + +impl DesktopItem { + + fn new(path: PathBuf, mtime: SystemTime) -> Self { + DesktopItem { path, mtime } + } + + fn filename(&self) -> &OsStr { + self.path.file_name() + .expect("DesktopItem does not have a filename") + } + + fn is_newer_than(&self, path: &Path) -> bool { + if let Some(mtime) = DesktopFileSync::mtime(path) { + self.mtime > mtime + } else { + true + } + } +} + +impl DesktopFileSync { + pub const CITADEL_APPLICATIONS: &'static str = "/home/citadel/.local/share/applications"; + + pub fn new_current() -> Option { + Realms::load_current_realm() + .filter(|r| r.is_active()) + .map(Self::new) + } + + pub fn new(realm: Realm) -> Self { + let icons = match IconSync::new() { + Ok(icons) => Some(icons), + Err(e) => { + warn!("Error creating IconSync: {}", e); + None + } + }; + DesktopFileSync { realm, items: HashSet::new(), icons } + } + + pub fn run_sync(&mut self, clear: bool) -> Result<()> { + + self.collect_source_files("rootfs/usr/share/applications")?; + self.collect_source_files("home/.local/share/applications")?; + + let target = Path::new(Self::CITADEL_APPLICATIONS); + + if !target.exists() { + fs::create_dir_all(&target)?; + } + + if clear { + Self::clear_target_files()?; + } else { + self.remove_missing_target_files()?; + } + + self.synchronize_items()?; + if let Some(ref icons) = self.icons { + icons.write_known_cache()?; + } + Ok(()) + } + + fn collect_source_files(&mut self, directory: impl AsRef) -> Result<()> { + let directory = Realms::current_realm_symlink().join(directory.as_ref()); + if directory.exists() { + for entry in fs::read_dir(directory)? { + self.process_source_entry(entry?); + } + } + Ok(()) + } + + fn process_source_entry(&mut self, entry: DirEntry) { + let path = entry.path(); + if path.extension() == Some(OsStr::new("desktop")) { + if let Some(mtime) = Self::mtime(&path) { + self.items.insert(DesktopItem::new(path, mtime)); + } + } + } + + pub fn clear_target_files() -> Result<()> { + for entry in fs::read_dir(Self::CITADEL_APPLICATIONS)? { + let entry = entry?; + fs::remove_file(entry.path())?; + } + Ok(()) + } + + fn remove_missing_target_files(&mut self) -> Result<()> { + let sources = self.source_filenames(); + for entry in fs::read_dir(Self::CITADEL_APPLICATIONS)? { + let entry = entry?; + if !sources.contains(&entry.file_name()) { + let path = entry.path(); + verbose!("Removing desktop entry that no longer exists: {:?}", path); + fs::remove_file(path)?; + } + } + Ok(()) + } + + fn mtime(path: &Path) -> Option { + path.metadata().and_then(|meta| meta.modified()).ok() + } + + fn source_filenames(&self) -> HashSet { + self.items.iter() + .flat_map(|item| item.path.file_name()) + .map(|s| s.to_os_string()) + .collect() + } + + fn synchronize_items(&self) -> Result<()> { + for item in &self.items { + let target = Path::new(Self::CITADEL_APPLICATIONS).join(item.filename()); + if item.is_newer_than(&target) { + if let Err(e) = self.sync_item(item) { + warn!("Error synchronzing desktop file {:?} from realm-{}: {}", item.filename(), self.realm.name(), e); + } + } + } + Ok(()) + } + + fn sync_item(&self, item: &DesktopItem) -> Result<()> { + let dfp = DesktopFileParser::parse_from_path(&item.path, "/usr/libexec/citadel-run ")?; + if dfp.is_showable() { + dfp.write_to_dir(Self::CITADEL_APPLICATIONS)?; + if let Some(icon_name)= dfp.icon() { + if let Some(ref icons) = self.icons { + icons.sync_icon(icon_name)?; + } + } + } else { + debug!("Ignoring desktop file {} as not showable", dfp.filename()); + } + Ok(()) + } +} diff --git a/systemd/citadel-current-watcher.path b/systemd/citadel-current-watcher.path new file mode 100644 index 0000000..6a1e5a0 --- /dev/null +++ b/systemd/citadel-current-watcher.path @@ -0,0 +1,9 @@ +[Unit] +Description=Current realm directory watcher +Before=launch-default-realm.service + +[Path] +PathChanged=/run/citadel/realms/current + +[Install] +WantedBy=multi-user.target diff --git a/systemd/citadel-current-watcher.service b/systemd/citadel-current-watcher.service new file mode 100644 index 0000000..270ff2d --- /dev/null +++ b/systemd/citadel-current-watcher.service @@ -0,0 +1,8 @@ +[Unit] +Description=Current Realm Directory Watcher +StartLimitIntervalSec=0 + +[Service] +Type=oneshot +ExecStart=/usr/libexec/citadel-desktop-sync --clear +ExecStart=/usr/bin/systemctl restart citadel-desktop-watcher.path diff --git a/systemd/citadel-desktop-watcher.path b/systemd/citadel-desktop-watcher.path new file mode 100644 index 0000000..36b09dd --- /dev/null +++ b/systemd/citadel-desktop-watcher.path @@ -0,0 +1,7 @@ +[Unit] +Description=Desktop File Watcher +StartLimitIntervalSec=0 + +[Path] +PathChanged=/run/citadel/realms/current/current.realm/rootfs/usr/share/applications +PathChanged=/run/citadel/realms/current/current.realm/home/.local/share/applications diff --git a/systemd/citadel-desktop-watcher.service b/systemd/citadel-desktop-watcher.service new file mode 100644 index 0000000..7f04ab7 --- /dev/null +++ b/systemd/citadel-desktop-watcher.service @@ -0,0 +1,7 @@ +[Unit] +Description=Desktop Watcher +StartLimitIntervalSec=0 + +[Service] +Type=oneshot +ExecStart=/usr/libexec/citadel-desktop-sync