| // Copyright 2019 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 "unbind_task.h" |
| |
| #include <zircon/status.h> |
| |
| #include "coordinator.h" |
| #include "src/devices/lib/log/log.h" |
| |
| UnbindTask::UnbindTask(fbl::RefPtr<Device> device, UnbindTaskOpts opts, Completion completion) |
| : Task(device->coordinator->dispatcher(), std::move(completion), opts.post_on_create), |
| device_(std::move(device)), |
| do_unbind_(opts.do_unbind), |
| driver_host_requested_(opts.driver_host_requested) {} |
| |
| UnbindTask::~UnbindTask() = default; |
| |
| fbl::RefPtr<UnbindTask> UnbindTask::Create(fbl::RefPtr<Device> device, UnbindTaskOpts opts, |
| Completion completion) { |
| return fbl::MakeRefCounted<UnbindTask>(std::move(device), opts, std::move(completion)); |
| } |
| |
| // Schedules the unbind tasks for the device's children. |
| void UnbindTask::ScheduleUnbindChildren() { |
| auto remove_task = device_->GetActiveRemove(); |
| if (remove_task == nullptr) { |
| LOGF(ERROR, "Unbind task failed, but no remove task exists for device %p '%s'", device_.get(), |
| device_->name().data()); |
| return; |
| } |
| |
| // Remove task needs to wait for the current unbind task to complete. |
| remove_task->AddDependency(fbl::RefPtr(this)); |
| |
| fbl::RefPtr<UnbindTask> proxy_unbind_task = nullptr; |
| if (device_->proxy() != nullptr) { |
| switch (device_->proxy()->state()) { |
| case Device::State::kDead: |
| // We are already in the process of unbinding ourselves and our children, |
| // no need to create a new one. |
| case Device::State::kUnbinding: |
| break; |
| // The created unbind task will wait for the init to complete. |
| case Device::State::kInitializing: |
| case Device::State::kSuspended: |
| // The created unbind task will wait for the suspend to complete. |
| case Device::State::kSuspending: |
| case Device::State::kResuming: |
| case Device::State::kResumed: |
| case Device::State::kActive: { |
| device_->proxy()->CreateUnbindRemoveTasks(UnbindTaskOpts{ |
| .do_unbind = false, .post_on_create = false, .driver_host_requested = false}); |
| |
| proxy_unbind_task = device_->proxy()->GetActiveUnbind(); |
| // The proxy's unbind task may have already completed, in which case we only |
| // have to wait on the remove task. |
| if (proxy_unbind_task) { |
| proxy_unbind_task->AddDependency(fbl::RefPtr(this)); |
| } |
| // The device should not be removed until its children have been removed. |
| remove_task->AddDependency(device_->proxy()->GetActiveRemove()); |
| } |
| } |
| // A device may have both a proxy device and children devices, |
| // so continue rather than returning early. |
| } |
| |
| auto children = device_->children(); |
| // Though we try to schedule the unbind tasks for both a device's proxy and its children, |
| // its possible for ScheduleRemove() to be called directly on a proxy unbind task, such as in the |
| // case of a forced remove. |
| // To handle this, we need to schedule unbind tasks for the proxy "children", which are actually |
| // stored in our parent's children list. |
| // This means we may end up adding the children as dependent on a proxy device twice, |
| // but that is handled by the task logic. |
| if (device_->flags & DEV_CTX_PROXY && device_->parent()) { |
| children = device_->parent()->children(); |
| } |
| |
| for (auto& child : children) { |
| // Use a switch statement here so that this gets reconsidered if we add |
| // more states. |
| switch (child.state()) { |
| case Device::State::kDead: |
| case Device::State::kUnbinding: |
| continue; |
| case Device::State::kInitializing: |
| case Device::State::kSuspended: |
| case Device::State::kSuspending: |
| case Device::State::kResuming: |
| case Device::State::kResumed: |
| case Device::State::kActive: |
| break; |
| } |
| child.CreateUnbindRemoveTasks( |
| UnbindTaskOpts{.do_unbind = true, .post_on_create = false, .driver_host_requested = false}); |
| |
| auto parent = device_->proxy() != nullptr ? device_->proxy() : device_; |
| |
| // The child unbind task may have already completed, in which case we only need to wait |
| // for the child's remove task. |
| auto child_unbind_task = child.GetActiveUnbind(); |
| if (child_unbind_task) { |
| auto parent_unbind_task = parent->GetActiveUnbind(); |
| if (parent_unbind_task) { |
| child_unbind_task->AddDependency(parent_unbind_task); |
| } |
| } |
| // Since the child is not dead, the remove task must exist. |
| auto child_remove_task = child.GetActiveRemove(); |
| ZX_ASSERT(child_remove_task != nullptr); |
| auto parent_remove_task = parent->GetActiveRemove(); |
| if (parent_remove_task) { |
| parent_remove_task->AddDependency(child_remove_task); |
| } |
| } |
| } |
| |
| void UnbindTask::Run() { |
| LOGF(INFO, "Running unbind task for device %p '%s', do_unbind %b", device_.get(), |
| device_->name().data(), do_unbind_); |
| |
| if (device_->state() == Device::State::kInitializing) { |
| auto init_task = device_->GetActiveInit(); |
| ZX_ASSERT(init_task != nullptr); |
| AddDependency(init_task); |
| return; |
| } |
| |
| // The device is currently suspending, wait for it to complete. |
| if (device_->state() == Device::State::kSuspending) { |
| auto suspend_task = device_->GetActiveSuspend(); |
| ZX_ASSERT(suspend_task != nullptr); |
| AddDependency(suspend_task); |
| return; |
| } |
| |
| if (device_->state() == Device::State::kResuming) { |
| auto resume_task = device_->GetActiveResume(); |
| ZX_ASSERT(resume_task != nullptr); |
| AddDependency(resume_task); |
| return; |
| } |
| |
| // We need to schedule the child tasks before completing the unbind task runs, |
| // as composite device disassociation may occur. |
| ScheduleUnbindChildren(); |
| |
| auto completion = [this](zx_status_t status) { |
| // If this unbind task failed, force remove all devices from the driver_host. |
| bool failed_unbind = status != ZX_OK && status != ZX_ERR_UNAVAILABLE; |
| if (failed_unbind && device_->state() != Device::State::kDead) { |
| LOGF(ERROR, "Unbind task failed, force removing device %p '%s': %s", device_.get(), |
| device_->name().data(), zx_status_get_string(status)); |
| device_->coordinator->RemoveDevice(device_, true /* forced */); |
| } |
| // The forced removal will schedule new unbind tasks if needed (e.g. for proxy tasks), |
| // so we should not propagate errors other than ZX_ERR_UNAVAILABLE. |
| Complete(status == ZX_OK ? ZX_OK : ZX_ERR_UNAVAILABLE); |
| }; |
| |
| // Check if we should send the unbind request to the driver_host. We do not want to send it if: |
| // - This device is not in a driver_host. This happens for the top-level devices like /sys |
| // provided by devcoordinator, or if the device has already been removed. |
| // - device_remove does not call unbind on the device. |
| bool send_unbind = (device_->host() != nullptr) && do_unbind_; |
| zx_status_t status = ZX_OK; |
| if (send_unbind) { |
| status = device_->SendUnbind(std::move(completion)); |
| if (status == ZX_OK) { |
| // Sent the unbind request, the driver_host will call our completion when ready. |
| return; |
| } |
| } |
| // Save a copy of the device in case this task's destructor runs after the |
| // completion returns. |
| fbl::RefPtr<Device> device = device_; |
| // No unbind request sent, need to call the completion now. |
| completion(status); |
| // Since the device didn't successfully send an Unbind request, it will not |
| // drop our unbind task reference. We need to drop it now unless the error was |
| // that the unbind request had already been sent (ZX_ERR_UNAVAILABLE). |
| if (status != ZX_ERR_UNAVAILABLE) { |
| device->DropUnbindTask(); |
| } |
| } |
| |
| RemoveTask::RemoveTask(fbl::RefPtr<Device> device, Completion completion) |
| : Task(device->coordinator->dispatcher(), std::move(completion), false /* post_on_create */), |
| device_(std::move(device)) {} |
| |
| RemoveTask::~RemoveTask() = default; |
| |
| fbl::RefPtr<RemoveTask> RemoveTask::Create(fbl::RefPtr<Device> device, Completion completion) { |
| return fbl::MakeRefCounted<RemoveTask>(std::move(device), std::move(completion)); |
| } |
| |
| void RemoveTask::Run() { |
| LOGF(INFO, "Running remove task for device %p '%s'", device_.get(), device_->name().data()); |
| auto completion = [this](zx_status_t status) { |
| // If this remove task failed, force remove all devices from the driver_host. |
| bool failed_remove = status != ZX_OK && status != ZX_ERR_UNAVAILABLE; |
| if (failed_remove && device_->state() != Device::State::kDead) { |
| LOGF(ERROR, "Remove task failed, forcing remove of device %p '%s': %s", device_.get(), |
| device_->name().data(), zx_status_get_string(status)); |
| device_->coordinator->RemoveDevice(device_, true /* forced */); |
| } |
| // The forced removal will schedule new remove tasks if needed (e.g. for proxy tasks), |
| // so we should not propagate errors other than ZX_ERR_UNAVAILABLE. |
| Complete(status == ZX_OK ? ZX_OK : ZX_ERR_UNAVAILABLE); |
| }; |
| |
| zx_status_t status = ZX_OK; |
| if (device_->host() != nullptr) { |
| status = device_->SendCompleteRemoval(std::move(completion)); |
| if (status == ZX_OK) { |
| // Sent the remove request, the driver_host will call our completion when ready. |
| return; |
| } |
| } |
| // Save a copy of the device in case this task's destructor runs after the |
| // completion returns. |
| fbl::RefPtr<Device> device = device_; |
| // No remove request sent, need to call the completion now. |
| completion(status); |
| // Since the device didn't successfully send an CompleteRemoval request, it will not |
| // drop our remove task reference. We need to drop it now unless the error was |
| // that the remove request had already been sent (ZX_ERR_UNAVAILABLE). |
| if (status != ZX_ERR_UNAVAILABLE) { |
| device->DropRemoveTask(); |
| } |
| } |