RunCMD() v0.97 : Capture stdout to variable. Non-blocking version. Pre-process/omit individual lines.

Post your working scripts, libraries and tools for AHK v1.1 and older
Adventurer
Posts: 23
Joined: 18 Apr 2019, 06:24

Re: RunCMD() v0.96 : Capture stdout to variable. Non-blocking version. Pre-process/omit individual lines.

17 Oct 2023, 14:47

teadrinker wrote:
14 Jul 2023, 07:04
I've tried to write code (v2) for asynchronous reading of stdout using a different approach. It turned out to be a bit cumbersome, but it lacks the above-mentioned disadvantage and is really non-blocking: :)

Code: Select all

#Requires AutoHotkey v2

inst := ''
F3::    ; F3 without timeout
F4:: {  ; F4 timeout 2 sec
    ReadOutput('', '')
    try global inst := AsyncStdoutReader('ping -n 8 google.com', ReadOutput, A_ThisHotkey = 'F3' ? unset : 2000)
    catch MethodError as e {
        MsgBox A_Clipboard := e.Stack
        MsgBox e.Message . '`n' . e.What . '`nLine: ' e.Line
        ExitApp
    }
    MsgBox 'AsyncStdoutReader is non-blocking', 'test', 0x1000 . ' T2'
}

ReadOutput(line, complete := 0) {
    global inst
    static EM_SETSEL := 0xB1, myGui := '', text := '', edit := ''
    if !myGui {
        myGui := Gui('+Resize', 'Async reading of stdout')
        myGui.MarginX := myGui.MarginY := 0
        myGui.SetFont('s12', 'Consolas')
        myGui.AddText('x10 y10', 'Complete: ')
        text := myGui.AddText('x+5 yp w100', 'false')
        edit := myGui.AddEdit('xm y+10 w650 h500')
        edit.GetPos(, &y := unset)
        myGui.OnEvent('Size', (o, m, w, h) => edit.Move(,, w, h - y))
        myGui.OnEvent('Close', (*) => ExitApp())
        myGui.Show()
    }
    (line = '' && complete = '' && edit.Value := '')
    text.Value := complete = -1 ? 'timed out' : complete = false ? 'false' : 'true'
    SendMessage EM_SETSEL, -2, -1, edit
    EditPaste line, edit

    if inst && complete {
        outData := inst.outData, inst := ''
        MsgBox outData, 'Complete stdout', 0x2040
    }
}

class AsyncStdoutReader
{
    __New(cmd, callback?, timeout?, encoding?) {
        encoding := encoding ?? 'cp' . DllCall('GetOEMCP', 'UInt')
        this.event := %this.__Class%.Event()
        this.params := {
            buf: Buffer(4096, 0),
            overlapped: Buffer(A_PtrSize * 3 + 8, 0),
            hEvent: this.event.handle,
            outData: '',
            encoding: encoding,
            complete: false
        }
        (IsSet(callback) && this.params.callback := callback)
        if IsSet(timeout) {
            this.params.timeout := timeout
            this.params.startTime := A_TickCount
        }
        this.process := %this.__Class%.Process(cmd, this.params)
        this.signal := %this.__Class%.EventSignal(this.process, this.params)
        this.process.Read()
    }

    processID => this.process.PID

    complete => this.params.complete

    outData => this.params.outData

    __Delete() {
        DllCall('CancelIoEx', 'Ptr', this.process.hPipeRead, 'Ptr', this.params.overlapped)
        this.event.Set()
        this.signal.Clear()
        this.process.Clear()
        this.params.buf.Size := 0
        this.params.outData := ''
    }

    class Event
    {
        __New() => this.handle := DllCall('CreateEvent', 'Int', 0, 'Int', 0, 'Int', 0, 'Int', 0, 'Ptr')
        __Delete() => DllCall('CloseHandle', 'Ptr', this.handle)
        Set() => DllCall('SetEvent', 'Ptr', this.handle)
    }

    class Process
    {
        __New(cmd, info) {
            this.info := info
            this.CreatePipes()
            if !this.PID := this.CreateProcess(cmd) {
                throw OSError('Failed to create process')
            }
        }

