citadel-desktopd replaced with citadel-desktop-sync util
This commit is contained in:
parent
ce10df3dfc
commit
fff6ddb15a
@ -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"
|
|
@ -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"
|
|
@ -1,5 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Desktop Integration Manager
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
ExecStart=/usr/libexec/citadel-desktopd /usr/share/citadel/citadel-desktopd.conf
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -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")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -2,7 +2,8 @@ use std::io::Write;
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use crate::Result;
|
|
||||||
|
use libcitadel::Result;
|
||||||
|
|
||||||
|
|
||||||
pub struct DesktopFile {
|
pub struct DesktopFile {
|
||||||
@ -18,8 +19,7 @@ pub struct DesktopFile {
|
|||||||
impl DesktopFile {
|
impl DesktopFile {
|
||||||
|
|
||||||
pub fn write_to_dir<P: AsRef<Path>>(&self, directory: P) -> Result<()> {
|
pub fn write_to_dir<P: AsRef<Path>>(&self, directory: P) -> Result<()> {
|
||||||
let mut path = directory.as_ref().to_path_buf();
|
let path = directory.as_ref().join(&self.filename);
|
||||||
path.push(self.filename.as_str());
|
|
||||||
let f = File::create(&path)?;
|
let f = File::create(&path)?;
|
||||||
self.write_to(f)?;
|
self.write_to(f)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -61,6 +61,10 @@ impl DesktopFile {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn icon(&self) -> Option<&str> {
|
||||||
|
self.get_key_val("Icon")
|
||||||
|
}
|
||||||
|
|
||||||
fn show_in_gnome(&self) -> bool {
|
fn show_in_gnome(&self) -> bool {
|
||||||
if self.key_exists("NotShowIn") && self.key_value_contains("NotShowIn", "GNOME") {
|
if self.key_exists("NotShowIn") && self.key_value_contains("NotShowIn", "GNOME") {
|
||||||
return false;
|
return false;
|
||||||
@ -105,7 +109,6 @@ impl DesktopFile {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn new(filename: &str) -> DesktopFile {
|
pub fn new(filename: &str) -> DesktopFile {
|
||||||
DesktopFile {
|
DesktopFile {
|
||||||
filename: filename.to_string(),
|
filename: filename.to_string(),
|
||||||
@ -131,8 +134,6 @@ impl DesktopFile {
|
|||||||
}
|
}
|
||||||
self.lines.push(line);
|
self.lines.push(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
91
citadel-tool/src/sync/icon_cache.rs
Normal file
91
citadel-tool/src/sync/icon_cache.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
121
citadel-tool/src/sync/icons.rs
Normal file
121
citadel-tool/src/sync/icons.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
27
citadel-tool/src/sync/mod.rs
Normal file
27
citadel-tool/src/sync/mod.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use crate::desktop::{DesktopFile,Line};
|
|
||||||
use crate::Result;
|
use libcitadel::Result;
|
||||||
|
use crate::sync::desktop_file::{DesktopFile,Line};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
// These are the keys which are copied into the translated .desktop files
|
// These are the keys which are copied into the translated .desktop files
|
166
citadel-tool/src/sync/sync.rs
Normal file
166
citadel-tool/src/sync/sync.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
9
systemd/citadel-current-watcher.path
Normal file
9
systemd/citadel-current-watcher.path
Normal 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
|
8
systemd/citadel-current-watcher.service
Normal file
8
systemd/citadel-current-watcher.service
Normal 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
|
7
systemd/citadel-desktop-watcher.path
Normal file
7
systemd/citadel-desktop-watcher.path
Normal 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
|
7
systemd/citadel-desktop-watcher.service
Normal file
7
systemd/citadel-desktop-watcher.service
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Desktop Watcher
|
||||||
|
StartLimitIntervalSec=0
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/libexec/citadel-desktop-sync
|
Loading…
Reference in New Issue
Block a user