[debugger] "ps" to show component info

Bug: 94893
Change-Id: I51fbf8b556d1f71a275d0f8983f5c141f98657af
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/692005
Commit-Queue: Dangyi Liu <dangyi@google.com>
Reviewed-by: Brett Wilson <brettw@google.com>
Reviewed-by: Xyan Bhatnagar <xbhatnag@google.com>
diff --git a/src/developer/debug/debug_agent/BUILD.gn b/src/developer/debug/debug_agent/BUILD.gn
index bc21422..598ed76 100644
--- a/src/developer/debug/debug_agent/BUILD.gn
+++ b/src/developer/debug/debug_agent/BUILD.gn
@@ -110,7 +110,9 @@
 
   public_deps = [
     "//sdk/fidl/fuchsia.exception",
+    "//sdk/fidl/fuchsia.io",
     "//sdk/fidl/fuchsia.kernel",
+    "//sdk/fidl/fuchsia.sys2",
     "//sdk/lib/fit",
     "//sdk/lib/syslog/cpp",
     "//src/developer/debug/ipc:agent",
diff --git a/src/developer/debug/debug_agent/component_manager.h b/src/developer/debug/debug_agent/component_manager.h
index b9fce91..419ebe4 100644
--- a/src/developer/debug/debug_agent/component_manager.h
+++ b/src/developer/debug/debug_agent/component_manager.h
@@ -26,6 +26,14 @@
  public:
   virtual ~ComponentManager() = default;
 
+  struct ComponentInfo {
+    std::string url;
+    std::string moniker;
+  };
+
+  // Find the component information if the job is the root job of an ELF component.
+  virtual std::optional<ComponentInfo> FindComponentInfo(zx_koid_t job_koid) const = 0;
+
   // Launches the component with the given command line.
   //
   // The root_job is the job for the attached component or system root job. The requirement is
diff --git a/src/developer/debug/debug_agent/debugged_job.cc b/src/developer/debug/debug_agent/debugged_job.cc
index d6d418d..3be45a5 100644
--- a/src/developer/debug/debug_agent/debugged_job.cc
+++ b/src/developer/debug/debug_agent/debugged_job.cc
@@ -40,7 +40,8 @@
   // Tools like fx serve will connect every second or so to the target, spamming logging for this
   // with a lot of "/boot/bin/sh" starting. We filter this out as it makes debugging much harder.
   if (proc_name != "/boot/bin/sh") {
-    DEBUG_LOG(Job) << "Debugged job " << koid() << ": Process " << proc_name << " starting.";
+    DEBUG_LOG(Job) << "Debugged job " << koid() << ": Process starting pid=" << process->GetKoid()
+                   << " name=" << proc_name;
   }
 
   // Search through the available filters. If the regex is not valid, fallback to checking if
diff --git a/src/developer/debug/debug_agent/meta/debug_agent.cml b/src/developer/debug/debug_agent/meta/debug_agent.cml
index ffa6dfd..cdb548f 100644
--- a/src/developer/debug/debug_agent/meta/debug_agent.cml
+++ b/src/developer/debug/debug_agent/meta/debug_agent.cml
@@ -27,7 +27,21 @@
 
                 // For running v1 components.
                 "fuchsia.sys.Launcher",
+
+                // To watch and attach to v2 components.
+                "fuchsia.sys2.EventSource",
+                "fuchsia.sys2.RealmExplorer.root",
+                "fuchsia.sys2.RealmQuery.root",
             ],
+            from: "parent",
+        },
+        {
+            // To watch and attach to v2 components.
+            event: [
+                "debug_started",
+                "stopped",
+            ],
+            from: "parent",
         },
         {
             // Some tests depend on "run /boot/bin/crasher".
diff --git a/src/developer/debug/debug_agent/meta/debug_agent.core_shard.cml b/src/developer/debug/debug_agent/meta/debug_agent.core_shard.cml
index 8e69de4..c392500 100644
--- a/src/developer/debug/debug_agent/meta/debug_agent.core_shard.cml
+++ b/src/developer/debug/debug_agent/meta/debug_agent.core_shard.cml
@@ -15,6 +15,17 @@
                 "fuchsia.kernel.RootJob",
                 "fuchsia.logger.LogSink",
                 "fuchsia.process.Launcher",
+                "fuchsia.sys2.EventSource",
+                "fuchsia.sys2.RealmExplorer.root",
+                "fuchsia.sys2.RealmQuery.root",
+            ],
+            from: "parent",
+            to: "#debug_agent",
+        },
+        {
+            event: [
+                "debug_started",
+                "stopped",
             ],
             from: "parent",
             to: "#debug_agent",
diff --git a/src/developer/debug/debug_agent/meta/debug_agent_integration_tests.cml b/src/developer/debug/debug_agent/meta/debug_agent_integration_tests.cml
index 556d0cf..73fd1fc 100644
--- a/src/developer/debug/debug_agent/meta/debug_agent_integration_tests.cml
+++ b/src/developer/debug/debug_agent/meta/debug_agent_integration_tests.cml
@@ -15,7 +15,25 @@
             protocol: [
                 "fuchsia.kernel.RootJob",
                 "fuchsia.process.Launcher",
+                "fuchsia.sys2.EventSource",
             ],
         },
