diff --git a/src/sst/core/Makefile.am b/src/sst/core/Makefile.am index 469f808bb..4c756feae 100644 --- a/src/sst/core/Makefile.am +++ b/src/sst/core/Makefile.am @@ -78,6 +78,7 @@ nobase_dist_sst_HEADERS = \ math/sqrt.h \ uninitializedQueue.h \ unitAlgebra.h \ + watchPoint.h \ eli/elementinfo.h \ eli/elibase.h \ eli/attributeInfo.h \ diff --git a/src/sst/core/config.cc b/src/sst/core/config.cc index 8f4647eba..a496948dd 100644 --- a/src/sst/core/config.cc +++ b/src/sst/core/config.cc @@ -526,16 +526,18 @@ Config::insertOptions() DEF_SECTION_HEADING("Advanced Options - Debug"); DEF_ARG("run-mode", 0, "MODE", "Set run mode [ init | run | both (default)]", runMode_, true, false, true); DEF_ARG("interactive-console", 0, "ACTION", - "[EXPERIMENTAL] Set console to use for interactive mode. NOTE: This currently only works for serial jobs and " - "this option will be ignored for parallel runs.", + "[EXPERIMENTAL] Set console to use for interactive mode (overrides default console: " + "sst.interactive.simpledebug). " + "NOTE: This currently only works for serial jobs and will be ignored for parallel runs.", interactive_console_, true, false, false); DEF_ARG_OPTVAL("interactive-start", 0, "TIME", "[EXPERIMENTAL] Drop into interactive mode at specified simulated time. If no time is specified, or the time " - "is 0, then it will " - "drop into interactive mode before any events are processed in the main run loop. This option is ignored if no " - "interactive console was set. NOTE: This currently only works for serial jobs and this option will be ignored " + "is 0, then it will drop into interactive mode before any events are processed in the main run loop. " + "NOTE: This currently only works for serial jobs and this option will be ignored " "for parallel runs.", interactive_start_time_, true, false, false); + DEF_ARG("replay-file", 0, "FILE", "Specify file for replaying an interactive debug console session.", replay_file_, + false); #ifdef USE_MEMPOOL DEF_ARG("output-undeleted-events", 0, "FILE", "file to write information about all undeleted events at the end of simulation (STDOUT and STDERR can be used " @@ -561,6 +563,10 @@ Config::insertOptions() /* Advanced Features - Checkpoint */ DEF_SECTION_HEADING("Advanced Options - Checkpointing (EXPERIMENTAL)"); + DEF_FLAG("checkpoint-enable", 0, + "Allows checkpoints to be triggered from the interactive debug console. " + "This option is not needed if checkpoint-wall-period, checkpoint-period, or checkpoint-sim-period are used.", + checkpoint_enable_, false, false, false); DEF_ARG("checkpoint-wall-period", 0, "PERIOD", "Set approximate frequency for checkpoints to be generated in terms of wall (real) time. PERIOD can be " "specified in hours, minutes, and seconds with " @@ -651,6 +657,7 @@ Config::setOptionFromModel(const std::string& entryName, const std::string& valu bool Config::canInitiateCheckpoint() { + if ( checkpoint_enable_.value == true ) return true; if ( checkpoint_wall_period_.value != 0 ) return true; if ( checkpoint_sim_period_.value != "" ) return true; return false; diff --git a/src/sst/core/config.h b/src/sst/core/config.h index 92850bc07..c21e59242 100644 --- a/src/sst/core/config.h +++ b/src/sst/core/config.h @@ -512,6 +512,10 @@ class Config : public ConfigShared, public SST::Core::Serialization::serializabl std::bind(&StandardConfigParsers::from_string_default, std::placeholders::_1, std::placeholders::_2, "0")); + /** + File to replay an interactive console script + */ + SST_CONFIG_DECLARE_OPTION(std::string, replay_file, "", &StandardConfigParsers::from_string); #ifdef USE_MEMPOOL /** @@ -577,6 +581,11 @@ class Config : public ConfigShared, public SST::Core::Serialization::serializabl /**** Advanced options - Checkpointing ****/ + /** + * Enable checkpointing for interactive debug + */ + SST_CONFIG_DECLARE_OPTION(bool, checkpoint_enable, 0, &StandardConfigParsers::flag_set_true); + /** * Interval at which to create a checkpoint in wall time */ diff --git a/src/sst/core/from_string.h b/src/sst/core/from_string.h index 7956691e9..51aecdce7 100644 --- a/src/sst/core/from_string.h +++ b/src/sst/core/from_string.h @@ -12,6 +12,9 @@ #ifndef SST_CORE_FROM_STRING_H #define SST_CORE_FROM_STRING_H +#include +#include +#include #include #include #include @@ -107,7 +110,16 @@ template std::enable_if_t, std::string> to_string(const T& input) { - if constexpr ( std::is_arithmetic_v ) + if constexpr ( std::is_floating_point_v ) { + std::stringstream s; + T abs_val = input < 0 ? -input : input; + if ( abs_val > (T)10e6 || abs_val < (T)10e-6 ) + s << std::scientific << std::setprecision(std::numeric_limits::max_digits10) << input; + else + s << std::fixed << std::setprecision(std::numeric_limits::max_digits10) << input; + return s.str().c_str(); + } + else if constexpr ( std::is_arithmetic_v ) return std::to_string(input); else return typeid(T).name(); // For now, return a string if the type isn't handled elsewhere diff --git a/src/sst/core/impl/interactive/Makefile.inc b/src/sst/core/impl/interactive/Makefile.inc index 1b5d39e49..db87d5d84 100644 --- a/src/sst/core/impl/interactive/Makefile.inc +++ b/src/sst/core/impl/interactive/Makefile.inc @@ -5,5 +5,7 @@ sst_core_sources += \ impl/interactive/simpleDebug.cc \ - impl/interactive/simpleDebug.h + impl/interactive/simpleDebug.h \ + impl/interactive/cmdLineEditor.cc \ + impl/interactive/cmdLineEditor.h diff --git a/src/sst/core/impl/interactive/cmdLineEditor.cc b/src/sst/core/impl/interactive/cmdLineEditor.cc new file mode 100644 index 000000000..20e610d7a --- /dev/null +++ b/src/sst/core/impl/interactive/cmdLineEditor.cc @@ -0,0 +1,377 @@ +// Copyright 2009-2025 NTESS. Under the terms +// of Contract DE-NA0003525 with NTESS, the U.S. +// Government retains certain rights in this software. +// +// Copyright (c) 2009-2025, NTESS +// All rights reserved. +// +// This file is part of the SST software package. For license +// information, see the LICENSE file in the top level directory of the +// distribution. + +#include "sst/core/impl/interactive/cmdLineEditor.h" + +#include +#include +#include +#include +#include +#include + +// #define _KEYB_DEBUG_ + +// only use termios read/write functions for console! + +CmdLineEditor::CmdLineEditor() +{ +#ifdef _KEYB_DEBUG_ + dbgFile.open("_keyb_debug_.out"); +#endif +} + +int +CmdLineEditor::checktty(int rc) +{ + if ( rc == -1 ) { + std::string msg("input error: "); + msg = msg + strerror(errno) + "\n"; + writeStr(msg); + errno = 0; + } + return rc; +} + +int +CmdLineEditor::setRawMode() +{ + termios rawTerm; + errno = 0; + int rc = checktty(tcgetattr(STDIN_FILENO, &originalTerm)); + if ( rc ) return rc; + rawTerm = originalTerm; + // Disable canonical mode and echoing + rawTerm.c_lflag &= ~(ICANON | ECHO); + // Apply new settings + rc = checktty(tcsetattr(STDIN_FILENO, TCSANOW, &rawTerm)); + return rc; +} + +int +CmdLineEditor::restoreTermMode() +{ + // Restore original settings + int rc = checktty(tcsetattr(STDIN_FILENO, TCSANOW, &originalTerm)); + return rc; +} + +bool +CmdLineEditor::read2chars(char (&seq)[3]) +{ + auto nbytes = read(STDIN_FILENO, &seq[0], 1); + if ( !nbytes ) { +#ifdef _KEYB_DEBUG_ + dbgFile << "oops: read2chars tried to read nbytes\n"; +#endif + return false; + } + nbytes = read(STDIN_FILENO, &seq[1], 1); + if ( !nbytes ) { +#ifdef _KEYB_DEBUG_ + dbgFile << "oops: read2chars missing 2nd nbyte\n"; +#endif + return false; + } + seq[2] = '\0'; + return (nbytes > 0); +} + +void +CmdLineEditor::move_cursor_left() +{ + if ( curpos > (int)prompt.size() + 1 ) { + writeStr(move_left_ctl); + curpos--; + } +} + +void +CmdLineEditor::move_cursor_right(int slen) +{ + if ( curpos < (int)prompt.size() + slen + 1 ) { + writeStr(move_right_ctl); + curpos++; + } +} + +void +CmdLineEditor::set_cmd_strings(const std::list& sortedStrings) +{ + cmdStrings = sortedStrings; +} + +bool +CmdLineEditor::selectMatches(const std::list& list, const std::string& searchfor, + std::vector& matches, std::string& newtoken) +{ + std::copy_if(list.begin(), list.end(), std::back_inserter(matches), + [&searchfor](const std::string& s) { return matchBeginStringCaseInsensitive(searchfor, s); }); + if ( matches.size() == 1 ) { + // unique. Set token to this complete string + newtoken = matches[0] + " "; + return true; + } + else if ( matches.size() > 0 ) { + // list all matching strings + writeStr("\n"); + for ( const std::string& s : matches ) { + writeStr(s); + writeStr(" "); + } + writeStr("\n"); + } + return false; +} + +void +CmdLineEditor::flush_bad_escape() +{ + char c; + int max = 4; + while ( read(STDIN_FILENO, &c, 1) ) { + if ( max-- <= 0 ) break; +#ifdef _KEYB_DEBUG_ + dbgFile << "Discarding: " << std::hex << (int)c << std::endl; +#endif + } +} + +void +CmdLineEditor::auto_complete(std::string& cmd) +{ + bool hasTrailingSpace = (!cmd.empty() && cmd.back() == ' '); + std::istringstream iss(cmd); + std::string token; + std::vector tokens; + while ( iss >> token ) + tokens.push_back(token); + if ( tokens.size() == 0 ) { + // list all command strings + if ( cmdStrings.size() > 0 ) { + writeStr("\n"); + for ( const std::string& s : cmdStrings ) { + writeStr(s); + writeStr(" "); + } + writeStr("\n"); + } + } + else if ( tokens.size() == 1 && !hasTrailingSpace ) { + // find all matching command strings starting with tokens[0] + std::vector matches; + bool exact_match = selectMatches(cmdStrings, tokens[0], matches, cmd); + if ( exact_match ) { + curpos = cmd.size() + prompt.size() + 1; +#ifdef _KEYB_DEBUG_ +// dbgFile << "prompt&" << &prompt << "'" << prompt << "'(" << prompt.size() << ") " +// << "cmd&" << &cmd << "'" << cmd << "'(" << cmd.size() << ")" << std::endl; +#endif + return; + } + } + else { + // after 1st token. provide matching strings in object map + if ( this->listing_callback_ == nullptr ) return; + std::list listing; + listing_callback_(listing); + if ( listing.size() == 0 ) return; + if ( hasTrailingSpace ) { + // list everything + writeStr("\n"); + for ( const std::string& s : listing ) { + writeStr(s); + writeStr(" "); + } + writeStr("\n"); + } + else { + std::vector matches; + std::string newtoken; + bool exact_match = selectMatches(listing, tokens[tokens.size() - 1], matches, newtoken); + if ( exact_match ) { + std::stringstream s; + for ( size_t i = 0; i < tokens.size() - 1; i++ ) + s << tokens[i] << " "; + s << newtoken; + cmd = s.str(); + curpos = cmd.size() + prompt.size() + 1; + return; + } + } + } +} + +void +CmdLineEditor::redraw_line(const std::string& s) +{ + std::stringstream line; + line << prompt_clear << s << esc_ctl << curpos << 'G'; + writeStr(line.str()); +#ifdef _KEYB_DEBUG_ +// dbgFile << curpos << " " << s.length() << std::endl; +#endif +}; + +void +CmdLineEditor::getline(const std::vector& cmdHistory, std::string& newcmd) +{ + + // save terminal settings and enable raw mode + if ( setRawMode() == -1 ) return; + + // local edited history + std::vector history = cmdHistory; + history.emplace_back(""); // current command entry + int max = history.size() - 1; // maximum index in history vector + int index = max; // position in history vector + + // initial empty prompt + std::stringstream prompt_line; + prompt_line << prompt_clear << history[index]; + writeStr(prompt_line.str()); + curpos = history[index].size() + prompt.size() + 1; + + // Start checking for keys + char c; + int bytesRead = 1; + std::stringstream oline; + while ( (bytesRead = read(STDIN_FILENO, &c, 1)) == 1 ) { +#ifdef _KEYB_DEBUG_ + dbgFile << std::hex << (int)c << std::endl; +#endif + if ( c == lf_char ) { + // Done if line feed + break; + } + else if ( c == esc_char ) { + // Escape character + char seq[3]; + if ( this->read2chars(seq) ) { + std::string key(seq); + if ( key == arrow_up ) { + if ( index > 0 ) index--; + oline << prompt_clear << history[index]; + writeStr(oline.str()); + curpos = history[index].size() + prompt.size() + 1; + } + else if ( key == arrow_dn ) { + if ( index < max ) index++; + oline << prompt_clear << history[index]; + writeStr(oline.str()); + curpos = history[index].size() + prompt.size() + 1; + } + else if ( key == arrow_lf ) { + move_cursor_left(); + } + else if ( key == arrow_rt ) { + move_cursor_right(history[index].size()); + } + else { +// unknown escape sequence ( TODO: longer than 2 escape sequences ) +#ifdef _KEYB_DEBUG_ + dbgFile << "Unhandled escape sequence\n" << std::endl; +#endif + flush_bad_escape(); + } + } + else { +#ifdef _KEYB_DEBUG_ + dbgFile << "read2chars failed\n" << std::endl; +#endif + } + } + else if ( c >= 32 && c < 127 ) { + if ( curpos >= max_line_size ) continue; + // Insert printable character + int position = history[index].size() == 0 ? 0 : curpos - 1 - prompt.size(); + if ( position < 0 || position > (int)history[index].size() ) { + oline << prompt_clear << position; + writeStr(oline.str()); + continue; // something went wrong + } + history[index].insert(position, 1, c); + curpos++; + redraw_line(history[index]); + } + else if ( c == bs_char ) { + // backspace. Delete a character to the left + if ( curpos <= (int)prompt.size() + 1 ) continue; + curpos--; + int position = curpos - prompt.size() - 1; + if ( position < 0 || position >= (int)history[index].size() ) { + continue; + } + history[index].erase(position, 1); + redraw_line(history[index]); + } + else if ( c == ctrl_d ) { + // delete character at cursor + int position = curpos - prompt.size() - 1; + if ( position < 0 || position >= (int)history[index].size() ) { + continue; + } + history[index].erase(position, 1); + redraw_line(history[index]); + } + else if ( c == ctrl_a ) { + // move cursor to start of line + curpos = prompt.size() + 1; + redraw_line(history[index]); + } + else if ( c == ctrl_e ) { + // move cursor to the end of the line + curpos = history[index].size() + prompt.size() + 1; + oline << prompt_clear << history[index]; + writeStr(oline.str()); + } + else if ( c == ctrl_k ) { + // remove characters from the cursor to the end of the line + int position = history[index].size() == 0 ? 0 : curpos - 1 - prompt.size(); + if ( position < 0 || position >= (int)history[index].size() ) continue; // something went wrong + int num2erase = history[index].size() - curpos; + // std::cout << num2erase << std::endl; + if ( num2erase < 0 || num2erase > (int)history[index].size() ) continue; // something else went wrong + history[index].erase(position); + curpos = history[index].size() + prompt.size() + 1; + oline << prompt_clear << history[index]; + writeStr(oline.str()); + } + else if ( c == ctrl_b ) { + move_cursor_left(); + } + else if ( c == ctrl_f ) { + move_cursor_right(history[index].size()); + } + else if ( c == tab_char ) { + auto_complete(history[index]); + redraw_line(history[index]); + } +#ifdef _KEYB_DEBUG_ + else { + dbgFile << "Unhandled char " << std::hex << c << std::endl; + } +#endif + } + + if ( bytesRead == -1 ) { + oline << "input error: " << strerror(errno); + writeStr(oline.str()); + errno = 0; + } + + // Restore original terminal settings + this->restoreTermMode(); + + // set the new line info + newcmd = history[index]; + writeStr("\n"); +} diff --git a/src/sst/core/impl/interactive/cmdLineEditor.h b/src/sst/core/impl/interactive/cmdLineEditor.h new file mode 100644 index 000000000..415e92890 --- /dev/null +++ b/src/sst/core/impl/interactive/cmdLineEditor.h @@ -0,0 +1,132 @@ +// Copyright 2009-2025 NTESS. Under the terms +// of Contract DE-NA0003525 with NTESS, the U.S. +// Government retains certain rights in this software. +// +// Copyright (c) 2009-2025, NTESS +// All rights reserved. +// +// This file is part of the SST software package. For license +// information, see the LICENSE file in the top level directory of the +// distribution. + +#ifndef SST_CORE_IMPL_INTERACTIVE_CMDLINEEDITOR_H +#define SST_CORE_IMPL_INTERACTIVE_CMDLINEEDITOR_H + +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * Common write to console + */ +inline void +writeStr(const std::string& msg) +{ + (void)!write(STDOUT_FILENO, msg.data(), msg.size()); +} + +/** + * The command line editor uses termios to detect key presses + * and perform auto-completions. Upon entering the editor, + * the current terminal settings are saved and we enter a + * "raw" terminal mode. While in raw terminal mode it is + * critical to ensure that exclusivel use read and write commands + * for std:out access. Mixing iostream access can corrupt the + * buffers. + */ +class CmdLineEditor +{ +public: + static constexpr char esc_char = '\x1B'; + static constexpr char tab_char = '\x9'; + static constexpr char lf_char = '\xa'; + static constexpr char bs_char = '\x7f'; + static constexpr char ctrl_a = '\x1'; + static constexpr char ctrl_b = '\x2'; + static constexpr char ctrl_d = '\x4'; + static constexpr char ctrl_e = '\x5'; + static constexpr char ctrl_f = '\x6'; + static constexpr char ctrl_k = '\xb'; + + const std::string arrow_up = "[A"; + const std::string arrow_dn = "[B"; + const std::string arrow_rt = "[C"; + const std::string arrow_lf = "[D"; + const std::map arrowKeyMap = { + { arrow_up, "Up" }, + { arrow_dn, "Down" }, + { arrow_rt, "Right" }, + { arrow_lf, "Left" }, + }; + + const std::string clear_line_ctl = "\x1B[2K"; + const std::string move_left_ctl = "\x1B[1D"; + const std::string move_right_ctl = "\x1B[1C"; + const std::string esc_ctl = "\x1B["; //"\x1b[5G" moves to column 5 + const std::string move_up_ctl = "\x1B[1F"; + + const std::string prompt = "> "; + const std::string prompt_clear = "\x1B[2K\r> "; + + static constexpr int max_line_size = 2048; + + CmdLineEditor(); + virtual ~CmdLineEditor() = default; + void redraw_line(const std::string& s); + void getline(const std::vector& cmdHistory, std::string& newcmd); + + // Auto-completion support + void set_cmd_strings(const std::list& sortedStrings); + void set_listing_callback(std::function&)> callback) { listing_callback_ = callback; } + + // debug helper + std::ofstream dbgFile; + +private: + termios originalTerm; + std::list cmdStrings = {}; + std::function& callback)> listing_callback_ = nullptr; + + int curpos = -1; + int checktty(int rc); + int setRawMode(); + int restoreTermMode(); + bool read2chars(char (&seq)[3]); + void move_cursor_left(); + void move_cursor_right(int slen); + void auto_complete(std::string& cmd); + bool selectMatches(const std::list& list, const std::string& searchfor, + std::vector& matches, std::string& newcmd); + +private: + // yet more string helpers + static bool compareCharCaseInsensitive(char c1, char c2) + { + return std::tolower(static_cast(c1)) == std::tolower(static_cast(c2)); + }; + + // exact, but case insenstive, match + // static bool compareStringCaseInsensitive(const std::string& s1, const std::string& s2) { + // if (s1.length() != s2.length()) + // return false; + // return std::equal(s1.begin(), s1.end(), s2.begin(), compareCharCaseInsensitive); + // }; + + // match if begining of second string matches the first + static bool matchBeginStringCaseInsensitive(const std::string& searchfor, const std::string& longstr) + { + if ( longstr.length() < searchfor.length() ) return false; + std::string matchstr = longstr.substr(0, searchfor.length()); + return std::equal(searchfor.begin(), searchfor.end(), matchstr.begin(), compareCharCaseInsensitive); + } + + // Bug: Why is this escape code injected at the 7th char after verbose printing of 0x...? + void flush_bad_escape(); +}; + +#endif // SST_CORE_IMPL_INTERACTIVE_CMDLINEEDITOR_H diff --git a/src/sst/core/impl/interactive/simpleDebug.cc b/src/sst/core/impl/interactive/simpleDebug.cc index c3e9ff83f..0ab8b9a2f 100644 --- a/src/sst/core/impl/interactive/simpleDebug.cc +++ b/src/sst/core/impl/interactive/simpleDebug.cc @@ -9,29 +9,193 @@ // information, see the LICENSE file in the top level directory of the // distribution. +// #include "simpleDebug.h" #include "sst_config.h" #include "sst/core/impl/interactive/simpleDebug.h" #include "sst/core/baseComponent.h" +#include "sst/core/simulation_impl.h" #include "sst/core/stringize.h" #include "sst/core/timeConverter.h" +#include #include #include +#include +#include #include +#include +#include + +#include "simpleDebug.h" namespace SST::IMPL::Interactive { -SimpleDebugger::SimpleDebugger(Params& UNUSED(params)) : +SimpleDebugger::SimpleDebugger(Params& params) : InteractiveConsole() -{} +{ + // registerAsPrimaryComponent(); + + // We can specify a replay file from the sst command line. + std::string sstReplayFilePath = params.find("replayFile", ""); + if ( sstReplayFilePath.size() > 0 ) injectedCommand << "replay " << sstReplayFilePath << std::endl; + + // Populate the command registry + cmdRegistry = { + { "help", "?", "<[CMD]>: show this help or detailed command help", ConsoleCommandGroup::GENERAL, + [this](std::vector& tokens) { cmd_help(tokens); } }, + { "verbose", "v", "[mask]: set verbosity mask or print if no mask specified", ConsoleCommandGroup::GENERAL, + [this](std::vector& tokens) { cmd_verbose(tokens); } }, + { "confirm", "cfm", ": set confirmation requests on (default) or off", ConsoleCommandGroup::GENERAL, + [this](std::vector& tokens) { cmd_setConfirm(tokens); } }, + { "pwd", "pwd", "print the current working directory in the object map", ConsoleCommandGroup::NAVIGATION, + [this](std::vector& tokens) { cmd_pwd(tokens); } }, + { "chdir", "cd", "change 1 directory level in the object map", ConsoleCommandGroup::NAVIGATION, + [this](std::vector& tokens) { cmd_cd(tokens); } }, + { "list", "ls", "list the objects in the current level of the object map", ConsoleCommandGroup::NAVIGATION, + [this](std::vector& tokens) { cmd_ls(tokens); } }, + { "time", "tm", "print current simulation time in cycles", ConsoleCommandGroup::STATE, + [this](std::vector& tokens) { cmd_time(tokens); } }, + { "print", "p", "[-rN] []: print objects at the current level", ConsoleCommandGroup::STATE, + [this](std::vector& tokens) { cmd_print(tokens); } }, + { "set", "s", "var value: set value for a variable at the current level", ConsoleCommandGroup::STATE, + [this](std::vector& tokens) { cmd_set(tokens); } }, + { "watch", "w", ": adds watchpoint to the watchlist", ConsoleCommandGroup::WATCH, + [this](std::vector& tokens) { cmd_watch(tokens); } }, + { "trace", "t", " : : ... : ", ConsoleCommandGroup::WATCH, + [this](std::vector& tokens) { cmd_trace(tokens); } }, + { "watchlist", "wl", "prints the current list of watchpoints", ConsoleCommandGroup::WATCH, + [this](std::vector& tokens) { cmd_watchlist(tokens); } }, + { "addTraceVar", "add", " ... ", ConsoleCommandGroup::WATCH, + [this](std::vector& tokens) { cmd_addTraceVar(tokens); } }, + { "printWatchPoint", "prw", ": prints a watchpoint", ConsoleCommandGroup::WATCH, + [this](std::vector& tokens) { cmd_printWatchpoint(tokens); } }, + { "printTrace", "prt", ": prints trace buffer for a watchpoint", ConsoleCommandGroup::WATCH, + [this](std::vector& tokens) { cmd_printTrace(tokens); } }, + { "resetTrace", "rst", ": reset trace buffer for a watchpoint", ConsoleCommandGroup::WATCH, + [this](std::vector& tokens) { cmd_resetTraceBuffer(tokens); } }, + { "setHandler", "shn", " ... : trigger check/sampling handler", ConsoleCommandGroup::WATCH, + [this](std::vector& tokens) { cmd_setHandler(tokens); } }, + { "unwatch", "uw", ": remove 1 or all watchpoints", ConsoleCommandGroup::WATCH, + [this](std::vector& tokens) { cmd_unwatch(tokens); } }, + { "run", "r", "[TIME]: continues the simulation", ConsoleCommandGroup::SIMULATION, + [this](std::vector& tokens) { cmd_run(tokens); } }, + { "continue", "c", "alias for run", ConsoleCommandGroup::SIMULATION, + [this](std::vector& tokens) { cmd_run(tokens); } }, + { "exit", "e", "exit debugger and continue simulation", ConsoleCommandGroup::SIMULATION, + [this](std::vector& tokens) { cmd_exit(tokens); } }, + { "quit", "q", "alias for exit", ConsoleCommandGroup::SIMULATION, + [this](std::vector& tokens) { cmd_exit(tokens); } }, + { "shutdown", "shutd", "exit the debugger and cleanly shutdown simulator", ConsoleCommandGroup::SIMULATION, + [this](std::vector& tokens) { cmd_shutdown(tokens); } }, + { "logging", "log", ": log command line entires to file", ConsoleCommandGroup::LOGGING, + [this](std::vector& tokens) { cmd_logging(tokens); } }, + { "replay", "rep", ": run commands from a file. See also: sst --replay", ConsoleCommandGroup::LOGGING, + [this](std::vector& tokens) { cmd_replay(tokens); } }, + { "history", "h", "[N]: display all or last N unique commands", ConsoleCommandGroup::LOGGING, + [this](std::vector& tokens) { cmd_history(tokens); } }, + { "autoComplete", "ac", "toggle command line auto-completion enable", ConsoleCommandGroup::MISC, + [this](std::vector& tokens) { cmd_autoComplete(tokens); } }, + { "clear", "clr", "reset terminal", ConsoleCommandGroup::MISC, + [this](std::vector& tokens) { cmd_clear(tokens); } }, + { "spinThread", "spin", "enter spin loop. See SimpleDebugger::cmd_spinThread", ConsoleCommandGroup::MISC, + [this](std::vector& tokens) { cmd_spinThread(tokens); } }, + }; + + // Detailed help from some commands. Can also add general things like 'help navigation' + cmdHelp = { + { "verbose", "[mask]: set verbosity mask or print if no mask specified\n" + "\tA mask is used to select which features to enable verbosity.\n" + "\tTo turn on all features set the mask to 0xffffffff\n" + "\t\t0x10: Show trigger details" }, + { "print", "[-rN][]: print objects in the current level of the object map\n" + "\tif -rN is provided print recursive N levels (default N=4)" }, + { "set", " : sets an object in the current scope to the provided value\n" + "\tobject must be a 'fundamental type' (arithmetic or string)\n" + "\t e.g. set mystring hello world" }, + { "watchpoints", + "Manage watchpoints (with or without tracing)\n" + "\tA can be a or a sequence of comparisons combined with a \n" + "\tE.g. = or ...\n" + "\tA can be ' changed' which checks whether the value has changed\n" + "\tor ' ' which compares the variable to a given value\n" + "\tAn can be <, <=, >, >=, ==, or !=\n" + "\tA can be && or ||\n" + "\t'watch' creates a default watchpoint that breaks into an interactive console when triggered\n" + "\t'trace' creates a watchpoint with a trace buffer to trace a set of variables and trigger an \n" + "\tAvailable actions include: \n" + "\t interactive, printTrace, checkpoint, set , printStatus, or shutdown" }, + { "watch", ": adds watchpoint to the watchlist; breaks into interactive console when triggered\n" + "\tExample: watch var1 > 90 && var2 < 100 || var3 changed" }, + { "trace", + " : : ... : \n" + "\tAdds watchpoint to the watchlist with a trace buffer of and a post trigger delay of " + "\n" + "\tTraces all of the variables specified in the var list and invokes the after postDelay " + "when triggered\n" + "\tAvailable actions include: \n" + "\t interactive, printTrace, checkpoint, set , printStatus, or shutdown\n" + "\t Note: checkpoint action must be enabled at startup via the '--checkpoint-enable' command line option\n" + "\tExample: trace var1 > 90 || var2 == 100 : 32 4 : size count state : printTrace" }, + { "watchlist", "prints the current list of watchpoints and their associated indices" }, + { "addtracevar", " ... : adds the specified variables to the specified " + "watchpoint's trace buffer" }, + { "printwatchpoint", ": prints the watchpoint based on the index specified by watchlist" }, + { "printtrace", ": prints the trace buffer for the specified watchpoint" }, + { "resettrace", ": resets the trace buffer for the specified watchpoint" }, + { "sethandler", " ... \n" + "\tset where to do trigger checks and sampling (before/after clock/event handler)" }, + { "unwatch", ": removes the specified watchpoint from the watch list.\n" + "\tIf no index is provided, all watchpoints are removed." }, + { "run", "[TIME]: runs the simulation from the current point for TIME and then returns to\n" + "\tinteractive mode; if no time is given, the simulation runs to completion;\n" + "\tTIME is of the format e.g. 4us" }, + { "history", "[N]: list previous N instructions. If N is not set list all\n" + "\tSupports bash-style commands:\n" + "\t!! execute previous command\n" + "\t!n execute command at index n\n" + "\t!-n execute commad n lines back in history\n" + "\t!string execute the most recent command starting with `string`\n" + "\t?string execute the most recent command containing `string`\n" + "\t!...:p print the instruction but not execute it." }, + { "editing", ": bash style command line editing using arrow and control keys:\n" + "\tUp/Down keys: navigate command history\n" + "\tLeft/Right keys: navigate command string\n" + "\tbackspace: delete characters to the left\n" + "\ttab: auto-completion\n" + "\tctrl-a: move cursor to beginning of line\n" + "\tctrl-b: move cursor to the left\n" + "\tctrl-d: delete character at cursor\n" + "\tctrl-e: move cursor to end of line\n" + "\tctrl-f: move cursor to the right\n" }, + }; + + // Command autofill strings + std::list cmdStrings; + for ( const ConsoleCommand& c : cmdRegistry ) { + cmdStrings.emplace_back(c.str_long()); + cmdStrings.emplace_back(c.str_short()); + } + cmdStrings.sort(); + cmdLineEditor.set_cmd_strings(cmdStrings); // could also realize as callback to generalize + + // Callback for directory listing strings + cmdLineEditor.set_listing_callback([this](std::list& vec) { get_listing_strings(vec); }); +} + +SimpleDebugger::~SimpleDebugger() +{ + if ( loggingFile.is_open() ) loggingFile.close(); + if ( replayFile.is_open() ) replayFile.close(); +} void SimpleDebugger::execute(const std::string& msg) { printf("Entering interactive mode at time %" PRI_SIMTIME " \n", getCurrentSimCycle()); printf("%s\n", msg.c_str()); + if ( nullptr == obj_ ) { obj_ = getComponentObjectMap(); } @@ -39,12 +203,117 @@ SimpleDebugger::execute(const std::string& msg) std::string line; while ( !done ) { - printf("> "); - std::getline(std::cin, line); - dispatch_cmd(line); + + try { + // User input prompt + std::cout << "> " << std::flush; + + if ( !injectedCommand.str().empty() ) { + // Injected command stream (currently just one command) + line = injectedCommand.str(); + injectedCommand.str(""); + std::cout << line << std::endl; + } + else if ( replayFile.is_open() ) { + // Replay commands from file + if ( std::getline(replayFile, line) ) { + std::cout << line << std::endl; + } + else { + if ( replayFile.eof() ) + std::cout << "(Finished reading from " << replayFilePath << ")" << std::endl; + else + std::cout << "An error occured reading from " << replayFilePath << std::endl; + replayFile.close(); + } + } + else { + // Standard Input + if ( !std::cin ) std::cin.clear(); // fix corrupted input after process resumed + std::cout.flush(); + if ( autoCompleteEnable && isatty(STDIN_FILENO) ) + cmdLineEditor.getline(cmdHistoryBuf.getBuffer(), line); + else + std::getline(std::cin, line); + } + + dispatch_cmd(line); + + // Command Logging + if ( enLogging ) loggingFile << line.c_str() << std::endl; + // This prevents logging the 'logging' command + if ( loggingFile.is_open() ) enLogging = true; + } + catch ( const std::runtime_error& e ) { + std::cout << "Parsing error. Ignoring " << line << std::endl; + } + } +} + +// Invoke the command. +// Substitution actions (!!, !?, ...) can modify the command. +// This ensure the final, resolved, command is captured in the command log +void +SimpleDebugger::dispatch_cmd(std::string& cmd) +{ + // empty command + if ( cmd.size() == 0 ) return; + + std::vector tokens; + tokenize(tokens, cmd); + + // just whitespace + if ( tokens.size() == 0 ) return; + + // comment + if ( tokens[0][0] == '#' ) return; + + // History !! and friends + if ( tokens[0][0] == '!' ) { + std::string newcmd; + auto rc = cmdHistoryBuf.bang(tokens[0], newcmd); + if ( rc == CommandHistoryBuffer::BANG_RC::ECHO_ONLY ) { + // replace, print, save command in history + cmd = newcmd; + std::cout << cmd << std::endl; + cmdHistoryBuf.append(cmd); + return; + } + else if ( rc == CommandHistoryBuffer::BANG_RC::EXEC ) { + // replace and print new command then let it flow through + std::cout << newcmd << std::endl; + tokens.clear(); + cmd = newcmd; + tokenize(tokens, cmd); + } + else if ( rc == CommandHistoryBuffer::BANG_RC::NOP ) { + // invalid search, just return + return; + } + } + + // Search for the requested command and execute it if found. + for ( auto consoleCommand : cmdRegistry ) { + if ( consoleCommand.match(tokens[0]) ) { + consoleCommand.exec(tokens); // TODO prefer having return code to know if succeeded + cmdHistoryBuf.append(cmd); + return; + } } + + // No matching command found + std::cout << "Unknown command: " << tokens[0].c_str() << std::endl; + cmdHistoryBuf.append(cmd); // want garbled command so we can fix using command line editor } +// +/* + !!: Executes the previous command + !n: Executes the command at history index n. + !-n: Executes the command n lines back in history. + !string: Executes the most recent command starting with "string". + !?string: Executes the most recent command containing "string" anywhere. +*/ // Functions for the Explorer @@ -62,43 +331,69 @@ SimpleDebugger::tokenize(std::vector& tokens, const std::string& in } void -SimpleDebugger::cmd_help(std::vector& UNUSED(tokens)) +SimpleDebugger::cmd_help(std::vector& tokens) { - std::string help = "SimpleDebug Console Commands\n"; - help.append(" Navigation: Navigate the current object map\n"); - help.append(" - pwd: print the current working directory in the object map\n"); - help.append(" - cd: change directory level in the object map\n"); - help.append(" - ls: list the objects in the current level of the object map\n"); - - help.append(" Current State: Print information about the current simulation state\n"); - help.append(" - time: print current simulation time in cycles\n"); - help.append(" - print [-rN][]: print objects in the current level of the object map;\n"); - help.append(" if -rN is provided print recursive N levels (default N=4)\n"); - - help.append(" Modify State: Modify simulation variables\n"); - help.append(" - set : sets an object in the current scope to the provided value;\n"); - help.append(" object must be a \"fundamental type\" e.g. int \n"); - - help.append(" Watch Points: Manage watch points which break into interactive console when triggered\n"); - help.append(" - watch: prints the current list of watch points and their associated indices\n"); - help.append(" watch : adds var to the watch list; triggered when value changes\n"); - help.append(" watch : add var to watch list; triggered when comparison with val is true\n"); - help.append(" Valid operators: <, <=, >, >=, ==, !=\n"); - help.append(" - unwatch : removes the indexed watch point from the watch list;\n"); - help.append(" is the associated index from the list of watch points\n"); - - help.append(" Execute: Execute the simulation for a specified duration\n"); - help.append(" - run [TIME]: runs the simulation from the current point for TIME and then returns to\n"); - help.append(" interactive mode; if no time is given, the simulation runs to completion;\n"); - help.append(" TIME is of the format e.g. 4us\n"); + // First check for specific command help + if ( tokens.size() == 1 ) { + for ( const auto& g : GroupText ) { + std::cout << "--- " << g.second << " ---" << std::endl; + for ( const auto& c : cmdRegistry ) { + if ( g.first == c.group() ) std::cout << c << std::endl; + } + } + std::cout << "\nMore detailed help also available for:\n"; + std::stringstream s; + for ( const auto& pair : cmdHelp ) { + if ( (s.str().length() + pair.first.length() > 39) ) { + std::cout << "\t" << s.str() << std::endl; + s.str(""); + s.clear(); + } + s << pair.first << " "; + } + std::cout << "\t" << s.str() << std::endl; + std::cout << std::endl; + return; + } - help.append(" Exit: Exit the interactive console\n"); - help.append(" - exit or quit: exits the interactive console and resumes simulation execution\n"); - help.append(" - shutdown: exits the interactive console and does a clean shutdown of the simulation\n"); + if ( tokens.size() > 1 ) { + std::string c = tokens[1]; + if ( cmdHelp.find(c) != cmdHelp.end() ) { + std::cout << c << " " << cmdHelp.at(c) << std::endl; + } + else { + for ( auto& creg : cmdRegistry ) { + if ( creg.match(c) ) std::cout << creg << std::endl; + } + } + } +} - printf("%s", help.c_str()); +void +SimpleDebugger::cmd_verbose(std::vector& tokens) +{ + if ( tokens.size() > 1 ) { + try { + verbosity = SST::Core::from_string(tokens[1]); + } + catch ( std::invalid_argument& e ) { + std::cout << "Invalid mask " << tokens[1] << std::endl; + } + } +#if 1 + // This messes up the auto-complete keyboard input + std::cout << "verbose=0x" << std::hex << verbosity << std::endl; +#else + std::cout << "verbose=" << verbosity << std::endl; +#endif + + // update watchpoint verbosity + for ( auto& x : watch_points_ ) { + if ( x.first ) x.first->setVerbosity(verbosity); + } } +// pwd: print current working directory void SimpleDebugger::cmd_pwd(std::vector& UNUSED(tokens)) { @@ -111,23 +406,41 @@ SimpleDebugger::cmd_pwd(std::vector& UNUSED(tokens)) // curr = curr->getParent(); // } - printf("%s (%s)\n", obj_->getFullName().c_str(), obj_->getType().c_str()); + std::cout << obj_->getFullName() << " (" << obj_->getType() << ")\n"; } +// ls: list current directory void SimpleDebugger::cmd_ls(std::vector& UNUSED(tokens)) { auto& vars = obj_->getVariables(); for ( auto& x : vars ) { if ( x.second->isFundamental() ) { - printf("%s = %s (%s)\n", x.first.c_str(), x.second->get().c_str(), x.second->getType().c_str()); + std::cout << x.first << " = " << x.second->get() << " (" << x.second->getType() << ")" << std::endl; } else { - printf("%s/ (%s)\n", x.first.c_str(), x.second->getType().c_str()); + std::cout << x.first.c_str() << "/ (" << x.second->getType() << ")\n"; } } } +// callback for autofill of object string (similar to ls) +void +SimpleDebugger::get_listing_strings(std::list& list) +{ + list.clear(); + auto& vars = obj_->getVariables(); + for ( auto& x : vars ) { + std::stringstream s; + s << x.first; + if ( !x.second->isFundamental() ) s << "/"; + list.emplace_back(s.str()); + } + list.sort(); +} + + +// cd : change to new directory void SimpleDebugger::cmd_cd(std::vector& tokens) { @@ -136,8 +449,12 @@ SimpleDebugger::cmd_cd(std::vector& tokens) return; } + // Allow for trailing '/' + std::string selection = tokens[1]; + if ( !selection.empty() && selection.back() == '/' ) selection.pop_back(); + // Check for .. - if ( tokens[1] == ".." ) { + if ( selection == ".." ) { auto* parent = obj_->selectParent(); if ( parent == nullptr ) { printf("Already at top of object hierarchy\n"); @@ -153,21 +470,22 @@ SimpleDebugger::cmd_cd(std::vector& tokens) } bool loop_detected = false; - SST::Core::Serialization::ObjectMap* new_obj = obj_->selectVariable(tokens[1], loop_detected); - - if ( !new_obj ) { - printf("Unknown object in cd command: %s\n", tokens[1].c_str()); + SST::Core::Serialization::ObjectMap* new_obj = obj_->selectVariable(selection, loop_detected); + assert(new_obj); + if ( !new_obj || (new_obj == obj_) ) { + printf("Unknown object in cd command: %s\n", selection.c_str()); return; } if ( new_obj->isFundamental() ) { - printf("Object %s is a fundamental type so you cannot cd into it\n", tokens[1].c_str()); + printf("Object %s is a fundamental type so you cannot cd into it\n", selection.c_str()); new_obj->selectParent(); return; } if ( loop_detected ) { - printf("Loop detected in cd. New working directory will be set to level of looped object: %s\n", + printf("Loop detected in cd. New working directory will be set to level " + "of looped object: %s\n", new_obj->getFullName().c_str()); } obj_ = new_obj; @@ -181,6 +499,7 @@ SimpleDebugger::cmd_cd(std::vector& tokens) } } +// print [-rN] []: print object void SimpleDebugger::cmd_print(std::vector& tokens) { @@ -237,31 +556,43 @@ SimpleDebugger::cmd_print(std::vector& tokens) } } - +// set : set object to value void SimpleDebugger::cmd_set(std::vector& tokens) { - if ( tokens.size() != 3 ) { + if ( tokens.size() < 3 ) { printf("Invalid format for set command (set )\n"); return; } + // kg It may be safer to check for exactly 3 params but because + // of strings we allow for more (until parser handled quoted strings) + // If var->selectParent() is not used prior to returning we + // can get a segmentation fault on a subsequent command. Address + // Sanitizer indicated use of previously freed memory. + // if ( obj_->isContainer() ) { bool found = false; bool read_only = false; obj_->set(tokens[1], tokens[2], found, read_only); - if ( !found ) printf("Unknown object in set command: %s\n", tokens[1].c_str()); - if ( read_only ) printf("Object specified in set command is read-only: %s\n", tokens[1].c_str()); + if ( !found ) printf("Unknown object in set command for container: %s\n", tokens[1].c_str()); + if ( read_only ) printf("Object specified in set command is read-only for container: %s\n", tokens[1].c_str()); + // TODO do we need var->selectParent() here? return; } bool loop_detected = false; auto* var = obj_->selectVariable(tokens[1], loop_detected); - if ( !var ) { + assert(var); + if ( !var || (var == obj_) ) { printf("Unknown object in set command: %s\n", tokens[1].c_str()); + // TODO make sure selectVariable hasn't altered any state. return; } + // Once we have a valid object, be sure to use var->selectParent() or + // future commands may attempt to use free'd memory. + if ( var->isReadOnly() ) { printf("Object specified in set command is read-only: %s\n", tokens[1].c_str()); var->selectParent(); @@ -273,9 +604,18 @@ SimpleDebugger::cmd_set(std::vector& tokens) var->selectParent(); return; } + std::string value = tokens[2]; + if ( var->getType() == "std::string" ) { + for ( size_t index = 3; index < tokens.size(); index++ ) { + value = value + " " + tokens[index]; + } + } + else { + value = tokens[2]; + } try { - var->set(tokens[2]); + var->set(value); } catch ( std::exception& e ) { printf("Invalid format: %s\n", tokens[2].c_str()); @@ -283,19 +623,21 @@ SimpleDebugger::cmd_set(std::vector& tokens) var->selectParent(); } +// time: print current simulation cycle void SimpleDebugger::cmd_time(std::vector& UNUSED(tokens)) { printf("current time = %" PRI_SIMTIME "\n", getCurrentSimCycle()); } +// run