Implement a debugger for Icinga scripts

fixes #10547
This commit is contained in:
Gunnar Beutner 2015-11-05 14:29:45 +01:00
parent 75eed9f30e
commit 7621870278
11 changed files with 150 additions and 18 deletions

View File

@ -193,9 +193,9 @@ int Main(void)
("app,a", po::value<std::string>(), "application library name (default: icinga)") ("app,a", po::value<std::string>(), "application library name (default: icinga)")
("library,l", po::value<std::vector<std::string> >(), "load a library") ("library,l", po::value<std::vector<std::string> >(), "load a library")
("include,I", po::value<std::vector<std::string> >(), "add include search directory") ("include,I", po::value<std::vector<std::string> >(), "add include search directory")
("log-level,x", po::value<std::string>() ("log-level,x", po::value<std::string>(), "specify the log level for the console log.\n"
, "specify the log level for the console log.\n" "The valid value is either debug, notice, information (default), warning, or critical")
"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"); 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::DeclareStatePath(Application::GetLocalStateDir() + "/lib/icinga2/icinga2.state");
Application::DeclareModAttrPath(Application::GetLocalStateDir() + "/lib/icinga2/modified-attributes.conf"); Application::DeclareModAttrPath(Application::GetLocalStateDir() + "/lib/icinga2/modified-attributes.conf");
Application::DeclareObjectsPath(Application::GetLocalStateDir() + "/cache/icinga2/icinga2.debug"); Application::DeclareObjectsPath(Application::GetLocalStateDir() + "/cache/icinga2/icinga2.debug");

View File

@ -63,6 +63,7 @@ static bool l_InExceptionHandler = false;
int Application::m_ArgC; int Application::m_ArgC;
char **Application::m_ArgV; char **Application::m_ArgV;
double Application::m_StartTime; double Application::m_StartTime;
bool Application::m_ScriptDebuggerEnabled = false;
/** /**
* Constructor for the Application class. * Constructor for the Application class.
@ -1450,6 +1451,16 @@ void Application::SetStartTime(double ts)
m_StartTime = 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) void Application::ValidateName(const String& value, const ValidationUtils& utils)
{ {
ObjectImpl<Application>::ValidateName(value, utils); ObjectImpl<Application>::ValidateName(value, utils);

View File

@ -134,6 +134,9 @@ public:
static double GetStartTime(void); static double GetStartTime(void);
static void SetStartTime(double ts); static void SetStartTime(double ts);
static bool GetScriptDebuggerEnabled(void);
static void SetScriptDebuggerEnabled(bool enabled);
static void DisplayInfoMessage(std::ostream& os, bool skipVersion = false); static void DisplayInfoMessage(std::ostream& os, bool skipVersion = false);
protected: protected:
@ -164,6 +167,7 @@ private:
static bool m_Debugging; /**< Whether debugging is enabled. */ static bool m_Debugging; /**< Whether debugging is enabled. */
static LogSeverity m_DebuggingSeverity; /**< Whether debugging severity is set. */ static LogSeverity m_DebuggingSeverity; /**< Whether debugging severity is set. */
static double m_StartTime; static double m_StartTime;
static bool m_ScriptDebuggerEnabled;
#ifndef _WIN32 #ifndef _WIN32
static void SigIntTermHandler(int signum); static void SigIntTermHandler(int signum);

View File

@ -254,7 +254,7 @@ ScriptError::ScriptError(const String& message)
{ } { }
ScriptError::ScriptError(const String& message, const DebugInfo& di, bool incompleteExpr) 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() ScriptError::~ScriptError(void) throw()
@ -275,6 +275,16 @@ bool ScriptError::IsIncompleteExpression(void) const
return m_IncompleteExpr;; return m_IncompleteExpr;;
} }
bool ScriptError::IsHandledByDebugger(void) const
{
return m_HandledByDebugger;
}
void ScriptError::SetHandledByDebugger(bool handled)
{
m_HandledByDebugger = handled;
}
posix_error::posix_error(void) posix_error::posix_error(void)
: m_Message(NULL) : m_Message(NULL)
{ } { }

View File

