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.

 

  1. PINVOKE
  2. 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).

 

  1. I started by creating a C++ Dynamic Linked Library project called MAF32.
  2. 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

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.marshalasattribute(v=vs.71).aspx

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.