blob: 91eb7c730662672865a0a99922c16b453a7f623c [file] [log] [blame]
// Copyright 2025 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.
#ifndef LIB_COMPONENT_INCOMING_CPP_SERVICE_MEMBER_WATCHER_H_
#define LIB_COMPONENT_INCOMING_CPP_SERVICE_MEMBER_WATCHER_H_
#include <fidl/fuchsia.io/cpp/wire.h>
#include <fidl/fuchsia.unknown/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/component/incoming/cpp/constants.h>
#include <lib/component/incoming/cpp/directory.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/component/incoming/cpp/service.h>
#include <lib/component/incoming/cpp/service_watcher.h>
#include <lib/zx/channel.h>
#include <lib/zx/clock.h>
#include <lib/zx/result.h>
#include <deque>
#include <type_traits>
#include <utility>
namespace component {
// Watch for service instances and connect to a member protocol of each instance.
//
// ServiceMemberWatcher and SyncServiceMemberWatcher are templated on a ServiceMember, which
// specifies both the service that it is a part of, and a member protocol of that service.
//
// For a fidl protocol and service defined as:
// library fidl.examples.echo;
// protocol DriverEcho {...}
// service DriverEchoService {
// echo_device client_end:DriverEcho;
// };
//
// The ServiceMember would be: fidl_examples_echo::Service::EchoDevice
// Note that the last part of the ServiceMember corresponds to the name of the
// client_end in the service, not the protocol type.
//
// Services can be waited on asynchronously with ServiceMemberWatcher and synchronously with
// SyncServiceMemberWatcher. If you have a service with multiple protocols, and need to access
// more than one protocol of a service for each instance, you can use component::ServiceWatcher
//
// Define a callback function:
// void OnInstanceFound(ClientEnd<fidl_examples_echo::DriverEcho> client_end) {...}
// Optionally define an idle function, which will be called when all existing instances have been
// enumerated:
// void AllExistingEnumerated() {...}
// Create the ServiceMemberWatcher:
// ServiceMemberWatcher<fidl_examples_echo::Service::EchoDevice> watcher;
// watcher->Begin(get_default_dispatcher(), &OnInstanceFound, &AllExistingEnumerated);
//
// The ServiceMemberWatcher will stop upon destruction, or when |Cancel| is called.
template <typename ServiceMember>
class ServiceMemberWatcher {
static_assert(fidl::IsServiceMemberV<ServiceMember>, "Type must be a member of a service");
public:
using Protocol = typename ServiceMember::ProtocolType;
using ClientCallback = fit::function<void(fidl::ClientEnd<Protocol>)>;
// Callback function which is invoked after the existing service instances have been
// reported via ClientCallback, and before newly-arriving service instances are delivered
// via ClientCallback.
using IdleCallback = fit::callback<void()>;
// Cancels watching for service instances.
zx::result<> Cancel() {
client_callback_ = nullptr;
return zx::make_result(service_watcher_.Cancel());
}
// For tests, the service root can be set manually
explicit ServiceMemberWatcher(fidl::UnownedClientEnd<fuchsia_io::Directory> svc_root)
: svc_dir_(svc_root) {}
ServiceMemberWatcher()
: default_svc_dir_(OpenServiceRoot().value()), svc_dir_(default_svc_dir_.borrow()) {}
// Begins asynchronously waiting for service instances using the given dispatcher.
//
// Waits for services in the incoming service directory: /svc/ServiceMember::ServiceName
//
// Asynchronously invokes |client_callback| for all existing service instances
// within the specified aggregate service type, as any subsequently added
// devices until service member watcher is destroyed. Ignores any service
// named ".".
//
// The |idle_callback| is invoked once immediately after all pre-existing
// service instances have been reported via |client_callback| shortly after creation.
// After |idle_callback| returns, any newly-arriving devices are reported via
// |client_callback|.
// |idle_callback| will be deleted after it is called, so captured context
// is guaranteed to not be retained.
zx::result<> Begin(
async_dispatcher_t* dispatcher, ClientCallback callback, IdleCallback idle_callback = [] {}) {
// Begin should only be called once
ZX_ASSERT(client_callback_ == nullptr);
client_callback_ = std::move(callback);
idle_callback_ = std::move(idle_callback);
auto service_directory_result = OpenDirectoryAt(svc_dir_, ServiceMember::ServiceName);
if (service_directory_result.is_error()) {
return service_directory_result.take_error();
}
return zx::make_result(service_watcher_.Begin(
std::move(service_directory_result.value()),
fit::bind_member<&ServiceMemberWatcher::OnWatchedEvent>(this), dispatcher));
}
private:
void OnWatchedEvent(fuchsia_io::wire::WatchEvent event, std::string instance) {
if (event == fuchsia_io::wire::WatchEvent::kIdle) {
idle_callback_();
return;
}
if (event == fuchsia_io::wire::WatchEvent::kRemoved || instance.size() < 2) {
return;
}
zx::result<fidl::ClientEnd<typename ServiceMember::ProtocolType>> client_result =
ConnectAtMember<ServiceMember>(svc_dir_, instance);
// This should not fail, since the directory just gave us the instance.
ZX_ASSERT(client_result.is_ok());
client_callback_(std::move(client_result.value()));
}
ClientCallback client_callback_;
IdleCallback idle_callback_;
// for default initialization we hold ownership of the client_end.
fidl::ClientEnd<fuchsia_io::Directory> default_svc_dir_;
fidl::UnownedClientEnd<fuchsia_io::Directory> svc_dir_;
component::ServiceWatcher service_watcher_;
};
// SyncServiceMemberWatcher allows services to be waited for synchronously.
// Note that the this class is templated on the service member name, not the service name.
// For example:
// SyncServiceMemberWatcher<fidl_examples_echo::Service::EchoDevice> watcher;
// zx::result<ClientEnd<fidl_examples_echo::DriverEcho>> result = watcher.GetNextInstance(true);
template <typename ServiceMember>
class SyncServiceMemberWatcher final : public ServiceMemberWatcher<ServiceMember> {
static_assert(fidl::IsServiceMemberV<ServiceMember>, "Type must be a member of a service");
public:
using Protocol = typename ServiceMember::ProtocolType;
explicit SyncServiceMemberWatcher(fidl::UnownedClientEnd<fuchsia_io::Directory> svc_root)
: ServiceMemberWatcher<ServiceMember>(svc_root) {}
SyncServiceMemberWatcher() : ServiceMemberWatcher<ServiceMember>() {}
// Sequentially query for service instances at /svc/ServiceMember::ServiceName
//
// This call will block until a service instance is found. When an instance of the given
// service is detected in the /svc/ServiceMember::ServiceName directory, this function
// will return a ClientEnd to the protocol specified by ServiceMember::ProtocolType.
//
// Subsequent calls to GetNextInstance will return other instances if they exist.
// GetNextInstance will iterate through all service instances of a given type.
// When all of the existing service instances have been returned,
// if |stop_at_idle| is true, GetNextInstance will return a zx::error(ZX_ERR_STOP).
// Otherwise, GetNextInstance will wait until |deadline| for a new instance to appear.
zx::result<fidl::ClientEnd<Protocol>> GetNextInstance(bool stop_at_idle,
zx::time deadline = zx::time::infinite()) {
if (!has_begun_iterating_) {
zx::result result = this->Begin(
loop_.dispatcher(),
[this](fidl::ClientEnd<Protocol> client_in) {
clients_.emplace_back(std::move(client_in));
},
[this]() { idle_called_ = true; });
if (result.is_error()) {
return result.take_error();
}
has_begun_iterating_ = true;
}
// Run the loop to get the file events
// Due to the nature of the fuchsia_io::Watcher protocol, one event for the service watcher may
// correspond to multiple file events. For this reason, we just run the loop until idle and
// then process all the file events that we have received.
zx_status_t run_status;
do {
run_status = loop_.RunUntilIdle();
if (run_status != ZX_OK) { // loop was cancelled or shutdown
return zx::error(run_status);
}
// First get all the entries that were existing/added:
if (!clients_.empty()) {
fidl::ClientEnd<Protocol> client = std::move(clients_.front());
clients_.pop_front();
return zx::ok(std::move(client));
}
// Once the queue is emptied, we can let the user know that the idle callback was invoked.
if (stop_at_idle && idle_called_) {
return zx::error(ZX_ERR_STOP);
}
// At this point, we are either in a race for the idle signal, or stop_at_idle == false,
// and we are just waiting for any future file events.
run_status = loop_.Run(deadline, true);
} while (run_status == ZX_OK);
// loop_.Run exited with a timeout or because it was canceled.
return zx::error(run_status);
}
private:
bool has_begun_iterating_ = false;
bool idle_called_ = false;
// For doing blocking waits:
std::deque<fidl::ClientEnd<Protocol>> clients_;
async::Loop loop_{&kAsyncLoopConfigNeverAttachToThread};
};
} // namespace component
#endif // LIB_COMPONENT_INCOMING_CPP_SERVICE_MEMBER_WATCHER_H_