        CreatePipes() {
            static FILE_FLAG_OVERLAPPED := 0x40000000, PIPE_ACCESS_INBOUND := 0x1
                 , pipeMode := (PIPE_TYPE_BYTE := 0) | (PIPE_WAIT := 0)
                 , GENERIC_WRITE := 0x40000000, OPEN_EXISTING := 0x3
                 , FILE_ATTRIBUTE_NORMAL := 0x80, HANDLE_FLAG_INHERIT := 0x1

            this.hPipeRead := DllCall('CreateNamedPipe', 'Str', pipeName := '\\.\pipe\StdOut_' . A_TickCount,
                'UInt', PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, 'UInt', pipeMode, 'UInt', 1,
                'UInt', this.info.buf.Size, 'UInt', this.info.buf.Size, 'UInt', 120000, 'Ptr', 0, 'Ptr')

            this.hPipeWrite := DllCall('CreateFile', 'Str', pipeName, 'UInt', GENERIC_WRITE, 'UInt', 0, 'Ptr', 0,
                'UInt', OPEN_EXISTING, 'UInt', FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, 'Ptr', 0, 'Ptr')
            DllCall('SetHandleInformation', 'Ptr', this.hPipeWrite, 'UInt', HANDLE_FLAG_INHERIT, 'UInt', HANDLE_FLAG_INHERIT)
        }

        CreateProcess(cmd) {
            static STARTF_USESTDHANDLES := 0x100, CREATE_NO_WINDOW := 0x8000000
            STARTUPINFO := Buffer(siSize := A_PtrSize * 9 + 4 * 8, 0)
            NumPut('UInt', siSize, STARTUPINFO)
            NumPut('UInt', STARTF_USESTDHANDLES, STARTUPINFO, A_PtrSize * 4 + 4 * 7)
            NumPut('Ptr', this.hPipeWrite, 'Ptr', this.hPipeWrite, STARTUPINFO, siSize - A_PtrSize * 2)

            PROCESS_INFORMATION := Buffer(A_PtrSize * 2 + 4 * 2, 0)
            if !DllCall('CreateProcess', 'Ptr', 0, 'Str', cmd, 'Ptr', 0, 'Ptr', 0, 'UInt', true,
                'UInt', CREATE_NO_WINDOW, 'Ptr', 0, 'Ptr', 0, 'Ptr', STARTUPINFO, 'Ptr', PROCESS_INFORMATION)
                Return this.Clear()
            DllCall('CloseHandle', 'Ptr', this.hPipeWrite), this.hPipeWrite := 0
            Return PID := NumGet(PROCESS_INFORMATION, A_PtrSize * 2, 'UInt')
        }

        Read() {
            buf := this.info.buf, overlapped := this.info.overlapped
            overlapped.__New(overlapped.Size, 0)
            NumPut('Ptr', this.info.hEvent, overlapped, A_PtrSize * 2 + 8)
            bool := DllCall('ReadFile', 'Ptr', this.hPipeRead, 'Ptr', buf, 'UInt', buf.Size, 'UIntP', &size := 0, 'Ptr', overlapped)
            if bool {
                this.info.outData .= str := StrGet(buf, size, this.info.encoding)
                (this.info.HasProp('callback') && SetTimer(this.info.callback.Bind(str), -10))
                this.Read()
            }
            else if !bool && A_LastError != ERROR_IO_PENDING := 997 {
                this.info.complete := true
                (this.info.HasProp('callback') && SetTimer(this.info.callback.Bind('', true), -10))
            }
        }

        Clear() {
            DllCall('CloseHandle', 'Ptr', this.hPipeRead)
            (this.hPipeWrite && DllCall('CloseHandle', 'Ptr', this.hPipeWrite))
        }
    }

    class EventSignal
    {
        __New(stdOut, info) {
            this.info := info
            this.stdOut := stdOut
            this.onEvent := ObjBindMethod(this, 'Signal')
            timeout := info.HasProp('timeout') ? info.timeout : -1
            this.regWait := this.RegisterWaitCallback(this.info.hEvent, this.onEvent, timeout)
        }

        Signal(handle, timedOut) {
            if timedOut {
                (this.info.HasProp('callback') && SetTimer(this.info.callback.Bind('', -1), -10))
                return
            }
            if !DllCall('GetOverlappedResult', 'Ptr', handle, 'Ptr', this.info.overlapped, 'UIntP', &size := 0, 'UInt', false) {
                (this.info.HasProp('callback') && SetTimer(this.info.callback.Bind('', true), -10))
                return this.info.complete := true
            }
            this.info.outData .= str := StrGet(this.info.buf, size, this.info.encoding)
            (this.info.HasProp('callback') && SetTimer(this.info.callback.Bind(str), -10))
            this.stdOut.Read()
            timeout := this.info.HasProp('timeout') ? this.info.timeout - A_TickCount + this.info.startTime : -1
            this.regWait := this.RegisterWaitCallback(this.info.hEvent, this.onEvent, timeout)
        }

        Clear() {
            this.regWait.Unregister()
            this.DeleteProp('regWait')
            this.DeleteProp('onEvent')
        }

