diff --git a/lldb/source/Commands/CMakeLists.txt b/lldb/source/Commands/CMakeLists.txt --- a/lldb/source/Commands/CMakeLists.txt +++ b/lldb/source/Commands/CMakeLists.txt @@ -13,6 +13,7 @@ CommandObjectFrame.cpp CommandObjectGUI.cpp CommandObjectHelp.cpp + CommandObjectLanguage.cpp CommandObjectLog.cpp CommandObjectMemory.cpp CommandObjectMultiword.cpp @@ -23,6 +24,7 @@ CommandObjectRegister.cpp CommandObjectReproducer.cpp CommandObjectSettings.cpp + CommandObjectShell.cpp CommandObjectSource.cpp CommandObjectStats.cpp CommandObjectTarget.cpp @@ -31,7 +33,6 @@ CommandObjectVersion.cpp CommandObjectWatchpoint.cpp CommandObjectWatchpointCommand.cpp - CommandObjectLanguage.cpp LINK_LIBS lldbBase diff --git a/lldb/source/Commands/CommandObjectShell.h b/lldb/source/Commands/CommandObjectShell.h new file mode 100644 --- /dev/null +++ b/lldb/source/Commands/CommandObjectShell.h @@ -0,0 +1,82 @@ +//===-- CommandObjectShell.h ---=--------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_SOURCE_INTERPRETER_COMMANDOBJECTSHELL_H +#define LLDB_SOURCE_INTERPRETER_COMMANDOBJECTSHELL_H + +#include "lldb/Host/OptionParser.h" +#include "lldb/Interpreter/CommandObject.h" +#include "lldb/Interpreter/Options.h" +#include "lldb/Utility/Timeout.h" + +namespace lldb_private { + +class CommandObjectShell : public CommandObjectRaw { +public: + CommandObjectShell(CommandInterpreter &interpreter); + + ~CommandObjectShell() override; + + void HandleCompletion(CompletionRequest &request) override; + + static void GenerateAdditionalHelpAvenuesMessage( + Stream *s, llvm::StringRef command, llvm::StringRef prefix, + llvm::StringRef subcommand, bool include_upropos = true, + bool include_type_lookup = true); + + class CommandOptions : public Options { + public: + CommandOptions() : Options() {} + + ~CommandOptions() override {} + + Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg, + ExecutionContext *execution_context) override { + Status error; + + const char short_option = (char)GetDefinitions()[option_idx].short_option; + + switch (short_option) { + case 't': + uint32_t timeout_sec; + if (option_arg.getAsInteger(10, timeout_sec)) + error.SetErrorStringWithFormat( + "could not convert \"%s\" to a numeric value.", + option_arg.str().c_str()); + else + m_timeout = std::chrono::seconds(timeout_sec); + break; + default: + llvm_unreachable("Unimplemented option"); + } + + return error; + } + + void OptionParsingStarting(ExecutionContext *execution_context) override {} + + llvm::ArrayRef GetDefinitions() override; + + // Instance variables to hold the values for command options. + + Timeout m_timeout = std::chrono::seconds(10); + }; + + Options *GetOptions() override { return &m_options; } + +protected: + bool DoExecute(llvm::StringRef raw_command_line, + CommandReturnObject &result) override; + +private: + CommandOptions m_options; +}; + +} // namespace lldb_private + +#endif // LLDB_SOURCE_INTERPRETER_COMMANDOBJECTSHELL_H diff --git a/lldb/source/Commands/CommandObjectShell.cpp b/lldb/source/Commands/CommandObjectShell.cpp new file mode 100644 --- /dev/null +++ b/lldb/source/Commands/CommandObjectShell.cpp @@ -0,0 +1,105 @@ +#include "CommandObjectShell.h" +#include "lldb/Interpreter/CommandInterpreter.h" +#include "lldb/Interpreter/CommandReturnObject.h" + +using namespace lldb; +using namespace lldb_private; + +// CommandObjectShell + +CommandObjectShell::CommandObjectShell(CommandInterpreter &interpreter) + : CommandObjectRaw(interpreter, "shell", "Run a shell command on the host.", + "shell ", 0), + m_options() {} + +CommandObjectShell::~CommandObjectShell() = default; + +// "shell" +#define LLDB_OPTIONS_shell +#include "CommandOptions.inc" + +llvm::ArrayRef +CommandObjectShell::CommandOptions::GetDefinitions() { + return llvm::makeArrayRef(g_shell_options); +} + +bool CommandObjectShell::DoExecute(llvm::StringRef raw_command_line, + CommandReturnObject &result) { + ExecutionContext exe_ctx = GetCommandInterpreter().GetExecutionContext(); + m_options.NotifyOptionParsingStarting(&exe_ctx); + + // Print out an usage syntax on an empty command line. + if (raw_command_line.empty()) { + result.GetOutputStream().Printf("%s\n", this->GetSyntax().str().c_str()); + return true; + } + + OptionsWithRaw args(raw_command_line); + const char *expr = args.GetRawPart().c_str(); + + if (args.HasArgs()) + if (!ParseOptions(args.GetArgs(), result)) + return false; + + PlatformSP platform_sp(Platform::GetHostPlatform()); + Status error; + if (platform_sp) { + FileSpec working_dir{}; + std::string output; + int status = -1; + int signo = -1; + error = (platform_sp->RunShellCommand(expr, working_dir, &status, &signo, + &output, m_options.m_timeout)); + if (!output.empty()) + result.GetOutputStream().PutCString(output); + if (status > 0) { + if (signo > 0) { + const char *signo_cstr = Host::GetSignalAsCString(signo); + if (signo_cstr) + result.GetOutputStream().Printf( + "error: command returned with status %i and signal %s\n", status, + signo_cstr); + else + result.GetOutputStream().Printf( + "error: command returned with status %i and signal %i\n", status, + signo); + } else + result.GetOutputStream().Printf( + "error: command returned with status %i\n", status); + } + } else { + result.GetOutputStream().Printf( + "error: cannot run remote shell commands without a platform\n"); + error.SetErrorString( + "error: cannot run remote shell commands without a platform"); + } + + if (error.Fail()) { + result.AppendError(error.AsCString()); + result.SetStatus(eReturnStatusFailed); + } else { + result.SetStatus(eReturnStatusSuccessFinishResult); + } + return true; +} + +void CommandObjectShell::HandleCompletion(CompletionRequest &request) { + // Return the completions of the commands in the help system: + if (request.GetCursorIndex() == 0) { + m_interpreter.HandleCompletionMatches(request); + return; + } + CommandObject *cmd_obj = + m_interpreter.GetCommandObject(request.GetParsedLine()[0].ref()); + + // The command that they are getting help on might be ambiguous, in which + // case we should complete that, otherwise complete with the command the + // user is getting help on... + + if (cmd_obj) { + request.ShiftArguments(); + cmd_obj->HandleCompletion(request); + return; + } + m_interpreter.HandleCompletionMatches(request); +} diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td --- a/lldb/source/Commands/Options.td +++ b/lldb/source/Commands/Options.td @@ -701,6 +701,12 @@ Desc<"Set the synchronicity of this command's executions with regard to " "LLDB event system.">; } + +let Command = "shell" in { + def shell_timeout : Option<"timeout", "t">, Arg<"Value">, + Desc<"Seconds to wait for the host to finish running the command.">; +} + let Command = "source info" in { def source_info_count : Option<"count", "c">, Arg<"Count">, Desc<"The number of line entries to display.">; diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp --- a/lldb/source/Interpreter/CommandInterpreter.cpp +++ b/lldb/source/Interpreter/CommandInterpreter.cpp @@ -32,6 +32,7 @@ #include "Commands/CommandObjectRegister.h" #include "Commands/CommandObjectReproducer.h" #include "Commands/CommandObjectSettings.h" +#include "Commands/CommandObjectShell.h" #include "Commands/CommandObjectSource.h" #include "Commands/CommandObjectStats.h" #include "Commands/CommandObjectTarget.h" @@ -485,6 +486,7 @@ CommandObjectSP(new CommandObjectScript(*this, script_language)); m_command_dict["settings"] = CommandObjectSP(new CommandObjectMultiwordSettings(*this)); + m_command_dict["shell"] = CommandObjectSP(new CommandObjectShell(*this)); m_command_dict["source"] = CommandObjectSP(new CommandObjectMultiwordSource(*this)); m_command_dict["statistics"] = CommandObjectSP(new CommandObjectStats(*this)); diff --git a/lldb/test/API/commands/shell/TestShellCommand.py b/lldb/test/API/commands/shell/TestShellCommand.py new file mode 100644 --- /dev/null +++ b/lldb/test/API/commands/shell/TestShellCommand.py @@ -0,0 +1,45 @@ +""" +Test the lldb host shell command. +""" + + + +import lldb +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbutil + + +class ShellCommandTestCase(TestBase): + + mydir = TestBase.compute_mydir(__file__) + + @no_debug_info_test + def test_help_platform(self): + self.expect("help shell", substrs=["shell ", + "-t ( --timeout )"]) + + @expectedFailureAll(oslist=["windows"]) + @no_debug_info_test + def test_shell(self): + """ Test that the shell command can invoke ls. """ + triple = self.dbg.GetSelectedPlatform().GetTriple() + if re.match(".*-.*-windows", triple): + self.expect("shell dir c:\\", substrs=["Windows", "Program Files"]) + elif re.match(".*-.*-.*-android", triple): + self.expect("shell ls /", + substrs=["cache", "dev", "system"]) + else: + self.expect("shell ls /", substrs=["dev", "tmp", "usr"]) + + @no_debug_info_test + def test_shell_builtin(self): + """ Test a shell built-in command (echo) """ + self.expect("shell echo hello lldb", + substrs=["hello lldb"]) + + @no_debug_info_test + def test_shell_timeout(self): + """ Test a shell built-in command (sleep) that times out """ + self.expect("shell -t 1 -- sleep 3", error=True, substrs=[ + "error: timed out waiting for shell command to complete"])