Showing posts with label WinAPI. Show all posts
Showing posts with label WinAPI. Show all posts

Wednesday, 27 November 2019

C++, VBA, WinApi - Signal another process with WinApi Synchronization Event

In this post I give code where a C++ program signals to a VBA program running in a separate process. This demonstrates inter-process communication (IPC) using the native Windows API (and not related to COM).

I do like playing with the Windows API and in this instance I am playing with a Windows API Event which is used for synchronization; it is not to be confused with COM events. A good definition of an Event is given by this doc page ...

Applications can use event objects in a number of situations to notify a waiting thread of the occurrence of an event. For example, overlapped I/O operations on files, named pipes, and communications devices use an event object to signal their completion.

I needed this for a use case where I wanted to signal to a child process to terminate. So instead of using brute force to terminate the child process, I signal an event which is periodically inspected and if signalled the child process will terminate cleanly and voluntarily.

Before we get to that use case (which I intend to post shortly) I wanted to post the details of ...

In the first (C++) process that hosts the event...
  • Creating an event, including creating a security descriptor.
  • Setting the event
  • Resetting the event
In the second (VBA) process...
  • Opening the event
  • Waiting for event

Code for the C++ process that hosts the event

I have moved some code the error reporting code to Appendix A, the remainder of code is relevant to the teaching point of this post.

The first thing required is to create a security descriptor object (of type SECURITY_DESCRIPTOR). The security descriptor is created on the heap using LocalAlloc and so has to be freed before the program exits; then it has to be initialized with a call to InitializeSecurityDescriptor(). Next an access control list (ACL) has to be set for the security descriptor or not! I have saved plenty of code by opting not to use an access control list (ACL). I have seen plenty of voluminous code which does program an ACL but I did not want to distract from the main teaching point. In production you'd likely want to use an ACL to secure the Event object. I'm skipping the ACL and so I pass NULL into 3rd argument of SetSecurityDescriptorDacl() and set the 2nd argument to FALSE to show I intended not to pass an ACL.

Then the security descriptor is added to a parent structure called security attributes (of type SECURITY_ATTRIBUTES).

It is worth stressing that in the post the bulk of the headaches in getting this to work was with the security. Once the security attributes structure is created (with its nested security descriptor) then we are good to start calling CreatEvent() which in comparison is straightforward. The call to CreatEvent() specifies that the Event object is to be signalled manually and that its initial state is False, i.e. not signalled/not set. The final argument to CreatEvent() is the name of the object.

After creating the event there is a line of code to SetEvent() which sets or signals the event. Then, the Event object needs to be reset and so the call to ResetEvent. If you are playing along then you will want to place a breakpoint on the SetEvent() line of code.

So the following code can be pasted into a C++ console app.

#include "pch.h"
#include <iostream>
#include <Windows.h>
#include <strsafe.h>

void ErrorExit(LPCTSTR lpszFunction)
{
 // Omitted , see Appendix A
}

//https://docs.microsoft.com/en-us/windows/win32/secauthz/creating-a-security-descriptor-for-a-new-object-in-c--
//https://stackoverflow.com/questions/49253537/the-createfilemapping-is-failed-with-security-attributes-the-revision-level-is