+        {
+            protocol: "fuchsia.sys2.RealmExplorer",
+            from: "framework",
+            path: "/svc/fuchsia.sys2.RealmExplorer.root",
+        },
+        {
+            protocol: "fuchsia.sys2.RealmQuery",
+            from: "framework",
+            path: "/svc/fuchsia.sys2.RealmQuery.root",
+        },
+        {
+            event: [
+                "debug_started",
+                "stopped",
+            ],
+            from: "framework",
+        },
     ],
 }
diff --git a/src/developer/debug/debug_agent/meta/debug_agent_unit_tests.cml b/src/developer/debug/debug_agent/meta/debug_agent_unit_tests.cml
index d0fb518..2211a78 100644
--- a/src/developer/debug/debug_agent/meta/debug_agent_unit_tests.cml
+++ b/src/developer/debug/debug_agent/meta/debug_agent_unit_tests.cml
@@ -15,7 +15,28 @@
             protocol: [
                 "fuchsia.kernel.RootJob",
                 "fuchsia.process.Launcher",
+                "fuchsia.sys2.EventSource",
             ],
         },
+
+        // In tests, use the component API from the framework for ourself.
+        // The result is we can only observe ourself as the only component.
+        {
+            protocol: "fuchsia.sys2.RealmExplorer",
+            from: "framework",
+            path: "/svc/fuchsia.sys2.RealmExplorer.root",
+        },
+        {
+            protocol: "fuchsia.sys2.RealmQuery",
+            from: "framework",
+            path: "/svc/fuchsia.sys2.RealmQuery.root",
+        },
+        {
+            event: [
+                "debug_started",
+                "stopped",
+            ],
+            from: "framework",
+        },
     ],
 }
