]> granicus.if.org Git - icinga2/blob - lib/cli/consolecommand.cpp
add some object locking to the Dump method (which could theoreticylly suffer from...
[icinga2] / lib / cli / consolecommand.cpp
1 /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
2
3 #include "cli/consolecommand.hpp"
4 #include "config/configcompiler.hpp"
5 #include "remote/apiclient.hpp"
6 #include "remote/consolehandler.hpp"
7 #include "remote/url.hpp"
8 #include "base/configwriter.hpp"
9 #include "base/serializer.hpp"
10 #include "base/json.hpp"
11 #include "base/console.hpp"
12 #include "base/application.hpp"
13 #include "base/objectlock.hpp"
14 #include "base/unixsocket.hpp"
15 #include "base/utility.hpp"
16 #include "base/networkstream.hpp"
17 #include "base/exception.hpp"
18 #include <iostream>
19 #include <fstream>
20 #ifdef HAVE_EDITLINE
21 #include "cli/editline.hpp"
22 #endif /* HAVE_EDITLINE */
23
24 using namespace icinga;
25 namespace po = boost::program_options;
26
27 static ScriptFrame *l_ScriptFrame;
28 static ApiClient::Ptr l_ApiClient;
29 static String l_Session;
30
31 REGISTER_CLICOMMAND("console", ConsoleCommand);
32
33 INITIALIZE_ONCE(&ConsoleCommand::StaticInitialize);
34
35 extern "C" void dbg_spawn_console()
36 {
37         ScriptFrame frame(true);
38         ConsoleCommand::RunScriptConsole(frame);
39 }
40
41 extern "C" void dbg_inspect_value(const Value& value)
42 {
43         ConfigWriter::EmitValue(std::cout, 1, Serialize(value, 0));
44         std::cout << std::endl;
45 }
46
47 extern "C" void dbg_inspect_object(Object *obj)
48 {
49         Object::Ptr objr = obj;
50         dbg_inspect_value(objr);
51 }
52
53 extern "C" void dbg_eval(const char *text)
54 {
55         std::unique_ptr<Expression> expr;
56
57         try {
58                 ScriptFrame frame(true);
59                 expr = ConfigCompiler::CompileText("<dbg>", text);
60                 Value result = Serialize(expr->Evaluate(frame), 0);
61                 dbg_inspect_value(result);
62         } catch (const std::exception& ex) {
63                 std::cout << "Error: " << DiagnosticInformation(ex) << "\n";
64         }
65 }
66
67 extern "C" void dbg_eval_with_value(const Value& value, const char *text)
68 {
69         std::unique_ptr<Expression> expr;
70
71         try {
72                 ScriptFrame frame(true);
73                 frame.Locals = new Dictionary({
74                         { "arg", value }
75                 });
76                 expr = ConfigCompiler::CompileText("<dbg>", text);
77                 Value result = Serialize(expr->Evaluate(frame), 0);
78                 dbg_inspect_value(result);
79         } catch (const std::exception& ex) {
80                 std::cout << "Error: " << DiagnosticInformation(ex) << "\n";
81         }
82 }
83
84 extern "C" void dbg_eval_with_object(Object *object, const char *text)
85 {
86         std::unique_ptr<Expression> expr;
87
88         try {
89                 ScriptFrame frame(true);
90                 frame.Locals = new Dictionary({
91                         { "arg", object }
92                 });
93                 expr = ConfigCompiler::CompileText("<dbg>", text);
94                 Value result = Serialize(expr->Evaluate(frame), 0);
95                 dbg_inspect_value(result);
96         } catch (const std::exception& ex) {
97                 std::cout << "Error: " << DiagnosticInformation(ex) << "\n";
98         }
99 }
100
101 void ConsoleCommand::BreakpointHandler(ScriptFrame& frame, ScriptError *ex, const DebugInfo& di)
102 {
103         static boost::mutex mutex;
104         boost::mutex::scoped_lock lock(mutex);
105
106         if (!Application::GetScriptDebuggerEnabled())
107                 return;
108
109         if (ex && ex->IsHandledByDebugger())
110                 return;
111
112         std::cout << "Breakpoint encountered.\n";
113
114         if (ex) {
115                 std::cout << "Exception: " << DiagnosticInformation(*ex) << "\n";
116                 ex->SetHandledByDebugger(true);
117         } else
118                 ShowCodeLocation(std::cout, di);
119
120         std::cout << "You can inspect expressions (such as variables) by entering them at the prompt.\n"
121                 << "To leave the debugger and continue the program use \"$continue\".\n"
122                 << "For further commands see \"$help\".\n";
123
124 #ifdef HAVE_EDITLINE
125         rl_completion_entry_function = ConsoleCommand::ConsoleCompleteHelper;
126         rl_completion_append_character = '\0';
127 #endif /* HAVE_EDITLINE */
128
129         ConsoleCommand::RunScriptConsole(frame);
130 }
131
132 void ConsoleCommand::StaticInitialize()
133 {
134         Expression::OnBreakpoint.connect(&ConsoleCommand::BreakpointHandler);
135 }
136
137 String ConsoleCommand::GetDescription() const
138 {
139         return "Interprets Icinga script expressions.";
140 }
141
142 String ConsoleCommand::GetShortDescription() const
143 {
144         return "Icinga console";
145 }
146
147 ImpersonationLevel ConsoleCommand::GetImpersonationLevel() const
148 {
149         return ImpersonateNone;
150 }
151
152 void ConsoleCommand::InitParameters(boost::program_options::options_description& visibleDesc,
153         boost::program_options::options_description& hiddenDesc) const
154 {
155         visibleDesc.add_options()
156                 ("connect,c", po::value<std::string>(), "connect to an Icinga 2 instance")
157                 ("eval,e", po::value<std::string>(), "evaluate expression and terminate")
158                 ("file,r", po::value<std::string>(), "evaluate a file and terminate")
159                 ("syntax-only", "only validate syntax (requires --eval or --file)")
160                 ("sandbox", "enable sandbox mode")
161         ;
162 }
163
164 #ifdef HAVE_EDITLINE
165 char *ConsoleCommand::ConsoleCompleteHelper(const char *word, int state)
166 {
167         static std::vector<String> matches;
168
169         if (state == 0) {
170                 if (!l_ApiClient)
171                         matches = ConsoleHandler::GetAutocompletionSuggestions(word, *l_ScriptFrame);
172                 else {
173                         boost::mutex mutex;
174                         boost::condition_variable cv;
175                         bool ready = false;
176                         Array::Ptr suggestions;
177
178                         l_ApiClient->AutocompleteScript(l_Session, word, l_ScriptFrame->Sandboxed,
179                                 std::bind(&ConsoleCommand::AutocompleteScriptCompletionHandler,
180                                 std::ref(mutex), std::ref(cv), std::ref(ready),
181                                 _1, _2,
182                                 std::ref(suggestions)));
183
184                         {
185                                 boost::mutex::scoped_lock lock(mutex);
186                                 while (!ready)
187                                         cv.wait(lock);
188                         }
189
190                         matches.clear();
191
192                         ObjectLock olock(suggestions);
193                         std::copy(suggestions->Begin(), suggestions->End(), std::back_inserter(matches));
194                 }
195         }
196
197         if (state >= static_cast<int>(matches.size()))
198                 return nullptr;
199
200         return strdup(matches[state].CStr());
201 }
202 #endif /* HAVE_EDITLINE */
203
204 /**
205  * The entry point for the "console" CLI command.
206  *
207  * @returns An exit status.
208  */
209 int ConsoleCommand::Run(const po::variables_map& vm, const std::vector<std::string>& ap) const
210 {
211 #ifdef HAVE_EDITLINE
212         rl_completion_entry_function = ConsoleCommand::ConsoleCompleteHelper;
213         rl_completion_append_character = '\0';
214 #endif /* HAVE_EDITLINE */
215
216         String addr, session;
217         ScriptFrame scriptFrame(true);
218
219         session = Utility::NewUniqueID();
220
221         if (vm.count("sandbox"))
222                 scriptFrame.Sandboxed = true;
223
224         scriptFrame.Self = scriptFrame.Locals;
225
226         if (!vm.count("eval") && !vm.count("file"))
227                 std::cout << "Icinga 2 (version: " << Application::GetAppVersion() << ")\n"
228                         << "Type $help to view available commands.\n";
229
230
231         String addrEnv = Utility::GetFromEnvironment("ICINGA2_API_URL");
232         if (!addrEnv.IsEmpty())
233                 addr = addrEnv;
234
235         if (vm.count("connect"))
236                 addr = vm["connect"].as<std::string>();
237
238         String command;
239         bool syntaxOnly = false;
240
241         if (vm.count("syntax-only")) {
242                 if (vm.count("eval") || vm.count("file"))
243                         syntaxOnly = true;
244                 else {
245                         std::cerr << "The option --syntax-only can only be used in combination with --eval or --file." << std::endl;
246                         return EXIT_FAILURE;
247                 }
248         }
249
250         String commandFileName;
251
252         if (vm.count("eval"))
253                 command = vm["eval"].as<std::string>();
254         else if (vm.count("file")) {
255                 commandFileName = vm["file"].as<std::string>();
256
257                 try {
258                         std::ifstream fp(commandFileName.CStr());
259                         fp.exceptions(std::ifstream::failbit | std::ifstream::badbit);
260                         command = String(std::istreambuf_iterator<char>(fp), std::istreambuf_iterator<char>());
261                 } catch (const std::exception&) {
262                         std::cerr << "Could not read file '" << commandFileName << "'." << std::endl;
263                         return EXIT_FAILURE;
264                 }
265         }
266
267         return RunScriptConsole(scriptFrame, addr, session, command, commandFileName, syntaxOnly);
268 }
269
270 int ConsoleCommand::RunScriptConsole(ScriptFrame& scriptFrame, const String& addr, const String& session, const String& commandOnce, const String& commandOnceFileName, bool syntaxOnly)
271 {
272         std::map<String, String> lines;
273         int next_line = 1;
274
275 #ifdef HAVE_EDITLINE
276         String homeEnv = Utility::GetFromEnvironment("HOME");
277
278         String historyPath;
279         std::fstream historyfp;
280
281         if (!homeEnv.IsEmpty()) {
282                 historyPath = String(homeEnv) + "/.icinga2_history";
283
284                 historyfp.open(historyPath.CStr(), std::fstream::in);
285
286                 String line;
287                 while (std::getline(historyfp, line.GetData()))
288                         add_history(line.CStr());
289
290                 historyfp.close();
291         }
292 #endif /* HAVE_EDITLINE */
293
294         l_ScriptFrame = &scriptFrame;
295         l_Session = session;
296
297         if (!addr.IsEmpty()) {
298                 Url::Ptr url;
299
300                 try {
301                         url = new Url(addr);
302                 } catch (const std::exception& ex) {
303                         Log(LogCritical, "ConsoleCommand", ex.what());
304                         return EXIT_FAILURE;
305                 }
306
307                 String usernameEnv = Utility::GetFromEnvironment("ICINGA2_API_USERNAME");
308                 String passwordEnv = Utility::GetFromEnvironment("ICINGA2_API_PASSWORD");
309
310                 if (!usernameEnv.IsEmpty())
311                         url->SetUsername(usernameEnv);
312                 if (!passwordEnv.IsEmpty())
313                         url->SetPassword(passwordEnv);
314
315                 if (url->GetPort().IsEmpty())
316                         url->SetPort("5665");
317
318                 l_ApiClient = new ApiClient(url->GetHost(), url->GetPort(), url->GetUsername(), url->GetPassword());
319         }
320
321         while (std::cin.good()) {
322                 String fileName;
323
324                 if (commandOnceFileName.IsEmpty())
325                         fileName = "<" + Convert::ToString(next_line) + ">";
326                 else
327                         fileName = commandOnceFileName;
328
329                 next_line++;
330
331                 bool continuation = false;
332                 std::string command;
333
334 incomplete:
335                 std::string line;
336
337                 if (commandOnce.IsEmpty()) {
338 #ifdef HAVE_EDITLINE
339                         std::ostringstream promptbuf;
340                         std::ostream& os = promptbuf;
341 #else /* HAVE_EDITLINE */
342                         std::ostream& os = std::cout;
343 #endif /* HAVE_EDITLINE */
344
345                         os << fileName;
346
347                         if (!continuation)
348                                 os << " => ";
349                         else
350                                 os << " .. ";
351
352 #ifdef HAVE_EDITLINE
353                         String prompt = promptbuf.str();
354
355                         char *cline;
356                         cline = readline(prompt.CStr());
357
358                         if (!cline)
359                                 break;
360
361                         if (commandOnce.IsEmpty() && cline[0] != '\0') {
362                                 add_history(cline);
363
364                                 if (!historyPath.IsEmpty()) {
365                                         historyfp.open(historyPath.CStr(), std::fstream::out | std::fstream::app);
366                                         historyfp << cline << "\n";
367                                         historyfp.close();
368                                 }
369                         }
370
371                         line = cline;
372
373                         free(cline);
374 #else /* HAVE_EDITLINE */
375                         std::getline(std::cin, line);
376 #endif /* HAVE_EDITLINE */
377                 } else
378                         line = commandOnce;
379
380                 if (!line.empty() && line[0] == '$') {
381                         if (line == "$continue" || line == "$quit" || line == "$exit")
382                                 break;
383                         else if (line == "$help")
384                                 std::cout << "Welcome to the Icinga 2 debug console.\n"
385                                         "Usable commands:\n"
386                                         "  $continue      Continue running Icinga 2 (script debugger).\n"
387                                         "  $quit, $exit   Stop debugging and quit the console.\n"
388                                         "  $help          Print this help.\n\n"
389                                         "For more information on how to use this console, please consult the documentation at https://icinga.com/docs\n";
390                         else
391                                 std::cout << "Unknown debugger command: " << line << "\n";
392
393                         continue;
394                 }
395
396                 if (!command.empty())
397                         command += "\n";
398
399                 command += line;
400
401                 std::unique_ptr<Expression> expr;
402
403                 try {
404                         lines[fileName] = command;
405
406                         Value result;
407
408                         if (!l_ApiClient) {
409                                 expr = ConfigCompiler::CompileText(fileName, command);
410
411                                 /* This relies on the fact that - for syntax errors - CompileText()
412                                  * returns an AST where the top-level expression is a 'throw'. */
413                                 if (!syntaxOnly || dynamic_cast<ThrowExpression *>(expr.get())) {
414                                         if (syntaxOnly)
415                                                 std::cerr << "    => " << command << std::endl;
416                                         result = Serialize(expr->Evaluate(scriptFrame), 0);
417                                 } else
418                                         result = true;
419                         } else {
420                                 boost::mutex mutex;
421                                 boost::condition_variable cv;
422                                 bool ready = false;
423                                 boost::exception_ptr eptr;
424
425                                 l_ApiClient->ExecuteScript(l_Session, command, scriptFrame.Sandboxed,
426                                         std::bind(&ConsoleCommand::ExecuteScriptCompletionHandler,
427                                         std::ref(mutex), std::ref(cv), std::ref(ready),
428                                         _1, _2,
429                                         std::ref(result), std::ref(eptr)));
430
431                                 {
432                                         boost::mutex::scoped_lock lock(mutex);
433                                         while (!ready)
434                                                 cv.wait(lock);
435                                 }
436
437                                 if (eptr)
438                                         boost::rethrow_exception(eptr);
439                         }
440
441                         if (commandOnce.IsEmpty()) {
442                                 std::cout << ConsoleColorTag(Console_ForegroundCyan);
443                                 ConfigWriter::EmitValue(std::cout, 1, result);
444                                 std::cout << ConsoleColorTag(Console_Normal) << "\n";
445                         } else {
446                                 std::cout << JsonEncode(result) << "\n";
447                                 break;
448                         }
449                 } catch (const ScriptError& ex) {
450                         if (ex.IsIncompleteExpression() && commandOnce.IsEmpty()) {
451                                 continuation = true;
452                                 goto incomplete;
453                         }
454
455                         DebugInfo di = ex.GetDebugInfo();
456
457                         if (commandOnceFileName.IsEmpty() && lines.find(di.Path) != lines.end()) {
458                                 String text = lines[di.Path];
459
460                                 std::vector<String> ulines = text.Split("\n");
461
462                                 for (decltype(ulines.size()) i = 1; i <= ulines.size(); i++) {
463                                         int start, len;
464
465                                         if (i == (decltype(i))di.FirstLine)
466                                                 start = di.FirstColumn;
467                                         else
468                                                 start = 0;
469
470                                         if (i == (decltype(i))di.LastLine)
471                                                 len = di.LastColumn - di.FirstColumn + 1;
472                                         else
473                                                 len = ulines[i - 1].GetLength();
474
475                                         int offset;
476
477                                         if (di.Path != fileName) {
478                                                 std::cout << di.Path << ": " << ulines[i - 1] << "\n";
479                                                 offset = 2;
480                                         } else
481                                                 offset = 4;
482
483                                         if (i >= (decltype(i))di.FirstLine && i <= (decltype(i))di.LastLine) {
484                                                 std::cout << String(di.Path.GetLength() + offset, ' ');
485                                                 std::cout << String(start, ' ') << String(len, '^') << "\n";
486                                         }
487                                 }
488                         } else {
489                                 ShowCodeLocation(std::cout, di);
490                         }
491
492                         std::cout << ex.what() << "\n";
493
494                         if (!commandOnce.IsEmpty())
495                                 return EXIT_FAILURE;
496                 } catch (const std::exception& ex) {
497                         std::cout << "Error: " << DiagnosticInformation(ex) << "\n";
498
499                         if (!commandOnce.IsEmpty())
500                                 return EXIT_FAILURE;
501                 }
502         }
503
504         return EXIT_SUCCESS;
505 }
506
507 void ConsoleCommand::ExecuteScriptCompletionHandler(boost::mutex& mutex, boost::condition_variable& cv,
508         bool& ready, const boost::exception_ptr& eptr, const Value& result, Value& resultOut, boost::exception_ptr& eptrOut)
509 {
510         if (eptr) {
511                 try {
512                         boost::rethrow_exception(eptr);
513                 } catch (const ScriptError&) {
514                         eptrOut = boost::current_exception();
515                 } catch (const std::exception& ex) {
516                         Log(LogCritical, "ConsoleCommand")
517                                 << "HTTP query failed: " << ex.what();
518
519 #ifdef HAVE_EDITLINE
520                         /* Ensures that the terminal state is resetted */
521                         rl_deprep_terminal();
522 #endif /* HAVE_EDITLINE */
523
524                         Application::Exit(EXIT_FAILURE);
525                 }
526         }
527
528         resultOut = result;
529
530         {
531                 boost::mutex::scoped_lock lock(mutex);
532                 ready = true;
533                 cv.notify_all();
534         }
535 }
536
537 void ConsoleCommand::AutocompleteScriptCompletionHandler(boost::mutex& mutex, boost::condition_variable& cv,
538         bool& ready, const boost::exception_ptr& eptr, const Array::Ptr& result, Array::Ptr& resultOut)
539 {
540         if (eptr) {
541                 try {
542                         boost::rethrow_exception(eptr);
543                 } catch (const std::exception& ex) {
544                         Log(LogCritical, "ConsoleCommand")
545                                 << "HTTP query failed: " << ex.what();
546
547 #ifdef HAVE_EDITLINE
548                         /* Ensures that the terminal state is resetted */
549                         rl_deprep_terminal();
550 #endif /* HAVE_EDITLINE */
551
552                         Application::Exit(EXIT_FAILURE);
553                 }
554         }
555
556         resultOut = result;
557
558         {
559                 boost::mutex::scoped_lock lock(mutex);
560                 ready = true;
561                 cv.notify_all();
562         }
563 }