// ---------------------------------------------------------------------------------------------------------
//
// CRunProcess class
//
// Purpose: Starts a Console Application and captures its stdout and stderr output while the app is running.
//
// Author: Elm�Soft (www.netcult.ch/elmue)
//
// ---------------------------------------------------------------------------------------------------------
/*
----------------------------------------------------------------------------------
Using these conventions results in better readable code and less coding errors !
----------------------------------------------------------------------------------
cName for generic class definitions
CName for MFC class definitions
tName for type definitions
eName for enum definitions
kName for struct definitions
e_Name for enum variables
E_Name for enum constant values
i_Name for instances of classes
h_Name for handles
T_Name for Templates
t_Name for TCHAR or LPTSTR
s_Name for strings
sa_Name for Ascii strings
sw_Name for Wide (Unicode) strings
bs_Name for BSTR
f_Name for function pointers
k_Name for contructs (struct)
b_Name bool,BOOL 1 Bit
s8_Name signed 8 Bit (char)
s16_Name signed 16 Bit (SHORT, WCHAR)
s32_Name signed 32 Bit (LONG, int)
s64_Name signed 64 Bit (LONGLONG)
u8_Name unsigned 8 Bit (BYTE)
u16_Name unsigned 16 bit (WORD)
u32_Name unsigned 32 Bit (DWORD, UINT)
u64_Name unsigned 64 Bit (ULONGLONG)
d_Name for double
----------------
m_Name for member variables of a class (e.g. ms32_Name for int member variable)
g_Name for global (static) variables (e.g. gu16_Name for global WORD)
p_Name for pointer (e.g. ps_Name *pointer to string)
pp_Name for pointer to pointer (e.g. ppd_Name **pointer to double)
*/
#include "stdafx.h"
#include "RunProcess.h"
// Constructor
CRunProcess::CRunProcess()
{
Init();
}
// Destructor
CRunProcess::~CRunProcess()
{
CloseHandles();
}
void CRunProcess::Init()
{
mh_Process = 0;
mh_OutPipe = 0;
mh_ErrPipe = 0;
mh_OutFile = 0;
mh_ErrFile = 0;
memset(&mk_OverOut, 0, sizeof(OVERLAPPED));
memset(&mk_OverErr, 0, sizeof(OVERLAPPED));
}
void CRunProcess::CloseHandles()
{
CloseHandle(mh_Process);
CloseHandle(mh_OutPipe);
CloseHandle(mh_ErrPipe);
CloseHandle(mh_OutFile);
CloseHandle(mh_ErrFile);
}
// Parameter description see ExecuteAW() in CaptureConsole.cpp !
// runs the process which is specified in s_Commandline
// waits until the process has terminated
// fills ps_StdOutput and ps_StdError with the stdout and stderr output of the Console application or batch script
// returns "" if OK, otherwise an error message
CString CRunProcess::Execute(CString s_Commandline, // IN
DWORD u32_FirstConvert, // IN
TCHAR* t_CurrentDir, // IN
CString s_UserVariables, // IN
BOOL b_SeparatePipes, // IN
DWORD u32_TimeOut, // IN
CFastString* ps_StdOutput, // OUT
CFastString* ps_StdError, // OUT
DWORD* pu32_ExitCode) // OUT
{
// Execute may be called multiple times from the same thread -> Close any open handles
CloseHandles();
Init();
*pu32_ExitCode = 0;
ps_StdOutput->Empty();
ps_StdError ->Empty();
CString s_Err = CreatePipe(&mh_OutPipe, &mh_OutFile, &mk_OverOut);
if (s_Err.GetLength() > 0)
return s_Err;
if (b_SeparatePipes)
{
s_Err = CreatePipe(&mh_ErrPipe, &mh_ErrFile, &mk_OverErr);
if (s_Err.GetLength() > 0)
return s_Err;
}
DWORD u32_Flags = CREATE_NO_WINDOW;
void* p_Environ = NULL; // Inherit the environment variables from the parent process
CString s_Environ;
if (s_UserVariables.GetLength())
{
#ifdef UNICODE
u32_Flags |= CREATE_UNICODE_ENVIRONMENT;
#endif
// Merge the user variables with the environment variables of the parent process
MergeUserEnvironmentStrings(s_UserVariables, &s_Environ);
p_Environ = (void*)s_Environ.GetBuffer(0);
}
PROCESS_INFORMATION k_Proc;
memset(&k_Proc, 0, sizeof(PROCESS_INFORMATION));
STARTUPINFO k_Start;
memset(&k_Start, 0, sizeof(STARTUPINFO));
k_Start.cb = sizeof(STARTUPINFO);
k_Start.hStdError = (b_SeparatePipes) ? mh_ErrFile : mh_OutFile;
k_Start.hStdOutput = mh_OutFile;
k_Start.dwFlags = STARTF_USESTDHANDLES;
// Convert special characters in the commandline parameters to DOS codepage
s_Commandline = CommandLineParamsToOEM(s_Commandline, u32_FirstConvert);
if (!CreateProcess(0, s_Commandline.GetBuffer(0), 0, 0, TRUE, u32_Flags, p_Environ, t_CurrentDir, &k_Start, &k_Proc))
{
return _T("CaptureConsole.dll: Error creating Process. ") + GetLastErrorMsg();
}
mh_Process = k_Proc.hProcess; // closed in destructor
// wait until the process has exited
DWORD u32_Event = WAIT_TIMEOUT;
DWORD u32_EndTime = GetTickCount() + u32_TimeOut;
do
{
if (u32_TimeOut && GetTickCount() > u32_EndTime)
{
TerminateProcess(k_Proc.hProcess, 0);
return _T("CaptureConsole.dll: Timeout elapsed.");
}
// WaitForSingleObject will return immediately when the process has exited otherwise wait 200 ms
u32_Event = WaitForSingleObject(k_Proc.hProcess, 200);
if (u32_Event == WAIT_FAILED)
{
return _T("CaptureConsole.dll: Error from WaitForSingleObject. ") + GetLastErrorMsg();
}
if (!GetExitCodeProcess(k_Proc.hProcess, pu32_ExitCode))
{
return _T("CaptureConsole.dll: Error getting ExitCode from Process. ") + GetLastErrorMsg();
}
s_Err = ReadPipe(mh_OutPipe, &mk_OverOut, ps_StdOutput);
if (s_Err.GetLength() > 0)
return s_Err;
if (b_SeparatePipes)
{
s_Err = ReadPipe(mh_ErrPipe, &mk_OverErr, ps_StdError);
if (s_Err.GetLength() > 0)
return s_Err;
}
}
while (u32_Event != WAIT_OBJECT_0);
return CString();
}
// Creates a named pipe and a file handle which is used to write to the pipe
CString CRunProcess::CreatePipe(HANDLE* ph_Pipe, HANDLE* ph_File, OVERLAPPED* pk_Overlapped)
{
// Create a threadsafe unique name for the Pipe
static int s32_Counter = 0;
CString s_PipeName;
s_PipeName.Format(_T("\\\\.\\pipe\\CRunProcessPipe_%X_%X_%X_%X"),
GetCurrentProcessId(), GetCurrentThreadId(), GetTickCount(), s32_Counter++);
SECURITY_ATTRIBUTES k_Secur;
k_Secur.nLength = sizeof(SECURITY_ATTRIBUTES);
k_Secur.lpSecurityDescriptor = 0;
k_Secur.bInheritHandle = TRUE;
*ph_Pipe = CreateNamedPipe(s_PipeName, PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED, 0, 1, 4096, 4096, 0, &k_Secur);
if (*ph_Pipe == INVALID_HANDLE_VALUE)
{
return _T("CaptureConsole.dll: Error creating Named Pipe. ") + GetLastErrorMsg();
}
*ph_File = CreateFile(s_PipeName, GENERIC_READ|GENERIC_WRITE, 0, &k_Secur, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
if (*ph_File == INVALID_HANDLE_VALUE)
{
return _T("CaptureConsole.dll: Error creating Pipe Reader. ") + GetLastErrorMsg();
}
if (!ConnectNamedPipe(*ph_Pipe, pk_Overlapped))
{
if (GetLastError() != ERROR_PIPE_CONNECTED)
{
return _T("CaptureConsole.dll: Error connecting Pipe. ") + GetLastErrorMsg();
}
}
SetHandleInformation(*ph_Pipe, HANDLE_FLAG_INHERIT, 0);
SetHandleInformation(*ph_File, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
return CString();
}
// Reads from the Pipe and appends the read data to ps_Data
CString CRunProcess::ReadPipe(HANDLE h_Pipe, OVERLAPPED* pk_Overlapped, CFastString* ps_Data)
{
// IMPORTANT: Check if there is data that can be read.
// The first console output will be lost if ReadFile() is called before data becomes available!
// It does not make any sense but the following 5 lines are indispensable!!
DWORD u32_Avail = 0;
if (!PeekNamedPipe(h_Pipe, 0, 0, 0, &u32_Avail, 0))
return _T("CaptureConsole.dll: Error peeking Pipe. ") + GetLastErrorMsg();
if (!u32_Avail)
return CString();
const BLOCK_SIZE = 4095;
char s8_Buf[BLOCK_SIZE+1];
WCHAR u16_Buf[BLOCK_SIZE+1];
DWORD u32_Read = 0;
do
{
if (!ReadFile(h_Pipe, s8_Buf, BLOCK_SIZE, &u32_Read, pk_Overlapped))
{
if (GetLastError() != ERROR_IO_PENDING)
{
return _T("CaptureConsole.dll: Error reading Pipe. ") + GetLastErrorMsg();
}
}
// ATTENTION: The Console always prints ANSI to the pipe independent if compiled as UNICODE or MBCS!
s8_Buf[u32_Read] = 0;
OemToCharA(s8_Buf, s8_Buf); // convert DOS codepage -> ANSI
#ifdef UNICODE
mbstowcs(u16_Buf, s8_Buf, u32_Read); // convert ANSI -> UNICODE
ps_Data->AppendBuffer(u16_Buf, u32_Read);
#else
ps_Data->AppendBuffer(s8_Buf, u32_Read);
#endif
}
while (u32_Read == BLOCK_SIZE);
return CString();
}
// returns the last error as string message
CString CRunProcess::GetLastErrorMsg()
{
DWORD u32_Error = GetLastError();
if (!u32_Error)
return CString();
CString s_Err;
s_Err.Format(_T("API Error %d: "), u32_Error);
const DWORD BUF_LEN = 2048;
TCHAR t_Msg[BUF_LEN];
if (FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, 0, u32_Error, 0, t_Msg, BUF_LEN, 0))
s_Err += t_Msg;
return s_Err;
}
// The commandline must be splitted into the application path+filename and the parameters
// The first parameter (path+filename) must NEVER be converted, it may contain Unicode characters
// ONLY the parameters must be converted into the DOS codepage!
// Attention: The filename and the parameters may be enclosed in parenthesis.
// Example:
// "C:\Test\HelloW�rld.exe Hello W�rld" will be converted into
// "C:\Test\HelloW�rld.exe Hello W�rld"
// u32_FirstConvert = 0 -> DOS Conversion completely turned off
// u32_FirstConvert = 1 -> convert the first and all following parameters
// u32_FirstConvert = 2 -> convert the second and all following parameters
CString CRunProcess::CommandLineParamsToOEM(CString s_CommandLine, DWORD u32_FirstConvert)
{
// NEVER EVER convert the path+filename!!!
if (!u32_FirstConvert)
return s_CommandLine;
s_CommandLine.TrimLeft();
BOOL b_Parentesis = FALSE;
for (int i=0; i<s_CommandLine.GetLength(); i++)
{
// Toggle parenthesis flag
if (s_CommandLine[i] == '\"')
{
b_Parentesis = !b_Parentesis;
continue;
}
// Decrement u32_FirstConvert for each space delimited param found in the commandline
if (s_CommandLine[i] == ' ' && !b_Parentesis)
{
// There may be multiple spaces
while (s_CommandLine[i+1] == ' ')
i++;
u32_FirstConvert--;
continue;
}
if (!u32_FirstConvert)
{
CString s_Leave = s_CommandLine.Left(i);
CString s_Conv = s_CommandLine.Mid (i);
char* s8_OEM = new char[s_Conv.GetLength()+1];
// If Unicode compiled: Unicode -> DOS codepage
// If MBCS compiled: ANSI -> DOS codepage
CharToOem(s_Conv, s8_OEM);
// If Unicode compiled: DOS codepage -> Unicode
s_CommandLine = s_Leave + s8_OEM;
delete s8_OEM;
break;
}
}
return s_CommandLine;
}
// Fills the CMap with the data in the zero terminated string "Key1=Value1\0Key2=Value2\0"
// If the map already contains a key it will be overriden
// returns the length of all added string data
int CRunProcess::ZeroStringToMap(const TCHAR* t_String, CMapStringToString *pi_Map)
{
int s32_TotLen = 0;
while (1)
{
CString s_String = t_String;
int s32_Len = s_String.GetLength();
if (!s32_Len)
break;
int s32_Equal = s_String.Find('=');
if (s32_Equal > 0)
{
CString s_Key = s_String.Left(s32_Equal);
CString s_Val = s_String.Mid (s32_Equal+1);
s_Key.MakeUpper();
pi_Map->SetAt(s_Key, s_Val);
}
t_String += s32_Len +1;
s32_TotLen += s32_Len +1;
};
return s32_TotLen;
}
// Fills the buffer t_String with zero terminated strings "Key1=Value1\0Key2=Value2\0"
// which are obtained from the Map
void CRunProcess::MapToZeroString(CMapStringToString *pi_Map, TCHAR* t_String)
{
CString s_Key, s_Val;
for (POSITION P=pi_Map->GetStartPosition(); P!=NULL; )
{
pi_Map->GetNextAssoc(P, s_Key, s_Val);
_tcscpy(t_String, s_Key);
t_String += s_Key.GetLength();
t_String[0] = '=';
t_String ++;
_tcscpy(t_String, s_Val);
t_String += s_Val.GetLength();
t_String[0] = 0;
t_String ++;
}
t_String[0] = 0;
}
// s_UserVar = "Key1=Value1\nKey2=Value2\n"
// Merges the User Environment Variables with the current System Environment variables
// and returns them in the buffer ps_Out as zero terminated strings
void CRunProcess::MergeUserEnvironmentStrings(CString s_UserVar, CString* ps_Out)
{
TCHAR* t_Environ = GetEnvironmentStrings();
CMapStringToString i_Map;
int s32_TotLen = ZeroStringToMap(t_Environ, &i_Map);
FreeEnvironmentStrings(t_Environ);
if (s_UserVar.GetLength())
{
if (s_UserVar.Right(1) != _T("\n"))
s_UserVar += _T("\n");
s_UserVar.Replace('\n', '\0');
s32_TotLen += ZeroStringToMap(s_UserVar, &i_Map);
}
TCHAR* t_Buf = ps_Out->GetBuffer(s32_TotLen);
MapToZeroString(&i_Map, t_Buf);
}