Efficient notification of process closure (or Win32 handle signal)

Post your working scripts, libraries and tools.
lexikos
Posts: 9592
Joined: 30 Sep 2013, 04:07
Contact:

Efficient notification of process closure (or Win32 handle signal)

19 Nov 2022, 22:18

RunWait or ProcessWaitClose are fine if you are waiting just for a single process, but what if you are waiting for multiple processes or other conditions?

One solution is to poll; i.e. repeatedly check the conditions. To respond to the conditions quickly enough, you might check every 100ms. This is how often RunWait and ProcessWaitClose check. However, in that case, the process has to wake every 100ms to check this condition. Sometimes the process might run for a long time, so checking every 100ms is quite inefficient; the longer the wait might be (say, several hours or days), the less efficient it seems.

A more efficient solution is to open a handle to the process and then wait on it. MsgWaitForMultipleObjects can allow us to wait for multiple process handles, while also allowing the script to respond to any messages that it receives. If this function is able to handle all of the conditions we're waiting for, it is almost perfect: the thread can sleep indefinitely while it waits, waking only if it needs to. It does still require some looping, as we need the function to return when a message is received, so AutoHotkey can process it (with Sleep -1), and then return to waiting.

However, if we need to mix in some other conditions not handled by the Win32 functions, or if the script's design doesn't allow for it to be sitting in a loop just for this, these solutions fall short.

Enter OnProcessClose. This example runs two processes. When each process terminates, a message is shown with the exit code (which can be set with e.g. exit 42 at the command prompt). The script stays persistent until the process counter (the runCount variable) reaches 0.

Code: Select all

Loop runCount := 2 {
    Run "cmd",,, &pid
    OnProcessClose(pid, processClosed)
}
Persistent

processClosed(proc, timedOut) {
    if timedOut
        MsgBox "Timed out"
    else {
        exitCode := proc.exitCode
        MsgBox "Process " proc.id " exited with code " (exitCode > 0x7fffffff ? Format("{:x}", exitCode) : exitCode)
    }
    global runCount
    Persistent --runCount > 0
}

OnProcessClose.ahk

Code: Select all

/*
OnProcessClose:
  Registers *callback* to be called when the process identified by *proc*
  exits, or after *timeout* milliseconds if it has not exited.
@arg proc - A process handle as either an Integer or an object with `.ptr`.
@arg callback - A function with signature `callback(handle, timedOut)`.\
  `handle` is a ProcessHandle with properties `ID`, `ExitCode` and `Ptr` (process handle).\
  `timedOut` is true if the wait timed out, otherwise false.
@arg timeout - The timeout in milliseconds. If omitted or -1, the wait
  never times out. If 0, the callback is called immediately.
@returns {RegisteredWait} - Optionally use the `Unregister()` method
  of the returned object to unregister the callback.
*/
OnProcessClose(proc, callback, timeout?) {
    if !(proc is Integer || proc := ProcessExist(proc))
        throw ValueError("Invalid PID or process name", -1, proc)
    if !proc := DllCall("OpenProcess", "uint", 0x101000, "int", false, "uint", proc, "ptr")
        throw OSError()
    return RegisterWaitCallback(ProcessHandle(proc), callback, timeout?)
}

class ProcessHandle {
    __new(handle) {
        this.ptr := handle
    }
    __delete() => DllCall("CloseHandle", "ptr", this)
    ID => DllCall("GetProcessId", "ptr", this)
    ExitCode {
        get {
            if !DllCall("GetExitCodeProcess", "ptr", this, "uint*", &exitCode:=0)
                throw OSError()
            return exitCode
        }
    }
}

#include RegisterWaitCallback.ahk
OnProcessClose requires RegisterWaitCallback, which can be also be used with other handles, such as Win32 event handles or console input buffers.


RegisterWaitCallback.ahk

Code: Select all

