From d0de816dac2c42aa601a94fcbec12c66fba9070a Mon Sep 17 00:00:00 2001 From: Mindaugas Date: Wed, 7 Oct 2020 21:21:16 +0300 Subject: [PATCH 1/6] Migrating from CreateProcess to ShellExecuteEx --- AUTHORS | 1 + include/shellanything/ActionExecute.h | 22 +++++++++- src/ActionExecute.cpp | 62 ++++++++++++++++++++++++++- src/ActionOpen.cpp | 4 +- src/ObjectFactory.cpp | 8 ++++ 5 files changed, 92 insertions(+), 5 deletions(-) diff --git a/AUTHORS b/AUTHORS index ef273297..ebee3a0c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,3 +3,4 @@ # their names here. Please keep the list sorted by first names. Antoine Beauchamp +Mindaugas Ribaconka \ No newline at end of file diff --git a/include/shellanything/ActionExecute.h b/include/shellanything/ActionExecute.h index e79128e7..5b494496 100644 --- a/include/shellanything/ActionExecute.h +++ b/include/shellanything/ActionExecute.h @@ -40,11 +40,18 @@ namespace shellanything virtual ~ActionExecute(); /// - /// Execute an application. + /// Execute an application. + /// + /// The current context of execution. + /// Returns true if the execution is successful. Returns false otherwise. + virtual bool Execute(const Context& iContext) const; + + /// + /// Execute an application with RapidAssist method. /// /// The current context of execution. /// Returns true if the execution is successful. Returns false otherwise. - virtual bool Execute(const Context & iContext) const; + virtual bool StartProcess(const Context & iContext) const; /// /// Getter for the 'path' parameter. @@ -76,10 +83,21 @@ namespace shellanything /// void SetArguments(const std::string & iArguments); + /// + /// Getter for the 'verb' parameter. + /// + const std::string& GetVerb() const; + + /// + /// Setter for the 'verb' parameter. + /// + void SetVerb(const std::string& iArguments); + private: std::string mPath; std::string mBaseDir; std::string mArguments; + std::string mVerb; }; } //namespace shellanything diff --git a/src/ActionExecute.cpp b/src/ActionExecute.cpp index 4b8465cb..6207c6e3 100644 --- a/src/ActionExecute.cpp +++ b/src/ActionExecute.cpp @@ -28,6 +28,7 @@ #include "rapidassist/filesystem_utf8.h" #include "PropertyManager.h" +#include #pragma warning( push ) #pragma warning( disable: 4355 ) // glog\install_dir\include\glog/logging.h(1167): warning C4355: 'this' : used in base member initializer list #include @@ -45,7 +46,56 @@ namespace shellanything { } - bool ActionExecute::Execute(const Context & iContext) const + bool ActionExecute::Execute(const Context& iContext) const + { + PropertyManager& pmgr = PropertyManager::GetInstance(); + std::string path = pmgr.Expand(mPath); + std::string basedir = pmgr.Expand(mBaseDir); + std::string arguments = pmgr.Expand(mArguments); + std::string verb = pmgr.Expand(mVerb); + + std::wstring pathW = ra::unicode::Utf8ToUnicode(path); + std::wstring argumentsW = ra::unicode::Utf8ToUnicode(arguments); + std::wstring basedirW = ra::unicode::Utf8ToUnicode(basedir); + std::wstring verbW = ra::unicode::Utf8ToUnicode(verb); + + SHELLEXECUTEINFOW info = { 0 }; + + info.cbSize = sizeof(SHELLEXECUTEINFOW); + + info.fMask |= SEE_MASK_NOCLOSEPROCESS; + info.fMask |= SEE_MASK_NOASYNC; + info.fMask |= SEE_MASK_FLAG_DDEWAIT; + + info.hwnd = HWND_DESKTOP; + info.nShow = SW_SHOWDEFAULT; + info.lpFile = pathW.c_str(); + + 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); + } + + bool ActionExecute::StartProcess(const Context & iContext) const { PropertyManager & pmgr = PropertyManager::GetInstance(); std::string path = pmgr.Expand(mPath); @@ -146,4 +196,14 @@ namespace shellanything mArguments = iArguments; } + const std::string& ActionExecute::GetVerb() const + { + return mArguments; + } + + void ActionExecute::SetVerb(const std::string& iVerb) + { + mVerb = iVerb; + } + } //namespace shellanything diff --git a/src/ActionOpen.cpp b/src/ActionOpen.cpp index 206ae130..6d95cab1 100644 --- a/src/ActionOpen.cpp +++ b/src/ActionOpen.cpp @@ -69,8 +69,8 @@ namespace shellanything info.nShow = SW_SHOWDEFAULT; info.lpVerb = L"open"; info.lpFile = pathW.c_str(); - info.lpParameters = NULL; //arguments - info.lpDirectory = NULL; // default directory + info.lpParameters = NULL; // arguments + info.lpDirectory = NULL; // Default directory BOOL success = ShellExecuteExW(&info); return (success == TRUE); diff --git a/src/ObjectFactory.cpp b/src/ObjectFactory.cpp index e66a0a44..569fd236 100644 --- a/src/ObjectFactory.cpp +++ b/src/ObjectFactory.cpp @@ -293,6 +293,14 @@ namespace shellanything action->SetBaseDir(tmp_str); } + //parse verb + tmp_str = ""; + tmp_int = -1; + if (ParseAttribute(element, "verb", true, true, tmp_str, error)) + { + action->SetVerb(tmp_str); + } + //done parsing return action; } From 23e3df99b06aae5dcad025d90911be6bcbf525a6 Mon Sep 17 00:00:00 2001 From: Antoine Date: Sun, 1 Nov 2020 07:54:18 -0500 Subject: [PATCH 2/6] Updated CHANGES file. Moved `ActionExecute::StartProcess()` private. Fixed a bad copy paste for `ActionExecute::SetVerb()` and `ActionExecute::GetVerb()`. Updated documentation for verb attribute of element. Fixed missing closing quotes in logs of ActionExecute::Execute(). Created TestObjectFactory.testParseActionExecute() unit tests to validate parsing. For issue #56. --- CHANGES | 1 + UserManual.md | 24 +++++- include/shellanything/ActionExecute.h | 21 ++--- src/ActionExecute.cpp | 10 +-- test/CMakeLists.txt | 1 + test/TestConfiguration.cpp | 1 + test/TestObjectFactory.cpp | 86 +++++++++++++++++++ ...stObjectFactory.testParseActionExecute.xml | 30 +++++++ 8 files changed, 158 insertions(+), 16 deletions(-) create mode 100644 test/test_files/TestObjectFactory.testParseActionExecute.xml diff --git a/CHANGES b/CHANGES index 06714cd1..64904a40 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,7 @@ Changes for 0.5.0 * Fixed issue #51: Action "Open command prompt here" from default.xml configuration does not work. * Fixed issue #52: Define a specific property for use as separator for handling multiple file selection. * Fixed issue #55: Menu name maximum length limit and escape string. +* Fixed issue #56: Not implemented administrator mode. * Fixed issue #58: More useful features: class and pattern attributes for validity / visibility. * Fixed issue #61: Support for WIX installer. * Fixed issue #66: Github don't identify the repository LICENSE as MIT. diff --git a/UserManual.md b/UserManual.md index 24806bdb..4db305bc 100644 --- a/UserManual.md +++ b/UserManual.md @@ -1,4 +1,4 @@ -![ShellAnything logo](docs/ShellAnything-splashscreen.jpg?raw=true) +![ShellAnything logo](docs/ShellAnything-splashscreen.jpg?raw=true) # Overview # @@ -550,6 +550,28 @@ For example, the following launche `notepad.exe` and open the `License.txt` docu +#### verb attribute: #### + +The `verb` attribute defines special directives on how to execute a file or launching the application. For example, the verb `open` or `edit` allows the user to open a document using the associated application. The attribute is optional. + +Verbs are specific to a file type but some are supported by multiple types. Commonly available verbs include: +| Verb | Description | +|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| edit | Launches an editor and opens the document for editing. | +| find | Initiates a search starting from the executed directory. | +| open | Launches an application. If this file is not an executable file, its associated application is launched. | +| print | Prints the document file. | +| properties | Displays the object's properties. | +| runas | Launches an application as Administrator. User Account Control (UAC) will prompt the user for consent to run the application elevated or enter the credentials of an administrator account used to run the application. | + +For example, the following launches `notepad.exe` and open the text file `C:\Windows\System32\drivers\etc\hosts` which can only be modified with elevated privileges (as an Administrator) : +```xml + +``` +To get extended information about verbs, see the following Microsoft documentation article: [ShellExecute and ShellExecuteEx, Object Verbs](https://docs.microsoft.com/en-us/windows/win32/shell/launch#object-verbs). + + + ### <open> action ### The <open> element is used to open a document by the default associated application. The <open> element must be added under the <actions> element. diff --git a/include/shellanything/ActionExecute.h b/include/shellanything/ActionExecute.h index 5b494496..9edf463f 100644 --- a/include/shellanything/ActionExecute.h +++ b/include/shellanything/ActionExecute.h @@ -40,18 +40,11 @@ namespace shellanything virtual ~ActionExecute(); /// - /// Execute an application. - /// - /// The current context of execution. - /// Returns true if the execution is successful. Returns false otherwise. - virtual bool Execute(const Context& iContext) const; - - /// - /// Execute an application with RapidAssist method. + /// Execute an application. /// /// The current context of execution. /// Returns true if the execution is successful. Returns false otherwise. - virtual bool StartProcess(const Context & iContext) const; + virtual bool Execute(const Context& iContext) const; /// /// Getter for the 'path' parameter. @@ -91,7 +84,15 @@ namespace shellanything /// /// Setter for the 'verb' parameter. /// - void SetVerb(const std::string& iArguments); + void SetVerb(const std::string& iVerb); + + private: + /// + /// Execute an application with RapidAssist method. + /// + /// The current context of execution. + /// Returns true if the execution is successful. Returns false otherwise. + virtual bool StartProcess(const Context & iContext) const; private: std::string mPath; diff --git a/src/ActionExecute.cpp b/src/ActionExecute.cpp index 6207c6e3..e5497d85 100644 --- a/src/ActionExecute.cpp +++ b/src/ActionExecute.cpp @@ -71,24 +71,24 @@ namespace shellanything info.nShow = SW_SHOWDEFAULT; info.lpFile = pathW.c_str(); - LOG(INFO) << "Exec: '" << path; + LOG(INFO) << "Exec: '" << path << "'."; if (!verb.empty()) { info.lpVerb = verbW.c_str(); // Verb - LOG(INFO) << "Verb: '" << verb; + LOG(INFO) << "Verb: '" << verb << "'."; } if (!arguments.empty()) { info.lpParameters = argumentsW.c_str(); // Arguments - LOG(INFO) << "Arguments: '" << arguments; + LOG(INFO) << "Arguments: '" << arguments << "'."; } if (!basedir.empty()) { info.lpDirectory = basedirW.c_str(); // Default directory - LOG(INFO) << "Basedir: '" << basedir; + LOG(INFO) << "Basedir: '" << basedir << "'."; } BOOL success = ShellExecuteExW(&info); @@ -198,7 +198,7 @@ namespace shellanything const std::string& ActionExecute::GetVerb() const { - return mArguments; + return mVerb; } void ActionExecute::SetVerb(const std::string& iVerb) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d1da2349..ecfb2de5 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -12,6 +12,7 @@ set(CONFIGURATION_TEST_FILES "" ${CMAKE_CURRENT_SOURCE_DIR}/test_files/TestConfigManager.testFileModifications.xml ${CMAKE_CURRENT_SOURCE_DIR}/test_files/TestConfigManager.testParentWithoutChildren.xml ${CMAKE_CURRENT_SOURCE_DIR}/test_files/TestConfiguration.testLoadProperties.xml + ${CMAKE_CURRENT_SOURCE_DIR}/test_files/TestObjectFactory.testParseActionExecute.xml ${CMAKE_CURRENT_SOURCE_DIR}/test_files/TestObjectFactory.testParseActionFile.xml ${CMAKE_CURRENT_SOURCE_DIR}/test_files/TestObjectFactory.testParseActionMessage.xml ${CMAKE_CURRENT_SOURCE_DIR}/test_files/TestObjectFactory.testParseActionPrompt.xml diff --git a/test/TestConfiguration.cpp b/test/TestConfiguration.cpp index e924227d..99f8bee4 100644 --- a/test/TestConfiguration.cpp +++ b/test/TestConfiguration.cpp @@ -112,6 +112,7 @@ namespace shellanything { namespace test "test_files\\TestConfigManager.testFileModifications.xml", "test_files\\TestConfigManager.testParentWithoutChildren.xml", "test_files\\TestConfiguration.testLoadProperties.xml", + "test_files\\TestObjectFactory.testParseActionExecute.xml", "test_files\\TestObjectFactory.testParseActionFile.xml", "test_files\\TestObjectFactory.testParseActionPrompt.xml", "test_files\\TestObjectFactory.testParseDefaults.xml", diff --git a/test/TestObjectFactory.cpp b/test/TestObjectFactory.cpp index 182268a0..084e15ba 100644 --- a/test/TestObjectFactory.cpp +++ b/test/TestObjectFactory.cpp @@ -25,6 +25,7 @@ #include "TestObjectFactory.h" #include "shellanything/ConfigManager.h" #include "shellanything/Context.h" +#include "shellanything/ActionExecute.h" #include "shellanything/ActionFile.h" #include "shellanything/ActionPrompt.h" #include "shellanything/ActionMessage.h" @@ -74,6 +75,23 @@ namespace shellanything { namespace test return NULL; } //-------------------------------------------------------------------------------------------------- + ActionExecute * GetFirstActionExecute(Menu * m) + { + if (!m) + return NULL; + + Action::ActionPtrList actions = m->GetActions(); + for(size_t i=0; i(action); + if (action_execute) + return action_execute; + } + + return NULL; + } + //-------------------------------------------------------------------------------------------------- ActionFile * GetFirstActionFile(Menu * m) { if (!m) @@ -283,6 +301,74 @@ namespace shellanything { namespace test ASSERT_TRUE( ra::filesystem::DeleteFile(template_target_path.c_str()) ) << "Failed deleting file '" << template_target_path << "'."; } //-------------------------------------------------------------------------------------------------- + TEST_F(TestObjectFactory, testParseActionExecute) + { + ConfigManager & cmgr = ConfigManager::GetInstance(); + + static const std::string path_separator = ra::filesystem::GetPathSeparatorStr(); + + //copy test template file to a temporary subdirectory to allow editing the file during the test + std::string test_name = ra::testing::GetTestQualifiedName(); + std::string template_source_path = std::string("test_files") + path_separator + test_name + ".xml"; + std::string template_target_path = std::string("test_files") + path_separator + test_name + path_separator + "tmp.xml"; + + //make sure the target directory exists + std::string template_target_dir = ra::filesystem::GetParentPath(template_target_path); + ASSERT_TRUE( ra::filesystem::CreateDirectory(template_target_dir.c_str()) ) << "Failed creating directory '" << template_target_dir << "'."; + + //copy the file + ASSERT_TRUE( ra::filesystem::CopyFile(template_source_path, template_target_path) ) << "Failed copying file '" << template_source_path << "' to file '" << template_target_path << "'."; + + //wait to make sure that the next files not dated the same date as this copy + ra::timing::Millisleep(1500); + + //setup ConfigManager to read files from template_target_dir + cmgr.ClearSearchPath(); + cmgr.AddSearchPath(template_target_dir); + cmgr.Refresh(); + + //ASSERT the file is loaded + Configuration::ConfigurationPtrList configs = cmgr.GetConfigurations(); + ASSERT_EQ( 1, configs.size() ); + + //ASSERT all menus are available + Menu::MenuPtrList menus = cmgr.GetConfigurations()[0]->GetMenus(); + ASSERT_EQ( 4, menus.size() ); + + //assert all menus have a file element as the first action + ActionExecute * exec00 = GetFirstActionExecute(menus[00]); + ActionExecute * exec01 = GetFirstActionExecute(menus[01]); + ActionExecute * exec02 = GetFirstActionExecute(menus[02]); + ActionExecute * exec03 = GetFirstActionExecute(menus[03]); + + ASSERT_TRUE( exec00 != NULL ); + ASSERT_TRUE( exec01 != NULL ); + ASSERT_TRUE( exec02 != NULL ); + ASSERT_TRUE( exec03 != NULL ); + + //assert menu00 attributes + ASSERT_EQ("C:\\Windows\\System32\\calc.exe", exec00->GetPath()); + + //assert menu01 attributes + // + ASSERT_EQ("C:\\Windows\\notepad.exe", exec01->GetPath()); + ASSERT_EQ("C:\\Program Files\\7-Zip", exec01->GetBaseDir()); + ASSERT_EQ("License.txt", exec01->GetArguments()); + + //assert menu02 attributes + // + ASSERT_EQ("C:\\Windows\\notepad.exe", exec02->GetPath()); + ASSERT_EQ("C:\\Windows\\System32\\drivers\\etc\\hosts", exec02->GetArguments()); + ASSERT_EQ("runas", exec02->GetVerb()); + + //assert menu03 attributes + // + ASSERT_EQ("", exec03->GetPath()); + + //cleanup + ASSERT_TRUE( ra::filesystem::DeleteFile(template_target_path.c_str()) ) << "Failed deleting file '" << template_target_path << "'."; + } + //-------------------------------------------------------------------------------------------------- TEST_F(TestObjectFactory, testParseActionFile) { ConfigManager & cmgr = ConfigManager::GetInstance(); diff --git a/test/test_files/TestObjectFactory.testParseActionExecute.xml b/test/test_files/TestObjectFactory.testParseActionExecute.xml new file mode 100644 index 00000000..60612688 --- /dev/null +++ b/test/test_files/TestObjectFactory.testParseActionExecute.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From bff3894c5f2d5f7f8c6213fecb10f8d6d82f1d59 Mon Sep 17 00:00:00 2001 From: Antoine Date: Tue, 3 Nov 2020 17:35:52 -0500 Subject: [PATCH 3/6] 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 From 7412ab8f231e72830e0a92dce724d28adea2dfa1 Mon Sep 17 00:00:00 2001 From: Antoine Date: Sat, 7 Nov 2020 09:22:08 -0500 Subject: [PATCH 4/6] Implemented custom command line arguments in unit tests. --- test/ArgumentsHandler.cpp | 131 ++++++++++++++++++++++++++++++++++++++ test/ArgumentsHandler.h | 39 ++++++++++++ test/CMakeLists.txt | 2 + test/main.cpp | 8 +++ 4 files changed, 180 insertions(+) create mode 100644 test/ArgumentsHandler.cpp create mode 100644 test/ArgumentsHandler.h diff --git a/test/ArgumentsHandler.cpp b/test/ArgumentsHandler.cpp new file mode 100644 index 00000000..5544fd05 --- /dev/null +++ b/test/ArgumentsHandler.cpp @@ -0,0 +1,131 @@ +/********************************************************************************** + * 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 "ArgumentsHandler.h" +#include "rapidassist/process.h" +#include "rapidassist/environment.h" +#include "rapidassist/filesystem.h" + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN 1 +#endif +#include +#undef GetEnvironmentVariable +#undef DeleteFile +#undef CreateDirectory +#undef CopyFile +#undef CreateFile +#undef GetCurrentDirectory + + +namespace shellanything +{ + + //https://stackoverflow.com/questions/8046097/how-to-check-if-a-process-has-the-administrative-rights + bool IsProcessElevated() + { + BOOL fRet = FALSE; + HANDLE hToken = NULL; + if( OpenProcessToken( GetCurrentProcess( ),TOKEN_QUERY,&hToken ) ) + { + TOKEN_ELEVATION Elevation; + DWORD cbSize = sizeof( TOKEN_ELEVATION ); + if( GetTokenInformation( hToken, TokenElevation, &Elevation, sizeof( Elevation ), &cbSize ) ) + { + fRet = Elevation.TokenIsElevated; + } + } + if( hToken ) + { + CloseHandle( hToken ); + } + return (fRet == TRUE); + } + + //http://blog.aaronballman.com/2011/08/how-to-check-access-rights/ + bool CanAccessFolder( LPCTSTR folderName, DWORD genericAccessRights ) + { + bool bRet = false; + DWORD length = 0; + if (!::GetFileSecurity( folderName, OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, NULL, NULL, &length ) && ERROR_INSUFFICIENT_BUFFER == ::GetLastError()) + { + PSECURITY_DESCRIPTOR security = static_cast< PSECURITY_DESCRIPTOR >( ::malloc( length ) ); + if (security && ::GetFileSecurity( folderName, OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, security, length, &length )) + { + HANDLE hToken = NULL; + if (::OpenProcessToken( ::GetCurrentProcess(), TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_DUPLICATE | STANDARD_RIGHTS_READ, &hToken )) + { + HANDLE hImpersonatedToken = NULL; + if (::DuplicateToken( hToken, SecurityImpersonation, &hImpersonatedToken )) + { + GENERIC_MAPPING mapping = { 0xFFFFFFFF }; + PRIVILEGE_SET privileges = { 0 }; + DWORD grantedAccess = 0, privilegesLength = sizeof( privileges ); + BOOL result = FALSE; + + mapping.GenericRead = FILE_GENERIC_READ; + mapping.GenericWrite = FILE_GENERIC_WRITE; + mapping.GenericExecute = FILE_GENERIC_EXECUTE; + mapping.GenericAll = FILE_ALL_ACCESS; + + ::MapGenericMask( &genericAccessRights, &mapping ); + if (::AccessCheck( security, hImpersonatedToken, genericAccessRights, &mapping, &privileges, &privilegesLength, &grantedAccess, &result )) + { + bRet = (result == TRUE); + } + ::CloseHandle( hImpersonatedToken ); + } + ::CloseHandle( hToken ); + } + ::free( security ); + } + } + + return bRet; + } + + int PrintStatistics() + { + std::string process_path = ra::process::GetCurrentProcessPath(); + ra::process::processid_t process_id = ra::process::GetCurrentProcessId(); + std::string process_current_directory = ra::filesystem::GetCurrentDirectory(); + int process_architecture = (ra::environment::IsProcess64Bit() ? 64 : 86); + int process_elevated = (IsProcessElevated() ? 1 : 0); + const char * build_configuration = (ra::environment::IsConfigurationDebug() ? "debug" : "release"); + std::string env_temp = ra::environment::GetEnvironmentVariable("TEMP"); + bool has_windows_write_access = CanAccessFolder("C:\\Windows", GENERIC_WRITE); + + printf("process.path=%s\n", process_path.c_str()); + printf("process.pid=%d\n", process_id); + printf("process.current_dir=%s\n", process_current_directory.c_str()); + printf("process.architecture=x%d\n", process_architecture); + printf("process.elevated=%d\n", process_elevated); + printf("build.configuration=%s\n", build_configuration); + printf("env.temp=%s\n", env_temp.c_str()); + printf("has_windows_write_access=%d\n", (int)has_windows_write_access); + + return 0; + } + +} //namespace shellanything diff --git a/test/ArgumentsHandler.h b/test/ArgumentsHandler.h new file mode 100644 index 00000000..67447ca9 --- /dev/null +++ b/test/ArgumentsHandler.h @@ -0,0 +1,39 @@ +/********************************************************************************** + * 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 SA_ARGUMENT_HANDLER_H +#define SA_ARGUMENT_HANDLER_H + +namespace shellanything +{ + + /// + /// Print statistics about this program settings. + /// + /// Returns 0 when the function succeed. Returns a non zero values otherwise. + int PrintStatistics(); + +} //namespace shellanything + +#endif //SA_ARGUMENT_HANDLER_H diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 34e6a408..61782970 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 + ArgumentsHandler.cpp + ArgumentsHandler.h TestActionExecute.cpp TestActionExecute.h TestActionFile.cpp diff --git a/test/main.cpp b/test/main.cpp index 3b71e327..8b594fdd 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -35,11 +35,19 @@ #include "rapidassist/testing.h" #include "rapidassist/environment.h" +#include "rapidassist/cli.h" + +#include "ArgumentsHandler.h" using namespace ra; int main(int argc, char **argv) { + // Look for custom command line arguments + bool has_printstatistics = ra::cli::ParseArgument("printstatistics", std::string(), argc, argv); + if (has_printstatistics) + return shellanything::PrintStatistics(); + // Prepare Google's logging library. fLB::FLAGS_logtostderr = false; //on error, print to stdout instead of stderr fLB::FLAGS_log_prefix = 1; //prefix each message in file/console with 'E0405 19:13:07.577863 6652 shellext.cpp:847]' From 740af0f4947e93cdeaf36f2dc3bd6a7f2c0cbec5 Mon Sep 17 00:00:00 2001 From: Antoine Date: Sat, 7 Nov 2020 10:57:20 -0500 Subject: [PATCH 5/6] Modified ArgumentsHandler::PrintProcessSettings() to also print command line arguments and also print the settings to an output file. --- test/ArgumentsHandler.cpp | 25 ++++++++++++++++++++++++- test/ArgumentsHandler.h | 2 +- test/main.cpp | 6 +++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/test/ArgumentsHandler.cpp b/test/ArgumentsHandler.cpp index 5544fd05..b9a16b4f 100644 --- a/test/ArgumentsHandler.cpp +++ b/test/ArgumentsHandler.cpp @@ -105,7 +105,7 @@ namespace shellanything return bRet; } - int PrintStatistics() + int PrintProcessSettings(int argc, char **argv) { std::string process_path = ra::process::GetCurrentProcessPath(); ra::process::processid_t process_id = ra::process::GetCurrentProcessId(); @@ -116,6 +116,10 @@ namespace shellanything std::string env_temp = ra::environment::GetEnvironmentVariable("TEMP"); bool has_windows_write_access = CanAccessFolder("C:\\Windows", GENERIC_WRITE); + // print to the console + printf("process.arguments.argc=%d\n", argc); + for(int i=0; i /// Returns 0 when the function succeed. Returns a non zero values otherwise. - int PrintStatistics(); + int PrintProcessSettings(int argc, char **argv); } //namespace shellanything diff --git a/test/main.cpp b/test/main.cpp index 8b594fdd..735c7d6f 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -44,9 +44,9 @@ using namespace ra; int main(int argc, char **argv) { // Look for custom command line arguments - bool has_printstatistics = ra::cli::ParseArgument("printstatistics", std::string(), argc, argv); - if (has_printstatistics) - return shellanything::PrintStatistics(); + bool has_PrintProcessSettings = ra::cli::ParseArgument("PrintProcessSettings", std::string(), argc, argv); + if (has_PrintProcessSettings) + return shellanything::PrintProcessSettings(argc, argv); // Prepare Google's logging library. fLB::FLAGS_logtostderr = false; //on error, print to stdout instead of stderr From 2a1452913d684b0456f87c2623ce9684f5df8b1e Mon Sep 17 00:00:00 2001 From: Antoine Date: Sat, 7 Nov 2020 10:58:28 -0500 Subject: [PATCH 6/6] Modified TestActionExecute.testVerb() to use shellanything_unittest.exe for debugging instead of cmd.exe which does not well with 'runas' verb. --- test/TestActionExecute.cpp | 128 ++++++++++++++++++++++++++----------- 1 file changed, 89 insertions(+), 39 deletions(-) diff --git a/test/TestActionExecute.cpp b/test/TestActionExecute.cpp index f11a49c7..a07f7c89 100644 --- a/test/TestActionExecute.cpp +++ b/test/TestActionExecute.cpp @@ -31,8 +31,19 @@ #include "rapidassist/user.h" #include "rapidassist/timing.h" #include "rapidassist/environment.h" +#include "rapidassist/process.h" +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN 1 +#endif #include +#undef GetEnvironmentVariable +#undef DeleteFile +#undef CreateDirectory +#undef CopyFile +#undef CreateFile +#undef GetCurrentDirectory + namespace shellanything { namespace test { @@ -58,7 +69,7 @@ namespace shellanything { namespace test c.RegisterProperties(); - //execute the action + //Execute the action ActionExecute ae; ae.SetPath("C:\\Windows\\System32\\calc.exe"); ae.SetBaseDir(""); @@ -67,7 +78,7 @@ namespace shellanything { namespace test bool executed = ae.Execute(c); ASSERT_TRUE( executed ); - //cleanup + //Cleanup ra::timing::Millisleep(500); system("cmd.exe /c taskkill /IM calc.exe >NUL 2>NUL"); } @@ -86,7 +97,7 @@ namespace shellanything { namespace test std::string home_dir = ra::user::GetHomeDirectory(); - //execute the action + //Execute the action ActionExecute ae; ae.SetPath("calc.exe"); ae.SetBaseDir("C:\\Windows\\System32"); @@ -95,7 +106,7 @@ namespace shellanything { namespace test bool executed = ae.Execute(c); ASSERT_TRUE( executed ); - //cleanup + //Cleanup ra::timing::Millisleep(500); system("cmd.exe /c taskkill /IM calc.exe >NUL 2>NUL"); } @@ -117,7 +128,7 @@ namespace shellanything { namespace test 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 + //Execute the action ActionExecute ae; ae.SetPath("cmd.exe"); ae.SetBaseDir(temp_dir); @@ -127,7 +138,7 @@ namespace shellanything { namespace test ASSERT_TRUE( executed ); //Wait for the copy to complete, with a timeout - static const double timeout_time = 5000; //ms + static const double timeout_time = 50; //seconds bool file_copied = false; double timer_start = ra::timing::GetMillisecondsTimer(); double time_elapsed = ra::timing::GetMillisecondsTimer() - timer_start; @@ -141,12 +152,23 @@ namespace shellanything { namespace test //Validate arguments ASSERT_TRUE(file_copied); - //cleanup + //Cleanup ra::filesystem::DeleteFileUtf8(destination_path.c_str()); } //-------------------------------------------------------------------------------------------------- TEST_F(TestActionExecute, testVerb) { + //-------------------------------------------------------------------- + //Note: + // The cmd.exe cannot be used for testing the verb and the basedir functionalities at the same time. + // Whenever cmd.exe detects it's running elevated, it ignores its launch parameters and always starts in %SystemRoot%\System32. You cannot override this behavior. + // For this reason, a command such as `net session >nul 2>&1` cannot be used to detect if the process is elevated or not. + // + // See the following for details: + // https://stackoverflow.com/questions/38033790/shellexecute-a-cmd-prompt-with-a-specific-working-directory + //-------------------------------------------------------------------- + + //Skip this test if run on AppVeyor as it requires administrative (elevated) privileges. if (ra::testing::IsAppVeyor() || ra::testing::IsJenkins() || @@ -166,28 +188,18 @@ namespace shellanything { namespace test c.RegisterProperties(); + std::string self_path = ra::process::GetCurrentProcessPath(); 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 + std::string output_filename = "shellanything_unittest.ProcessSettings.txt"; + std::string output_file_path = temp_dir + "\\" + output_filename; + std::string arguments = "--PrintProcessSettings --foobar"; + + //Cleanup + ra::filesystem::DeleteFileUtf8(output_file_path.c_str()); + + //Execute the action ActionExecute ae; - ae.SetPath("cmd.exe"); + ae.SetPath(self_path); ae.SetBaseDir(temp_dir); ae.SetArguments(arguments); ae.SetVerb("runas"); @@ -196,13 +208,13 @@ namespace shellanything { namespace test ASSERT_TRUE( executed ); //Wait for the operation to complete, with a timeout - static const double timeout_time = 5000; //ms + static const double timeout_time = 50; //seconds 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()); + result_file_found = ra::filesystem::FileExists(output_file_path.c_str()); ra::timing::Millisleep(500); //allow process to complete time_elapsed = ra::timing::GetMillisecondsTimer() - timer_start; //evaluate elapsed time again } @@ -210,17 +222,55 @@ namespace shellanything { namespace test //Validate arguments ASSERT_TRUE(result_file_found); - //Read the result file + //Read the result file, search for the elevated process signature. 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()); + ASSERT_TRUE( ra::filesystem::ReadTextFile(output_file_path, result) ); + static const std::string PROCESS_ELEVATED_SIGNATURE = "process.elevated=1"; + ASSERT_NE(result.find(PROCESS_ELEVATED_SIGNATURE), std::string::npos); + + //Cleanup + ra::filesystem::DeleteFileUtf8(output_file_path.c_str()); + } + //-------------------------------------------------------------------------------------------------- + TEST_F(TestActionExecute, DISABLED_demoAdminCommandPromptHere) + { + //---------------------------------- + //Note: + // This is not actually a test but a demontration + // of a "Open Adminitrator command prompt here" command. + //---------------------------------- + + + //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(); + + std::string home_dir = ra::user::GetHomeDirectory(); + + //Create a valid context + Context c; + Context::ElementList elements; + elements.push_back(home_dir); + c.SetElements(elements); + + c.RegisterProperties(); + + //Execute the action as explained in https://stackoverflow.com/questions/38033790/shellexecute-a-cmd-prompt-with-a-specific-working-directory + ActionExecute ae; + ae.SetPath("cmd.exe"); + ae.SetBaseDir(home_dir); + ae.SetArguments("/k cd /d ${selection.path}"); + ae.SetVerb("runas"); + + bool executed = ae.Execute(c); + ASSERT_TRUE( executed ); } //--------------------------------------------------------------------------------------------------