Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synchronize HTTP and D-Bus localization interfaces #1120

Merged
merged 10 commits into from
Apr 5, 2024
1 change: 1 addition & 0 deletions rust/agama-lib/src/localization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ mod settings;
mod store;

pub use client::LocalizationClient;
pub use proxies::LocaleProxy;
pub use settings::LocalizationSettings;
pub use store::LocalizationStore;
9 changes: 9 additions & 0 deletions rust/agama-lib/src/localization/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::error::ServiceError;
use zbus::Connection;

/// D-Bus client for the software service
#[derive(Clone)]
pub struct LocalizationClient<'a> {
localization_proxy: LocaleProxy<'a>,
}
Expand All @@ -22,6 +23,10 @@ impl<'a> LocalizationClient<'a> {
Ok(first)
}

pub async fn locales(&self) -> zbus::Result<Vec<String>> {
self.localization_proxy.locales().await
}

pub async fn keyboard(&self) -> Result<String, ServiceError> {
Ok(self.localization_proxy.keymap().await?)
}
Expand All @@ -35,6 +40,10 @@ impl<'a> LocalizationClient<'a> {
self.localization_proxy.set_locales(&locales).await
}

pub async fn set_locales(&self, locales: &[&str]) -> zbus::Result<()> {
self.localization_proxy.set_locales(locales).await
}

pub async fn set_keyboard(&self, keyboard: &str) -> zbus::Result<()> {
self.localization_proxy.set_keymap(keyboard).await
}
Expand Down
2 changes: 1 addition & 1 deletion rust/agama-lib/src/localization/proxies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ trait Locale {
/// UILocale property
#[dbus_proxy(property, name = "UILocale")]
fn uilocale(&self) -> zbus::Result<String>;
#[dbus_proxy(property)]
#[dbus_proxy(property, name = "UILocale")]
fn set_uilocale(&self, value: &str) -> zbus::Result<()>;
}
2 changes: 1 addition & 1 deletion rust/agama-server/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use axum::{
};
use serde_json::json;

use crate::{l10n::web::LocaleError, questions::QuestionsError};
use crate::{l10n::LocaleError, questions::QuestionsError};

