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