From: Gunnar Beutner Date: Thu, 5 Nov 2015 13:29:45 +0000 (+0100) Subject: Implement a debugger for Icinga scripts X-Git-Tag: v2.4.0~86 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=762187027873bf89328dc712ccccdedf4bdbf6b1;p=icinga2 Implement a debugger for Icinga scripts fixes #10547 --- diff --git a/icinga-app/icinga.cpp b/icinga-app/icinga.cpp index f09df27a1..2b154d1ce 100644 --- a/icinga-app/icinga.cpp +++ b/icinga-app/icinga.cpp @@ -193,9 +193,9 @@ int Main(void) ("app,a", po::value(), "application library name (default: icinga)") ("library,l", po::value >(), "load a library") ("include,I", po::value >(), "add include search directory") - ("log-level,x", po::value() - , "specify the log level for the console log.\n" - "The valid value is either debug, notice, information (default), warning, or critical"); + ("log-level,x", po::value(), "specify the log level for the console log.\n" + "The valid value is either debug, notice, information (default), warning, or critical") + ("script-debugger,X", "whether to enable the script debugger"); po::options_description hiddenDesc("Hidden options"); @@ -264,6 +264,9 @@ int Main(void) } } + if (vm.count("script-debugger")) + Application::SetScriptDebuggerEnabled(true); + Application::DeclareStatePath(Application::GetLocalStateDir() + "/lib/icinga2/icinga2.state"); Application::DeclareModAttrPath(Application::GetLocalStateDir() + "/lib/icinga2/modified-attributes.conf"); Application::DeclareObjectsPath(Application::GetLocalStateDir() + "/cache/icinga2/icinga2.debug"); diff --git a/lib/base/application.cpp b/lib/base/application.cpp index 5cc81d3c0..3866836b4 100644 --- a/lib/base/application.cpp +++ b/lib/base/application.cpp @@ -63,6 +63,7 @@ static bool l_InExceptionHandler = false; int Application::m_ArgC; char **Application::m_ArgV; double Application::m_StartTime; +bool Application::m_ScriptDebuggerEnabled = false; /** * Constructor for the Application class. @@ -1450,6 +1451,16 @@ void Application::SetStartTime(double ts) m_StartTime = ts; } +bool Application::GetScriptDebuggerEnabled(void) +{ + return m_ScriptDebuggerEnabled; +} + +void Application::SetScriptDebuggerEnabled(bool enabled) +{ + m_ScriptDebuggerEnabled = enabled; +} + void Application::ValidateName(const String& value, const ValidationUtils& utils) { ObjectImpl::ValidateName(value, utils); diff --git a/lib/base/application.hpp b/lib/base/application.hpp index 8f908dfac..f6a53ff40 100644 --- a/lib/base/application.hpp +++ b/lib/base/application.hpp @@ -134,6 +134,9 @@ public: static double GetStartTime(void); static void SetStartTime(double ts); + static bool GetScriptDebuggerEnabled(void); + static void SetScriptDebuggerEnabled(bool enabled); + static void DisplayInfoMessage(std::ostream& os, bool skipVersion = false); protected: @@ -164,6 +167,7 @@ private: static bool m_Debugging; /**< Whether debugging is enabled. */ static LogSeverity m_DebuggingSeverity; /**< Whether debugging severity is set. */ static double m_StartTime; + static bool m_ScriptDebuggerEnabled; #ifndef _WIN32 static void SigIntTermHandler(int signum); diff --git a/lib/base/exception.cpp b/lib/base/exception.cpp index 42ddcd3db..4e0693873 100644 --- a/lib/base/exception.cpp +++ b/lib/base/exception.cpp @@ -254,7 +254,7 @@ ScriptError::ScriptError(const String& message) { } ScriptError::ScriptError(const String& message, const DebugInfo& di, bool incompleteExpr) - : m_Message(message), m_DebugInfo(di), m_IncompleteExpr(incompleteExpr) + : m_Message(message), m_DebugInfo(di), m_IncompleteExpr(incompleteExpr), m_HandledByDebugger(false) { } ScriptError::~ScriptError(void) throw() @@ -275,6 +275,16 @@ bool ScriptError::IsIncompleteExpression(void) const return m_IncompleteExpr;; } +bool ScriptError::IsHandledByDebugger(void) const +{ + return m_HandledByDebugger; +} + +void ScriptError::SetHandledByDebugger(bool handled) +{ + m_HandledByDebugger = handled; +} + posix_error::posix_error(void) : m_Message(NULL) { } diff --git a/lib/base/exception.hpp b/lib/base/exception.hpp index 90e219239..4f6713df1 100644 --- a/lib/base/exception.hpp +++ b/lib/base/exception.hpp @@ -60,10 +60,14 @@ public: DebugInfo GetDebugInfo(void) const; bool IsIncompleteExpression(void) const; + bool IsHandledByDebugger(void) const; + void SetHandledByDebugger(bool handled); + private: String m_Message; DebugInfo m_DebugInfo; bool m_IncompleteExpr; + bool m_HandledByDebugger; }; /* diff --git a/lib/cli/consolecommand.cpp b/lib/cli/consolecommand.cpp index 32162b35d..435a58cba 100644 --- a/lib/cli/consolecommand.cpp +++ b/lib/cli/consolecommand.cpp @@ -46,6 +46,36 @@ static String l_Session; REGISTER_CLICOMMAND("console", ConsoleCommand); +INITIALIZE_ONCE(&ConsoleCommand::StaticInitialize); + +void ConsoleCommand::BreakpointHandler(ScriptFrame& frame, ScriptError *ex, const DebugInfo& di) +{ + static boost::mutex mutex; + boost::mutex::scoped_lock lock(mutex); + + if (!Application::GetScriptDebuggerEnabled()) + return; + + if (ex && ex->IsHandledByDebugger()) + return; + + std::cout << "Breakpoint encountered in '" << di.Path << "' at " << di << "\n"; + + if (ex) { + std::cout << "Exception: " << DiagnosticInformation(*ex); + ex->SetHandledByDebugger(true); + } + + std::cout << "You can leave the debugger and continue the program with \"$quit\".\n"; + + ConsoleCommand::RunScriptConsole(frame); +} + +void ConsoleCommand::StaticInitialize(void) +{ + Expression::OnBreakpoint.connect(&ConsoleCommand::BreakpointHandler); +} + String ConsoleCommand::GetDescription(void) const { return "Interprets Icinga script expressions."; @@ -118,19 +148,15 @@ char *ConsoleCommand::ConsoleCompleteHelper(const char *word, int state) */ int ConsoleCommand::Run(const po::variables_map& vm, const std::vector& ap) const { - std::map lines; - int next_line = 1; - #ifdef HAVE_EDITLINE rl_completion_entry_function = ConsoleCommand::ConsoleCompleteHelper; rl_completion_append_character = '\0'; #endif /* HAVE_EDITLINE */ - String addr; + String addr, session; ScriptFrame scriptFrame; - l_ScriptFrame = &scriptFrame; - l_Session = Utility::NewUniqueID(); + session = Utility::NewUniqueID(); if (vm.count("sandbox")) scriptFrame.Sandboxed = true; @@ -144,9 +170,24 @@ int ConsoleCommand::Run(const po::variables_map& vm, const std::vector(); - } + + String command; + + if (vm.count("eval")) + command = vm["eval"].as(); + + return RunScriptConsole(scriptFrame, addr, session, command);; +} + +int ConsoleCommand::RunScriptConsole(ScriptFrame& scriptFrame, const String& addr, const String& session, const String& commandOnce) +{ + std::map lines; + int next_line = 1; + + l_ScriptFrame = &scriptFrame; + l_Session = session; if (!addr.IsEmpty()) { Url::Ptr url; @@ -182,7 +223,7 @@ int ConsoleCommand::Run(const po::variables_map& vm, const std::vector(); + line = commandOnce; + + if (!line.empty() && line[0] == '$') { + if (line == "$quit") + break; + + std::cout << "Unknown debugger command: " << line; + } if (!command.empty()) command += "\n"; @@ -254,7 +302,7 @@ incomplete: boost::rethrow_exception(eptr); } - if (!vm.count("eval")) { + if (commandOnce.IsEmpty()) { std::cout << ConsoleColorTag(Console_ForegroundCyan); ConfigWriter::EmitValue(std::cout, 1, result); std::cout << ConsoleColorTag(Console_Normal) << "\n"; @@ -308,12 +356,12 @@ incomplete: std::cout << ex.what() << "\n"; - if (vm.count("eval")) + if (!commandOnce.IsEmpty()) return EXIT_FAILURE; } catch (const std::exception& ex) { std::cout << "Error: " << DiagnosticInformation(ex) << "\n"; - if (vm.count("eval")) + if (!commandOnce.IsEmpty()) return EXIT_FAILURE; } diff --git a/lib/cli/consolecommand.hpp b/lib/cli/consolecommand.hpp index f144e9a04..3928f2bfd 100644 --- a/lib/cli/consolecommand.hpp +++ b/lib/cli/consolecommand.hpp @@ -22,6 +22,7 @@ #include "cli/clicommand.hpp" #include "base/exception.hpp" +#include "base/scriptframe.hpp" namespace icinga { @@ -36,6 +37,8 @@ class ConsoleCommand : public CLICommand public: DECLARE_PTR_TYPEDEFS(ConsoleCommand); + static void StaticInitialize(void); + virtual String GetDescription(void) const override; virtual String GetShortDescription(void) const override; virtual ImpersonationLevel GetImpersonationLevel(void) const override; @@ -43,6 +46,9 @@ public: boost::program_options::options_description& hiddenDesc) const override; virtual int Run(const boost::program_options::variables_map& vm, const std::vector& ap) const override; + static int RunScriptConsole(ScriptFrame& scriptFrame, const String& addr = String(), + const String& session = String(), const String& commandOnce = String()); + private: mutable boost::mutex m_Mutex; mutable boost::condition_variable m_CV; @@ -58,6 +64,8 @@ private: static char *ConsoleCompleteHelper(const char *word, int state); #endif /* HAVE_EDITLINE */ + static void BreakpointHandler(ScriptFrame& frame, ScriptError *ex, const DebugInfo& di); + }; } diff --git a/lib/config/config_lexer.ll b/lib/config/config_lexer.ll index 7343a7635..98078c592 100644 --- a/lib/config/config_lexer.ll +++ b/lib/config/config_lexer.ll @@ -198,6 +198,7 @@ throw return T_THROW; ignore_on_error return T_IGNORE_ON_ERROR; current_filename return T_CURRENT_FILENAME; current_line return T_CURRENT_LINE; +debugger return T_DEBUGGER; =\> return T_FOLLOWS; \<\< return T_SHIFT_LEFT; \>\> return T_SHIFT_RIGHT; diff --git a/lib/config/config_parser.yy b/lib/config/config_parser.yy index 271a22830..1b5e5ed09 100644 --- a/lib/config/config_parser.yy +++ b/lib/config/config_parser.yy @@ -146,6 +146,7 @@ static void MakeRBinaryOp(Expression** result, Expression *left, Expression *rig %token T_IGNORE_ON_ERROR "ignore_on_error (T_IGNORE_ON_ERROR)" %token T_CURRENT_FILENAME "current_filename (T_CURRENT_FILENAME)" %token T_CURRENT_LINE "current_line (T_CURRENT_LINE)" +%token T_DEBUGGER "debugger (T_DEBUGGER)" %token T_USE "use (T_USE)" %token T_OBJECT "object (T_OBJECT)" %token T_TEMPLATE "template (T_TEMPLATE)" @@ -514,6 +515,10 @@ lterm: T_LIBRARY rterm { $$ = new ContinueExpression(@$); } + | T_DEBUGGER + { + $$ = new BreakpointExpression(@$); + } | apply | object | T_FOR '(' identifier T_FOLLOWS identifier T_IN rterm ')' rterm_scope_require_side_effect diff --git a/lib/config/expression.cpp b/lib/config/expression.cpp index a13c76064..0aa9a484f 100644 --- a/lib/config/expression.cpp +++ b/lib/config/expression.cpp @@ -34,9 +34,23 @@ using namespace icinga; +boost::signals2::signal Expression::OnBreakpoint; +boost::thread_specific_ptr l_InBreakpointHandler; + Expression::~Expression(void) { } +void Expression::ScriptBreakpoint(ScriptFrame& frame, ScriptError *ex, const DebugInfo& di) +{ + bool *inHandler = l_InBreakpointHandler.get(); + if (!inHandler || !*inHandler) { + inHandler = new bool(true); + l_InBreakpointHandler.reset(inHandler); + OnBreakpoint(frame, ex, di); + *inHandler = false; + } +} + ExpressionResult Expression::Evaluate(ScriptFrame& frame, DebugHint *dhint) const { try { @@ -48,7 +62,8 @@ ExpressionResult Expression::Evaluate(ScriptFrame& frame, DebugHint *dhint) cons #endif /* I2_DEBUG */ return DoEvaluate(frame, dhint); - } catch (const ScriptError& ex) { + } catch (ScriptError& ex) { + ScriptBreakpoint(frame, &ex, GetDebugInfo()); throw; } catch (const std::exception& ex) { BOOST_THROW_EXCEPTION(ScriptError("Error while evaluating expression: " + String(ex.what()), GetDebugInfo()) @@ -859,3 +874,11 @@ ExpressionResult IncludeExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dh return res; } + +ExpressionResult BreakpointExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const +{ + ScriptBreakpoint(frame, NULL, GetDebugInfo()); + + return Empty; +} + diff --git a/lib/config/expression.hpp b/lib/config/expression.hpp index 506c67c62..ea77fd752 100644 --- a/lib/config/expression.hpp +++ b/lib/config/expression.hpp @@ -198,6 +198,10 @@ public: virtual const DebugInfo& GetDebugInfo(void) const; virtual ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const = 0; + + static boost::signals2::signal OnBreakpoint; + + static void ScriptBreakpoint(ScriptFrame& frame, ScriptError *ex, const DebugInfo& di); }; I2_CONFIG_API Expression *MakeIndexer(ScopeSpecifier scopeSpec, const String& index); @@ -937,6 +941,17 @@ private: String m_Package; }; +class I2_CONFIG_API BreakpointExpression : public DebuggableExpression +{ +public: + BreakpointExpression(const DebugInfo& debugInfo = DebugInfo()) + : DebuggableExpression(debugInfo) + { } + +protected: + virtual ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const override; +}; + } #endif /* EXPRESSION_H */