blob: e7452297ef9120194f4d3ac1e83cad81e85f19bb [file] [log] [blame]
// Copyright 2020 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::{self, Context},
argh::FromArgs,
fidl::endpoints::{create_proxy, ClientEnd, Proxy},
fidl_fuchsia_io::{self as fio, DirectoryMarker, DirectoryProxy},
fidl_fuchsia_mem as fmem,
fidl_fuchsia_pkg::{PackageResolverMarker, PackageResolverProxy},
fidl_fuchsia_sys2::{
self as fsys, ComponentResolverMarker, ComponentResolverRequest,
ComponentResolverRequestStream,
},
fuchsia_component::{client::connect_to_service, server::ServiceFs},
fuchsia_url::{errors::ParseError as PkgUrlParseError, pkg_url::PkgUrl},
fuchsia_zircon::Status,
futures::prelude::*,
log::*,
thiserror::Error,
};
#[derive(FromArgs, Debug)]
#[argh(description = "Controls for universe resolver")]
struct Args {
#[argh(
switch,
description = "if true the fuchsia.pkg.PkgResolver protocol provided to \
this component is used, otherwise the fuchsia.sys2.ComponentResolver \
protocol (assumed to come from base-resolver) is used"
)]
enable_ephemeral_components: bool,
}
#[fuchsia::component]
async fn main() -> anyhow::Result<()> {
info!("started");
let args: Args = argh::from_env();
let mut service_fs = ServiceFs::new_local();
service_fs.dir("svc").add_fidl_service(|stream: ComponentResolverRequestStream| stream);
service_fs.take_and_serve_directory_handle().context("failed to serve outgoing namespace")?;
service_fs
.for_each_concurrent(None, |stream| {
let all = args.enable_ephemeral_components;
async move {
let r = if all { serve(stream).await } else { forward_to_base(stream).await };
match r {
Ok(()) => {}
Err(err) => error!("failed to serve resolve request: {:?}", err),
}
}
})
.await;
Ok(())
}
async fn forward_to_base(mut stream: ComponentResolverRequestStream) -> anyhow::Result<()> {
let base_resolver = connect_to_service::<ComponentResolverMarker>()
.context("failed to connect to base package resolver")?;
while let Some(ComponentResolverRequest::Resolve { component_url, responder }) =
stream.try_next().await.context("failed to read request from FIDL stream")?
{
match base_resolver.resolve(&component_url).await {
Ok(Ok(r)) => responder.send(&mut Ok(r)),
Ok(Err(e)) => responder.send(&mut Err(e)),
Err(err) => {
info!("universe resolver got FIDL error forward request to base resolver for component URL {}: {}", &component_url, &err);
responder.send(&mut Err(fsys::ResolverError::Internal))
}
}
.context("failed to send response")?;
}
Ok(())
}
async fn serve(mut stream: ComponentResolverRequestStream) -> anyhow::Result<()> {
let package_resolver = connect_to_service::<PackageResolverMarker>()
.context("failed to connect to PackageResolver service")?;
while let Some(ComponentResolverRequest::Resolve { component_url, responder }) =
stream.try_next().await.context("failed to read request from FIDL stream")?
{
match resolve_component(&component_url, &package_resolver).await {
Ok(result) => responder.send(&mut Ok(result)),
Err(err) => {
info!("failed to resolve component URL {}: {}", &component_url, &err);
responder.send(&mut Err(err.into()))
}
}
.context("failed sending response")?;
}
Ok(())
}
async fn resolve_component(
component_url: &str,
package_resolver: &PackageResolverProxy,
) -> Result<fsys::Component, ResolverError> {
let package_url = PkgUrl::parse(component_url)?;
let cm_path = package_url
.resource()
.ok_or_else(|| ResolverError::InvalidUrl(PkgUrlParseError::InvalidResourcePath))?;
let package_dir = resolve_package(&package_url, package_resolver).await?;
// Read the component manifest (.cm file) from the package directory.
let cm_file = io_util::directory::open_file(&package_dir, cm_path, fio::OPEN_RIGHT_READABLE)
.await
.map_err(ResolverError::ComponentNotFound)?;
let (status, buffer) =
cm_file.get_buffer(fio::VMO_FLAG_READ).await.map_err(ResolverError::IoError)?;
Status::ok(status).map_err(ResolverError::VmoFailure)?;
let data = match buffer {
Some(buffer) => fmem::Data::Buffer(*buffer),
None => fmem::Data::Bytes(
io_util::file::read(&cm_file).await.map_err(ResolverError::ReadManifest)?,
),
};
let package_dir = ClientEnd::new(
package_dir.into_channel().expect("could not convert proxy to channel").into_zx_channel(),
);
Ok(fsys::Component {
resolved_url: Some(component_url.into()),
decl: Some(data),
package: Some(fsys::Package {
package_url: Some(package_url.root_url().to_string()),
package_dir: Some(package_dir),
..fsys::Package::EMPTY
}),
..fsys::Component::EMPTY
})
}
async fn resolve_package(
package_url: &PkgUrl,
package_resolver: &PackageResolverProxy,
) -> Result<DirectoryProxy, ResolverError> {
let package_url = package_url.root_url();
let selectors = Vec::new();
let (proxy, server_end) =
create_proxy::<DirectoryMarker>().expect("failed to create channel pair");
package_resolver
.resolve(&package_url.to_string(), &mut selectors.into_iter(), server_end)
.await
.map_err(ResolverError::IoError)?
.map_err(Status::from_raw)
.map_err(|err| match err {
Status::NOT_FOUND => ResolverError::PackageNotFound,
Status::ADDRESS_UNREACHABLE | Status::UNAVAILABLE => ResolverError::Unavailable,
Status::NO_SPACE => ResolverError::NoSpace,
_ => ResolverError::Internal,
})?;
Ok(proxy)
}
#[derive(Error, Debug)]
enum ResolverError {
#[error("an unexpected error ocurred")]
Internal,
#[error("invalid component URL: {}", .0)]
InvalidUrl(#[from] PkgUrlParseError),
#[error("component not found: {}", .0)]
ComponentNotFound(#[source] io_util::node::OpenError),
#[error("package not found")]
PackageNotFound,
#[error("read manifest error: {}", .0)]
ReadManifest(#[source] io_util::file::ReadError),
#[error("IO error: {}", .0)]
IoError(#[source] fidl::Error),
#[error("failed to get manifest VMO: {}", .0)]
VmoFailure(#[source] Status),
#[error("insufficient space to store package")]
NoSpace,
#[error("the component's package is temporarily unavailable")]
Unavailable,
}
impl From<ResolverError> for fsys::ResolverError {
fn from(err: ResolverError) -> fsys::ResolverError {
match err {
ResolverError::Internal => fsys::ResolverError::Internal,
ResolverError::InvalidUrl(_) => fsys::ResolverError::InvalidArgs,
ResolverError::ComponentNotFound(_) => fsys::ResolverError::ManifestNotFound,
ResolverError::PackageNotFound => fsys::ResolverError::PackageNotFound,
ResolverError::ReadManifest(_)
| ResolverError::VmoFailure(_)
| ResolverError::IoError(_) => fsys::ResolverError::Io,
ResolverError::NoSpace => fsys::ResolverError::NoSpace,
ResolverError::Unavailable => fsys::ResolverError::ResourceUnavailable,
}
}
}
#[cfg(test)]
mod tests {
use {
super::*,
anyhow::Error,
fidl::{encoding::encode_persistent, endpoints::ServerEnd},
fidl_fuchsia_mem,
fidl_fuchsia_pkg::{self as fpkg, PackageResolverRequest},
fidl_fuchsia_sys2::{self as fsys, ComponentResolverMarker},
fuchsia_async as fasync,
fuchsia_component::server as fserver,
fuchsia_component_test::{
builder::{Capability, CapabilityRoute, ComponentSource, RealmBuilder, RouteEndpoint},
mock::{Mock, MockHandles},
},
fuchsia_zircon as zx,
fuchsia_zircon::Vmo,
futures::{channel::mpsc, join, lock::Mutex},
matches::assert_matches,
std::{boxed::Box, sync::Arc},
vfs::{
directory::entry::DirectoryEntry,
execution_scope::ExecutionScope,
file::{vmo::asynchronous::read_only_static, vmo::asynchronous::NewVmo},
path::Path,
pseudo_directory,
},
};
async fn mock_pkg_resolver(
trigger: Arc<Mutex<Option<mpsc::Sender<Result<(), Error>>>>>,
handles: MockHandles,
) -> Result<(), Error> {
let mut fs = fserver::ServiceFs::new();
fs.dir("svc").add_fidl_service(
move |mut req_stream: fpkg::PackageResolverRequestStream| {
let tx = trigger.clone();
fasync::Task::local(async move {
while let Some(fpkg::PackageResolverRequest::Resolve { responder, .. }) =
req_stream.try_next().await.expect("Serving package resolver stream failed")
{
responder
.send(&mut Err(zx::Status::NOT_FOUND.into_raw()))
.expect("failed sending package resolver response to client");
{
let mut lock = tx.lock().await;
let mut c = lock.take().unwrap();
c.send(Ok(())).await.expect("failed sending oneshot to test");
lock.replace(c);
}
}
})
.detach();
},
);
fs.serve_connection(handles.outgoing_dir.into_channel())?;
fs.collect::<()>().await;
Ok(())
}
async fn package_requester(
trigger: Arc<Mutex<Option<mpsc::Sender<Result<(), Error>>>>>,
url: String,
handles: MockHandles,
) -> Result<(), Error> {
let resolver_proxy = handles.connect_to_service::<ComponentResolverMarker>()?;
let _ = resolver_proxy.resolve(&url).await?;
fasync::Task::local(async move {
let mut lock = trigger.lock().await;
let mut c = lock.take().unwrap();
c.send(Ok(())).await.expect("sending oneshot from package requester failed");
lock.replace(c);
})
.detach();
Ok(())
}
#[fasync::run_singlethreaded(test)]
// Test that the default configuration which forwards requests to
// PackageResolver works properly.
async fn test_using_pkg_resolver() {
let (sender, mut receiver) = mpsc::channel(2);
let tx = Arc::new(Mutex::new(Some(sender)));
let resolver_url =
"fuchsia-pkg://fuchsia.com/universe-resolver-unittests#meta/universe-resolver-for-test.cm"
.to_string();
let requested_url =
"fuchsia-pkg://fuchsia.com/test-pkg-request#meta/test-component.cm".to_string();
let mut builder = RealmBuilder::new().await.expect("Failed to create test realm builder");
builder
.add_component("universe-resolver", ComponentSource::url(resolver_url))
.await
.expect("Failed add universe-resolver to test topology")
.add_component(
"fake-pkg-resolver",
ComponentSource::Mock(Mock::new({
let sender = tx.clone();
move |mock_handles: MockHandles| {
Box::pin(mock_pkg_resolver(sender.clone(), mock_handles))
}
})),
)
.await
.expect("Failed adding base resolver mock")
.add_eager_component(
"requesting-component",
ComponentSource::Mock(Mock::new({
let sender = tx.clone();
move |mock_handles: MockHandles| {
Box::pin(package_requester(
sender.clone(),
requested_url.clone(),
mock_handles,
))
}
})),
)
.await
.expect("Failed adding mock request component")
.add_route(CapabilityRoute {
capability: Capability::protocol("fuchsia.pkg.PackageResolver"),
source: RouteEndpoint::component("fake-pkg-resolver"),
targets: vec![RouteEndpoint::component("universe-resolver")],
})
.expect("Failed adding resolver route from fake-base-resolver to universe-resolver")
.add_route(CapabilityRoute {
capability: Capability::protocol("fuchsia.sys2.ComponentResolver"),
source: RouteEndpoint::component("universe-resolver"),
targets: vec![RouteEndpoint::component("requesting-component")],
})
.expect("Failed adding resolver route from universe-resolver to requesting-component")
.add_route(CapabilityRoute {
capability: Capability::protocol("fuchsia.logger.LogSink"),
source: RouteEndpoint::AboveRoot,
targets: vec![
RouteEndpoint::component("universe-resolver"),
RouteEndpoint::component("fake-pkg-resolver"),
RouteEndpoint::component("requesting-component"),
],
})
.expect("Failed adding LogSink route to test components");
let _test_topo = builder.build().create().await.unwrap();
receiver.next().await.expect("Unexpected error waiting for response").expect("error sent");
receiver.next().await.expect("Unexpected error waiting for response").expect("error sent");
}
async fn mock_base_resolver(
trigger: Arc<Mutex<Option<mpsc::Sender<Result<(), Error>>>>>,
handles: MockHandles,
) -> Result<(), Error> {
let mut fs = fserver::ServiceFs::new();
fs.dir("svc").add_fidl_service(
move |mut req_stream: fsys::ComponentResolverRequestStream| {
let tx = trigger.clone();
fasync::Task::local(async move {
while let Some(fsys::ComponentResolverRequest::Resolve { responder, .. }) =
req_stream
.try_next()
.await
.expect("Serving component resolver stream failed")
{
responder
.send(&mut Err(fsys::ResolverError::PackageNotFound))
.expect("failed sending resolve response to client");
{
let mut lock = tx.lock().await;
let mut c = lock.take().unwrap();
c.send(Ok(())).await.expect("failed sending oneshot to test");
lock.replace(c);
}
}
})
.detach();
},
);
fs.serve_connection(handles.outgoing_dir.into_channel())?;
fs.collect::<()>().await;
Ok(())
}
#[fasync::run_singlethreaded(test)]
// Test configuration where use of package resolver is disabled and
// requests are forwarded to the base resolver.
async fn test_using_base_resolver() {
let (sender, mut receiver) = mpsc::channel(2);
let tx = Arc::new(Mutex::new(Some(sender)));
let resolver_url =
"fuchsia-pkg://fuchsia.com/universe-resolver-unittests#meta/universe-resolver-base-only-for-test.cm"
.to_string();
let requested_url =
"fuchsia-pkg://fuchsia.com/test-pkg-request#meta/test-component.cm".to_string();
let mut builder = RealmBuilder::new().await.expect("Failed to create test realm builder");
builder
.add_component("universe-resolver", ComponentSource::url(resolver_url))
.await
.expect("Failed add universe-resolver to test topology")
.add_component(
"fake-base-resolver",
ComponentSource::Mock(Mock::new({
let sender = tx.clone();
move |mock_handles: MockHandles| {
Box::pin(mock_base_resolver(sender.clone(), mock_handles))
}
})),
)
.await
.expect("Failed adding base resolver mock")
.add_eager_component(
"requesting-component",
ComponentSource::Mock(Mock::new({
let sender = tx.clone();
move |mock_handles: MockHandles| {
Box::pin(package_requester(
sender.clone(),
requested_url.clone(),
mock_handles,
))
}
})),
)
.await
.expect("Failed adding mock request component")
.add_route(CapabilityRoute {
capability: Capability::protocol("fuchsia.sys2.ComponentResolver"),
source: RouteEndpoint::component("fake-base-resolver"),
targets: vec![RouteEndpoint::component("universe-resolver")],
})
.expect("Failed adding resolver route from fake-base-resolver to universe-resolver")
.add_route(CapabilityRoute {
capability: Capability::protocol("fuchsia.sys2.ComponentResolver"),
source: RouteEndpoint::component("universe-resolver"),
targets: vec![RouteEndpoint::component("requesting-component")],
})
.expect("Failed adding resolver route from universe-resolver to requesting-component")
.add_route(CapabilityRoute {
capability: Capability::protocol("fuchsia.logger.LogSink"),
source: RouteEndpoint::AboveRoot,
targets: vec![
RouteEndpoint::component("universe-resolver"),
RouteEndpoint::component("fake-base-resolver"),
RouteEndpoint::component("requesting-component"),
],
})
.expect("Failed adding LogSink route to test components");
let _test_topo = builder.build().create().await.unwrap();
receiver.next().await.expect("Unexpected error waiting for response").expect("error sent");
receiver.next().await.expect("Unexpected error waiting for response").expect("error sent");
}
#[fuchsia::test]
async fn resolve_package_succeeds() {
let (proxy, mut server) =
fidl::endpoints::create_proxy_and_stream::<PackageResolverMarker>().unwrap();
let server = async move {
let fs = pseudo_directory! {
"test_file" => read_only_static(b"foo"),
};
while let Some(request) = server.try_next().await.unwrap() {
match request {
PackageResolverRequest::Resolve { package_url, selectors, dir, responder } => {
assert_eq!(
package_url, "fuchsia-pkg://fuchsia.com/test",
"unexpected package URL"
);
assert!(
selectors.is_empty(),
"Call to Resolve should not contain any selectors"
);
fs.clone().open(
ExecutionScope::new(),
fio::OPEN_RIGHT_READABLE,
fio::MODE_TYPE_DIRECTORY,
Path::empty(),
ServerEnd::new(dir.into_channel()),
);
responder.send(&mut Ok(())).unwrap();
}
_ => panic!("unexpected API call"),
}
}
};
let client = async move {
let result = resolve_package(
&PkgUrl::new_package("fuchsia.com".into(), "/test".into(), None).unwrap(),
&proxy,
)
.await;
let directory = result.expect("package resolver failed unexpectedly");
let file =
io_util::directory::open_file(&directory, "test_file", fio::OPEN_RIGHT_READABLE)
.await
.expect("failed to open 'test_file' from package resolver directory");
let contents = io_util::file::read(&file)
.await
.expect("failed to read 'test_file' contents from package resolver directory");
assert_eq!(&contents, b"foo");
};
join!(server, client);
}
#[fuchsia::test]
async fn resolve_component_succeeds() {
let (proxy, mut server) =
fidl::endpoints::create_proxy_and_stream::<PackageResolverMarker>().unwrap();
let server = async move {
let cm_bytes = encode_persistent(&mut fsys::ComponentDecl::EMPTY.clone())
.expect("failed to encode ComponentDecl FIDL");
let fs = pseudo_directory! {
"meta" => pseudo_directory!{
"test.cm" => read_only_static(cm_bytes),
},
};
while let Some(request) = server.try_next().await.unwrap() {
match request {
PackageResolverRequest::Resolve { package_url, selectors, dir, responder } => {
assert_eq!(
package_url, "fuchsia-pkg://fuchsia.com/test",
"unexpected package URL"
);
assert!(
selectors.is_empty(),
"Call to Resolve should not contain any selectors"
);
fs.clone().open(
ExecutionScope::new(),
fio::OPEN_RIGHT_READABLE,
fio::MODE_TYPE_DIRECTORY,
Path::empty(),
ServerEnd::new(dir.into_channel()),
);
responder.send(&mut Ok(())).unwrap();
}
_ => panic!("unexpected API call"),
}
}
};
let client = async move {
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.com/test#meta/test.cm", &proxy).await,
Ok(fsys::Component {
decl: Some(fidl_fuchsia_mem::Data::Buffer(fidl_fuchsia_mem::Buffer { .. })),
..
})
);
};
join!(server, client);
}
#[fuchsia::test]
async fn resolve_component_succeeds_with_hash() {
let (proxy, mut server) =
fidl::endpoints::create_proxy_and_stream::<PackageResolverMarker>().unwrap();
let server = async move {
let cm_bytes = encode_persistent(&mut fsys::ComponentDecl::EMPTY.clone())
.expect("failed to encode ComponentDecl FIDL");
let fs = pseudo_directory! {
"meta" => pseudo_directory!{
"test.cm" => read_only_static(cm_bytes),
},
};
while let Some(request) = server.try_next().await.unwrap() {
match request {
PackageResolverRequest::Resolve { package_url, selectors, dir, responder } => {
assert_eq!(package_url, "fuchsia-pkg://fuchsia.com/test?hash=9e3a3f63c018e2a4db0ef93903a87714f036e3e8ff982a7a2020eca86cc4677c", "unexpected package URL");
assert!(
selectors.is_empty(),
"Call to Resolve should not contain any selectors"
);
fs.clone().open(
ExecutionScope::new(),
fio::OPEN_RIGHT_READABLE,
fio::MODE_TYPE_DIRECTORY,
Path::empty(),
ServerEnd::new(dir.into_channel()),
);
responder.send(&mut Ok(())).unwrap();
}
_ => panic!("unexpected API call"),
}
}
};
let client = async move {
assert_matches!(resolve_component("fuchsia-pkg://fuchsia.com/test?hash=9e3a3f63c018e2a4db0ef93903a87714f036e3e8ff982a7a2020eca86cc4677c#meta/test.cm", &proxy).await, Ok(_));
};
join!(server, client);
}
#[fuchsia::test]
async fn resolve_component_succeeds_with_vmo_manifest() {
let (proxy, mut server) =
fidl::endpoints::create_proxy_and_stream::<PackageResolverMarker>().unwrap();
let server = async move {
let fs = pseudo_directory! {
"meta" => pseudo_directory!{
"test.cm" => vfs::file::vmo::read_only(|| async move {
let cm_bytes = encode_persistent(&mut fsys::ComponentDecl::EMPTY.clone())
.expect("failed to encode ComponentDecl FIDL");
let capacity = cm_bytes.len() as u64;
let vmo = Vmo::create(capacity)?;
Ok(NewVmo { vmo, size: capacity, capacity })
}),
},
};
while let Some(request) = server.try_next().await.unwrap() {
match request {
PackageResolverRequest::Resolve { dir, responder, .. } => {
fs.clone().open(
ExecutionScope::new(),
fio::OPEN_RIGHT_READABLE,
fio::MODE_TYPE_DIRECTORY,
Path::empty(),
ServerEnd::new(dir.into_channel()),
);
responder.send(&mut Ok(())).unwrap();
}
_ => panic!("unexpected API call"),
}
}
};
let client = async move {
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.com/test#meta/test.cm", &proxy).await,
Ok(fsys::Component { decl: Some(fmem::Data::Buffer(_)), .. })
);
};
join!(server, client);
}
#[fuchsia::test]
async fn resolve_component_fails_bad_connection() {
let (proxy, server) =
fidl::endpoints::create_proxy_and_stream::<PackageResolverMarker>().unwrap();
drop(server);
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.com/test#meta/test.cm", &proxy).await,
Err(ResolverError::IoError(_))
);
}
#[fuchsia::test]
async fn resolve_component_fails_with_package_resolver_failure() {
let (proxy, mut server) =
fidl::endpoints::create_proxy_and_stream::<PackageResolverMarker>().unwrap();
let server = async move {
while let Some(request) = server.try_next().await.unwrap() {
match request {
PackageResolverRequest::Resolve { responder, .. } => {
responder.send(&mut Err(Status::NO_SPACE.into_raw())).unwrap();
}
_ => panic!("unexpected API call"),
}
}
};
let client = async move {
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.com/test#meta/test.cm", &proxy).await,
Err(ResolverError::NoSpace)
);
};
join!(server, client);
}
#[fuchsia::test]
async fn resolve_component_fails_with_component_not_found() {
let (proxy, mut server) =
fidl::endpoints::create_proxy_and_stream::<PackageResolverMarker>().unwrap();
let server = async move {
let fs = pseudo_directory! {};
while let Some(request) = server.try_next().await.unwrap() {
match request {
PackageResolverRequest::Resolve { dir, responder, .. } => {
fs.clone().open(
ExecutionScope::new(),
fio::OPEN_RIGHT_READABLE,
fio::MODE_TYPE_DIRECTORY,
Path::empty(),
ServerEnd::new(dir.into_channel()),
);
responder.send(&mut Ok(())).unwrap();
}
_ => panic!("unexpected API call"),
}
}
};
let client = async move {
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.com/test#meta/test.cm", &proxy).await,
Err(ResolverError::ComponentNotFound(_))
);
};
join!(server, client);
}
}