;
; ConsoleApps V2
;
; AutoHotkey Version: 1.x
; Language:       English
; Platform:       Win9x/NT
; Author:         Marcus Cortes <macortes84@yahoo.com>
;
; Script Function:
;   Provides a set of functions to redirect and capture
;   the standard input and output of other programs.
;
#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.

;;; <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="PID">
;;;     Specifies a variable to receive the ProcessID of the program. This parameter is optional.
;;; </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>
;;;     MsgBox, % ConsoleApp_RunWait("cmd.exe /c echo Hello World.")
;;; </example>
ConsoleApp_RunWait(CmdLine, WorkingDir="", 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
   {
      ; Append the ConsoleApps standard output to StdOut.
      ConsoleAppStillRunning := ConsoleApp_GetStdOut(ConsoleAppHandle, StdOut, 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.
      sleep 0
   }
   ; 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>
;;;     ConsoleAppHandle := ConsoleApp_Run("cmd.exe /c echo Hello World.")
;;;     if (ConsoleAppHandle == 0)
;;;         MsgBox, Unable to start cmd.exe
;;;     else
;;;         ConsoleApp_CloseHandle(ConsoleAppHandle)
;;; </example>
ConsoleApp_Run(CmdLine, WorkingDir="", Reserved="", 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 HANDLES
   CONSOLEAPPS_PRIVATE_malloc(hStdoutRead, 4)
   CONSOLEAPPS_PRIVATE_malloc(hStdoutWrite, 4)
   CONSOLEAPPS_PRIVATE_malloc(hStdinRead, 4)
   CONSOLEAPPS_PRIVATE_malloc(hStdinWrite, 4)
   
   ;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(true, sa, 8, "int")
   
   ;CREATE PIPE FOR STDOUT
   if (!DllCall("CreatePipe", "uint", &hStdoutRead, "uint", &hStdoutWrite, "uint", &sa, "uint", 0))
      CONSOLEAPPS_PRIVATE_throw(CONSOLEAPPS_PRIVATE_ERROR_WIN32_ERROR, "Unable to create stdout pipe: ")
   hStdoutRead := NumGet(hStdoutRead, 0, "uint")
   hStdoutWrite := NumGet(hStdoutWrite, 0, "uint")
   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, "uint", &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)
   }
   hStdinRead := NumGet(hStdinRead, 0, "uint")
   hStdinWrite := NumGet(hStdinWrite, 0, "uint")
   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
                               , "uint", &CmdLine
                               , "uint", 0
                               , "uint", 0
                               , "int", true
                               , "uint", 0
                               , "uint", 0
                               , "uint", lpCurrentDirectory
                               , "uint", &si
                               , "uint", &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="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>
;;;     ; To run this example perform the following steps:
;;;     ;   1. Create a folder named ConsoleApps
;;;     ;   2. Copy "ConsoleApps.ahk" to the folder
;;;     ;   3. Create 2 new files in the folder named "ConsoleApps_Test.ahk" and "ConsoleApps_TestFile.bat"
;;;     ;   4. Paste the following text (without the comments) into "ConsoleApps_TestFile.bat":
;;;             @ECHO OFF
;;;             ECHO Processing %2 1 of 3
;;;             ping 127.0.0.1 -n 2 -w 1000 > nul
;;;             ping 127.0.0.1 -n 1 -w 1000 > nul
;;;             ECHO Processing %2 2 of 3
;;;             ping 127.0.0.1 -n 2 -w 1000 > nul
;;;             ping 127.0.0.1 -n 1 -w 1000 > nul
;;;             ECHO Processing %2 3 of 3
;;;             ping 127.0.0.1 -n 2 -w 1000 > nul
;;;             ping 127.0.0.1 -n 1 -w 1000 > nul
;;;             ECHO Processing Complete;;;     ;   
;;;     ;   5. Paste the following text (without the comments) into "ConsoleApps_Test.ahk", and then run "ConsoleApps_Test.ahk"
;;;             #NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.
;;;             #Include ConsoleApps.ahk
;;;             
;;;             ;Create GUI window with an edit control to print the output.
;;;             Gui, Add, Edit, x15 y15 w460 h300 -Wrap ReadOnly hscroll vtxtStdout,
;;;             Gui, Show, w490 h330, Standard Input/Output Redirection Example
;;;             
;;;             ; Create the process
;;;             ConsoleAppHandle := ConsoleApp_Run("ConsoleApps_TestFile.bat -p nothing", A_ScriptDir)
;;;             ; 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)
;;;             {
;;;                 MsgBox, Error running cmd.exe
;;;                 ExitApp
;;;             }
;;;             
;;;             /* Continuously retrieve the process's output and write it to the edit control 
;;;              * in a loop as it arrives.
;;;              */
;;;             Loop
;;;             {
;;;                 ; Append the ConsoleApps standard output to StdOut.
;;;                 ConsoleAppStillRunning := ConsoleApp_GetStdOut(ConsoleAppHandle, StdOut, BytesAppended, ExitCode)
;;;                 ; If StdOut was appended with new text, write it to the GUI window.
;;;                 if (BytesAppended)
;;;                    GuiControl, , txtStdOut, %StdOut%  ; write the output to the edit control.
;;;                 ; 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.
;;;                 sleep 0
;;;             }
;;;             ; This function must be called when the ConsoleAppHandle is no longer needed.
;;;             ConsoleApp_CloseHandle(ConsoleAppHandle)
;;;             if (!ExitCode)
;;;                 MsgBox, The program has completed successfully.
;;;             else
;;;                 MsgBox, The program has exitted with an error code of %ExitCode%.
;;;             return
;;;             
;;;             ; The following label is receives control when the user closes the GUI window.
;;;             GuiClose:
;;;             ExitApp
;;; </example>
ConsoleApp_GetStdOut(ConsoleAppHandle, byref Stdout, 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)
   
   CONSOLEAPPS_PRIVATE_malloc(cb, 4)
   BytesAppended := 0
   Loop
   {
      DllCall("PeekNamedPipe", "uint", hStdoutRead, "uint", 0, "uint", 0, "uint", 0, "uint", &cb, "uint", 0)
      if (!NumGet(cb, 0, "uint"))
         break
      if (!CONSOLEAPPS_PRIVATE_ReadFile(hStdoutRead, buf, cb, 1024))
      {
         FirstWin32Error := A_LastError
         ConsoleApp_CloseHandle(ConsoleAppHandle)
         CONSOLEAPPS_PRIVATE_throw(CONSOLEAPPS_PRIVATE_ERROR_WIN32_ERROR, "Error reading process stdout: ", "", FirstWin32Error)
      }
      stdout := stdout . buf
      BytesAppended += NumGet(cb, 0, "uint")
   }
   
   CONSOLEAPPS_PRIVATE_malloc(ec, 4)
   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 := NumGet(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 is including the terminating null character.
CONSOLEAPPS_PRIVATE_ReadFile(hFile, byref buf, byref BytesRead=0, BufferSize=4096)
{
   if (BufferSize <= 0)
      return false
   CONSOLEAPPS_PRIVATE_malloc(Buf, BufferSize)
   CONSOLEAPPS_PRIVATE_malloc(BytesRead, 4)
   if (!DllCall("ReadFile", "uint", hFile, "uint", &buf, "uint", BufferSize-1, "uint", &BytesRead, "uint", 0))
      return false
   BytesRead := NumGet(BytesRead, 0, "uint")
   NumPut(0, Buf, BytesRead, "uchar") ;terminate data read with a null
   VarSetCapacity(buf, -1)
   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
}

CONSOLEAPPS_PRIVATE_free(byref Var)
{
   VarSetCapacity(Var, 0)
}

CONSOLEAPPS_PRIVATE_WIN32_MAKELANGID(p, s)
{
   return (s << 10) | p
}

CONSOLEAPPS_PRIVATE_malloc(byref var, size)
{
   global
   if (cb := VarSetCapacity(var, size) != size)
      CONSOLEAPPS_PRIVATE_throw(CONSOLEAPPS_PRIVATE_ERROR_NOT_ENOUGH_MEMORY)
   return cb
}

;;; <summary>
;;;     Returns the C string at the address in lpStr
;;; </summary>
;;; <param="lpStr">
;;;     The address of a string in memory. lpStr is an AutoHotkey integer numeric.
;;;     NOT a 4-byte dword value.
;;; </param>
CONSOLEAPPS_PRIVATE_PtrToStr(lpStr)
{
   return DllCall("MSVCRT\memcpy", UINT, lpStr, UINT, 0, UINT, 0, "str")  ; Doesn't copy data, only returns string
}

; 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
      CONSOLEAPPS_PRIVATE_malloc(lpMsg, 4)
      /* DWORD TCharsCopied = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS,
       *                                    0,
       *                                    LastWin32Error,
       *                                    MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT),
       *                                    (LPTSTR)&lpMsg,
       *                                    0,
       *                                    NULL)
       */
      if (t := DllCall("FormatMessageA"
                , "uint", 0x1300
                , "uint", 0
                , "uint", LastWin32Error
                , "uint", CONSOLEAPPS_PRIVATE_WIN32_MAKELANGID(0x0, 0x1)
                , "uint", &lpMsg
                , "uint", 0
                , "uint", 0, "uint") == 0)
      {
         ErrorMessage .= CONSOLEAPPS_PRIVATE_STD_ERROR_CODES[%CONSOLEAPPS_PRIVATE_ERROR_WIN32_ERROR%]
      }
      else
      {
         ErrorMessage .= CONSOLEAPPS_PRIVATE_PtrToStr(NumGet(lpMsg))
         DllCall("LocalFree", "uint", lpMsg)
         CONSOLEAPPS_PRIVATE_free(lpMsg)
      }
   }
   else
   {
      if (StrLen(ErrorMessage) == 0)
         ErrorMessage := CONSOLEAPPS_PRIVATE_STD_ERROR_CODES[%ErrorCode%]
   }
   MsgBox, 0x1010, %A_ScriptName%, %ErrorMessage%
   CONSOLEAPPS_PRIVATE_abort()
}
