I modified the code so that it works with AHK_L x32.
I'm still having troubles with AHK_L x64: CreatePipe function fails with error "invalid memory access". Does the child process have to be x64 too?
This version is
not compatible with old AHK.
I also followed Lexicos' advices, a few posts above.
I added a parameter to the functions ConsoleApp_RunWait() and ConsoleApp_GetStdOut() to be able to specify the encoding of stdout so that it gets converted correctly to unicode. See AHK_L help on StrGet() for accepted values.
There is an example featuring unicode stdout (comspec /U switch) and regular stdout (CP850 - i.e. code page 850 - reproduced stdout of ping.exe correctly for me). Uncomment the first commented block to run the example.
I was not able to find out how to write to stdin, which might be useful in some cases.
Code:
; ConsoleApps V3
;
; AutoHotkey_L Version: 1.x
; Language: English
; Platform: Win9x/NT
; Original program from Marcus Cortes <macortes84@yahoo.com>
; Modified and optimized for AutoHotkey_L by Louis Blesch
;
; Script Function:
; Provides a set of functions to redirect and capture
; the standard input and output of other programs.
;
/*
; Uncomment to run the example
#NoEnv ; Recommended for performance and compatibility with future AutoHotkey releases.
Gui, Add, Edit, W400 R20 vLog
Gui, Show
GuiControl, , Log, % so:=ConsoleApp_RunWait("cmd /U /c Echo 你好^|Привет^|Hi^|Hallo^|Salut&Echo.", "", "UTF-16")
hP:=ConsoleApp_Run("cmd /c ping -n 2 -w 700 1.2.3.4")
Loop
{
sleep 250
bRunning:=ConsoleApp_GetStdOut(hP, so, "CP850")
GuiControl, , Log, %so%
if !bRunning
break
}
ConsoleApp_CloseHandle(hP)
msgbox The End
exitapp
*/
/*
<summary>
Runs an application and retrieves its standard output.
</summary>
<param="CmdLine">
The executable file to run including any path information and arguments.
</param>
<param="WorkingDir">
The startup directory for the program.
</param>
<param="CharSet">
The character set of stdout. This parameter is optional.
Acceptable values : 0/empty=ASCII,1=UTF8,2=Unicode
</param>
<param="ExitCode">
When the function returns, this parameter is set to the exit code of the program.
</param>
<returns>
Returns the StdOut of the program.
</returns>
<remarks>
The output of programs that are explicity redirected or "piped" in their command-lines will
not be captured by this function (e.g. "cmd.exe /C echo Hello World > test.txt").
</remarks>
<example>
</example>
*/
ConsoleApp_RunWait(CmdLine, WorkingDir="", encoding="CP0", byref ExitCode="")
{
global
local ConsoleAppHandle, ConsoleAppStillRunning, StdOut, BytesAppended, FirstWin32Error
ConsoleApps_Initialize()
; Create the process
ConsoleAppHandle := ConsoleApp_Run(CmdLine, WorkingDir)
; In reality, this function will never return with a ConsoleAppHandle value of 0, but
; it is good to check for this possibility in case future versions do return a value of
; 0 for failure.
if (ConsoleAppHandle = 0)
CONSOLEAPPS_PRIVATE_throw(ERROR_GENERIC_ERROR, "Unable to run console application.")
/* Continuously retrieve the process's output and write it to the edit control
* in a loop as it arrives.
*/
Loop
{
sleep 100
; Append the ConsoleApps standard output to StdOut.
ConsoleAppStillRunning := ConsoleApp_GetStdOut(ConsoleAppHandle, StdOut, encoding, BytesAppended, ExitCode)
; If the ConsoleApp is no longer running, then stop checking for output.
if (!ConsoleAppStillRunning)
break
; Give up remainder of time-slice to give the child process a chance to generate more output
; before checking again.
}
; This function must be called when the ConsoleAppHandle is no longer needed.
ConsoleApp_CloseHandle(ConsoleAppHandle)
return StdOut
}
/*
<summary>
Runs an application and redirects its standard input/output handles such that they can be accessed
from the script using the ConsoleApp_GetStdOut() function.
</summary>
<param="CmdLine">
The executable file to run including any path information and arguments.
</param>
<param="WorkingDir">
The startup directory for the program.
</param>
<param="Reserved">
This parameter is reserved for future use and should be a null string ("") or should be omitted.
</param>
<param="PID">
Specifies a variable to receive the ProcessID of the program. This parameter is optional.
</param>
<returns>
Returns a proprietary handle to the ConsoleApp for use in calls to ConsoleApp_GetStdOut() and
ConsoleApp_CloseHandle(). This handle cannot be specified in any Win32 API functions to identify
a process.
</returns>
<remarks>
If this function completes successfully, the handle returned by this function must be closed
when no longer needed using the ConsoleApp_CloseHandle() function.
The output of programs that are explicity redirected or "piped" in their command-lines will
not be captured in calls to ConsoleApp_GetStdOut (e.g. "cmd.exe /C echo Hello World > test.txt").
</remarks>
<example>
</example>
*/
ConsoleApp_Run(CmdLine, WorkingDir="", byref PID="")
{
global
local sa, pi, si, lpCurrentDirectory
local hStdoutRead, hStdoutWrite
; local hStdinRead, hStdinWrite
local FirstWin32Error
local hHeap, lpCAPI
static RedProcNextHandle := 1
ConsoleApps_Initialize()
CONSOLEAPPS_PRIVATE_calloc(pi, 16, 0) ;PROCESS_INFORMATION
CONSOLEAPPS_PRIVATE_calloc(si, 4*18, 0) ;STARTUP_INFO
NumPut(4*18, si, 0, "uint") ;STARTUP_INFO {HANDLE cbSize}
NumPut(0x100 | 0x1, si, 4*11, "uint") ;STARTUP_INFO {dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW}
;NumPut(0x0, si, 4*12, "ushort") ;STARTUP_INFO {wShowWindow = SW_HIDE}
;ALLOC AND INIT SECURITY_ATTRIBUTES STRUC
CONSOLEAPPS_PRIVATE_calloc(sa, 12, 0) ;security attributes
NumPut(12, sa, 0, "uint")
NumPut(0, sa, 4, "uint")
NumPut(1, sa, 8, "int")
;CREATE PIPE FOR STDOUT
;NOTE: For some reason, CreatePipe works fine with x32 and fails with "invalid memory access" with x64
if !DllCall("CreatePipe", "uint*", hStdoutRead, "uint*", hStdoutWrite, "ptr", &sa, "uint", 0)
CONSOLEAPPS_PRIVATE_throw(CONSOLEAPPS_PRIVATE_ERROR_WIN32_ERROR, "Unable to create stdout pipe: ")
if (!DllCall("SetHandleInformation", "uint", hStdoutRead, "uint", 1, "uint", 0, "int"))
{
FirstWin32Error := A_LastError
DllCall("CloseHandle", "uint", hStdoutRead) ;HANDLE hThread
DllCall("CloseHandle", "uint", hStdoutWrite) ;HANDLE hThread
CONSOLEAPPS_PRIVATE_throw(CONSOLEAPPS_PRIVATE_ERROR_WIN32_ERROR, "Unable to set handle information: ", "", FirstWin32Error)
}
/*
;CREATE PIPE FOR STDIN
if (!DllCall("CreatePipe", "uint*", hStdinRead, "uint*", hStdinWrite, "ptr", &sa, "uint", 0))
{
FirstWin32Error := A_LastError
DllCall("CloseHandle", "uint", hStdoutRead) ;HANDLE hThread
DllCall("CloseHandle", "uint", hStdoutWrite) ;HANDLE hThread
CONSOLEAPPS_PRIVATE_throw(CONSOLEAPPS_PRIVATE_ERROR_WIN32_ERROR, "Unable to create stdin pipe: ", "", FirstWin32Error)
}
if (!DllCall("SetHandleInformation", "uint", hStdinWrite, "uint", 1, "uint", 0, "int"))
{
FirstWin32Error := A_LastError
DllCall("CloseHandle", "uint", hStdoutRead) ;HANDLE hThread
DllCall("CloseHandle", "uint", hStdoutWrite) ;HANDLE hThread
DllCall("CloseHandle", "uint", hStdinRead) ;HANDLE hThread
DllCall("CloseHandle", "uint", hStdinWrite) ;HANDLE hThread
CONSOLEAPPS_PRIVATE_throw(CONSOLEAPPS_PRIVATE_ERROR_WIN32_ERROR, "Unable to set handle information: ", "", FirstWin32Error)
}
*/
;USE PIPE HANDLES FOR STDIO
NumPut(hStdinRead, si, 4*14, "uint") ;STARTUP_INFO {HANDLE hStdInput}
NumPut(hStdoutWrite, si, 4*15, "uint") ;STARTUP_INFO {HANDLE hStdOutput}
NumPut(hStdoutWrite, si, 4*16, "uint") ;STARTUP_INFO {HANDLE hStdError}
if (StrLen(WorkingDir) = 0)
lpCurrentDirectory := 0
else
lpCurrentDirectory := &WorkingDir
if (!DllCall("CreateProcess", "uint", 0
, "ptr", &CmdLine
, "uint", 0
, "uint", 0
, "int", true
, "uint", 0
, "uint", 0
, "uint", lpCurrentDirectory
, "ptr", &si
, "ptr", &pi))
{
FirstWin32Error := A_LastError
DllCall("CloseHandle", "uint", hStdoutRead) ;HANDLE hThread
DllCall("CloseHandle", "uint", hStdoutWrite) ;HANDLE hThread
; DllCall("CloseHandle", "uint", hStdinRead) ;HANDLE hThread
; DllCall("CloseHandle", "uint", hStdinWrite) ;HANDLE hThread
CONSOLEAPPS_PRIVATE_throw(CONSOLEAPPS_PRIVATE_ERROR_WIN32_ERROR, "Unable to run process: ", "", FirstWin32Error)
}
hProcess := NumGet(pi, 0, "uint")
hThread := NumGet(pi, 4, "uint")
PID := NumGet(pi, 8, "uint") ;DWORD dwProcessId
DllCall("CloseHandle", "uint", hThread) ;HANDLE hThread
; struct ConsoleAppsProcessInfo
; {
; int hProcess;
; int hStdoutRead;
; int hStdoutWrite;
; //int hStdinRead;
; //int hStdinWrite;
; }
hHeap := DllCall("GetProcessHeap", "uint")
lpCAPI := DllCall("HeapAlloc", "uint", hHeap, "uint", 0, "uint", 0xC, "uint")
NumPut(hProcess, lpCAPI + 0x0)
NumPut(hStdoutRead, lpCAPI + 0x4)
NumPut(hStdoutWrite, lpCAPI + 0x8)
NumPut(hStdinRead, lpCAPI + 0xC)
NumPut(hStdinWrite, lpCAPI + 0x10)
return lpCAPI
}
/*
<summary>
Retrieves the standard output of a ConsoleApp that was run using ConsoleApp_Run().
</summary>
<param="ConsoleAppHandle">
A handle to a ConsoleApp that was created in a call to ConsoleApp_Run.
</param>
<param="Stdout">
A variable that is to be appended with the standard output of the ConsoleApp. The
variable is appended with all of the standard output of the program since the last
call to ConsoleApp_GetStdOut.
</param>
<param="encoding">
A string indicating the encoding of stdout. See AutoHotKey_L help on StrGet() for more information.
</param>
<param="bytesAppended">
A variable that is to receive the number of bytes that were appended to the Stdout
parameter.
</param>
<returns>
Returns true if the ConsoleApp is still running.
Returns false if the ConsoleApp is no longer running.
</returns>
<remarks>
The output of programs that are explicity redirected or "piped" in their command-lines will
not be captured by this function (e.g. "cmd.exe /C echo Hello World > test.txt").
</remarks>
<example>
</example>
*/
ConsoleApp_GetStdOut(ConsoleAppHandle, byref Stdout, encoding="CP0", byref BytesAppended = 0, byref ExitCode="")
{
global
local hProcess, hStdoutRead, buf, ec, cb, FirstWin32Error
ConsoleApps_Initialize()
hProcess := NumGet(ConsoleAppHandle + 0x0)
hStdoutRead := NumGet(ConsoleAppHandle + 0x4)
; hStdoutWrite := NumGet(ConsoleAppHandle + 0x8)
; hStdinRead := NumGet(ConsoleAppHandle + 0xC)
; hStdinWrite := NumGet(ConsoleAppHandle + 0x10)
BytesAppended := 0
Loop
{
DllCall("PeekNamedPipe", "uint", hStdoutRead, "uint", 0, "uint", 0, "uint", 0, "uint*", cb, "uint", 0)
if !cb
break
if (!CONSOLEAPPS_PRIVATE_ReadFile(hStdoutRead, encoding, buf, cb, 1024))
{
FirstWin32Error := A_LastError
ConsoleApp_CloseHandle(ConsoleAppHandle)
CONSOLEAPPS_PRIVATE_throw(CONSOLEAPPS_PRIVATE_ERROR_WIN32_ERROR, "Error reading process stdout: ", "", FirstWin32Error)
}
stdout .= buf
BytesAppended += NumGet(cb, 0, "uint")
}
if (!DllCall("GetExitCodeProcess", "uint", hProcess, "uint*", ec))
{
FirstWin32Error := A_LastError
ConsoleApp_CloseHandle(ConsoleAppHandle)
CONSOLEAPPS_PRIVATE_throw(CONSOLEAPPS_PRIVATE_ERROR_WIN32_ERROR, "Error getting process exit code: ", "", FirstWin32Error)
}
ExitCode := ec
; If the exit code of the process is STILL_ACTIVE (0x103) then the process is still running
if (ExitCode = 0x103)
return true
return false
}
/*
<summary>
Closes a ConsoleApp handle returned by ConsoleApp_Run()
</summary>
<param="ConsoleAppHandle">
Handle to close.
</param>
<returns>
This function does not return a value.
</returns>
<remarks>
Every handle returned by a successful call to ConsoleApp_Run() must be closed
with this function.
</remarks>
*/
ConsoleApp_CloseHandle(ConsoleAppHandle)
{
global
local hProcess, hStdoutRead, hStdOutWrite
ConsoleApps_Initialize()
hProcess := NumGet(ConsoleAppHandle + 0x0)
hStdoutRead := NumGet(ConsoleAppHandle + 0x4)
hStdoutWrite := NumGet(ConsoleAppHandle + 0x8)
hStdinRead := NumGet(ConsoleAppHandle + 0xC)
hStdinWrite := NumGet(ConsoleAppHandle + 0x10)
; If any of the following functions fail, then the ConsoleAppProcessInfo structure must
; not be valid, or has already been closed. In this case we raise an error to alert the
; user of a possible mis-handling of the handle and prevent further corruption and memory
; leaks.
if (!DllCall("CloseHandle", "uint", hProcess)) ;HANDLE hThread
CONSOLEAPPS_PRIVATE_throw(ERROR_GENERIC_ERROR, "Error ConsoleAppHandle is corrupt or has already been closed.")
if (!DllCall("CloseHandle", "uint", hStdoutRead)) ;HANDLE hThread
CONSOLEAPPS_PRIVATE_throw(ERROR_GENERIC_ERROR, "Error ConsoleAppHandle is corrupt or has already been closed.")
if (!DllCall("CloseHandle", "uint", hStdoutWrite)) ;HANDLE hThread
CONSOLEAPPS_PRIVATE_throw(ERROR_GENERIC_ERROR, "Error ConsoleAppHandle is corrupt or has already been closed.")
/*
if (!DllCall("CloseHandle", "uint", hStdinRead)) ;HANDLE hThread
CONSOLEAPPS_PRIVATE_throw(ERROR_GENERIC_ERROR, "Error ConsoleAppHandle is corrupt or has already been closed.")
if (!DllCall("CloseHandle", "uint", hStdinWrite)) ;HANDLE hThread
CONSOLEAPPS_PRIVATE_throw(ERROR_GENERIC_ERROR, "Error ConsoleAppHandle is corrupt or has already been closed.")
*/
hHeap := DllCall("GetProcessHeap", "uint")
if (!DllCall("HeapFree", "uint", hHeap, "uint", 0, "uint", ConsoleAppHandle))
CONSOLEAPPS_PRIVATE_throw(CONSOLEAPPS_PRIVATE_ERROR_WIN32_ERROR, "Error deallocating heap. This is a serious error which can cause heap corruption. This program will now close. Specifically: ")
}
; +----------------------+
; | PRIVATE DECLARATIONS |
; +----------------------+
ConsoleApps_Initialize()
{
global
if (ConsoleApps_Initialized)
Return
ConsoleApps_Initialized := True
CONSOLEAPPS_PRIVATE_EXIT_FAILURE := 1
CONSOLEAPPS_PRIVATE_MAXIMUM_INTEGER := 0x7FFFFFFF ; 2147483647
CONSOLEAPPS_PRIVATE_MAXIMUM_UNSIGNED_INTEGER := 0xFFFFFFFF ; 4294967295
CONSOLEAPPS_PRIVATE_ERROR_NOT_ENOUGH_MEMORY := 0x8
CONSOLEAPPS_PRIVATE_ERROR_INVALID_PARAMETER := 0x57
CONSOLEAPPS_PRIVATE_ERROR_WIN32_ERROR := CONSOLEAPPS_PRIVATE_MAXIMUM_UNSIGNED_INTEGER + 1
CONSOLEAPPS_PRIVATE_STD_ERROR_CODES_%CONSOLEAPPS_PRIVATE_ERROR_NOT_ENOUGH_MEMORY% := "Not enough storage is available to process this command."
CONSOLEAPPS_PRIVATE_STD_ERROR_CODES_%CONSOLEAPPS_PRIVATE_ERROR_INVALID_PARAMETER% := "Invalid parameter value."
CONSOLEAPPS_PRIVATE_STD_ERROR_CODES_%CONSOLEAPPS_PRIVATE_ERROR_WIN32_ERROR% := "Unspecified exception."
}
;bufferSize includes a terminating 16-bit null character.
CONSOLEAPPS_PRIVATE_ReadFile(hFile, encoding="CP0", byref buf="", byref bytesRead=0, bufferSize=4096)
{
if (bufferSize <= 2)
return false
if (VarSetCapacity(buf, bufferSize) < bufferSize)
CONSOLEAPPS_PRIVATE_throw(CONSOLEAPPS_PRIVATE_ERROR_NOT_ENOUGH_MEMORY)
if (!DllCall("ReadFile", "uint", hFile, "ptr", &buf, "uint", bufferSize-2, "uint*", bytesRead, "uint", 0))
return false
NumPut(0, buf, bytesRead, "ushort") ;UTF-16 null terminator
VarSetCapacity(buf, -1)
buf := StrGet(&buf, encoding)
return true
}
CONSOLEAPPS_PRIVATE_abort()
{
global
Critical, %CONSOLEAPPS_PRIVATE_MAXIMUM_INTEGER%
CONSOLEAPPS_PRIVATE_STD_Exiting := True
OnExit
BlockInput, MouseMoveOff
BlockInput, Off
ExitApp, %CONSOLEAPPS_PRIVATE_EXIT_FAILURE%
}
CONSOLEAPPS_PRIVATE_calloc(byref Var, size, fillbyte=0)
{
global
local cb
if (cb := VarSetCapacity(Var, size, fillbyte) < size)
CONSOLEAPPS_PRIVATE_throw(CONSOLEAPPS_PRIVATE_ERROR_NOT_ENOUGH_MEMORY)
return cb
}
; ParamName is only valid if ErrorCode is CONSOLEAPPS_PRIVATE_ERROR_INVALID_PARAMETER
; LastWin32Error is only necessary if ErrorCode is CONSOLEAPPS_PRIVATE_ERROR_WIN32_ERROR
; and the A_LastError built-in variable is no longer valid.
CONSOLEAPPS_PRIVATE_throw(ErrorCode, ErrorMessage="", ParamName="", LastWin32Error="")
{
global
local lpMsg
Critical, %CONSOLEAPPS_PRIVATE_MAXIMUM_INTEGER%
if (ErrorCode = CONSOLEAPPS_PRIVATE_ERROR_INVALID_PARAMETER)
{
if (StrLen(ErrorMessage) = 0)
ErrorMessage := CONSOLEAPPS_PRIVATE_STD_ERROR_CODES_%ErrorCode%
if (StrLen(ParamName) != 0)
ErrorMessage .= "\nParameter: " . ParamName
}
else if (ErrorCode = CONSOLEAPPS_PRIVATE_ERROR_WIN32_ERROR)
{
if (StrLen(ErrorMessage) = 0)
ErrorMessage := CONSOLEAPPS_PRIVATE_STD_ERROR_CODES_%ErrorCode%
if (StrLen(LastWin32Error) = 0)
LastWin32Error := A_LastError
;varsetcapacity(lpMsg,4)
if !(DllCall("FormatMessage"
, "uint", 0x1300
, "uint", 0
, "uint", LastWin32Error
, "uint", ((1 << 10) | 0)
, "uint*", lpMsg
, "uint", 0
, "uint", 0, "uint"))
ErrorMessage .= CONSOLEAPPS_PRIVATE_STD_ERROR_CODES_%CONSOLEAPPS_PRIVATE_ERROR_WIN32_ERROR%
else
{
ErrorMessage .= DllCall("MSVCRT\memcpy", "uint", lpMsg, "uint", 0, "uint", 0, "str")
DllCall("LocalFree", "uint", lpMsg)
VarSetCapacity(lpMsg, 0)
}
}
else
{
if (StrLen(ErrorMessage) = 0)
ErrorMessage := CONSOLEAPPS_PRIVATE_STD_ERROR_CODES_%ErrorCode%
}
MsgBox, 0x1010, %A_ScriptName%, %ErrorMessage%
CONSOLEAPPS_PRIVATE_abort()
}