blob: 8fb3cf6db7b02e913b0e78f5133e5753ca9634ef [file] [log] [blame]
// Copyright 2022 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_LIB_ZXDUMP_INCLUDE_LIB_ZXDUMP_TASK_H_
#define SRC_LIB_ZXDUMP_INCLUDE_LIB_ZXDUMP_TASK_H_
#include <lib/elfldltl/constants.h>
#include <lib/fit/result.h>
#include <lib/stdcompat/version.h>
#include <zircon/errors.h>
#include <zircon/syscalls/debug.h>
#include <cstddef>
#include <cstring>
#include <functional>
#include <map>
#include <memory>
#include <utility>
#include <vector>
#include <fbl/unique_fd.h>
#ifdef __Fuchsia__
#include <lib/zx/handle.h>
#endif
#include "buffer.h"
#include "types.h"
namespace zxdump {
constexpr size_t kReadMemoryStringLimit = 1024;
// This is the type of the optional argument to Process::read_memory, below.
enum class ReadMemorySize {
// Return exactly the amount requested, never more or less--except that an
// emtpy buffer may be returned for memory elided from the dump.
kExact,
// Return at least the amount requested, and possibly more if convenient.
// (Still return an empty buffer for elided memory.)
kMore,
// Return any amount conveniently available. An empty buffer still means the
// requested memory was elided entirely. But for any nonempty buffer less
// than what was requested, the caller should make an additional read_memory
// call at a higher address to get the remainder piecemeal; a later call will
// fail outright if that higher address is not valid in the process.
//
// This mode is recommended for large reads that don't need to be contiguous
// in a single buffer, or that will be copied elsewhere anyway. It allows
// Process::read_memory to minimize data copying.
kLess,
};
// On Fuchsia, live task handles can be used via the lib/zx API. On other
// systems, the API parts for live tasks are still available but they use a
// stub handle type that is always invalid.
#ifdef __Fuchsia__
using LiveHandle = zx::handle;
#else
// The stub type is move-only and contextually convertible to bool just like
// the real one. It supports only a few basic methods, which do nothing and
// always report an invalid handle.
struct LiveHandle {
LiveHandle() = default;
LiveHandle(const LiveHandle&) = delete;
LiveHandle(LiveHandle&&) = default;
LiveHandle& operator=(const LiveHandle&) = delete;
LiveHandle& operator=(LiveHandle&&) = default;
void reset() {}
bool is_valid() const { return false; }
explicit operator bool() const { return is_valid(); }
zx_status_t get_info(uint32_t topic, void* buffer, size_t buffer_size, size_t* actual_count,
size_t* avail_count) const {
return ZX_ERR_BAD_HANDLE;
}
zx_status_t get_property(uint32_t property, void* value, size_t size) const {
return ZX_ERR_BAD_HANDLE;
}
zx_status_t get_child(uint64_t koid, zx_rights_t rights, LiveHandle* result) const {
return ZX_ERR_BAD_HANDLE;
}
};
#endif
// This is an opaque type used internally.
namespace internal {
class DumpFile;
} // namespace internal
// Forward declarations for below.
class Object;
class Resource;
class Task;
class Job;
class Process;
class Thread;
// This is an opaque type used internally.
namespace internal {
class DumpFile;
} // namespace internal
// This is the API for reading in dumps, both `ET_CORE` files and job archives.
//
// The zxdump::TaskHolder object is a container that holds the data from any
// number of dump files. It provides access to the data as a Zircon job tree.
// The Job, Process, and Thread objects represent the jobs and processes found
// in dump files. Each object provides calls analogous to the Zircon get_info,
// get_property, read_memory, and read_state calls, as well as get_child for
// nagivating a task tree.
//
// Dumps are inserted into the container by providing the file descriptor. The
// type of file will be determined automatically from its contents. Dump files
// can be ELF core dump (`ET_CORE`) files, or `ar` archive files. An archive
// file can be a job archive or just a plain archive of other dump files. Job
// archives can be mere "stub archives", or full hierarchical job archives, or
// flattened job archives.
//
// All the jobs and processes found in the dumps inserted then self-assemble
// into a job tree. If the same task (same KOID) appears a second time either
// in two dump files or in two members of a job archive, insertion fails but
// may have added some of the tasks from the dump anyway.
//
// If every process and every job but one is a child of another job found in
// the dump so they all form a single job tree, then `root_job` returns the
// root of that tree. If not, then `root_job` returns a fake "root job" with a
// KOID of 0 and no information or properties available except for the children
// and process lists. These show all the jobs that don't have parent jobs that
// were dumped, i.e. the roots of job trees; and all the processes that aren't
// part of any dumped job at all. Hence populating the container with a single
// ELF core dump will yield a fake root job whose sole child is that process.
//
// Methods that can fail use a result type with zxdump::Error. When the
// `status_` field is ZX_ERR_IO, that means the failure was in a POSIXish
// filesystem access function and `errno` is set to indicate the exact error.
// Otherwise the error codes have mostly the same meaning they would have for
// the real Zircon calls, with some amendments:
//
// * ZX_ERR_NOT_SUPPORTED just means the dump didn't include the requested
// type of data. It doesn't indicate whether the kernel didn't support it,
// or the dump-writer intentionally chose not to dump it, or the dump was
// just truncated, etc.
//
// * Process::read_memory fails with ZX_ERR_NOT_FOUND if the dump indicated
// the memory mapping existed but the dump did not include that memory.
// ZX_ERR_OUT_OF_RANGE means the memory is absent because the dump was
// truncated though this memory was intended to be included in the dump.
// ZX_ERR_NO_MEMORY has the kernel's meaning that there was no memory
// mapped at that address in the process. ZX_ERR_NOT_SUPPORTED means that
// the dump was inserted with the `read_memory=false` flag.
//
class TaskHolder {
public:
// Default-constructible, move-only.
TaskHolder();
TaskHolder(TaskHolder&&) = default;
TaskHolder& operator=(TaskHolder&&) = default;
~TaskHolder();
// Read the dump file from the file descriptor and insert its tasks. If
// `read_memory` is false, state will be trimmed after reading in all the
// notes so less memory is used and the file descriptor is never kept open;
// but read_memory calls will always fail with ZX_ERR_NOT_SUPPORTED.
fit::result<Error> Insert(fbl::unique_fd fd, bool read_memory = true);
// Insert a live task (job or process) or resource. Live threads cannot be
// inserted alone, only their containing process.
fit::result<Error, std::reference_wrapper<Object>> Insert(LiveHandle obj);
// Insert system data (returned by system_get_*, below) taken from the
// currently running system.
fit::result<Error> InsertSystem();
// Yields the current root job. If all tasks in the eye of the TaskHolder
// form a unified tree, this returns the actual root job in that tree.
// Otherwise, this is the fake "root job" that reads as KOID 0 with no data
// available except the Job::children and Job::processes lists holding each
// orphaned task not claimed by any parent job. It's always safe to hold
// onto this reference for the life of the TaskHolder. If more tasks are
// added, this will start returning a different reference. An old reference
// to the fake root job will read as having no children and no processes if
// all the tasks self-assembled into a single tree after more dumps were
// inserted, and later start reporting new orphan tasks inserted after that.
Job& root_job() const;
// Yields the root resource. If a live handle to the root resource was
// passed to Insert, this can access its data. If dumps inserted include the
// "privileged kernel" information, that is attached to this fake root
// resource though it doesn't have information like a KOID itself. If
// multiple dumps supply kernel information, conflicting data inserted later
// will be ignored. If both live and dump data are available, the dump data
// will obscure the live data.
Resource& root_resource() const;
// These can't fail, but return empty/zero if no corresponding data is in the
// dump. If multiple dumps supply system-wide information, only the first
// dump's data will be used. There is no checking that the system-wide data
// in dumps is valid; malformed data may be treated like no data at all but
// still may prevent well-formed data in other dumps from being used.
uint32_t system_get_dcache_line_size() const;
uint32_t system_get_num_cpus() const;
uint64_t system_get_page_size() const;
uint64_t system_get_physmem() const;
std::string_view system_get_version_string() const;
// Get and set the limit (in bytes) of unreferenced Process::read_memory
// pages from live processes that may be cached for reuse.
size_t memory_cache_limit() const;
void set_memory_cache_limit(size_t limit);
private:
friend Object;
friend Task;
friend Job;
friend Process;
friend Thread;
friend Resource;
class JobTree;
class LiveMemoryCache;
std::unique_ptr<JobTree> tree_;
};
// This is the superclass of all (virtual) kernel object types.
// All the methods here correspond to the generic zx::object methods.
class Object {
public:
// None of the Object types can be constructed in any way.
// They are only used via references returned by the owning TaskHolder.
Object(const Object&) = delete;
// The subclass constructors need to be public so that they can be called by
// the std::optional and std::map emplace methods, but they are intended as
// private so none of these objects can exist outside the TaskHolder. Since
// the JobTree& is a required argument in the subclass constructors and the
// class is private, outside code cannot construct new objects.
explicit Object(TaskHolder::JobTree& tree, LiveHandle live = {})
: tree_{tree}, live_(std::move(live)) {}
struct WaitItem {
std::reference_wrapper<Object> handle;
zx_signals_t waitfor = 0;
zx_signals_t pending = 0;
};
using WaitItemVector = std::vector<WaitItem>;
// Every task has a KOID. This is just shorthand for extracting it from
// ZX_INFO_HANDLE_BASIC. The fake root job returns zero (ZX_KOID_INVALID).
zx_koid_t koid() const;
// This is a shorthand for extracting the type from ZX_INFO_HANDLE_BASIC.
// * If it returns ZX_OBJ_TYPE_JOB, `static_cast<Job&>(*this)` is safe.
// * If it returns ZX_OBJ_TYPE_PROCESS, `static_cast<Process&>(*this)` is.
// * If it returns ZX_OBJ_TYPE_THREAD, `static_cast<Thread&>(*this)` is.
// * If it returns ZX_OBJ_TYPE_RESOURCE, `static_cast<Resource&>(*this)` is.
// The only objects on which get_info<ZX_INFO_HANDLE_BASIC> can fail are the
// fake root job and the fake root resource; type() on those returns zero
// (ZX_OBJ_TYPE_NONE).
zx_obj_type_t type() const;
// This returns the timestamp of the dump, which may be zero.
time_t date() const { return date_; }
// This is provided for parity with zx::object::get_child, but just using
// Process::threads, Job::children, or Job::processes is much more convenient
// for iterating through the lists reported by get_info. However, on live
// tasks either get_child or find to reach specific known KOIDs can be much
// more efficient than accessing the one of the child-list methods, which
// will acquire all the child handles implicitly, not just the matching one.
//
// **Notes for live objects:** Unlike zx::object::get_child, this does not
// take a zx_rights_t parameter even when referring to a live object.
// Instead, it always requests the full suite of rights that are necessary to
// do get_info, get_property, read_memory, read_state, etc.--everything the
// dumper usually needs. There is only ever a single Object for each KOID,
// so returning live object handle with lesser rights once would mean later
// calls couldn't use greater rights on the same KOID later.
fit::result<Error, std::reference_wrapper<Object>> get_child(zx_koid_t koid);
// Find a task by KOID: this task or a descendent task.
fit::result<Error, std::reference_wrapper<Object>> find(zx_koid_t koid);
// This gets the full info block for this topic, whatever its size. Note the
// data is not necessarily aligned in memory, so it can't be safely accessed
// with reinterpret_cast.
//
// **Notes for live objects:** When called on a live object, this ordinarily
// gets each topic just once and then returns the cached value; the optional
// refresh_live flag says to ignore any cached data and always get fresh data
// now (which will be cached for later calls without the flag). Note that a
// failed call with refresh_live will *not* remove old cached data, so it
// might be retrieved by calling again without the flag even if something
// prevents getting new info.
fit::result<Error, ByteView> get_info(zx_object_info_topic_t topic, bool refresh_live = false,
size_t record_size = 0);
// Get statically-typed info for a topic chosen at a compile time. Some
// types return a single `zx_info_*_t` object. Others return a span of const
// type that points into storage permanently cached for the lifetime of the
// containing TaskHolder. See <lib/zxdump/types.h> for topic->type mappings.
template <zx_object_info_topic_t Topic>
fit::result<Error, InfoTraitsType<Topic>> get_info(bool refresh_live = false) {
using Info = typename InfoTraits<Topic>::type;
if constexpr (kIsSpan<Info>) {
using Element = typename RemoveSpan<Info>::type;
auto result = get_info_aligned(Topic, sizeof(Element), alignof(Element), refresh_live);
if (result.is_error()) {
return result.take_error();
}
return fit::ok(Info{
reinterpret_cast<Element*>(result.value().data()),
result.value().size() / sizeof(Element),
});
} else {
auto result = get_info(Topic, refresh_live, sizeof(Info));
if (result.is_error()) {
return result.take_error();
}
ByteView bytes = result.value();
Info data;
if (bytes.size() < sizeof(data)) {
return fit::error(Error{"truncated info note", ZX_ERR_NOT_SUPPORTED});
}
memcpy(&data, bytes.data(), sizeof(data));
return fit::ok(data);
}
}
// This gets the property, whatever its size. Note the data is not
// necessarily aligned in memory, so it can't be safely accessed with
// reinterpret_cast.
fit::result<Error, ByteView> get_property(uint32_t property);
// Get a statically-typed property chosen at compile time.
// See <lib/zxdump/types.h> for property->type mappings.
template <uint32_t Property>
fit::result<Error, PropertyTraitsType<Property>> get_property() {
auto result = get_property(Property);
if (result.is_error()) {
return result.take_error();
}
ByteView bytes = result.value();
PropertyTraitsType<Property> data;
if (bytes.size() < sizeof(data)) {
return fit::error(Error{"truncated property note", ZX_ERR_NOT_SUPPORTED});
}
memcpy(&data, bytes.data(), sizeof(data));
return fit::ok(data);
}
static fit::result<Error> wait_many(WaitItemVector& wait_items);
bool is_live() const { return live_.is_valid(); }
// As a convenience, TaskHolder::root_resource() is proxied by every Object.
Resource& root_resource();
// A job or process can have dump remarks, stored as a vector of {name, data}
// pairs.
const auto& remarks() const { return remarks_; }
protected:
// The class is abstract. Only the subclasses can be created and destroyed.
Object() = delete;
// Move construction and assignment are only used during initial creation.
Object(Object&&) noexcept = default;
Object& operator=(Object&&) noexcept = default;
~Object();
LiveHandle& live() { return live_; }
TaskHolder::JobTree& tree() { return tree_; }
bool empty() const { return info_.empty(); }
std::byte* GetBuffer(size_t size);
void TakeBuffer(std::unique_ptr<std::byte[]> buffer);
private:
friend TaskHolder::JobTree;
fit::result<Error, ByteView> get_info_aligned(zx_object_info_topic_t topic, size_t record_size,
size_t align, bool refresh_live);
fit::result<Error, ByteView> GetSuperrootInfo(zx_object_info_topic_t topic);
// A plain reference would make the type not movable.
std::reference_wrapper<TaskHolder::JobTree> tree_;
std::map<zx_object_info_topic_t, ByteView> info_;
std::map<uint32_t, ByteView> properties_;
std::vector<std::pair<std::string_view, ByteView>> remarks_;
time_t date_ = 0;
LiveHandle live_;
};
// As with zx::task, this is the superclass of Job, Process, and Thread.
class Task : public Object {
public:
using Object::Object;
// This returns a suspend token.
fit::result<Error, LiveHandle> suspend();
protected:
Task(Task&&) noexcept = default;
Task& operator=(Task&&) noexcept = default;
~Task();
};
// A Thread is a Task and also has register state.
class Thread : public Task {
public:
using Task::Task;
~Thread();
Process& process() const { return *process_; }
fit::result<Error, ByteView> read_state(zx_thread_state_topic_t topic);
// Use read_state<zx_thread_state_*_t>() with the machine-specific type for a
// particular kind of state, which implies the topic.
template <class StateType>
fit::result<Error, StateType> read_state() {
StateType state;
using Traits = ThreadStateTraits<StateType>;
auto result = read_state(Traits::kTopic, Traits::kMachine, sizeof(state));
if (result.is_error()) {
return result.take_error();
}
memcpy(&state, result->data(), sizeof(state));
return fit::ok(state);
}
private:
friend Process;
friend TaskHolder::JobTree;
Thread(Thread&&) noexcept = default;
Thread& operator=(Thread&&) noexcept = default;
fit::result<Error, ByteView> read_state(zx_thread_state_topic_t topic,
elfldltl::ElfMachine machine, size_t size);
std::map<zx_thread_state_topic_t, ByteView> state_;
Process* process_ = nullptr;
};
// A Process is a Task and also has threads and memory.
class Process : public Task {
public:
using ThreadMap = std::map<zx_koid_t, Thread>;
using Task::Task;
~Process();
// Return the size of memory pages in this process, always a power of two.
// For a live process, this returns zx_system_get_page_size(). In a dump,
// it's determined from the PT_LOAD segments found (even if the dump elides
// the actual memory). If the dump had no memory segments and no system
// information to indicate the page size post mortem, this is 1 to maintain
// the power-of-two invariant.
uint64_t dump_page_size() const { return dump_page_size_; }
// Return the machine architecture of this process.
// In a dump, it's determined by the ELF file header.
elfldltl::ElfMachine dump_machine() const { return dump_machine_; }
// This is the same as what you'd get from get_info<ZX_INFO_PROCESS_THREADS>
// and then get_child on each KOID, but pre-cached. Note the returned map is
// not const so the Thread references can be non-const, but the caller must
// not modify the map itself.
fit::result<Error, std::reference_wrapper<ThreadMap>> threads();
// Find a task by KOID: this process or one of its threads.
fit::result<Error, std::reference_wrapper<Task>> find(zx_koid_t koid);
// zxdump::Object::get_child actually just dispatches to this method.
fit::result<Error, std::reference_wrapper<Thread>> get_child(zx_koid_t koid);
// Read process memory at the given vaddr. This tries to read at least count
// contiguous elements of type T, and succeeds if that was valid memory of
// type T in the process. The optional size_mode argument can permit it to
// return a buffer with more or less data than the exact size requested; see
// the enum definition above for details. In all modes it can return an
// empty buffer if the memory was elided from the dump. The success return
// value is a move-only type; see <zxdump/buffer.h> for full details.
template <typename T = std::byte, class View = cpp20::span<const T>>
fit::result<Error, Buffer<T, View>> read_memory(
uint64_t vaddr, size_t count, ReadMemorySize size_mode = ReadMemorySize::kExact) {
static_assert(!std::is_reference_v<T>,
"cannot instantiate zxdump::Process::read_memory with a reference type");
static_assert(
std::is_same_v<const T*, decltype(std::data(View{}))>,
"must instantiate zxdump::Process::read_memory with corresponding T and View types");
static_assert(alignof(T) <= __STDCPP_DEFAULT_NEW_ALIGNMENT__,
"alignment too large; must read as bytes and copy data");
if (vaddr % alignof(T) != 0) {
return fit::error{Error{
.op_ = "vaddr misaligned for type",
.status_ = ZX_ERR_INVALID_ARGS,
}};
}
Buffer<> buffer;
if (auto result = ReadMemoryImpl(vaddr, count * sizeof(T), size_mode, alignof(T));
result.is_ok()) {
buffer = *std::move(result);
} else {
return result.take_error();
}
return fit::ok(static_cast<Buffer<T, View>>(std::move(buffer)));
}
// This just reads a single datum of type T from dumped process memory at
// vaddr, and simply returns it by value.
template <typename T>
fit::result<Error, T> read_memory(uint64_t vaddr) {
using namespace std::literals;
auto result = read_memory<T>(vaddr, 1);
if (result.is_error()) {
return result.take_error();
}
if (result->empty()) {
return fit::error{Error{
.op_ = "memory elided from dump"sv,
.status_ = ZX_ERR_NO_MEMORY,
}};
}
return fit::ok(result->front());
}
// Convenience function to read a C-style NUL-terminated string, while
// reading no more than limit chars total. No string is returned if no NUL
// terminator is found, but the std::string returned does not include the NUL
// terminator in its size() value (though of course its c_str() value is
// NUL-terminated). If the memory is accessible but no NUL terminator is
// found before the size limit is reached, the error result will use
// ZX_ERR_OUT_OF_RANGE. If the necessary memory was simply elided from the
// dump, the error result will use ZX_ERR_NOT_SUPPORTED.
fit::result<Error, std::string> read_memory_string(uint64_t vaddr,
size_t limit = kReadMemoryStringLimit) {
return read_memory_basic_string<char>(vaddr, limit);
}
// The same thing is provided in u8string, u16string, u32string, and wstring
// variants.
#if __cpp_lib_char8_t
fit::result<Error, std::u8string> read_memory_u8string(uint64_t vaddr,
size_t limit = kReadMemoryStringLimit /
sizeof(char8_t)) {
return read_memory_basic_string<char8_t>(vaddr, limit);
}
#endif
fit::result<Error, std::u16string> read_memory_u16string(uint64_t vaddr,
size_t limit = kReadMemoryStringLimit /
sizeof(char16_t)) {
return read_memory_basic_string<char16_t>(vaddr, limit);
}
fit::result<Error, std::u32string> read_memory_u32string(uint64_t vaddr,
size_t limit = kReadMemoryStringLimit /
sizeof(char32_t)) {
return read_memory_basic_string<char32_t>(vaddr, limit);
}
fit::result<Error, std::wstring> read_memory_wstring(uint64_t vaddr,
size_t limit = kReadMemoryStringLimit /
sizeof(wchar_t)) {
return read_memory_basic_string<wchar_t>(vaddr, limit);
}
private:
friend TaskHolder;
class LiveMemory;
Process(Process&&) noexcept = default;
Process& operator=(Process&&) noexcept = default;
struct Segment {
uint64_t offset, filesz, memsz;
};
fit::result<Error, Buffer<>> ReadMemoryImpl(uint64_t vaddr, size_t size, ReadMemorySize size_mode,
size_t alignment);
fit::result<Error, Buffer<>> ReadLiveMemory(uint64_t vaddr, size_t size,
ReadMemorySize size_mode);
template <typename CharT = char>
fit::result<Error, std::basic_string<CharT>> read_memory_basic_string(
uint64_t vaddr, size_t limit = kReadMemoryStringLimit / sizeof(CharT));
std::map<zx_koid_t, Thread> threads_;
std::map<uint64_t, Segment> memory_;
uint64_t dump_page_size_ = 1;
elfldltl::ElfMachine dump_machine_ = elfldltl::ElfMachine::kNone;
internal::DumpFile* dump_ = nullptr;
std::unique_ptr<LiveMemory> live_memory_;
};
// Only these instantiations are actually defined in the library.
extern template fit::result<Error, std::basic_string<char>> Process::read_memory_basic_string<char>(
uint64_t vaddr, size_t limit);
#if __cpp_lib_char8_t
extern template fit::result<Error, std::basic_string<char8_t>>
Process::read_memory_basic_string<char8_t>(uint64_t vaddr, size_t limit);
#endif
extern template fit::result<Error, std::basic_string<char16_t>>
Process::read_memory_basic_string<char16_t>(uint64_t vaddr, size_t limit);
extern template fit::result<Error, std::basic_string<char32_t>>
Process::read_memory_basic_string<char32_t>(uint64_t vaddr, size_t limit);
extern template fit::result<Error, std::basic_string<wchar_t>>
Process::read_memory_basic_string<wchar_t>(uint64_t vaddr, size_t limit);
// A Job is a Task and also has child jobs and processes.
class Job : public Task {
public:
using JobMap = std::map<zx_koid_t, Job>;
using ProcessMap = std::map<zx_koid_t, Process>;
using Task::Task;
~Job();
// This is the same as what you'd get from get_info<ZX_INFO_JOB_CHILDREN> and
// then get_child on each KOID, but pre-cached. Note the returned map is not
// const so the Job references can be non-const, but the caller must not
// modify the map itself.
fit::result<Error, std::reference_wrapper<JobMap>> children();
// This is the same as what you'd get from get_info<ZX_INFO_JOB_PROCESSES>
// and then get_child on each KOID, but pre-cached. Note the returned map is
// not const so the Process references can be non-const, but the caller must
// not modify the map itself.
fit::result<Error, std::reference_wrapper<ProcessMap>> processes();
// Find a task by KOID: this task or a descendent task.
fit::result<Error, std::reference_wrapper<Task>> find(zx_koid_t koid);
// zxdump::Object::get_child actually just dispatches to this method.
fit::result<Error, std::reference_wrapper<Task>> get_child(zx_koid_t koid);
private:
friend TaskHolder::JobTree;
Job(Job&&) noexcept = default;
Job& operator=(Job&&) noexcept = default;
fit::result<Error, ByteView> GetSuperrootInfo(zx_object_info_topic_t topic);
JobMap children_;
ProcessMap processes_;
};
// Resource objects just hold access to information, but are a distinct type.
class Resource : public Object {
public:
using Object::Object;
~Resource();
private:
friend TaskHolder::JobTree;
Resource& operator=(Resource&&) noexcept = default;
};
// Get the live root job of the running system, e.g. for TaskHolder::Insert.
fit::result<Error, LiveHandle> GetRootJob();
// Get the live root resource handle of the running system.
fit::result<Error, LiveHandle> GetRootResource();
} // namespace zxdump
#endif // SRC_LIB_ZXDUMP_INCLUDE_LIB_ZXDUMP_TASK_H_