blob: f8aaf38289aad7c9000d415b668201fc423f167f [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 "src/developer/debug/zxdb/console/command_utils.h"
#include <ctype.h>
#include <inttypes.h>
#include <lib/syslog/cpp/macros.h>
#include <stdio.h>
#include <limits>
#include "src/developer/debug/zxdb/client/breakpoint.h"
#include "src/developer/debug/zxdb/client/breakpoint_location.h"
#include "src/developer/debug/zxdb/client/client_eval_context_impl.h"
#include "src/developer/debug/zxdb/client/filter.h"
#include "src/developer/debug/zxdb/client/frame.h"
#include "src/developer/debug/zxdb/client/job.h"
#include "src/developer/debug/zxdb/client/process.h"
#include "src/developer/debug/zxdb/client/session.h"
#include "src/developer/debug/zxdb/client/setting_schema_definition.h"
#include "src/developer/debug/zxdb/client/source_file_provider_impl.h"
#include "src/developer/debug/zxdb/client/target.h"
#include "src/developer/debug/zxdb/client/thread.h"
#include "src/developer/debug/zxdb/common/err.h"
#include "src/developer/debug/zxdb/common/string_util.h"
#include "src/developer/debug/zxdb/console/command.h"
#include "src/developer/debug/zxdb/console/console.h"
#include "src/developer/debug/zxdb/console/console_context.h"
#include "src/developer/debug/zxdb/console/format_context.h"
#include "src/developer/debug/zxdb/console/format_job.h"
#include "src/developer/debug/zxdb/console/format_location.h"
#include "src/developer/debug/zxdb/console/format_name.h"
#include "src/developer/debug/zxdb/console/format_node_console.h"
#include "src/developer/debug/zxdb/console/format_target.h"
#include "src/developer/debug/zxdb/console/input_location_parser.h"
#include "src/developer/debug/zxdb/console/output_buffer.h"
#include "src/developer/debug/zxdb/console/string_util.h"
#include "src/developer/debug/zxdb/expr/eval_context_impl.h"
#include "src/developer/debug/zxdb/expr/expr.h"
#include "src/developer/debug/zxdb/expr/expr_parser.h"
#include "src/developer/debug/zxdb/expr/expr_value.h"
#include "src/developer/debug/zxdb/expr/format.h"
#include "src/developer/debug/zxdb/expr/number_parser.h"
#include "src/developer/debug/zxdb/expr/return_value.h"
#include "src/developer/debug/zxdb/symbols/base_type.h"
#include "src/developer/debug/zxdb/symbols/elf_symbol.h"
#include "src/developer/debug/zxdb/symbols/function.h"
#include "src/developer/debug/zxdb/symbols/identifier.h"
#include "src/developer/debug/zxdb/symbols/location.h"
#include "src/developer/debug/zxdb/symbols/modified_type.h"
#include "src/developer/debug/zxdb/symbols/process_symbols.h"
#include "src/developer/debug/zxdb/symbols/symbol_utils.h"
#include "src/developer/debug/zxdb/symbols/target_symbols.h"
#include "src/developer/debug/zxdb/symbols/variable.h"
#include "src/lib/fxl/strings/string_printf.h"
#include "src/lib/fxl/strings/trim.h"
namespace zxdb {
Err AssertRunningTarget(ConsoleContext* context, const char* command_name, Target* target) {
Target::State state = target->GetState();
if (state == Target::State::kRunning)
return Err();
return Err(ErrType::kInput,
fxl::StringPrintf("%s requires a running process but process %d is %s.", command_name,
context->IdForTarget(target), TargetStateToString(state)));
}
Err AssertStoppedThreadWithFrameCommand(ConsoleContext* context, const Command& cmd,
const char* command_name, bool validate_nouns) {
if (validate_nouns) {
if (Err err = cmd.ValidateNouns({Noun::kProcess, Noun::kThread, Noun::kFrame}); err.has_error())
return err;
}
if (!cmd.thread()) {
return Err("\"%s\" requires a thread but there is no current thread.", command_name);
}
if (!cmd.thread()->CurrentStopSupportsFrames()) {
return Err(
"\"%s\" requires a suspended thread but thread %d is %s.\n"
"To view and sync thread state with the remote system, type "
"\"thread\".\nOr type \"pause\" to pause a running thread.",
command_name, context->IdForThread(cmd.thread()),
ThreadStateToString(cmd.thread()->GetState(), cmd.thread()->GetBlockedReason()).c_str());
} else if (!cmd.frame()) {
// Theoretically this shouldn't happen: if the thread is in a proper blocked state it should
// have a frame. But we check this because callers will crash if the frame is not valid after
// a successful return.
FX_NOTREACHED() << "Thread has no frame but its state is "
<< ThreadStateToString(cmd.thread()->GetState(),
cmd.thread()->GetBlockedReason());
return Err("Thread has no frame.");
}
return Err();
}
size_t CheckHexPrefix(const std::string& s) {
if (s.size() >= 2u && s[0] == '0' && (s[1] == 'x' || s[1] == 'X'))
return 2u;
return 0u;
}
Err StringToInt(const std::string& s, int* out) {
int64_t value64;
Err err = StringToInt64(s, &value64);
if (err.has_error())
return err;
// Range check it can be stored in an int.
if (value64 < static_cast<int64_t>(std::numeric_limits<int>::min()) ||
value64 > static_cast<int64_t>(std::numeric_limits<int>::max()))
return Err("This value is too large for an integer.");
*out = static_cast<int>(value64);
return Err();
}
Err StringToInt64(const std::string& s, int64_t* out) {
*out = 0;
// StringToNumber expects pre-trimmed input.
std::string trimmed(fxl::TrimString(s, " "));
ErrOrValue number_value = StringToNumber(ExprLanguage::kC, trimmed);
if (number_value.has_error())
return number_value.err();
// Be careful to read the number out in its original sign-edness.
if (number_value.value().GetBaseType() == BaseType::kBaseTypeUnsigned) {
uint64_t u64;
Err err = number_value.value().PromoteTo64(&u64);
if (err.has_error())
return err;
// Range-check that the unsigned value can be put in a signed.
if (u64 > static_cast<uint64_t>(std::numeric_limits<int64_t>::max()))
return Err("This value is too large.");
*out = static_cast<int64_t>(u64);
return Err();
}
// Expect everything else to be a signed number.
if (number_value.value().GetBaseType() != BaseType::kBaseTypeSigned)
return Err("This value is not the correct type.");
return number_value.value().PromoteTo64(out);
}
Err StringToUint32(const std::string& s, uint32_t* out) {
// Re-uses StringToUint64's and just size-checks the output.
uint64_t value64;
Err err = StringToUint64(s, &value64);
if (err.has_error())
return err;
if (value64 > static_cast<uint64_t>(std::numeric_limits<uint32_t>::max())) {
return Err("Expected 32-bit unsigned value, but %s is too large.", s.c_str());
}
*out = static_cast<uint32_t>(value64);
return Err();
}
Err StringToUint64(const std::string& s, uint64_t* out) {
*out = 0;
// StringToNumber expects pre-trimmed input.
std::string trimmed(fxl::TrimString(s, " "));
ErrOrValue number_value = StringToNumber(ExprLanguage::kC, trimmed);
if (number_value.has_error())
return number_value.err();
// Be careful to read the number out in its original sign-edness.
if (number_value.value().GetBaseType() == BaseType::kBaseTypeSigned) {
int64_t s64;
Err err = number_value.value().PromoteTo64(&s64);
if (err.has_error())
return err;
// Range-check that the signed value can be put in an unsigned.
if (s64 < 0)
return Err("This value can not be negative.");
*out = static_cast<uint64_t>(s64);
return Err();
}
// Expect everything else to be an unsigned number.
if (number_value.value().GetBaseType() != BaseType::kBaseTypeUnsigned)
return Err("This value is not the correct type.");
return number_value.value().PromoteTo64(out);
}
Err ReadUint64Arg(const Command& cmd, size_t arg_index, const char* param_desc, uint64_t* out) {
if (cmd.args().size() <= arg_index) {
return Err(ErrType::kInput,
fxl::StringPrintf("Not enough arguments when reading the %s.", param_desc));
}
Err result = StringToUint64(cmd.args()[arg_index], out);
if (result.has_error()) {
return Err(ErrType::kInput, fxl::StringPrintf("Invalid number \"%s\" when reading the %s.",
cmd.args()[arg_index].c_str(), param_desc));
}
return Err();
}
std::string ThreadStateToString(debug_ipc::ThreadRecord::State state,
debug_ipc::ThreadRecord::BlockedReason blocked_reason) {
// Blocked can have many cases, so we handle it separately.
if (state != debug_ipc::ThreadRecord::State::kBlocked)
return debug_ipc::ThreadRecord::StateToString(state);
FX_DCHECK(blocked_reason != debug_ipc::ThreadRecord::BlockedReason::kNotBlocked)
<< "A blocked thread has to have a valid reason.";
return fxl::StringPrintf("Blocked (%s)",
debug_ipc::ThreadRecord::BlockedReasonToString(blocked_reason));
}
std::string ExecutionScopeToString(const ConsoleContext* context, const ExecutionScope& scope) {
switch (scope.type()) {
case ExecutionScope::kSystem:
return "global";
case ExecutionScope::kTarget:
if (scope.target())
return fxl::StringPrintf("pr %d", context->IdForTarget(scope.target()));
return "<Deleted process>";
case ExecutionScope::kThread:
if (scope.thread()) {
return fxl::StringPrintf("pr %d t %d", context->IdForTarget(scope.target()),
context->IdForThread(scope.thread()));
}
return "<Deleted thread>";
}
FX_NOTREACHED();
return std::string();
}
ExecutionScope ExecutionScopeForCommand(const Command& cmd) {
if (cmd.HasNoun(Noun::kThread))
return ExecutionScope(cmd.thread()); // Thread context given explicitly.
if (cmd.HasNoun(Noun::kProcess))
return ExecutionScope(cmd.target()); // Target context given explicitly.
return ExecutionScope(); // Everything else becomes global scope.
}
Err ResolveBreakpointsForModification(const Command& cmd, const char* command_name,
std::vector<Breakpoint*>* output) {
output->clear();
if (cmd.args().size() > 1) {
return Err(ErrType::kInput,
"Expecting zero or one arg for the location.\n"
"Formats: <function>, <file>:<line#>, <line#>, or 0x<address>");
}
if (cmd.args().size() == 1) {
// "bp <index> clear <location>" is pointless.
Err err = cmd.ValidateNouns({});
if (err.has_error())
return err;
// No need to resolve the location here because pending breakpoints only have input_locations.
// As a result, if a user has breakpoints on both main and $main, "clear main" will only clear
// the first breakpoint.
std::vector<InputLocation> input_locations;
if (Err err = ParseLocalInputLocation(cmd.frame(), cmd.args()[0], &input_locations);
err.has_error())
return err;
ConsoleContext* context = &Console::get()->context();
std::vector<Breakpoint*> breakpoints = context->session()->system().GetBreakpoints();
for (Breakpoint* breakpoint : breakpoints) {
// We compare the input_locations vector directly, in hopes that the same input will
// resolve to the same order.
if (input_locations == breakpoint->GetSettings().locations) {
output->push_back(breakpoint);
}
}
if (output->size() == 0) {
auto msg = fxl::StringPrintf("\"%s\" matches zero breakpoints.", cmd.args()[0].c_str());
if (cmd.args()[0].size() && isdigit(cmd.args()[0][0]))
msg += fxl::StringPrintf(" Maybe you want to use \"bp %s %s\"?", cmd.args()[0].c_str(),
command_name);
return Err(msg);
}
return Err();
}
// When cmd.args().size() == 0, use the command's breakpoint context.
Err err = cmd.ValidateNouns({Noun::kBreakpoint});
if (err.has_error())
return err;
if (!cmd.breakpoint()) {
return Err(
fxl::StringPrintf("There is no active breakpoint and no breakpoint or location was given.\n"
"Use \"bp <index> %s\" or \"%s <location>\" to specify one.\n",
command_name, command_name));
}
output->push_back(cmd.breakpoint());
return Err();
}
OutputBuffer FormatThread(const ConsoleContext* context, const Thread* thread) {
OutputBuffer out("Thread ");
out.Append(Syntax::kSpecial, std::to_string(context->IdForThread(thread)));
out.Append(Syntax::kVariable, " state");
out.Append("=" + FormatConsoleString(
ThreadStateToString(thread->GetState(), thread->GetBlockedReason())));
out.Append(Syntax::kVariable, " koid");
out.Append("=" + std::to_string(thread->GetKoid()));
out.Append(Syntax::kVariable, " name");
out.Append("=" + FormatConsoleString(thread->GetName()));
return out;
}
OutputBuffer FormatBreakpoint(const ConsoleContext* context, const Breakpoint* breakpoint,
bool show_context) {
BreakpointSettings settings = breakpoint->GetSettings();
OutputBuffer result("Breakpoint ");
result.Append(Syntax::kSpecial, std::to_string(context->IdForBreakpoint(breakpoint)) + " ");
// Most breakpoints are simple global software breakpoints. To keep things easier to follow,
// only show values that aren't the default.
if (settings.scope.type() != ExecutionScope::kSystem) {
result.Append(Syntax::kVariable, ClientSettings::Breakpoint::kScope);
result.Append(std::string("=\"") + ExecutionScopeToString(context, settings.scope) + "\" ");
}
if (settings.stop_mode != BreakpointSettings::StopMode::kAll) {
result.Append(Syntax::kVariable, ClientSettings::Breakpoint::kStopMode);
result.Append(std::string("=") + BreakpointSettings::StopModeToString(settings.stop_mode) +
" ");
}
if (!settings.enabled) {
result.Append(Syntax::kVariable, ClientSettings::Breakpoint::kEnabled);
result.Append("=");
if (settings.enabled) {
result.Append("true");
} else {
// Highlight disabled breakpoints since that's an unusual condition.
result.Append(Syntax::kError, "false");
}
result.Append(" ");
}
// Include type only for non-software (the normal ones) breakpoints.
if (settings.type != BreakpointSettings::Type::kSoftware) {
result.Append(Syntax::kVariable, ClientSettings::Breakpoint::kType);
result.Append(std::string("=") + BreakpointSettings::TypeToString(settings.type) + " ");
}
if (BreakpointSettings::TypeHasSize(settings.type)) {
result.Append(Syntax::kVariable, ClientSettings::Breakpoint::kSize);
result.Append("=" + std::to_string(settings.byte_size) + " ");
}
if (settings.one_shot) {
result.Append(Syntax::kVariable, ClientSettings::Breakpoint::kOneShot);
result.Append(std::string("=") + BoolToString(settings.one_shot) + " ");
}
bool show_location_details = !settings.locations.empty() && show_context;
size_t matched_locs = breakpoint->GetLocations().size();
if (matched_locs == 0) {
// When more details are being shown below, don't duplicate the "pending" warning.
if (!show_location_details)
result.Append(Syntax::kWarning, "pending ");
result.Append("@ ");
} else if (matched_locs == 1) {
result.Append("@ ");
} else {
result.Append(fxl::StringPrintf("(%zu addrs) @ ", matched_locs));
}
result.Append(FormatInputLocations(settings.locations));
result.Append("\n");
if (show_location_details) {
// Append the source code location.
//
// There is a question of how to show the breakpoint enabled state. The breakpoint has a main
// enabled bit and each location (it can apply to more than one address -- think templates and
// inlined functions) within that breakpoint has its own. But each location normally resolves to
// the same source code location so we can't practically show the individual location's enabled
// state separately.
//
// For simplicity, just base it on the main enabled bit. Most people won't use location-specific
// enabling anyway.
//
// Ignore errors from printing the source, it doesn't matter that much. Since breakpoints are in
// the global scope we have to use the global settings for the build dir. We could use the
// process build dir for process-specific breakpoints but both process-specific breakpoints and
// process-specific build settings are rare.
if (auto locs = breakpoint->GetLocations(); !locs.empty()) {
FormatBreakpointContext(locs[0]->GetLocation(),
SourceFileProviderImpl(breakpoint->session()->system().settings()),
settings.enabled, &result);
} else {
// When the breakpoint resolved to nothing, warn the user, they may have made a typo.
result.Append(Syntax::kWarning, "Pending");
result.Append(
": No current matches for location. It will be matched against new\n"
" processes and shared libraries.\n");
}
}
return result;
}
OutputBuffer FormatInputLocation(const InputLocation& location) {
switch (location.type) {
case InputLocation::Type::kNone: {
return OutputBuffer(Syntax::kComment, "<no location>");
}
case InputLocation::Type::kLine: {
// Don't pass a TargetSymbols to FormatFileLine because we always want the full file name as
// passed-in by the user (as this is an "input" location object). It is surprising if the
// debugger deletes some input.
return OutputBuffer(FormatFileLine(location.line));
}
case InputLocation::Type::kName: {
FormatIdentifierOptions opts;
opts.show_global_qual = true; // Imporant to disambiguate for InputLocations.
opts.bold_last = true;
return FormatIdentifier(location.name, opts);
}
case InputLocation::Type::kAddress: {
return OutputBuffer(to_hex_string(location.address));
}
}
FX_NOTREACHED();
return OutputBuffer();
}
OutputBuffer FormatInputLocations(const std::vector<InputLocation>& locations) {
if (locations.empty())
return OutputBuffer(Syntax::kComment, "<no location>");
// Comma-separate if there are multiples.
bool first_location = true;
OutputBuffer result;
for (const auto& loc : locations) {
if (!first_location)
result.Append(", ");
else
first_location = false;
result.Append(FormatInputLocation(loc));
}
return result;
}
fxl::RefPtr<EvalContext> GetEvalContextForCommand(const Command& cmd) {
if (cmd.frame())
return cmd.frame()->GetEvalContext();
std::optional<ExprLanguage> language;
auto language_setting =
cmd.target()->session()->system().settings().GetString(ClientSettings::System::kLanguage);
if (language_setting == ClientSettings::System::kLanguage_Rust) {
language = ExprLanguage::kRust;
} else if (language_setting == ClientSettings::System::kLanguage_Cpp) {
language = ExprLanguage::kC;
} else {
FX_DCHECK(language_setting == ClientSettings::System::kLanguage_Auto);
}
// Target context only (it may or may not have a process).
return fxl::MakeRefCounted<ClientEvalContextImpl>(cmd.target(), language);
}
Err EvalCommandExpression(const Command& cmd, const char* verb,
const fxl::RefPtr<EvalContext>& eval_context, bool follow_references,
bool verbose_errors, EvalCallback cb) {
if (Err err = cmd.ValidateNouns({Noun::kProcess, Noun::kThread, Noun::kFrame}); err.has_error())
return err;
if (cmd.args().size() != 1)
return Err("Usage: %s <expression>\nSee \"help %s\" for more.", verb, verb);
EvalExpression(
cmd.args()[0], std::move(eval_context), follow_references,
[verbose_errors, cb = std::move(cb), verb = std::string(verb)](ErrOrValue result) mutable {
if (verbose_errors && result.has_error()) {
cb(RewriteCommandExpressionError(verb, result.err()));
} else {
cb(std::move(result));
}
});
return Err();
}
Err EvalCommandAddressExpression(
const Command& cmd, const char* verb, const fxl::RefPtr<EvalContext>& eval_context,
fit::callback<void(const Err& err, uint64_t address, std::optional<uint32_t> size)> cb) {
return EvalCommandExpression(
cmd, verb, eval_context, true, true,
[eval_context, verb = std::string(verb), cb = std::move(cb)](ErrOrValue value) mutable {
if (value.has_error())
return cb(value.err(), 0, std::nullopt);
uint64_t address = 0;
std::optional<uint32_t> size;
if (Err err = ValueToAddressAndSize(eval_context, value.value(), &address, &size);
err.has_error())
return cb(RewriteCommandExpressionError(verb, err), 0, std::nullopt);
cb(Err(), address, size);
});
}
// Errors from the evaluation of expressions of commands oftem don't make sense without context.
Err RewriteCommandExpressionError(const std::string& verb, const Err& err) {
if (err.type() == ErrType::kOptimizedOut) {
// The common error messagess "unavailable" and "optimized out" (both sharing kOptimizedOut) are
// very short because they're often included in long dumps of structures and local variables.
// But it makes this common class of errors very mysterious here.
return Err(
"This variable is %s. Nothing has happened.\n"
"See \"help expressions\" for more on what this means and what to do.",
err.msg().c_str());
}
// All other errors.
std::string effective_verb = verb.empty() ? std::string("the command") : verb;
return Err("Unable to evaluate the expression for " + effective_verb + ". The result was:\n " +
err.msg());
}
std::string FormatConsoleString(const std::string& input) {
// The console parser accepts two forms:
// - A C-style string (raw or not) with quotes and C-style escape sequences.
// - A whitespace-separated string with no escape character handling.
if (input.empty())
return std::string("\"\""); // Empty strings need quotes.
// Determine which of the cases is required.
bool has_space = false;
bool has_special = false;
bool has_quote = false;
for (unsigned char c : input) {
if (isspace(c)) {
has_space = true;
} else if (c < ' ') {
has_special = true;
} else if (c == '"') {
has_quote = true;
}
// We assume any high-bit characters are part of UTF-8 sequences. This isn't necessarily the
// case. We could validate UTF-8 sequences but currently that effort isn't worth it.
}
if (!has_space && !has_special && !has_quote)
return input;
std::string result;
if (has_quote && !has_special) {
// Raw-encode strings with embedded quotes as long as nothing else needs escaping.
// Make sure there's a unique delimiter in case the string has an embedded )".
std::string delim;
while (input.find(")" + delim + "\"") != std::string::npos)
delim += "*";
result = "R\"" + delim + "(";
result += input;
result += ")" + delim + "\"";
} else {
// Normal C string.
result = "\"";
for (char c : input)
AppendCEscapedChar(c, &result);
result += "\"";
}
return result;
}
ErrOr<Target*> GetRunnableTarget(ConsoleContext* context, const Command& cmd) {
Target::State state = cmd.target()->GetState();
if (state == Target::State::kNone)
return cmd.target(); // Current one is usable.
if (cmd.GetNounIndex(Noun::kProcess) != Command::kNoIndex) {
// A process was specified explicitly in the command. Since it's not usable, report an error.
if (state == Target::State::kStarting || state == Target::State::kAttaching) {
return Err(
"The specified process is in the process of starting or attaching.\n"
"Either \"kill\" it or create a \"new\" process context.");
}
return Err(
"The specified process is already running.\n"
"Either \"kill\" it or create a \"new\" process context.");
}
// Create a new target based on the given one.
Target* new_target = context->session()->system().CreateNewTarget(cmd.target());
context->SetActiveTarget(new_target);
return new_target;
}
Err VerifySystemHasRunningProcess(System* system) {
for (const Target* target : system->GetTargets()) {
if (target->GetProcess())
return Err();
}
return Err("No processes are running.");
}
void ProcessCommandCallback(fxl::WeakPtr<Target> target, bool display_message_on_success,
const Err& err, CommandCallback callback) {
if (display_message_on_success || err.has_error()) {
// Display messaging.
Console* console = Console::get();
OutputBuffer out;
if (err.has_error()) {
out.Append(err);
} else if (target) {
out.Append(FormatTarget(&console->context(), target.get()));
}
console->Output(out);
}
if (callback)
callback(err);
}
void JobCommandCallback(const char* verb, fxl::WeakPtr<Job> job, bool display_message_on_success,
const Err& err, CommandCallback callback) {
if (!display_message_on_success && !err.has_error())
return;
Console* console = Console::get();
OutputBuffer out;
if (err.has_error()) {
if (job) {
out.Append(
fxl::StringPrintf("Job %d %s failed.\n", console->context().IdForJob(job.get()), verb));
}
out.Append(err);
} else if (job) {
out.Append(FormatJob(&console->context(), job.get()));
}
console->Output(out);
if (callback) {
callback(err);
}
}
void AsyncPrintReturnValue(const FunctionReturnInfo& info, fit::deferred_callback cb) {
// This only works for symbolized functions.
const Function* func = info.symbol.Get()->As<Function>();
if (!func)
return;
const Stack& stack = info.thread->GetStack();
if (stack.empty())
return; // Something is messed up.
auto eval_context = stack[0]->GetEvalContext();
GetReturnValue(eval_context, func,
[eval_context, func = RefPtrTo(func), cb = std::move(cb)](ErrOrValue val) mutable {
if (val.has_error() || !val.value().type())
return; // Error or void.
auto out = fxl::MakeRefCounted<AsyncOutputBuffer>();
FormatFunctionNameOptions func_name_options;
func_name_options.name.elide_templates = true;
func_name_options.name.bold_last = true;
func_name_options.params = FormatFunctionNameOptions::kNoParams;
out->Append(FormatFunctionName(func.get(), func_name_options));
out->Append(Syntax::kOperatorBold, " 🡲 ");
ConsoleFormatOptions val_options;
val_options.verbosity = ConsoleFormatOptions::Verbosity::kMinimal;
val_options.wrapping = ConsoleFormatOptions::Wrapping::kSmart;
val_options.max_depth = 3;
out->Append(FormatValueForConsole(val.value(), val_options, eval_context));
out->Complete();
if (out->is_complete()) {
Console::get()->Output(out->DestructiveFlatten());
} else {
out->SetCompletionCallback([out = std::move(out), cb = std::move(cb)]() {
Console::get()->Output(out->DestructiveFlatten());
});
}
});
}
void ScheduleAsyncPrintReturnValue(const FunctionReturnInfo& info) {
info.thread->AddPostStopTask(
[weak_thread = info.thread->GetWeakPtr(), info](fit::deferred_callback cb) {
// The FunctionReturnInfo has a thread pointer in it and we need to be sure it stays in
// scope before continuing, even though we don't use the weak pointer directly.
if (!weak_thread)
return;
AsyncPrintReturnValue(info, std::move(cb));
});
}
} // namespace zxdb