@ -60,10 +60,14 @@ public:
DebugInfo GetDebugInfo(void) const; DebugInfo GetDebugInfo(void) const;
bool IsIncompleteExpression(void) const; bool IsIncompleteExpression(void) const;
bool IsHandledByDebugger(void) const;
void SetHandledByDebugger(bool handled);
private: private:
String m_Message; String m_Message;
DebugInfo m_DebugInfo; DebugInfo m_DebugInfo;
bool m_IncompleteExpr; bool m_IncompleteExpr;
bool m_HandledByDebugger;
}; };
/* /*

View File

@ -46,6 +46,36 @@ static String l_Session;
REGISTER_CLICOMMAND("console", ConsoleCommand); 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 String ConsoleCommand::GetDescription(void) const
{ {
return "Interprets Icinga script expressions."; 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<std::string>& ap) const int ConsoleCommand::Run(const po::variables_map& vm, const std::vector<std::string>& ap) const
{ {
std::map<String, String> lines;
int next_line = 1;
#ifdef HAVE_EDITLINE #ifdef HAVE_EDITLINE
rl_completion_entry_function = ConsoleCommand::ConsoleCompleteHelper; rl_completion_entry_function = ConsoleCommand::ConsoleCompleteHelper;
rl_completion_append_character = '\0'; rl_completion_append_character = '\0';
#endif /* HAVE_EDITLINE */ #endif /* HAVE_EDITLINE */
String addr; String addr, session;
ScriptFrame scriptFrame; ScriptFrame scriptFrame;
l_ScriptFrame = &scriptFrame; session = Utility::NewUniqueID();
l_Session = Utility::NewUniqueID();
if (vm.count("sandbox")) if (vm.count("sandbox"))
scriptFrame.Sandboxed = true; scriptFrame.Sandboxed = true;
@ -144,9 +170,24 @@ int ConsoleCommand::Run(const po::variables_map& vm, const std::vector<std::stri
if (addrEnv) if (addrEnv)
addr = addrEnv; addr = addrEnv;
if (vm.count("connect")) { if (vm.count("connect"))
addr = vm["connect"].as<std::string>(); addr = vm["connect"].as<std::string>();
}
String command;
if (vm.count("eval"))
command = vm["eval"].as<std::string>();
return RunScriptConsole(scriptFrame, addr, session, command);;
}
int ConsoleCommand::RunScriptConsole(ScriptFrame& scriptFrame, const String& addr, const String& session, const String& commandOnce)
{
std::map<String, String> lines;
int next_line = 1;
l_ScriptFrame = &scriptFrame;
l_Session = session;
if (!addr.IsEmpty()) { if (!addr.IsEmpty()) {
Url::Ptr url; Url::Ptr url;
@ -182,7 +223,7 @@ int ConsoleCommand::Run(const po::variables_map& vm, const std::vector<std::stri
incomplete: incomplete:
std::string line; std::string line;
if (!vm.count("eval")) { if (commandOnce.IsEmpty()) {
#ifdef HAVE_EDITLINE #ifdef HAVE_EDITLINE
std::ostringstream promptbuf; std::ostringstream promptbuf;
std::ostream& os = promptbuf; std::ostream& os = promptbuf;
@ -215,7 +256,14 @@ incomplete:
std::getline(std::cin, line); std::getline(std::cin, line);
#endif /* HAVE_EDITLINE */ #endif /* HAVE_EDITLINE */
} else } else
line = vm["eval"].as<std::string>(); line = commandOnce;
if (!line.empty() && line[0] == '$') {
if (line == "$quit")
break;
std::cout << "Unknown debugger command: " << line;
}
if (!command.empty()) if (!command.empty())
command += "\n"; command += "\n";
@ -254,7 +302,7 @@ incomplete:
boost::rethrow_exception(eptr); boost::rethrow_exception(eptr);
} }
if (!vm.count("eval")) { if (commandOnce.IsEmpty()) {
std::cout << ConsoleColorTag(Console_ForegroundCyan); std::cout << ConsoleColorTag(Console_ForegroundCyan);
ConfigWriter::EmitValue(std::cout, 1, result); ConfigWriter::EmitValue(std::cout, 1, result);
std::cout << ConsoleColorTag(Console_Normal) << "\n"; std::cout << ConsoleColorTag(Console_Normal) << "\n";
@ -308,12 +356,12 @@ incomplete:
std::cout << ex.what() << "\n"; std::cout << ex.what() << "\n";
if (vm.count("eval")) if (!commandOnce.IsEmpty())
return EXIT_FAILURE; return EXIT_FAILURE;
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
std::cout << "Error: " << DiagnosticInformation(ex) << "\n"; std::cout << "Error: " << DiagnosticInformation(ex) << "\n";
if (vm.count("eval")) if (!commandOnce.IsEmpty())
return EXIT_FAILURE; return EXIT_FAILURE;
} }

View File

@ -22,6 +22,7 @@
#include "cli/clicommand.hpp" #include "cli/clicommand.hpp"
#include "base/exception.hpp" #include "base/exception.hpp"
#include "base/scriptframe.hpp"
namespace icinga namespace icinga
{ {
@ -36,6 +37,8 @@ class ConsoleCommand : public CLICommand
public: public:
DECLARE_PTR_TYPEDEFS(ConsoleCommand); DECLARE_PTR_TYPEDEFS(ConsoleCommand);
static void StaticInitialize(void);
virtual String GetDescription(void) const override; virtual String GetDescription(void) const override;
virtual String GetShortDescription(void) const override; virtual String GetShortDescription(void) const override;
virtual ImpersonationLevel GetImpersonationLevel(void) const override; virtual ImpersonationLevel GetImpersonationLevel(void) const override;
@ -43,6 +46,9 @@ public:
boost::program_options::options_description& hiddenDesc) const override; boost::program_options::options_description& hiddenDesc) const override;
virtual int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override; virtual int Run(const boost::program_options::variables_map& vm, const std::vector<std::string>& ap) const override;
static int RunScriptConsole(ScriptFrame& scriptFrame, const String& addr = String(),
const String& session = String(), const String& commandOnce = String());
private: private:
mutable boost::mutex m_Mutex; mutable boost::mutex m_Mutex;
mutable boost::condition_variable m_CV; mutable boost::condition_variable m_CV;
@ -58,6 +64,8 @@ private:
static char *ConsoleCompleteHelper(const char *word, int state); static char *ConsoleCompleteHelper(const char *word, int state);
#endif /* HAVE_EDITLINE */ #endif /* HAVE_EDITLINE */
static void BreakpointHandler(ScriptFrame& frame, ScriptError *ex, const DebugInfo& di);
}; };
} }

