The class is used to create a console application process without a console window and read its stdout stream. The stdout stream is read in asynchronous non-blocking mode.
The process corresponding to the command line passed to the class constructor is created when an instance of the class is created.
The code was developed using RegisterWaitCallback.ahk by @lexikos and some ideas prompted by @Helgef and @swagfag, thanks to them!
Code: Select all
class AsyncStdoutReader
{
/*
cmd: the command line of the process to be created. The stdout stream of this process will be read asynchronously.
callback: a function that will be called when a new portion of data is written to stdout.
The function accepts three parameters:
PID — ID of the process whose stdout is passed
str — next data chunk from stdout
state — current state of writing data to stdout, can be 0 (incompleted), 1 (completed) and -1 (timed out)
timeout: maximum time in milliseconds to wait for a new data portion in stdout
encoding: sometimes it is necessary to specify the encoding in which to read text from stdout, in most cases this parameter can be omitted.
*/
__New(cmd, callback?, timeout?, encoding?) {
this.event := AsyncStdoutReader.Event()
this.params := {
encoding: encoding ?? 'cp' . DllCall('GetOEMCP'),
overlapped: Buffer(A_PtrSize * 3 + 8, 0),
callback: callback ?? unset,
hEvent: this.event.handle,
timeout: timeout ?? unset,
startTime: A_TickCount,
buf: Buffer(4096, 0),
complete: false,
outData: ''
}
this.process := AsyncStdoutReader.Process(cmd, this.params)
this.signal := AsyncStdoutReader.EventSignal(this.process, this.params)
this.params.processID := this.processID
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()
Sleep 50
this.signal.Clear()
this.process.Clear()
this.params.buf.Size := 0
this.params.outData := ''
ProcessClose(this.processID)
}
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 + 32, 0)
NumPut('UInt', siSize, STARTUPINFO)
NumPut('UInt', STARTF_USESTDHANDLES, STARTUPINFO, A_PtrSize * 4 + 28)
NumPut('Ptr', this.hPipeWrite, 'Ptr', this.hPipeWrite, STARTUPINFO, siSize - A_PtrSize * 2)
PROCESS_INFORMATION := Buffer(A_PtrSize * 2 + 8, 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)
res := DllCall('ReadFile', 'Ptr', this.hPipeRead, 'Ptr', buf, 'UInt', buf.Size, 'UIntP', &size := 0, 'Ptr', overlapped)
if res {
this.info.startTime := A_TickCount
this.info.outData .= str := StrGet(buf, size, this.info.encoding)
(this.info.HasProp('callback') && SetTimer(this.info.callback.Bind(this.PID, str, 0), -10))
this.Read()
}
else if !res && A_LastError != ERROR_IO_PENDING := 997 {
this.info.complete := true
(this.info.HasProp('callback') && SetTimer(this.info.callback.Bind(this.PID, '', 1), -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(this.info.processID, '', -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(this.info.processID, '', 1), -10))
return this.info.complete := true
}
this.info.startTime := A_TickCount
this.info.outData .= str := StrGet(this.info.buf, size, this.info.encoding)
(this.info.HasProp('callback') && SetTimer(this.info.callback.Bind(this.info.processID, str, 0), -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) {
a := A_PtrSize = 8 ? 0x8BCAB60F44C18B48 : 0x448B50082444B60F
b := A_PtrSize = 8 ? 0x498B48C18B4C1051 : 0x70FF0870FF500824
c := A_PtrSize = 8 ? 0x0000000020FF4808 : 0x0008C2D0FF008B04
NumPut('int64', a, 'int64', b, 'int64', c, waitCallback := Buffer(24))
DllCall('VirtualProtect', 'ptr', waitCallback, 'ptr', 24, 'uint', 0x40, 'uint*', 0)
hLib := DllCall('GetModuleHandle', 'str', 'user32', 'ptr')
postMessageW := DllCall('GetProcAddress', 'ptr', hLib, '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 := AsyncStdoutReader.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'))
try (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()
}
}
}
}
Code: Select all
#Requires AutoHotkey v2
Persistent
ReadOutput()
reader := AsyncStdoutReader('ping -n 8 google.com', ReadOutput)
ReadOutput(PID := 0, str := '', state := 0) {
global reader
static EM_SETSEL := 0xB1, wnd := '', text := '', edit := ''
if !wnd {
wnd := Gui('+Resize', 'Async reading of stdout')
wnd.MarginX := wnd.MarginY := 0
wnd.SetFont('s12', 'Consolas')
wnd.AddText('x10 y10', 'Complete: ')
text := wnd.AddText('x+5 yp w100', 'false')
edit := wnd.AddEdit('xm y+10 w650 h500')
edit.GetPos(, &y)
wnd.OnEvent('Size', (o, m, w, h) => edit.Move(,, w, h - y))
wnd.OnEvent('Close', (*) => ExitApp())
wnd.Show()
}
if !PID {
text.Value := 'false'
edit.Value := ''
return
}
text.Value := state = -1 ? 'timed out' : state = 0 ? 'false' : 'true'
SendMessage EM_SETSEL, -2, -1, edit
EditPaste str, edit
if reader && state {
outData := reader.outData, reader := ''
MsgBox outData, 'Complete stdout', 0x2040
}
}
Code: Select all
#Requires AutoHotkey v2
Persistent
reader := [AsyncStdoutReader('cmd /c cd /?', (pid, str, state) => (
; when state is 1 (complete) or -1 (timed out), output full stdout
state && (stdout := reader[1].outData, reader[1] := '', MsgBox(stdout, 'Console command CD help info'))
))]