Modern App Automation
I recently was assigned the task of doing a POC on Modern App Automation, specifically that are to do with application state management. For instance things like,
- Activating the App.
- Suspending/Resuming/Terminating an app.
- App Bar activation.
- Snapping the app.
- Activate Sharing etc.
I started off with the unmanaged code presented in the blog post https://blogs.msdn.com/b/windowsappdev/archive/2012/09/04/automating-the-testing-of-windows-8-apps.aspx
--------------------------------------------------------------------------------------------------------------------------------
HRESULT LaunchApp(const std::wstring& strAppUserModelId, PDWORD pdwProcessId)
{
CComPtr<IApplicationActivationManager> spAppActivationManager;
HRESULT hrResult = E_INVALIDARG;
if (!strAppUserModelId.empty())
{
// Instantiate IApplicationActivationManager
hrResult = CoCreateInstance(CLSID_ApplicationActivationManager,
NULL,
CLSCTX_LOCAL_SERVER,
IID_IApplicationActivationManager,
(LPVOID*)&spAppActivationManager);
if (SUCCEEDED(hrResult))
{
// This call ensures that the app is launched as the foreground window
hrResult = CoAllowSetForegroundWindow(spAppActivationManager, NULL);
// Launch the app
if (SUCCEEDED(hrResult))
{
hrResult = spAppActivationManager->ActivateApplication(strAppUserModelId.c_str(),
NULL,
AO_NONE,
pdwProcessId);
}
}
}
return hrResult;
}
int _tmain(int argc, _TCHAR* argv[])
{
HRESULT hrResult = S_OK;
if (SUCCEEDED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)))
{
if (argc == 2)
{
DWORD dwProcessId = 0;
++argv;
hrResult = LaunchApp(*argv, &dwProcessId);
}
else
{
hrResult = E_INVALIDARG;
}
CoUninitialize();
}
return hrResult;
}
---------------------------------------------------------------------------------------------------------------------------------
One of the requirements was to be able to execute this from managed code. I did some reading on how to call Unmanaged COM Code from managed code and decided to try the below options.
- PINVOKE
- COM Class Wrappers. (https://msdn.microsoft.com/en-us/library/aa645736%28v=VS.71%29.aspx)
This article documents my efforts in getting the above piece of code executed from managed code using PINVOKE. I am not familiar with COM & Unmanaged Code (C++). I had to do a lot of learning before I attempted this.Some of the things that I write here can be very basic (as I mentioned this is my first try at Unmanaged COM code).
- I started by creating a C++ Dynamic Linked Library project called MAF32.
- Refactored the above code,
-
- Moved everything into a single method (Moved the logic with the main method into the Launch app method.)
- Updated the first parameter type from constant pointer to std::wstring to LPWSTR. This was done to facilitate marshaling of managed types while using PINVOKE.
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
HRESULT LaunchApp(LPWSTR strAppUserModelId, PDWORD pdwProcessId)
{
HRESULT hrResult =
S_OK;
if (SUCCEEDED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)))
{
CComPtr<IApplicationActivationManager> spAppActivationManager;
HRESULT hrResult = E_INVALIDARG;
size_t strlength = wcslen(strAppUserModelId);
if (strlength != 0)
{
// Instantiate IApplicationActivationManager
hrResult = CoCreateInstance(CLSID_ApplicationActivationManager,
NULL,
CLSCTX_LOCAL_SERVER,
IID_IApplicationActivationManager,
(LPVOID*)&spAppActivationManager);
if (SUCCEEDED(hrResult))
{
// This call ensures that the app is launched as the foreground window
hrResult = CoAllowSetForegroundWindow(spAppActivationManager,
NULL);
// Launch the app
if (SUCCEEDED(hrResult))
{
hrResult = spAppActivationManager->ActivateApplication(strAppUserModelId,
NULL,
AO_NONE,
pdwProcessId);
}
}
}
CoUninitialize();
}
return hrResult;
}
---------------------------------------------------------------------------------------------------------------------------------------------------------
In short, the above COM code does the following.
- Initializes a COM instance (COM must be initialized for every thread that executes COM code).
- Creates an instance of the COM interface IApplicationActivationManager.
- Calls the activate application method.
- Un-Initializes the COM instance
3. Created a C# Console application project and added the following code which allows us to call the LaunchApp method from the Unmanaged dll (MAF32.dll).
[DllImport("MAF32.dll")] //Note: Name of the C++ dll that we created in Step 1.
public static extern int LaunchApp( //Note: Name of the method must be exactly same as the one defined in C++ code.
string processIdentifier,
out int processId); //The C++ code takes a pointer to DWORD as parameter. In C#, I am passing reference (note the out keyword) to a integer as a parameter.
4. Now in the Console application's Main method, I called the method as follows.
uint processId;
PlatformInvokeTest.LaunchApp(@"Microsoft.BingTravel_8wekyb3d8bbwe!AppexTravel", out processId);
Console.WriteLine(processId);
Note: For this to work, the unmanaged dll created in step1 (MAF32.dll) must be present in the same directory as that of the C# application.
5. On executing the managed code, I got "EntryPointNotFoundException".
6. I downloaded the Depends.exe (SysInternals tool) to see the methods exposed by MAF32.dll) .
Even though "MAF.dll" contains a method call LaunchApp, it is not visible to the clients. In order to make this method visible to the clients we must do one of the following
- Explicitly define the dll's interface using the dll export attribute. https://msdn.microsoft.com/en-us/library/3y1sfaz2(v=VS.80).aspx
- Use Module definition (.def) files to specify the methods that are exposed by the dll. https://msdn.microsoft.com/en-us/library/d91k01sh.aspx
I chose to follow the first method. So I made the below changes.
- Added the code "#define DllExport extern "C" __declspec( dllexport )" to the code.
- Decorated the LanuchApp method with the DllExport keyword.
Now on running the depends tool once again, you can see the LaunchApp method.
7. I re-executed the C# Console application. I no longer got the EntryPointNotFound exception. However the method doesn't launch the app as expected. On debugging I found that the call to the method
if(SUCCEEDED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)))
always returns a failure and the rest of the code is not being executed. The CoInitializeEx method returns the error code "RPC_E_CHANGED_MODE" which means the COM has been already initialized on that thread in a different mode. From .Net 2.0 onwards CLR initializes COM on every Dot net thread in Multi Threaded Apartment state by default. The C++ code is trying to initialize it in Single threaded apartment state, Hence the error.
I have removed the call to CoInitializeEx and CoUninitialize since this is already handle by CLR.
8. On further execution, the call to the ActivateApplication method return the error code "-2144927148" (0x80270254) which is "E_APPLICATION_NOT_REGISTERED" . After debugging for quiet sometime and trial and error, I figured out that this was because of marshaling issues. I changed the below marshaling code as follows,
[DllImport("MAF32.dll")]
public static extern int LaunchApp(
[In, MarshalAs(UnmanagedType.LPWStr)]string processIdentifier,
[Out, MarshalAs(UnmanagedType.U4)] out int processId);
For more info on marshaling see
https://msdn.microsoft.com/en-us/library/aa288468(v=VS.71).aspx#pinvoke_defaultmarshaling
https://msdn.microsoft.com/en-us/library/system.runtime.interopservices.unmanagedtype(v=vs.71).aspx
After this I was able to launch the application from managed code.
Final Code for launching a modern app from Managed code is below.
*************Unmanaged code in MAF32.cpp************************
#include "stdafx.h"
#include "stdafx.h"
#include <shlobj.h>
#include <stdio.h>
#include <shobjidl.h>
#include <objbase.h>
#include <atlbase.h>
#include <string>
#define DllExport extern "C" __declspec( dllexport )
DllExport HRESULT LaunchApp(LPWSTRstrAppUserModelId, PDWORDpdwProcessId)
{
HRESULT hrResult = E_INVALIDARG;
CComPtr<IApplicationActivationManager> spAppActivationManager;
size_t strlength = wcslen(strAppUserModelId);
if (strlength != 0)
{
hrResult = CoCreateInstance(
CLSID_ApplicationActivationManager,
NULL,
CLSCTX_LOCAL_SERVER,
IID_IApplicationActivationManager,
(LPVOID*)&spAppActivationManager);
if (SUCCEEDED(hrResult))
{
hrResult = CoAllowSetForegroundWindow(
spAppActivationManager,
NULL);
if (SUCCEEDED(hrResult))
{
hrResult = spAppActivationManager->ActivateApplication(
strAppUserModelId,
NULL,
AO_NONE,
pdwProcessId);
}
}
}
return hrResult;
}
**********************************************************************************************************************************
***************Managed Code ********************************************************************
class Program
{
static void Main(string[] args)
{
int processId;
Console.WriteLine(PlatformInvokeTest.LaunchApp(@"Microsoft.BingTravel_8wekyb3d8bbwe!AppexTravel", out processId));
Console.WriteLine(processId);
}
}
public class PlatformInvokeTest
{
[DllImport("MAF32.dll")]
public static extern int LaunchApp(
[In, MarshalAs(UnmanagedType.LPWStr)]string processIdentifier,
[Out, MarshalAs(UnmanagedType.U4)] outint processId);
}
Similar code can then be used to perform other application level activities from managed code.
Comments
- Anonymous
November 26, 2012
Great post - could you share a compiled DLL (I am NOT a C++ developer at all) ?