/*
RegisterWaitCallback:
  Register *callback* to be called when *handle* is signaled, or after
  *timeout* milliseconds if it has not been signaled.
@arg handle - A process handle or any other handle type supported by 
  RegisterWaitForSingleObject(). This can be an Integer or an object
  with a `.ptr` property.
@arg callback - A function with signature `callback(handle, timedOut)`,
  where `timedOut` is true if the wait timed out, otherwise false.
@arg timeout - The timeout in milliseconds. If omitted or -1, the wait
  never times out. If 0, the callback is called immediately.
@returns {RegisteredWait} - Optionally use the `Unregister()` method
  of the returned object to unregister the wait (cancel the callback).
*/
RegisterWaitCallback(handle, callback, timeout:=-1) {
    static waitCallback, postMessageW, wnd, nmsg := 0x5743
    if !IsSet(waitCallback) {
        if A_PtrSize = 8 {
            NumPut("int64", 0x8BCAB60F44C18B48, "int64", 0x498B48C18B4C1051, "int64", 0x20FF4808, waitCallback := Buffer(24))
            DllCall("VirtualProtect", "ptr", waitCallback, "ptr", 24, "uint", 0x40, "uint*", 0)
        }
        else {
            NumPut("int64", 0x448B50082444B60F, "int64", 0x70FF0870FF500824, "int64", 0x0008C2D0FF008B04, waitCallback := Buffer(24))
            DllCall("VirtualProtect", "ptr", waitCallback, "ptr", 24, "uint", 0x40, "uint*", 0)
        }
        postMessageW := DllCall("GetProcAddress", "ptr", DllCall("GetModuleHandle", "str", "user32", "ptr"), "astr", "PostMessageW", "ptr")
        wnd := Gui(), DllCall("SetParent", "ptr", wnd.hwnd, "ptr", -3) ; HWND_MESSAGE = -3
        OnMessage(nmsg, messaged, 255)
    }
    NumPut("ptr", postMessageW, "ptr", wnd.hwnd, "uptr", nmsg, param := RegisteredWait())
    NumPut("ptr", ObjPtr(param), param, A_PtrSize * 3)
    param.callback := callback, param.handle := handle
    if !DllCall("RegisterWaitForSingleObject", "ptr*", &waitHandle:=0, "ptr", handle
        , "ptr", waitCallback, "ptr", param, "uint", timeout, "uint", 8)
        throw OSError()
    param.waitHandle := waitHandle, param.locked := ObjPtrAddRef(param)
    return param
    static messaged(wParam, lParam, nmsg, hwnd) {
        if hwnd = wnd.hwnd {
            local param := ObjFromPtrAddRef(NumGet(wParam + A_PtrSize * 3, "ptr"))
            (param.callback)(param.handle, lParam)
            param._unlock()
        }
    }
}

class RegisteredWait extends Buffer {
    static prototype.waitHandle := 0, prototype.locked := 0
    __new() => super.__new(A_PtrSize * 5, 0)
    __delete() => this.Unregister()
    _unlock() {
        (p := this.locked) && (this.locked := 0, ObjRelease(p))
    }
    Unregister() {
        wh := this.waitHandle, this.waitHandle := 0
        (wh) && DllCall("UnregisterWaitEx", "ptr", wh, "ptr", -1)
        this._unlock()
    }
}

