tools/hdb/hdb.cpp (654 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#ifndef HERMES_ENABLE_DEBUGGER
#include <iostream>
int main(void) {
std::cout << "hdb compiled without Hermes debugger enabled." << std::endl;
return EXIT_FAILURE;
}
#else // defined(HERMES_ENABLE_DEBUGGER)
#include <hermes/DebuggerAPI.h>
#include <hermes/Support/OSCompat.h>
#include <hermes/hermes.h>
#include <jsi/jsi.h>
#include <signal.h>
#include <chrono>
#include <cmath>
#include <condition_variable>
#include <cstdlib>
#include <deque>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <map>
#include <mutex>
#include <thread>
// hdb is a sample of using Hermes. Therefore, it should avoid introducing
// a dependency on third-party libraries.
// In addition, practically, we cannot use LLVM in hdb because Hermes build
// rules builds LLVM without exceptions/RTTI, and so we will incur
// link errors.
using namespace facebook::hermes;
using namespace facebook::jsi;
// Declare global variables for HDB Help text
static const std::map<std::string, std::string> commandToHelpText = {
{"frame",
"Modifies selected frame based on specified frame_id (integer)\n\n"
"USAGE: frame <frame_id>\n"},
{"set",
"Sets pauseOnThrow on or off for errors.\n\n"
"USAGE: set pauseOnThrow <on/uncaught/off>\n"},
{"break",
"Sets breakpoint on a given SourceLocation. \n\n"
"Location Formats accepted are:\n\t"
"<line>\n\t"
"<filename> <line>\n\t"
"<line> <column>\n\t"
"<filename> <line> <column>\n"
"Optionally a conditional breakpoint can be specified as: if <condition>\n\n"
"USAGE: break <filename> <line> [<column>] [if <condition>]\n"},
{"delete",
"Deletes all or specified breakpoints.\n\n"
"USAGE: delete [all/<breakpoint_id>]\n"},
{"enable",
"Mark a breakpoint as enabled. Breakpoints are by default enabled.\n\n"
"USAGE: enable <breakpoint_id>\n"},
{"disable",
"Disable a breakpoint given. Returns true if successful.\n\n"
"USAGE: disable <breakpoint_id>\n"},
{"info",
"Displays information about a breakpoint or the paused ProgramState.\n\n"
"USAGE: info <breakpoint_id> to display state of breakpoint\n"
" info variables to display the paused ProgramState and variables\n"},
{"backtrace",
"Prints the current backtrace.\n\n"
"USAGE: backtrace\n"},
{"expand",
"For all property references e.g., foo.bar, prints the properties e.g., foo.bar.baz\n\n"
"USAGE: expand\n"},
{"continue",
"Debugger command: Continues execution only stopping at a breakpoint.\n\n"
"USAGE: continue\n"},
{"step",
"Debugger command: Execute the current line, stop at the first possible occasion.\nThis can be either a function call or the next line in the current function)\n\n"
"USAGE: step\n"},
{"next",
"Debugger command: Continue execution until the next line in the current function is reached or the function returns.\n'next' doesn't stop inside a called function unlike 'step'.\n\n"
"USAGE: step\n"},
{"finish",
"Debugger command: Continue execution until the current function returns.\n\n"
"USAGE: finish\n"},
{"exec",
"Debugger command: Executes the given line.\n\n"
"USAGE: exec\n"},
{"sourceMap",
"Given a file ID, retrieves the source map URL, provided it has been stored in the file. Else, returns an empty string.\n\n"
"USAGE: sourceMap <fileId>\n"},
{"quit",
"Quit the debugger.\n\n"
"USAGE: quit\n"}};
static const std::string topLevelHelpText =
"These hdb commands are defined internally. Type `help' to see this list.\n"
"Type `help name' to find out more about the function `name'.\n\n"
"frame [frame_id]\n"
"set pauseOnThrow [on/uncaught/off]\n"
"break <filename> <line> [<column>] [if <condition>]\n"
"delete [all/<breakpoint_id>]\n"
"enable <breakpoint_id>\n"
"disable <breakpoint_id>\n"
"info <breakpoint_id> <variables>\n"
"backtrace\n"
"expand\n"
"sourceMap <fileId>\n"
"quit\n"
"Debugger commands: continue, step, next, finish, exec\n";
namespace {
/// Removes the first token delimited by any character in \p separators from \p
/// str, and sets \p str to the remainder. \return the token. or an empty string
/// if none.
std::string chompToken(std::string *str, const char *separators = " \t") {
std::string result;
// Chomp leading spaces, then split at the first space.
str->erase(0, str->find_first_not_of(separators));
auto sepOrNpos = str->find_first_of(separators);
result.assign(*str, 0, sepOrNpos);
str->erase(0, sepOrNpos);
// Chomp trailing spaces.
str->erase(0, str->find_first_not_of(separators));
return result;
}
void printUsageAndExit() {
std::cerr
<< "USAGE: hdb [--break-at-start] [--break-after <secs>] [--lazy|--eager|--smart] <input JS file>\n";
exit(EXIT_FAILURE);
}
struct Options {
std::string fileName{}; // required
bool breakAtStart{false};
hermes::vm::CompilationMode compilationMode{
hermes::vm::ForceEagerCompilation};
double breakAfterDelay{-1.}; // -1 disables breakAfterDelay
};
Options getCommandLineOptions(int argc, char **argv) {
Options result;
int idx = 1;
/// return next command line argument, or fail if no more is available
auto nextArg = [argc, argv, &idx]() {
if (idx >= argc) {
printUsageAndExit();
}
return argv[idx++];
};
while (idx < argc) {
char *arg = nextArg();
if (arg[0] != '-') {
if (result.fileName != "") {
printUsageAndExit();
}
result.fileName = arg;
} else if (strcmp(arg, "--break-at-start") == 0) {
result.breakAtStart = true;
} else if (strcmp(arg, "--lazy") == 0) {
result.compilationMode = hermes::vm::ForceLazyCompilation;
} else if (strcmp(arg, "--eager") == 0) {
result.compilationMode = hermes::vm::ForceEagerCompilation;
} else if (strcmp(arg, "--smart") == 0) {
result.compilationMode = hermes::vm::SmartCompilation;
} else if (strcmp(arg, "--break-after") == 0) {
char *endptr = nullptr;
char *strValue = nextArg();
double breakAfterDelay = std::strtod(strValue, &endptr);
if (*endptr != '\0' || (breakAfterDelay < 0 && breakAfterDelay != -1.) ||
!std::isfinite(breakAfterDelay)) {
std::cerr << "Invalid break-after delay: " << strValue << std::endl;
exit(EXIT_FAILURE);
}
result.breakAfterDelay = breakAfterDelay;
} else {
printUsageAndExit();
}
}
if (result.fileName.empty()) {
printUsageAndExit();
}
return result;
}
/// Pointer to debugger for use in our async break signal handlers.
debugger::Debugger *volatile gDebugger = nullptr;
/// Mutex used to control access to gDebugger.
std::mutex gDebuggerMutex;
/// Controls the wait in the scheduled pause thread.
/// Notified when JS is done executing and gDebugger has been set to nullptr,
/// to allow the sleeping thread to complete.
std::condition_variable delayPauseCondVar;
// A signal handler that triggers an async pause in the debugger.
void triggerAsyncPause(int sig) {
if (gDebugger)
gDebugger->triggerAsyncPause(debugger::AsyncPauseKind::Explicit);
}
/// Control whether a SIGINT (aka control-C) should result in an async pause
/// (\p flag = true) or the default behavior (\p flag = false).
void setSIGINTShouldPause(bool flag) {
signal(SIGINT, flag ? triggerAsyncPause : SIG_DFL);
}
/// Trigger an asynchrous pause after the given \p delaySecs. If \p delaySecs is
/// 0, trigger an async pause immediately.
/// \return the thread object representing the pause. This may not represent an
/// actual joinable thread.
std::thread schedulePause(double delaySecs) {
assert(delaySecs >= 0 && std::isfinite(delaySecs) && "Invalid delay");
if (delaySecs == 0) {
gDebugger->triggerAsyncPause(debugger::AsyncPauseKind::Explicit);
return {};
}
return std::thread([delaySecs] {
std::unique_lock<std::mutex> lk{gDebuggerMutex};
// Wait for the main thread to be done running JS and send data, or if JS
// is still executing, take the lock and trigger the async pause.
delayPauseCondVar.wait_for(
lk, std::chrono::duration<double>(delaySecs), []() -> bool {
return !gDebugger;
});
// If JS is done being run then gDebugger will have been set to nullptr.
// If not, then we want to trigger the async pause.
if (gDebugger) {
gDebugger->triggerAsyncPause(debugger::AsyncPauseKind::Explicit);
}
});
}
} // namespace
struct HDBDebugger : public debugger::EventObserver {
/// Construct, referencing a runtime.
HDBDebugger(HermesRuntime &runtime) : runtime_(runtime) {}
/// Parse a string \p input as a debugger command and its arguments.
/// This parses only commands that should result in returning from the
/// didPause handler; commands that manipulate the debugger state are handled
/// in parseAndRunImmediateCommand(). Returns the resulting debugger command
/// by reference in \p outCommand. \return true if we got a Command, false
/// otherwise.
bool parseCommand(
std::string input,
debugger::Debugger &debugger,
debugger::Command *outCommand) const {
using namespace std;
using debugger::Command;
using debugger::StepMode;
// Parse the command.
std::string command = chompToken(&input);
if (command == "continue" || command == "c") {
std::cout << "Continuing execution" << std::endl;
*outCommand = Command::continueExecution();
} else if (command == "step" || command == "s") {
*outCommand = Command::step(StepMode::Into);
} else if (command == "next" || command == "n") {
*outCommand = Command::step(StepMode::Over);
} else if (command == "finish" || command == "out" || command == "o") {
*outCommand = Command::step(StepMode::Out);
} else if (command == "exec" || command == "e") {
*outCommand = Command::eval(input, frame_);
} else {
return false;
}
return true;
}
/// Print a stack trace \p stackTrace, printing a sigil in front of \p
/// calloutFrame to identify it. If calloutFrame exceeds the frame count, no
/// frame will receive the sigil.
void printStackTrace(
const debugger::StackTrace &stackTrace,
uint32_t calloutFrame = UINT32_MAX) const {
for (uint32_t i = 0, e = stackTrace.callFrameCount(); i < e; ++i) {
const auto frame = stackTrace.callFrameForIndex(i);
std::string funcName = frame.functionName;
std::cout << (i == calloutFrame ? '>' : ' ') << ' ';
if (funcName.empty()) {
std::cout << i << ": (unknown)" << std::endl;
} else if (funcName == "(native)") {
std::cout << i << ": " << funcName << std::endl;
} else {
std::cout << i << ": " << funcName << ": " << frame.location.fileName
<< ':' << frame.location.line << ':' << frame.location.column
<< std::endl;
}
}
}
/// Print information about current variables in the selected frame.
void printVariables(const debugger::ProgramState &state) {
if (frame_ >= state.getStackTrace().callFrameCount()) {
std::cout << "Invalid frame " << frame_ << std::endl;
return;
}
auto thisInfo = state.getVariableInfoForThis(frame_);
std::cout << thisInfo.name << " = "
<< thisInfo.value.toString(runtime_).utf8(runtime_) << std::endl;
auto lexicalInfo = state.getLexicalInfo(frame_);
for (uint32_t depth = 0, max = lexicalInfo.getScopesCount(); depth < max;
depth++) {
uint32_t count = lexicalInfo.getVariablesCountInScope(depth);
for (uint32_t i = 0; i < count; i++) {
auto varInfo = state.getVariableInfo(frame_, depth, i);
std::cout << std::setw(2) << depth << ": " << std::setw(10)
<< varInfo.name << " = "
<< varInfo.value.toString(runtime_).utf8(runtime_)
<< std::endl;
}
}
}
/// Parse a string \p input as an immediate command (for example, setting a
/// breakpoint). \return true if we got an immediate command, false otherwise.
bool parseAndRunImmediateCommand(
std::string input,
debugger::Debugger &debugger) {
using debugger::BreakpointID;
using debugger::PauseOnThrowMode;
std::string command = chompToken(&input);
if (command == "frame" || command == "f") {
// Modify the selected frame.
try {
frame_ = std::stoul(input);
std::cout << "Selected frame " << frame_ << '\n';
} catch (const std::invalid_argument &e) {
std::cout << "Invalid frame: " << e.what() << '\n';
}
} else if (command == "set") {
std::string toSet = chompToken(&input);
std::string value = chompToken(&input);
if (toSet == "pauseOnThrow" && value == "on") {
debugger.setPauseOnThrowMode(PauseOnThrowMode::All);
std::cout << "Set pauseOnThrow: all errors\n";
} else if (toSet == "pauseOnThrow" && value == "uncaught") {
debugger.setPauseOnThrowMode(PauseOnThrowMode::Uncaught);
std::cout << "Set pauseOnThrow: uncaught errors\n";
} else if (toSet == "pauseOnThrow" && value == "off") {
debugger.setPauseOnThrowMode(PauseOnThrowMode::None);
std::cout << "Disabled pauseOnThrow\n";
} else {
std::cout << "Invalid 'set' command\n";
}
} else if (command == "break" || command == "b") {
// Break command supports an "if" token.
// When it exists, the rest of the input is interpreted as a condition.
// Chomp one token at a time, and stop if we see "if",
// since every location argument except the first must be a number.
const char *ifStr = "if";
bool conditional = false;
std::vector<std::string> toks{};
// In conditional breakpoints, the 4th token could be "if",
// so read 4 tokens.
for (uint32_t i = 0; i < 4; ++i) {
auto tok = chompToken(&input);
if (tok.empty()) {
break;
}
if (tok == ifStr) {
conditional = true;
break;
}
toks.push_back(tok);
}
try {
debugger::SourceLocation loc{};
// Formats accepted:
// <line>
// <filename> <line>
// <line> <column>
// <filename> <line> <column>
switch (toks.size()) {
case 1:
loc.line = std::stoul(toks[0]);
loc.column = debugger::kInvalidLocation;
loc.fileId = debugger::kInvalidLocation;
break;
case 2:
try {
// Attempt to parse as <line> <column> and use <filename> <line>
// as a fallback.
loc.line = std::stoul(toks[0]);
loc.column = std::stoul(toks[1]);
loc.fileId = debugger::kInvalidLocation;
} catch (const std::invalid_argument &e) {
// If this fails, then give up on parsing the command.
loc.fileName = toks[0];
loc.line = std::stoul(toks[1]);
loc.column = debugger::kInvalidLocation;
loc.fileId = debugger::kInvalidLocation;
}
break;
case 3:
loc.fileName = toks[0];
loc.line = std::stoul(toks[1]);
loc.column = std::stoul(toks[2]);
loc.fileId = debugger::kInvalidLocation;
break;
default:
std::cout << "Invalid breakpoint request\n";
return false;
}
// If the breakpoint was conditional, we've already taken the "if"
// token, so use the rest of the line as the condition.
BreakpointID breakpointId = debugger.setBreakpoint(loc);
if (conditional) {
debugger.setBreakpointCondition(breakpointId, input);
}
if (breakpointId == debugger::kInvalidBreakpoint) {
// Failed to set the breakpoint.
// TODO: Improve error reporting here.
std::cout << "Invalid or duplicate breakpoint not set\n";
} else {
const auto info = debugger.getBreakpointInfo(breakpointId);
const auto &loc =
info.resolved ? info.resolvedLocation : info.requestedLocation;
std::cout << "Set breakpoint " << breakpointId << " at "
<< loc.fileName << '[' << loc.fileId << ']' << ':'
<< loc.line << ':' << loc.column
<< (info.resolved ? "" : " (unresolved)");
if (conditional) {
std::cout << " if " << input;
}
std::cout << '\n';
}
} catch (const std::invalid_argument &e) {
std::cout << "Invalid breakpoint request: " << e.what() << '\n';
}
} else if (command == "delete") {
std::string request = chompToken(&input);
if (request == "all" || request == "a") {
debugger.deleteAllBreakpoints();
std::cout << "Deleted all breakpoints\n";
} else {
try {
BreakpointID breakpointId = std::stoul(request);
debugger.deleteBreakpoint(breakpointId);
std::cout << "Deleted breakpoint " << breakpointId << "\n";
} catch (const std::invalid_argument &e) {
std::cout << "Invalid breakpoint: " << e.what() << '\n';
}
}
} else if (command == "enable") {
try {
BreakpointID breakpointId = std::stoul(input);
debugger.setBreakpointEnabled(breakpointId, true);
std::cout << "Enabled breakpoint " << breakpointId << "\n";
} catch (const std::invalid_argument &e) {
std::cout << "Invalid breakpoint: " << e.what() << '\n';
}
} else if (command == "disable") {
try {
BreakpointID breakpointId = std::stoul(input);
debugger.setBreakpointEnabled(breakpointId, false);
std::cout << "Disabled breakpoint " << breakpointId << "\n";
} catch (const std::invalid_argument &e) {
std::cout << "Invalid breakpoint: " << e.what() << '\n';
}
} else if (command == "info" || command == "i") {
std::string request = chompToken(&input);
if (request == "breakpoints" || request == "b") {
const std::vector<BreakpointID> ids = debugger.getBreakpoints();
for (const auto &id : ids) {
const auto info = debugger.getBreakpointInfo(id);
assert(info.id == id && "invalid breakpoint ID");
const auto &loc =
info.resolved ? info.resolvedLocation : info.requestedLocation;
std::cout << id << ' ' << (info.enabled ? 'E' : 'D') << ' '
<< loc.fileName << ':' << loc.line << ':' << loc.column
<< (info.resolved ? "" : " (unresolved)") << '\n';
}
} else if (request == "variables" || request == "v") {
printVariables(debugger.getProgramState());
} else {
std::cout << "Not a valid info sub-command. See below for list of"
<< "info sub-commands.\n\n"
<< "info breakpoints\n"
<< "info variables\n";
}
} else if (command == "backtrace" || command == "bt") {
printStackTrace(debugger.getProgramState().getStackTrace(), frame_);
} else if (command == "expand") {
try {
expandProperties(debugger, input);
} catch (const std::exception &e) {
std::cout << e.what() << '\n';
}
} else if (command == "sourceMap") {
try {
uint32_t fileId = std::stoul(input);
auto sourceMappingUrl = debugger.getSourceMappingUrl(fileId);
if (sourceMappingUrl.empty()) {
std::cout << "Source map not found for file\n";
} else {
std::cout << sourceMappingUrl << '\n';
}
} catch (const std::invalid_argument &e) {
std::cout << "Invalid fileId: " << e.what() << '\n';
}
} else if (command == "help" || command == "h") {
// Given help <command> print definition of command otherwise
// print the list of available commands
std::string helpCmd = chompToken(&input);
if (commandToHelpText.find(helpCmd) != commandToHelpText.end()) {
std::cout << commandToHelpText.at(helpCmd);
} else if (!helpCmd.empty()) {
std::cout << "Not a valid command. See below for list of commands.\n\n"
<< topLevelHelpText;
} else {
std::cout << topLevelHelpText;
}
} else if (command == "quit" || command == "q") {
exit(0);
} else {
return false;
}
return true;
}
/// Print the properties of \p value.
void outputValue(const Value &value, bool isThrownValue = false) const {
Runtime &r = runtime_;
if (!value.isObject()) {
std::cout << value.toString(r).utf8(r) << std::endl;
} else {
auto valueObj = value.getObject(r);
auto props = valueObj.getPropertyNames(r);
size_t propCount = props.size(r);
// If this is a thrown value, search for a "message" property in the list.
if (isThrownValue) {
auto messageStr = String::createFromAscii(r, "message");
bool haveMessage = false;
for (size_t i = 0; i < propCount; i++) {
if (String::strictEquals(
r, props.getValueAtIndex(r, i).getString(r), messageStr)) {
haveMessage = true;
break;
}
}
// If not found and the property exists, add it to the list.
if (!haveMessage && valueObj.hasProperty(r, messageStr)) {
props.setProperty(r, Value((int)propCount++).toString(r), messageStr);
}
}
bool compact = (propCount <= 1);
std::cout << '{';
for (size_t i = 0; i < propCount; i++) {
auto keyString = props.getValueAtIndex(r, i).getString(r);
auto value = valueObj.getProperty(r, keyString);
auto valueString = value.toString(r);
std::cout << (compact ? " " : "\n ") << keyString.utf8(r) << ": "
<< valueString.utf8(r);
}
std::cout << (compact ? " }\n" : "\n}\n");
}
std::cout.flush();
}
/// Given a property reference like foo.bar, print the properties of the
/// result.
void expandProperties(
debugger::Debugger &debugger,
const std::string &input) {
Runtime &r = runtime_;
// Trim whitespace.
std::string remaining(input, input.find_first_not_of(" \t"));
// Get the initial variable.
Value variable =
getVariable(debugger.getProgramState(), chompToken(&remaining, "."));
// Walk the properties.
while (!remaining.empty()) {
if (!variable.isObject()) {
throw std::runtime_error("Variable is not an object");
}
std::string component = chompToken(&remaining, ".");
variable = variable.getObject(r).getProperty(r, component.c_str());
}
outputValue(variable);
}
Value getVariable(
const debugger::ProgramState &state,
const std::string &name) {
auto lexicalInfo = state.getLexicalInfo(frame_);
for (uint32_t depth = 0, max = lexicalInfo.getScopesCount(); depth < max;
depth++) {
uint32_t count = lexicalInfo.getVariablesCountInScope(depth);
for (uint32_t i = 0; i < count; i++) {
auto info = state.getVariableInfo(frame_, depth, i);
if (info.name == name)
return std::move(info.value);
}
}
throw std::runtime_error("No variable named " + name);
}
/// Print information about the paused program state \p state to stdout.
void printPauseInfo(const debugger::ProgramState &state) {
using debugger::PauseReason;
bool showLocation = true;
switch (state.getPauseReason()) {
case PauseReason::ScriptLoaded:
std::cout << "Break on script load in ";
break;
case PauseReason::DebuggerStatement:
std::cout << "Break on 'debugger' statement in ";
break;
case PauseReason::Breakpoint:
std::cout << "Break on breakpoint " << state.getBreakpoint() << " in ";
break;
case PauseReason::Exception:
std::cout << "Break on exception in ";
break;
case PauseReason::AsyncTrigger:
std::cout << "Interrupted in ";
break;
case PauseReason::StepFinish:
std::cout << "Stepped to ";
break;
case PauseReason::EvalComplete: {
auto evalResult = state.getEvalResult();
if (!evalResult.isException) {
outputValue(evalResult.value);
} else {
const auto &exception = evalResult.exceptionDetails;
std::cout << "Exception: " << exception.text << std::endl;
printStackTrace(exception.getStackTrace(), -1);
std::cout << "Thrown value is: ";
outputValue(evalResult.value, true);
}
showLocation = false;
break;
}
}
if (showLocation) {
auto frame = state.getStackTrace().callFrameForIndex(0);
std::string funcName = frame.functionName;
if (funcName.empty())
funcName = "(unknown)";
std::cout << funcName << ": " << frame.location.fileName << '['
<< frame.location.fileId << ']' << ':' << frame.location.line
<< ':' << frame.location.column << std::endl;
}
}
/// Override of event observer callback. The debugger has paused; we get to
/// report on the pause and then return an action.
debugger::Command didPause(debugger::Debugger &debugger) {
setSIGINTShouldPause(false);
const auto &state = debugger.getProgramState();
printPauseInfo(state);
std::string line;
for (;;) {
if (::isatty(STDIN_FILENO)) {
std::cout << "(hdb) ";
}
if (!getline(line)) {
// Input exhausted. If we're interactive, exit on control-D, otherwise
// just continue.
if (::isatty(STDIN_FILENO))
exit(0);
return debugger::Command::continueExecution();
} else if (line.empty()) {
if (defaultCommand_.empty() ||
!hermes::oscompat::isatty(STDIN_FILENO)) {
continue;
}
line = defaultCommand_;
}
defaultCommand_ = line;
// Try interpreting it as an immediate command.
if (parseAndRunImmediateCommand(line, debugger)) {
continue;
}
// Try parsing the command.
debugger::Command command = debugger::Command::continueExecution();
if (parseCommand(line, debugger, &command)) {
std::cout.flush();
setSIGINTShouldPause(true);
return command;
} else {
std::cout << "Unrecognized command" << std::endl;
}
}
}
void breakpointResolved(
debugger::Debugger &debugger,
debugger::BreakpointID breakpoint) {
const auto info = debugger.getBreakpointInfo(breakpoint);
assert(info.resolved && "resolved event on unresolved breakpoint");
const auto &loc = info.resolvedLocation;
std::cout << "Breakpoint " << breakpoint << " resolved to " << loc.fileName
<< '[' << loc.fileId << ']' << ':' << loc.line << ':'
<< loc.column << '\n';
}
private:
/// Wrapper around std::getline().
/// Read a line from cin, storing it into \p line.
/// \return true if we have a line, false if input was exhausted.
static bool getline(std::string &line) {
for (;;) {
// On receiving EINTR, getline() in libc++ appears to incorrectly mark
// cin's EOF bit. This means that sucessive getline() calls will return
// EOF. Workaround this iostream bug by clearing the cin flags on EINTR.
errno = 0;
if (std::getline(std::cin, line)) {
return true;
} else if (errno == EINTR) {
std::cin.clear();
} else {
// Input exhausted.
return false;
}
}
}
/// Which frame is selected, for the purpose of exec.
uint32_t frame_ = 0;
/// The command to run if no command is entered by a user.
std::string defaultCommand_{};
/// Our runtime.
HermesRuntime &runtime_;
};
int main(int argc, char **argv) {
Options options = getCommandLineOptions(argc, argv);
HermesRuntime::DebugFlags debugFlags{};
// Read the file in 'filename'.
std::ifstream fileStream(options.fileName);
if (!fileStream) {
const char *err = strerror(errno);
std::cerr << "Unable to open file " << argv[1] << ": " << err << std::endl;
return EXIT_FAILURE;
}
std::string fileContents(
(std::istreambuf_iterator<char>(fileStream)),
std::istreambuf_iterator<char>());
std::unique_ptr<HermesRuntime> runtime = makeHermesRuntime();
HDBDebugger debugger(*runtime);
runtime->getDebugger().setEventObserver(&debugger);
runtime->getDebugger().setShouldPauseOnScriptLoad(options.breakAtStart);
gDebugger = &runtime->getDebugger();
int statusCode = 0;
std::thread delayThread;
setSIGINTShouldPause(true);
try {
if (options.breakAfterDelay >= 0) {
delayThread = schedulePause(options.breakAfterDelay);
}
runtime->debugJavaScript(fileContents, options.fileName, debugFlags);
} catch (const facebook::jsi::JSIException &e) {
std::cout << "JavaScript terminated via uncaught exception: " << e.what()
<< '\n';
statusCode = EXIT_FAILURE;
}
// Clean up the delay thread.
{
std::lock_guard<std::mutex> lk{gDebuggerMutex};
setSIGINTShouldPause(false);
gDebugger = nullptr;
}
// Let the thread finish running.
delayPauseCondVar.notify_all();
// Join it to avoid destructors running while the thread finishes.
// Note: delayThread is only NOT joinable if it doesn't actually represent a
// thread: see schedulePause.
if (delayThread.joinable()) {
delayThread.join();
}
return statusCode;
}
#endif // HERMES_ENABLE_DEBUGGER