1
0
forked from brl/citadel-tools

citadel-desktopd replaced with citadel-desktop-sync util

This commit is contained in:
Bruce Leidl 2019-04-02 15:22:55 -04:00
parent ce10df3dfc
commit fff6ddb15a
17 changed files with 446 additions and 540 deletions

View File

@ -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"

View File

@ -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"

View File

@ -1,5 +0,0 @@
[Unit]
Description=Desktop Integration Manager
[Service]
ExecStart=/usr/libexec/citadel-desktopd /usr/share/citadel/citadel-desktopd.conf

View File

@ -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<PathBuf>,
}
impl Config {
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Config> {
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<PathBuf> {
self.source_paths.as_ref()
}
}
#[derive(Deserialize)]
struct ConfigToml {
options: Option<Options>,
sources: Option<Vec<Source>>,
}
#[derive(Deserialize)]
struct Options {
exec_prefix: Option<String>,
target_directory: Option<String>,
}
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<String>,
}
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<String> {
let mut f = File::open(path)?;
let mut buffer = String::new();
f.read_to_string(&mut buffer)?;
Ok(buffer)
}
impl ConfigToml {
fn from_path<P: AsRef<Path>>(path: P) -> Result<ConfigToml> {
let s = load_as_string(path.as_ref())?;
let config = toml::from_str::<ConfigToml>(&s)?;
Ok(config)
}
fn options(&self) -> Result<&Options> {
self.options.as_ref()
.ok_or(format_err!("missing '[options]' section"))
}
fn sources(&self) -> Result<Vec<Source>> {
match self.sources {
Some(ref srcs) => Ok(srcs.clone()),
None => Err(format_err!("missing '[[sources]]' section(s)")),
}
}
fn to_config(&self) -> Result<Config> {
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,
})
}
}

View File

@ -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")),
}
}

View File

@ -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<T> = result::Result<T, Error>;
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));
}
}

View File

@ -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<Mutex<MonitorEventHandler>>,
worker_handle: Option<WorkerHandle>,
}
impl DirectoryMonitor {
pub fn new(handler: Arc<Mutex<MonitorEventHandler>>) -> 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<WatchDescriptor,PathBuf>,
inotify: Inotify,
exit_flag: Arc<AtomicBool>,
watch_paths: Vec<PathBuf>,
handler: Arc<Mutex<MonitorEventHandler>>,
}
impl MonitorWorker {
fn start_worker(watch_paths: Vec<PathBuf>, handler: Arc<Mutex<MonitorEventHandler>>) -> 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<PathBuf>, exit_flag: Arc<AtomicBool>, handler: Arc<Mutex<MonitorEventHandler>>) -> Result<MonitorWorker> {
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<Option<Events<'a>>> {
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<PathBuf> {
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<AtomicBool>,
}
impl WorkerHandle {
fn new(join_handle: JoinHandle<()>, exit_flag: Arc<AtomicBool>) -> 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
}

View File

@ -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<P: AsRef<Path>>(&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);
}
}

View File

@ -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<P: AsRef<Path>>(path: P) -> Result<Self> {
let file = File::open(path.as_ref())?;
Ok(IconCache { file })
}
pub fn find_image(&self, icon_name: &str) -> Result<bool> {
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<String> {
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<usize> {
buffer.iter().enumerate().find(|(_,b)| **b == 0).map(|(idx,_)| idx)
}
fn read_offset(&self, offset: usize) -> Result<usize> {
let offset = self.read_u32(offset)? as usize;
Ok(offset as usize)
}
fn read_u32(&self, offset: usize) -> Result<u32> {
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(())
}
}

View File

@ -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<HashSet<String>>,
known_changed: Cell<bool>,
}
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<Self> {
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<String> = 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<HashSet<String>> {
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<Path>, icon_name: &str) -> Result<bool> {
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<bool> {
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(())
}
}

View File

@ -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<String>) {
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()
}
}

View File

@ -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

View File

@ -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<DesktopItem>,
icons: Option<IconSync>,
}
#[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<Self> {
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<Path>) -> 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<SystemTime> {
path.metadata().and_then(|meta| meta.modified()).ok()
}
fn source_filenames(&self) -> HashSet<OsString> {
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(())
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,7 @@
[Unit]
Description=Desktop Watcher
StartLimitIntervalSec=0
[Service]
Type=oneshot
ExecStart=/usr/libexec/citadel-desktop-sync