Skip to content

Commit bff3894

Browse files
committed
Modified ActionExecute::Execute() to delegate to ExecuteVerb() or ExecuteProcess(). Created unit tests.
1 parent 23e3df9 commit bff3894

File tree

5 files changed

+325
-14
lines changed

5 files changed

+325
-14
lines changed

include/shellanything/ActionExecute.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,21 @@ namespace shellanything
8787
void SetVerb(const std::string& iVerb);
8888

8989
private:
90+
/// <summary>
91+
/// Execute an application with ShellExecuteEx method.
92+
/// This execute method supports verbs.
93+
/// </summary>
94+
/// <param name="iContext">The current context of execution.</param>
95+
/// <returns>Returns true if the execution is successful. Returns false otherwise.</returns>
96+
virtual bool ExecuteVerb(const Context & iContext) const;
97+
9098
/// <summary>
9199
/// Execute an application with RapidAssist method.
100+
/// This execute method does not supports verbs.
92101
/// </summary>
93102
/// <param name="iContext">The current context of execution.</param>
94103
/// <returns>Returns true if the execution is successful. Returns false otherwise.</returns>
95-
virtual bool StartProcess(const Context & iContext) const;
104+
virtual bool ExecuteProcess(const Context & iContext) const;
96105

97106
private:
98107
std::string mPath;

src/ActionExecute.cpp

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ namespace shellanything
4747
}
4848

4949
bool ActionExecute::Execute(const Context& iContext) const
50+
{
51+
PropertyManager& pmgr = PropertyManager::GetInstance();
52+
std::string verb = pmgr.Expand(mVerb);
53+
54+
//If a verb was specified, delegate to VerbExecute(). Otherwise, use ProcessExecute().
55+
if (verb.empty())
56+
return ExecuteProcess(iContext);
57+
else
58+
return ExecuteVerb(iContext);
59+
}
60+
61+
bool ActionExecute::ExecuteVerb(const Context& iContext) const
5062
{
5163
PropertyManager& pmgr = PropertyManager::GetInstance();
5264
std::string path = pmgr.Expand(mPath);
@@ -71,35 +83,44 @@ namespace shellanything
7183
info.nShow = SW_SHOWDEFAULT;
7284
info.lpFile = pathW.c_str();
7385

86+
//Print execute values in the logs
7487
LOG(INFO) << "Exec: '" << path << "'.";
75-
7688
if (!verb.empty())
7789
{
7890
info.lpVerb = verbW.c_str(); // Verb
7991
LOG(INFO) << "Verb: '" << verb << "'.";
8092
}
81-
8293
if (!arguments.empty())
8394
{
8495
info.lpParameters = argumentsW.c_str(); // Arguments
8596
LOG(INFO) << "Arguments: '" << arguments << "'.";
8697
}
87-
8898
if (!basedir.empty())
8999
{
90100
info.lpDirectory = basedirW.c_str(); // Default directory
91101
LOG(INFO) << "Basedir: '" << basedir << "'.";
92102
}
93103

94-
BOOL success = ShellExecuteExW(&info);
95-
return (success == TRUE);
104+
//Execute and get the pid
105+
bool success = (ShellExecuteExW(&info) == TRUE);
106+
if (!success)
107+
return false;
108+
DWORD pId = GetProcessId(info.hProcess);
109+
110+
success = (pId != ra::process::INVALID_PROCESS_ID);
111+
if (success)
112+
{
113+
LOG(INFO) << "Process created. PID=" << pId;
114+
}
115+
116+
return success;
96117
}
97118

98-
bool ActionExecute::StartProcess(const Context & iContext) const
119+
bool ActionExecute::ExecuteProcess(const Context & iContext) const
99120
{
100-
PropertyManager & pmgr = PropertyManager::GetInstance();
101-
std::string path = pmgr.Expand(mPath);
102-
std::string basedir = pmgr.Expand(mBaseDir);
121+
PropertyManager& pmgr = PropertyManager::GetInstance();
122+
std::string path = pmgr.Expand(mPath);
123+
std::string basedir = pmgr.Expand(mBaseDir);
103124
std::string arguments = pmgr.Expand(mArguments);
104125

105126
bool basedir_missing = basedir.empty();
@@ -144,20 +165,29 @@ namespace shellanything
144165
LOG(WARNING) << "attribute 'basedir' not specified.";
145166
}
146167

147-
//debug
168+
//Print execute values in the logs
169+
LOG(INFO) << "Exec: '" << path << "'.";
170+
if (!arguments.empty())
171+
{
172+
LOG(INFO) << "Arguments: '" << arguments << "'.";
173+
}
174+
if (!basedir.empty())
175+
{
176+
LOG(INFO) << "Basedir: '" << basedir << "'.";
177+
}
178+
179+
//Execute and get the pid
148180
uint32_t pId = ra::process::INVALID_PROCESS_ID;
149181
if (arguments_missing)
150182
{
151-
LOG(INFO) << "Running '" << path << "' from directory '" << basedir << "'.";
152183
pId = ra::process::StartProcessUtf8(path, basedir);
153184
}
154185
else
155186
{
156-
LOG(INFO) << "Running '" << path << "' from directory '" << basedir << "' with arguments '" << arguments << "'.";
157187
pId = ra::process::StartProcessUtf8(path, basedir, arguments);
158188
}
159189