diff --git a/src/developer/debug/debug_agent/mock_component_manager.cc b/src/developer/debug/debug_agent/mock_component_manager.cc
index c301f87..f8b7f54 100644
--- a/src/developer/debug/debug_agent/mock_component_manager.cc
+++ b/src/developer/debug/debug_agent/mock_component_manager.cc
@@ -6,6 +6,11 @@
 
 namespace debug_agent {
 
+std::optional<ComponentManager::ComponentInfo> MockComponentManager::FindComponentInfo(
+    zx_koid_t job_koid) const {
+  return std::nullopt;
+}
+
 debug::Status MockComponentManager::LaunchComponent(DebuggedJob* root_job,
                                                     const std::vector<std::string>& argv,
                                                     uint64_t* component_id) {
diff --git a/src/developer/debug/debug_agent/mock_component_manager.h b/src/developer/debug/debug_agent/mock_component_manager.h
index 512e6a90..5540647 100644
--- a/src/developer/debug/debug_agent/mock_component_manager.h
+++ b/src/developer/debug/debug_agent/mock_component_manager.h
@@ -15,6 +15,7 @@
   ~MockComponentManager() override = default;
 
   // ComponentManager implementation.
+  std::optional<ComponentInfo> FindComponentInfo(zx_koid_t job_koid) const override;
   debug::Status LaunchComponent(DebuggedJob* root_job, const std::vector<std::string>& argv,
                                 uint64_t* component_id) override;
   uint64_t OnProcessStart(const std::string& filter, StdioHandles& out_stdio) override;
diff --git a/src/developer/debug/debug_agent/system_interface.cc b/src/developer/debug/debug_agent/system_interface.cc
index a69bf09..4ab5367 100644
--- a/src/developer/debug/debug_agent/system_interface.cc
+++ b/src/developer/debug/debug_agent/system_interface.cc
@@ -4,17 +4,24 @@
 
 #include "src/developer/debug/debug_agent/system_interface.h"
 
+#include "src/developer/debug/debug_agent/component_manager.h"
+
 namespace debug_agent {
 
 namespace {
 
 using debug_ipc::ProcessTreeRecord;
 
-ProcessTreeRecord GetProcessTreeFrom(const JobHandle& job) {
+ProcessTreeRecord GetProcessTreeFrom(const JobHandle& job,
+                                     const ComponentManager& component_manager) {
   ProcessTreeRecord result;
   result.type = ProcessTreeRecord::Type::kJob;
   result.koid = job.GetKoid();
   result.name = job.GetName();
+  if (auto component_info = component_manager.FindComponentInfo(job.GetKoid())) {
+    result.component_url = component_info->url;
+    result.component_moniker = component_info->moniker;
+  }
 
   for (const auto& child_process : job.GetChildProcesses()) {
     ProcessTreeRecord& proc_record = result.children.emplace_back();
@@ -24,16 +31,17 @@
   }
 
   for (const auto& child_job : job.GetChildJobs())
-    result.children.push_back(GetProcessTreeFrom(*child_job));
+    result.children.push_back(GetProcessTreeFrom(*child_job, component_manager));
 
   return result;
 }
 
 }  // namespace
 
-ProcessTreeRecord SystemInterface::GetProcessTree() const {
+ProcessTreeRecord SystemInterface::GetProcessTree() {
+  const ComponentManager& component_manager = GetComponentManager();
   if (std::unique_ptr<JobHandle> root_job = GetRootJob())
-    return GetProcessTreeFrom(*root_job);
+    return GetProcessTreeFrom(*root_job, component_manager);
   return ProcessTreeRecord();
 }
 
diff --git a/src/developer/debug/debug_agent/system_interface.h b/src/developer/debug/debug_agent/system_interface.h
index 7d7fc87..268f7bb 100644
--- a/src/developer/debug/debug_agent/system_interface.h
+++ b/src/developer/debug/debug_agent/system_interface.h
@@ -57,7 +57,7 @@
   // These all use the virtual interface above to implement their functionality.
 
   // Collects the process tree starting from the given job handle.
-  debug_ipc::ProcessTreeRecord GetProcessTree() const;
+  debug_ipc::ProcessTreeRecord GetProcessTree();
 
   // Returns a handle to the job/process with the given koid. Returns an empty pointer if it was not
   // found. This can also happen if the debug_agent doesn't have permission to see it.
diff --git a/src/developer/debug/debug_agent/zircon_component_manager.cc b/src/developer/debug/debug_agent/zircon_component_manager.cc
index 712a2e1..75d9806 100644
--- a/src/developer/debug/debug_agent/zircon_component_manager.cc
+++ b/src/developer/debug/debug_agent/zircon_component_manager.cc
@@ -4,12 +4,16 @@
 
 #include "src/developer/debug/debug_agent/zircon_component_manager.h"
 
+#include <fuchsia/io/cpp/fidl.h>
 #include <fuchsia/sys/cpp/fidl.h>
-#include <lib/fdio/fd.h>
-#include <lib/fdio/io.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"
@@ -48,13 +52,38 @@
   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 ComponentLauncher {
+class V1ComponentLauncher {
  public:
-  explicit ComponentLauncher(std::shared_ptr<sys::ServiceDirectory> services)
+  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.
@@ -71,9 +100,9 @@
   fuchsia::sys::LaunchInfo launch_info_;
 };
 
-zx_status_t ComponentLauncher::Prepare(std::vector<std::string> argv,
-                                       ZirconComponentManager::ComponentDescription* description,
-                                       StdioHandles* handles) {
+zx_status_t V1ComponentLauncher::Prepare(std::vector<std::string> argv,
+                                         ZirconComponentManager::ComponentDescription* description,
+                                         StdioHandles* handles) {
   FX_DCHECK(services_);
   FX_DCHECK(!argv.empty());
 
@@ -106,7 +135,7 @@
   return ZX_OK;
 }
 
-fuchsia::sys::ComponentControllerPtr ComponentLauncher::Launch() {
+fuchsia::sys::ComponentControllerPtr V1ComponentLauncher::Launch() {
   FX_DCHECK(services_);
 
   fuchsia::sys::LauncherSyncPtr launcher;
@@ -125,20 +154,120 @@
 }  // namespace
 
 ZirconComponentManager::ZirconComponentManager(std::shared_ptr<sys::ServiceDirectory> services)
-    : services_(std::move(services)), weak_factory_(this) {}
+    : 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;
 
-  ComponentLauncher launcher(services_);
+  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_components_.count(description.filter) == 0);
+  FX_DCHECK(expected_v1_components_.count(description.filter) == 0);
 
   root_job->AppendFilter(description.filter);
 
@@ -177,35 +306,35 @@
                                          fuchsia::sys::TerminationReason reason) {
     // If the agent is gone, there isn't anything more to do.
     if (mgr)
-      mgr->OnComponentTerminated(return_code, description, reason);
+      mgr->OnV1ComponentTerminated(return_code, description, reason);
   };
 
-  ExpectedComponent expected_component;
+  ExpectedV1Component expected_component;
   expected_component.description = description;
   expected_component.handles = std::move(handles);
   expected_component.controller = std::move(controller);
-  expected_components_[description.filter] = std::move(expected_component);
+  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_components_.find(filter); it != expected_components_.end()) {
+  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_components_[component_id] = std::move(it->second.controller);
+    running_v1_components_[component_id] = std::move(it->second.controller);
 
-    expected_components_.erase(it);
+    expected_v1_components_.erase(it);
     return component_id;
   }
   return 0;
 }
 
-void ZirconComponentManager::OnComponentTerminated(int64_t return_code,
-                                                   const ComponentDescription& description,
-                                                   fuchsia::sys::TerminationReason reason) {
+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);
 
@@ -217,12 +346,12 @@
 
   // We look for the filter and remove it.
   // If we couldn't find it, the component was already caught and cleaned.
-  expected_components_.erase(description.filter);
+  expected_v1_components_.erase(description.filter);
 
   if (debug::IsDebugLoggingActive()) {
     std::stringstream ss;
-    ss << "Still expecting the following components: " << expected_components_.size();
-    for (auto& expected : expected_components_) {
+    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();
diff --git a/src/developer/debug/debug_agent/zircon_component_manager.h b/src/developer/debug/debug_agent/zircon_component_manager.h
index 3674e20..bd061e2 100644
--- a/src/developer/debug/debug_agent/zircon_component_manager.h
+++ b/src/developer/debug/debug_agent/zircon_component_manager.h
@@ -6,6 +6,8 @@
 #define SRC_DEVELOPER_DEBUG_DEBUG_AGENT_ZIRCON_COMPONENT_MANAGER_H_
 
 #include <fuchsia/sys/cpp/fidl.h>
+#include <fuchsia/sys2/cpp/fidl.h>
+#include <lib/fidl/cpp/binding.h>
 #include <lib/sys/cpp/service_directory.h>
 
 #include "src/developer/debug/debug_agent/component_manager.h"
@@ -13,7 +15,7 @@
 
 namespace debug_agent {
 
-class ZirconComponentManager : public ComponentManager {
+class ZirconComponentManager : public ComponentManager, public fuchsia::sys2::EventStream {
  public:
   struct ComponentDescription {
     uint64_t component_id = 0;  // 0 is invalid.
@@ -26,29 +28,36 @@
   ~ZirconComponentManager() override = default;
 
   // ComponentManager implementation.
+  std::optional<ComponentInfo> FindComponentInfo(zx_koid_t job_koid) const override;
   debug::Status LaunchComponent(DebuggedJob* root_job, const std::vector<std::string>& argv,
                                 uint64_t* component_id) override;
   uint64_t OnProcessStart(const std::string& filter, StdioHandles& out_stdio) override;
 
+  // fuchsia::sys2::EventStream implementation.
+  void OnEvent(fuchsia::sys2::Event event) override;
+
  private:
-  void OnComponentTerminated(int64_t return_code, const ComponentDescription& description,
-                             fuchsia::sys::TerminationReason reason);
+  void OnV1ComponentTerminated(int64_t return_code, const ComponentDescription& description,
+                               fuchsia::sys::TerminationReason reason);
 
   std::shared_ptr<sys::ServiceDirectory> services_;
 
+  std::map<zx_koid_t, ComponentInfo> running_component_info_;
+  fidl::Binding<fuchsia::sys2::EventStream> event_stream_binding_;
+
   // Each component launch is assigned an unique filter and id. This is because new components are
   // attached via the job filter mechanism. When a particular filter attached, we use this id to
   // know which component launch just happened and we can communicate it to the client.
-  struct ExpectedComponent {
+  struct ExpectedV1Component {
     ComponentDescription description;
     StdioHandles handles;
     fuchsia::sys::ComponentControllerPtr controller;
   };
-  std::map<std::string, ExpectedComponent> expected_components_;
+  std::map<std::string, ExpectedV1Component> expected_v1_components_;
 
   // References to the running components. These need to be kept alive to keep the components
   // running.
-  std::map<uint64_t, fuchsia::sys::ComponentControllerPtr> running_components_;
+  std::map<uint64_t, fuchsia::sys::ComponentControllerPtr> running_v1_components_;
 
   fxl::WeakPtrFactory<ZirconComponentManager> weak_factory_;
 };
diff --git a/src/developer/debug/debug_agent/zircon_system_interface.cc b/src/developer/debug/debug_agent/zircon_system_interface.cc
index ac1d9d9..f143d12 100644
--- a/src/developer/debug/debug_agent/zircon_system_interface.cc
+++ b/src/developer/debug/debug_agent/zircon_system_interface.cc
@@ -5,37 +5,31 @@
 #include "src/developer/debug/debug_agent/zircon_system_interface.h"
 
 #include <fuchsia/kernel/cpp/fidl.h>
-#include <lib/fdio/directory.h>
-#include <lib/fdio/fdio.h>
 
 #include "src/developer/debug/debug_agent/zircon_binary_launcher.h"
 #include "src/developer/debug/debug_agent/zircon_job_handle.h"
 #include "src/developer/debug/debug_agent/zircon_process_handle.h"
 #include "src/developer/debug/debug_agent/zircon_utils.h"
-#include "src/lib/files/file.h"
 
 namespace debug_agent {
 
 namespace {
 
 // Returns an !is_valid() job object on failure.
-zx::job GetRootZxJob() {
+zx::job GetRootZxJob(const sys::ServiceDirectory& services) {
   zx::job root_job;
   fuchsia::kernel::RootJobSyncPtr root_job_ptr;
 
-  std::string root_job_path("/svc/");
-  root_job_path.append(fuchsia::kernel::RootJob::Name_);
+  zx_status_t status = services.Connect(root_job_ptr.NewRequest());
 
-  zx_status_t status = fdio_service_connect(root_job_path.c_str(),
-                                            root_job_ptr.NewRequest().TakeChannel().release());
   if (status != ZX_OK) {
-    FX_NOTREACHED();
+    FX_PLOGS(ERROR, status) << "Cannot connect to fuchsia.kernel.RootJob";
     return zx::job();
   }
 
   status = root_job_ptr->Get(&root_job);
   if (status != ZX_OK) {
-    FX_NOTREACHED();
+    FX_PLOGS(ERROR, status) << "Cannot get root job handle";
     return zx::job();
   }
   return root_job;
@@ -47,7 +41,7 @@
     : services_(sys::ServiceDirectory::CreateFromNamespace()),
       component_manager_(services_),
       limbo_provider_(services_) {
-  if (zx::job zx_root = GetRootZxJob(); zx_root.is_valid())
+  if (zx::job zx_root = GetRootZxJob(*services_); zx_root.is_valid())
     root_job_ = std::make_unique<ZirconJobHandle>(std::move(zx_root));
 }
 
@@ -58,8 +52,6 @@
 std::unique_ptr<JobHandle> ZirconSystemInterface::GetRootJob() const {
   if (root_job_)
     return std::make_unique<ZirconJobHandle>(*root_job_);
-
-  FX_LOGS(WARNING) << "Failed to get the root job";
   return nullptr;
 }
 
diff --git a/src/developer/debug/debug_agent/zircon_system_interface_unittest.cc b/src/developer/debug/debug_agent/zircon_system_interface_unittest.cc
index b2044c5..bd2e1c98 100644
--- a/src/developer/debug/debug_agent/zircon_system_interface_unittest.cc
+++ b/src/developer/debug/debug_agent/zircon_system_interface_unittest.cc
@@ -2,34 +2,48 @@
 // 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/system_interface.h"
+#include "src/developer/debug/debug_agent/zircon_system_interface.h"
+
+#include <string_view>
 
 #include <gtest/gtest.h>
 
+#include "src/developer/debug/debug_agent/component_manager.h"
+#include "src/developer/debug/debug_agent/system_interface.h"
 #include "src/developer/debug/debug_agent/zircon_job_handle.h"
-#include "src/developer/debug/debug_agent/zircon_system_interface.h"
 #include "src/developer/debug/debug_agent/zircon_utils.h"
+#include "src/developer/debug/shared/test_with_loop.h"
 
 namespace debug_agent {
 
 namespace {
 
 // Recursively walks the process tree and returns true if there is a process
-// with the given name and koid.
-bool FindProcess(const debug_ipc::ProcessTreeRecord& record, const std::string& name_to_find,
-                 zx_koid_t koid_to_find) {
-  if (record.name == name_to_find && record.koid == koid_to_find)
+// matching the given koid. Fills the process_name if such process can be found.
+// Fills the component_info if the process belongs to some component.
+bool FindProcess(const debug_ipc::ProcessTreeRecord& record, zx_koid_t koid_to_find,
+                 std::string* process_name, ComponentManager::ComponentInfo* component_info) {
+  if (record.koid == koid_to_find) {
+    *process_name = record.name;
     return true;
+  }
   for (const auto& child : record.children) {
-    if (FindProcess(child, name_to_find, koid_to_find))
+    if (FindProcess(child, koid_to_find, process_name, component_info)) {
+      if (component_info->moniker.empty() && !record.component_url.empty()) {
+        component_info->moniker = record.component_moniker;
+        component_info->url = record.component_url;
+      }
       return true;
+    }
   }
   return false;
 }
 
+class SystemInfoTest : public debug::TestWithLoop {};
+
 }  // namespace
 
-TEST(SystemInfo, GetProcessTree) {
+TEST_F(SystemInfoTest, GetProcessTree) {
   ZirconSystemInterface system_interface;
 
   debug_ipc::ProcessTreeRecord root = system_interface.GetProcessTree();
@@ -38,15 +52,26 @@
   EXPECT_EQ(debug_ipc::ProcessTreeRecord::Type::kJob, root.type);
   EXPECT_FALSE(root.children.empty());
 
-  // Compute our own process name and koid.
+  // Query ourself.
   auto self = zx::process::self();
-  std::string self_name = zircon::NameForObject(*self);
-  EXPECT_FALSE(self_name.empty());
   zx_koid_t self_koid = zircon::KoidForObject(*self);
-  ASSERT_NE(0u, self_koid);
+  ASSERT_NE(ZX_KOID_INVALID, self_koid);
 
-  // Our name and koid should be somewhere in the tree.
-  EXPECT_TRUE(FindProcess(root, self_name, self_koid));
+  // Our koid should be somewhere in the tree.
+  std::string process_name;
+  ComponentManager::ComponentInfo component_info;
+  EXPECT_TRUE(FindProcess(root, self_koid, &process_name, &component_info));
+
+  // The process_name and component info should match
+  EXPECT_EQ(zircon::NameForObject(*self), process_name);
+  // The moniker is empty because it's actually "." in the test environment and the "." is stripped.
+  EXPECT_EQ("", component_info.moniker);
+  // The url will include a hash that cannot be compared.
+  EXPECT_FALSE(component_info.url.empty());
+  std::string_view prefix = "fuchsia-pkg://fuchsia.com/debug_agent_unit_tests";
+  std::string_view suffix = "#meta/debug_agent_unit_tests.cm";
+  EXPECT_EQ(prefix, component_info.url.substr(0, prefix.size()));
+  EXPECT_EQ(suffix, component_info.url.substr(component_info.url.size() - suffix.size()));
 }
 
 }  // namespace debug_agent
diff --git a/src/developer/debug/ipc/agent_protocol.cc b/src/developer/debug/ipc/agent_protocol.cc
index 7224d03..8a9cfc1 100644
--- a/src/developer/debug/ipc/agent_protocol.cc
+++ b/src/developer/debug/ipc/agent_protocol.cc
@@ -125,7 +125,11 @@
   writer->WriteUint32(static_cast<uint32_t>(record.type));
   writer->WriteUint64(record.koid);
   writer->WriteString(record.name);
-  Serialize(record.children, writer);
+  if (record.type == ProcessTreeRecord::Type::kJob) {
+    writer->WriteString(record.component_url);
+    writer->WriteString(record.component_moniker);
+    Serialize(record.children, writer);
+  }
 }
 
 void Serialize(const ThreadRecord& record, MessageWriter* writer) {
diff --git a/src/developer/debug/ipc/client_protocol.cc b/src/developer/debug/ipc/client_protocol.cc
index 89fc20d..56821f1 100644
--- a/src/developer/debug/ipc/client_protocol.cc
+++ b/src/developer/debug/ipc/client_protocol.cc
@@ -21,7 +21,15 @@
     return false;
   if (!reader->ReadString(&record->name))
     return false;
-  return Deserialize(reader, &record->children);
+  if (record->type == ProcessTreeRecord::Type::kJob) {
+    if (!reader->ReadString(&record->component_url))
+      return false;
+    if (!reader->ReadString(&record->component_moniker))
+      return false;
+    if (!Deserialize(reader, &record->children))
+      return false;
+  }
+  return true;
 }
 
 bool Deserialize(MessageReader* reader, ThreadRecord* record) {
diff --git a/src/developer/debug/ipc/protocol.h b/src/developer/debug/ipc/protocol.h
index d45a59c6..344d4f0 100644
--- a/src/developer/debug/ipc/protocol.h
+++ b/src/developer/debug/ipc/protocol.h
@@ -12,7 +12,7 @@
 
 namespace debug_ipc {
 
-constexpr uint32_t kProtocolVersion = 38;
+constexpr uint32_t kProtocolVersion = 39;
 
 // This is so that it's obvious if the timestamp wasn't properly set (that number should be at
 // least 30,000 years) but it's not the max so that if things add to it then time keeps moving
diff --git a/src/developer/debug/ipc/protocol_unittests.cc b/src/developer/debug/ipc/protocol_unittests.cc
index ad15f84..4b8004c 100644
--- a/src/developer/debug/ipc/protocol_unittests.cc
+++ b/src/developer/debug/ipc/protocol_unittests.cc
@@ -377,6 +377,8 @@
   initial.root.type = ProcessTreeRecord::Type::kJob;
   initial.root.koid = 1234;
   initial.root.name = "root";
+  initial.root.component_url = "fuchsia-pkg://package#meta/component.cm";
+  initial.root.component_moniker = "/moniker";
 
   initial.root.children.resize(1);
   initial.root.children[0].type = ProcessTreeRecord::Type::kProcess;
@@ -389,6 +391,8 @@
   EXPECT_EQ(initial.root.type, second.root.type);
   EXPECT_EQ(initial.root.koid, second.root.koid);
   EXPECT_EQ(initial.root.name, second.root.name);
+  EXPECT_EQ(initial.root.component_moniker, second.root.component_moniker);
+  EXPECT_EQ(initial.root.component_url, second.root.component_url);
   ASSERT_EQ(initial.root.children.size(), second.root.children.size());
   EXPECT_EQ(initial.root.children[0].type, second.root.children[0].type);
   EXPECT_EQ(initial.root.children[0].koid, second.root.children[0].koid);
diff --git a/src/developer/debug/ipc/records.h b/src/developer/debug/ipc/records.h
index b194cd6..c9b9ef4 100644
--- a/src/developer/debug/ipc/records.h
+++ b/src/developer/debug/ipc/records.h
@@ -142,6 +142,12 @@
   uint64_t koid = 0;
   std::string name;
 
+  // The following fields are only valid on kJob and will be skipped if type is kProcess.
+
+  // The component information if the current job is the root job of an ELF component.
+  std::string component_url;
+  std::string component_moniker;
+
   std::vector<ProcessTreeRecord> children;
 };
 
diff --git a/src/developer/debug/zxdb/console/commands/verb_ps.cc b/src/developer/debug/zxdb/console/commands/verb_ps.cc
index f9e7375..47966d1 100644
--- a/src/developer/debug/zxdb/console/commands/verb_ps.cc
+++ b/src/developer/debug/zxdb/console/commands/verb_ps.cc
@@ -8,6 +8,7 @@
 #include <optional>
 #include <set>
 #include <sstream>
+#include <string_view>
 
 #include "src/developer/debug/zxdb/client/job.h"
 #include "src/developer/debug/zxdb/client/process.h"
@@ -73,7 +74,13 @@
 
   output->Append(syntax, prefix);
   output->Append(Syntax::kSpecial, std::to_string(rec.koid));
-  output->Append(syntax, " " + rec.name + "\n");
+  if (!rec.name.empty())
+    output->Append(syntax, " " + rec.name);
+  if (!rec.component_moniker.empty())
+    output->Append(" " + rec.component_moniker, TextForegroundColor::kCyan);
+  if (!rec.component_url.empty())
+    output->Append(" " + rec.component_url, TextForegroundColor::kGray);
+  output->Append(syntax, "\n");
 
   for (const auto& child : rec.children)
     OutputProcessTreeRecord(child, indent + 1, attached, output);
@@ -86,16 +93,32 @@
     const debug_ipc::ProcessTreeRecord& rec, const std::string& filter) {
   debug_ipc::ProcessTreeRecord result;
 
-  for (const auto& child : rec.children) {
-    if (auto matched_child = FilterProcessTree(child, filter))
-      result.children.push_back(*matched_child);
+  // A record matches if its name or component_moniker matches.
+  bool matched = rec.name.find(filter) != std::string::npos;
+  if (!matched && !rec.component_moniker.empty()) {
+    // Use the last segment of the moniker as the "component name".
+    std::string_view moniker = rec.component_moniker;
+    std::string_view name = moniker.substr(moniker.find_last_of('/') + 1);
+    matched = name.find(filter) != std::string_view::npos;
+  }
+
+  // If a record matches, show all its children.
+  if (matched) {
+    result.children = rec.children;
+  } else {
+    for (const auto& child : rec.children) {
+      if (auto matched_child = FilterProcessTree(child, filter))
+        result.children.push_back(*matched_child);
+    }
   }
 
   // Return the node when it matches or any of its children do.
-  if (rec.name.find(filter) != std::string::npos || !result.children.empty()) {
+  if (matched || !result.children.empty()) {
     result.type = rec.type;
     result.koid = rec.koid;
     result.name = rec.name;
+    result.component_url = rec.component_url;
+    result.component_moniker = rec.component_moniker;
     return result;
   }
 
@@ -132,6 +155,9 @@
   If a filter-string is provided only jobs and processes whose names contain the
   given case-sensitive substring. It does not support regular expressions.
 
+  If a job is the root job of a component, the component information will also
+  be printed.
+
   Jobs are annotated with "j: <job koid>"
   Processes are annotated with "p: <process koid>")";
 
diff --git a/src/developer/debug/zxdb/console/commands/verb_ps_unittest.cc b/src/developer/debug/zxdb/console/commands/verb_ps_unittest.cc
index f3db53d..ef4eaed 100644
--- a/src/developer/debug/zxdb/console/commands/verb_ps_unittest.cc
+++ b/src/developer/debug/zxdb/console/commands/verb_ps_unittest.cc
@@ -24,6 +24,8 @@
   reply.root.children.emplace_back();
   reply.root.children[0].koid = 100;
   reply.root.children[0].name = "j1";
+  reply.root.children[0].component_moniker = "/some/moniker";
+  reply.root.children[0].component_url = "schema://url";
 
   reply.root.children[0].children.emplace_back();
   reply.root.children[0].children[0].koid = 101;
@@ -79,9 +81,9 @@
 
 TEST_F(VerbPSTest, Filter) {
   // "ps" by itself should show everything.
-  EXPECT_EQ(RunCommandAndGetOutput("ps b"),
+  EXPECT_EQ(RunCommandAndGetOutput("ps"),
             " j: 1 root\n"
-            "   j: 100 j1\n"
+            "   j: 100 j1 /some/moniker schema://url\n"
             "     j: 101 j2\n"
             "       p: 102 baz\n"
             "▶  p: 875123541 foo bar\n");
@@ -89,7 +91,7 @@
   // Both processes have a "b" in them, this should match everything.
   EXPECT_EQ(RunCommandAndGetOutput("ps b"),
             " j: 1 root\n"
-            "   j: 100 j1\n"
+            "   j: 100 j1 /some/moniker schema://url\n"
             "     j: 101 j2\n"
             "       p: 102 baz\n"
             "▶  p: 875123541 foo bar\n");
@@ -97,8 +99,16 @@
   // Look for just a job name.
   EXPECT_EQ(RunCommandAndGetOutput("ps j2"),
             " j: 1 root\n"
-            "   j: 100 j1\n"
-            "     j: 101 j2\n");
+            "   j: 100 j1 /some/moniker schema://url\n"
+            "     j: 101 j2\n"
+            "       p: 102 baz\n");
+
+  // Look for a component name.
+  EXPECT_EQ(RunCommandAndGetOutput("ps moniker"),
+            " j: 1 root\n"
+            "   j: 100 j1 /some/moniker schema://url\n"
+            "     j: 101 j2\n"
+            "       p: 102 baz\n");
 
   // Look for just one process name with a space in it (matches "foo bar").
   EXPECT_EQ(RunCommandAndGetOutput("ps o b"),
@@ -107,6 +117,7 @@
 
   // Matches nothing.
   EXPECT_EQ(RunCommandAndGetOutput("ps zzz"), "No processes or jobs matching \"zzz\".\n");
+  EXPECT_EQ(RunCommandAndGetOutput("ps some"), "No processes or jobs matching \"some\".\n");
 }
 
 }  // namespace zxdb
diff --git a/src/sys/root/root-base.shard.cml b/src/sys/root/root-base.shard.cml
index ca34d38..20614c5 100644
--- a/src/sys/root/root-base.shard.cml
+++ b/src/sys/root/root-base.shard.cml
@@ -267,6 +267,16 @@
             to: "#core",
         },
 
+        // Offer events to `core` for debugger usage.
+        {
+            event: [
+                "debug_started",
+                "stopped",
+            ],
+            from: "framework",
+            to: "#core",
+        },
+
         // Offer boot resolver to `bootstrap` for driver usage.
         {
             resolver: "boot_resolver",