int main()
{
 PSECURITY_DESCRIPTOR pSD = NULL;
 // Initialize a security descriptor.  
 pSD = (PSECURITY_DESCRIPTOR)LocalAlloc(LPTR,
  SECURITY_DESCRIPTOR_MIN_LENGTH);
 if (NULL == pSD)
 {
  ErrorExit(TEXT("LocalAlloc"));
 }

 if (NULL == InitializeSecurityDescriptor(pSD, SECURITY_DESCRIPTOR_REVISION))
 {
  ErrorExit(TEXT("InitializeSecurityDescriptor"));
 }

 // Add the ACL to the security descriptor. 
 if (!SetSecurityDescriptorDacl(pSD,
  FALSE,     // bDaclPresent flag   
  NULL,      // We're passing NULL to say there is no Access Control List for this object
  FALSE))   // not a default DACL 
 {
  ErrorExit(TEXT("SetSecurityDescriptorDacl"));
 }

 SECURITY_ATTRIBUTES sa;
 // Initialize a security attributes structure.
 sa.nLength = sizeof(SECURITY_ATTRIBUTES);
 sa.lpSecurityDescriptor = pSD;
 sa.bInheritHandle = FALSE;

 /*
  * Use this following line to abolish any mention of security attributes/descriptors
  * HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, TEXT("C++ExcelVBAIPC"));
  */
 HANDLE hEvent = CreateEvent(&sa, TRUE, FALSE, TEXT("C++ExcelVBAIPC"));
 if (0 == hEvent)
 {
  ErrorExit(TEXT("CreateEvent"));
 }
 else
 {
  // PLACE BREAKPOINT HERE TO EXPERIMENT WITH A CLIENT CALLING WaitForSingleObject
  SetEvent(hEvent);
 }
 ResetEvent(hEvent);
 CloseHandle(hEvent);
        std::cout << "Finished!\n"; 

 // free any allocated heap memory
 if (pSD)
  LocalFree(pSD);

}

Code for the VBA ++ process that waits on the event

The following VBA code can sit in Excel or Word. It requires the GetSystemErrorMessageText module from Chip Pearson to give nice error messages for any errors encountered using the WinAPI, I recommend!

The code is relatively simple. It opens an event with OpenEvent() specifying SYNCHRONIZE permissions only. Then the code calls WaitForSingleObject(). In this code we pass -1 which means wait indefinitely but in a more realistic scenario one would pass a positive integer of milliseconds for how long to wait.

If you are playing along with the breakpoint in the C++ code then the VBA code will wait forever (hang) so do please save your work! The VBA code will progress once the event object is signalled on the C++ side, so switch back to the C++ code and make it continue then the VBA code will continue. If running the experiment multiple times don't forget to let the C++ side call ResetEvent (or pass the C++ CloseHandle() statement) otherwise the Event object will remain signalled.

On detroying Event objects a good quote here from this page ...

Use the CloseHandle function to close the handle. The system closes the handle automatically when the process terminates. The event object is destroyed when its last handle has been closed.
Option Explicit

Private Const SYNCHRONIZE As Long = &H100000

Private Declare Function CloseHandle Lib "kernel32.dll" (ByVal hObject As Long) As Long

Private Declare Function OpenEvent Lib "kernel32.dll" Alias "OpenEventA" (ByVal dwDesiredAccess As Long, _
        ByVal bInheritHandle As Long, ByVal lpName As String) As Long

Private Declare Function WaitForSingleObject Lib "kernel32" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long

Private Enum eWaitResult
    WAIT_ABANDONED = &H80       'The specified object is a mutex object that was not released by the thread that owned the mutex
    WAIT_OBJECT_0 = &H0         'The state of the specified object is signaled.
    WAIT_TIMEOUT = &H102        'The time-out interval elapsed, and the object's state is nonsignaled.
    WAIT_FAILED = &HFFFFFFFF    'The function has failed. To get extended error information, call GetLastError.
End Enum

Private hEvent As Long

Sub OpenEventAndWait()

    hEvent = OpenEvent(SYNCHRONIZE, 0, "C++ExcelVBAIPC")
    If hEvent = 0 Then
        Debug.Print "Failed in call to OpenEvent."

        // requires http://www.cpearson.com/Excel/FormatMessage.aspx
        Debug.Print GetSystemErrorMessageText(Err.LastDllError) 
    Else
        Stop
        Dim dwWaitResult As eWaitResult
        dwWaitResult = WaitForSingleObject(hEvent, -1)
        
        Stop
        Select Case dwWaitResult
        
        Case eWaitResult.WAIT_OBJECT_0
            Debug.Print "The state of the specified object is signaled."
        Case eWaitResult.WAIT_TIMEOUT
            Debug.Print "wait timeout"
            
        Case eWaitResult.WAIT_FAILED
            Debug.Print "wait failed"
            Debug.Print GetSystemErrorMessageText(Err.LastDllError)
        Case eWaitResult.WAIT_ABANDONED
            Debug.Print "wait abandoned"
        End Select
        'Stop
        Call CloseHandle(hEvent)
    End If
    
    'Stop
