| // Copyright 2026 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/forensics/feedback/attachments/process_tree.h" |
| |
| #include <lib/fpromise/promise.h> |
| #include <lib/syslog/cpp/macros.h> |
| #include <zircon/status.h> |
| #include <zircon/syscalls.h> |
| #include <zircon/syscalls/exception.h> |
| #include <zircon/syscalls/object.h> |
| |
| #include <array> |
| #include <iomanip> |
| #include <memory> |
| #include <sstream> |
| #include <string> |
| #include <variant> |
| #include <vector> |
| |
| #include <task-utils/walker.h> |
| |
| namespace forensics::feedback { |
| namespace { |
| |
| // Helper struct template to enable pattern matching visitor over std::variant. |
| template <class... Ts> |
| struct overloaded : Ts... { |
| using Ts::operator()...; |
| }; |
| |
| // Represents the state of a Zircon process object. |
| enum class ProcessState { |
| kNone, |
| kExited, |
| }; |
| |
| // Represents the execution state of a Zircon thread object. |
| enum class ThreadState { |
| kNew, |
| kRunning, |
| kSuspended, |
| kBlocked, |
| kException, |
| kDying, |
| kDead, |
| kUnknown, |
| }; |
| |
| // Details specific to a job node. |
| struct JobDetail {}; |
| |
| // Details specific to a process node. |
| struct ProcessDetail { |
| ProcessState state; |
| }; |
| |
| // Details specific to a thread node. |
| struct ThreadDetail { |
| ThreadState state; |
| }; |
| |
| // Type-safe variant representing task-type specific detail. |
| using TaskDetail = std::variant<JobDetail, ProcessDetail, ThreadDetail>; |
| |
| // Represents any node (job, process, or thread) in the task tree walk. |
| class TaskEntry { |
| public: |
| TaskEntry(zx_koid_t koid, std::string name, TaskDetail detail) |
| : koid_(koid), name_(std::move(name)), detail_(std::move(detail)) {} |
| |
| static std::unique_ptr<TaskEntry> Job(zx_koid_t koid, std::string name) { |
| return std::make_unique<TaskEntry>(koid, std::move(name), JobDetail{}); |
| } |
| |
| static std::unique_ptr<TaskEntry> Process(zx_koid_t koid, std::string name, ProcessState state) { |
| return std::make_unique<TaskEntry>(koid, std::move(name), ProcessDetail{.state = state}); |
| } |
| |
| static std::unique_ptr<TaskEntry> Thread(zx_koid_t koid, std::string name, ThreadState state) { |
| return std::make_unique<TaskEntry>(koid, std::move(name), ThreadDetail{.state = state}); |
| } |
| |
| zx_koid_t Koid() const { return koid_; } |
| const std::string& Name() const { return name_; } |
| const std::vector<std::unique_ptr<TaskEntry>>& Children() const { return children_; } |
| |
| void AddChild(std::unique_ptr<TaskEntry> child) { children_.push_back(std::move(child)); } |
| |
| char TypeChar() const { |
| return std::visit(overloaded{ |
| [](const JobDetail&) { return 'j'; }, |
| [](const ProcessDetail&) { return 'p'; }, |
| [](const ThreadDetail&) { return 't'; }, |
| }, |
| detail_); |
| } |
| |
| std::string StateString() const { |
| return std::visit(overloaded{ |
| [](const JobDetail&) -> std::string { return ""; }, |
| [](const ProcessDetail& p) -> std::string { |
| switch (p.state) { |
| case ProcessState::kNone: |
| return ""; |
| case ProcessState::kExited: |
| return "exited"; |
| } |
| }, |
| [](const ThreadDetail& t) -> std::string { |
| switch (t.state) { |
| case ThreadState::kNew: |
| return "new"; |
| case ThreadState::kRunning: |
| return "running"; |
| case ThreadState::kSuspended: |
| return "susp"; |
| case ThreadState::kBlocked: |
| return "blocked"; |
| case ThreadState::kException: |
| return "excp"; |
| case ThreadState::kDying: |
| return "dying"; |
| case ThreadState::kDead: |
| return "dead"; |
| case ThreadState::kUnknown: |
| return "unknown"; |
| } |
| }, |
| }, |
| detail_); |
| } |
| |
| private: |
| zx_koid_t koid_; |
| std::string name_; |
| TaskDetail detail_; |
| std::vector<std::unique_ptr<TaskEntry>> children_; |
| }; |
| |
| // Converts a zx_info_thread_t struct into a ThreadState enum. |
| ThreadState GetThreadState(const zx_info_thread_t& info) { |
| if (ZX_THREAD_STATE_BASIC(info.state) == ZX_THREAD_STATE_BLOCKED_EXCEPTION && |
| info.wait_exception_channel_type != ZX_EXCEPTION_CHANNEL_TYPE_NONE) { |
| return ThreadState::kException; |
| } |
| switch (ZX_THREAD_STATE_BASIC(info.state)) { |
| case ZX_THREAD_STATE_NEW: |
| return ThreadState::kNew; |
| case ZX_THREAD_STATE_RUNNING: |
| return ThreadState::kRunning; |
| case ZX_THREAD_STATE_SUSPENDED: |
| return ThreadState::kSuspended; |
| case ZX_THREAD_STATE_BLOCKED: |
| return ThreadState::kBlocked; |
| case ZX_THREAD_STATE_DYING: |
| return ThreadState::kDying; |
| case ZX_THREAD_STATE_DEAD: |
| return ThreadState::kDead; |
| default: |
| return ThreadState::kUnknown; |
| } |
| } |
| |
| // Reads the ZX_PROP_NAME property of a task object. Returns an empty string if reading fails. |
| std::string GetTaskName(zx_handle_t task) { |
| std::array<char, ZX_MAX_NAME_LEN> name_buf{}; |
| if (zx_object_get_property(task, ZX_PROP_NAME, name_buf.data(), name_buf.size()) == ZX_OK) { |
| return std::string(name_buf.data()); |
| } |
| return {}; |
| } |
| |
| // Subclasses TaskEnumerator to collect all jobs, processes, and threads during a job tree walk. |
| class DumpEnumerator : public TaskEnumerator { |
| public: |
| zx_status_t OnJob(int depth, zx_handle_t job, zx_koid_t koid, zx_koid_t parent_koid) override { |
| AddEntry(parent_koid, TaskEntry::Job(koid, GetTaskName(job))); |
| return ZX_OK; |
| } |
| |
| zx_status_t OnProcess(int depth, zx_handle_t process, zx_koid_t koid, |
| zx_koid_t parent_koid) override { |
| ProcessState state = ProcessState::kNone; |
| zx_info_process_t info; |
| if (zx_object_get_info(process, ZX_INFO_PROCESS, &info, sizeof(info), nullptr, nullptr) == |
| ZX_OK) { |
| if (info.flags & ZX_INFO_PROCESS_FLAG_EXITED) { |
| state = ProcessState::kExited; |
| } |
| } |
| |
| AddEntry(parent_koid, TaskEntry::Process(koid, GetTaskName(process), state)); |
| return ZX_OK; |
| } |
| |
| zx_status_t OnThread(int depth, zx_handle_t thread, zx_koid_t koid, |
| zx_koid_t parent_koid) override { |
| ThreadState state = ThreadState::kUnknown; |
| zx_info_thread_t info; |
| if (zx_object_get_info(thread, ZX_INFO_THREAD, &info, sizeof(info), nullptr, nullptr) == |
| ZX_OK) { |
| state = GetThreadState(info); |
| } |
| |
| AddEntry(parent_koid, TaskEntry::Thread(koid, GetTaskName(thread), state)); |
| return ZX_OK; |
| } |
| |
| // Formats the collected job/process/thread entries into a text table. |
| // Trims trailing whitespace from each line. |
| // |
| // Example output: |
| // TASK STATE NAME |
| // j: 1024 root |
| // p: 1035 app.cm |
| // t: 1045 running main |
| // t: 1056 blocked worker |
| // p: 1060 exited dead_process |
| std::string RenderTable() const { |
| if (entries_.empty()) { |
| return ""; |
| } |
| |
| int id_w = 4; // minimum width for header "TASK" |
| for (const std::unique_ptr<TaskEntry>& entry : entries_) { |
| id_w = std::max(id_w, MaxIdWidth(*entry)); |
| } |
| |
| std::ostringstream ss; |
| std::string header = (std::ostringstream() << std::left << std::setw(id_w) << "TASK" << " " |
| << std::setw(7) << "STATE" << " NAME") |
| .str(); |
| while (!header.empty() && header.back() == ' ') { |
| header.pop_back(); |
| } |
| ss << header << "\n"; |
| |
| for (const std::unique_ptr<TaskEntry>& entry : entries_) { |
| RenderNode(*entry, /*depth=*/0, id_w, ss); |
| } |
| |
| return ss.str(); |
| } |
| |
| protected: |
| bool has_on_job() const override { return true; } |
| bool has_on_process() const override { return true; } |
| bool has_on_thread() const override { return true; } |
| |
| private: |
| void AddEntry(zx_koid_t parent_koid, std::unique_ptr<TaskEntry> entry) { |
| while (!task_stack_.empty() && task_stack_.back()->Koid() != parent_koid) { |
| task_stack_.pop_back(); |
| } |
| |
| TaskEntry* added = entry.get(); |
| if (task_stack_.empty()) { |
| entries_.push_back(std::move(entry)); |
| } else { |
| task_stack_.back()->AddChild(std::move(entry)); |
| } |
| |
| task_stack_.push_back(added); |
| } |
| |
| // Recursively calculates the maximum width of the formatted task ID string |
| // (including indentation, task type character, colon-space separator, and KOID) |
| // for a task entry and all its subtree descendants. |
| static int MaxIdWidth(const TaskEntry& entry, int depth = 0) { |
| int max_w = depth * 2 + 3 + static_cast<int>(std::to_string(entry.Koid()).length()); |
| for (const std::unique_ptr<TaskEntry>& child : entry.Children()) { |
| max_w = std::max(max_w, MaxIdWidth(*child, depth + 1)); |
| } |
| return max_w; |
| } |
| |
| // Recursively renders a task entry and its children as table rows into |ss|. |
| // Formats each row with proper depth indentation, aligned TASK ID column of width |id_w|, |
| // STATE string, and task NAME, stripping any trailing spaces. |
| static void RenderNode(const TaskEntry& entry, int depth, int id_w, std::ostringstream& ss) { |
| std::string task_id(depth * 2, ' '); |
| task_id += entry.TypeChar(); |
| task_id += ": "; |
| task_id += std::to_string(entry.Koid()); |
| |
| std::ostringstream line_stream; |
| line_stream << std::left << std::setw(id_w) << task_id << " " << std::setw(7) |
| << entry.StateString() << " " << entry.Name(); |
| |
| std::string line_str = line_stream.str(); |
| while (!line_str.empty() && line_str.back() == ' ') { |
| line_str.pop_back(); |
| } |
| ss << line_str << "\n"; |
| |
| for (const std::unique_ptr<TaskEntry>& child : entry.Children()) { |
| RenderNode(*child, depth + 1, id_w, ss); |
| } |
| } |
| |
| std::vector<std::unique_ptr<TaskEntry>> entries_; |
| std::vector<TaskEntry*> task_stack_; |
| }; |
| |
| } // namespace |
| |
| ProcessTree::ProcessTree(zx::job job) : job_(std::move(job)) {} |
| |
| ::fpromise::promise<AttachmentData> ProcessTree::Get(uint64_t ticket) { |
| if (!job_.is_valid()) { |
| return fpromise::make_ok_promise(AttachmentData(Error::kMissingValue)); |
| } |
| |
| DumpEnumerator enumerator; |
| if (const zx_status_t status = enumerator.WalkJobTree(job_.get()); status != ZX_OK) { |
| FX_PLOGS(WARNING, status) << "Failed to walk job tree for process tree"; |
| return fpromise::make_ok_promise(AttachmentData(Error::kMissingValue)); |
| } |
| |
| std::string dump = enumerator.RenderTable(); |
| if (dump.empty()) { |
| return fpromise::make_ok_promise(AttachmentData(Error::kMissingValue)); |
| } |
| |
| return fpromise::make_ok_promise(AttachmentData(std::move(dump))); |
| } |
| |
| void ProcessTree::ForceCompletion(uint64_t ticket, Error error) {} |
| |
| } // namespace forensics::feedback |