#[derive(thiserror::Error, Debug)]
pub enum Error {
Expand Down
208 changes: 6 additions & 202 deletions rust/agama-server/src/l10n.rs
Original file line number Diff line number Diff line change
@@ -1,212 +1,16 @@
mod dbus;
pub mod error;
pub mod helpers;
mod keyboard;
pub mod l10n;
mod locale;
mod timezone;
pub mod web;

use crate::error::Error;
use agama_locale_data::{KeymapId, LocaleId};

use keyboard::KeymapsDatabase;
use locale::LocalesDatabase;
use regex::Regex;
use std::{io, process::Command};
use timezone::TimezonesDatabase;
use zbus::{dbus_interface, Connection};

pub use dbus::export_dbus_objects;
pub use error::LocaleError;
pub use keyboard::Keymap;
pub use l10n::L10n;
pub use locale::LocaleEntry;
pub use timezone::TimezoneEntry;
pub use web::LocaleConfig;

pub struct Locale {
timezone: String,
timezones_db: TimezonesDatabase,
locales: Vec<String>,
pub locales_db: LocalesDatabase,
keymap: KeymapId,
keymaps_db: KeymapsDatabase,
ui_locale: LocaleId,
pub ui_keymap: KeymapId,
}

#[dbus_interface(name = "org.opensuse.Agama1.Locale")]
impl Locale {
#[dbus_interface(property)]
fn locales(&self) -> Vec<String> {
self.locales.to_owned()
}

#[dbus_interface(property)]
fn set_locales(&mut self, locales: Vec<String>) -> zbus::fdo::Result<()> {
if locales.is_empty() {
return Err(zbus::fdo::Error::Failed(
"The locales list cannot be empty".to_string(),
));
}
for loc in &locales {
if !self.locales_db.exists(loc.as_str()) {
return Err(zbus::fdo::Error::Failed(format!(
"Unsupported locale value '{loc}'"
)));
}
}
self.locales = locales;
Ok(())
}

#[dbus_interface(property, name = "UILocale")]
fn ui_locale(&self) -> String {
self.ui_locale.to_string()
}

#[dbus_interface(property, name = "UILocale")]
fn set_ui_locale(&mut self, locale: &str) -> zbus::fdo::Result<()> {
let locale: LocaleId = locale
.try_into()
.map_err(|_e| zbus::fdo::Error::Failed(format!("Invalid locale value '{locale}'")))?;
helpers::set_service_locale(&locale);
Ok(self.translate(&locale)?)
}

#[dbus_interface(property)]
fn keymap(&self) -> String {
self.keymap.to_string()
}

#[dbus_interface(property)]
fn set_keymap(&mut self, keymap_id: &str) -> Result<(), zbus::fdo::Error> {
let keymap_id: KeymapId = keymap_id
.parse()
.map_err(|_e| zbus::fdo::Error::InvalidArgs("Cannot parse keymap ID".to_string()))?;

if !self.keymaps_db.exists(&keymap_id) {
return Err(zbus::fdo::Error::Failed(
"Cannot find this keymap".to_string(),
));
}
self.keymap = keymap_id;
Ok(())
}

#[dbus_interface(property)]
fn timezone(&self) -> &str {
self.timezone.as_str()
}

#[dbus_interface(property)]
fn set_timezone(&mut self, timezone: &str) -> Result<(), zbus::fdo::Error> {
let timezone = timezone.to_string();
if !self.timezones_db.exists(&timezone) {
return Err(zbus::fdo::Error::Failed(format!(
"Unsupported timezone value '{timezone}'"
)));
}
self.timezone = timezone;
Ok(())
}

// TODO: what should be returned value for commit?
fn commit(&mut self) -> zbus::fdo::Result<()> {
const ROOT: &str = "/mnt";

Command::new("/usr/bin/systemd-firstboot")
.args([
"--root",
ROOT,
"--force",
"--locale",
self.locales.first().unwrap_or(&"en_US.UTF-8".to_string()),
"--keymap",
&self.keymap.to_string(),
"--timezone",
&self.timezone,
])
.status()
.map_err(|e| {
zbus::fdo::Error::Failed(format!("Could not apply the l10n configuration: {e}"))
})?;

Ok(())
}
}

impl Locale {
pub fn new_with_locale(ui_locale: &LocaleId) -> Result<Self, Error> {
const DEFAULT_TIMEZONE: &str = "Europe/Berlin";

let locale = ui_locale.to_string();
let mut locales_db = LocalesDatabase::new();
locales_db.read(&locale)?;

let mut default_locale = ui_locale.to_string();
if !locales_db.exists(locale.as_str()) {
// TODO: handle the case where the database is empty (not expected!)
default_locale = locales_db.entries().first().unwrap().id.to_string();
};

let mut timezones_db = TimezonesDatabase::new();
timezones_db.read(&ui_locale.language)?;

let mut default_timezone = DEFAULT_TIMEZONE.to_string();
if !timezones_db.exists(&default_timezone) {
default_timezone = timezones_db.entries().first().unwrap().code.to_string();
};

let mut keymaps_db = KeymapsDatabase::new();
keymaps_db.read()?;

let ui_keymap = Self::x11_keymap().unwrap_or("us".to_string());

let locale = Self {
keymap: "us".parse().unwrap(),
timezone: default_timezone,
locales: vec![default_locale],
locales_db,
timezones_db,
keymaps_db,
ui_locale: ui_locale.clone(),
ui_keymap: ui_keymap.parse().unwrap_or_default(),
};

Ok(locale)
}

pub fn translate(&mut self, locale: &LocaleId) -> Result<(), Error> {
self.timezones_db.read(&locale.language)?;
self.locales_db.read(&locale.language)?;
self.ui_locale = locale.clone();
Ok(())
}

fn x11_keymap() -> Result<String, io::Error> {
let output = Command::new("setxkbmap")
.arg("-query")
.env("DISPLAY", ":0")
.output()?;
let output = String::from_utf8(output.stdout)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;

let keymap_regexp = Regex::new(r"(?m)^layout: (.+)$").unwrap();
let captures = keymap_regexp.captures(&output);
let keymap = captures
.and_then(|c| c.get(1).map(|e| e.as_str()))
.unwrap_or("us")
.to_string();

Ok(keymap)
}
}

pub async fn export_dbus_objects(
connection: &Connection,
locale: &LocaleId,
) -> Result<(), Box<dyn std::error::Error>> {
const PATH: &str = "/org/opensuse/Agama1/Locale";

// When serving, request the service name _after_ exposing the main object
let locale_iface = Locale::new_with_locale(locale)?;
connection.object_server().at(PATH, locale_iface).await?;

Ok(())
}
110 changes: 110 additions & 0 deletions rust/agama-server/src/l10n/dbus.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use std::sync::{Arc, RwLock};

use agama_locale_data::{KeymapId, LocaleId};
use zbus::{dbus_interface, Connection};

use super::L10n;

struct L10nInterface {
backend: Arc<RwLock<L10n>>,
}

#[dbus_interface(name = "org.opensuse.Agama1.Locale")]
impl L10nInterface {
#[dbus_interface(property)]
pub fn locales(&self) -> Vec<String> {
let backend = self.backend.read().unwrap();
backend.locales.to_owned()
}

#[dbus_interface(property)]
pub fn set_locales(&mut self, locales: Vec<String>) -> zbus::fdo::Result<()> {
let mut backend = self.backend.write().unwrap();
if locales.is_empty() {
return Err(zbus::fdo::Error::Failed(
"The locales list cannot be empty".to_string(),
));
}
backend.set_locales(&locales).map_err(|e| {
zbus::fdo::Error::Failed(format!("Could not set the locales: {}", e.to_string()))
})?;
Ok(())
}

#[dbus_interface(property, name = "UILocale")]
pub fn ui_locale(&self) -> String {
let backend = self.backend.read().unwrap();
backend.ui_locale.to_string()
}

#[dbus_interface(property, name = "UILocale")]
pub fn set_ui_locale(&mut self, locale: &str) -> zbus::fdo::Result<()> {
let mut backend = self.backend.write().unwrap();
let locale: LocaleId = locale
.try_into()
.map_err(|_e| zbus::fdo::Error::Failed(format!("Invalid locale value '{locale}'")))?;
Ok(backend.translate(&locale)?)
}

#[dbus_interface(property)]
pub fn keymap(&self) -> String {
let backend = self.backend.read().unwrap();
backend.keymap.to_string()
}

#[dbus_interface(property)]
fn set_keymap(&mut self, keymap_id: &str) -> Result<(), zbus::fdo::Error> {
let mut backend = self.backend.write().unwrap();
let keymap_id: KeymapId = keymap_id
.parse()
.map_err(|_e| zbus::fdo::Error::InvalidArgs("Cannot parse keymap ID".to_string()))?;

backend.set_keymap(keymap_id).map_err(|e| {
zbus::fdo::Error::Failed(format!("Could not set the keymap: {}", e.to_string()))
})?;

Ok(())
}

#[dbus_interface(property)]
pub fn timezone(&self) -> String {
let backend = self.backend.read().unwrap();
backend.timezone.clone()
}

#[dbus_interface(property)]
pub fn set_timezone(&mut self, timezone: &str) -> Result<(), zbus::fdo::Error> {
let mut backend = self.backend.write().unwrap();

backend.set_timezone(timezone).map_err(|e| {
zbus::fdo::Error::Failed(format!("Could not set the timezone: {}", e.to_string()))
})?;
Ok(())
}

// TODO: what should be returned value for commit?
pub fn commit(&mut self) -> zbus::fdo::Result<()> {
let backend = self.backend.read().unwrap();

backend.commit().map_err(|e| {
zbus::fdo::Error::Failed(format!("Could not apply the l10n configuration: {e}"))
})?;
Ok(())
}
}

pub async fn export_dbus_objects(
connection: &Connection,
locale: &LocaleId,
) -> Result<(), Box<dyn std::error::Error>> {
const PATH: &str = "/org/opensuse/Agama1/Locale";

// When serving, request the service name _after_ exposing the main object
let backend = L10n::new_with_locale(locale)?;
let locale_iface = L10nInterface {
backend: Arc::new(RwLock::new(backend)),
};
connection.object_server().at(PATH, locale_iface).await?;

Ok(())
}
15 changes: 15 additions & 0 deletions rust/agama-server/src/l10n/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use agama_locale_data::{InvalidKeymap, KeymapId};

#[derive(thiserror::Error, Debug)]
pub enum LocaleError {
#[error("Unknown locale code: {0}")]
UnknownLocale(String),
#[error("Unknown timezone: {0}")]
UnknownTimezone(String),
#[error("Unknown keymap: {0}")]
UnknownKeymap(KeymapId),
#[error("Invalid keymap: {0}")]
InvalidKeymap(#[from] InvalidKeymap),
#[error("Could not apply the changes")]
Commit(#[from] std::io::Error),
}
Loading
Loading