blob: 46c34c6e7f62c78abf1595d7d1220c8298c84d59 [file]
// Copyright 2026 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.
//! Per-[`Kernel`] discovery, driver fan-out, and connection lifecycle management for Fuchsia FIDL
//! service directories (`/svc/<ServiceName>`).
use crate::device::kobject::Device;
use crate::task::{CurrentTask, Kernel};
use anyhow::Context as _;
use fidl::endpoints::{
MemberOpener, Proxy as _, ServiceMarker, ServiceProxy as _, SynchronousProxy,
};
use fidl_fuchsia_io as fio;
use fuchsia_async as fasync;
use fuchsia_component::client as fclient;
use fuchsia_fs::directory::{WatchEvent, WatchMessage, Watcher};
use futures::{StreamExt as _, TryStreamExt as _};
use starnix_logging::{log_info, log_warn};
use starnix_sync::Mutex;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Weak};
use zx::Peered as _;
type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
trait MonitoredChannel: Send + Sync + 'static {
fn as_channel(&self) -> &zx::Channel;
}
impl<P: SynchronousProxy + Send + Sync + 'static> MonitoredChannel for P {
fn as_channel(&self) -> &zx::Channel {
SynchronousProxy::as_channel(self)
}
}
/// Registration scope passed to a driver's `bind` callback for a discovered FIDL service instance.
///
/// Devices registered through [`Self::add_scoped_device`] and cleanup callbacks registered via
/// [`Self::on_disconnect`] are tied to the lifetime of the service instance and any channel
/// passed to [`Self::monitor_channel`] (later calls overwrite any previously monitored channel).
/// When a monitored channel observes `ZX_CHANNEL_PEER_CLOSED`, or when the service instance
/// directory entry is removed or replaced in `/svc/<S::SERVICE_NAME>`, the watcher runs all
/// registered teardown hooks and device removals in reverse registration order on the kernel's
/// main async executor.
#[derive(Default)]
pub struct ServiceBindingScope {
monitored_channel: Option<Arc<dyn MonitoredChannel>>,
teardown_hooks: Vec<Box<dyn FnOnce(&CurrentTask) + Send>>,
}
impl ServiceBindingScope {
/// Retains `proxy` for the duration of the binding and monitors its underlying Zircon
/// channel for `ZX_CHANNEL_PEER_CLOSED`, running all registered teardown hooks when the
/// remote peer closes its endpoint.
pub fn monitor_channel<P: SynchronousProxy + Send + Sync + 'static>(&mut self, proxy: Arc<P>) {
self.monitored_channel = Some(proxy);
}
/// Registers `hook` to be invoked on the kernel's main async executor with
/// [`KernelThreads::system_task`](crate::task::KernelThreads::system_task) when the binding
/// disconnects, executing in reverse registration order relative to other hooks and scoped
/// devices.
pub fn on_disconnect(&mut self, hook: impl FnOnce(&CurrentTask) + Send + 'static) {
self.teardown_hooks.push(Box::new(hook));
}
/// Registers a disconnect hook that removes `device` via
/// [`DeviceRegistry::remove_device`](crate::device::DeviceRegistry::remove_device) when the
/// binding disconnects, returning `device`.
pub fn add_scoped_device(&mut self, device: Device) -> Device {
let device_to_remove = device.clone();
self.on_disconnect(move |current_task| {
current_task.kernel().device_registry.remove_device(current_task, device_to_remove);
});
device
}
}
impl std::fmt::Debug for ServiceBindingScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServiceBindingScope")
.field("has_monitored_channel", &self.monitored_channel.is_some())
.field("teardown_hooks_len", &self.teardown_hooks.len())
.finish()
}
}
struct InstanceEntry<P> {
name: String,
proxy: Arc<P>,
active: Arc<AtomicBool>,
}
impl<P> Clone for InstanceEntry<P> {
fn clone(&self) -> Self {
Self { name: self.name.clone(), proxy: self.proxy.clone(), active: self.active.clone() }
}
}
struct ActiveBinding {
instance_active: Arc<AtomicBool>,
monitored_channel: Option<Arc<dyn MonitoredChannel>>,
_cancel_monitor: Option<futures::channel::oneshot::Sender<()>>,
teardown_hooks: Vec<Box<dyn FnOnce(&CurrentTask) + Send>>,
}
impl ActiveBinding {
fn is_stale(&self) -> bool {
!self.instance_active.load(Ordering::Acquire)
|| self
.monitored_channel
.as_ref()
.is_some_and(|ch| ch.as_channel().is_closed().unwrap_or(true))
}
fn teardown(self, current_task: &CurrentTask) {
for hook in self.teardown_hooks.into_iter().rev() {
hook(current_task);
}
}
}
struct DriverState<Bind> {
bind: Bind,
active_bindings: Vec<ActiveBinding>,
}
trait ServiceDriver<S: ServiceMarker>: Send + Sync {
fn handle_instance<'a>(
&'a self,
state: &'a Arc<ServiceWatcherState<S>>,
weak_kernel: &'a Weak<Kernel>,
instance: InstanceEntry<S::Proxy>,
) -> BoxFuture<'a, ()>;
fn cleanup_stale_bindings<'a>(
&'a self,
state: &'a Arc<ServiceWatcherState<S>>,
weak_kernel: &'a Weak<Kernel>,
) -> BoxFuture<'a, ()>;
}
struct DriverEntry<S: ServiceMarker, P, Probe, ProbeFut, Bind> {
singleton: bool,
probe: Probe,
/// Protects `bind` and `active_bindings`, serializing all `teardown` and `bind` calls for
/// this driver so an old binding's teardown always completes before a replacement or
/// restarted instance's `bind` closure runs.
///
/// To prevent lock inversion with [`ServiceWatcherState::inner`], code holding `state` must
/// never lock `ServiceWatcherState::inner`; instance liveness and channel closure are checked
/// via lock-free atomics (`InstanceEntry::active`) and kernel handle signals.
state: Mutex<DriverState<Bind>>,
_marker: PhantomData<fn(S, P, ProbeFut)>,
}
impl<S, P, Probe, ProbeFut, Bind> DriverEntry<S, P, Probe, ProbeFut, Bind>
where
S: ServiceMarker<Proxy: Send + Sync + 'static>,
P: Send + 'static,
Probe: Fn(Arc<S::Proxy>) -> ProbeFut + Send + Sync + 'static,
ProbeFut: Future<Output = Option<P>> + Send + 'static,
Bind: FnMut(&CurrentTask, P, &mut ServiceBindingScope) + Send + 'static,
{
fn new(singleton: bool, probe: Probe, bind: Bind) -> Arc<Self> {
Arc::new(Self {
singleton,
probe,
state: Mutex::new(DriverState { bind, active_bindings: Vec::new() }),
_marker: PhantomData,
})
}
fn cleanup_stale_bindings_locked(
state: &mut DriverState<Bind>,
current_task: &CurrentTask,
) -> bool {
let mut removed_any = false;
let mut i = 0;
while i < state.active_bindings.len() {
if state.active_bindings[i].is_stale() {
let binding = state.active_bindings.swap_remove(i);
binding.instance_active.store(false, Ordering::Release);
binding.teardown(current_task);
removed_any = true;
} else {
i += 1;
}
}
removed_any
}
async fn cleanup_stale_bindings(
&self,
watcher_state: &Arc<ServiceWatcherState<S>>,
weak_kernel: &Weak<Kernel>,
) {
let should_promote = {
let Some(kernel) = weak_kernel.upgrade() else {
return;
};
let system_task = kernel.kthreads.system_task();
let mut state = self.state.lock();
let removed = Self::cleanup_stale_bindings_locked(&mut state, system_task);
self.singleton && removed && state.active_bindings.is_empty()
};
watcher_state.prune_inactive_instances();
if should_promote {
self.promote_singleton_instance(watcher_state, weak_kernel).await;
}
}
async fn promote_singleton_instance(
&self,
watcher_state: &Arc<ServiceWatcherState<S>>,
weak_kernel: &Weak<Kernel>,
) {
for candidate in watcher_state.active_instances() {
if !self.state.lock().active_bindings.is_empty() {
break;
}
self.probe_and_bind_instance(watcher_state, weak_kernel, candidate).await;
}
}
async fn probe_and_bind_instance(
&self,
watcher_state: &Arc<ServiceWatcherState<S>>,
weak_kernel: &Weak<Kernel>,
instance: InstanceEntry<S::Proxy>,
) {
let Some(probed) = (self.probe)(instance.proxy.clone()).await else {
return;
};
if let Some(kernel) = weak_kernel.upgrade()
&& instance.active.load(Ordering::Acquire)
{
self.bind_probed(kernel.kthreads.system_task(), watcher_state, &instance, probed);
}
}
fn bind_probed(
&self,
system_task: &CurrentTask,
watcher_state: &Arc<ServiceWatcherState<S>>,
instance: &InstanceEntry<S::Proxy>,
probed: P,
) {
let monitored_channel = {
let mut state = self.state.lock();
// Synchronously tear down any bindings whose channels have already closed or whose
// service directory entries have been removed or replaced so a restarted provider
// instance never races against a pending `OnSignals` or `REMOVE_FILE` wakeup.
Self::cleanup_stale_bindings_locked(&mut state, system_task);
if !instance.active.load(Ordering::Acquire) {
None
} else if state
.active_bindings
.iter()
.any(|binding| Arc::ptr_eq(&binding.instance_active, &instance.active))
{
None
} else if self.singleton && !state.active_bindings.is_empty() {
log_warn!(
"{}: ignoring additional service instance while singleton driver is bound",
S::SERVICE_NAME
);
None
} else {
let mut scope = ServiceBindingScope::default();
(state.bind)(system_task, probed, &mut scope);
let (cancel_tx, monitored) = match scope.monitored_channel.clone() {
Some(channel) => {
let (tx, rx) = futures::channel::oneshot::channel();
(Some(tx), Some((channel, rx)))
}
None => (None, None),
};
state.active_bindings.push(ActiveBinding {
instance_active: instance.active.clone(),
monitored_channel: scope.monitored_channel,
_cancel_monitor: cancel_tx,
teardown_hooks: scope.teardown_hooks,
});
monitored
}
};
watcher_state.prune_inactive_instances();
if let Some((monitored_channel, cancel_rx)) = monitored_channel {
let kernel = system_task.kernel();
let weak_kernel = kernel.weak_self.clone();
let watcher_state = watcher_state.clone();
let instance_active = instance.active.clone();
kernel.kthreads.spawn_future(
move || async move {
let peer_closed = {
let on_signals = std::pin::pin!(fasync::OnSignals::new(
monitored_channel.as_channel(),
zx::Signals::CHANNEL_PEER_CLOSED,
));
matches!(
futures::future::select(on_signals, cancel_rx).await,
futures::future::Either::Left(_)
)
};
drop(monitored_channel);
if peer_closed {
let drivers = watcher_state.deactivate_instance(&instance_active);
futures::future::join_all(drivers.into_iter().map(|driver| {
let watcher_state = &watcher_state;
let weak_kernel = &weak_kernel;
async move {
driver.cleanup_stale_bindings(watcher_state, weak_kernel).await;
}
}))
.await;
}
},
"service_watcher_disconnect_monitor",
);
}
}
}
impl<S, P, Probe, ProbeFut, Bind> ServiceDriver<S> for DriverEntry<S, P, Probe, ProbeFut, Bind>
where
S: ServiceMarker<Proxy: Send + Sync + 'static>,
P: Send + 'static,
Probe: Fn(Arc<S::Proxy>) -> ProbeFut + Send + Sync + 'static,
ProbeFut: Future<Output = Option<P>> + Send + 'static,
Bind: FnMut(&CurrentTask, P, &mut ServiceBindingScope) + Send + 'static,
{
fn handle_instance<'a>(
&'a self,
state: &'a Arc<ServiceWatcherState<S>>,
weak_kernel: &'a Weak<Kernel>,
instance: InstanceEntry<S::Proxy>,
) -> BoxFuture<'a, ()> {
Box::pin(async move {
self.probe_and_bind_instance(state, weak_kernel, instance).await;
DriverEntry::cleanup_stale_bindings(self, state, weak_kernel).await;
})
}
fn cleanup_stale_bindings<'a>(
&'a self,
state: &'a Arc<ServiceWatcherState<S>>,
weak_kernel: &'a Weak<Kernel>,
) -> BoxFuture<'a, ()> {
Box::pin(DriverEntry::cleanup_stale_bindings(self, state, weak_kernel))
}
}
struct ServiceWatcherInner<S: ServiceMarker> {
instances: Vec<InstanceEntry<S::Proxy>>,
drivers: Vec<Arc<dyn ServiceDriver<S>>>,
}
impl<S: ServiceMarker> Default for ServiceWatcherInner<S> {
fn default() -> Self {
Self { instances: Vec::new(), drivers: Vec::new() }
}
}
struct ServiceWatcherState<S: ServiceMarker> {
watch_started: AtomicBool,
inner: Mutex<ServiceWatcherInner<S>>,
}
impl<S: ServiceMarker> Default for ServiceWatcherState<S> {
fn default() -> Self {
Self {
watch_started: AtomicBool::new(false),
inner: Mutex::new(ServiceWatcherInner::default()),
}
}
}
enum WatchDispatch<S: ServiceMarker> {
Bind(InstanceEntry<S::Proxy>, Vec<Arc<dyn ServiceDriver<S>>>),
Remove(Vec<Arc<dyn ServiceDriver<S>>>),
}
impl<S: ServiceMarker<Proxy: Send + Sync + 'static>> ServiceWatcherState<S> {
fn deactivate_instance(
&self,
instance_active: &Arc<AtomicBool>,
) -> Vec<Arc<dyn ServiceDriver<S>>> {
instance_active.store(false, Ordering::Release);
let mut inner = self.inner.lock();
inner.instances.retain(|entry| entry.active.load(Ordering::Acquire));
inner.drivers.clone()
}
fn prune_inactive_instances(&self) {
self.inner.lock().instances.retain(|entry| entry.active.load(Ordering::Acquire));
}
fn active_instances(&self) -> Vec<InstanceEntry<S::Proxy>> {
let mut inner = self.inner.lock();
inner.instances.retain(|entry| entry.active.load(Ordering::Acquire));
inner.instances.clone()
}
fn record_instance(
&self,
instance_name: String,
proxy: Arc<S::Proxy>,
) -> (InstanceEntry<S::Proxy>, Vec<Arc<dyn ServiceDriver<S>>>) {
let mut inner = self.inner.lock();
inner.instances.retain(|entry| {
if entry.name == instance_name {
entry.active.store(false, Ordering::Release);
false
} else {
entry.active.load(Ordering::Acquire)
}
});
let entry =
InstanceEntry { name: instance_name, proxy, active: Arc::new(AtomicBool::new(true)) };
inner.instances.push(entry.clone());
let drivers = inner.drivers.clone();
(entry, drivers)
}
fn remove_instance(&self, instance_name: &str) -> Vec<Arc<dyn ServiceDriver<S>>> {
let mut inner = self.inner.lock();
inner.instances.retain(|entry| {
if entry.name == instance_name {
entry.active.store(false, Ordering::Release);
false
} else {
entry.active.load(Ordering::Acquire)
}
});
inner.drivers.clone()
}
async fn watch_service_dir(
self: &Arc<Self>,
weak_kernel: &Weak<Kernel>,
service_dir: fio::DirectoryProxy,
) -> Result<(), anyhow::Error> {
let watcher = Watcher::new(&service_dir)
.await
.with_context(|| format!("failed to watch {} service", S::SERVICE_NAME))?;
let sync_dir = Arc::new(fio::DirectorySynchronousProxy::new(
service_dir
.into_channel()
.map_err(|_| anyhow::anyhow!("service directory proxy still in use"))?
.into_zx_channel(),
));
watcher
.take_while(|msg| {
let keep_going = weak_kernel.strong_count() > 0
&& !matches!(msg, Ok(WatchMessage { event: WatchEvent::DELETED, .. }));
std::future::ready(keep_going)
})
.try_for_each_concurrent(/* limit = */ None, |msg| {
let dispatch = msg.filename.to_str().filter(|&name| name != ".").and_then(
|filename| match msg.event {
WatchEvent::ADD_FILE | WatchEvent::EXISTING => {
let instance_name = filename.to_owned();
let proxy = Arc::new(S::Proxy::from_member_opener(Box::new(
SyncServiceInstanceOpener {
service_dir: sync_dir.clone(),
instance_name: instance_name.clone(),
},
)));
let (entry, drivers) = self.record_instance(instance_name, proxy);
Some(WatchDispatch::Bind(entry, drivers))
}
WatchEvent::REMOVE_FILE => {
Some(WatchDispatch::Remove(self.remove_instance(filename)))
}
_ => None,
},
);
async move {
match dispatch {
Some(WatchDispatch::Bind(entry, drivers)) => {
let futures = drivers.into_iter().map(|driver| {
let entry = entry.clone();
async move {
driver.handle_instance(self, weak_kernel, entry).await;
}
});
futures::future::join_all(futures).await;
}
Some(WatchDispatch::Remove(drivers)) => {
let futures = drivers.into_iter().map(|driver| async move {
driver.cleanup_stale_bindings(self, weak_kernel).await;
});
futures::future::join_all(futures).await;
}
None => {}
}
Ok(())
}
})
.await
.with_context(|| format!("error watching {} service", S::SERVICE_NAME))
}
}
/// Per-[`Kernel`] manager that watches `/svc/<S::SERVICE_NAME>` at most once and fans out
/// discovered [`ServiceMarker::Proxy`] instances to registered Starnix drivers.
///
/// Active instances are retained so drivers registered after initial discovery still receive
/// previously discovered live instances, and each bound instance can register devices and
/// disconnect hooks on a [`ServiceBindingScope`] that are automatically torn down when a monitored
/// channel closes or when the service instance directory entry is removed or replaced.
#[derive(Debug)]
pub struct ServiceWatcher<S: ServiceMarker>(PhantomData<S>);
impl<S: ServiceMarker<Proxy: Send + Sync + 'static>> ServiceWatcher<S> {
/// Registers a driver that binds each discovered service instance on the kernel's main async
/// executor.
pub fn register<Bind>(kernel: &Kernel, marker: S, bind: Bind)
where
Bind: FnMut(&CurrentTask, Arc<S::Proxy>, &mut ServiceBindingScope) + Send + 'static,
{
Self::register_driver(
kernel,
marker,
/* singleton = */ false,
async |proxy| Some(proxy),
bind,
);
}
/// Registers a singleton driver that binds at most one service instance at a time on the
/// kernel's main async executor, ignoring additional instances while bound and promoting a
/// remaining active instance if the bound instance disconnects.
pub fn register_singleton<Bind>(kernel: &Kernel, marker: S, bind: Bind)
where
Bind: FnMut(&CurrentTask, Arc<S::Proxy>, &mut ServiceBindingScope) + Send + 'static,
{
Self::register_driver(
kernel,
marker,
/* singleton = */ true,
async |proxy| Some(proxy),
bind,
);
}
/// Registers a driver that probes each discovered service instance asynchronously (without
/// holding kernel locks) and, when `probe` returns `Some(probed)`, invokes `bind` on the
/// kernel's main async executor with
/// [`KernelThreads::system_task`](crate::task::KernelThreads::system_task) and a
/// [`ServiceBindingScope`].
///
/// `probe` futures may run concurrently across instances, whereas `bind` and teardown
/// invocations are serialized per driver after synchronously tearing down any stale bindings.
/// Any active service instances discovered before this call are replayed to the driver on the
/// kernel's main async executor.
pub fn register_with_probe<P, Probe, ProbeFut, Bind>(
kernel: &Kernel,
marker: S,
probe: Probe,
bind: Bind,
) where
P: Send + 'static,
Probe: Fn(Arc<S::Proxy>) -> ProbeFut + Send + Sync + 'static,
ProbeFut: Future<Output = Option<P>> + Send + 'static,
Bind: FnMut(&CurrentTask, P, &mut ServiceBindingScope) + Send + 'static,
{
Self::register_driver(kernel, marker, /* singleton = */ false, probe, bind);
}
fn ensure_watcher_started(kernel: &Kernel) -> Arc<ServiceWatcherState<S>> {
let state = kernel.expando.get::<ServiceWatcherState<S>>();
if !state.watch_started.swap(true, Ordering::AcqRel) {
let weak_kernel = kernel.weak_self.clone();
let state_for_task = state.clone();
kernel.kthreads.spawn_future(
move || async move {
// Open the service directory inside the future so its channel receiver is
// registered on the `kthreads` executor rather than the caller's thread-local
// executor.
let result = match fclient::open_service::<S>() {
Ok(service_dir) => {
state_for_task.watch_service_dir(&weak_kernel, service_dir).await
}
Err(e) => Err(e.context("failed to open service directory")),
};
if let Err(e) = result {
log_info!("{}: service unavailable: {e:#}", S::SERVICE_NAME);
}
},
S::SERVICE_NAME,
);
}
state
}
fn register_driver<P, Probe, ProbeFut, Bind>(
kernel: &Kernel,
_marker: S,
singleton: bool,
probe: Probe,
bind: Bind,
) where
P: Send + 'static,
Probe: Fn(Arc<S::Proxy>) -> ProbeFut + Send + Sync + 'static,
ProbeFut: Future<Output = Option<P>> + Send + 'static,
Bind: FnMut(&CurrentTask, P, &mut ServiceBindingScope) + Send + 'static,
{
let state = Self::ensure_watcher_started(kernel);
let driver: Arc<dyn ServiceDriver<S>> = DriverEntry::new(singleton, probe, bind);
let existing_instances = {
let mut inner = state.inner.lock();
inner.instances.retain(|entry| entry.active.load(Ordering::Acquire));
let instances = inner.instances.clone();
inner.drivers.push(driver.clone());
instances
};
if !existing_instances.is_empty() {
let weak_kernel = kernel.weak_self.clone();
kernel.kthreads.spawn_future(
move || async move {
futures::future::join_all(
existing_instances
.into_iter()
.map(|instance| driver.handle_instance(&state, &weak_kernel, instance)),
)
.await;
},
"service_watcher_replay",
);
}
}
}
struct SyncServiceInstanceOpener {
service_dir: Arc<fio::DirectorySynchronousProxy>,
instance_name: String,
}
impl MemberOpener for SyncServiceInstanceOpener {
fn open_member(&self, member: &str, server_end: zx::Channel) -> Result<(), fidl::Error> {
let path = format!("{}/{}", self.instance_name, member);
self.service_dir.open(
&path,
fio::Flags::PROTOCOL_SERVICE,
&fio::Options::default(),
server_end,
)
}
fn instance_name(&self) -> &str {
&self.instance_name
}
}
/// Connects `service_dir` to the per-[`Kernel`] [`ServiceWatcher`] for `S` on the kernel's main
/// async executor while suppressing the default `/svc/<S::SERVICE_NAME>` lookup.
#[cfg(test)]
pub(crate) fn watch_service_dir_for_testing<S>(kernel: &Kernel, service_dir: fio::DirectoryProxy)
where
S: ServiceMarker<Proxy: Send + Sync + 'static>,
{
let state = kernel.expando.get::<ServiceWatcherState<S>>();
state.watch_started.store(true, Ordering::Release);
let weak_kernel = kernel.weak_self.clone();
kernel.kthreads.spawn_future(
move || async move {
let _ = state.watch_service_dir(&weak_kernel, service_dir).await;
},
"service_watcher_test_dir",
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::device::mem::DevNull;
use crate::device::{DeviceMode, simple_device_ops};
use crate::testing::spawn_kernel_and_run;
use fidl_fuchsia_hardware_power_stats as fpowerstats;
use futures::channel::{mpsc, oneshot};
use starnix_uapi::error;
use std::collections::HashSet;
use vfs::directory::helper::DirectlyMutable as _;
use vfs::directory::immutable::Simple;
fn start_test_service_dir(kernel: &Kernel) -> Arc<Simple> {
let dir = vfs::directory::immutable::simple();
let proxy = vfs::directory::serve_read_only(dir.clone(), vfs::ExecutionScope::new());
watch_service_dir_for_testing::<fpowerstats::ServiceMarker>(kernel, proxy);
dir
}
fn add_instance(
dir: &Arc<Simple>,
name: &str,
on_open: impl Fn(zx::Channel) + Send + Sync + 'static,
) {
let instance_dir = vfs::pseudo_directory! {
"device" => vfs::service::endpoint(move |_scope, channel| {
on_open(channel.into_zx_channel());
}),
};
dir.add_entry_may_overwrite(name, instance_dir, /* overwrite = */ true)
.expect("add service instance");
}
#[::fuchsia::test]
async fn watches_multiple_instances_concurrently() {
spawn_kernel_and_run(async |current_task| {
let kernel = current_task.kernel();
let dir = vfs::directory::immutable::simple();
add_instance(&dir, "first", |_| {});
add_instance(&dir, "second", |_| {});
let (second_tx, second_rx) = oneshot::channel();
let second_tx = Arc::new(Mutex::new(Some(second_tx)));
let second_rx = Arc::new(Mutex::new(Some(second_rx)));
let (done_tx, done_rx) = oneshot::channel();
let done_tx = Arc::new(Mutex::new(Some(done_tx)));
let seen = Arc::new(Mutex::new(HashSet::new()));
ServiceWatcher::register_with_probe(
kernel,
fpowerstats::ServiceMarker,
move |instance| {
let second_tx = second_tx.clone();
let second_rx = second_rx.clone();
async move {
let name = instance.instance_name().to_string();
if name == "first" {
// Wait for `"second"` to enter `probe` before completing `"first"`,
// verifying that `probe` runs concurrently across instances.
let rx = second_rx.lock().take().expect("second_rx taken once");
let _ = rx.await;
} else if name == "second" {
if let Some(tx) = second_tx.lock().take() {
let _ = tx.send(());
}
}
Some(name)
}
},
move |_current_task, name, _scope| {
let mut seen = seen.lock();
seen.insert(name);
if seen.len() == 2
&& let Some(tx) = done_tx.lock().take()
{
let _ = tx.send(std::mem::take(&mut *seen));
}
},
);
let proxy = vfs::directory::serve_read_only(dir, vfs::ExecutionScope::new());
watch_service_dir_for_testing::<fpowerstats::ServiceMarker>(kernel, proxy);
let seen = done_rx.await.expect("both instances discovered concurrently");
assert_eq!(seen, HashSet::from(["first".to_string(), "second".to_string()]));
})
.await;
}
#[::fuchsia::test]
async fn fans_out_to_multiple_drivers_and_replays_to_late_subscribers() {
spawn_kernel_and_run(async |current_task| {
let kernel = current_task.kernel();
let dir = start_test_service_dir(kernel);
let (early_tx, mut early_rx) = mpsc::unbounded();
ServiceWatcher::register(
kernel,
fpowerstats::ServiceMarker,
move |_current_task, instance, _scope| {
let _ = early_tx.unbounded_send(instance.instance_name().to_string());
},
);
add_instance(&dir, "first", |_| {});
assert_eq!(early_rx.next().await.as_deref(), Some("first"));
// Register a second subscriber after `"first"` has already been discovered.
let (late_tx, mut late_rx) = mpsc::unbounded();
ServiceWatcher::register(
kernel,
fpowerstats::ServiceMarker,
move |_current_task, instance, _scope| {
let _ = late_tx.unbounded_send(instance.instance_name().to_string());
},
);
assert_eq!(late_rx.next().await.as_deref(), Some("first"));
// Subsequent instances fan out to both early and late subscribers.
add_instance(&dir, "second", |_| {});
assert_eq!(early_rx.next().await.as_deref(), Some("second"));
assert_eq!(late_rx.next().await.as_deref(), Some("second"));
})
.await;
}
#[::fuchsia::test]
async fn tears_down_scoped_devices_and_hooks_on_peer_close() {
spawn_kernel_and_run(async |current_task| {
let kernel = current_task.kernel();
let dir = start_test_service_dir(kernel);
let (event_tx, mut event_rx) = mpsc::unbounded();
let (all_seen_tx, mut all_seen_rx) = mpsc::unbounded();
let devts = Arc::new(Mutex::new(Vec::new()));
let devts_for_bind = devts.clone();
ServiceWatcher::register_singleton(
kernel,
fpowerstats::ServiceMarker,
move |current_task, instance, scope| {
let name = instance.instance_name().to_string();
let proxy =
Arc::new(instance.connect_to_device_sync().expect("connect to device"));
scope.monitor_channel(proxy);
let tx1 = event_tx.clone();
let name1 = name.clone();
scope.on_disconnect(move |_| {
let _ = tx1.unbounded_send(format!("hook1:{name1}"));
});
let tx2 = event_tx.clone();
let name2 = name.clone();
scope.on_disconnect(move |_| {
let _ = tx2.unbounded_send(format!("hook2:{name2}"));
});
let kernel = current_task.kernel();
let misc_dev = scope.add_scoped_device(
kernel
.device_registry
.register_misc_device(
kernel,
"watcher_misc".into(),
simple_device_ops::<DevNull>,
)
.expect("register misc device"),
);
devts_for_bind.lock().push(misc_dev.metadata().expect("metadata").devt);
let _ = event_tx.unbounded_send(format!("bind:{name}"));
},
);
ServiceWatcher::register(
kernel,
fpowerstats::ServiceMarker,
move |_current_task, instance, _scope| {
let _ = all_seen_tx.unbounded_send(instance.instance_name().to_string());
},
);
let (first_srv_tx, mut first_srv_rx) = mpsc::unbounded();
add_instance(&dir, "first", move |ch| {
let _ = first_srv_tx.unbounded_send(ch);
});
assert_eq!(event_rx.next().await.as_deref(), Some("bind:first"));
assert_eq!(all_seen_rx.next().await.as_deref(), Some("first"));
let first_server = first_srv_rx.next().await.expect("first server channel");
let registry = &kernel.device_registry;
let first_devt = devts.lock()[0];
assert!(
registry.objects.root.lookup("devices/virtual/misc/watcher_misc".into()).is_some()
);
assert!(registry.get_device(first_devt, DeviceMode::Char).is_ok());
// Adding `"second"` while `"first"` is active is ignored by the singleton driver.
let (second_srv_tx, mut second_srv_rx) = mpsc::unbounded();
add_instance(&dir, "second", move |ch| {
let _ = second_srv_tx.unbounded_send(ch);
});
assert_eq!(all_seen_rx.next().await.as_deref(), Some("second"));
assert!(event_rx.try_recv().is_err());
// Closing `"first"` runs teardown hooks in reverse registration order, frees the misc
// device ID, and promotes `"second"`.
drop(first_server);
assert_eq!(event_rx.next().await.as_deref(), Some("hook2:first"));
assert_eq!(event_rx.next().await.as_deref(), Some("hook1:first"));
assert_eq!(event_rx.next().await.as_deref(), Some("bind:second"));
let second_server = second_srv_rx.next().await.expect("second server channel");
assert_eq!(devts.lock()[1], first_devt);
// Closing `"second"` tears down the promoted binding and removes the misc device.
drop(second_server);
assert_eq!(event_rx.next().await.as_deref(), Some("hook2:second"));
assert_eq!(event_rx.next().await.as_deref(), Some("hook1:second"));
assert!(
registry.objects.root.lookup("devices/virtual/misc/watcher_misc".into()).is_none()
);
assert_eq!(
registry.get_device(first_devt, DeviceMode::Char).map(|_| ()),
error!(ENODEV)
);
})
.await;
}
#[::fuchsia::test]
async fn tears_down_binding_on_directory_remove_or_replace() {
spawn_kernel_and_run(async |current_task| {
let kernel = current_task.kernel();
let dir = start_test_service_dir(kernel);
let (event_tx, mut event_rx) = mpsc::unbounded();
let mut generation = 0u32;
ServiceWatcher::register(
kernel,
fpowerstats::ServiceMarker,
move |current_task, _instance, scope| {
generation += 1;
let current_gen = generation;
let tx_disconnect = event_tx.clone();
scope.on_disconnect(move |_| {
let _ = tx_disconnect.unbounded_send(format!("disconnect:{current_gen}"));
});
let kernel = current_task.kernel();
scope.add_scoped_device(
kernel
.device_registry
.register_misc_device(
kernel,
"watcher_replace_misc".into(),
simple_device_ops::<DevNull>,
)
.expect("register misc device"),
);
let _ = event_tx.unbounded_send(format!("bind:{current_gen}"));
},
);
add_instance(&dir, "first", |_| {});
assert_eq!(event_rx.next().await.as_deref(), Some("bind:1"));
let root = &kernel.device_registry.objects.root;
assert!(root.lookup("devices/virtual/misc/watcher_replace_misc".into()).is_some());
// Replacing `"first"` in the watched directory tears down the existing binding (even
// without a monitored channel) before binding the replacement.
add_instance(&dir, "first", |_| {});
assert_eq!(event_rx.next().await.as_deref(), Some("disconnect:1"));
assert_eq!(event_rx.next().await.as_deref(), Some("bind:2"));
assert!(root.lookup("devices/virtual/misc/watcher_replace_misc".into()).is_some());
// Removing `"first"` from the directory tears down the active binding.
dir.remove_entry("first", /* must_be_directory = */ false)
.expect("remove service instance");
assert_eq!(event_rx.next().await.as_deref(), Some("disconnect:2"));
assert!(root.lookup("devices/virtual/misc/watcher_replace_misc".into()).is_none());
})
.await;
}
#[::fuchsia::test]
async fn stops_watching_when_kernel_is_dropped() {
let dir = vfs::directory::immutable::simple();
add_instance(&dir, "first", |_| {});
let service_dir = vfs::directory::serve_read_only(dir, vfs::ExecutionScope::new());
let state = Arc::new(ServiceWatcherState::<fpowerstats::ServiceMarker>::default());
let called = Arc::new(AtomicBool::new(false));
let called_for_bind = called.clone();
state.inner.lock().drivers.push(DriverEntry::new(
/* singleton = */ false,
async |_| Some(()),
move |_, (), _| {
called_for_bind.store(true, Ordering::SeqCst);
},
));
state
.watch_service_dir(&Weak::new(), service_dir)
.await
.expect("service watcher should exit cleanly when kernel is dropped");
assert!(!called.load(Ordering::SeqCst));
}
}