blob: a2e4c94de9f86f8eac7d078156c834af32e46787 [file] [log] [blame]
// Copyright 2021 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 "src/devices/bin/driver_manager/v1/device_manager.h"
#include <lib/ddk/driver.h>
#include "src/devices/bin/driver_manager/devfs.h"
#include "src/devices/lib/log/log.h"
namespace fdm = fuchsia_device_manager;
DeviceManager::DeviceManager(Coordinator* coordinator, DriverHostCrashPolicy crash_policy)
: coordinator_(coordinator), crash_policy_(crash_policy) {}
zx_status_t DeviceManager::AddDevice(
const fbl::RefPtr<Device>& parent, fidl::ClientEnd<fdm::DeviceController> device_controller,
fidl::ServerEnd<fdm::Coordinator> coordinator, const fdm::wire::DeviceProperty* props_data,
size_t props_count, const fdm::wire::DeviceStrProperty* str_props_data, size_t str_props_count,
std::string_view name, uint32_t protocol_id, std::string_view driver_path,
std::string_view args, bool skip_autobind, bool has_init, bool always_init, zx::vmo inspect,
zx::channel client_remote, fidl::ClientEnd<fio::Directory> outgoing_dir,
fbl::RefPtr<Device>* new_device) {
// If this is true, then |name_data|'s size is properly bounded.
static_assert(fdm::wire::kDeviceNameMax == ZX_DEVICE_NAME_MAX);
static_assert(fdm::wire::kPropertiesMax <= UINT32_MAX);
if (coordinator_->suspend_resume_manager()->InSuspend()) {
LOGF(ERROR, "Add device '%.*s' forbidden in suspend", static_cast<int>(name.size()),
name.data());
return ZX_ERR_BAD_STATE;
}
if (coordinator_->suspend_resume_manager()->InResume()) {
LOGF(ERROR, "Add device '%.*s' forbidden in resume", static_cast<int>(name.size()),
name.data());
return ZX_ERR_BAD_STATE;
}
if (parent->state() == Device::State::kUnbinding) {
LOGF(ERROR, "Add device '%.*s' forbidden while parent is unbinding",
static_cast<int>(name.size()), name.data());
return ZX_ERR_BAD_STATE;
}
// Convert the device properties and string properties.
fbl::Array<zx_device_prop_t> props(new zx_device_prop_t[props_count], props_count);
if (!props) {
return ZX_ERR_NO_MEMORY;
}
for (uint32_t i = 0; i < props_count; i++) {
props[i] = zx_device_prop_t{
.id = props_data[i].id,
.reserved = props_data[i].reserved,
.value = props_data[i].value,
};
}
fbl::Array<StrProperty> str_props(new StrProperty[str_props_count], str_props_count);
if (!str_props) {
return ZX_ERR_NO_MEMORY;
}
for (uint32_t i = 0; i < str_props_count; i++) {
str_props[i].key = str_props_data[i].key.get();
if (str_props_data[i].value.is_int_value()) {
str_props[i].value.emplace<StrPropValueType::Integer>(str_props_data[i].value.int_value());
} else if (str_props_data[i].value.is_str_value()) {
str_props[i].value.emplace<StrPropValueType::String>(
std::string(str_props_data[i].value.str_value().get()));
} else if (str_props_data[i].value.is_bool_value()) {
str_props[i].value.emplace<StrPropValueType::Bool>(str_props_data[i].value.bool_value());
} else if (str_props_data[i].value.is_enum_value()) {
str_props[i].value.emplace<StrPropValueType::Enum>(
std::string(str_props_data[i].value.enum_value().get()));
}
}
fbl::String name_str(name);
fbl::String driver_path_str(driver_path);
fbl::String args_str(args);
// TODO(fxbug.dev/43370): remove this check once init tasks can be enabled for all devices.
bool want_init_task = has_init || always_init;
fbl::RefPtr<Device> dev;
zx_status_t status = Device::Create(
coordinator_, parent, std::move(name_str), std::move(driver_path_str), std::move(args_str),
protocol_id, std::move(props), std::move(str_props), std::move(coordinator),
std::move(device_controller), want_init_task, skip_autobind, std::move(inspect),
std::move(client_remote), std::move(outgoing_dir), &dev);
if (status != ZX_OK) {
return status;
}
devices_.push_back(dev);
// Note that |dev->parent()| may not match |parent| here, so we should always
// use |dev->parent()|. This case can happen if |parent| refers to a device
// proxy.
// If we're creating a device that's using the fragment driver, inform the
// fragment.
if (dev->libname() == coordinator_->GetFragmentDriverUrl()) {
for (auto& cur_fragment : dev->parent()->fragments()) {
// Pick the first fragment that does not have a device added by the fragment
// driver.
if (cur_fragment.fragment_device() == nullptr &&
!cur_fragment.bound_device()->has_outgoing_directory()) {
cur_fragment.set_fragment_device(dev);
status = cur_fragment.composite()->TryAssemble();
if (status != ZX_OK && status != ZX_ERR_SHOULD_WAIT) {
LOGF(ERROR, "Failed to assemble composite device: %s", zx_status_get_string(status));
}
break;
}
}
}
VLOGF(1, "Added device %p '%s'", dev.get(), dev->name().data());
// TODO(fxbug.dev/43370): remove this once init tasks can be enabled for all devices.
if (!want_init_task) {
status = dev->SignalReadyForBind();
if (status != ZX_OK) {
return status;
}
VLOGF(1, "Published device %p '%s' args='%s' props=%zu parent=%p", dev.get(),
dev->name().data(), dev->args().data(), dev->props().size(), dev->parent().get());
}
*new_device = std::move(dev);
return ZX_OK;
}
zx_status_t DeviceManager::AddCompositeDevice(const fbl::RefPtr<Device>& dev, std::string_view name,
fdm::wire::CompositeDeviceDescriptor comp_desc) {
std::unique_ptr<CompositeDevice> new_device;
zx_status_t status = CompositeDevice::Create(name, std::move(comp_desc), &new_device);
if (status != ZX_OK) {
return status;
}
// Try to bind the new composite device specification against existing
// devices.
for (auto& dev : devices_) {
if (!dev.is_bindable() && !dev.is_composite_bindable()) {
continue;
}
new_device->TryMatchBindFragments(fbl::RefPtr(&dev));
}
composite_devices_.push_back(std::move(new_device));
return ZX_OK;
}
void DeviceManager::AddToDevices(fbl::RefPtr<Device> new_device) { devices_.push_back(new_device); }
void DeviceManager::HandleNewDevice(const fbl::RefPtr<Device>& dev) {
// If the device has a proxy, we actually want to wait for the proxy device to be
// created and connect to that.
if (!(dev->flags & DEV_CTX_MUST_ISOLATE)) {
zx::channel client_remote = dev->take_client_remote();
if (client_remote.is_valid()) {
zx_status_t status =
devfs_connect(dev.get(), fidl::ServerEnd<fio::Node>(std::move(client_remote)));
if (status != ZX_OK) {
LOGF(ERROR, "Failed to connect to service from proxy device '%s': %s", dev->name().data(),
zx_status_get_string(status));
}
}
}
// TODO(tesienbe): We probably should do something with the return value
// from this...
coordinator_->bind_driver_manager()->BindDevice(dev, {} /* libdrvname */, true /* new device */);
}
void DeviceManager::ScheduleRemove(const fbl::RefPtr<Device>& dev) {
dev->CreateUnbindRemoveTasks(
UnbindTaskOpts{.do_unbind = false, .post_on_create = true, .driver_host_requested = false});
}
void DeviceManager::ScheduleDriverHostRequestedRemove(const fbl::RefPtr<Device>& dev,
bool do_unbind) {
dev->CreateUnbindRemoveTasks(UnbindTaskOpts{
.do_unbind = do_unbind, .post_on_create = true, .driver_host_requested = true});
}
void DeviceManager::ScheduleDriverHostRequestedUnbindChildren(const fbl::RefPtr<Device>& parent) {
for (auto& child : parent->children()) {
child.CreateUnbindRemoveTasks(
UnbindTaskOpts{.do_unbind = true, .post_on_create = true, .driver_host_requested = true});
}
}
void DeviceManager::ScheduleUnbindRemoveAllDevices(const fbl::RefPtr<DriverHost> driver_host) {
for (auto& dev : driver_host->devices()) {
// This will also call on all the children of the device.
dev.CreateUnbindRemoveTasks(
UnbindTaskOpts{.do_unbind = true, .post_on_create = true, .driver_host_requested = false});
}
}
zx_status_t DeviceManager::RemoveDevice(const fbl::RefPtr<Device>& dev, bool forced) {
if (forced && crash_policy_ == DriverHostCrashPolicy::kRebootSystem) {
// TODO(fxbug.dev/67168): Trigger system restart more gracefully.
ZX_ASSERT(false);
}
dev->inc_num_removal_attempts();
if (dev->state() == Device::State::kDead) {
// This should not happen
LOGF(ERROR, "Cannot remove device %p '%s' twice", dev.get(), dev->name().data());
return ZX_ERR_BAD_STATE;
}
if (dev->flags & DEV_CTX_IMMORTAL) {
// This too should not happen
LOGF(ERROR, "Cannot remove device %p '%s' (immortal)", dev.get(), dev->name().data());
return ZX_ERR_BAD_STATE;
}
LOGF(INFO, "Removing device %p '%s' parent=%p", dev.get(), dev->name().data(),
dev->parent().get());
dev->set_state(Device::State::kDead);
// remove from devfs, preventing further OPEN attempts
devfs_unpublish(dev.get());
// Mark any suspend that's in-flight as completed, since if the device is
// removed it should be in its lowest state.
// TODO(teisenbe): Should we mark it as failed if this is a forced removal?
dev->CompleteSuspend(ZX_OK);
dev->CompleteInit(ZX_ERR_UNAVAILABLE);
fbl::RefPtr<DriverHost> dh = dev->host();
bool driver_host_dying = (dh != nullptr && (dh->flags() & DriverHost::Flags::kDying));
if (forced || driver_host_dying) {
// We are force removing all devices in the driver_host, so force complete any outstanding
// tasks.
dev->CompleteUnbind(ZX_ERR_UNAVAILABLE);
dev->CompleteRemove(ZX_ERR_UNAVAILABLE);
// If there is a device proxy, we need to create a new unbind task for it.
// For non-forced removals, the unbind task will handle scheduling the proxy removal.
if (dev->proxy()) {
ScheduleRemove(dev->proxy());
}
if (dev->new_proxy()) {
ScheduleRemove(dev->new_proxy());
}
} else {
// We should not be removing a device while the unbind task is still running.
ZX_ASSERT(dev->GetActiveUnbind() == nullptr);
}
// Check if this device is a composite device, and if so disconnects from it
if (dev->composite()) {
CompositeDevice* composite = dev->composite();
for (auto itr = composite->bound_fragments().begin();
itr != composite->bound_fragments().end();) {
CompositeDeviceFragment& bound_fragment = *itr;
// Advance the iterator because we will erase the current element from the list in Unbind.
++itr;
// Ignore any fragments that are associated with a fragment device.
if (bound_fragment.fragment_device() != nullptr) {
continue;
}
ZX_ASSERT_MSG(bound_fragment.bound_device() != nullptr,
"Composite device has an unbound fragment in bound fragments list");
const fbl::RefPtr<Device>& parent = bound_fragment.bound_device();
// Erase from the parent the fragment that matches this composite device.
CompositeDeviceFragment* fragment =
parent->fragments().erase_if([&composite](const CompositeDeviceFragment& fragment) {
return fragment.composite() == composite;
});
ZX_ASSERT_MSG(fragment != nullptr, "Unable to find fragment in bound device's fragments");
fragment->Unbind();
}
composite->Remove();
}
// Check if this device is a composite fragment device
if (dev->libname() == coordinator_->GetFragmentDriverUrl()) {
// If it is, then its parent will know about which one (since the parent
// is the actual device matched by the fragment description).
const auto& parent = dev->parent();
if (parent) {
// Erase from the parent the fragment that matches this fragment device.
CompositeDeviceFragment* fragment =
parent->fragments().erase_if([&dev](const CompositeDeviceFragment& fragment) {
return fragment.fragment_device() == dev;
});
ZX_ASSERT_MSG(fragment != nullptr,
"Unable to find fragment matching fragment device in parent");
fragment->Unbind();
}
}
// Detach from driver_host
if (dh != nullptr) {
// We're holding on to a reference to the driver_host through |dh|.
// This is necessary to prevent it from being freed in the middle of
// the code below.
dev->set_host(nullptr);
// If we are responding to a disconnect, we'll remove all the other devices
// on this driver_host too. A side-effect of this is that the driver_host
// will be released, as well as any proxy devices.
if (forced) {
dh->flags() |= DriverHost::Flags::kDying;
fbl::RefPtr<Device> next;
fbl::RefPtr<Device> last;
while (!dh->devices().is_empty()) {
next = fbl::RefPtr(&dh->devices().front());
if (last == next) {
// This shouldn't be possible, but let's not infinite-loop if it happens
LOGF(FATAL, "Failed to remove device %p '%s' from driver_host", next.get(),
next->name().data());
}
RemoveDevice(next, false);
last = std::move(next);
}
// TODO: set a timer so if this driver_host does not finish dying
// in a reasonable amount of time, we fix the glitch.
}
dh.reset();
}
// if we have a parent, disconnect and downref it
fbl::RefPtr<Device> parent = dev->parent();
if (parent != nullptr) {
dev->DetachFromParent();
if (!(dev->flags & DEV_CTX_PROXY)) {
if (parent->children().is_empty()) {
parent->flags &= (~DEV_CTX_BOUND);
// TODO: This code is to cause the bind process to
// restart and get a new driver_host to be launched
// when a driver_host dies. It should probably be
// more tied to driver_host teardown than it is.
// IF the policy is set such that we take action
// AND we are the last child of our parent
// AND our parent is not itself dead
// AND our parent is a BUSDEV
// AND our parent's driver_host is not dying
// THEN we will want to rebind our parent
if (crash_policy_ == DriverHostCrashPolicy::kRestartDriverHost &&
parent->state() != Device::State::kDead && parent->flags & DEV_CTX_MUST_ISOLATE &&
((parent->host() == nullptr) ||
!(parent->host()->flags() & DriverHost::Flags::kDying))) {
VLOGF(1, "Bus device %p '%s' is unbound", parent.get(), parent->name().data());
if (parent->retries > 0) {
LOGF(INFO, "Suspected crash: attempting to re-bind %s", parent->name().data());
// Add device with an exponential backoff.
zx_status_t r = parent->SignalReadyForBind(parent->backoff);
if (r != ZX_OK) {
return r;
}
parent->backoff *= 2;
parent->retries--;
}
}
}
}
}
if (!(dev->flags & DEV_CTX_PROXY)) {
// remove from list of all devices
devices_.erase(*dev);
}
return ZX_OK;
}