delphiwinapivclkernel32

How to pass a pointer to a list of handles to the UpdateProcThreadAttribute function


I have an application that spawns multiple CreateProcess threads and I'm successfully redirecting the stdout and stderr output to text files for each one.

However, I've discovered the feature whereby the stdout/strderr handles are inherited by all such threads and not just the ones I want them to be inherited by. So I've embarked on a journey to use the InitializeProcThreadAttributeList, UpdateProcThreadAttribute functions and EXTENDED_STARTUPINFO_PRESENT and a STARTUPINFOEX structure in the CreateProcess function to get around this but I'm stuck.

If I use PROC_THREAD_ATTRIBUTE_HANDLE_LIST as the Attribute argument in UpdateProcThreadAttribute procedure it expects the lpValue parameter to be a pointer to a list of handles to be inherited by the child process.

For the List I've tried using a

TList<Cardinal>

and also creating an array of Cardinals but couldn't get either approaches to compile!

Question: How do I create and populate such a list?

Secondly, in this example it is using the functions and procedures from kernel32.dll but they also exist in the Windows unit too (I'm using Delphi 10.3) although the definitions differ:

For example, InitializeProcThreadAttributeList( nil, 1, 0, vAListSize ); won't compile using the Windows unit due to the nil argument because Types of actual and formal var parameters must be identical but I have no such issue using the one in kernel32

Question: Which version of these functions/procedures should I be using?

Thanks.


Solution

  • In case it is useful, here's my code to implement all of this:

    function InitializeProcThreadAttributeList(
      lpAttributeList: Pointer;
      dwAttributeCount: DWORD;
      dwFlags: DWORD;
      var lpSize: SIZE_T
    ): BOOL; stdcall; external kernel32;
    
    function UpdateProcThreadAttribute(
      lpAttributeList: Pointer;
      dwFlags: DWORD;
      Attribute: DWORD_PTR;
      lpValue: Pointer;
      cbSize: SIZE_T;
      lpPreviousValue: PPointer;
      lpReturnSize: PSIZE_T
    ): BOOL; stdcall; external kernel32;
    
    function DeleteProcThreadAttributeList(
      lpAttributeList: Pointer
    ): BOOL; stdcall; external kernel32;
    
    // see https://devblogs.microsoft.com/oldnewthing/20111216-00/?p=8873
    function CreateProcessWithInheritedHandles(
      lpApplicationName: LPCWSTR;
      lpCommandLine: LPWSTR;
      lpProcessAttributes,
      lpThreadAttributes: PSecurityAttributes;
      const Handles: array of THandle;
      dwCreationFlags: DWORD;
      lpEnvironment: Pointer;
      lpCurrentDirectory: LPCWSTR;
      const lpStartupInfo: TStartupInfo;
      var lpProcessInformation: TProcessInformation
    ): Boolean;
    
    const
      PROC_THREAD_ATTRIBUTE_HANDLE_LIST = $00020002;
    
      // this flag is ignored these days, and struct size is used, but we may as well follow the letter of the docs
      EXTENDED_STARTUPINFO_PRESENT = $00080000;
    
    type
      TStartupInfoEx = record
        StartupInfo: TStartupInfo;
        lpAttributeList: Pointer;
      end;
    
    var
      Handle: THandle;
      StartupInfoEx: TStartupInfoEx;
      size: SIZE_T;
    
    begin
      Assert(Length(Handles)>0);
    
      StartupInfoEx.StartupInfo := lpStartupInfo;
      StartupInfoEx.StartupInfo.cb := SizeOf(StartupInfoEx);
      StartupInfoEx.lpAttributeList := nil;
    
      Win32Check(not InitializeProcThreadAttributeList(nil, 1, 0, size) and (GetLastError=ERROR_INSUFFICIENT_BUFFER));
      GetMem(StartupInfoEx.lpAttributeList, size);
      try
        Win32Check(InitializeProcThreadAttributeList(StartupInfoEx.lpAttributeList, 1, 0, size));
        try
          Win32Check(UpdateProcThreadAttribute(
            StartupInfoEx.lpAttributeList,
            0,
            PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
            @Handles[0],
            Length(Handles) * SizeOf(Handles[0]),
            nil,
            nil
          ));
    
          for Handle in Handles do begin
            Win32Check(SetHandleInformation(Handle, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
          end;
    
          Result := CreateProcess(
            lpApplicationName,
            lpCommandLine,
            lpProcessAttributes,
            lpThreadAttributes,
            True,
            dwCreationFlags or EXTENDED_STARTUPINFO_PRESENT,
            lpEnvironment,
            lpCurrentDirectory,
            StartupInfoEx.StartupInfo,
            lpProcessInformation
          );
        finally
          DeleteProcThreadAttributeList(StartupInfoEx.lpAttributeList);
        end;
      finally
        FreeMem(StartupInfoEx.lpAttributeList);
      end;
    end;
    

    From your post it would seem that there are some declarations of InitializeProcThreadAttributeList, UpdateProcThreadAttribute and DeleteProcThreadAttributeList in the Windows unit in the latest versions of Delphi, but your post implies that they are incorrectly declared. The above code is known to work correctly.

    Update: I've updated the code to include the EXTENDED_STARTUPINFO_PRESENT process creation flag as suggested by @blerontin. I don't believe that it is necessary because the size of the startup info struct is what the system uses to make that determination. However, the docs say it should be used, and it's harmless to include it.

    I've also looked at the declarations of InitializeProcThreadAttributeList and UpdateProcThreadAttribute in the latest Delphi Windows unit in the RTL (version 11.3 as I write) and they are indeed bogus.