blob: a65639e32c2e57bf13fd7dfede18dc908a87821f [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
anyhow::{Context as _, Error},
fidl::endpoints::RequestStream,
fidl_fuchsia_examples_intl_manager::{PropertyManagerRequest, PropertyManagerRequestStream},
fidl_fuchsia_intl::{
LocaleId, Profile, PropertyProviderControlHandle, PropertyProviderRequest,
PropertyProviderRequestStream, TimeZoneId,
},
fidl_fuchsia_intl_ext::CloneExt,
fuchsia_async as fasync,
fuchsia_component::server::{ServiceFs, ServiceObjLocal},
fuchsia_syslog::{self, fx_log_err, fx_log_info},
futures::{self, lock::Mutex, prelude::*},
std::{
collections::HashMap,
fmt::{self, Debug},
rc::Rc,
sync::{Arc, RwLock},
},
structopt::StructOpt,
};
static LOG_TAG: &str = "intl_property_manager";
static LOG_VERBOSITY: u16 = 1;
/// Convenience wrapper around `fx_vlog`, with a pre-set verbosity.
macro_rules! fx_log_verbose {
($($arg:tt)*) => {
fuchsia_syslog::fx_vlog!(LOG_VERBOSITY as i32, $($arg)*);
};
}
#[derive(Clone)]
struct Server(Arc<ServerState>);
struct ServerState {
/// Current profile being served.
profile: RwLock<Option<Profile>>,
/// Listeners of `OnChange` events.
listeners: Mutex<PropertyProviderListeners>,
}
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
struct PropertyProviderListenerKey(usize);
struct PropertyProviderListeners {
collection: HashMap<PropertyProviderListenerKey, PropertyProviderControlHandle>,
counter: usize,
}
impl PropertyProviderListeners {
fn new() -> PropertyProviderListeners {
PropertyProviderListeners { collection: HashMap::new(), counter: 0 }
}
fn add(&mut self, listener: PropertyProviderControlHandle) -> PropertyProviderListenerKey {
let key = PropertyProviderListenerKey(self.counter);
self.collection.insert(key, listener);
self.counter += 1;
key
}
fn remove(&mut self, key: PropertyProviderListenerKey) {
self.collection.remove(&key);
}
fn notify(&mut self) {
// Prune any listeners for which sending the event fails. This means they have
// disconnected.
self.collection.retain(|_, control_handle| match control_handle.send_on_change() {
Ok(_) => true,
Err(_) => false,
})
}
}
impl Server {
fn new(initial_profile: Option<Profile>) -> Self {
Server(Arc::new(ServerState {
profile: RwLock::new(initial_profile),
listeners: Mutex::new(PropertyProviderListeners::new()),
}))
}
/// Atomically set the profile being served. Returns `true` if the value is changed.
fn set_profile(&mut self, new_profile: Profile) -> bool {
let mut p = self.0.profile.write().unwrap();
let changed = match p.as_ref() {
Some(stored_profile) => (stored_profile != &new_profile),
None => true,
};
*p = Some(new_profile);
changed
}
/// Get a clone of the current `Profile`, or `None` if the profile hasn't been initialized.
fn get_profile(&self) -> Option<Profile> {
let p = self.0.profile.read().unwrap();
p.as_ref().map(CloneExt::clone)
}
/// Register a new listener for profile change events. Note that `PropertyProviderControlHandle`
/// contains an `Arc` and should be cloned before being passed to `add_listener`.
///
/// Returns a listener key.
async fn add_listener(
&mut self,
listener: PropertyProviderControlHandle,
) -> PropertyProviderListenerKey {
let fut = self.0.listeners.lock().map(|mut listeners| listeners.add(listener));
fut.await
}
/// Remove a registered listener by key.
async fn remove_listener(&mut self, key: PropertyProviderListenerKey) {
let fut = self.0.listeners.lock().map(|mut listeners| listeners.remove(key));
fut.await
}
/// Send `OnChange` event to registered listeners of `PropertyProvider`.
async fn notify_listeners(&mut self) {
fx_log_verbose!("Notifying listeners");
let fut = self.0.listeners.lock().map(|mut listeners| listeners.notify());
fut.await;
fx_log_verbose!("Notified listeners");
}
/// Entry point into the service. Register handlers for both of the protocols
/// (`PropertyProvider`, `PropertyManager`).
async fn run(&mut self, fs: ServiceFs<ServiceObjLocal<'static, Service>>) {
let self_ = Rc::new(self.clone());
fs.for_each_concurrent(None, move |service| self_.clone().handle_service_stream(service))
.await;
fx_log_verbose!("Registered services");
}
async fn handle_service_stream(self: Rc<Self>, service: Service) {
fx_log_verbose!("handle_service_stream: {:#?}", service);
let mut self_ = self.as_ref().clone();
match service {
Service::Provider(stream) => self_.run_provider(stream).await.unwrap_or_default(),
Service::Manager(stream) => self_.run_manager(stream).await.unwrap_or_default(),
}
}
/// Handle `PropertyProvider` requests as an infinite stream.
async fn run_provider(
&mut self,
mut stream: PropertyProviderRequestStream,
) -> Result<(), Error> {
let listener_key = self.add_listener(stream.control_handle().clone()).await;
while let Some(PropertyProviderRequest::GetProfile { responder }) =
stream.try_next().await.context("Error running property provider server")?
{
{
fx_log_verbose!("Received profile get request");
match self.get_profile() {
Some(profile) => {
responder.send(profile).context("Error sending response")?;
fx_log_verbose!("Sent profile");
}
None => {
fx_log_err!("Profile not initialized");
responder
.send(Profile {
locales: None,
time_zones: None,
calendars: None,
temperature_unit: None,
})
.context("Error sending response")?;
fx_log_verbose!("Sent empty profile");
}
}
}
}
// Don't leak listeners after they disconnect.
self.remove_listener(listener_key).await;
Ok(())
}
/// Handle `PropertyManager` requests as an infinite stream.
async fn run_manager(&mut self, mut stream: PropertyManagerRequestStream) -> Result<(), Error> {
while let Some(PropertyManagerRequest::SetProfile { intl_profile, responder }) =
stream.try_next().await.context("Error running property manager server")?
{
fx_log_verbose!("Received profile set request: {:#?}", &intl_profile);
let changed = self.set_profile(intl_profile);
responder.send().context("Error sending response")?;
fx_log_verbose!("Sent profile set response");
if changed {
self.notify_listeners().await;
}
}
Ok(())
}
}
enum Service {
Provider(PropertyProviderRequestStream),
Manager(PropertyManagerRequestStream),
}
/// Manual implementation because `__RequestStream` doesn't implement `Debug`.
impl Debug for Service {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
Service::Provider(_) => "Provider",
Service::Manager(_) => "Manager",
}
)
}
}
#[derive(Debug, StructOpt)]
#[structopt(
name = "intl property manager example",
about = "provides a test implementation of fuchsia.intl.ProfileProvider"
)]
struct Opts {
#[structopt(long)]
/// If set to `true`, the starting profile will be created based on the
/// flag settings like `--locale_ids=...`.
set_initial_profile: bool,
#[structopt(long, raw(use_delimiter = "true"))]
/// A list of comma-separated BCP-47 locale ID strings to serve initially, in the order of
/// priority.
locale_ids: Vec<String>,
#[structopt(long, raw(use_delimiter = "true"))]
/// A list of comma-separated BCP-47 timezone IDs (e.g. und-tz-usnyc) to serve initially, in
/// order of preference.
timezone_ids: Vec<String>,
}
impl From<&Opts> for Profile {
fn from(opts: &Opts) -> Self {
Profile {
locales: Some(
opts.locale_ids
.iter()
.map(|loc_id| LocaleId { id: String::from(loc_id) })
.collect(),
),
time_zones: Some(
opts.timezone_ids
.iter()
.map(|tz_id| TimeZoneId { id: String::from(tz_id) })
.collect(),
),
// TODO(fmil): Implement these too.
calendars: None,
temperature_unit: None,
}
}
}
fn initial_profile(opts: &Opts) -> Option<Profile> {
// Not sure if it is possible to evict the set_initial flag altogether.
match opts.set_initial_profile {
false => None,
true => {
let profile = opts.into();
fx_log_info!("Serving initial profile: {:?}", profile);
Some(profile)
}
}
}
#[fasync::run_singlethreaded]
async fn main() -> Result<(), Error> {
let opts = Opts::from_args();
fuchsia_syslog::init_with_tags(&[LOG_TAG])?;
fuchsia_syslog::set_severity(fuchsia_syslog::levels::INFO);
fuchsia_syslog::set_verbosity(LOG_VERBOSITY);
fx_log_info!("Launched component");
let mut fs = ServiceFs::new_local();
fs.dir("svc").add_fidl_service(Service::Provider).add_fidl_service(Service::Manager);
fs.take_and_serve_directory_handle()?;
let fs = fs;
let mut server = Server::new(initial_profile(&opts));
fx_log_info!("Starting server...");
server.run(fs).await;
Ok(())
}
#[cfg(test)]
mod test {
use {
anyhow::{Context as _, Error},
fidl_fuchsia_examples_intl_manager::{PropertyManagerMarker, PropertyManagerProxy},
fidl_fuchsia_intl::{
CalendarId, LocaleId, Profile, PropertyProviderEventStream, PropertyProviderMarker,
PropertyProviderProxy, TemperatureUnit, TimeZoneId,
},
fidl_fuchsia_intl_ext::CloneExt,
fidl_fuchsia_sys::LauncherProxy,
fuchsia_async as fasync,
fuchsia_component::client,
futures::{self, prelude::*},
lazy_static::lazy_static,
};
static COMPONENT_URL: &str =
"fuchsia-pkg://fuchsia.com/intl_property_manager#meta/intl_property_manager.cmx";
static COMPONENT_URL_WITHOUT_FLAGS: &str =
"fuchsia-pkg://fuchsia.com/intl_property_manager#meta/intl_property_manager_without_flags.cmx";
lazy_static! {
static ref PROFILE_EMPTY: Profile =
Profile { locales: None, calendars: None, time_zones: None, temperature_unit: None };
static ref PROFILE_A: Profile = Profile {
locales: Some(vec![
LocaleId { id: "en-US".to_string() },
LocaleId { id: "fr-CA".to_string() }
]),
calendars: Some(vec![CalendarId { id: "gregorian".to_string() }]),
time_zones: Some(vec![TimeZoneId { id: "America/New_York".to_string() }]),
temperature_unit: Some(TemperatureUnit::Celsius),
};
static ref PROFILE_B: Profile = Profile {
locales: Some(vec![
LocaleId { id: "ar-EG".to_string() },
LocaleId { id: "el-GR".to_string() }
]),
calendars: Some(vec![CalendarId { id: "gregorian".to_string() }]),
time_zones: Some(vec![TimeZoneId { id: "Europe/Athens".to_string() }]),
temperature_unit: Some(TemperatureUnit::Celsius),
};
// This profile corresponds to the flag settings in the manifest at `COMPONENT_URL`.
static ref INITIAL_PROFILE: Profile = Profile {
locales: Some(vec![
LocaleId { id: "en-US".to_string() },
LocaleId { id: "nl-NL".to_string() }
]),
calendars: None,
time_zones: Some(vec![TimeZoneId { id: "und-u-tz-uslax".to_string() }]),
temperature_unit: None,
};
}
/// The test launches the provider/manager, then sets and gets `Profile` values several times,
/// confirming that `OnChange` events are sent and the updated values are correct.
#[fasync::run_singlethreaded]
#[test]
async fn test_get_set_profile() -> Result<(), Error> {
let launcher: LauncherProxy =
client::launcher().context("Failed to open launcher service")?;
let app = client::launch(&launcher, COMPONENT_URL_WITHOUT_FLAGS.to_string(), None)
.context("Failed to launch Intl Property Manager")?;
let property_manager: PropertyManagerProxy = app
.connect_to_service::<PropertyManagerMarker>()
.context("Failed to connect to intl PropertyManager service")?;
let property_provider: PropertyProviderProxy = app
.connect_to_service::<PropertyProviderMarker>()
.context("Failed to connect to intl PropertyProvider service")?;
let initial_profile = property_provider.get_profile().await?;
assert_eq!(initial_profile, *PROFILE_EMPTY);
let mut event_stream: PropertyProviderEventStream = property_provider.take_event_stream();
property_manager.set_profile(PROFILE_A.clone()).await?;
let event_a_msg = "Failed to get event for PROFILE_A";
event_stream.next().await.expect(event_a_msg).expect(event_a_msg);
let actual = property_provider.get_profile().await?;
assert_eq!(actual, *PROFILE_A);
property_manager.set_profile(PROFILE_B.clone()).await?;
let event_b_msg = "Failed to get event for PROFILE_B";
event_stream.next().await.expect(event_b_msg).expect(event_b_msg);
let actual = property_provider.get_profile().await?;
assert_eq!(actual, *PROFILE_B);
Ok(())
}
/// This test confirms that the provider will serve a nonempty initial
/// profile when invoked from the default component manifest URL, and that
/// the served profile corresponds to the settings that are currently in
/// the manifest.
#[fasync::run_singlethreaded]
#[test]
async fn test_set_initial_profile() -> Result<(), Error> {
let launcher: LauncherProxy =
client::launcher().context("Failed to open launcher service")?;
let app = client::launch(&launcher, COMPONENT_URL.to_string(), None)
.context("Failed to launch Intl Property Manager")?;
let property_provider: PropertyProviderProxy = app
.connect_to_service::<PropertyProviderMarker>()
.context("Failed to connect to intl PropertyProvider service")?;
let initial_profile = property_provider.get_profile().await?;
assert_eq!(initial_profile, *INITIAL_PROFILE);
Ok(())
}
}