[gazelle] Add chrome to the session. To allow launching Chrome with `ffx session add`, this also introduces a new component to route `element.Manager/ProposeElement` requests to the correct destination. Change-Id: I9a9ea29949ac63107ce982bfe04febd6824140cc Reviewed-on: https://fuchsia-review.googlesource.com/c/experiences/+/728362 Reviewed-by: Sanjay Chouksey <sanjayc@google.com> Commit-Queue: Hunter Freyer <hjfreyer@google.com>
diff --git a/session_shells/gazelle/BUILD.gn b/session_shells/gazelle/BUILD.gn index f4e5186..dfd0fdc 100644 --- a/session_shells/gazelle/BUILD.gn +++ b/session_shells/gazelle/BUILD.gn
@@ -6,6 +6,7 @@ group("gazelle") { public_deps = [ + "element_router", "shell", "wm", ]
diff --git a/session_shells/gazelle/element_router/BUILD.gn b/session_shells/gazelle/element_router/BUILD.gn new file mode 100644 index 0000000..2049821 --- /dev/null +++ b/session_shells/gazelle/element_router/BUILD.gn
@@ -0,0 +1,61 @@ +# Copyright 2022 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. + +import("//build/components.gni") +import("//build/rust/rustc_binary.gni") + +rustc_binary("bin") { + output_name = "element_router" + version = "0.1.0" + edition = "2018" + + source_root = "src/main.rs" + sources = [ "src/main.rs" ] + + deps = [ + ":config_lib", + "//sdk/fidl/fuchsia.element:fuchsia.element_rust", + "//src/lib/fidl/rust/fidl", + "//src/lib/fuchsia", + "//src/lib/fuchsia-component", + "//src/sys/lib/fidl-connector", + "//third_party/rust_crates:anyhow", + "//third_party/rust_crates:futures", + "//third_party/rust_crates:tracing", + "//third_party/rust_crates:url", + ] +} + +fuchsia_component_manifest("manifest") { + component_name = "element_router" + manifest = "meta/element_router.cml" +} + +fuchsia_structured_config_rust_lib("config_lib") { + cm_label = ":manifest" +} + +fuchsia_component("component") { + cm_label = ":manifest" + deps = [ ":bin" ] +} + +fuchsia_package("element_router") { + deps = [ + ":component", + ":workstation_config", + ] +} + +fuchsia_structured_config_values("workstation_config") { + cm_label = ":manifest" + values = { + url_to_backend = [ "fuchsia-pkg://chromium.org/chrome#meta/chrome.cm|fuchsia.element.Manager-chrome" ] + scheme_to_backend = [ + "http|fuchsia.element.Manager-chrome", + "https|fuchsia.element.Manager-chrome", + ] + default_backend = "fuchsia.element.Manager-default" + } +}
diff --git a/session_shells/gazelle/element_router/README.md b/session_shells/gazelle/element_router/README.md new file mode 100644 index 0000000..c94c01d --- /dev/null +++ b/session_shells/gazelle/element_router/README.md
@@ -0,0 +1,11 @@ +# `element_router` + +`element_router` is a tiny component that helps when there's more than one +implementer of `fuchsia.element.Manager` in a session. + +The `element_router` is given a mapping from component URL to +incoming-`element.Manager`-capability name, and uses that to proxy proposals to +the appropriate `element.Manager`. + +This functionality probably belongs somewhere else, but at the moment it's not +clear where. So this will do for now!
diff --git a/session_shells/gazelle/element_router/meta/element_router.cml b/session_shells/gazelle/element_router/meta/element_router.cml new file mode 100644 index 0000000..4e1ca7a --- /dev/null +++ b/session_shells/gazelle/element_router/meta/element_router.cml
@@ -0,0 +1,71 @@ +// Copyright 2022 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. +{ + include: [ + "inspect/client.shard.cml", + "syslog/client.shard.cml", + ], + program: { + runner: "elf", + binary: "bin/element_router", + }, + capabilities: [ + { + protocol: [ "fuchsia.element.Manager" ], + }, + ], + use: [ + { + protocol: [ + "fuchsia.element.Manager-chrome", + "fuchsia.element.Manager-default", + ], + }, + ], + expose: [ + { + protocol: [ "fuchsia.element.Manager" ], + from: "self", + }, + ], + config: { + // A list of mappings from component URL to capability name. The URL and + // capability are separated by a | character. + // + // For instance, one entry might be: + // "fuchsia-pkg://chromium.org/chrome#meta/chrome.cm|fuchsia.element.Manager-chrome" + url_to_backend: { + type: "vector", + max_count: 100, + element: { + type: "string", + max_size: 512, + }, + }, + + // A list of mappings from component URL scheme to capability name. The + // scheme and capability are separated by a | character. + // + // For instance, one entry might be: + // "https|fuchsia.element.Manager-chrome" + // + // If a URL matches both an exact match and a scheme, the exact match + // takes precedence. + scheme_to_backend: { + type: "vector", + max_count: 100, + element: { + type: "string", + max_size: 512, + }, + }, + + // The default capability to use when a component URL does not match a + // rule in `url_to_backend` or `scheme_to_backend`. + default_backend: { + type: "string", + max_size: 512, + }, + }, +}
diff --git a/session_shells/gazelle/element_router/src/main.rs b/session_shells/gazelle/element_router/src/main.rs new file mode 100644 index 0000000..2a6324a --- /dev/null +++ b/session_shells/gazelle/element_router/src/main.rs
@@ -0,0 +1,189 @@ +// Copyright 2022 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 std::collections::{BTreeSet, HashMap}; + +use anyhow::Context; +use config_lib::Config; +use fidl::endpoints; +use fidl_connector::Connect; +use fidl_fuchsia_element as felement; +use fuchsia_component::server; +use futures::StreamExt; + +type Backend = fidl_connector::ServiceReconnector<felement::ManagerMarker>; + +struct Router<'a> { + /// When a `element.Manager/ProposeElement` request comes in, if the URL + /// matches any of these `rules`, that request is proxied to to the + /// corresponding `element.Manager` backend. + rules: Vec<(Matcher, &'a Backend)>, + + /// If the URL doesn't match any of the given `rules`, or the request is + /// somehow malformed, it gets proxied to the `default` backend. + default: &'a Backend, +} + +impl Router<'_> { + async fn propose_element( + &self, + spec: felement::Spec, + controller: Option<endpoints::ServerEnd<felement::ControllerMarker>>, + ) -> anyhow::Result<Result<(), felement::ProposeElementError>> { + let backend = self + .select_backend(spec.component_url.as_ref()) + .connect() + .context("acquiring channel")?; + backend.propose_element(spec, controller).await.context("calling propose_element") + } + + fn select_backend(&self, url: Option<&String>) -> &Backend { + if let Some(url) = url { + for (matcher, backend) in self.rules.iter() { + if matcher.matches(url) { + return backend; + } + } + } + self.default + } + + async fn serve_element_manager_request_stream(&self, stream: felement::ManagerRequestStream) { + stream + .for_each_concurrent(None, |request: fidl::Result<felement::ManagerRequest>| async { + match request { + Err(err) => { + tracing::error!("FIDL error receiving element.Manager request: {}", err); + return; + } + Ok(felement::ManagerRequest::ProposeElement { + spec, + controller, + responder, + }) => { + let mut response = match self.propose_element(spec, controller).await { + Ok(response) => response, + Err(err) => { + tracing::error!( + "FIDL error proxying ProposeElement request. + Dropping responder without replying: {}", + err + ); + return; + } + }; + if let Err(err) = responder.send(&mut response) { + tracing::error!("FIDL error proxying ProposeElement response: {}", err) + } + } + } + }) + .await; + } +} + +/// A Matcher is a predicate over component_urls. +enum Matcher { + /// An Exact matcher matches a component_url if it exactly matches the given + /// string. + Exact(String), + + /// A Scheme matcher matches a component_url if the component_url correctly + /// parses as a URL, and has a matching scheme. + Scheme(String), +} + +impl Matcher { + fn matches(&self, component_url: &str) -> bool { + match self { + Matcher::Exact(matcher_url) => matcher_url == component_url, + Matcher::Scheme(matcher_scheme) => { + // NOTE: Parsing the URL inside `matches` means we may be + // parsing the same URL multiple times, but it really doesn't + // matter from a performance perspective, and the logic is + // easier to understand this way without having to worry about + // memoization. + match url::Url::parse(component_url) { + Ok(parsed_url) => matcher_scheme == parsed_url.scheme(), + Err(err) => { + tracing::info!( + "URL for ProposeElement ({:?}) request does not parse: {:?}", + component_url, + err + ); + false + } + } + } + } + } +} + +enum IncomingService { + ElementManager(felement::ManagerRequestStream), +} + +#[fuchsia::main(logging = true)] +async fn main() -> anyhow::Result<()> { + let config = Config::take_from_startup_handle(); + tracing::info!("element_router config: {:?}", config); + + let exact_rules = config.url_to_backend.into_iter().map(|rule: String| -> (Matcher, String) { + let split: Vec<&str> = rule.split("|").collect(); + assert_eq!(split.len(), 2, "malformed rule: {:?}", rule); + (Matcher::Exact(split[0].to_owned()), split[1].to_owned()) + }); + + let scheme_rules = + config.scheme_to_backend.into_iter().map(|rule: String| -> (Matcher, String) { + let split: Vec<&str> = rule.split("|").collect(); + assert_eq!(split.len(), 2, "malformed rule: {:?}", rule); + (Matcher::Scheme(split[0].to_owned()), split[1].to_owned()) + }); + + let all_rules: Vec<(Matcher, String)> = exact_rules.chain(scheme_rules).collect(); + + let all_backend_names: BTreeSet<String> = all_rules + .iter() + .map(|(_, backend)| backend.to_owned()) + .chain(std::iter::once(config.default_backend.to_owned())) + .collect(); + + let all_backends: HashMap<String, Backend> = all_backend_names + .into_iter() + .map(|backend_name| { + let backend = Backend::with_service_at_path("/svc/".to_owned() + &backend_name); + (backend_name, backend) + }) + .collect(); + + let router = Router { + rules: all_rules + .into_iter() + .map(|(matcher, backend_name)| { + let backend = all_backends + .get(&backend_name) + .expect("somehow we didn't provision this backend?"); + (matcher, backend) + }) + .collect(), + default: all_backends + .get(&config.default_backend) + .expect("somehow we didn't provision this backend?"), + }; + + let mut fs = server::ServiceFs::new(); + fs.dir("svc").add_fidl_service(IncomingService::ElementManager); + fs.take_and_serve_directory_handle()?; + + fs.for_each_concurrent(None, |connection_request| async { + match connection_request { + IncomingService::ElementManager(stream) => { + router.serve_element_manager_request_stream(stream).await; + } + } + }) + .await; + Ok(()) +}
diff --git a/session_shells/gazelle/shell/meta/gazelle_shell.cml b/session_shells/gazelle/shell/meta/gazelle_shell.cml index d4b1343..1f67abb 100644 --- a/session_shells/gazelle/shell/meta/gazelle_shell.cml +++ b/session_shells/gazelle/shell/meta/gazelle_shell.cml
@@ -8,14 +8,23 @@ ], children: [ { - name: "wm", - url: "fuchsia-pkg://fuchsia.com/wm#meta/wm.cm", - startup: "eager", + name: "chrome", + url: "fuchsia-pkg://chromium.org/chrome#meta/chrome.cm", + environment: "#full-resolver-env", }, { name: "element_manager", url: "fuchsia-pkg://fuchsia.com/element_manager#meta/element_manager.cm", }, + { + name: "element_router", + url: "fuchsia-pkg://fuchsia.com/element_router#meta/element_router.cm", + }, + { + name: "wm", + url: "fuchsia-pkg://fuchsia.com/wm#meta/wm.cm", + startup: "eager", + }, ], collections: [ { @@ -25,10 +34,18 @@ ], offer: [ { - protocol: [ - "fuchsia.logger.LogSink", - "fuchsia.ui.composition.Flatland", + protocol: [ "fuchsia.logger.LogSink" ], + from: "parent", + to: [ + "#chrome", + "#element_manager", + "#element_router", + "#elements", + "#wm", ], + }, + { + protocol: [ "fuchsia.ui.composition.Flatland" ], from: "parent", to: [ "#wm" ], }, @@ -41,6 +58,7 @@ protocol: "fuchsia.element.GraphicalPresenter", from: "#wm", to: [ + "#chrome", "#element_manager", "#elements", ], @@ -52,19 +70,34 @@ }, { protocol: [ - "fuchsia.logger.LogSink", "fuchsia.sys.Launcher", "fuchsia.ui.scenic.Scenic", ], from: "parent", to: "#element_manager", }, + + // Route all the implementations of `fuchsia.element.Manager` to the + // `element_router`. + { + protocol: [ "fuchsia.element.Manager" ], + from: "#element_manager", + as: "fuchsia.element.Manager-default", + to: "#element_router", + }, + { + protocol: [ "fuchsia.element.Manager" ], + from: "#chrome", + as: "fuchsia.element.Manager-chrome", + to: "#element_router", + }, + + // Capabilities for all elements. { protocol: [ "fuchsia.accessibility.semantics.SemanticsManager", "fuchsia.fonts.Provider", "fuchsia.intl.PropertyProvider", - "fuchsia.logger.LogSink", "fuchsia.media.Audio", "fuchsia.sys.Launcher", "fuchsia.sysmem.Allocator", @@ -80,6 +113,8 @@ from: "parent", to: "#elements", }, + + // Capabilities for terminal. { // TODO(fxbug.dev/105828): These additional `protocol` offers to // `#elements` are only required by the `terminal` component. @@ -140,6 +175,61 @@ from: "parent", to: "#elements", }, + + // Capabilities for chrome. + { + protocol: [ + "fuchsia.buildinfo.Provider", + "fuchsia.camera3.DeviceWatcher", + "fuchsia.fonts.Provider", + "fuchsia.intl.PropertyProvider", + "fuchsia.kernel.VmexResource", + "fuchsia.media.Audio", + "fuchsia.media.AudioDeviceEnumerator", + "fuchsia.media.ProfileProvider", + "fuchsia.mediacodec.CodecFactory", + "fuchsia.memorypressure.Provider", + "fuchsia.net.interfaces.State", + "fuchsia.net.name.Lookup", + "fuchsia.posix.socket.Provider", + "fuchsia.process.Launcher", + "fuchsia.sysmem.Allocator", + "fuchsia.tracing.perfetto.ProducerConnector", + "fuchsia.tracing.provider.Registry", + "fuchsia.ui.composition.Allocator", + "fuchsia.ui.composition.Flatland", + "fuchsia.ui.composition.internal.ScreenCapture", + "fuchsia.ui.composition.ScreenCapture", + "fuchsia.ui.input3.Keyboard", + "fuchsia.ui.scenic.Scenic", + "fuchsia.vulkan.loader.Loader", + ], + from: "parent", + to: "#chrome", + }, + { + directory: "root-ssl-certificates", + from: "parent", + to: [ "#chrome" ], + }, + { + storage: "account_cache", + from: "parent", + as: "cache", + to: "#chrome", + }, + { + storage: "account_tmp", + from: "parent", + as: "tmp", + to: "#chrome", + }, + { + storage: "account", + from: "parent", + as: "data", + to: "#chrome", + }, ], expose: [ { @@ -155,7 +245,20 @@ }, { protocol: [ "fuchsia.element.Manager" ], - from: "#element_manager", + from: "#element_router", + }, + ], + environments: [ + { + name: "full-resolver-env", + extends: "realm", + resolvers: [ + { + resolver: "full-resolver", + from: "parent", + scheme: "fuchsia-pkg", + }, + ], }, ], }