[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",
+ },
+ ],
},
],
}