#include "stdafx.h" #include "ProcessExecute.h" #include "WcharMbcsConverter.h" /* The Console Redirection is taken from the TagsView plugin from Vitaliy Dovgan. * My thanks to him for pointing me in the right direction. :) * And of course for NppExec, without which, Notepad++ would only * be half as powerful. */ #define DEFAULT_PIPE_SIZE 1 #define PIPE_READBUFSIZE 4096 const char *ProcessExecute::STREAM_NAME_STDOUT = "OUT"; const char *ProcessExecute::STREAM_NAME_STDERR = "ERR"; ProcessExecute::ProcessExecute() { } ProcessExecute::~ProcessExecute() { } bool ProcessExecute::isWindowsNT() { OSVERSIONINFO osv; osv.dwOSVersionInfoSize = sizeof(osv); ::GetVersionEx(&osv); return (osv.dwPlatformId >= VER_PLATFORM_WIN32_NT); } DWORD ProcessExecute::execute(const TCHAR *commandLine, boost::python::object pyStdout, boost::python::object pyStderr, boost::python::object /*pyStdin*/, bool spoolToFile /* = false */) { DWORD returnValue = 0; if (pyStdout.is_none()) throw process_start_exception("stdout cannot be None"); if (pyStderr.is_none()) throw process_start_exception("stderr cannot be None"); // Create out, err, and in pipes (ignore in, initially) SECURITY_DESCRIPTOR sd; SECURITY_ATTRIBUTES sa; process_start_exception exceptionThrown(""); bool thrown = false; PipeReaderArgs stdoutReaderArgs; PipeReaderArgs stderrReaderArgs; // Only used if spooling, but we need to delete it later. TCHAR tmpFilename[MAX_PATH] = {0}; HANDLE hStdOutReadPipe, hStdOutWritePipe; HANDLE hStdErrReadPipe, hStdErrWritePipe; Py_BEGIN_ALLOW_THREADS try { if (isWindowsNT()) { ::InitializeSecurityDescriptor( &sd, SECURITY_DESCRIPTOR_REVISION ); ::SetSecurityDescriptorDacl( &sd, TRUE, NULL, FALSE ); sa.lpSecurityDescriptor = &sd; } else { sa.lpSecurityDescriptor = NULL; } sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.bInheritHandle = TRUE; if (!::CreatePipe(&hStdOutReadPipe, &hStdOutWritePipe, &sa, DEFAULT_PIPE_SIZE)) { throw process_start_exception("Error creating pipe for stdout"); } if (!::CreatePipe(&hStdErrReadPipe, &hStdErrWritePipe, &sa, DEFAULT_PIPE_SIZE)) { throw process_start_exception("Error creating pipe for stderr"); } HANDLE stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL); DWORD dwThreadId; stdoutReaderArgs.processExecute = this; stdoutReaderArgs.hPipeRead = hStdOutReadPipe; stdoutReaderArgs.hPipeWrite = hStdOutWritePipe; stdoutReaderArgs.pythonFile = pyStdout; stdoutReaderArgs.stopEvent = stopEvent; stdoutReaderArgs.completedEvent = CreateEvent(NULL, FALSE, FALSE, NULL); stdoutReaderArgs.streamName = STREAM_NAME_STDOUT; stdoutReaderArgs.toFile = false; stderrReaderArgs.processExecute = this; stderrReaderArgs.hPipeRead = hStdErrReadPipe; stderrReaderArgs.hPipeWrite = hStdErrWritePipe; stderrReaderArgs.pythonFile = pyStderr; stderrReaderArgs.stopEvent = stopEvent; stderrReaderArgs.completedEvent = CreateEvent(NULL, FALSE, FALSE, NULL); stderrReaderArgs.streamName = STREAM_NAME_STDERR; stderrReaderArgs.toFile = false; /* If we're in an event, we need to spool the output to a file * first, before we write it to python - so that the python writing is * done in *this* thread */ if (spoolToFile) { stdoutReaderArgs.toFile = true; stderrReaderArgs.toFile = true; /// Create the mutex for writing to the file stdoutReaderArgs.fileMutex = CreateMutex(NULL, FALSE, NULL); stderrReaderArgs.fileMutex = stdoutReaderArgs.fileMutex; /// Create the temp file TCHAR tmpPath[MAX_PATH]; GetTempPath(MAX_PATH, tmpPath); if(!GetTempFileName(tmpPath, _T("py"), 0, tmpFilename)) { throw process_start_exception("Error creating temporary filename for output spooling"); } stdoutReaderArgs.file = new std::fstream(tmpFilename, std::fstream::binary | std::fstream::in | std::fstream::out); stderrReaderArgs.file = stdoutReaderArgs.file; /* stdoutReaderArgs.fileHandle = CreateFile(tmpFilename, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); stderrReaderArgs.fileHandle = stdoutReaderArgs.fileHandle; if (INVALID_HANDLE_VALUE == stdoutReaderArgs.fileHandle) { throw process_start_exception("Error opening temporary file for output spooling"); } */ } // start thread functions for stdout and stderr HANDLE hStdoutThread = CreateThread( NULL, // no security attribute 0, // default stack size pipeReader, // thread proc (LPVOID) &stdoutReaderArgs, // thread parameter 0, // not suspended &dwThreadId); // returns thread ID HANDLE hStderrThread = CreateThread( NULL, // no security attribute 0, // default stack size pipeReader, // thread proc (LPVOID) &stderrReaderArgs, // thread parameter 0, // not suspended &dwThreadId); // returns thread ID // start process PROCESS_INFORMATION pi; STARTUPINFO si; ::SetHandleInformation(hStdOutReadPipe, HANDLE_FLAG_INHERIT, 0); ::SetHandleInformation(hStdErrReadPipe, HANDLE_FLAG_INHERIT, 0); // initialize STARTUPINFO struct ::ZeroMemory( &si, sizeof(STARTUPINFO) ); si.cb = sizeof(STARTUPINFO); si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES; si.wShowWindow = SW_HIDE; si.hStdInput = NULL; si.hStdOutput = hStdOutWritePipe; si.hStdError = hStdErrWritePipe; ::ZeroMemory( &pi, sizeof(PROCESS_INFORMATION) ); size_t commandLineLength = _tcslen(commandLine) + 1; // We use an auto_ptr here because of a potential early out due to an exception thrown. std::auto_ptr cmdLine(new TCHAR[commandLineLength]); _tcscpy_s(cmdLine.get(), commandLineLength, commandLine); bool processStartSuccess; if ( ::CreateProcess( NULL, cmdLine.get(), NULL, // security NULL, // security TRUE, // inherits handles CREATE_NEW_PROCESS_GROUP, // creation flags NULL, // environment NULL, // current directory &si, // startup info &pi // process info )) { // wait for process to exit ::WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hThread); processStartSuccess = true; } else { processStartSuccess = false; } // Stop the reader threads SetEvent(stopEvent); // Wait for the pipe reader threads to complete HANDLE handles[] = { stdoutReaderArgs.completedEvent, stderrReaderArgs.completedEvent }; WaitForMultipleObjects(2, handles, TRUE, INFINITE); CloseHandle(stdoutReaderArgs.completedEvent); CloseHandle(stderrReaderArgs.completedEvent); CloseHandle(hStdoutThread); CloseHandle(hStderrThread); CloseHandle(stopEvent); CloseHandle(hStdOutReadPipe); CloseHandle(hStdOutWritePipe); CloseHandle(hStdErrReadPipe); CloseHandle(hStdErrWritePipe); if (processStartSuccess) { GetExitCodeProcess(pi.hProcess, &returnValue); CloseHandle(pi.hProcess); } else { DWORD errorNo = ::GetLastError(); TCHAR *buffer; ::FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER, NULL, errorNo, 0, reinterpret_cast(&buffer), 0, NULL); std::shared_ptr message = WcharMbcsConverter::tchar2char(buffer); process_start_exception ex(message.get()); ::LocalFree(buffer); throw ex; } } catch(process_start_exception& ex) { exceptionThrown = ex; thrown = true; } Py_END_ALLOW_THREADS if (spoolToFile) { CloseHandle(stdoutReaderArgs.fileMutex); spoolFile(stdoutReaderArgs.file, pyStdout, pyStderr); stdoutReaderArgs.file->close(); if (tmpFilename[0] != 0) { DeleteFile(tmpFilename); } } if (thrown) { throw exceptionThrown; } return returnValue; } DWORD WINAPI ProcessExecute::pipeReader(void *args) { PipeReaderArgs* pipeReaderArgs = reinterpret_cast(args); DWORD bytesRead; char buffer[PIPE_READBUFSIZE]; BOOL processFinished = FALSE; for(;;) { ::PeekNamedPipe(pipeReaderArgs->hPipeRead, NULL, 0, NULL, &bytesRead, NULL); if (processFinished && 0 == bytesRead) { break; } if (bytesRead > 0) { if (ReadFile(pipeReaderArgs->hPipeRead, buffer, PIPE_READBUFSIZE - 1, &bytesRead, NULL)) { if (pipeReaderArgs->toFile) { pipeReaderArgs->processExecute->writeToFile(pipeReaderArgs, bytesRead, buffer); } else { pipeReaderArgs->processExecute->writeToPython(pipeReaderArgs, bytesRead, buffer); } } } else { DWORD handleIndex = WaitForSingleObject(pipeReaderArgs->stopEvent, 100); if (WAIT_OBJECT_0 == handleIndex) { processFinished = TRUE; } } } SetEvent(pipeReaderArgs->completedEvent); return 0; } void ProcessExecute::writeToPython(PipeReaderArgs *pipeReaderArgs, DWORD bytesRead, char *buffer) { buffer[bytesRead] = '\0'; PyGILState_STATE gstate = PyGILState_Ensure(); try { pipeReaderArgs->pythonFile.attr("write")(boost::python::str(const_cast(buffer))); } catch(...) { PyErr_Print(); } PyGILState_Release(gstate); } void ProcessExecute::writeToFile(PipeReaderArgs *pipeReaderArgs, DWORD bytesRead, char *buffer) { WaitForSingleObject(pipeReaderArgs->fileMutex, INFINITE); pipeReaderArgs->file->write(pipeReaderArgs->streamName, ProcessExecute::STREAM_NAME_LENGTH); if (pipeReaderArgs->file->bad()) throw process_start_exception("Error writing to spool file"); char byteCount[20]; sprintf_s(byteCount, 20, "%ul\n", bytesRead); pipeReaderArgs->file->write(byteCount, strlen(byteCount)); pipeReaderArgs->file->write(buffer, bytesRead); if (pipeReaderArgs->file->bad()) throw process_start_exception("Error writing buffer to spool file"); ReleaseMutex(pipeReaderArgs->fileMutex); } void ProcessExecute::spoolFile(std::fstream* file, boost::python::object pyStdout, boost::python::object pyStderr) { // Rewind file->seekg(0); char infoBuffer[30]; char *buffer = NULL; size_t bufferSize = 0; size_t bytesToRead; while (!file->eof()) { file->getline(infoBuffer, 30, '\n'); if (file->eof()) break; bytesToRead = strtoul(infoBuffer + STREAM_NAME_LENGTH, NULL, 0); if (bufferSize < bytesToRead || !buffer) { if (buffer) delete[] buffer; bufferSize = bytesToRead + 1; buffer = new char[bufferSize]; } file->read(buffer, bytesToRead); buffer[bytesToRead] = '\0'; if (0 == strncmp(infoBuffer, STREAM_NAME_STDOUT, STREAM_NAME_LENGTH)) { // Is a stdout message pyStdout.attr("write")(boost::python::str(const_cast(buffer))); } else { // is a stderr message pyStderr.attr("write")(boost::python::str(const_cast(buffer))); } } if (buffer) { delete[] buffer; } }