End Sub

Conclusions and other thoughts

So even this post with its stripped down permissioning requires a lot of explanation. If you feel cheated in that I have skipped code regarding security and access control lists then I recommend this link which has some relevant code.

Now that I have given explanation for this code I can now proceed with the post where I spawn a child process and when I need to terminate it I signal using an Event object. That code will be VBA creating the event whilst a Python script polls the event.

UPDATE: it looks like one can pass NULL instead of a security attributes object into CreateEvent and this also has no permissions. Oh well, I'm not going to strip out the security attributes code because I hope to upgrade it with some ACL code one day. But it does mean the VBA equivalent should be much easier to write and I will slip in another post to show this.

Appendix A - Extra C++ code which calls GetLastError, formats the error message and throws an message box

So this code was taken out of the full C++ listing given above in order not to distract from the main teaching point. Nevertheless it is useful.

//With thanks to https://docs.microsoft.com/en-us/windows/win32/debug/retrieving-the-last-error-code

#include <strsafe.h>

void ErrorExit(LPCTSTR lpszFunction)
{
 // Retrieve the system error message for the last-error code

 LPVOID lpMsgBuf;
 LPVOID lpDisplayBuf;
 DWORD dw = GetLastError();

 FormatMessage(
  FORMAT_MESSAGE_ALLOCATE_BUFFER |
  FORMAT_MESSAGE_FROM_SYSTEM |
  FORMAT_MESSAGE_IGNORE_INSERTS,
  NULL,
  dw,
  MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
  (LPTSTR)&lpMsgBuf,
  0, NULL);

 // Display the error message and exit the process

 lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT,
  (lstrlen((LPCTSTR)lpMsgBuf) + lstrlen((LPCTSTR)lpszFunction) + 40) * sizeof(TCHAR));
 StringCchPrintf((LPTSTR)lpDisplayBuf,
  LocalSize(lpDisplayBuf) / sizeof(TCHAR),
  TEXT("%s failed with error %d: %s"),
  lpszFunction, dw, lpMsgBuf);
 MessageBox(NULL, (LPCTSTR)lpDisplayBuf, TEXT("Error"), MB_OK);

 LocalFree(lpMsgBuf);
 LocalFree(lpDisplayBuf);
 ExitProcess(dw);
}

Sunday, 17 December 2017

WinAPI - there's a difference between Child windows and Popup windows - for controlling visibility at least

So there is currently a SO bounty where poster wants to hide all windows for a program. They launched the program using the Windows Script Host library (c:\Windows\SysWOW64\wshom.ocx). They use the Run method of the Shell object (another link). But despite specifying WshWindowStyle.WshHide, popups still appear. Because of the bounty I decided to investigate, but I started with C# (see bottom of page), then I translated my findings into VBA (see first listing ).

Firstly, a key reading resource is Windows Features which tells that all windows are created with CreateWindowEx but popups are create by specifying WS_POPUP and child windows are created by specifying WS_CHILD. So popups and child windows are different.

On the same page in the section Window Visibility it explains that we can set the visibility of a main window and the change will cascade down to all child windows but there is no mention of this cascade affecting popups.

The short answer to the investigation is that to hide popups it is required to call ShowOwnedPopups(hwnd,0). The VBA declaration is given here


Declare Function ShowOwnedPopups Lib "user32" Alias "ShowOwnedPopups" _
       (ByVal hwnd As Long, ByVal fShow As Long) As Long

And here is some final VBA code but which depends upon a simple C# demo program called VisibilityExperiment

