[debugger] Add handle printing capabilities.

Adds a "handle" command that prints a table of all handles in the
process by default, and prints more details if a specific handle is
given.

Adds string decoder functions for the various handle values.

Bug: 43944

Change-Id: I2a33793c063bc67b9679567aaa23fe9d5092477a
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/402821
Reviewed-by: Dangyi Liu <dangyi@google.com>
Testability-Review: Brett Wilson <brettw@google.com>
Commit-Queue: Brett Wilson <brettw@google.com>
diff --git a/src/developer/debug/shared/BUILD.gn b/src/developer/debug/shared/BUILD.gn
index 2abda1c..b7a2730 100644
--- a/src/developer/debug/shared/BUILD.gn
+++ b/src/developer/debug/shared/BUILD.gn
@@ -15,6 +15,8 @@
     "buffered_fd.h",
     "component_utils.cc",
     "component_utils.h",
+    "handle_info.cc",
+    "handle_info.h",
     "message_loop.cc",
     "message_loop.h",
     "regex.cc",
@@ -76,6 +78,7 @@
   sources = [
     "address_range_unittest.cc",
     "component_utils_unittest.cc",
+    "handle_info_unittest.cc",
     "message_loop_unittest.cc",
     "regex_unittest.cc",
     "stream_buffer_unittest.cc",
diff --git a/src/developer/debug/shared/handle_info.cc b/src/developer/debug/shared/handle_info.cc
new file mode 100644
index 0000000..ebf1f67a
--- /dev/null
+++ b/src/developer/debug/shared/handle_info.cc
@@ -0,0 +1,140 @@
+// Copyright 2020 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/shared/handle_info.h"
+
+namespace debug_ipc {
+
+std::string HandleTypeToString(uint32_t handle_type) {
+  // Don't use Zircon headers from here, so need to hardcode the values.
+  switch (handle_type) {
+    case 0u:
+      return "ZX_OBJ_TYPE_NONE";
+    case 1u:
+      return "ZX_OBJ_TYPE_PROCESS";
+    case 2u:
+      return "ZX_OBJ_TYPE_THREAD";
+    case 3u:
+      return "ZX_OBJ_TYPE_VMO";
+    case 4u:
+      return "ZX_OBJ_TYPE_CHANNEL";
+    case 5u:
+      return "ZX_OBJ_TYPE_EVENT";
+    case 6u:
+      return "ZX_OBJ_TYPE_PORT";
+    case 9u:
+      return "ZX_OBJ_TYPE_INTERRUPT";
+    case 11u:
+      return "ZX_OBJ_TYPE_PCI_DEVICE";
+    case 12u:
+      return "ZX_OBJ_TYPE_LOG";
+    case 14u:
+      return "ZX_OBJ_TYPE_SOCKET";
+    case 15u:
+      return "ZX_OBJ_TYPE_RESOURCE";
+    case 16u:
+      return "ZX_OBJ_TYPE_EVENTPAIR";
+    case 17u:
+      return "ZX_OBJ_TYPE_JOB";
+    case 18u:
+      return "ZX_OBJ_TYPE_VMAR";
+    case 19u:
+      return "ZX_OBJ_TYPE_FIFO";
+    case 20u:
+      return "ZX_OBJ_TYPE_GUEST";
+    case 21u:
+      return "ZX_OBJ_TYPE_VCPU";
+    case 22u:
+      return "ZX_OBJ_TYPE_TIMER";
+    case 23u:
+      return "ZX_OBJ_TYPE_IOMMU";
+    case 24u:
+      return "ZX_OBJ_TYPE_BTI";
+    case 25u:
+      return "ZX_OBJ_TYPE_PROFILE";
+    case 26u:
+      return "ZX_OBJ_TYPE_PMT";
+    case 27u:
+      return "ZX_OBJ_TYPE_SUSPEND_TOKEN";
+    case 28u:
+      return "ZX_OBJ_TYPE_PAGER";
+    case 29u:
+      return "ZX_OBJ_TYPE_EXCEPTION";
+    case 30u:
+      return "ZX_OBJ_TYPE_CLOCK";
+    case 31u:
+      return "ZX_OBJ_TYPE_STREAM";
+    case 32u:
+      return "ZX_OBJ_TYPE_MSI_ALLOCATION";
+    case 33u:
+      return "ZX_OBJ_TYPE_MSI_INTERRUPT";
+    default:
+      return "<unknown (" + std::to_string(handle_type) + ")>";
+  }
+}
+
+std::string HandlePropsToString(uint32_t handle_props) {
+  if (handle_props == 0)
+    return "ZX_OBJ_PROP_NONE";
+
+  // Currently this is just one possible value. It may be extended to be a bitfield in the future.
+  // In that case, we probably want a version that returns an array like the "handle rights" one.
+  if (handle_props == 1)
+    return "ZX_OBJ_PROP_WAITABLE";
+
+  return "<unknown (" + std::to_string(handle_props) + ")>";
+}
+
+std::vector<std::string> HandleRightsToStrings(uint32_t handle_rights) {
+  std::vector<std::string> result;
+  if (handle_rights == 0) {
+    result.emplace_back("ZX_RIGHT_NONE");
+    return result;
+  }
+
+  struct RightMapping {
+    uint32_t bit_value;
+    const char* name;
+  };
+  static const RightMapping kRightMapping[] = {
+      {1u << 0, "ZX_RIGHT_DUPLICATE"},      {1u << 1, "ZX_RIGHT_TRANSFER"},
+      {1u << 2, "ZX_RIGHT_READ"},           {1u << 3, "ZX_RIGHT_WRITE"},
+      {1u << 4, "ZX_RIGHT_EXECUTE"},        {1u << 5, "ZX_RIGHT_MAP"},
+      {1u << 6, "ZX_RIGHT_GET_PROPERTY"},   {1u << 7, "ZX_RIGHT_SET_PROPERTY"},
+      {1u << 8, "ZX_RIGHT_ENUMERATE"},      {1u << 9, "ZX_RIGHT_DESTROY"},
+      {1u << 10, "ZX_RIGHT_SET_POLICY"},    {1u << 11, "ZX_RIGHT_GET_POLICY"},
+      {1u << 12, "ZX_RIGHT_SIGNAL"},        {1u << 13, "ZX_RIGHT_SIGNAL_PEER"},
+      {1u << 14, "ZX_RIGHT_WAIT"},          {1u << 15, "ZX_RIGHT_INSPECT"},
+      {1u << 16, "ZX_RIGHT_MANAGE_JOB"},    {1u << 17, "ZX_RIGHT_MANAGE_PROCESS"},
+      {1u << 18, "ZX_RIGHT_MANAGE_THREAD"}, {1u << 19, "<unknown (1 << 19)>"},
+      {1u << 20, "<unknown (1 << 20)>"},    {1u << 21, "<unknown (1 << 21)>"},
+      {1u << 22, "<unknown (1 << 22)>"},    {1u << 23, "<unknown (1 << 23)>"},
+      {1u << 24, "<unknown (1 << 24)>"},    {1u << 25, "<unknown (1 << 25)>"},
+      {1u << 26, "<unknown (1 << 26)>"},    {1u << 27, "<unknown (1 << 27)>"},
+      {1u << 28, "<unknown (1 << 28)>"},    {1u << 29, "<unknown (1 << 29)>"},
+      {1u << 30, "<unknown (1 << 30)>"},    {1u << 31, "ZX_RIGHT_SAME_RIGHTS"},
+  };
+
+  for (const auto& mapping : kRightMapping) {
+    if (handle_rights & mapping.bit_value)
+      result.emplace_back(mapping.name);
+  }
+
+  return result;
+}
+
+std::string HandleRightsToString(uint32_t handle_rights) {
+  auto rights = HandleRightsToStrings(handle_rights);
+
+  std::string result;
+  for (size_t i = 0; i < rights.size(); i++) {
+    if (i > 0)
+      result += " | ";
+    result += rights[i];
+  }
+
+  return result;
+}
+
+}  // namespace debug_ipc
diff --git a/src/developer/debug/shared/handle_info.h b/src/developer/debug/shared/handle_info.h
new file mode 100644
index 0000000..2534d1c
--- /dev/null
+++ b/src/developer/debug/shared/handle_info.h
@@ -0,0 +1,29 @@
+// Copyright 2020 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.
+
+#ifndef SRC_DEVELOPER_DEBUG_SHARED_HANDLE_INFO_H_
+#define SRC_DEVELOPER_DEBUG_SHARED_HANDLE_INFO_H_
+
+#include <stdint.h>
+
+#include <string>
+#include <vector>
+
+namespace debug_ipc {
+
+// Converts a uint32_t handle type to a string. Returns "<unknown>" on failure.
+std::string HandleTypeToString(uint32_t handle_type);
+
+// Returns "<none>" when unset, and "<unknown>" if unknown.
+std::string HandlePropsToString(uint32_t handle_props);
+
+// Returns a vector of strings, one for each right set.
+std::vector<std::string> HandleRightsToStrings(uint32_t handle_rights);
+
+// Returns a vector of right strings separated by "|" characters.
+std::string HandleRightsToString(uint32_t handle_rights);
+
+}  // namespace debug_ipc
+
+#endif  // SRC_DEVELOPER_DEBUG_SHARED_HANDLE_INFO_H_
diff --git a/src/developer/debug/shared/handle_info_unittest.cc b/src/developer/debug/shared/handle_info_unittest.cc
new file mode 100644
index 0000000..46ce686
--- /dev/null
+++ b/src/developer/debug/shared/handle_info_unittest.cc
@@ -0,0 +1,33 @@
+// Copyright 2020 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/shared/handle_info.h"
+
+#include <gtest/gtest.h>
+
+namespace debug_ipc {
+
+TEST(HandleInfo, HandleTypeToString) {
+  EXPECT_EQ("ZX_OBJ_TYPE_NONE", HandleTypeToString(0u));
+  EXPECT_EQ("ZX_OBJ_TYPE_SOCKET", HandleTypeToString(14u));
+  std::string a = HandleTypeToString(9999);
+  EXPECT_EQ("<unknown (9999)>", a);
+}
+
+TEST(HandleInfo, HandlePropsToString) {
+  EXPECT_EQ("ZX_OBJ_PROP_NONE", HandlePropsToString(0));
+  EXPECT_EQ("ZX_OBJ_PROP_WAITABLE", HandlePropsToString(1));
+  EXPECT_EQ("<unknown (999)>", HandlePropsToString(999));
+}
+
+TEST(HandleInfo, HandleRightsToString) {
+  EXPECT_EQ("ZX_RIGHT_NONE", HandleRightsToString(0));
+  EXPECT_EQ("ZX_RIGHT_DUPLICATE", HandleRightsToString(1));
+  EXPECT_EQ("ZX_RIGHT_TRANSFER", HandleRightsToString(2));
+  EXPECT_EQ("ZX_RIGHT_DUPLICATE | ZX_RIGHT_TRANSFER", HandleRightsToString(3));
+  EXPECT_EQ("ZX_RIGHT_DUPLICATE | <unknown (1 << 29)> | ZX_RIGHT_SAME_RIGHTS",
+            HandleRightsToString(0b10100000'00000000'00000000'00000001));
+}
+
+}  // namespace debug_ipc
diff --git a/src/developer/debug/zxdb/console/BUILD.gn b/src/developer/debug/zxdb/console/BUILD.gn
index fc5c644..3883fde4 100644
--- a/src/developer/debug/zxdb/console/BUILD.gn
+++ b/src/developer/debug/zxdb/console/BUILD.gn
@@ -55,6 +55,8 @@
     "commands/verb_enable.h",
     "commands/verb_finish.cc",
     "commands/verb_finish.h",
+    "commands/verb_handle.cc",
+    "commands/verb_handle.h",
     "commands/verb_help.cc",
     "commands/verb_help.h",
     "commands/verb_jump.cc",
@@ -139,6 +141,8 @@
     "format_exception.h",
     "format_frame.cc",
     "format_frame.h",
+    "format_handle.cc",
+    "format_handle.h",
     "format_job.cc",
     "format_job.h",
     "format_location.cc",
@@ -232,6 +236,7 @@
     "format_context_unittest.cc",
     "format_exception_unittest.cc",
     "format_frame_unittest.cc",
+    "format_handle_unittest.cc",
     "format_job_unittest.cc",
     "format_location_unittest.cc",
     "format_memory_unittest.cc",
diff --git a/src/developer/debug/zxdb/console/commands/verb_handle.cc b/src/developer/debug/zxdb/console/commands/verb_handle.cc
new file mode 100644
index 0000000..5ae353d
--- /dev/null
+++ b/src/developer/debug/zxdb/console/commands/verb_handle.cc
@@ -0,0 +1,120 @@
+// Copyright 2020 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/zxdb/console/commands/verb_handle.h"
+
+#include "src/developer/debug/zxdb/client/process.h"
+#include "src/developer/debug/zxdb/client/target.h"
+#include "src/developer/debug/zxdb/console/command.h"
+#include "src/developer/debug/zxdb/console/command_utils.h"
+#include "src/developer/debug/zxdb/console/console.h"
+#include "src/developer/debug/zxdb/console/format_handle.h"
+#include "src/developer/debug/zxdb/console/output_buffer.h"
+#include "src/developer/debug/zxdb/console/verbs.h"
+
+namespace zxdb {
+
+namespace {
+
+constexpr int kHexSwitch = 1;
+
+const char kHandleShortHelp[] = "handle[s]: Print handle list or details.";
+const char kHandleHelp[] =
+    R"(handle[s] [ <handle-expression> ]
+
+  With no arguments, prints all handles for the process.
+
+  If an expression is given, the information corresponding to the resulting
+  handle value will be printed.
+
+  👉 See "help expressions" for how to write expressions.
+
+Options
+
+  -x
+     Print numbers as hexadecimal. Defaults to decimal.
+
+Examples
+
+  handle
+  process 1 handles
+      Print all handles for the current/given process.
+
+  handle -x h
+  handle -x some_object->handle
+      Prints the information for the given handle.
+)";
+
+void OnEvalComplete(fxl::RefPtr<EvalContext> eval_context, fxl::WeakPtr<Process> weak_process,
+                    ErrOrValue value, bool hex) {
+  Console* console = Console::get();
+  if (!weak_process)
+    return console->Output(Err("Process exited while requesting handles."));
+  if (value.has_error())
+    return console->Output(value.err());
+
+  uint64_t handle_value = 0;
+  if (Err err = value.value().PromoteTo64(&handle_value); err.has_error())
+    return console->Output(err);
+
+  weak_process->LoadInfoHandleTable([handle_value, hex](
+                                        ErrOr<std::vector<debug_ipc::InfoHandleExtended>> handles) {
+    Console* console = Console::get();
+    if (handles.has_error())
+      return console->Output(handles.err());
+
+    // Find the handle in the table.
+    for (const auto& handle : handles.value()) {
+      if (handle.handle_value == handle_value)
+        return console->Output(FormatHandle(handle, hex));
+    }
+    console->Output("No handle with value " + std::to_string(handle_value) + " in the process.");
+  });
+}
+
+Err RunVerbHandle(ConsoleContext* context, const Command& cmd) {
+  if (Err err = AssertRunningTarget(context, "handle", cmd.target()); err.has_error())
+    return err;
+
+  bool hex = cmd.HasSwitch(kHexSwitch);
+
+  if (cmd.args().empty()) {
+    cmd.target()->GetProcess()->LoadInfoHandleTable(
+        [hex](ErrOr<std::vector<debug_ipc::InfoHandleExtended>> handles) {
+          Console* console = Console::get();
+          if (handles.has_error())
+            return console->Output(handles.err());
+
+          auto handles_sorted = handles.take_value();
+          std::sort(
+              handles_sorted.begin(), handles_sorted.end(),
+              [](const debug_ipc::InfoHandleExtended& a, const debug_ipc::InfoHandleExtended& b) {
+                return a.handle_value < b.handle_value;
+              });
+          console->Output(FormatHandles(handles_sorted, hex));
+        });
+  } else {
+    // Evaluate the expression, then print just that handle.
+    fxl::RefPtr<EvalContext> eval_context = GetEvalContextForCommand(cmd);
+    return EvalCommandExpression(
+        cmd, "handle", eval_context, false, false,
+        [eval_context, weak_process = cmd.target()->GetProcess()->GetWeakPtr(),
+         hex](ErrOrValue value) {
+          OnEvalComplete(eval_context, weak_process, std::move(value), hex);
+        });
+  }
+  return Err();
+}
+
+}  // namespace
+
+VerbRecord GetHandleVerbRecord() {
+  VerbRecord handle(&RunVerbHandle, {"handle", "handles"}, kHandleShortHelp, kHandleHelp,
+                    CommandGroup::kQuery);
+  handle.param_type = VerbRecord::kOneParam;
+  handle.switches.emplace_back(kHexSwitch, false, "", 'x');
+  return handle;
+}
+
+}  // namespace zxdb
diff --git a/src/developer/debug/zxdb/console/commands/verb_handle.h b/src/developer/debug/zxdb/console/commands/verb_handle.h
new file mode 100644
index 0000000..e2e3166
--- /dev/null
+++ b/src/developer/debug/zxdb/console/commands/verb_handle.h
@@ -0,0 +1,16 @@
+// Copyright 2020 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.
+
+#ifndef SRC_DEVELOPER_DEBUG_ZXDB_CONSOLE_COMMANDS_VERB_HANDLE_H_
+#define SRC_DEVELOPER_DEBUG_ZXDB_CONSOLE_COMMANDS_VERB_HANDLE_H_
+
+namespace zxdb {
+
+struct VerbRecord;
+
+VerbRecord GetHandleVerbRecord();
+
+}  // namespace zxdb
+
+#endif  // SRC_DEVELOPER_DEBUG_ZXDB_CONSOLE_COMMANDS_VERB_HANDLE_H_
diff --git a/src/developer/debug/zxdb/console/commands/verb_print.cc b/src/developer/debug/zxdb/console/commands/verb_print.cc
index 5ceb30b..e05b202 100644
--- a/src/developer/debug/zxdb/console/commands/verb_print.cc
+++ b/src/developer/debug/zxdb/console/commands/verb_print.cc
@@ -58,7 +58,6 @@
   if (options.has_error())
     return options.err();
 
-  auto data_provider = eval_context->GetDataProvider();
   return EvalCommandExpression(
       cmd, "print", eval_context, false, false,
       [options = options.value(), eval_context](ErrOrValue value) {
diff --git a/src/developer/debug/zxdb/console/format_handle.cc b/src/developer/debug/zxdb/console/format_handle.cc
new file mode 100644
index 0000000..547cd23
--- /dev/null
+++ b/src/developer/debug/zxdb/console/format_handle.cc
@@ -0,0 +1,81 @@
+// Copyright 2020 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/zxdb/console/format_handle.h"
+
+#include "src/developer/debug/ipc/records.h"
+#include "src/developer/debug/shared/handle_info.h"
+#include "src/developer/debug/zxdb/common/string_util.h"
+#include "src/developer/debug/zxdb/console/format_table.h"
+#include "src/developer/debug/zxdb/console/string_util.h"
+
+namespace zxdb {
+
+namespace {
+
+// Appends a two element string vector to the given output.
+void AppendTwoEltRow(const std::string& a, const std::string& b,
+                     std::vector<std::vector<std::string>>& rows) {
+  auto& row = rows.emplace_back();
+  row.push_back(a);
+  row.push_back(b);
+}
+
+template <class T>
+std::string NumToString(T value, bool hex) {
+  if (hex)
+    return to_hex_string(value);
+  return std::to_string(value);
+}
+
+}  // namespace
+
+OutputBuffer FormatHandles(const std::vector<debug_ipc::InfoHandleExtended>& handles, bool hex) {
+  if (handles.empty())
+    return OutputBuffer("No handles.");
+
+  std::vector<std::vector<std::string>> rows;
+  for (const auto& handle : handles) {
+    auto& row = rows.emplace_back();
+    row.push_back(NumToString(handle.handle_value, hex));
+    row.push_back(debug_ipc::HandleTypeToString(handle.type));
+    row.push_back(NumToString(handle.koid, hex));
+  }
+
+  OutputBuffer out;
+  FormatTable({ColSpec(Align::kRight, 0, "Handle", 2), ColSpec(Align::kLeft, 0, "Type", 1),
+               ColSpec(Align::kRight, 0, "Koid", 1)},
+              rows, &out);
+  return out;
+}
+
+OutputBuffer FormatHandle(const debug_ipc::InfoHandleExtended& handle, bool hex) {
+  std::vector<std::vector<std::string>> rows;
+  AppendTwoEltRow("Type", debug_ipc::HandleTypeToString(handle.type), rows);
+  AppendTwoEltRow("Value", NumToString(handle.handle_value, hex), rows);
+
+  // Put each right on a separate line.
+  std::vector<std::string> rights = debug_ipc::HandleRightsToStrings(handle.rights);
+  for (size_t i = 0; i < rights.size(); i++) {
+    if (i == 0)
+      AppendTwoEltRow("Rights", rights[i], rows);
+    else
+      AppendTwoEltRow(std::string(), rights[i], rows);
+  }
+
+  AppendTwoEltRow("Properties", debug_ipc::HandlePropsToString(handle.props), rows);
+  AppendTwoEltRow("Koid", NumToString(handle.koid, hex), rows);
+  if (handle.related_koid)
+    AppendTwoEltRow("Related koid", NumToString(handle.related_koid, hex), rows);
+  if (handle.peer_owner_koid)
+    AppendTwoEltRow("Peer-owner koid", NumToString(handle.peer_owner_koid, hex), rows);
+
+  OutputBuffer out;
+  FormatTable({ColSpec(Align::kRight, 0, std::string(), 2, Syntax::kHeading),
+               ColSpec(Align::kLeft, 0, std::string(), 1)},
+              rows, &out);
+  return out;
+}
+
+}  // namespace zxdb
diff --git a/src/developer/debug/zxdb/console/format_handle.h b/src/developer/debug/zxdb/console/format_handle.h
new file mode 100644
index 0000000..a889284
--- /dev/null
+++ b/src/developer/debug/zxdb/console/format_handle.h
@@ -0,0 +1,29 @@
+// Copyright 2020 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.
+
+#ifndef SRC_DEVELOPER_DEBUG_ZXDB_CONSOLE_FORMAT_HANDLE_H_
+#define SRC_DEVELOPER_DEBUG_ZXDB_CONSOLE_FORMAT_HANDLE_H_
+
+#include <vector>
+
+#include "src/developer/debug/zxdb/console/output_buffer.h"
+
+namespace debug_ipc {
+struct InfoHandleExtended;
+}
+
+namespace zxdb {
+
+// Formats a table of the handles with minimal information. The order of the table will be the
+// same as the input vector. The hex flag prints values in hexadecimal. Otherwise decimal will be
+// used.
+OutputBuffer FormatHandles(const std::vector<debug_ipc::InfoHandleExtended>& handles, bool hex);
+
+// Formats a detailed summary of a single handle's information. The hex flag prints values in
+// hexadecimal. Otherwise decimal will be used.
+OutputBuffer FormatHandle(const debug_ipc::InfoHandleExtended& handle, bool hex);
+
+}  // namespace zxdb
+
+#endif  // SRC_DEVELOPER_DEBUG_ZXDB_CONSOLE_FORMAT_HANDLE_H_
diff --git a/src/developer/debug/zxdb/console/format_handle_unittest.cc b/src/developer/debug/zxdb/console/format_handle_unittest.cc
new file mode 100644
index 0000000..0b972fc
--- /dev/null
+++ b/src/developer/debug/zxdb/console/format_handle_unittest.cc
@@ -0,0 +1,84 @@
+// Copyright 2020 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/zxdb/console/format_handle.h"
+
+#include <gtest/gtest.h>
+
+#include "src/developer/debug/ipc/records.h"
+
+namespace zxdb {
+
+TEST(FormatHandle, Table) {
+  std::vector<debug_ipc::InfoHandleExtended> handles;
+
+  // Empty case.
+  OutputBuffer out = FormatHandles(handles, false);
+  EXPECT_EQ("No handles.", out.AsString());
+
+  // Give it two objects.
+  handles.resize(2);
+  handles[0].type = 2;  // Thread.
+  handles[0].handle_value = 1234;
+  handles[0].koid = 7890;
+
+  handles[1].type = 1;  // Process.
+  handles[1].handle_value = 1123;
+  handles[1].koid = 7891;
+
+  out = FormatHandles(handles, false);
+  EXPECT_EQ(
+      "  Handle  Type                 Koid\n"
+      "    1234  ZX_OBJ_TYPE_THREAD   7890\n"
+      "    1123  ZX_OBJ_TYPE_PROCESS  7891\n",
+      out.AsString());
+
+  // Hex formatting
+  out = FormatHandles(handles, true);
+  EXPECT_EQ(
+      "  Handle  Type                   Koid\n"
+      "   0x4d2  ZX_OBJ_TYPE_THREAD   0x1ed2\n"
+      "   0x463  ZX_OBJ_TYPE_PROCESS  0x1ed3\n",
+      out.AsString());
+}
+
+TEST(FormatHandle, BasicDetails) {
+  debug_ipc::InfoHandleExtended handle;
+  handle.type = 2;  // Thread.
+  handle.handle_value = 1234;
+  handle.rights = 3;
+  handle.props = 0;
+  handle.koid = 7890;
+  handle.related_koid = 1111;
+  handle.peer_owner_koid = 2222;
+
+  OutputBuffer out = FormatHandle(handle, false);
+  EXPECT_EQ(
+      "             Type  ZX_OBJ_TYPE_THREAD\n"
+      "            Value  1234\n"
+      "           Rights  ZX_RIGHT_DUPLICATE\n"
+      "                   ZX_RIGHT_TRANSFER\n"
+      "       Properties  ZX_OBJ_PROP_NONE\n"
+      "             Koid  7890\n"
+      "     Related koid  1111\n"
+      "  Peer-owner koid  2222\n",
+      out.AsString());
+
+  // Related and peer owner koid should be omitted when 0 (not all handle types have these and
+  // it looks confusing). This one also tests hex formatting.
+  handle.props = 1;
+  handle.related_koid = 0;
+  handle.peer_owner_koid = 0;
+  out = FormatHandle(handle, true);
+  EXPECT_EQ(
+      "        Type  ZX_OBJ_TYPE_THREAD\n"
+      "       Value  0x4d2\n"
+      "      Rights  ZX_RIGHT_DUPLICATE\n"
+      "              ZX_RIGHT_TRANSFER\n"
+      "  Properties  ZX_OBJ_PROP_WAITABLE\n"
+      "        Koid  0x1ed2\n",
+      out.AsString());
+}
+
+}  // namespace zxdb
diff --git a/src/developer/debug/zxdb/console/verbs.cc b/src/developer/debug/zxdb/console/verbs.cc
index dd067e8..21825c4 100644
--- a/src/developer/debug/zxdb/console/verbs.cc
+++ b/src/developer/debug/zxdb/console/verbs.cc
@@ -24,6 +24,7 @@
 #include "src/developer/debug/zxdb/console/commands/verb_down.h"
 #include "src/developer/debug/zxdb/console/commands/verb_enable.h"
 #include "src/developer/debug/zxdb/console/commands/verb_finish.h"
+#include "src/developer/debug/zxdb/console/commands/verb_handle.h"
 #include "src/developer/debug/zxdb/console/commands/verb_help.h"
 #include "src/developer/debug/zxdb/console/commands/verb_jump.h"
 #include "src/developer/debug/zxdb/console/commands/verb_kill.h"
@@ -130,6 +131,7 @@
     all_verbs[Verb::kDown] = GetDownVerbRecord();
     all_verbs[Verb::kEnable] = GetEnableVerbRecord();
     all_verbs[Verb::kFinish] = GetFinishVerbRecord();
+    all_verbs[Verb::kHandle] = GetHandleVerbRecord();
     all_verbs[Verb::kHelp] = GetHelpVerbRecord();
     all_verbs[Verb::kJump] = GetJumpVerbRecord();
     all_verbs[Verb::kKill] = GetKillVerbRecord();
diff --git a/src/developer/debug/zxdb/console/verbs.h b/src/developer/debug/zxdb/console/verbs.h
index 2acd33f..2d80e98 100644
--- a/src/developer/debug/zxdb/console/verbs.h
+++ b/src/developer/debug/zxdb/console/verbs.h
@@ -54,6 +54,7 @@
   kEnable,
   kFinish,
   kGet,
+  kHandle,
   kHelp,
   kJump,
   kKill,