View File

@ -198,6 +198,7 @@ throw return T_THROW;
ignore_on_error return T_IGNORE_ON_ERROR; ignore_on_error return T_IGNORE_ON_ERROR;
current_filename return T_CURRENT_FILENAME; current_filename return T_CURRENT_FILENAME;
current_line return T_CURRENT_LINE; current_line return T_CURRENT_LINE;
debugger return T_DEBUGGER;
=\> return T_FOLLOWS; =\> return T_FOLLOWS;
\<\< return T_SHIFT_LEFT; \<\< return T_SHIFT_LEFT;
\>\> return T_SHIFT_RIGHT; \>\> return T_SHIFT_RIGHT;

View File

@ -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_IGNORE_ON_ERROR "ignore_on_error (T_IGNORE_ON_ERROR)"
%token T_CURRENT_FILENAME "current_filename (T_CURRENT_FILENAME)" %token T_CURRENT_FILENAME "current_filename (T_CURRENT_FILENAME)"
%token T_CURRENT_LINE "current_line (T_CURRENT_LINE)" %token T_CURRENT_LINE "current_line (T_CURRENT_LINE)"
%token T_DEBUGGER "debugger (T_DEBUGGER)"
%token T_USE "use (T_USE)" %token T_USE "use (T_USE)"
%token T_OBJECT "object (T_OBJECT)" %token T_OBJECT "object (T_OBJECT)"
%token T_TEMPLATE "template (T_TEMPLATE)" %token T_TEMPLATE "template (T_TEMPLATE)"
@ -514,6 +515,10 @@ lterm: T_LIBRARY rterm
{ {
$$ = new ContinueExpression(@$); $$ = new ContinueExpression(@$);
} }
| T_DEBUGGER
{
$$ = new BreakpointExpression(@$);
}
| apply | apply
| object | object
| T_FOR '(' identifier T_FOLLOWS identifier T_IN rterm ')' rterm_scope_require_side_effect | T_FOR '(' identifier T_FOLLOWS identifier T_IN rterm ')' rterm_scope_require_side_effect

View File

@ -34,9 +34,23 @@
using namespace icinga; using namespace icinga;
boost::signals2::signal<void (ScriptFrame&, ScriptError *ex, const DebugInfo&)> Expression::OnBreakpoint;
boost::thread_specific_ptr<bool> l_InBreakpointHandler;
Expression::~Expression(void) 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 ExpressionResult Expression::Evaluate(ScriptFrame& frame, DebugHint *dhint) const
{ {
try { try {
@ -48,7 +62,8 @@ ExpressionResult Expression::Evaluate(ScriptFrame& frame, DebugHint *dhint) cons
#endif /* I2_DEBUG */ #endif /* I2_DEBUG */
return DoEvaluate(frame, dhint); return DoEvaluate(frame, dhint);
} catch (const ScriptError& ex) { } catch (ScriptError& ex) {
ScriptBreakpoint(frame, &ex, GetDebugInfo());
throw; throw;
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
BOOST_THROW_EXCEPTION(ScriptError("Error while evaluating expression: " + String(ex.what()), GetDebugInfo()) 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; return res;
} }
ExpressionResult BreakpointExpression::DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const
{
ScriptBreakpoint(frame, NULL, GetDebugInfo());
return Empty;
}

View File

@ -198,6 +198,10 @@ public:
virtual const DebugInfo& GetDebugInfo(void) const; virtual const DebugInfo& GetDebugInfo(void) const;
virtual ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const = 0; virtual ExpressionResult DoEvaluate(ScriptFrame& frame, DebugHint *dhint) const = 0;
static boost::signals2::signal<void (ScriptFrame& frame, ScriptError *ex, const DebugInfo& di)> OnBreakpoint;
static void ScriptBreakpoint(ScriptFrame& frame, ScriptError *ex, const DebugInfo& di);
}; };
I2_CONFIG_API Expression *MakeIndexer(ScopeSpecifier scopeSpec, const String& index); I2_CONFIG_API Expression *MakeIndexer(ScopeSpecifier scopeSpec, const String& index);
@ -937,6 +941,17 @@ private:
String m_Package; 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 */ #endif /* EXPRESSION_H */