text: // Copyright 2016 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/sys/appmgr/component_controller_impl.h"
  
  #include <fuchsia/inspect/cpp/fidl.h>
  #include <lib/async/default.h>
  #include <lib/fdio/directory.h>
  #include <lib/fdio/fd.h>
  #include <lib/fdio/fdio.h>
  #include <lib/fit/function.h>
  #include <lib/fpromise/bridge.h>
  #include <lib/inspect/service/cpp/service.h>
  #include <lib/syslog/cpp/macros.h>
  #include <lib/trace/event.h>
  #include <lib/zx/job.h>
  #include <lib/zx/status.h>
  #include <zircon/errors.h>
  #include <zircon/types.h>
  
  #include <cinttypes>
  #include <string>
  #include <utility>
  
  #include <fbl/string_printf.h>
  #include <task-utils/walker.h>
  
  #include "fbl/ref_ptr.h"
  #include "lib/inspect/service/cpp/service.h"
  #include "lib/vfs/cpp/service.h"
  #include "src/lib/fsl/handles/object_info.h"
  #include "src/lib/storage/vfs/cpp/pseudo_file.h"
  #include "src/lib/storage/vfs/cpp/remote_dir.h"
  #include "src/lib/storage/vfs/cpp/service.h"
  #include "src/sys/appmgr/component_container.h"
  #include "src/sys/appmgr/namespace.h"
  #include "src/sys/appmgr/realm.h"
  
  namespace component {
  
  using fuchsia::sys::TerminationReason;
  
  namespace {
  zx::process DuplicateProcess(const zx::process& process) {
    zx::process ret;
    zx_status_t status = process.duplicate(ZX_RIGHT_SAME_RIGHTS, &ret);
    if (status != ZX_OK) {
      FX_LOGS(ERROR) << "Failed to duplicate process handle: " << status;
    }
    return ret;
  }
  
  // TODO(fxbug.dev/46803): The out/diagnostics directory propagation for runners includes a retry.
  // The reason of this is that flutter fills the out/ directory *after*
  // serving it. Therefore we need to watch that directory to notify.
  // Sadly the PseudoDir exposed in the SDK (and used by flutter) returns ZX_ERR_NOT_SUPPORTED on
  // Watch. A solution using a watcher is implemented in fxr/366977 pending watch support.
  const uint32_t MAX_RETRIES_OUT_DIAGNOSTICS = 30;
  const uint32_t OUT_DIAGNOSTICS_INITIAL_RETRY_DELAY_MS = 500;
  const uint32_t OUT_DIAGNOSTICS_MAXIMUM_DELAY_MS = 15000;
  
  }  // namespace
  
  ComponentRequestWrapper::ComponentRequestWrapper(
      fidl::InterfaceRequest<fuchsia::sys::ComponentController> request, int64_t default_return,
      fuchsia::sys::TerminationReason default_reason)
      : request_(std::move(request)), return_code_(default_return), reason_(default_reason) {}
  
  ComponentRequestWrapper::~ComponentRequestWrapper() {
    if (request_) {
      FailedComponentController failed(return_code_, reason_, std::move(request_));
    }
  }
  
  ComponentRequestWrapper::ComponentRequestWrapper(ComponentRequestWrapper&& other) {
    *this = std::move(other);
  }
  
  void ComponentRequestWrapper::operator=(ComponentRequestWrapper&& other) {
    request_ = std::move(other.request_);
    return_code_ = std::move(other.return_code_);
    reason_ = std::move(other.reason_);
  }
  
  void ComponentRequestWrapper::SetReturnValues(int64_t return_code, TerminationReason reason) {
    return_code_ = return_code;
    reason_ = reason;
  }
  
  FailedComponentController::FailedComponentController(
      int64_t return_code, TerminationReason termination_reason,
      fidl::InterfaceRequest<fuchsia::sys::ComponentController> controller)
      : binding_(this), return_code_(return_code), termination_reason_(termination_reason) {
    if (controller) {
      binding_.Bind(std::move(controller));
    }
  }
  
  FailedComponentController::~FailedComponentController() {
    // This can be false if other side of the channel dies before this object dies.
    if (binding_.is_bound()) {
      binding_.events().OnTerminated(return_code_, termination_reason_);
    }
  }
  
  void FailedComponentController::Kill() {}
  
  void FailedComponentController::Detach() {}
  
  ComponentControllerBase::ComponentControllerBase(
      fidl::InterfaceRequest<fuchsia::sys::ComponentController> request, std::string url,
      std::string args, std::string label, std::string hub_instance_id, fxl::RefPtr<Namespace> ns,
      zx::channel exported_dir, zx::channel client_request, uint32_t diagnostics_max_retries)
      : executor_(async_get_default_dispatcher()),
        binding_(this),
        label_(std::move(label)),
        hub_instance_id_(std::move(hub_instance_id)),
        url_(std::move(url)),
        hub_(fbl::MakeRefCounted<fs::PseudoDir>()),
        ns_(std::move(ns)),
        weak_ptr_factory_(this),
        diagnostics_max_retries_(diagnostics_max_retries),
        diagnostics_retry_backoff_ms_(OUT_DIAGNOSTICS_INITIAL_RETRY_DELAY_MS) {
    if (request.is_valid()) {
      binding_.Bind(std::move(request));
      binding_.set_error_handler([this](zx_status_t /*status*/) { Kill(); });
    }
    if (!exported_dir) {
      return;
    }
    exported_dir_.Bind(std::move(exported_dir), async_get_default_dispatcher());
  
    if (client_request) {
      fdio_service_connect_at(exported_dir_.channel().get(), "svc", client_request.release());
    }
  
    ns_->set_component_id(hub_instance_id_);
    hub_.SetName(label_);
    hub_.AddEntry("url", url_);
    hub_.AddEntry("args", std::move(args));
    exported_dir_->Clone(fuchsia::io::OpenFlags::DESCRIBE | fuchsia::io::OpenFlags::RIGHT_READABLE |
                             fuchsia::io::OpenFlags::RIGHT_WRITABLE,
                         cloned_exported_dir_.NewRequest());
  
    cloned_exported_dir_.events().OnOpen = [this](zx_status_t status,
                                                  std::unique_ptr<fuchsia::io::NodeInfo> /*info*/) {
      if (status != ZX_OK) {
        FX_LOGS(WARNING) << "could not bind out directory for component" << label_ << "): " << status;
        return;
      }
      out_ready_ = true;
      auto output_dir =
          fbl::MakeRefCounted<fs::RemoteDir>(cloned_exported_dir_.Unbind().TakeChannel());
      hub_.PublishOut(std::move(output_dir));
      NotifyDiagnosticsDirReady(diagnostics_max_retries_);
      TRACE_DURATION_BEGIN("appmgr", "ComponentController::OnDirectoryReady");
      SendOnDirectoryReadyEvent();
      TRACE_DURATION_END("appmgr", "ComponentController::OnDirectoryReady");
    };
  
    cloned_exported_dir_.set_error_handler(
        [this](zx_status_t status) { cloned_exported_dir_.Unbind(); });
  }
  
  void ComponentControllerBase::NotifyDiagnosticsDirReady(uint32_t max_retries) {
    auto promise =
        GetDiagnosticsDir()
            .and_then([self = weak_ptr_factory_.GetWeakPtr()](
                          fidl::InterfaceHandle<fuchsia::io::Directory>& dir) {
              if (self) {
                self->ns_->NotifyComponentDiagnosticsDirReady(self->url_, self->label_,
                                                              self->hub_instance_id_, std::move(dir));
              }
            })
            .or_else([self = weak_ptr_factory_.GetWeakPtr(), max_retries](zx_status_t& status) {
              if (self && status == ZX_ERR_NOT_FOUND) {
                zx::duration next_backoff_duration = zx::msec(self->diagnostics_retry_backoff_ms_);
                self->diagnostics_retry_backoff_ms_ = std::min(
                    self->diagnostics_retry_backoff_ms_ + OUT_DIAGNOSTICS_INITIAL_RETRY_DELAY_MS,
                    OUT_DIAGNOSTICS_MAXIMUM_DELAY_MS);
                async::PostDelayedTask(
                    self->executor_.dispatcher(),
                    [self = std::move(self), max_retries]() {
                      if (self && max_retries > 0) {
                        self->NotifyDiagnosticsDirReady(max_retries - 1);
                      }
                    },
                    next_backoff_duration);
              }
              return fpromise::error(status);
            });
    executor_.schedule_task(std::move(promise));
  }
  
  fpromise::promise<fidl::InterfaceHandle<fuchsia::io::Directory>, zx_status_t>
  ComponentControllerBase::GetDir(std::string path) {
    // This error would occur if the method was called when the component out/ directory wasn't ready
    // yet. This can be triggered when a listener is attached to a realm and notifies about existing
    // components. It could happen that the component exists, but its out is not ready yet. Under such
    // scenario, the listener will receive a START event for the existing component, but won't
    // receive a DIAGNOSTICS_DIR_READY event during the existing flow. The DIAGNOSTICS_READY_EVENT
    // will be triggered later once the out/ directory is ready if the component exposes a
    // diagnostics directory.
    if (!out_ready_) {
      return fpromise::make_result_promise<fidl::InterfaceHandle<fuchsia::io::Directory>>(
          fpromise::error(ZX_ERR_BAD_STATE));
    }
    fuchsia::io::NodePtr diagnostics_dir_node;
    fpromise::bridge<void, zx_status_t> bridge;
    diagnostics_dir_node.events().OnOpen =
        [completer = std::move(bridge.completer), label = label_](
            zx_status_t status, std::unique_ptr<fuchsia::io::NodeInfo> node_info) mutable {
          if (status != ZX_OK) {
            completer.complete_error(status);
          } else if (!node_info) {
            completer.complete_error(ZX_ERR_NOT_FOUND);
          } else if (!node_info->is_directory()) {
            completer.complete_error(ZX_ERR_NOT_DIR);
          } else {
            completer.complete_ok();
          }
        };
  
    const fuchsia::io::OpenFlags flags = fuchsia::io::OpenFlags::DESCRIBE |
                                         fuchsia::io::OpenFlags::RIGHT_READABLE |
                                         fuchsia::io::OpenFlags::RIGHT_WRITABLE;
    exported_dir_->Open(flags, 0u /* mode */, path, diagnostics_dir_node.NewRequest());
    return bridge.consumer.promise().and_then([diagnostics_dir_node =
                                                   std::move(diagnostics_dir_node)]() mutable {
      auto diagnostics_dir =
          fidl::InterfaceHandle<fuchsia::io::Directory>(diagnostics_dir_node.Unbind().TakeChannel());
      return fpromise::make_result_promise<fidl::InterfaceHandle<fuchsia::io::Directory>,
                                           zx_status_t>(fpromise::ok(std::move(diagnostics_dir)));
    });
  }
  
  fpromise::promise<fidl::InterfaceHandle<fuchsia::io::Directory>, zx_status_t>
  ComponentControllerBase::GetDiagnosticsDir() {
    return GetDir("diagnostics");
  }
  
  fpromise::promise<fidl::InterfaceHandle<fuchsia::io::Directory>, zx_status_t>
  ComponentControllerBase::GetServiceDir() {
    return GetDir("svc");
  }
  
  void ComponentControllerBase::SendOnDirectoryReadyEvent() {
    // This can be false if
    // 1. Other side of the channel dies before this call happens.
    // 2. Component Controller request was not passed while creating the component.
    if (binding_.is_bound()) {
      binding_.events().OnDirectoryReady();
    }
  }
  
  void ComponentControllerBase::SendOnTerminationEvent(
      int64_t return_code, fuchsia::sys::TerminationReason termination_reason) {
    // `binding_.is_bound()` can be false if
    //  1. Other side of the channel dies before this call happens.
    //  2. Component Controller request was not passed while creating the component.
    if (on_terminated_event_sent_ || !binding_.is_bound()) {
      return;
    }
    FX_VLOGS(1) << "Sending termination callback with return code: " << return_code;
    binding_.events().OnTerminated(return_code, termination_reason);
    on_terminated_event_sent_ = true;
  }
  
  ComponentControllerBase::~ComponentControllerBase() { ns_->FlushAndShutdown(ns_); }
  
  HubInfo ComponentControllerBase::HubInfo() {
    return component::HubInfo(label_, hub_instance_id_, hub_.dir());
  }
  
  void ComponentControllerBase::Detach() {
    binding_.set_error_handler([](zx_status_t /*status*/) {});
  }
  
  ComponentControllerImpl::ComponentControllerImpl(
      fidl::InterfaceRequest<fuchsia::sys::ComponentController> request,
      ComponentContainer<ComponentControllerImpl>* container, zx::job job, zx::process process,
      std::string url, std::string args, std::string label, fxl::RefPtr<Namespace> ns,
      zx::channel exported_dir, zx::channel client_request, zx::channel package_handle)
      : ComponentControllerBase(std::move(request), std::move(url), std::move(args), std::move(label),
                                std::to_string(fsl::GetKoid(process.get())), std::move(ns),
                                std::move(exported_dir), std::move(client_request)),
        container_(container),
        job_(std::move(job)),
        process_(std::move(process)),
        process_koid_(std::to_string(fsl::GetKoid(process_.get()))),
        job_koid_(std::to_string(fsl::GetKoid(job_.get()))),
        wait_(this, process_.get(), ZX_TASK_TERMINATED),
        system_diagnostics_(DuplicateProcess(process_)) {
    zx_status_t status = wait_.Begin(async_get_default_dispatcher());
    FX_DCHECK(status == ZX_OK);
  
    hub()->SetJobId(job_koid_);
    hub()->SetProcessId(process_koid_);
  
    // Serve connections to the system_diagnostics interface.
    auto system_diagnostics = fbl::MakeRefCounted<fs::PseudoDir>();
    system_diagnostics->AddEntry(
        fuchsia::inspect::Tree::Name_,
        fbl::MakeRefCounted<fs::Service>(
            [handler = inspect::MakeTreeHandler(&system_diagnostics_.inspector())](zx::channel chan) {
              handler(fidl::InterfaceRequest<fuchsia::inspect::Tree>(std::move(chan)));
              return ZX_OK;
            }));
  
    hub()->AddEntry("system_diagnostics", system_diagnostics);
  
    hub()->AddIncomingServices(this->incoming_services());
  
    if (package_handle.is_valid()) {
      hub()->AddPackageHandle(fbl::MakeRefCounted<fs::RemoteDir>(std::move(package_handle)));
    }
  
    zx::job watch_job;
    if (job_.duplicate(ZX_RIGHT_SAME_RIGHTS, &watch_job) != ZX_OK) {
      FX_LOGS(ERROR) << "Failed to duplicate job handle";
    }
    ComputeComponentInstancePath();
    auto realm = this->ns()->realm();
    if (realm && realm->cpu_watcher() && !instance_path_.empty()) {
      realm->cpu_watcher()->AddTask(instance_path_,
                                    std::make_unique<JobStatsReader>(std::move(watch_job)));
    }
  }
  
  ComponentControllerImpl::~ComponentControllerImpl() {
    zx_info_handle_basic_t info = {};
    // Two ways we end up here:
    // 1) OnHandleReady() destroys this object; in which case, process is dead.
    // 2) Our owner destroys this object; in which case, the process may still be
    //    alive.
    if (job_) {
      job_.get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr);
      job_.kill();
      // Our owner destroyed this object before we could obtain a termination
      // reason.
  
      SendOnTerminationEvent(-1, TerminationReason::UNKNOWN);
    }
  
    auto realm = this->ns()->realm();
    if (realm && realm->cpu_watcher()) {
      if (!instance_path_.empty()) {
        realm->cpu_watcher()->RemoveTask(instance_path_);
      }
    }
  
    // Clean up system diagnostics before deleting the backing objects.
    hub()->dir()->RemoveEntry("system_diagnostics");
  }
  
  void ComponentControllerImpl::Kill() {
    FX_VLOGS(1) << "ComponentControllerImpl::Kill() called";
    TRACE_DURATION("appmgr", "ComponentController::Kill");
    if (job_) {
      job_.kill();
      job_.reset();
    }
  }
  
  bool ComponentControllerImpl::SendReturnCodeIfTerminated() {
    // Get process info.
    zx_info_process_t process_info;
    zx_status_t result =
        process_.get_info(ZX_INFO_PROCESS, &process_info, sizeof(process_info), NULL, NULL);
    FX_DCHECK(result == ZX_OK);
  
    if (process_info.flags & ZX_INFO_PROCESS_FLAG_EXITED) {
      SendOnTerminationEvent(process_info.return_code, TerminationReason::EXITED);
    }
  
    return (process_info.flags & ZX_INFO_PROCESS_FLAG_EXITED) != 0;
  }
  
  zx_status_t ComponentControllerImpl::AddSubComponentHub(const component::HubInfo& hub_info) {
    hub()->EnsureComponentDir();
    return hub()->AddComponent(hub_info);
  }
  
  zx_status_t ComponentControllerImpl::RemoveSubComponentHub(const component::HubInfo& hub_info) {
    return hub()->RemoveComponent(hub_info);
  }
  
  // Called when process terminates, regardless of if Kill() was invoked.
  void ComponentControllerImpl::Handler(async_dispatcher_t* dispatcher, async::WaitBase* wait,
                                        zx_status_t status, const zx_packet_signal* signal) {
    FX_DCHECK(status == ZX_OK);
    FX_DCHECK(signal->observed == ZX_TASK_TERMINATED);
    FX_VLOGS(1) << "ComponentControllerImpl::Handler() called";
  
    bool terminated = SendReturnCodeIfTerminated();
    FX_DCHECK(terminated);
  
    process_.reset();
    container_->ExtractComponent(this);
    // The destructor of the temporary returned by ExtractComponent destroys
    // |this| at the end of the previous statement.
  }
  
  void ComponentControllerImpl::ComputeComponentInstancePath() {
    if (!instance_path_.empty()) {
      return;
    }
    std::vector<std::string> path;
    auto realm = this->ns()->realm();
    if (realm) {
      instance_path_.push_back(this->label());
      auto cur = realm;
      while (cur) {
        instance_path_.push_back(cur->label());
        cur = cur->parent();
      }
      std::reverse(instance_path_.begin(), instance_path_.end());
      instance_path_.push_back(job_koid_);
    }
  }
  
  ComponentBridge::ComponentBridge(fidl::InterfaceRequest<fuchsia::sys::ComponentController> request,
                                   fuchsia::sys::ComponentControllerPtr remote_controller,
                                   ComponentContainer<ComponentBridge>* container, std::string url,
                                   std::string args, std::string label, std::string hub_instance_id,
                                   fxl::RefPtr<Namespace> ns, zx::channel exported_dir,
                                   zx::channel client_request,
                                   std::optional<zx::channel> package_handle)
      : ComponentControllerBase(std::move(request), std::move(url), std::move(args), std::move(label),
                                hub_instance_id, std::move(ns), std::move(exported_dir),
                                std::move(client_request), MAX_RETRIES_OUT_DIAGNOSTICS),
        remote_controller_(std::move(remote_controller)),
        container_(std::move(container)),
        termination_reason_(TerminationReason::UNKNOWN) {
    // Forward termination callbacks from the remote component over the bridge.
    remote_controller_.events().OnTerminated = [this](int64_t return_code,
                                                      TerminationReason termination_reason) mutable {
      // Propagate the events to the external proxy.
      if (on_terminated_event_) {
        on_terminated_event_(return_code, termination_reason);
      }
  
      SendOnTerminationEvent(return_code, termination_reason);
      remote_controller_.events().OnTerminated = nullptr;
      container_->ExtractComponent(this);
    };
  
    remote_controller_.events().OnDirectoryReady = [this] { SendOnDirectoryReadyEvent(); };
  
    remote_controller_.set_error_handler([this](zx_status_t status) {
      if (remote_controller_.events().OnTerminated != nullptr) {
        remote_controller_.events().OnTerminated(-1, TerminationReason::UNKNOWN);
      }
    });
    // The destructor of the temporary returned by ExtractComponent destroys
    // |this| at the end of the previous statement.
  
    hub()->AddIncomingServices(this->incoming_services());
    if (package_handle.has_value() && package_handle->is_valid()) {
      hub()->AddPackageHandle(fbl::MakeRefCounted<fs::RemoteDir>(std::move(*package_handle)));
    }
  }
  
  void ComponentBridge::NotifyStopped() {
    ns()->NotifyComponentStopped(url(), label(), hub_instance_id());
  }
  
  ComponentBridge::~ComponentBridge() {
    if (remote_controller_.events().OnTerminated) {
      SendOnTerminationEvent(-1, termination_reason_);
    }
  }
  
  void ComponentBridge::SetParentJobId(const std::string& id) { hub()->SetJobId(id); }
  
  void ComponentBridge::Kill() { remote_controller_->Kill(); }
  
  void ComponentBridge::SetTerminationReason(TerminationReason termination_reason) {
    termination_reason_ = termination_reason;
  }
  
  }  // namespace component
  