Option Explicit

Private Declare Function ShowOwnedPopups Lib _
    "user32" (ByVal hwnd As Long, _
    ByVal fShow As Long) As Long

Private Declare Function EnumWindows _
    Lib "user32" ( _
        ByVal lpEnumFunc As Long, _
        ByVal lParam As Long) _
        As Long
        
Private Declare Function GetWindowThreadProcessId _
    Lib "user32" (ByVal hwnd As Long, lpdwprocessid As Long) As Long
    

    
Private mlPid As Long
Private mlHWnd As Variant


Private Function EnumAllWindows(ByVal hwnd As Long, ByVal lParam As Long) As Long
    
    Dim plProcID As Long
    GetWindowThreadProcessId hwnd, plProcID
    If plProcID = mlPid Then
        If IsEmpty(mlHWnd) Then
            mlHWnd = hwnd
            Debug.Print "HWnd:&" & Hex$(mlHWnd) & "  PID:&" & Hex$(mlPid) & "(" & mlPid & ")"
        End If
    End If

    EnumAllWindows = True
End Function

Private Function GetPID(ByVal sExe As String) As Long

    Static oServ As Object
    If oServ Is Nothing Then Set oServ = GetObject("winmgmts:\\.\root\cimv2")
    
    Dim cProc As Object
    Set cProc = oServ.ExecQuery("Select * from Win32_Process")
    
    Dim oProc As Object
    For Each oProc In cProc
        If oProc.Name = sExe Then
            Dim lPid As Long
            GetPID = oProc.ProcessID
        End If
    Next

End Function


Private Sub Test()
    '* Tools->References   "Windows Script Host library"

    Dim wsh As IWshRuntimeLibrary.WshShell
    Set wsh = New IWshRuntimeLibrary.WshShell

    Dim lWinStyle As WshWindowStyle
    lWinStyle = WshNormalFocus

    Dim sExe As String
    sExe = "VisibilityExperiment.exe"

    Dim sExeFullPath As String
    sExeFullPath = Environ$("USERPROFILE") & "\source\repos\VisibilityExperiment\VisibilityExperiment\bin\Debug\" & sExe

    Dim x As Long
    x = wsh.Run(sExeFullPath, lWinStyle, False)

    mlPid = GetPID(sExe)

    mlHWnd = Empty
    Call EnumWindows(AddressOf EnumAllWindows, 0)


    Stop
    Call ShowOwnedPopups(mlHWnd, 0)  '* o to hide, 1 to show

End Sub



To repeat, to hide popups one must call ShowOwnedPopups(). Sadly, I cannot see around this restriction. Even if we tried to use the Windows API directly to spawn the process there is nothing in the STARTUPINFO structure (Windows) which looks like it will help, there is nothing to specify the visibility of popups.

Here is the source code of the form to the C# program VisibilityExperiment.exe, it is a normal Windows Forms application and I have added a single button called button1,


using System;
using System.Windows.Forms;

namespace VisibilityExperiment
{
    public partial class VisibilityExperiment : Form
    {
        public VisibilityExperiment()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            MessageBox.Show("message", "caption", MessageBoxButtons.YesNo);
        }
    }
}

And here is my experiment program call VisibilityExperiment2.exe . It is again a simple WinForms Application. You run VisibilityExperiment manually and you can see the effect of calling different Windows API functions. You will see that the message box does will not be hidden unless you use ShowOwnedPopups.

Here is the form layout

.

You will have to rename the controls btnGetMainWindowHandle, btnShow, btnHide, grpShowHide, chkShowOwnedPopups, chkShowWindowAsync, chkSetWindowPos for the following form code to work...


