From bff3894c5f2d5f7f8c6213fecb10f8d6d82f1d59 Mon Sep 17 00:00:00 2001 From: Antoine Date: Tue, 3 Nov 2020 17:35:52 -0500 Subject: [PATCH] Modified ActionExecute::Execute() to delegate to ExecuteVerb() or ExecuteProcess(). Created unit tests. --- include/shellanything/ActionExecute.h | 11 +- src/ActionExecute.cpp | 56 +++++-- test/CMakeLists.txt | 2 + test/TestActionExecute.cpp | 228 ++++++++++++++++++++++++++ test/TestActionExecute.h | 42 +++++ 5 files changed, 325 insertions(+), 14 deletions(-) create mode 100644 test/TestActionExecute.cpp create mode 100644 test/TestActionExecute.h diff --git a/include/shellanything/ActionExecute.h b/include/shellanything/ActionExecute.h index 9edf463f..f6cc51f1 100644 --- a/include/shellanything/ActionExecute.h +++ b/include/shellanything/ActionExecute.h @@ -87,12 +87,21 @@ namespace shellanything void SetVerb(const std::string& iVerb); private: + /// + /// Execute an application with ShellExecuteEx method. + /// This execute method supports verbs. + /// + /// The current context of execution. + /// Returns true if the execution is successful. Returns false otherwise. + virtual bool ExecuteVerb(const Context & iContext) const; + /// /// Execute an application with RapidAssist method. + /// This execute method does not supports verbs. /// /// The current context of execution. /// Returns true if the execution is successful. Returns false otherwise. - virtual bool StartProcess(const Context & iContext) const; + virtual bool ExecuteProcess(const Context & iContext) const; private: std::string mPath; diff --git a/src/ActionExecute.cpp b/src/ActionExecute.cpp index e5497d85..8a24071c 100644 --- a/src/ActionExecute.cpp +++ b/src/ActionExecute.cpp @@ -47,6 +47,18 @@ namespace shellanything } bool ActionExecute::Execute(const Context& iContext) const + { + PropertyManager& pmgr = PropertyManager::GetInstance(); + std::string verb = pmgr.Expand(mVerb); + + //If a verb was specified, delegate to VerbExecute(). Otherwise, use ProcessExecute(). + if (verb.empty()) + return ExecuteProcess(iContext); + else + return ExecuteVerb(iContext); + } + + bool ActionExecute::ExecuteVerb(const Context& iContext) const { PropertyManager& pmgr = PropertyManager::GetInstance(); std::string path = pmgr.Expand(mPath); @@ -71,35 +83,44 @@ namespace shellanything info.nShow = SW_SHOWDEFAULT; info.lpFile = pathW.c_str(); + //Print execute values in the logs LOG(INFO) << "Exec: '" << path << "'."; - if (!verb.empty()) { info.lpVerb = verbW.c_str(); // Verb LOG(INFO) << "Verb: '" << verb << "'."; } - if (!arguments.empty()) { info.lpParameters = argumentsW.c_str(); // Arguments LOG(INFO) << "Arguments: '" << arguments << "'."; } - if (!basedir.empty()) { info.lpDirectory = basedirW.c_str(); // Default directory LOG(INFO) << "Basedir: '" << basedir << "'."; } - BOOL success = ShellExecuteExW(&info); - return (success == TRUE); + //Execute and get the pid + bool success = (ShellExecuteExW(&info) == TRUE); + if (!success) + return false; + DWORD pId = GetProcessId(info.hProcess); + + success = (pId != ra::process::INVALID_PROCESS_ID); + if (success) + { + LOG(INFO) << "Process created. PID=" << pId; + } + + return success; } - bool ActionExecute::StartProcess(const Context & iContext) const + bool ActionExecute::ExecuteProcess(const Context & iContext) const { - PropertyManager & pmgr = PropertyManager::GetInstance(); - std::string path = pmgr.Expand(mPath); - std::string basedir = pmgr.Expand(mBaseDir); + PropertyManager& pmgr = PropertyManager::GetInstance(); + std::string path = pmgr.Expand(mPath); + std::string basedir = pmgr.Expand(mBaseDir); std::string arguments = pmgr.Expand(mArguments); bool basedir_missing = basedir.empty(); @@ -144,20 +165,29 @@ namespace shellanything LOG(WARNING) << "attribute 'basedir' not specified."; } - //debug + //Print execute values in the logs + LOG(INFO) << "Exec: '" << path << "'."; + if (!arguments.empty()) + { + LOG(INFO) << "Arguments: '" << arguments << "'."; + } + if (!basedir.empty()) + { + LOG(INFO) << "Basedir: '" << basedir << "'."; + } + + //Execute and get the pid uint32_t pId = ra::process::INVALID_PROCESS_ID; if (arguments_missing) { - LOG(INFO) << "Running '" << path << "' from directory '" << basedir << "'."; pId = ra::process::StartProcessUtf8(path, basedir); } else { - LOG(INFO) << "Running '" << path << "' from directory '" << basedir << "' with arguments '" << arguments << "'."; pId = ra::process::StartProcessUtf8(path, basedir, arguments); } - bool success = pId != ra::process::INVALID_PROCESS_ID; + bool success = (pId != ra::process::INVALID_PROCESS_ID); if (success) { LOG(INFO) << "Process created. PID=" << pId; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ecfb2de5..34e6a408 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -48,6 +48,8 @@ add_executable(shellanything_unittest ${CONFIGURATION_TEST_FILES} ${SHELLANYTHING_PRIVATE_FILES} main.cpp + TestActionExecute.cpp + TestActionExecute.h TestActionFile.cpp TestActionFile.h TestBitmapCache.cpp diff --git a/test/TestActionExecute.cpp b/test/TestActionExecute.cpp new file mode 100644 index 00000000..f11a49c7 --- /dev/null +++ b/test/TestActionExecute.cpp @@ -0,0 +1,228 @@ +/********************************************************************************** + * MIT License + * + * Copyright (c) 2018 Antoine Beauchamp + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + *********************************************************************************/ + +#include "TestActionExecute.h" +#include "shellanything/Context.h" +#include "shellanything/ActionExecute.h" +#include "PropertyManager.h" +#include "rapidassist/testing.h" +#include "rapidassist/filesystem_utf8.h" +#include "rapidassist/user.h" +#include "rapidassist/timing.h" +#include "rapidassist/environment.h" + +#include + +namespace shellanything { namespace test +{ + + //-------------------------------------------------------------------------------------------------- + void TestActionExecute::SetUp() + { + } + //-------------------------------------------------------------------------------------------------- + void TestActionExecute::TearDown() + { + } + //-------------------------------------------------------------------------------------------------- + TEST_F(TestActionExecute, testBasic) + { + PropertyManager & pmgr = PropertyManager::GetInstance(); + + //Create a valid context + Context c; + Context::ElementList elements; + elements.push_back("C:\\Windows\\System32\\calc.exe"); + c.SetElements(elements); + + c.RegisterProperties(); + + //execute the action + ActionExecute ae; + ae.SetPath("C:\\Windows\\System32\\calc.exe"); + ae.SetBaseDir(""); + ae.SetArguments(""); + + bool executed = ae.Execute(c); + ASSERT_TRUE( executed ); + + //cleanup + ra::timing::Millisleep(500); + system("cmd.exe /c taskkill /IM calc.exe >NUL 2>NUL"); + } + //-------------------------------------------------------------------------------------------------- + TEST_F(TestActionExecute, testBaseDir) + { + PropertyManager & pmgr = PropertyManager::GetInstance(); + + //Create a valid context + Context c; + Context::ElementList elements; + elements.push_back("C:\\Windows\\System32\\calc.exe"); + c.SetElements(elements); + + c.RegisterProperties(); + + std::string home_dir = ra::user::GetHomeDirectory(); + + //execute the action + ActionExecute ae; + ae.SetPath("calc.exe"); + ae.SetBaseDir("C:\\Windows\\System32"); + ae.SetArguments(""); + + bool executed = ae.Execute(c); + ASSERT_TRUE( executed ); + + //cleanup + ra::timing::Millisleep(500); + system("cmd.exe /c taskkill /IM calc.exe >NUL 2>NUL"); + } + //-------------------------------------------------------------------------------------------------- + TEST_F(TestActionExecute, testArguments) + { + PropertyManager & pmgr = PropertyManager::GetInstance(); + + //Create a valid context + Context c; + Context::ElementList elements; + elements.push_back("C:\\Windows\\System32\\calc.exe"); + c.SetElements(elements); + + c.RegisterProperties(); + + std::string home_dir = ra::user::GetHomeDirectory(); + std::string temp_dir = ra::filesystem::GetTemporaryDirectory(); + std::string destination_path = temp_dir + "\\my_calc.exe"; + std::string arguments = "/c copy C:\\Windows\\System32\\calc.exe " + destination_path + ">NUL 2>NUL"; + + //execute the action + ActionExecute ae; + ae.SetPath("cmd.exe"); + ae.SetBaseDir(temp_dir); + ae.SetArguments(arguments); + + bool executed = ae.Execute(c); + ASSERT_TRUE( executed ); + + //Wait for the copy to complete, with a timeout + static const double timeout_time = 5000; //ms + bool file_copied = false; + double timer_start = ra::timing::GetMillisecondsTimer(); + double time_elapsed = ra::timing::GetMillisecondsTimer() - timer_start; + while(!file_copied && time_elapsed <= timeout_time) + { + file_copied = ra::filesystem::FileExists(destination_path.c_str()); + ra::timing::Millisleep(500); //allow process to complete + time_elapsed = ra::timing::GetMillisecondsTimer() - timer_start; //evaluate elapsed time again + } + + //Validate arguments + ASSERT_TRUE(file_copied); + + //cleanup + ra::filesystem::DeleteFileUtf8(destination_path.c_str()); + } + //-------------------------------------------------------------------------------------------------- + TEST_F(TestActionExecute, testVerb) + { + //Skip this test if run on AppVeyor as it requires administrative (elevated) privileges. + if (ra::testing::IsAppVeyor() || + ra::testing::IsJenkins() || + ra::testing::IsTravis()) + { + printf("Skipping tests as it requires administrative (elevated) privileges.\n"); + return; + } + + PropertyManager & pmgr = PropertyManager::GetInstance(); + + //Create a valid context + Context c; + Context::ElementList elements; + elements.push_back("C:\\Windows\\System32\\calc.exe"); + c.SetElements(elements); + + c.RegisterProperties(); + + std::string temp_dir = ra::filesystem::GetTemporaryDirectory(); + std::string batch_file_filename = ra::testing::GetTestQualifiedName() + ".bat"; + std::string result_file_filename = ra::testing::GetTestQualifiedName() + ".txt"; + std::string batch_file_path = temp_dir + "\\" + batch_file_filename; + std::string result_file_path = temp_dir + "\\" + result_file_filename; + std::string arguments = "/c " + batch_file_filename + " >" + result_file_filename; + + //This is a batch file that prints ADMIN if it is run in elevated mode or prints FAIL otherwise. + //Inspired from https://stackoverflow.com/questions/4051883/batch-script-how-to-check-for-admin-rights + const std::string content = + "@echo off\n" + "net session >nul 2>&1\n" + "if %errorLevel% == 0 (\n" + " echo ADMIN\n" + ") else (\n" + " echo FAIL\n" + ")\n"; + ASSERT_TRUE( ra::filesystem::WriteTextFile(batch_file_path, content) ); + + //execute the action + ActionExecute ae; + ae.SetPath("cmd.exe"); + ae.SetBaseDir(temp_dir); + ae.SetArguments(arguments); + ae.SetVerb("runas"); + + bool executed = ae.Execute(c); + ASSERT_TRUE( executed ); + + //Wait for the operation to complete, with a timeout + static const double timeout_time = 5000; //ms + bool result_file_found = false; + double timer_start = ra::timing::GetMillisecondsTimer(); + double time_elapsed = ra::timing::GetMillisecondsTimer() - timer_start; + while(!result_file_found && time_elapsed <= timeout_time) + { + result_file_found = ra::filesystem::FileExists(result_file_path.c_str()); + ra::timing::Millisleep(500); //allow process to complete + time_elapsed = ra::timing::GetMillisecondsTimer() - timer_start; //evaluate elapsed time again + } + + //Validate arguments + ASSERT_TRUE(result_file_found); + + //Read the result file + std::string result; + ASSERT_TRUE( ra::filesystem::ReadTextFile(result_file_path, result) ); + ra::strings::Replace(result, ra::environment::GetLineSeparator(), ""); + ra::strings::Replace(result, "\n", ""); + static const std::string EXPECTED_RESULT = "ADMIN"; + ASSERT_EQ(EXPECTED_RESULT, result); + + //cleanup + ra::filesystem::DeleteFileUtf8(batch_file_path.c_str()); + ra::filesystem::DeleteFileUtf8(result_file_path.c_str()); + } + //-------------------------------------------------------------------------------------------------- + +} //namespace test +} //namespace shellanything diff --git a/test/TestActionExecute.h b/test/TestActionExecute.h new file mode 100644 index 00000000..e8e1f569 --- /dev/null +++ b/test/TestActionExecute.h @@ -0,0 +1,42 @@ +/********************************************************************************** + * MIT License + * + * Copyright (c) 2018 Antoine Beauchamp + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + *********************************************************************************/ + +#ifndef TEST_SA_ACTIONEXECUTE_H +#define TEST_SA_ACTIONEXECUTE_H + +#include + +namespace shellanything { namespace test +{ + class TestActionExecute : public ::testing::Test + { + public: + virtual void SetUp(); + virtual void TearDown(); + }; + +} //namespace test +} //namespace shellanything + +#endif //TEST_SA_ACTIONEXECUTE_H