/*
#include <windows.h>
struct Param {
    decltype(&PostMessageW) pm;
    HWND wnd;
    UINT msg;
};
VOID CALLBACK WaitCallback(Param *param, BOOLEAN waitFired) {
    param->pm(param->wnd, param->msg, (WPARAM)param, (LPARAM)waitFired);
}
---- 64-bit
00000	48 8b c1		 mov	 rax, rcx
00003	44 0f b6 ca		 movzx	 r9d, dl
00007	8b 51 10		 mov	 edx, DWORD PTR [rcx+16]
0000a	4c 8b c1		 mov	 r8, rcx
0000d	48 8b 49 08		 mov	 rcx, QWORD PTR [rcx+8]
00011	48 ff 20		 rex_jmp QWORD PTR [rax]
---- 32-bit
00000	0f b6 44 24 08	 movzx	 eax, BYTE PTR _waitFired$[esp-4]
00005	50				 push	 eax
00006	8b 44 24 08		 mov	 eax, DWORD PTR _param$[esp]
0000a	50				 push	 eax
0000b	ff 70 08		 push	 DWORD PTR [eax+8]
0000e	ff 70 04		 push	 DWORD PTR [eax+4]
00011	8b 00			 mov	 eax, DWORD PTR [eax]
00013	ff d0			 call	 eax
00015	c2 08 00		 ret	 8
*/
RegisterWaitCallback requires the use of machine code because RegisterWaitForSingleObject calls its callback on a worker thread, and AutoHotkey isn't thread-safe. The source code and disassembly for the machine code are in the comments.
iseahound
Posts: 1446
Joined: 13 Aug 2016, 21:04
Contact:

Re: Efficient notification of process closure (or Win32 handle signal)

20 Nov 2022, 11:42

I think

Code: Select all

return RegisterWaitCallback(ProcessHandle(proc), processClosed, timeout?)
should be callback
lexikos
Posts: 9592
Joined: 30 Sep 2013, 04:07
Contact:

Re: Efficient notification of process closure (or Win32 handle signal)

20 Nov 2022, 19:10

@iseahound Not sure how that snuck in there. Fixed.
User avatar
cyruz
Posts: 346
Joined: 30 Sep 2013, 13:31

Re: Efficient notification of process closure (or Win32 handle signal)

27 Mar 2024, 16:06

@lexikos I remember that in the old forum I wrote a library using RegisterWaitForSingleObject with an ordinary AHK callback and you outlined the mistakes in that approach, mentioning machine code callbacks as an alternative.

Now after all this time I need this code again and it's cool to see it in V2.

I slightly modified OpenProcess to allow for arbitrary data to be passed to the ProcessHandle wrapper:

Code: Select all

/*
OnProcessClose:
  Registers *callback* to be called when the process identified by *proc*
  exits, or after *timeout* milliseconds if it has not exited.
@arg proc - A process handle as either an Integer or an object with `.ptr`.
@arg callback - A function with signature `callback(handle, timedOut)`.\
  `handle` is a ProcessHandle with properties `ID`, `ExitCode` and `Ptr` (process handle).\
  `timedOut` is true if the wait timed out, otherwise false.
@arg data - Arbitrary data to be passed to the ProcessHandle object.
@arg timeout - The timeout in milliseconds. If omitted or -1, the wait
  never times out. If 0, the callback is called immediately.
@returns {RegisteredWait} - Optionally use the `Unregister()` method
  of the returned object to unregister the callback.
*/
OnProcessClose(proc, callback, data?, timeout?) {
    if !(proc is Integer || proc := ProcessExist(proc))
        throw ValueError("Invalid PID or process name", -1, proc)
    if !proc := DllCall("OpenProcess", "uint", 0x101000, "int", false, "uint", proc, "ptr")
        throw OSError()
    return RegisterWaitCallback(ProcessHandle(proc, data?), callback, timeout?)
}

class ProcessHandle {
    __new(handle, data:="") {
        this.ptr := handle
        this.data := data
    }
    __delete() => DllCall("CloseHandle", "ptr", this)
    ID => DllCall("GetProcessId", "ptr", this)
    ExitCode {
        get {
            if !DllCall("GetExitCodeProcess", "ptr", this, "uint*", &exitCode:=0)
                throw OSError()
            return exitCode
        }
    }
}

#include RegisterWaitCallback.ahk
ABCza on the old forum.
My GitHub.

Return to “Scripts and Functions (v2)”

Who is online

Users browsing this forum: No registered users and 119 guests