160-
bool success = pId != ra::process::INVALID_PROCESS_ID;
190+
bool success = (pId != ra::process::INVALID_PROCESS_ID);
161191
if (success)
162192
{
163193
LOG(INFO) << "Process created. PID=" << pId;

test/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ add_executable(shellanything_unittest
4848
${CONFIGURATION_TEST_FILES}
4949
${SHELLANYTHING_PRIVATE_FILES}
5050
main.cpp
51+
TestActionExecute.cpp
52+
TestActionExecute.h
5153
TestActionFile.cpp
5254
TestActionFile.h
5355
TestBitmapCache.cpp

test/TestActionExecute.cpp

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/**********************************************************************************
2+
* MIT License
3+
*
4+
* Copyright (c) 2018 Antoine Beauchamp
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*********************************************************************************/
24+
25+
#include "TestActionExecute.h"
26+
#include "shellanything/Context.h"
27+
#include "shellanything/ActionExecute.h"
28+
#include "PropertyManager.h"
29+
#include "rapidassist/testing.h"
30+
#include "rapidassist/filesystem_utf8.h"
31+
#include "rapidassist/user.h"
32+
#include "rapidassist/timing.h"
33+
#include "rapidassist/environment.h"
34+
35+
#include <Windows.h>
36+
37+
namespace shellanything { namespace test
38+
{
39+
40+
//--------------------------------------------------------------------------------------------------
41+
void TestActionExecute::SetUp()
42+
{
43+
}
44+
//--------------------------------------------------------------------------------------------------
45+
void TestActionExecute::TearDown()
46+
{
47+
}
48+
//--------------------------------------------------------------------------------------------------
49+
TEST_F(TestActionExecute, testBasic)
50+
{
51+
PropertyManager & pmgr = PropertyManager::GetInstance();
52+
53+
//Create a valid context
54+
Context c;
55+
Context::ElementList elements;
56+
elements.push_back("C:\\Windows\\System32\\calc.exe");
57+
c.SetElements(elements);
58+
59+
c.RegisterProperties();
60+
61+
//execute the action
62+
ActionExecute ae;
63+
ae.SetPath("C:\\Windows\\System32\\calc.exe");
64+
ae.SetBaseDir("");
65+
ae.SetArguments("");
66+
67+
bool executed = ae.Execute(c);
68+
ASSERT_TRUE( executed );
69+
70+
//cleanup
71+
ra::timing::Millisleep(500);
72+
system("cmd.exe /c taskkill /IM calc.exe >NUL 2>NUL");
73+
}
74+
//--------------------------------------------------------------------------------------------------
75+
TEST_F(TestActionExecute, testBaseDir)
76+
{
77+
PropertyManager & pmgr = PropertyManager::GetInstance();
78+
79+
//Create a valid context
80+
Context c;
81+
Context::ElementList elements;
82+
elements.push_back("C:\\Windows\\System32\\calc.exe");
83+
c.SetElements(elements);
84+
85+
c.RegisterProperties();
86+
87+
std::string home_dir = ra::user::GetHomeDirectory();
88+
89+
//execute the action
90+
ActionExecute ae;
91+
ae.SetPath("calc.exe");
92+
ae.SetBaseDir("C:\\Windows\\System32");
93+
ae.SetArguments("");
94+
95+
bool executed = ae.Execute(c);
96+
ASSERT_TRUE( executed );
97+
98+
//cleanup
99+
ra::timing::Millisleep(500);
100+
system("cmd.exe /c taskkill /IM calc.exe >NUL 2>NUL");
101+
}
102+
//--------------------------------------------------------------------------------------------------
103+
TEST_F(TestActionExecute, testArguments)
104+
{
105+
PropertyManager & pmgr = PropertyManager::GetInstance();
106+
107+
//Create a valid context
108+
Context c;
109+
Context::ElementList elements;
110+
elements.push_back("C:\\Windows\\System32\\calc.exe");
111+
c.SetElements(elements);
112+
113+
c.RegisterProperties();
114+
115+
std::string home_dir = ra::user::GetHomeDirectory();
116+
std::string temp_dir = ra::filesystem::GetTemporaryDirectory();
117+
std::string destination_path = temp_dir + "\\my_calc.exe";
118+
std::string arguments = "/c copy C:\\Windows\\System32\\calc.exe " + destination_path + ">NUL 2>NUL";
119+
120+
//execute the action
121+
ActionExecute ae;
122+
ae.SetPath("cmd.exe");
123+
ae.SetBaseDir(temp_dir);
124+
ae.SetArguments(arguments);
125+
126+
bool executed = ae.Execute(c);
127+
ASSERT_TRUE( executed );
128+
129+
//Wait for the copy to complete, with a timeout
130+
static const double timeout_time = 5000; //ms
131+
bool file_copied = false;
132+
double timer_start = ra::timing::GetMillisecondsTimer();
133+
double time_elapsed = ra::timing::GetMillisecondsTimer() - timer_start;
134+
while(!file_copied && time_elapsed <= timeout_time)
135+
{
136+
file_copied = ra::filesystem::FileExists(destination_path.c_str());
137+
ra::timing::Millisleep(500); //allow process to complete
138+
time_elapsed = ra::timing::GetMillisecondsTimer() - timer_start; //evaluate elapsed time again
139+
}
140+
141+
//Validate arguments
142+
ASSERT_TRUE(file_copied);
143+
144+
//cleanup
145+
ra::filesystem::DeleteFileUtf8(destination_path.c_str());
146+
}
147+
//--------------------------------------------------------------------------------------------------
148+
TEST_F(TestActionExecute, testVerb)
149+
{
150+
//Skip this test if run on AppVeyor as it requires administrative (elevated) privileges.
151+
if (ra::testing::IsAppVeyor() ||
152+
ra::testing::IsJenkins() ||
153+
ra::testing::IsTravis())
154+
{
155+
printf("Skipping tests as it requires administrative (elevated) privileges.\n");
156+
return;
157+
}
158+
159+
PropertyManager & pmgr = PropertyManager::GetInstance();
160+
161+
//Create a valid context
162+
Context c;
163+
Context::ElementList elements;
164+
elements.push_back("C:\\Windows\\System32\\calc.exe");
165+
c.SetElements(elements);
166+
167+
c.RegisterProperties();
168+
169+
std::string temp_dir = ra::filesystem::GetTemporaryDirectory();
170+
std::string batch_file_filename = ra::testing::GetTestQualifiedName() + ".bat";
171+
std::string result_file_filename = ra::testing::GetTestQualifiedName() + ".txt";
172+
std::string batch_file_path = temp_dir + "\\" + batch_file_filename;
173+
std::string result_file_path = temp_dir + "\\" + result_file_filename;
174+
std::string arguments = "/c " + batch_file_filename + " >" + result_file_filename;
175+
176+
//This is a batch file that prints ADMIN if it is run in elevated mode or prints FAIL otherwise.
177+
//Inspired from https://stackoverflow.com/questions/4051883/batch-script-how-to-check-for-admin-rights
178+
const std::string content =
179+
"@echo off\n"
180+
"net session >nul 2>&1\n"
181+
"if %errorLevel% == 0 (\n"
182+
" echo ADMIN\n"
183+
") else (\n"
184+
" echo FAIL\n"
185+
")\n";
186+
ASSERT_TRUE( ra::filesystem::WriteTextFile(batch_file_path, content) );
187+
188+
//execute the action
189+
ActionExecute ae;
190+
ae.SetPath("cmd.exe");
191+
ae.SetBaseDir(temp_dir);
192+
ae.SetArguments(arguments);
193+
ae.SetVerb("runas");
194+
195+
bool executed = ae.Execute(c);
196+
ASSERT_TRUE( executed );
197+
198+
//Wait for the operation to complete, with a timeout
199+
static const double timeout_time = 5000; //ms
200+
bool result_file_found = false;
201+
double timer_start = ra::timing::GetMillisecondsTimer();
202+
double time_elapsed = ra::timing::GetMillisecondsTimer() - timer_start;
203+
while(!result_file_found && time_elapsed <= timeout_time)
204+
{
205+
result_file_found = ra::filesystem::FileExists(result_file_path.c_str());
206+
ra::timing::Millisleep(500); //allow process to complete
207+
time_elapsed = ra::timing::GetMillisecondsTimer() - timer_start; //evaluate elapsed time again
208+
}
209+
210+
//Validate arguments
211+
ASSERT_TRUE(result_file_found);
212+
213+
//Read the result file
214+
std::string result;
215+
ASSERT_TRUE( ra::filesystem::ReadTextFile(result_file_path, result) );
216+
ra::strings::Replace(result, ra::environment::GetLineSeparator(), "");
217+
ra::strings::Replace(result, "\n", "");
218+
static const std::string EXPECTED_RESULT = "ADMIN";
219+
ASSERT_EQ(EXPECTED_RESULT, result);
220+
221+
//cleanup
222+
ra::filesystem::DeleteFileUtf8(batch_file_path.c_str());
223+
ra::filesystem::DeleteFileUtf8(result_file_path.c_str());
224+
}
225+
//--------------------------------------------------------------------------------------------------
226+
227+
} //namespace test
228+
} //namespace shellanything

0 commit comments

Comments
 (0)