using System;
using System.Diagnostics;
using System.Dynamic;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace VisibilityExperiment2
{


    public partial class ShowerHider : Form
    {
        IntPtr mainWindowHandle;

        [DllImport("user32.dll")]
        static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);

        [DllImport("user32.dll")]
        static extern bool ShowOwnedPopups(IntPtr hWnd, int nCmdShow);

        [DllImport("user32.dll", SetLastError = true)]
        static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, SetWindowPosFlags uFlags);

        #region Constants

        enum SetWindowPosFlags
        {
            NOSIZE = 0x0001,
            NOMOVE = 0x0002,
            NOZORDER = 0x0004,
            NOREDRAW = 0x0008,
            NOACTIVATE = 0x0010,
            DRAWFRAME = 0x0020,
            FRAMECHANGED = 0x0020,
            SHOWWINDOW = 0x0040,
            HIDEWINDOW = 0x0080,
            NOCOPYBITS = 0x0100,
            NOOWNERZORDER = 0x0200,
            NOREPOSITION = 0x0200,
            NOSENDCHANGING = 0x0400,
            DEFERERASE = 0x2000,
            ASYNCWINDOWPOS = 0x4000
        };

        static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
        static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
        static readonly IntPtr HWND_TOP = new IntPtr(0);
        static readonly IntPtr HWND_BOTTOM = new IntPtr(1);

        private const int SW_HIDE = 0;
        private const int SW_SHOWNORMAL = 1;
        private const int SW_SHOW = 5;
        #endregion Constants

        public ShowerHider()
        {
            InitializeComponent();
        }



        private ExpandoObject CheckForPID()
        {
            dynamic retVal = new ExpandoObject();

            Process[] processes = Process.GetProcessesByName("VisibilityExperiment");
            if (processes.Length==0)
            {
                retVal.pid = -1;
                
            } else if (processes.Length == 1) {

                Process proc = processes[0];
                retVal.pid = proc.Id;
                retVal.hWnd = proc.MainWindowHandle;
                

            } else
            {
                retVal.pid = -2;
            }
            return retVal;
        }

        private void btnGetMainWindowHandle_Click(object sender, EventArgs e)
        {
            dynamic retVal = CheckForPID();

            if (retVal.pid>=0)
            {
                mainWindowHandle = retVal.hWnd;
                this.textBox1.Text = retVal.hWnd.ToString("X8");
                this.grpShowHide.Enabled = true;
            }

            
            
        }

        private void btnShow_Click(object sender, EventArgs e)
        {
            if (this.chkSetWindowPos.Checked == true)
            {
                SetWindowPos(mainWindowHandle, IntPtr.Zero, 0, 0, 0, 0,SetWindowPosFlags.SHOWWINDOW | 
                    SetWindowPosFlags.NOMOVE | SetWindowPosFlags.NOSIZE);
            }


            if (this.chkShowWindowAsync.Checked==true)
            { 
                ShowWindowAsync(mainWindowHandle, SW_SHOW);
                //* Need to call this twice of the first time
                //* TODO Eastablish first time versus subsequent time logic 
                ShowWindowAsync(mainWindowHandle, SW_SHOW);
            }

            if (this.chkShowOwnedPopups.Checked == true)
                ShowOwnedPopups(mainWindowHandle, 1);
        }

        private void btnHide_Click(object sender, EventArgs e)
        {

            if (this.chkSetWindowPos.Checked == true)
            {
                SetWindowPos(mainWindowHandle, IntPtr.Zero, 0, 0, 0, 0,SetWindowPosFlags.HIDEWINDOW |
                    SetWindowPosFlags.NOMOVE | SetWindowPosFlags.NOSIZE);
            }

            if (this.chkShowWindowAsync.Checked == true)
            {
                ShowWindowAsync(mainWindowHandle, SW_HIDE);
                //* Need to call this twice of the first time
                //* TODO Eastablish first time versus subsequent time logic 

                ShowWindowAsync(mainWindowHandle, SW_HIDE);
            }
            if (this.chkShowOwnedPopups.Checked == true)
                ShowOwnedPopups(mainWindowHandle, 0);



        }

        private void chkShowWindowAsync_CheckedChanged(object sender, EventArgs e)
        {

        }
    }
}