blob: 3c7abfefe617b62cadf8e6b3fb90c010c35ca789 [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.
#include <fcntl.h>
#include <getopt.h>
#include <lib/fit/defer.h>
#include <lib/stdcompat/functional.h>
#include <lib/zxdump/dump.h>
#include <lib/zxdump/elf-search.h>
#include <lib/zxdump/fd-writer.h>
#include <lib/zxdump/task.h>
#include <lib/zxdump/zstd-writer.h>
#include <zircon/assert.h>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
#include <fbl/unique_fd.h>
#include <rapidjson/error/en.h>
#include <rapidjson/filereadstream.h>
#include <rapidjson/reader.h>
#include <rapidjson/stream.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include "cli.h"
namespace {
using namespace std::literals;
constexpr std::string_view kOutputPrefix = "core."sv;
constexpr std::string_view kArchiveSuffix = ".a"sv;
constexpr std::string_view kZstdSuffix = ".zst"sv;
constexpr std::string_view kDefaultRemarksName = "remarks"sv;
struct Remarks {
std::string name;
std::unique_ptr<rapidjson::StringBuffer> data{new rapidjson::StringBuffer};
};
class Writer; // Forward declaration.
// Command-line flags controlling the dump are parsed into this object, which
// is passed around to the methods affected by policy choices.
struct Flags {
std::string OutputFile(zx_koid_t pid, bool outer = true, std::string_view suffix = {}) const {
std::string filename{outer ? output_prefix : kOutputPrefix};
filename += std::to_string(pid);
filename += suffix;
if (outer && zstd) {
filename += kZstdSuffix;
}
return filename;
}
time_t Date(zxdump::Task& task) const { return record_date ? task.date() : 0; }
std::vector<Remarks> remarks;
std::string_view output_prefix = kOutputPrefix;
std::unique_ptr<Writer> streaming;
size_t limit = zxdump::DefaultLimit();
bool dump_memory = true;
bool collect_system = false;
bool repeat_system = false;
bool collect_kernel = false;
bool repeat_kernel = false;
bool collect_threads = true;
bool collect_job_children = true;
bool collect_job_processes = true;
bool flatten_jobs = false;
bool record_date = true;
bool repeat_remarks = false;
bool zstd = false;
bool streaming_archive = false;
};
// This handles writing a single output file, and removing that output file if
// the dump is aborted before `Ok(true)` is called.
class Writer {
public:
using error_type = zxdump::FdWriter::error_type;
Writer() = delete;
Writer(fbl::unique_fd fd, std::string filename, bool zstd)
: writer_{zstd ? WhichWriter{zxdump::ZstdWriter(std::move(fd))}
: WhichWriter{zxdump::FdWriter(std::move(fd))}},
filename_{std::move(filename)} {}
auto AccumulateFragmentsCallback() {
return std::visit(
[](auto& writer)
-> fit::function<fit::result<error_type>(size_t offset, zxdump::ByteView data)> {
return writer.AccumulateFragmentsCallback();
},
writer_);
}
auto WriteFragments() {
return std::visit(
[](auto& writer) -> fit::result<error_type, size_t> { return writer.WriteFragments(); },
writer_);
}
auto WriteCallback() {
return std::visit(
[](auto& writer)
-> fit::function<fit::result<error_type>(size_t offset, zxdump::ByteView data)> {
return writer.WriteCallback();
},
writer_);
}
void ResetOffset() {
std::visit([](auto& writer) { writer.ResetOffset(); }, writer_);
}
// Write errors from errno use the file name.
void Error(zxdump::FdError error) {
std::string_view fn = filename_;
if (fn.empty()) {
fn = "<stdout>"sv;
}
std::cerr << fn << ": "sv << error << std::endl;
}
// Called with true if the output file should be preserved at destruction.
bool Ok(bool ok) {
if (ok) {
if (auto writer = std::get_if<zxdump::ZstdWriter>(&writer_)) {
auto result = writer->Finish();
if (result.is_error()) {
Error(result.error_value());
return false;
}
}
filename_.clear();
}
return ok;
}
bool StartArchive() {
auto result = zxdump::JobDump::DumpArchiveHeader(WriteCallback());
if (result.is_error()) {
Error(result.error_value());
}
return result.is_ok();
}
bool DumpMemberHeader(std::string_view name, size_t size, time_t mtime) {
// File offset calculations start fresh with each member.
ResetOffset();
auto result = zxdump::JobDump::DumpMemberHeader(WriteCallback(), 0, name, size, mtime);
if (result.is_error()) {
Error(*result.error_value().dump_error_);
}
return result.is_ok();
}
~Writer() {
if (!filename_.empty()) {
remove(filename_.c_str());
}
}
private:
using WhichWriter = std::variant<zxdump::FdWriter, zxdump::ZstdWriter>;
WhichWriter writer_;
std::string filename_;
};
size_t MemberHeaderSize() { return zxdump::JobDump::MemberHeaderSize(); }
// This is the base class of ProcessDumper and JobDumper; the object
// handles collecting and producing the dump for one process or one job.
// The Writer and Flags objects get passed in to control the details
// and of the dump and where it goes.
class DumperBase {
public:
// Read errors from syscalls use the PID (or job KOID).
void Error(const zxdump::Error& error) const {
std::cerr << koid_ << ": "sv << error << std::endl;
}
zx_koid_t koid() const { return koid_; }
time_t ClockIn(const Flags& flags) { return 0; }
protected:
constexpr explicit DumperBase(zx_koid_t koid) : koid_(koid) {}
private:
zx_koid_t koid_ = ZX_KOID_INVALID;
};
// This does the Collect* calls that are common to ProcessDumper and JobDumper.
constexpr auto CollectCommon = //
[](const Flags& flags, bool top, auto& dumper,
const zxdump::TaskHolder& holder) -> fit::result<zxdump::Error> {
if (flags.collect_system && (top || flags.repeat_system)) {
auto result = dumper.CollectSystem(holder);
if (result.is_error()) {
return result.take_error();
}
}
if (flags.collect_kernel && (top || flags.repeat_kernel)) {
auto result = dumper.CollectKernel();
if (result.is_error()) {
return result.take_error();
}
}
if (top || flags.repeat_remarks) {
for (const auto& [name, data] : flags.remarks) {
std::string_view contents{data->GetString(), data->GetSize()};
auto result = dumper.Remarks(name, contents);
if (result.is_error()) {
return result.take_error();
}
}
}
return fit::ok();
};
class ProcessDumper : public DumperBase {
public:
explicit ProcessDumper(zxdump::Process& process) : DumperBase{process.koid()}, dumper_{process} {}
auto OutputFile(const Flags& flags, bool outer = true) const {
return flags.OutputFile(koid(), outer);
}
time_t ClockIn(const Flags& flags) {
time_t dump_date = flags.Date(dumper_.process());
if (dump_date != 0) {
dumper_.set_date(dump_date);
}
return dump_date;
}
// Phase 1: Collect underpants!
std::optional<size_t> Collect(const Flags& flags, bool top, const zxdump::TaskHolder& holder) {
if (flags.collect_threads) {
auto result = dumper_.SuspendAndCollectThreads();
if (result.is_error()) {
Error(result.error_value());
return std::nullopt;
}
}
if (auto result = CollectCommon(flags, top, dumper_, holder); result.is_error()) {
Error(result.error_value());
return std::nullopt;
}
auto result = dumper_.CollectProcess(ChooseSegmentCallback(flags), flags.limit);
if (result.is_error()) {
Error(result.error_value());
return std::nullopt;
}
return result.value();
}
// Phase 2: ???
bool Dump(Writer& writer, const Flags& flags, const zxdump::TaskHolder& holder) {
// File offset calculations start fresh in each ET_CORE file.
writer.ResetOffset();
// Now gather the accumulated header data first: not including the memory.
// These iovecs will point into storage in the ProcessDump object itself.
size_t total;
if (auto result = dumper_.DumpHeaders(writer.AccumulateFragmentsCallback(), flags.limit);
result.is_error()) {
Error(result.error_value());
return false;
} else {
total = result.value();
}
if (total > flags.limit) {
writer.Error({.op_ = "not written"sv, .error_ = EFBIG});
return false;
}
// All the fragments gathered above get written at once.
if (auto result = writer.WriteFragments(); result.is_error()) {
writer.Error(result.error_value());
return false;
}
// Stream the memory out via a temporary buffer that's reused repeatedly
// for each callback.
if (auto memory = dumper_.DumpMemory(writer.WriteCallback(), flags.limit); memory.is_error()) {
Error(memory.error_value());
return false;
}
return true;
}
private:
zxdump::SegmentCallback ChooseSegmentCallback(const Flags& flags) {
if (!flags.dump_memory) {
return PruneAll;
}
// TODO(mcgrathr): more filtering switches
return cpp20::bind_front(&ProcessDumper::PruneDefault, this);
}
static fit::result<zxdump::Error, zxdump::SegmentDisposition> PruneAll(
zxdump::SegmentDisposition segment, const zx_info_maps_t& mapping, const zx_info_vmo_t& vmo) {
segment.filesz = 0;
return fit::ok(segment);
}
fit::result<zxdump::Error, zxdump::SegmentDisposition> PruneDefault(
zxdump::SegmentDisposition segment, const zx_info_maps_t& mapping, const zx_info_vmo_t& vmo) {
if (mapping.u.mapping.committed_pages == 0 && // No private RAM here,
vmo.parent_koid == ZX_KOID_INVALID && // and none shared,
!(vmo.flags & ZX_INFO_VMO_PAGER_BACKED)) { // and no backing store.
// Since it's not pager-backed, there isn't data hidden in backing
// store. If we read this, it would just be zero-fill anyway.
segment.filesz = 0;
}
// TODO(mcgrathr): for now, dump everything else.
// Check for build ID.
if (segment.filesz > 0 && zxdump::IsLikelyElfMapping(mapping)) {
auto result = dumper_.FindBuildIdNote(mapping);
if (result.is_error()) {
return result.take_error();
}
segment.note = result.value();
if (segment.note) {
// TODO(mcgrathr): This could e.g. decide under some switch to truncate
// the segment to just enough pages to include the build ID (usually
// one).
//
// segment.filesz = PageAlign(note->vaddr + note->size) - mapping.base;
}
}
return fit::ok(segment);
}
zxdump::ProcessDump dumper_;
};
// JobDumper handles dumping one job archive, either hierarchical or flattened.
class JobDumper : public DumperBase {
public:
explicit JobDumper(zxdump::Job& job) : DumperBase{job.koid()}, dumper_{job} {}
auto OutputFile(const Flags& flags, bool outer = true) const {
return flags.OutputFile(koid(), outer, kArchiveSuffix);
}
// The job dumper records the date of collection to use in the stub archive
// member headers. If the job archive is collected inside another archive,
// this will also be the date in the member header for the nested archive.
time_t date() const { return date_; }
auto* operator->() { return &dumper_; }
time_t ClockIn(const Flags& flags) {
date_ = flags.Date(dumper_.job());
return date_;
}
// Collect the job-wide data and reify the lists of children and processes.
std::optional<size_t> Collect(const Flags& flags, bool top, const zxdump::TaskHolder& holder) {
if (auto result = CollectCommon(flags, top, dumper_, holder); result.is_error()) {
Error(result.error_value());
return std::nullopt;
}
size_t size;
if (auto result = dumper_.CollectJob(); result.is_error()) {
Error(result.error_value());
return std::nullopt;
} else {
size = result.value();
}
if (flags.collect_job_children) {
if (auto result = dumper_.CollectChildren(); result.is_error()) {
Error(result.error_value());
return std::nullopt;
} else {
children_ = &(result.value().get());
}
}
if (flags.collect_job_processes) {
if (auto result = dumper_.CollectProcesses(); result.is_error()) {
Error(result.error_value());
return std::nullopt;
} else {
processes_ = &(result.value().get());
}
}
return size;
}
// Dump the job archive: first dump the stub archive, and then collect and
// dump each process and each child.
bool Dump(Writer& writer, const Flags& flags, const zxdump::TaskHolder& holder);
private:
class CollectedJob;
JobDumper(zxdump::JobDump job, zx_koid_t koid) : DumperBase{koid}, dumper_{std::move(job)} {}
bool DumpHeaders(Writer& writer, const Flags& flags) {
// File offset calculations start fresh in each archive.
writer.ResetOffset();
if (auto result = dumper_.DumpHeaders(writer.AccumulateFragmentsCallback(), date_);
result.is_error()) {
Error(result.error_value());
return false;
}
auto result = writer.WriteFragments();
if (result.is_error()) {
writer.Error(result.error_value());
return false;
}
return true;
}
static bool DumpMemberHeader(Writer& writer, std::string_view name, size_t size, time_t mtime) {
// File offset calculations start fresh with each member.
writer.ResetOffset();
auto result = zxdump::JobDump::DumpMemberHeader(writer.WriteCallback(), 0, name, size, mtime);
if (result.is_error()) {
writer.Error(*result.error_value().dump_error_);
}
return result.is_ok();
}
zxdump::JobDump dumper_;
zxdump::Job::JobMap* children_ = nullptr;
zxdump::Job::ProcessMap* processes_ = nullptr;
time_t date_ = 0;
};
// When dumping an hierarchical job archive, a CollectedJob object supports
// DeepCollect, that populates a tree of CollectedJob and CollectedProcess
// objects before the whole tree is dumped en masse.
class JobDumper::CollectedJob {
public:
CollectedJob(CollectedJob&&) = default;
CollectedJob& operator=(CollectedJob&&) = default;
explicit CollectedJob(JobDumper dumper) : dumper_(std::move(dumper)) {}
// This is false either if the original Collect failed and this is an empty
// object; or if any process or child collection failed so the archive is
// still valid but just omits some processes and/or children.
bool ok() const { return ok_; }
// This includes the whole size of the job archive itself plus its
// own member header as a member of its parent archive.
size_t size_bytes() const { return MemberHeaderSize() + content_size_; }
time_t date() const { return dumper_.date(); }
// Returns true if the job itself was collected.
// Later ok() indicates if any process or child collection failed.
bool DeepCollect(const Flags& flags, const zxdump::TaskHolder& holder) {
// Collect the job itself.
dumper_.ClockIn(flags);
if (auto collected_size = dumper_.Collect(flags, false, holder)) {
content_size_ += *collected_size;
// Collect all its processes and children.
if (dumper_.processes_) {
for (auto& [pid, process] : *dumper_.processes_) {
CollectProcess(process, flags, holder);
}
}
if (dumper_.children_) {
for (auto& [koid, job] : *dumper_.children_) {
CollectJob(job, flags, holder);
}
}
return true;
}
ok_ = false;
return false;
}
bool Dump(Writer& writer, const Flags& flags, const zxdump::TaskHolder& holder) {
// First dump the member header for this archive as a member of its parent.
// Then dump the "stub archive" describing the job itself.
if (!writer.DumpMemberHeader(dumper_.OutputFile(flags, false), content_size_, date()) ||
!dumper_.DumpHeaders(writer, flags)) {
ok_ = false;
} else {
for (auto& process : processes_) {
// Each CollectedProcess dumps its own member header and ET_CORE file.
ok_ = process.Dump(writer, flags, holder) && ok_;
}
for (auto& job : children_) {
// Recurse on each child to dump its own member header and job archive.
ok_ = job.Dump(writer, flags, holder) && ok_;
}
}
return ok_;
}
private:
// At the leaves of the tree are processes still suspended after collection.
class CollectedProcess {
public:
CollectedProcess(CollectedProcess&&) = default;
CollectedProcess& operator=(CollectedProcess&&) = default;
// Constructed with the process dumper and the result of Collect on it.
CollectedProcess(ProcessDumper&& dumper, size_t size, time_t date)
: dumper_(std::move(dumper)), content_size_(size), date_(date) {}
// This includes the whole size of the ET_CORE file itself plus its
// own member header as a member of its parent archive.
size_t size_bytes() const { return MemberHeaderSize() + content_size_; }
time_t date() const { return date_; }
// Dump the member header and then the ET_CORE file contents.
bool Dump(Writer& writer, const Flags& flags, const zxdump::TaskHolder& holder) {
return writer.DumpMemberHeader(dumper_.OutputFile(flags, false), content_size_, date()) &&
dumper_.Dump(writer, flags, holder);
}
private:
ProcessDumper dumper_;
size_t content_size_ = 0;
time_t date_ = 0;
};
void CollectProcess(zxdump::Process& process, const Flags& flags,
const zxdump::TaskHolder& holder) {
ProcessDumper dump{process};
time_t dump_date = dump.ClockIn(flags);
if (auto collected_size = dump.Collect(flags, false, holder)) {
CollectedProcess core_file{std::move(dump), *collected_size, dump_date};
content_size_ += core_file.size_bytes();
processes_.push_back(std::move(core_file));
} else {
ok_ = false;
}
}
void CollectJob(zxdump::Job& job, const Flags& flags, const zxdump::TaskHolder& holder) {
CollectedJob archive{JobDumper{job}};
if (archive.DeepCollect(flags, holder)) {
content_size_ += archive.size_bytes();
children_.push_back(std::move(archive));
}
// The job archive reports not OK if it was collected but omits some dumps.
ok_ = archive.ok() && ok_;
}
JobDumper dumper_;
std::vector<CollectedProcess> processes_;
std::vector<CollectedJob> children_;
size_t content_size_ = 0;
bool ok_ = true;
};
bool JobDumper::Dump(Writer& writer, const Flags& flags, const zxdump::TaskHolder& holder) {
if (!DumpHeaders(writer, flags)) {
return false;
}
bool ok = true;
if (processes_) {
for (auto& [pid, process] : *processes_) {
// Collect the process and thus discover the ET_CORE file size.
ProcessDumper process_dump{process};
time_t process_dump_date = process_dump.ClockIn(flags);
if (auto collected_size = process_dump.Collect(flags, false, holder)) {
// Dump the member header, now complete with size.
if (!writer.DumpMemberHeader(process_dump.OutputFile(flags, false), //
*collected_size, process_dump_date)) {
// Bail early for a write error, since later writes would fail too.
return false;
}
// Now dump the member contents, the ET_CORE file for the process.
ok = process_dump.Dump(writer, flags, holder) && ok;
}
}
}
if (children_) {
for (auto& [koid, job] : *children_) {
if (flags.flatten_jobs) {
// Collect just this job first.
JobDumper child{job};
auto collected_job_size = child.Collect(flags, false, holder);
ok = collected_job_size &&
// Stream out the member header for just the stub archive alone.
writer.DumpMemberHeader(child.OutputFile(flags, false), //
*collected_job_size, child.date()) &&
// Now recurse to dump the stub archive followed by process and
// child members. Since the member header for the inner archive
// only covers the stub archive, these become members in the outer
// (flat) archive rather than members of the inner job archive.
// Another inner recursion will do the same thing, so all the
// recursions stream out a single flat archive.
child.Dump(writer, flags, holder) && ok;
} else {
// Pre-collect the whole job tree and thus discover the archive size.
// The pre-collected archive dumps its own member header first.
CollectedJob archive{JobDumper{job}};
ok = archive.DeepCollect(flags, holder) && archive.Dump(writer, flags, holder) && ok;
}
}
}
return ok;
}
fbl::unique_fd CreateOutputFile(const std::string& outfile) {
fbl::unique_fd fd{open(outfile.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0666)};
if (!fd) {
perror(outfile.c_str());
}
return fd;
}
// Phase 3: Profit!
template <typename Dumper>
bool WriteDump(Dumper dumper, const Flags& flags, const zxdump::TaskHolder& holder) {
time_t date = dumper.ClockIn(flags);
if (flags.streaming) {
Writer& writer = *flags.streaming;
if (auto size = dumper.Collect(flags, true, holder)) {
if (!flags.streaming_archive ||
writer.DumpMemberHeader(dumper.OutputFile(flags, false), *size, date)) {
return writer.Ok(dumper.Dump(writer, flags, holder));
}
}
return false;
}
std::string outfile = dumper.OutputFile(flags);
fbl::unique_fd fd = CreateOutputFile(outfile);
if (!fd) {
return false;
}
Writer writer{std::move(fd), std::move(outfile), flags.zstd};
dumper.ClockIn(flags);
return writer.Ok(dumper.Collect(flags, true, holder) && dumper.Dump(writer, flags, holder));
}
// "Dump" a job tree by actually just making separate dumps of each process.
// We only use the JobDumper to find the processes and/or children.
bool WriteManyCoreFiles(JobDumper dumper, const Flags& flags, const zxdump::TaskHolder& holder) {
bool ok = true;
if (flags.collect_job_processes) {
if (auto result = dumper->CollectProcesses(); result.is_error()) {
dumper.Error(result.error_value());
ok = false;
} else {
for (auto& [pid, process] : result.value().get()) {
ok = WriteDump(ProcessDumper{process}, flags, holder) && ok;
}
}
}
if (flags.collect_job_children) {
if (auto result = dumper->CollectChildren(); result.is_error()) {
dumper.Error(result.error_value());
ok = false;
} else {
for (auto& [koid, job] : result.value().get()) {
ok = WriteManyCoreFiles(JobDumper{job}, flags, holder) && ok;
}
}
}
return ok;
}
enum class RemarksType { kText, kJson, kBinary };
std::optional<Remarks> ParseRemarks(const char* arg, RemarksType type) {
Remarks result;
if (const char* eq = strchr(arg, '=')) {
result.name = {arg, static_cast<size_t>(eq - arg)};
arg = eq + 1;
} else {
if (type == RemarksType::kBinary) {
std::cerr << "--remarks-raw requires NAME= prefix" << std::endl;
return std::nullopt;
}
result.name = kDefaultRemarksName;
}
switch (type) {
case RemarksType::kText:
result.name += ".txt"sv;
break;
case RemarksType::kJson:
result.name += ".json"sv;
break;
case RemarksType::kBinary:
break;
}
constexpr auto error = [](const char* filename) -> std::ostream& {
if (filename) {
std::cerr << filename << ": "sv;
}
return std::cerr;
};
auto empty_remarks = [error](const char* filename = nullptr) {
error(filename) << "empty remarks not allowed"sv << std::endl;
return std::nullopt;
};
auto parse_json = [error, &result](auto&& stream, const char* filename = nullptr) {
rapidjson::Reader reader;
rapidjson::Writer<rapidjson::StringBuffer> writer(*result.data);
if (!reader.Parse(stream, writer)) {
error(filename) << "cannot parse JSON at offset " << reader.GetErrorOffset() << ": "
<< GetParseError_En(reader.GetParseErrorCode()) << std::endl;
return false;
}
return true;
};
if (arg[0] == '@') {
// Read the response file.
const char* filename = &arg[1];
FILE* f = fopen(filename, "r");
if (!f) {
perror(filename);
return std::nullopt;
}
auto close_f = fit::defer([f]() { fclose(f); });
if (type == RemarksType::kJson) {
char buffer[BUFSIZ];
rapidjson::FileReadStream stream(f, buffer, sizeof(buffer));
if (!parse_json(stream, filename)) {
return std::nullopt;
}
} else {
int c;
while ((c = getc(f)) != EOF) {
result.data->Put(static_cast<char>(c));
}
if (ferror(f)) {
perror(filename);
return std::nullopt;
}
if (result.data->GetSize() == 0) {
return empty_remarks(filename);
}
}
} else if (type == RemarksType::kJson) {
if (!parse_json(rapidjson::StringStream(arg))) {
return std::nullopt;
}
} else {
std::string_view data{arg};
if (data.empty()) {
return empty_remarks();
}
data.copy(result.data->Push(data.size()), data.size());
}
return result;
}
constexpr const char kOptString[] = "hlo:OzamtcpJjfDUsSkKr:q:B:Rd:L";
constexpr const option kLongOpts[] = {
{"help", no_argument, nullptr, 'h'}, //
{"limit", required_argument, nullptr, 'l'}, //
{"output-prefix", required_argument, nullptr, 'o'}, //
{"streaming", no_argument, nullptr, 'O'}, //
{"zstd", no_argument, nullptr, 'z'}, //
{"exclude-memory", no_argument, nullptr, 'm'}, //
{"no-threads", no_argument, nullptr, 't'}, //
{"no-children", no_argument, nullptr, 'c'}, //
{"no-processes", no_argument, nullptr, 'p'}, //
{"jobs", no_argument, nullptr, 'J'}, //
{"job-archive", no_argument, nullptr, 'j'}, //
{"flat-job-archive", no_argument, nullptr, 'f'}, //
{"no-date", no_argument, nullptr, 'D'}, //
{"date", no_argument, nullptr, 'U'}, //
{"system", no_argument, nullptr, 's'}, //
{"system-recursive", no_argument, nullptr, 'S'}, //
{"kernel", no_argument, nullptr, 'k'}, //
{"kernel-recursive", no_argument, nullptr, 'K'}, //
{"remarks", required_argument, nullptr, 'r'}, //
{"remarks-json", required_argument, nullptr, 'q'}, //
{"remarks-raw", required_argument, nullptr, 'B'}, //
{"remarks-recursive", no_argument, nullptr, 'R'}, //
{"root-job", no_argument, nullptr, 'a'}, //
{"dump-file", required_argument, nullptr, 'd'}, //
{"live", no_argument, nullptr, 'L'}, //
{nullptr, no_argument, nullptr, 0}, //
};
} // namespace
int main(int argc, char** argv) {
Flags flags;
bool streaming = false;
const char* output_argument = nullptr;
bool allow_jobs = false;
constexpr auto handle_process = WriteDump<ProcessDumper>;
auto handle_job = WriteManyCoreFiles;
CommandLineHelper cli;
auto usage = [&](int status = EXIT_FAILURE) {
std::cerr << "Usage: " << argv[0] << R"""( [SWITCHES...] PID...
--help, -h print this message
--output-prefix=PREFIX, -o PREFIX write <PREFIX><PID>, not core.<PID>
--streaming, -O write streaming output
--zstd, -z compress output files with zstd -11
--limit=BYTES, -l BYTES truncate output to BYTES per process
--exclude-memory, -M exclude all process memory from dumps
--no-threads, -t collect only memory, threads left to run
--jobs, -J allow PIDs to be job KOIDs instead
--job-archive, -j write job archives, not process dumps
--flat-job-archive, -f write flattened job archives
--no-children, -c don't recurse to child jobs
--no-processes, -p don't dump processes found in jobs
--no-date, -D don't record dates in dumps
--date, -U record dates in dumps (default)
--system, -s include system-wide information
--system-recursive, -S ... repeated in each child dump
--kernel, -k include privileged kernel information
--kernel-recursive, -K ... repeated in each child dump
--remarks=REMARKS, -r REMARKS add dump remarks (UTF-8 text)
--remarks-json=REMARKS, -q REMARKS add dump remarks (JSON)
--remarks-raw=REMARKS, -B REMARKS add dump remarks (raw binary)
--remarks-recursive, -R repeat dump remarks in each child dump
--root-job, -a dump the root job
--dump-file=FILE, -d FILE read a previous dump file
--live, -L use live data from the running system
By default, each PID must be the KOID of a process.
With --jobs, the KOID of a job is allowed. Each process gets a separate dump
named for its individual PID.
With --job-archive, the KOID of a job is allowed. Each job is dumped into a
job archive named <PREFIX><KOID>.a instead of producing per-process dump files.
If child jobs are dumped they become `core.<KOID>.a` archive members that are
themselves job archives.
With --no-children, don't recurse into child jobs of a job.
With --no-process, don't dump processes within a job, only its child jobs.
Using --no-process with --jobs rather than --job-archive means no dumps are
produced from job KOIDs at all, but valid job KOIDs are ignored rather than
causing errors.
REMARKS can be `NAME=@FILE` to read the remarks from the file, or `NAME=TEXT`
to use the literal text in the argument; just `@FILE` or just `TEXT` is like
`remarks=@FILE` or `remarks=TEXT`. All text is expected to be in UTF-8.
Text for --remarks-json must parse as valid JSON but no particular schema is
expected; it is dumped as compact canonical UTF-8 JSON text.
With `--raw-remarks` (-B), REMARKS is still `NAME=CONTENTS` or `NAME=@FILE`,
but the contents are uninterpreted binary and `NAME` is expected to have its
own suffix rather than having `.txt` or `.json` appended. Note that since
CONTENTS cannot have embedded NUL characters, using `@FILE` for binary data is
always recommended.
Each argument is dumped synchronously before processing the next argument.
Errors dumping each process are reported and cause a failing exit status at
the end of the run, but do not prevent additional processes from being dumped.
Without --no-threads, each process is held suspended while being dumped.
Processes within a job are dumped serially. When dumping a child job inside a
job archive, all processes inside that whole subtree are held suspended until
the whole child job archive is dumped.
With --flat-job-archive, child job archives inside a job archive are instead
"stub" job archives that only describe the job itself. A child job's process
and (grand)child job dumps are all included directly in the outer "flat" job
archive. In this mode, only one process is held suspended at a time.
Jobs are always dumped while they continue to run and may omit new processes
or child jobs created after the dump collection begins. Job dumps may report
process or child job KOIDs that were never dumped if they died during
collection.
With --root-job (-a), dump the root job. Without --no-children, that means
dumping every job on the system; and without --no-process, it means dumping
every process on the system. Doing this without --no-threads may deadlock
essential services. PID arguments are not allowed with --root-job unless
--no-children is also given, since they would always be redundant.
With --streaming (-O), a single contiguous output stream is written, usually to
stdout. If --output=FILE (-o FILE) is given along with --streaming (-O), then
it names a single output file to write in place of stdout rather than a prefix.
When there is a single PID argument or just the --root-job (-a) switch, then
the output stream is a single ELF core file or a single job archive file. When
there are multiple PID arguments, or multiple separate single-process dumps
under --jobs (-J), or a fake root job from postportem data that doesn't form a
single job tree, the output stream is a simple archive that contains the
individual ELF core file or job archive file for each PID argument as member
files with the names used by default (core.<PID> or core.<PID>.a). Readers
treat such an archive just like the collection of separate dump files.
By default, data for dumps is drawn from the running system. Of course, this
only works on Fuchsia. One or more --dump-file (-d) switches can be given to
read old postmortem data instead. This allows, for example, extracting a
subset of the jobs or processes from the original dump set; merging multiple
dumps into one job archive; adding dump remarks; changing configuration details
to dump a subset of the original postmortem data; etc. If no --dump-file (-d)
switches are given, then --live (-L) is the default. Using --live (-L)
explicitly in combination with dump files is allowed, but the results may be
confusing either if the dumps are not from the current running system or if a
task found in the postmortem data is also still alive on the running system.
)""";
return status;
};
while (true) {
switch (getopt_long(argc, argv, kOptString, kLongOpts, nullptr)) {
case -1:
// This ends the loop. All other cases continue (or return).
break;
case 'D':
flags.record_date = false;
continue;
case 'U':
flags.record_date = true;
continue;
case 'o':
flags.output_prefix = optarg;
output_argument = optarg;
continue;
case 'O':
streaming = true;
continue;
case 'l': {
char* p;
flags.limit = strtoul(optarg, &p, 0);
if (*p != '\0') {
return usage();
}
continue;
}
case 'm':
flags.dump_memory = false;
continue;
case 't':
flags.collect_threads = false;
continue;
case 'f':
flags.flatten_jobs = true;
[[fallthrough]];
case 'j':
handle_job = WriteDump<JobDumper>;
[[fallthrough]];
case 'J':
allow_jobs = true;
continue;
case 'c':
flags.collect_job_children = false;
continue;
case 'p':
flags.collect_job_processes = false;
continue;
case 'S':
flags.repeat_system = true;
[[fallthrough]];
case 's':
flags.collect_system = true;
continue;
case 'K':
flags.repeat_kernel = true;
[[fallthrough]];
case 'k':
flags.collect_kernel = true;
continue;
case 'r':
if (auto note = ParseRemarks(optarg, RemarksType::kText)) {
flags.remarks.push_back(std::move(*note));
continue;
}
return usage();
case 'R':
flags.repeat_remarks = true;
continue;
case 'q':
if (auto note = ParseRemarks(optarg, RemarksType::kJson)) {
flags.remarks.push_back(std::move(*note));
continue;
}
return usage();
case 'B':
if (auto note = ParseRemarks(optarg, RemarksType::kBinary)) {
flags.remarks.push_back(std::move(*note));
continue;
}
return usage();
case 'a':
cli.RootJobArgument();
continue;
case 'z':
flags.zstd = true;
continue;
case 'd':
cli.DumpFileArgument(optarg);
continue;
case 'L':
cli.LiveArgument();
continue;
case 'h':
return usage(EXIT_SUCCESS);
default:
return usage();
}
break;
}
cli.KoidArguments(argc, argv, optind, allow_jobs);
cli.NeedRootResource(flags.collect_kernel);
cli.NeedSystem(flags.collect_system);
if (cli.empty() && cli.ok()) {
return usage();
}
constexpr auto is_job = [](zxdump::Task& task) { return task.type() == ZX_OBJ_TYPE_JOB; };
auto call_dumper = [&](auto&& handle_task, auto dumper) {
cli.Ok(handle_task(std::move(dumper), flags, cli.holder()));
};
auto dump_one_task = [&](zxdump::Task& task) {
if (is_job(task)) {
auto& job = static_cast<zxdump::Job&>(task);
call_dumper(handle_job, JobDumper{job});
} else {
ZX_DEBUG_ASSERT(task.type() == ZX_OBJ_TYPE_PROCESS);
auto& process = static_cast<zxdump::Process&>(task);
call_dumper(handle_process, ProcessDumper{process});
}
};
auto tasks = cli.take_tasks();
if (streaming) {
// There will be just one output stream with just one writer.
fbl::unique_fd fd;
std::string outname;
if (output_argument) {
outname = output_argument;
fd = CreateOutputFile(outname);
if (!fd) {
return EXIT_FAILURE;
}
} else {
fd.reset(STDOUT_FILENO);
}
// This gets the single writer passed down to all the dumpers.
// When not streaming, each dumper makes its own writer.
flags.streaming = std::make_unique<Writer>(std::move(fd), std::move(outname), flags.zstd);
// If there will be more than one separate dump, switch to "streaming
// archive" mode. That is, if there are multiple separate KOID arguments
// or the sole KOID is a job but under -J rather than -j.
flags.streaming_archive =
tasks.size() > 1 || (is_job(tasks.front()) && handle_job == WriteManyCoreFiles);
if (flags.streaming_archive && !flags.streaming->StartArchive()) {
return EXIT_FAILURE;
}
}
while (!tasks.empty()) {
zxdump::Task& task = tasks.front();
tasks.pop();
dump_one_task(task);
}
return cli.exit_status();
}