blob: 9a39ed8f5b39e810500b5a3a8965e63e63e645b6 [file] [log] [blame]
// Copyright 2018 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 "garnet/bin/zxdb/console/console_context.h"
#include <inttypes.h>
#include "garnet/bin/zxdb/client/breakpoint.h"
#include "garnet/bin/zxdb/client/frame.h"
#include "garnet/bin/zxdb/client/process.h"
#include "garnet/bin/zxdb/client/session.h"
#include "garnet/bin/zxdb/client/target.h"
#include "garnet/bin/zxdb/client/thread.h"
#include "garnet/bin/zxdb/console/command.h"
#include "garnet/bin/zxdb/console/command_utils.h"
#include "garnet/bin/zxdb/console/console.h"
#include "garnet/bin/zxdb/console/format_context.h"
#include "garnet/bin/zxdb/console/output_buffer.h"
#include "garnet/bin/zxdb/symbols/location.h"
#include "garnet/public/lib/fxl/logging.h"
#include "garnet/public/lib/fxl/strings/string_printf.h"
namespace zxdb {
ConsoleContext::ConsoleContext(Session* session) : session_(session) {
session->system().AddObserver(this);
// Pick up any previously created targets. This will normally just be the
// default one.
for (Target* target : session->system().GetTargets())
DidCreateTarget(target);
for (JobContext* job_context : session->system().GetJobContexts())
DidCreateJobContext(job_context);
}
ConsoleContext::~ConsoleContext() {
// Unregister for all observers.
session_->system().RemoveObserver(this);
for (auto& target_pair : id_to_target_) {
target_pair.second.target->RemoveObserver(this);
Process* process = target_pair.second.target->GetProcess();
if (process)
process->RemoveObserver(this);
for (auto& thread_pair : target_pair.second.id_to_thread)
thread_pair.second.thread->RemoveObserver(this);
}
}
int ConsoleContext::IdForTarget(const Target* target) const {
const auto& found = target_to_id_.find(target);
if (found == target_to_id_.end()) {
FXL_NOTREACHED();
return 0;
}
return found->second;
}
int ConsoleContext::IdForJobContext(const JobContext* job_context) const {
const auto& found = job_context_to_id_.find(job_context);
if (found == job_context_to_id_.end()) {
FXL_NOTREACHED();
return 0;
}
return found->second;
}
int ConsoleContext::IdForThread(const Thread* thread) const {
const TargetRecord* record =
const_cast<ConsoleContext*>(this)->GetTargetRecord(
thread->GetProcess()->GetTarget());
if (!record)
return 0;
auto found = record->thread_to_id.find(thread);
if (found == record->thread_to_id.end()) {
FXL_NOTREACHED();
return 0;
}
return found->second;
}
int ConsoleContext::IdForFrame(const Frame* frame) const {
// Find the frame in the thread's backtrace. We don't have to worry about
// whether the frames have been synced, since if there is a frame here,
// we know it's present in the thread's list.
Thread* thread = frame->GetThread();
const Stack& stack = thread->GetStack();
for (size_t i = 0; i < stack.size(); i++) {
if (stack[i] == frame)
return static_cast<int>(i);
}
FXL_NOTREACHED(); // Should have found the frame.
return 0;
}
int ConsoleContext::IdForBreakpoint(const Breakpoint* breakpoint) const {
FXL_DCHECK(!breakpoint->IsInternal())
<< "Should not be trying to get the ID of internal breakpoints. The "
"client layer should filter these out.";
auto found = breakpoint_to_id_.find(breakpoint);
if (found == breakpoint_to_id_.end()) {
FXL_NOTREACHED();
return 0;
}
return found->second;
}
void ConsoleContext::SetActiveJobContext(const JobContext* job_context) {
auto found = job_context_to_id_.find(job_context);
if (found == job_context_to_id_.end()) {
FXL_NOTREACHED();
return;
}
active_job_context_id_ = found->second;
}
int ConsoleContext::GetActiveJobContextId() const {
return active_job_context_id_;
}
JobContext* ConsoleContext::GetActiveJobContext() const {
auto found = id_to_job_context_.find(active_job_context_id_);
if (found == id_to_job_context_.end())
return nullptr;
return found->second.job_context;
}
void ConsoleContext::SetActiveTarget(const Target* target) {
auto found = target_to_id_.find(target);
if (found == target_to_id_.end()) {
FXL_NOTREACHED();
return;
}
active_target_id_ = found->second;
}
int ConsoleContext::GetActiveTargetId() const { return active_target_id_; }
Target* ConsoleContext::GetActiveTarget() const {
auto found = id_to_target_.find(active_target_id_);
if (found == id_to_target_.end())
return nullptr;
return found->second.target;
}
void ConsoleContext::SetActiveThreadForTarget(const Thread* thread) {
TargetRecord* record = GetTargetRecord(thread->GetProcess()->GetTarget());
if (!record)
return;
auto found = record->thread_to_id.find(thread);
if (found == record->thread_to_id.end()) {
FXL_NOTREACHED();
return;
}
record->active_thread_id = found->second;
}
int ConsoleContext::GetActiveThreadIdForTarget(const Target* target) {
const TargetRecord* record = GetTargetRecord(target);
if (!record) {
FXL_NOTREACHED();
return 0;
}
return record->active_thread_id;
}
void ConsoleContext::SetActiveFrameForThread(const Frame* frame) {
ThreadRecord* record = GetThreadRecord(frame->GetThread());
if (!record) {
FXL_NOTREACHED();
return;
}
record->active_frame_id = IdForFrame(frame);
}
void ConsoleContext::SetActiveFrameIdForThread(const Thread* thread, int id) {
ThreadRecord* record = GetThreadRecord(thread);
if (!record) {
FXL_NOTREACHED();
return;
}
record->active_frame_id = id;
}
int ConsoleContext::GetActiveFrameIdForThread(const Thread* thread) const {
const ThreadRecord* record = GetThreadRecord(thread);
if (!record) {
FXL_NOTREACHED();
return 0;
}
// Should be a valid frame index in the thread (or no frames and == 0).
FXL_DCHECK((thread->GetStack().empty() &&
record->active_frame_id == 0) ||
(record->active_frame_id >= 0 &&
record->active_frame_id <
static_cast<int>(thread->GetStack().size())));
return record->active_frame_id;
}
void ConsoleContext::SetActiveBreakpoint(const Breakpoint* breakpoint) {
int id = IdForBreakpoint(breakpoint);
if (id != 0)
active_breakpoint_id_ = id;
}
int ConsoleContext::GetActiveBreakpointId() const {
return active_breakpoint_id_;
}
Breakpoint* ConsoleContext::GetActiveBreakpoint() const {
if (active_breakpoint_id_ == 0)
return nullptr;
auto found = id_to_breakpoint_.find(active_breakpoint_id_);
if (found == id_to_breakpoint_.end()) {
FXL_NOTREACHED();
return nullptr;
}
return found->second;
}
SourceAffinity ConsoleContext::GetSourceAffinityForThread(
const Thread* thread) const {
const ThreadRecord* record = GetThreadRecord(thread);
if (!record)
return SourceAffinity::kSource;
return record->source_affinity;
}
void ConsoleContext::SetSourceAffinityForThread(
const Thread* thread, SourceAffinity source_affinity) {
if (source_affinity == SourceAffinity::kNone)
return; // Don't change anything, previous command still stands.
ThreadRecord* record = GetThreadRecord(thread);
if (!record)
return;
record->source_affinity = source_affinity;
}
void ConsoleContext::OutputThreadContext(
const Thread* thread, debug_ipc::NotifyException::Type type,
const std::vector<fxl::WeakPtr<Breakpoint>>& hit_breakpoints) const {
Target* target = thread->GetProcess()->GetTarget();
Console* console = Console::get();
OutputBuffer out;
// Only print out the process when there's more than one.
if (id_to_target_.size() > 1)
out.Append(fxl::StringPrintf("Process %d ", IdForTarget(target)));
out.Append(fxl::StringPrintf("Thread %d stopped ", IdForThread(thread)));
// Skip the exception reason for the debugger breakpoints because they
// mostly add noise.
if (!hit_breakpoints.empty()) {
out.Append(DescribeHitBreakpoints(hit_breakpoints));
} else if (type != debug_ipc::NotifyException::Type::kNone &&
type != debug_ipc::NotifyException::Type::kHardware &&
type != debug_ipc::NotifyException::Type::kSingleStep &&
type != debug_ipc::NotifyException::Type::kSoftware) {
// Show exception type for non-debug exceptions. Most debug exceptions
// are generated by the debugger internally so adds noise.
out.Append(fxl::StringPrintf(
"on %s exception ", debug_ipc::NotifyException::TypeToString(type)));
}
// Frame (current position will always be frame 0).
const Stack& stack = thread->GetStack();
FXL_DCHECK(!stack.empty());
const Location& location = stack[0]->GetLocation();
out.Append("at ");
out.Append(FormatLocation(location, false, false));
if (location.has_symbols()) {
out.Append("\n");
} else {
out.Append(" (no symbol info)\n");
}
console->Output(std::move(out));
Err err = OutputSourceContext(thread->GetProcess(), location,
GetSourceAffinityForThread(thread));
if (err.has_error())
console->Output(err);
}
Err ConsoleContext::FillOutCommand(Command* cmd) const {
// JobContext.
Err result = FillOutJobContext(cmd);
if (result.has_error())
return result;
// Target.
const TargetRecord* target_record = nullptr;
result = FillOutTarget(cmd, &target_record);
if (result.has_error())
return result;
// Thread.
const ThreadRecord* thread_record = nullptr;
result = FillOutThread(cmd, target_record, &thread_record);
if (result.has_error())
return result;
// Frame.
result = FillOutFrame(cmd, thread_record);
if (result.has_error())
return result;
// Breakpoint.
result = FillOutBreakpoint(cmd);
if (result.has_error())
return result;
return Err();
}
void ConsoleContext::DidCreateTarget(Target* target) {
target->AddObserver(this);
int new_id = next_target_id_;
next_target_id_++;
TargetRecord record;
record.target_id = new_id;
record.target = target;
id_to_target_[new_id] = std::move(record);
target_to_id_[target] = new_id;
// Set the active target only if there's none already.
if (active_target_id_ == 0)
active_target_id_ = new_id;
}
void ConsoleContext::DidCreateJobContext(JobContext* job_context) {
// TODO(anmittal): Add observer if required.
int new_id = next_job_context_id_;
next_job_context_id_++;
JobContextRecord record;
record.job_context_id = new_id;
record.job_context = job_context;
id_to_job_context_[new_id] = std::move(record);
job_context_to_id_[job_context] = new_id;
// Set the active job_context only if there's none already.
if (active_job_context_id_ == 0)
active_job_context_id_ = new_id;
}
void ConsoleContext::WillDestroyTarget(Target* target) {
target->RemoveObserver(this);
TargetRecord* record = GetTargetRecord(target);
if (!record) {
FXL_NOTREACHED();
return;
}
if (active_target_id_ == record->target_id) {
// Need to update the default target ID.
if (id_to_target_.empty()) {
// This should only happen in the shutting-down case.
active_target_id_ = 0;
} else {
// Just pick the first target to be the active one. It might be nice to
// have an ordering of which one the user had selected previously in
// case they're toggling between two.
active_target_id_ = id_to_target_.begin()->first;
}
}
// There should be no threads by the time we erase the target mapping.
FXL_DCHECK(record->id_to_thread.empty());
FXL_DCHECK(record->thread_to_id.empty());
target_to_id_.erase(target);
id_to_target_.erase(record->target_id);
// *record is now invalid.
}
void ConsoleContext::DidCreateBreakpoint(Breakpoint* breakpoint) {
int id = next_breakpoint_id_;
next_breakpoint_id_++;
id_to_breakpoint_[id] = breakpoint;
breakpoint_to_id_[breakpoint] = id;
}
void ConsoleContext::WillDestroyBreakpoint(Breakpoint* breakpoint) {
auto found_breakpoint = breakpoint_to_id_.find(breakpoint);
if (found_breakpoint == breakpoint_to_id_.end()) {
FXL_NOTREACHED();
return;
}
int id = found_breakpoint->second;
// Clear any active breakpoint if it's the deleted one.
if (active_breakpoint_id_ == id)
active_breakpoint_id_ = 0;
id_to_breakpoint_.erase(id);
breakpoint_to_id_.erase(found_breakpoint);
}
void ConsoleContext::OnSymbolIndexingInformation(const std::string& msg) {
Console* console = Console::get();
console->Output(OutputBuffer(Syntax::kComment, msg));
}
void ConsoleContext::DidCreateProcess(Target* target, Process* process,
bool autoattached_to_new_process) {
TargetRecord* record = GetTargetRecord(target);
if (!record) {
FXL_NOTREACHED();
return;
}
process->AddObserver(this);
// Restart the thread ID counting when the process starts in case this
// target was previously running (we want to restart numbering every time).
record->next_thread_id = 1;
// Do the feedback.
int target_id = IdForTarget(target);
const char* msg = nullptr;
Process::StartType start_type = process->start_type();
if (start_type == Process::StartType::kAttach) {
msg = "attached to";
} else if (start_type == Process::StartType::kLaunch) {
msg = "launched";
}
FXL_DCHECK(msg);
OutputBuffer out;
out.Append(fxl::StringPrintf("Process %d %s %s\n", target_id, msg,
process->GetName().data()));
if (autoattached_to_new_process) {
out.Append(Syntax::kComment,
" The process is currently in an initializing state. You can "
"only set\n pending breakpoints (symbols haven't been loaded yet) "
"or \"continue\" (DX-912).");
}
Console::get()->Output(std::move(out));
}
void ConsoleContext::WillDestroyProcess(Target* target, Process* process,
DestroyReason reason, int exit_code) {
TargetRecord* record = GetTargetRecord(target);
if (!record) {
FXL_NOTREACHED();
return;
}
Console* console = Console::get();
std::string msg;
switch (reason) {
case TargetObserver::DestroyReason::kExit:
msg = fxl::StringPrintf("Exited with code %d: ", exit_code);
break;
case TargetObserver::DestroyReason::kDetach:
msg += "Detached: ";
break;
case TargetObserver::DestroyReason::kKill:
msg += "Killed: ";
break;
}
msg += DescribeTarget(this, target);
console->Output(msg);
}
void ConsoleContext::DidCreateThread(Process* process, Thread* thread) {
TargetRecord* record = GetTargetRecord(process->GetTarget());
if (!record) {
FXL_NOTREACHED();
return;
}
thread->AddObserver(this);
int thread_id = record->next_thread_id;
record->next_thread_id++;
record->id_to_thread[thread_id].thread = thread;
record->thread_to_id[thread] = thread_id;
// Only make a new thread the default if there is no current thread,
// otherwise the context will be swapping out from under the user as the
// program runs.
if (record->active_thread_id == 0)
record->active_thread_id = thread_id;
}
void ConsoleContext::WillDestroyThread(Process* process, Thread* thread) {
TargetRecord* record = GetTargetRecord(process->GetTarget());
if (!record) {
FXL_NOTREACHED();
return;
}
thread->RemoveObserver(this);
auto found_thread_to_id = record->thread_to_id.find(thread);
if (found_thread_to_id == record->thread_to_id.end()) {
FXL_NOTREACHED();
return;
}
int thread_id = found_thread_to_id->second;
record->id_to_thread.erase(found_thread_to_id->second);
record->thread_to_id.erase(found_thread_to_id);
// Update the active thread if the currently active one is being deleted.
if (thread_id == record->active_thread_id) {
// Just pick the first thread to be the active one. It might be nice to
// have an ordering of which one the user had selected previously in
// case they're toggling between two.
if (record->id_to_thread.empty()) {
record->active_thread_id = 0;
} else {
record->active_thread_id = record->id_to_thread.begin()->first;
}
}
}
void ConsoleContext::OnSymbolLoadFailure(Process* process, const Err& err) {
Console::get()->Output(err);
}
// For comparison, GDB's printout for a breakpoint hit is:
//
// Breakpoint 1, main () at eraseme.c:4
// 4 printf("Hello\n");
//
// And LLDB's is:
//
// * thread #1: tid = 33767, 0x000055555555463e a.out`main + 4 at
// eraseme.c:4, name = 'a.out', stop reason = breakpoint 1.1
// frame #0: 0x000055555555463e a.out`main + 4 at eraseme.c:4
// 1 #include <stdio.h>
// 2
// 3 int main() {
// -> 4 printf("Hello\n");
// 5 return 1;
// 6 }
//
// When stepping, GDB prints out only the 2nd line with source info, and LLDB
// prints out the whole thing with "step over" for "stop reason".
void ConsoleContext::OnThreadStopped(
Thread* thread, debug_ipc::NotifyException::Type type,
const std::vector<fxl::WeakPtr<Breakpoint>>& hit_breakpoints) {
// The stopped, process, thread, and frame should be active.
Target* target = thread->GetProcess()->GetTarget();
SetActiveTarget(target);
SetActiveThreadForTarget(thread);
SetActiveFrameIdForThread(thread, 0);
// Show the location information.
OutputThreadContext(thread, type, hit_breakpoints);
}
void ConsoleContext::OnThreadFramesInvalidated(Thread* thread) {
ThreadRecord* record = GetThreadRecord(thread);
if (!record) {
FXL_NOTREACHED();
return;
}
// Reset the active frame.
record->active_frame_id = 0;
}
ConsoleContext::TargetRecord* ConsoleContext::GetTargetRecord(int target_id) {
return const_cast<TargetRecord*>(
const_cast<const ConsoleContext*>(this)->GetTargetRecord(target_id));
}
const ConsoleContext::TargetRecord* ConsoleContext::GetTargetRecord(
int target_id) const {
auto found_to_record = id_to_target_.find(target_id);
if (found_to_record == id_to_target_.end()) {
FXL_NOTREACHED();
return nullptr;
}
return &found_to_record->second;
}
ConsoleContext::TargetRecord* ConsoleContext::GetTargetRecord(
const Target* target) {
return const_cast<TargetRecord*>(
const_cast<const ConsoleContext*>(this)->GetTargetRecord(target));
}
const ConsoleContext::TargetRecord* ConsoleContext::GetTargetRecord(
const Target* target) const {
auto found_to_id = target_to_id_.find(target);
if (found_to_id == target_to_id_.end()) {
FXL_NOTREACHED();
return nullptr;
}
return GetTargetRecord(found_to_id->second);
}
ConsoleContext::ThreadRecord* ConsoleContext::GetThreadRecord(
const Thread* thread) {
// Share implementation with the non-const version.
return const_cast<ThreadRecord*>(
const_cast<const ConsoleContext*>(this)->GetThreadRecord(thread));
}
const ConsoleContext::ThreadRecord* ConsoleContext::GetThreadRecord(
const Thread* thread) const {
const TargetRecord* target_record =
GetTargetRecord(thread->GetProcess()->GetTarget());
if (!target_record) {
FXL_NOTREACHED();
return nullptr;
}
auto found_thread_to_id = target_record->thread_to_id.find(thread);
if (found_thread_to_id == target_record->thread_to_id.end()) {
FXL_NOTREACHED();
return nullptr;
}
int thread_id = found_thread_to_id->second;
auto found_id_to_thread = target_record->id_to_thread.find(thread_id);
if (found_thread_to_id == target_record->thread_to_id.end()) {
FXL_NOTREACHED();
return nullptr;
}
return &found_id_to_thread->second;
}
Err ConsoleContext::FillOutJobContext(Command* cmd) const {
int job_context_id = cmd->GetNounIndex(Noun::kJob);
if (job_context_id == Command::kNoIndex) {
// No index: use the active one (may or may not exist).
job_context_id = active_job_context_id_;
auto found_job_context = id_to_job_context_.find(job_context_id);
if (found_job_context == id_to_job_context_.end()) {
// When there are no job contexts, the active ID should be 0.
FXL_DCHECK(job_context_id == 0);
} else {
cmd->set_job_context(found_job_context->second.job_context);
}
return Err();
}
// Explicit index given, look it up.
auto found_job_context = id_to_job_context_.find(job_context_id);
if (found_job_context == id_to_job_context_.end()) {
return Err(ErrType::kInput,
fxl::StringPrintf("There is no job %d.", job_context_id));
}
cmd->set_job_context(found_job_context->second.job_context);
return Err();
}
Err ConsoleContext::FillOutTarget(
Command* cmd, TargetRecord const** out_target_record) const {
int target_id = cmd->GetNounIndex(Noun::kProcess);
if (target_id == Command::kNoIndex) {
// No index: use the active one (which should always exist).
target_id = active_target_id_;
auto found_target = id_to_target_.find(target_id);
FXL_DCHECK(found_target != id_to_target_.end());
cmd->set_target(found_target->second.target);
FXL_DCHECK(cmd->target()); // Default target should always exist.
*out_target_record = GetTargetRecord(target_id);
return Err();
}
// Explicit index given, look it up.
auto found_target = id_to_target_.find(target_id);
if (found_target == id_to_target_.end()) {
return Err(ErrType::kInput,
fxl::StringPrintf("There is no process %d.", target_id));
}
cmd->set_target(found_target->second.target);
*out_target_record = GetTargetRecord(target_id);
return Err();
}
Err ConsoleContext::FillOutThread(
Command* cmd, const TargetRecord* target_record,
ThreadRecord const** out_thread_record) const {
int thread_id = cmd->GetNounIndex(Noun::kThread);
const ThreadRecord* thread_record = nullptr;
if (thread_id == Command::kNoIndex) {
// No thread specified, use the default one.
thread_id = target_record->active_thread_id;
auto found_thread = target_record->id_to_thread.find(thread_id);
if (found_thread == target_record->id_to_thread.end()) {
// When there are no threads, the active thread ID will be 0 and that's
// fine. But if it's nonzero, the thread should always be valid.
FXL_DCHECK(thread_id == 0);
} else {
thread_record = &found_thread->second;
cmd->set_thread(thread_record->thread);
}
*out_thread_record = thread_record;
return Err();
}
// Explicit index given, look it up.
auto found_thread = target_record->id_to_thread.find(thread_id);
if (found_thread == target_record->id_to_thread.end()) {
if (target_record->id_to_thread.empty()) {
return Err(ErrType::kInput, "There are no threads in the process.");
}
return Err(
ErrType::kInput,
fxl::StringPrintf("There is no thread %d in the process.", thread_id));
}
thread_record = &found_thread->second;
cmd->set_thread(thread_record->thread);
*out_thread_record = thread_record;
return Err();
}
Err ConsoleContext::FillOutFrame(Command* cmd,
const ThreadRecord* thread_record) const {
int frame_id = cmd->GetNounIndex(Noun::kFrame);
if (frame_id == Command::kNoIndex) {
// No index: use the active one (if any).
if (thread_record) {
auto& stack = thread_record->thread->GetStack();
frame_id = thread_record->active_frame_id;
if (frame_id >= 0 && frame_id < static_cast<int>(stack.size())) {
cmd->set_frame(stack[frame_id]);
} else if (!stack.empty()) {
// Invalid frame index, default to 0th frame.
frame_id = 0;
cmd->set_frame(stack[0]);
}
}
return Err();
}
// Frame index specified, use it.
if (!thread_record)
return Err(ErrType::kInput, "There is no thread to have frames.");
Stack& stack = thread_record->thread->GetStack();
if (frame_id >= 0 && frame_id < static_cast<int>(stack.size())) {
cmd->set_frame(stack[frame_id]);
return Err();
}
// Invalid frame specified. The full backtrace list is populated on
// demand. It could be if the frames aren't synced for the thread we
// could delay processing this command and get the frames, but we're not
// set up to do that (this function is currently synchronous). Instead
// if we detect the list isn't populated and the user requested one
// that's out-of-range, request they manually sync the list.
//
// Check for the presence of one frame to indicate that the thread is in
// a state to have frames at all (stopped). There will always be the
// topmost frame in this case. If the thread is running there will be no
// frames.
if (stack.size() == 1 &&
!thread_record->thread->GetStack().has_all_frames()) {
return Err(ErrType::kInput,
"The frames for this thread haven't been synced.\n"
"Use \"frame\" to list the frames before selecting one to "
"populate the frame list.");
}
return Err(ErrType::kInput,
"Invalid frame index.\n"
"Use \"frame\" to list available ones.");
}
Err ConsoleContext::FillOutBreakpoint(Command* cmd) const {
int breakpoint_id = cmd->GetNounIndex(Noun::kBreakpoint);
if (breakpoint_id == Command::kNoIndex) {
// No index: use the active one (which may not exist).
cmd->set_breakpoint(GetActiveBreakpoint());
return Err();
}
// Explicit index given, look it up.
auto found_breakpoint = id_to_breakpoint_.find(breakpoint_id);
if (found_breakpoint == id_to_breakpoint_.end()) {
return Err(ErrType::kInput,
fxl::StringPrintf("There is no breakpoint %d.", breakpoint_id));
}
cmd->set_breakpoint(found_breakpoint->second);
return Err();
}
std::string ConsoleContext::DescribeHitBreakpoints(
const std::vector<fxl::WeakPtr<Breakpoint>>& hits) const {
// Do two passes since some of the weak pointers may be gone.
std::vector<int> ids;
for (const auto& hit : hits) {
if (hit)
ids.push_back(IdForBreakpoint(hit.get()));
}
if (ids.empty())
return std::string();
if (ids.size() == 1)
return fxl::StringPrintf("on breakpoint %d ", ids[0]);
std::string result("on breakpoints");
for (size_t i = 0; i < ids.size(); i++) {
result += fxl::StringPrintf(" %d", ids[i]);
if (i < ids.size() - 1)
result.push_back(',');
}
result.push_back(' ');
return result;
}
} // namespace zxdb