|
|
//+---------------------------------------------------------------------------
//
// Microsoft Windows
// Copyright (C) Microsoft Corporation, 1992 - 1995
//
// File: process.cxx
//
// Contents: Implementation of the CProcessThread class
//
//----------------------------------------------------------------------------
#include "headers.hxx"
#include "stdlib.h"
DeclareTag(tagProcess, "MTScript", "Monitor Process Creation");
char *g_pszExitCodeStr = "PROCESS_EXIT_CODE=";
#define PIPEREAD_TIMEOUT 2
CProcessParams::CProcessParams() { pszCommand = 0; pszDir = 0; pszTitle = 0; }
CProcessParams::~CProcessParams() { Free(); }
bool CProcessParams::Copy(const PROCESS_PARAMS *params) { return Assign(*params); }
void CProcessParams::Free() { SysFreeString(pszCommand); SysFreeString(pszDir); SysFreeString(pszTitle);
pszCommand = 0; pszDir = 0; pszTitle = 0; }
bool CProcessParams::Assign(const PROCESS_PARAMS ¶ms) { pszCommand = SysAllocString(params.pszCommand); pszDir = SysAllocString(params.pszDir); pszTitle = SysAllocString(params.pszTitle); fMinimize = params.fMinimize; fGetOutput = params.fGetOutput; fNoEnviron = params.fNoEnviron; fNoCrashPopup = params.fNoCrashPopup;
if (params.pszCommand && !pszCommand || params.pszDir && !pszDir || params.pszTitle && !pszTitle) { return false; } return true; }
//+---------------------------------------------------------------------------
//
// Member: CProcessThread::CProcessThread, public
//
// Synopsis: ctor
//
//----------------------------------------------------------------------------
CProcessThread::CProcessThread(CScriptHost *pSH) : _cstrOutput(CSTR_NOINIT) { _ulRefs = 1;
_pSH = pSH; _pSH->AddRef();
// This object should be initialized to all zero.
Assert(_hPipe == NULL); Assert(_hJob == NULL); Assert(_hIoPort == NULL); Assert(_dwExitCode == 0); Assert(_piProc.hProcess == NULL); }
//+---------------------------------------------------------------------------
//
// Member: CProcessThread::~CProcessThread, public
//
// Synopsis: dtor
//
//----------------------------------------------------------------------------
CProcessThread::~CProcessThread() { AssertSz(!_hJob, "NONFATAL: Job handle should be NULL!"); AssertSz(!_hIoPort, "NONFATAL: Port handle should be NULL!");
TraceTag((tagProcess, "Closing process PID=%d", ProcId())); if (_hPipe) CloseHandle(_hPipe);
if (_piProc.hProcess) CloseHandle(_piProc.hProcess);
ReleaseInterface(_pSH); }
//+---------------------------------------------------------------------------
//
// Member: CProcessThread::QueryInterface, public
//
// Synopsis: Standard implementation. This class implements no meaningful
// interfaces.
//
//----------------------------------------------------------------------------
HRESULT CProcessThread::QueryInterface(REFIID iid, void **ppvObj) { if (iid == IID_IUnknown) { *ppvObj = (IUnknown *)this; } else { *ppvObj = NULL; return E_NOINTERFACE; }
((IUnknown *)*ppvObj)->AddRef(); return S_OK; }
//+---------------------------------------------------------------------------
//
// Member: CProcessThread::Init, public
//
// Synopsis: Initializes the class
//
//----------------------------------------------------------------------------
BOOL CProcessThread::Init() { //$ FUTURE: For NT4 and Win9x support we will need to dynamically use the
// job APIs since those platforms do not support them.
_hJob = CreateJobObject(NULL, NULL); if (!_hJob) { TraceTag((tagError, "CreateJobObject failed with %d", GetLastError())); return FALSE; }
return CThreadComm::Init(); }
//+---------------------------------------------------------------------------
//
// Member: CProcessThread::GetProcessOutput, public
//
// Synopsis: Returns the collected STDOUT/STDERR data from the process
// up to this point.
//
// Arguments: [pbstrOutput] -- Pointer to BSTR where the string should be
// copied. The BSTR will be allocated.
//
// Returns: HRESULT
//
//----------------------------------------------------------------------------
HRESULT CProcessThread::GetProcessOutput(BSTR *pbstrOutput) { LOCK_LOCALS(this);
return _cstrOutput.AllocBSTR(pbstrOutput); }
//+---------------------------------------------------------------------------
//
// Member: CProcessThread::GetExitCode, public
//
// Synopsis: Returns the exit code of the process. If the process is still
// running, it returns STILL_ACTIVE (0x103, dec 259)
//
//----------------------------------------------------------------------------
DWORD CProcessThread::GetExitCode() { DWORD dwExit;
AssertSz(_piProc.hProcess, "FATAL: Bad process handle");
GetExitCodeProcess(_piProc.hProcess, &dwExit);
if (dwExit == STILL_ACTIVE || !_fUseExitCode) { return STILL_ACTIVE; }
return _dwExitCode; }
//+---------------------------------------------------------------------------
//
// Member: CProcessThread::GetDeadTime, public
//
// Synopsis: Returns the number of ms since the process exited. If it
// hasn't exited yet, it returns 0.
//
//----------------------------------------------------------------------------
ULONG CProcessThread::GetDeadTime() { _int64 i64CurrentTime; DWORD dwExit;
AssertSz(_piProc.hProcess, "FATAL: Bad process handle");
GetExitCodeProcess(_piProc.hProcess, &dwExit);
// If the process is still running return 0.
if (dwExit == STILL_ACTIVE || !_i64ExitTime) { return 0; }
GetSystemTimeAsFileTime((FILETIME*)&i64CurrentTime);
// Calculate the difference in milli-seconds. There are 10,000
// FILETIME intervals in one second (each increment of 1 represents 100
// nano-seconds)
return (i64CurrentTime - _i64ExitTime) / 10000; }
//+---------------------------------------------------------------------------
//
// Member: CProcessThread::Terminate, public
//
// Synopsis: Exits this thread. The process we own is terminated if it's
// still running.
//
//----------------------------------------------------------------------------
void CProcessThread::Terminate() { DWORD dwCode; CProcessThread * pThis = this; BOOL fTerminated = FALSE; BOOL fFireEvent = FALSE;
VERIFY_THREAD();
TraceTag((tagProcess, "Entering Terminate"));
//
// Flush out any data the process may have written to the pipe.
//
ReadPipeData();
//
// Make sure we've received all messages from our completion port
//
CheckIoPort();
if (_piProc.hProcess) { //
// Terminate the process(es) if still running.
//
fFireEvent = TRUE;
GetExitCodeProcess(_piProc.hProcess, &dwCode);
if (dwCode == STILL_ACTIVE) { TraceTag((tagProcess, "Root process still active!"));
if (_hJob) { TerminateJobObject(_hJob, ERROR_PROCESS_ABORTED); } else { TraceTag((tagProcess, "Terminating process, not job!"));
TerminateProcess(_piProc.hProcess, ERROR_PROCESS_ABORTED); }
dwCode = ERROR_PROCESS_ABORTED;
//
// Flush out any data the process may have written to the pipe.
//
ReadPipeData();
//
// Make sure we've received all messages from our completion port
//
CheckIoPort();
fTerminated = TRUE; }
if (!_fUseExitCode) { _dwExitCode = dwCode; _fUseExitCode = TRUE; } }
if (_hIoPort) CloseHandle(_hIoPort);
if (_hJob) CloseHandle(_hJob);
_hIoPort = NULL; _hJob = NULL;
// We need to hold onto _piProc.hProcess so the system doesn't reuse the
// process ID.
if (fFireEvent) { if (fTerminated) { PostToThread(_pSH, MD_PROCESSTERMINATED, &pThis, sizeof(CProcessThread*)); } else { PostToThread(_pSH, MD_PROCESSEXITED, &pThis, sizeof(CProcessThread*)); } }
GetSystemTimeAsFileTime((FILETIME*)&_i64ExitTime);
TraceTag((tagProcess, "Exiting process thread!"));
Release();
ExitThread(0); }
//+---------------------------------------------------------------------------
//
// Member: CProcessThread::RealThreadRoutine, public
//
// Synopsis: Main loop which runs this thread while it is active. The thread
// will terminate if this method returns. Termination normally
// happens when the client disconnects by calling Terminate().
//
//----------------------------------------------------------------------------
DWORD CProcessThread::ThreadMain() { DWORD dwRet; HANDLE ahEvents[3]; int cEvents = 2; HRESULT hr;
PROCESS_PARAMS *pParams = (PROCESS_PARAMS *)_pvParams;
_ProcParams.Copy(pParams);
#if DBG == 1
{ char achBuf[10]; CStr cstrCmd;
cstrCmd.Set(pParams->pszCommand); cstrCmd.GetMultiByte(achBuf, 10); SetName(achBuf); } #endif
AddRef();
hr = LaunchProcess(pParams);
ThreadStarted(hr); // Release our calling thread
if (hr) { Terminate(); return 1; // Just in case
}
ahEvents[0] = _hCommEvent; ahEvents[1] = _piProc.hProcess;
while (TRUE) { //
// Anonymous pipes don't support asynchronous I/O, and we can't wait
// on a handle to see if there's data on the completion port. So,
// what we do is poll for those every 2 seconds instead.
//
dwRet = WaitForMultipleObjects(cEvents, ahEvents, FALSE, (_hPipe || _hIoPort) ? PIPEREAD_TIMEOUT * 1000 : INFINITE); if (dwRet == WAIT_OBJECT_0) { //
// Another thread is sending us a message.
//
HandleThreadMessage(); } else if (dwRet == WAIT_OBJECT_0 + 1) { //
// The process terminated.
//
HandleProcessExit(); } else if (dwRet == WAIT_TIMEOUT) { ReadPipeData();
CheckIoPort(); } else if (dwRet == WAIT_FAILED) { AssertSz(FALSE, "NONFATAL: WaitForMultipleObjectsFailure");
break; } }
// Make sure we clean everything up OK
Terminate();
return 0; }
//+---------------------------------------------------------------------------
//
// Member: CProcessThread::HandleThreadMessage, public
//
// Synopsis: Handles any messages other threads send us.
//
//----------------------------------------------------------------------------
void CProcessThread::HandleThreadMessage() { THREADMSG tm; BYTE bData[MSGDATABUFSIZE]; DWORD cbData;
while (GetNextMsg(&tm, (void **)bData, &cbData)) { switch (tm) { case MD_PLEASEEXIT: //
// We're being asked to terminate.
//
Terminate(); break;
default: AssertSz(FALSE, "FATAL: CProcessThread got a message it couldn't handle!"); break; } } }
//+---------------------------------------------------------------------------
//
// Member: CProcessThread::HandleProcessExit, public
//
// Synopsis: The process we started has exited. Send out the status and
// exit.
//
//----------------------------------------------------------------------------
void CProcessThread::HandleProcessExit() { //
// Flush out any data the process may have written to the pipe.
//
ReadPipeData();
// Save the exit code
if (!_fUseExitCode) { GetExitCodeProcess(_piProc.hProcess, &_dwExitCode);
_fUseExitCode = TRUE;
AssertSz(_dwExitCode != STILL_ACTIVE, "LEAK: Exited process still active!"); }
Terminate(); }
//+---------------------------------------------------------------------------
//
// Member: CProcessThread::IsDataInPipe, public
//
// Synopsis: Is there any data waiting to be read from the anonymous pipe?
//
//----------------------------------------------------------------------------
BOOL CProcessThread::IsDataInPipe() { DWORD dwBytesAvail = 0; BOOL fSuccess;
if (!_hPipe) return FALSE;
fSuccess = PeekNamedPipe(_hPipe, NULL, NULL, NULL, &dwBytesAvail, NULL);
if (!fSuccess) { //
// Stop trying to read from the pipe
//
CloseHandle(_hPipe); _hPipe = NULL; }
return (dwBytesAvail != 0); }
//+---------------------------------------------------------------------------
//
// Member: CProcessThread::ReadPipeData, public
//
// Synopsis: Read any and all data which is waiting on the pipe. This will
// be anything that the process writes to STDOUT.
//
//----------------------------------------------------------------------------
void CProcessThread::ReadPipeData() { BOOL fSuccess = TRUE; DWORD cbRead; char *pch;
while (IsDataInPipe()) { CStr cstrTemp;
fSuccess = ReadFile(_hPipe, _abBuffer, PIPE_BUFFER_SIZE - 1, &cbRead, NULL);
if (!fSuccess || cbRead == 0) { Terminate();
return; // just in case
}
_abBuffer[cbRead] = '\0';
//
// Is the process letting us know that we should pretend the exit code
// is some value other than the actual exit code?
// (using PROCESS_EXIT_CODE=x in its STDOUT stream)
//
pch = strstr((const char*)_abBuffer, g_pszExitCodeStr); if (pch) { _dwExitCode = atol(pch + strlen(g_pszExitCodeStr)); _fUseExitCode = TRUE; }
// The string is now in _abBuffer. Add it to our buffer.
//
// Assume the data coming across is MultiByte string data.
//
LOCK_LOCALS(this);
_cstrOutput.AppendMultiByte((const char *)_abBuffer); } }
//+---------------------------------------------------------------------------
//
// Member: CProcessThread::CheckIoPort, public
//
// Synopsis: Check the CompletionPort that we are using to receive messages
// from the job object. This will tell us if any process we
// created or that is a child of the one we created crashed.
//
//----------------------------------------------------------------------------
void CProcessThread::CheckIoPort() { DWORD dwCode = 0; DWORD dwKey = 0; DWORD dwParam = 0;;
if (!_hIoPort) { return; }
while (GetQueuedCompletionStatus(_hIoPort, &dwCode, &dwKey, (LPOVERLAPPED*)&dwParam, 0)) { AssertSz(dwKey == (DWORD)this, "NONFATAL: Bogus port value from GetQueuedCompletionStatus");
switch (dwCode) { case JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS:
{ CProcessThread *pThis = this;
// There is no reliable way to get the actual exit code, since
// the process is already gone and we don't have a handle to it.
TraceTag((tagProcess, "Process %d crashed!", dwParam));
PostToThread(_pSH, MD_PROCESSCRASHED, &pThis, sizeof(CProcessThread*)); }
break;
case JOB_OBJECT_MSG_EXIT_PROCESS: TraceTag((tagProcess, "Process %d exited.", dwParam)); break;
case JOB_OBJECT_MSG_NEW_PROCESS:
TraceTag((tagProcess, "Process %d started.", dwParam)); // Remember the process id so we can use it later
_aryProcIds.Append(dwParam); break;
default: break; } }
DWORD dwError = GetLastError();
if (dwError != WAIT_TIMEOUT) { TraceTag((tagProcess, "GetQueuedCompletionStatus returned %d", dwError)); }
return; }
//+---------------------------------------------------------------------------
//
// Member: CProcessThread::LaunchProcess, public
//
// Synopsis: Start the child process for the given command.
//
//----------------------------------------------------------------------------
HRESULT CProcessThread::LaunchProcess(const PROCESS_PARAMS *pProcParams) { BOOL fSuccess; TCHAR achCommand[MAX_PATH * 2]; TCHAR achDir[MAX_PATH]; BOOL fUseDir; HRESULT hr = S_OK; CStr cstrEnvironment;
STARTUPINFO si = { 0 }; HANDLE hPipeStdout = NULL; HANDLE hPipeStderr = NULL;
if (pProcParams->fGetOutput) { //
// Setup the anonymous pipe which we will use to get status from the
// process. Anytime it writes to STDOUT or STDERR it will come across
// this pipe to us.
//
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE;
fSuccess = CreatePipe(&_hPipe, &hPipeStdout, &sa, 0); if (!fSuccess) { AssertSz(FALSE, "FATAL: Could not create anonymous pipe");
goto Win32Error; }
//
// Change our end of the pipe to non-inheritable so the new process
// doesn't pick it up.
//
fSuccess = DuplicateHandle(GetCurrentProcess(), _hPipe, GetCurrentProcess(), NULL, 0, FALSE, DUPLICATE_SAME_ACCESS); if (!fSuccess) { AssertSz(FALSE, "FATAL: Error removing inheritance from handle!");
goto Win32Error; }
//
// Now duplicate the stdout handle for stderr
//
fSuccess = DuplicateHandle(GetCurrentProcess(), hPipeStdout, GetCurrentProcess(), &hPipeStderr, 0, TRUE, DUPLICATE_SAME_ACCESS); if (!fSuccess) { AssertSz(FALSE, "Error duplicating stdout handle!");
goto Win32Error; } } else { hPipeStdout = GetStdHandle(STD_OUTPUT_HANDLE); hPipeStderr = GetStdHandle(STD_ERROR_HANDLE); }
if (_hJob) { JOBOBJECT_EXTENDED_LIMIT_INFORMATION joLimit = { 0 }; JOBOBJECT_ASSOCIATE_COMPLETION_PORT joPort = { 0 };
if (pProcParams->fNoCrashPopup) { // Force crashes to terminate the job (and all processes in it).
joLimit.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION;
fSuccess = SetInformationJobObject(_hJob, JobObjectExtendedLimitInformation, &joLimit, sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); if (!fSuccess) // Ignore failures except to report them.
{ TraceTag((tagError, "SetInformationJobObject failed with %d", GetLastError())); } }
// Now we need to setup a completion port so we can find out if one
// of the processes crashed.
_hIoPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, (DWORD)this, 0);
if (_hIoPort) { joPort.CompletionKey = this; joPort.CompletionPort = _hIoPort;
fSuccess = SetInformationJobObject(_hJob, JobObjectAssociateCompletionPortInformation, &joPort, sizeof(JOBOBJECT_ASSOCIATE_COMPLETION_PORT));
if (!fSuccess) // Ignore failures except to report them.
{ TraceTag((tagError, "Failed to set completion port on job: %d", GetLastError())); } } else { TraceTag((tagError, "CreateIoCompletionPort failed with %d!", GetLastError())); } }
si.cb = sizeof(STARTUPINFO);
if (pProcParams->fMinimize) { si.wShowWindow = SW_SHOWMINNOACTIVE; } else { si.wShowWindow = SW_SHOWNORMAL; }
if (pProcParams->pszTitle != NULL && _tcslen(pProcParams->pszTitle) > 0) { si.lpTitle = pProcParams->pszTitle; }
//
// Setup our inherited standard handles.
//
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); si.hStdOutput = hPipeStdout; si.hStdError = hPipeStderr;
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
if (!pProcParams->fGetOutput) { si.dwFlags &= ~STARTF_USESTDHANDLES; }
//
// Expand environment strings in the command name as well as the working
// dir.
//
ExpandEnvironmentStrings(pProcParams->pszCommand, achCommand, MAX_PATH * 2);
fUseDir = pProcParams->pszDir && _tcslen(pProcParams->pszDir) > 0;
if (fUseDir) { WCHAR *psz; WCHAR achDir2[MAX_PATH];
ExpandEnvironmentStrings(pProcParams->pszDir, achDir2, MAX_PATH); GetFullPathName(achDir2, MAX_PATH, achDir, &psz); }
GetProcessEnvironment(&cstrEnvironment, pProcParams->fNoEnviron);
TraceTag((tagProcess, "Launching Process: %ls in %ls", achCommand, achDir));
//
// Let's do it!
//
fSuccess = CreateProcess(NULL, achCommand, NULL, NULL, TRUE, BELOW_NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT, cstrEnvironment, (fUseDir) ? achDir : NULL, &si, &_piProc); if (!fSuccess) goto Win32Error;
if (_hJob) { if (!AssignProcessToJobObject(_hJob, _piProc.hProcess)) { TraceTag((tagError, "AssignProcessToJobObject failed with %d", GetLastError())); CloseHandle(_hJob); _hJob = NULL; } }
TraceTag((tagProcess, "Created process PID=%d %ls", _piProc.dwProcessId, achCommand)); CloseHandle(_piProc.hThread);
Cleanup: if (pProcParams->fGetOutput) { //
// The write handles have been inherited by the child process, so we
// let go of them.
//
CloseHandle(hPipeStdout); CloseHandle(hPipeStderr); }
return hr;
Win32Error:
hr = HRESULT_FROM_WIN32(GetLastError());
TraceTag((tagProcess, "Error creating process: %x", hr));
goto Cleanup; }
//+---------------------------------------------------------------------------
//
// Member: CProcessThread::GetProcessEnvironment, public
//
// Synopsis: Builds an environment block for the new process that has
// our custom id so we can identify them.
//
// Arguments: [pcstr] -- Place to put environment block
// [fNoEnviron] -- If TRUE, don't inherit the environment block
//
//----------------------------------------------------------------------------
void CProcessThread::GetProcessEnvironment(CStr *pcstr, BOOL fNoEnviron) { TCHAR *pszEnviron; TCHAR achNewVar[50]; int cch = 1; TCHAR *pch;
static long s_lProcID = 0;
_lEnvID = InterlockedIncrement(&s_lProcID);
wsprintf(achNewVar, _T("__MTSCRIPT_ENV_ID=%d\0"), _lEnvID);
if (!fNoEnviron) { pszEnviron = GetEnvironmentStrings();
pch = pszEnviron; cch = 2; // Always have two terminating nulls at least
while (*pch || *(pch+1)) { pch++; cch++; }
pcstr->Set(NULL, cch + _tcslen(achNewVar));
memcpy((LPTSTR)*pcstr, pszEnviron, cch * sizeof(TCHAR));
FreeEnvironmentStrings(pszEnviron); } else { pcstr->Set(NULL, _tcslen(achNewVar) + 1); }
memcpy((LPTSTR)*pcstr + cch - 1, achNewVar, (_tcslen(achNewVar)+1) * sizeof(TCHAR)); }
|