daemon: scaffolding for settings / sync
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -851,6 +851,7 @@ dependencies = [
|
|||||||
"kordophone",
|
"kordophone",
|
||||||
"kordophone-db",
|
"kordophone-db",
|
||||||
"log",
|
"log",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ env_logger = "0.11.6"
|
|||||||
kordophone = { path = "../kordophone" }
|
kordophone = { path = "../kordophone" }
|
||||||
kordophone-db = { path = "../kordophone-db" }
|
kordophone-db = { path = "../kordophone-db" }
|
||||||
log = "0.4.25"
|
log = "0.4.25"
|
||||||
|
thiserror = "2.0.12"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
||||||
<node name="/">
|
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||||
<interface name="net.buzzert.kordophone.Server">
|
<node>
|
||||||
|
<interface name="net.buzzert.kordophone.Repository">
|
||||||
<method name="GetVersion">
|
<method name="GetVersion">
|
||||||
<arg type="s" name="version" direction="out" />
|
<arg type="s" name="version" direction="out" />
|
||||||
</method>
|
</method>
|
||||||
@@ -15,5 +16,36 @@
|
|||||||
'is_unread' (boolean): Unread status"/>
|
'is_unread' (boolean): Unread status"/>
|
||||||
</arg>
|
</arg>
|
||||||
</method>
|
</method>
|
||||||
|
|
||||||
|
<method name="SyncAllConversations">
|
||||||
|
<arg type="b" name="success" direction="out" />
|
||||||
|
</method>
|
||||||
|
|
||||||
|
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="net.buzzert.kordophone.Settings">
|
||||||
|
<!-- editable properties -->
|
||||||
|
<property name="ServerURL" type="s" access="readwrite"/>
|
||||||
|
<property name="Username" type="s" access="readwrite"/>
|
||||||
|
|
||||||
|
<!-- Secret-Service handle (object path) -->
|
||||||
|
<property name="CredentialItem" type="o" access="readwrite">
|
||||||
|
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal"
|
||||||
|
value="true"/>
|
||||||
|
</property>
|
||||||
|
|
||||||
|
<!-- helpers for atomic updates -->
|
||||||
|
<method name="SetServer">
|
||||||
|
<arg name="url" type="s" direction="in"/>
|
||||||
|
<arg name="user" type="s" direction="in"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<method name="SetCredentialItem">
|
||||||
|
<arg name="item_path" type="o" direction="in"/>
|
||||||
|
</method>
|
||||||
|
|
||||||
|
<!-- emitted when anything changes -->
|
||||||
|
<signal name="ConfigChanged"/>
|
||||||
</interface>
|
</interface>
|
||||||
</node>
|
</node>
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use thiserror::Error;
|
||||||
use kordophone_db::{
|
use kordophone_db::{
|
||||||
database::Database,
|
database::Database,
|
||||||
settings::Settings,
|
|
||||||
models::Conversation,
|
models::Conversation,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use kordophone::api::http_client::HTTPAPIClient;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DaemonError {
|
||||||
|
#[error("Client Not Configured")]
|
||||||
|
ClientNotConfigured,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Daemon {
|
pub struct Daemon {
|
||||||
pub version: String,
|
pub version: String,
|
||||||
database: Database,
|
database: Database,
|
||||||
|
client: Option<HTTPAPIClient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Daemon {
|
impl Daemon {
|
||||||
@@ -23,17 +31,25 @@ impl Daemon {
|
|||||||
std::fs::create_dir_all(database_dir)?;
|
std::fs::create_dir_all(database_dir)?;
|
||||||
|
|
||||||
let database = Database::new(&database_path.to_string_lossy())?;
|
let database = Database::new(&database_path.to_string_lossy())?;
|
||||||
Ok(Self { version: "0.1.0".to_string(), database })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_version(&self) -> String {
|
// TODO: Check to see if we have client settings in the database
|
||||||
self.version.clone()
|
|
||||||
|
|
||||||
|
Ok(Self { version: "0.1.0".to_string(), database, client: None })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_conversations(&mut self) -> Vec<Conversation> {
|
pub fn get_conversations(&mut self) -> Vec<Conversation> {
|
||||||
self.database.with_repository(|r| r.all_conversations().unwrap())
|
self.database.with_repository(|r| r.all_conversations().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sync_all_conversations(&mut self) -> Result<()> {
|
||||||
|
let client = self.client
|
||||||
|
.as_mut()
|
||||||
|
.ok_or(DaemonError::ClientNotConfigured)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn get_database_path() -> PathBuf {
|
fn get_database_path() -> PathBuf {
|
||||||
if let Some(proj_dirs) = ProjectDirs::from("com", "kordophone", "kordophone") {
|
if let Some(proj_dirs) = ProjectDirs::from("com", "kordophone", "kordophone") {
|
||||||
let data_dir = proj_dirs.data_dir();
|
let data_dir = proj_dirs.data_dir();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use log::info;
|
use log::info;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use crate::{daemon::Daemon, dbus::interface};
|
|
||||||
|
|
||||||
use dbus_crossroads::Crossroads;
|
use dbus_crossroads::Crossroads;
|
||||||
use dbus_tokio::connection;
|
use dbus_tokio::connection;
|
||||||
@@ -11,13 +10,13 @@ use dbus::{
|
|||||||
Path,
|
Path,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Endpoint {
|
pub struct Endpoint<T: Send + Clone + 'static> {
|
||||||
connection: Arc<SyncConnection>,
|
connection: Arc<SyncConnection>,
|
||||||
daemon: Arc<Mutex<Daemon>>,
|
implementation: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Endpoint {
|
impl<T: Send + Clone + 'static> Endpoint<T> {
|
||||||
pub fn new(daemon: Daemon) -> Self {
|
pub fn new(implementation: T) -> Self {
|
||||||
let (resource, connection) = connection::new_session_sync().unwrap();
|
let (resource, connection) = connection::new_session_sync().unwrap();
|
||||||
|
|
||||||
// The resource is a task that should be spawned onto a tokio compatible
|
// The resource is a task that should be spawned onto a tokio compatible
|
||||||
@@ -31,15 +30,24 @@ impl Endpoint {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
connection,
|
connection,
|
||||||
daemon: Arc::new(Mutex::new(daemon))
|
implementation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(&self) {
|
pub async fn register<F, R>(
|
||||||
use crate::dbus::interface;
|
&self,
|
||||||
|
name: &str,
|
||||||
|
path: &str,
|
||||||
|
register_fn: F
|
||||||
|
)
|
||||||
|
where
|
||||||
|
F: Fn(&mut Crossroads) -> R,
|
||||||
|
R: IntoIterator<Item = dbus_crossroads::IfaceToken<T>>,
|
||||||
|
{
|
||||||
|
let dbus_path = String::from(path);
|
||||||
|
|
||||||
self.connection
|
self.connection
|
||||||
.request_name(interface::NAME, false, true, false)
|
.request_name(name, false, true, false)
|
||||||
.await
|
.await
|
||||||
.expect("Unable to acquire dbus name");
|
.expect("Unable to acquire dbus name");
|
||||||
|
|
||||||
@@ -54,9 +62,9 @@ impl Endpoint {
|
|||||||
}),
|
}),
|
||||||
)));
|
)));
|
||||||
|
|
||||||
// Register the daemon as a D-Bus object.
|
// Register the daemon as a D-Bus object with multiple interfaces
|
||||||
let token = interface::register_net_buzzert_kordophone_server(&mut cr);
|
let tokens: Vec<_> = register_fn(&mut cr).into_iter().collect();
|
||||||
cr.insert(interface::OBJECT_PATH, &[token], self.daemon.clone());
|
cr.insert(dbus_path, &tokens, self.implementation.clone());
|
||||||
|
|
||||||
// Start receiving messages.
|
// Start receiving messages.
|
||||||
self.connection.start_receive(
|
self.connection.start_receive(
|
||||||
@@ -66,14 +74,14 @@ impl Endpoint {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
info!(target: "dbus", "DBus server started");
|
info!(target: "dbus", "Registered endpoint at {} with {} interfaces", path, tokens.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_signal<S>(&self, signal: S) -> Result<u32, ()>
|
pub fn send_signal<S>(&self, path: &str, signal: S) -> Result<u32, ()>
|
||||||
where
|
where
|
||||||
S: dbus::message::SignalArgs + dbus::arg::AppendAll,
|
S: dbus::message::SignalArgs + dbus::arg::AppendAll,
|
||||||
{
|
{
|
||||||
let message = signal.to_emit_message(&Path::new(interface::OBJECT_PATH).unwrap());
|
let message = signal.to_emit_message(&Path::new(path).unwrap());
|
||||||
self.connection.send(message)
|
self.connection.send(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
pub mod endpoint;
|
pub mod endpoint;
|
||||||
mod server_impl;
|
pub mod server_impl;
|
||||||
|
|
||||||
mod interface {
|
pub mod interface {
|
||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
|
|
||||||
pub const NAME: &str = "net.buzzert.kordophonecd";
|
pub const NAME: &str = "net.buzzert.kordophonecd";
|
||||||
pub const OBJECT_PATH: &str = "/net/buzzert/kordophonecd";
|
pub const OBJECT_PATH: &str = "/net/buzzert/kordophonecd/daemon";
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/kordophone-server.rs"));
|
include!(concat!(env!("OUT_DIR"), "/kordophone-server.rs"));
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,36 @@
|
|||||||
use dbus::arg;
|
use dbus::arg;
|
||||||
use dbus_tree::MethodErr;
|
use dbus_tree::MethodErr;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex, MutexGuard};
|
||||||
|
use log::info;
|
||||||
|
|
||||||
use crate::daemon::Daemon;
|
use crate::daemon::Daemon;
|
||||||
use crate::dbus::interface::NetBuzzertKordophoneServer as DbusServer;
|
use crate::dbus::interface::NetBuzzertKordophoneRepository as DbusRepository;
|
||||||
|
use crate::dbus::interface::NetBuzzertKordophoneSettings as DbusSettings;
|
||||||
|
|
||||||
impl DbusServer for Arc<Mutex<Daemon>> {
|
#[derive(Clone)]
|
||||||
|
pub struct ServerImpl {
|
||||||
|
daemon: Arc<Mutex<Daemon>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerImpl {
|
||||||
|
pub fn new(daemon: Arc<Mutex<Daemon>>) -> Self {
|
||||||
|
Self { daemon }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_daemon(&self) -> Result<MutexGuard<'_, Daemon>, MethodErr> {
|
||||||
|
self.daemon.lock().map_err(|_| MethodErr::failed("Failed to lock daemon"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DbusRepository for ServerImpl {
|
||||||
fn get_version(&mut self) -> Result<String, MethodErr> {
|
fn get_version(&mut self) -> Result<String, MethodErr> {
|
||||||
let daemon = self.lock().map_err(|_| MethodErr::failed("Failed to lock daemon"))?;
|
let daemon = self.get_daemon()?;
|
||||||
Ok(daemon.version.clone())
|
Ok(daemon.version.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_conversations(&mut self) -> Result<Vec<arg::PropMap>, dbus::MethodErr> {
|
fn get_conversations(&mut self) -> Result<Vec<arg::PropMap>, dbus::MethodErr> {
|
||||||
// Get a repository instance and use it to fetch conversations
|
// Get a repository instance and use it to fetch conversations
|
||||||
let mut daemon = self.lock().map_err(|_| MethodErr::failed("Failed to lock daemon"))?;
|
let mut daemon = self.get_daemon()?;
|
||||||
let conversations = daemon.get_conversations();
|
let conversations = daemon.get_conversations();
|
||||||
|
|
||||||
// Convert conversations to DBus property maps
|
// Convert conversations to DBus property maps
|
||||||
@@ -27,4 +44,49 @@ impl DbusServer for Arc<Mutex<Daemon>> {
|
|||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
fn sync_all_conversations(&mut self) -> Result<bool, dbus::MethodErr> {
|
||||||
|
let mut daemon = self.get_daemon()?;
|
||||||
|
daemon.sync_all_conversations().map_err(|e| {
|
||||||
|
log::error!("Failed to sync conversations: {}", e);
|
||||||
|
MethodErr::failed(&format!("Failed to sync conversations: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DbusSettings for ServerImpl {
|
||||||
|
fn set_server(&mut self, url: String, user: String) -> Result<(), dbus::MethodErr> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_credential_item_(&mut self, item_path: dbus::Path<'static>) -> Result<(), dbus::MethodErr> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_url(&self) -> Result<String, dbus::MethodErr> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_server_url(&self, value: String) -> Result<(), dbus::MethodErr> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn username(&self) -> Result<String, dbus::MethodErr> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_username(&self, value: String) -> Result<(), dbus::MethodErr> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn credential_item(&self) -> Result<dbus::Path<'static>, dbus::MethodErr> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_credential_item(&self, value: dbus::Path<'static>) -> Result<(), dbus::MethodErr> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ mod dbus;
|
|||||||
mod daemon;
|
mod daemon;
|
||||||
|
|
||||||
use std::future;
|
use std::future;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
|
|
||||||
use daemon::Daemon;
|
use daemon::Daemon;
|
||||||
use dbus::endpoint::Endpoint as DbusEndpoint;
|
use dbus::endpoint::Endpoint as DbusEndpoint;
|
||||||
|
use dbus::interface;
|
||||||
|
use dbus::server_impl::ServerImpl;
|
||||||
|
|
||||||
fn initialize_logging() {
|
fn initialize_logging() {
|
||||||
env_logger::Builder::from_default_env()
|
env_logger::Builder::from_default_env()
|
||||||
@@ -19,16 +22,32 @@ async fn main() {
|
|||||||
initialize_logging();
|
initialize_logging();
|
||||||
|
|
||||||
// Create the daemon
|
// Create the daemon
|
||||||
let daemon = Daemon::new()
|
let daemon = Arc::new(
|
||||||
.map_err(|e| {
|
Mutex::new(
|
||||||
log::error!("Failed to start daemon: {}", e);
|
Daemon::new()
|
||||||
std::process::exit(1);
|
.map_err(|e| {
|
||||||
})
|
log::error!("Failed to start daemon: {}", e);
|
||||||
.unwrap();
|
std::process::exit(1);
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Create the D-Bus endpoint
|
// Create the server implementation
|
||||||
let endpoint = DbusEndpoint::new(daemon);
|
let server = ServerImpl::new(daemon);
|
||||||
endpoint.start().await;
|
|
||||||
|
// Register DBus interfaces with endpoint
|
||||||
|
let endpoint = DbusEndpoint::new(server.clone());
|
||||||
|
endpoint.register(
|
||||||
|
interface::NAME,
|
||||||
|
interface::OBJECT_PATH,
|
||||||
|
|cr| {
|
||||||
|
vec![
|
||||||
|
interface::register_net_buzzert_kordophone_repository(cr),
|
||||||
|
interface::register_net_buzzert_kordophone_settings(cr)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
).await;
|
||||||
|
|
||||||
future::pending::<()>().await;
|
future::pending::<()>().await;
|
||||||
unreachable!()
|
unreachable!()
|
||||||
|
|||||||
@@ -10,10 +10,16 @@ mod dbus_interface {
|
|||||||
include!(concat!(env!("OUT_DIR"), "/kordophone-client.rs"));
|
include!(concat!(env!("OUT_DIR"), "/kordophone-client.rs"));
|
||||||
}
|
}
|
||||||
|
|
||||||
use dbus_interface::NetBuzzertKordophoneServer as KordophoneServer;
|
use dbus_interface::NetBuzzertKordophoneRepository as KordophoneRepository;
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
|
/// Gets all known conversations.
|
||||||
|
Conversations,
|
||||||
|
|
||||||
|
/// Runs a sync operation.
|
||||||
|
Sync,
|
||||||
|
|
||||||
/// Prints the server Kordophone version.
|
/// Prints the server Kordophone version.
|
||||||
Version,
|
Version,
|
||||||
}
|
}
|
||||||
@@ -23,6 +29,8 @@ impl Commands {
|
|||||||
let mut client = DaemonCli::new()?;
|
let mut client = DaemonCli::new()?;
|
||||||
match cmd {
|
match cmd {
|
||||||
Commands::Version => client.print_version().await,
|
Commands::Version => client.print_version().await,
|
||||||
|
Commands::Conversations => client.print_conversations().await,
|
||||||
|
Commands::Sync => client.sync_conversations().await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,8 +51,20 @@ impl DaemonCli {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn print_version(&mut self) -> Result<()> {
|
pub async fn print_version(&mut self) -> Result<()> {
|
||||||
let version = KordophoneServer::get_version(&self.proxy())?;
|
let version = KordophoneRepository::get_version(&self.proxy())?;
|
||||||
println!("Server version: {}", version);
|
println!("Server version: {}", version);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn print_conversations(&mut self) -> Result<()> {
|
||||||
|
let conversations = KordophoneRepository::get_conversations(&self.proxy())?;
|
||||||
|
println!("Conversations: {:?}", conversations);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn sync_conversations(&mut self) -> Result<()> {
|
||||||
|
let success = KordophoneRepository::sync_all_conversations(&self.proxy())?;
|
||||||
|
println!("Synced conversations: {}", success);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -48,6 +48,6 @@ async fn main() {
|
|||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
run_command(cli.command).await
|
run_command(cli.command).await
|
||||||
.map_err(|e| log::error!("Error: {}", e))
|
.map_err(|e| println!("Error: {}", e))
|
||||||
.err();
|
.err();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user