| // 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/developer/debug/debug_agent/zircon_component_manager.h" |
| |
| #include <fuchsia/io/cpp/fidl.h> |
| #include <fuchsia/sys/cpp/fidl.h> |
| #include <lib/sys/cpp/termination_reason.h> |
| #include <lib/syslog/cpp/macros.h> |
| #include <zircon/processargs.h> |
| #include <zircon/types.h> |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| |
| #include "src/developer/debug/debug_agent/debugged_job.h" |
| #include "src/developer/debug/shared/component_utils.h" |
| #include "src/developer/debug/shared/logging/logging.h" |
| |
| namespace debug_agent { |
| |
| namespace { |
| |
| uint64_t next_component_id = 1; |
| |
| // Attempts to link a zircon socket into the new component's file descriptor number represented by |
| // |fd|. If successful, the socket will be connected and a (one way) communication channel with that |
| // file descriptor will be made. |
| zx::socket AddStdio(int fd, fuchsia::sys::LaunchInfo* launch_info) { |
| zx::socket local; |
| zx::socket target; |
| |
| zx_status_t status = zx::socket::create(0, &local, &target); |
| if (status != ZX_OK) |
| return zx::socket(); |
| |
| auto io = std::make_unique<fuchsia::sys::FileDescriptor>(); |
| io->type0 = PA_HND(PA_FD, fd); |
| io->handle0 = std::move(target); |
| |
| if (fd == STDOUT_FILENO) { |
| launch_info->out = std::move(io); |
| } else if (fd == STDERR_FILENO) { |
| launch_info->err = std::move(io); |
| } else { |
| FX_NOTREACHED() << "Invalid file descriptor: " << fd; |
| return zx::socket(); |
| } |
| |
| return local; |
| } |
| |
| // Read the content of "elf/job_id" in the runtime directory of an ELF component. |
| // Return ZX_KOID_INVALID if no such file. |
| zx_koid_t ReadElfJobId(fuchsia::io::DirectoryHandle runtime_dir_handle) { |
| fuchsia::io::DirectorySyncPtr runtime_dir = runtime_dir_handle.BindSync(); |
| fuchsia::io::FileSyncPtr job_id_file; |
| runtime_dir->Open( |
| fuchsia::io::OpenFlags::RIGHT_READABLE, 0, "elf/job_id", |
| fidl::InterfaceRequest<fuchsia::io::Node>(job_id_file.NewRequest().TakeChannel())); |
| fuchsia::io::File2_Read_Result job_id_res; |
| job_id_file->Read(fuchsia::io::MAX_TRANSFER_SIZE, &job_id_res); |
| if (job_id_res.is_err()) { |
| return ZX_KOID_INVALID; |
| } |
| std::string job_id_str(reinterpret_cast<const char*>(job_id_res.response().data.data()), |
| job_id_res.response().data.size()); |
| // We use std::strtoull here because std::stoull is not exception-safe. |
| char* end; |
| zx_koid_t job_id = std::strtoull(job_id_str.c_str(), &end, 10); |
| if (end != job_id_str.c_str() + job_id_str.size()) { |
| FX_LOGS(ERROR) << "Invalid elf/job_id: " << job_id_str; |
| return ZX_KOID_INVALID; |
| } |
| return job_id; |
| } |
| |
| // Class designed to help setup a component and then launch it. These setups are necessary because |
| // the agent needs some information about how the component will be launch before it actually |
| // launches it. This is because the debugger will set itself to "catch" the component when it starts |
| // as a process. |
| class V1ComponentLauncher { |
| public: |
| explicit V1ComponentLauncher(std::shared_ptr<sys::ServiceDirectory> services) |
| : services_(std::move(services)) {} |
| |
| // Will fail if |argv| is invalid. The first element should be the component url needed to launch. |
| zx_status_t Prepare(std::vector<std::string> argv, |
| ZirconComponentManager::ComponentDescription* description, |
| StdioHandles* handles); |
| |
| // The launcher has to be already successfully prepared. The lifetime of the controller is bound |
| // to the lifetime of the component. |
| fuchsia::sys::ComponentControllerPtr Launch(); |
| |
| private: |
| std::shared_ptr<sys::ServiceDirectory> services_; |
| fuchsia::sys::LaunchInfo launch_info_; |
| }; |
| |
| zx_status_t V1ComponentLauncher::Prepare(std::vector<std::string> argv, |
| ZirconComponentManager::ComponentDescription* description, |
| StdioHandles* handles) { |
| FX_DCHECK(services_); |
| FX_DCHECK(!argv.empty()); |
| |
| auto pkg_url = argv.front(); |
| debug::ComponentDescription url_desc; |
| if (!debug::ExtractComponentFromPackageUrl(pkg_url, &url_desc)) { |
| FX_LOGS(WARNING) << "Invalid package url: " << pkg_url; |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| // Prepare the launch info. The parameters to the component do not include |
| // the component URL. |
| launch_info_.url = argv.front(); |
| if (argv.size() > 1) { |
| launch_info_.arguments.emplace(); |
| for (size_t i = 1; i < argv.size(); i++) |
| launch_info_.arguments->push_back(std::move(argv[i])); |
| } |
| |
| *description = {}; |
| description->component_id = next_component_id++; |
| description->url = pkg_url; |
| description->process_name = url_desc.component_name; |
| description->filter = url_desc.component_name; |
| |
| *handles = {}; |
| handles->out = AddStdio(STDOUT_FILENO, &launch_info_); |
| handles->err = AddStdio(STDERR_FILENO, &launch_info_); |
| |
| return ZX_OK; |
| } |
| |
| fuchsia::sys::ComponentControllerPtr V1ComponentLauncher::Launch() { |
| FX_DCHECK(services_); |
| |
| fuchsia::sys::LauncherSyncPtr launcher; |
| services_->Connect(launcher.NewRequest()); |
| |
| // Controller is a way to manage the newly created component. We need it in |
| // order to receive the terminated events. Sadly, there is no component |
| // started event. This also makes us need an async::Loop so that the fidl |
| // plumbing can work. |
| fuchsia::sys::ComponentControllerPtr controller; |
| launcher->CreateComponent(std::move(launch_info_), controller.NewRequest()); |
| |
| return controller; |
| } |
| |
| } // namespace |
| |
| ZirconComponentManager::ZirconComponentManager(std::shared_ptr<sys::ServiceDirectory> services) |
| : services_(std::move(services)), event_stream_binding_(this), weak_factory_(this) { |
| // 1. Subscribe to "debug_started" and "stopped" events. |
| fuchsia::sys2::EventSourceSyncPtr event_source; |
| services_->Connect(event_source.NewRequest()); |
| std::vector<fuchsia::sys2::EventSubscription> subscriptions; |
| subscriptions.resize(2); |
| subscriptions[0].set_event_name("debug_started"); |
| subscriptions[1].set_event_name("stopped"); |
| fuchsia::sys2::EventStreamHandle stream; |
| event_stream_binding_.Bind(stream.NewRequest()); |
| fuchsia::sys2::EventSource_Subscribe_Result subscribe_res; |
| event_source->Subscribe(std::move(subscriptions), std::move(stream), &subscribe_res); |
| if (subscribe_res.is_err()) { |
| FX_LOGS(ERROR) << "Failed to Subscribe: " << static_cast<uint32_t>(subscribe_res.err()); |
| } |
| |
| // 2. List existing components via fuchsia.sys2.RealmExplorer and fuchsia.sys2.RealmQuery. |
| fuchsia::sys2::RealmExplorerSyncPtr realm_explorer; |
| fuchsia::sys2::RealmQuerySyncPtr realm_query; |
| services_->Connect(realm_explorer.NewRequest(), "fuchsia.sys2.RealmExplorer.root"); |
| services_->Connect(realm_query.NewRequest(), "fuchsia.sys2.RealmQuery.root"); |
| |
| fuchsia::sys2::RealmExplorer_GetAllInstanceInfos_Result all_instance_infos_res; |
| realm_explorer->GetAllInstanceInfos(&all_instance_infos_res); |
| if (all_instance_infos_res.is_err()) { |
| FX_LOGS(ERROR) << "Failed to GetAllInstanceInfos: " |
| << static_cast<uint32_t>(all_instance_infos_res.err()); |
| return; |
| } |
| fuchsia::sys2::InstanceInfoIteratorSyncPtr instance_it = |
| all_instance_infos_res.response().iterator.BindSync(); |
| |
| while (true) { |
| std::vector<fuchsia::sys2::InstanceInfo> infos; |
| instance_it->Next(&infos); |
| if (infos.empty()) { |
| break; |
| } |
| for (auto& info : infos) { |
| if (info.state != fuchsia::sys2::InstanceState::STARTED || info.moniker.empty()) { |
| continue; |
| } |
| fuchsia::sys2::RealmQuery_GetInstanceInfo_Result instace_info_res; |
| realm_query->GetInstanceInfo(info.moniker, &instace_info_res); |
| if (instace_info_res.is_err() || !instace_info_res.response().resolved->started || |
| !instace_info_res.response().resolved->started->runtime_dir) { |
| continue; |
| } |
| zx_koid_t job_id = |
| ReadElfJobId(std::move(instace_info_res.response().resolved->started->runtime_dir)); |
| if (job_id != ZX_KOID_INVALID) { |
| // Remove the "." at the beginning of the moniker. It's safe because moniker is not empty. |
| running_component_info_[job_id] = {.url = info.url, .moniker = info.moniker.substr(1)}; |
| } |
| } |
| } |
| } |
| |
| void ZirconComponentManager::OnEvent(fuchsia::sys2::Event event) { |
| if (!event.has_header() || !event.header().has_moniker() || event.header().moniker().empty() || |
| !event.has_event_result() || !event.event_result().is_payload()) { |
| return; |
| } |
| switch (event.header().event_type()) { |
| case fuchsia::sys2::EventType::DEBUG_STARTED: |
| if (event.event_result().payload().is_debug_started() && |
| event.event_result().payload().debug_started().has_runtime_dir()) { |
| zx_koid_t job_id = ReadElfJobId(std::move( |
| *event.mutable_event_result()->payload().debug_started().mutable_runtime_dir())); |
| if (job_id != ZX_KOID_INVALID) { |
| running_component_info_[job_id] = {.url = event.header().component_url(), |
| .moniker = event.header().moniker().substr(1)}; |
| DEBUG_LOG(Process) << "Component started job_id=" << job_id |
| << " moniker=" << running_component_info_[job_id].moniker |
| << " url=" << running_component_info_[job_id].url; |
| } |
| } |
| break; |
| case fuchsia::sys2::EventType::STOPPED: { |
| std::string moniker = event.header().moniker().substr(1); |
| for (auto it = running_component_info_.begin(); it != running_component_info_.end(); it++) { |
| if (it->second.moniker == moniker) { |
| DEBUG_LOG(Process) << "Component stopped job_id=" << it->first |
| << " moniker=" << it->second.moniker << " url=" << it->second.url; |
| running_component_info_.erase(it); |
| break; |
| } |
| } |
| break; |
| } |
| default: |
| FX_NOTREACHED(); |
| } |
| } |
| |
| std::optional<ZirconComponentManager::ComponentInfo> ZirconComponentManager::FindComponentInfo( |
| zx_koid_t job_koid) const { |
| if (auto it = running_component_info_.find(job_koid); it != running_component_info_.end()) |
| return it->second; |
| return std::nullopt; |
| } |
| |
| debug::Status ZirconComponentManager::LaunchComponent(DebuggedJob* root_job, |
| const std::vector<std::string>& argv, |
| uint64_t* component_id) { |
| *component_id = 0; |
| |
| V1ComponentLauncher launcher(services_); |
| ComponentDescription description; |
| StdioHandles handles; |
| if (zx_status_t status = launcher.Prepare(argv, &description, &handles); status != ZX_OK) { |
| return debug::ZxStatus(status); |
| } |
| FX_DCHECK(expected_v1_components_.count(description.filter) == 0); |
| |
| root_job->AppendFilter(description.filter); |
| |
| if (debug::IsDebugLoggingActive()) { |
| std::stringstream ss; |
| |
| ss << "Launching component. " << std::endl |
| << "Url: " << description.url << std::endl |
| << ", name: " << description.process_name << std::endl |
| << ", filter: " << description.filter << std::endl |
| << ", component_id: " << description.component_id << std::endl; |
| |
| auto& filters = root_job->filters(); |
| ss << "Current component filters: " << filters.size(); |
| for (auto& filter : filters) { |
| ss << std::endl << "* " << filter.filter; |
| } |
| |
| DEBUG_LOG(Process) << ss.str(); |
| } |
| |
| *component_id = description.component_id; |
| |
| // Launch the component. |
| auto controller = launcher.Launch(); |
| if (!controller) { |
| FX_LOGS(WARNING) << "Could not launch component " << description.url; |
| return debug::Status("Could not launch component."); |
| } |
| |
| // TODO(donosoc): This should hook into the debug agent so it can correctly |
| // shutdown the state associated with waiting for this |
| // component. |
| controller.events().OnTerminated = [mgr = weak_factory_.GetWeakPtr(), description]( |
| int64_t return_code, |
| fuchsia::sys::TerminationReason reason) { |
| // If the agent is gone, there isn't anything more to do. |
| if (mgr) |
| mgr->OnV1ComponentTerminated(return_code, description, reason); |
| }; |
| |
| ExpectedV1Component expected_component; |
| expected_component.description = description; |
| expected_component.handles = std::move(handles); |
| expected_component.controller = std::move(controller); |
| expected_v1_components_[description.filter] = std::move(expected_component); |
| |
| return debug::Status(); |
| } |
| |
| uint64_t ZirconComponentManager::OnProcessStart(const std::string& filter, |
| StdioHandles& out_stdio) { |
| if (auto it = expected_v1_components_.find(filter); it != expected_v1_components_.end()) { |
| out_stdio = std::move(it->second.handles); |
| |
| uint64_t component_id = it->second.description.component_id; |
| running_v1_components_[component_id] = std::move(it->second.controller); |
| |
| expected_v1_components_.erase(it); |
| return component_id; |
| } |
| return 0; |
| } |
| |
| void ZirconComponentManager::OnV1ComponentTerminated(int64_t return_code, |
| const ComponentDescription& description, |
| fuchsia::sys::TerminationReason reason) { |
| DEBUG_LOG(Process) << "Component " << description.url << " exited with " |
| << sys::HumanReadableTerminationReason(reason); |
| |
| // TODO(donosoc): This need to be communicated over to the client. |
| if (reason != fuchsia::sys::TerminationReason::EXITED) { |
| FX_LOGS(WARNING) << "Component " << description.url << " exited with " |
| << sys::HumanReadableTerminationReason(reason); |
| } |
| |
| // We look for the filter and remove it. |
| // If we couldn't find it, the component was already caught and cleaned. |
| expected_v1_components_.erase(description.filter); |
| |
| if (debug::IsDebugLoggingActive()) { |
| std::stringstream ss; |
| ss << "Still expecting the following components: " << expected_v1_components_.size(); |
| for (auto& expected : expected_v1_components_) { |
| ss << std::endl << "* " << expected.first; |
| } |
| DEBUG_LOG(Process) << ss.str(); |
| } |
| } |
| |
| } // namespace debug_agent |