        RegisterWaitCallback(handle, callback, timeout := -1) {
            ; by lexikos https://www.autohotkey.com/boards/viewtopic.php?t=110691
            static waitCallback, postMessageW, wnd, nmsg := 0x5743
            if !IsSet(waitCallback) {
                if A_PtrSize = 8 {
                    NumPut('int64', 0x8BCAB60F44C18B48, 'int64', 0x498B48C18B4C1051, 'int64', 0x20FF4808, waitCallback := Buffer(24))
                } 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 := %StrSplit(this.__Class, '.')[1]%.EventSignal.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()
            }
        }
    }
}
I'd like to see this posted in the v2 Scripts forum as well, especially since @SKAN has showed a disinterest in updating RunCMD due to the flaws you fixed. I don't think he'd want to copy off you unless you gave him express permission to (though he might not want to even then).
bonobo
Posts: 75
Joined: 03 Sep 2023, 20:13

Re: RunCMD() v0.97 : Capture stdout to variable. Non-blocking version. Pre-process/omit individual lines.

23 Oct 2023, 11:58

teadrinker wrote:
17 Oct 2023, 17:18
Since there are so many requests, I'll post it of these days. :)
Thanks for this class @teadrinker.

Would you say the following code is a minimal working example of how you intend the class to be used to get stdout?

Code: Select all

	inst := AsyncStdoutReader("C:\rust\p1\src\main.exe hullo", (line, complete := 0){
		if inst && complete {
			outData := inst.outData
			inst := ''
			MsgBox outData ; do things with stdout
		}
	})
Incidentally, in AHK is there something like promises to avoid "callback hell"?
teadrinker
Posts: 4336
Joined: 29 Mar 2015, 09:41
Contact:

Re: RunCMD() v0.97 : Capture stdout to variable. Non-blocking version. Pre-process/omit individual lines.

24 Oct 2023, 15:47

bonobo wrote: Would you say the following code is a minimal working example of how you intend the class to be used to get stdout?

Code: Select all

AsyncStdoutReader("C:\rust\p1\src\main.exe hullo", (line, complete := 0){
This is not v2.0 syntax, so it is not a working example. :)
bonobo wrote: Incidentally, in AHK is there something like promises to avoid "callback hell"?
At least in v2.0 they are not.
Also, AsyncStdoutReader is a class, not a function, how would you use promis here?
teadrinker
Posts: 4336
Joined: 29 Mar 2015, 09:41
Contact:

Re: RunCMD() v0.97 : Capture stdout to variable. Non-blocking version. Pre-process/omit individual lines.

24 Oct 2023, 19:43

@bonobo
As an option:

Code: Select all

inst := [AsyncStdoutReader('ping google.com', (line, complete := 0) => (
    complete && (outData := inst[1].outData, inst[1] := '', MsgBox(outData))
))]
User avatar
thqby
Posts: 408
Joined: 16 Apr 2021, 11:18
Contact:

Re: RunCMD() v0.97 : Capture stdout to variable. Non-blocking version. Pre-process/omit individual lines.

25 Oct 2023, 01:05

@bonobo
https://github.com/thqby/ahk2_lib/blob/master/Promise.ahk
I have implemented ahk's promise, but i don't think this is suitable for streaming callbacks.

Code: Select all

((...){
})
The above is a valid v2.1 grammar.
bonobo
Posts: 75
Joined: 03 Sep 2023, 20:13

Re: RunCMD() v0.97 : Capture stdout to variable. Non-blocking version. Pre-process/omit individual lines.

29 Oct 2023, 18:02

teadrinker wrote:
24 Oct 2023, 19:43
@bonobo
As an option:

Code: Select all

inst := [AsyncStdoutReader('ping google.com', (line, complete := 0) => (
    complete && (outData := inst[1].outData, inst[1] := '', MsgBox(outData))
))]
Thanks @teadrinker. I was using 2.1 alpha hence the function def expression. Your class works there without a hitch and has very low latency, from my testing so far.
thqby wrote:
25 Oct 2023, 01:05
@bonobo
https://github.com/thqby/ahk2_lib/blob/master/Promise.ahk
I have implemented ahk's promise, but i don't think this is suitable for streaming callbacks.

Code: Select all

((...){
})
The above is a valid v2.1 grammar.
Thanks @thqby. I thought I have vague memory of such a class from when I dug into ahk2lib last time!
User avatar
ArkuS
Posts: 14
Joined: 29 Nov 2019, 22:54

Re: RunCMD() v0.97 : Capture stdout to variable. Non-blocking version. Pre-process/omit individual lines.

13 Apr 2024, 22:22

Hello, I need to enable the hidden windows command line. Execute the command, enter the password and confirm it. Finally get the output from the console to the variable.
Is this doable using RunCmd? The topic I created about this. viewtopic.php?style=17&f=76&t=128671

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: ioSIS and 123 guests