blob: 9f9e84da549ecb4d89127d46fb8f726fd0ade28e [file]
// 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