Private
Public Access
1
0

daemon: scaffolding for settings / sync

This commit is contained in:
2025-04-25 18:02:54 -07:00
parent 0c6b55fa38
commit fe32efef2c
10 changed files with 204 additions and 45 deletions

1
Cargo.lock generated
View File

@@ -851,6 +851,7 @@ dependencies = [
"kordophone", "kordophone",
"kordophone-db", "kordophone-db",
"log", "log",
"thiserror",
"tokio", "tokio",
] ]

View File

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

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

@@ -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!()

View File

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

View File

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