diff --git a/Build/OSXi386/PerforcePlugin b/Build/OSXi386/PerforcePlugin new file mode 100755 index 0000000..f3c1b51 Binary files /dev/null and b/Build/OSXi386/PerforcePlugin differ diff --git a/Makefile.osx b/Makefile.osx index 2c2ea2c..93563ad 100644 --- a/Makefile.osx +++ b/Makefile.osx @@ -12,6 +12,9 @@ PLATFORM = OSXi386 COMMON_MODULES = $(COMMON_SRCS:.c=.o) COMMON_MODULES := $(COMMON_MODULES:.cpp=.o) +TESTSERVER_MODULES = $(TESTSERVER_SRCS:.c=.o) +TESTSERVER_TARGET= Build/$(PLATFORM)/TestServer + P4PLUGIN_MODULES = $(P4PLUGIN_SRCS:.c=.o) P4PLUGIN_MODULES := $(P4PLUGIN_MODULES:.cpp=.o) P4PLUGIN_TARGET = PerforcePlugin @@ -22,14 +25,27 @@ SVNPLUGIN_TARGET = SubversionPlugin default: all -all: P4Plugin SvnPlugin +all: P4Plugin SvnPlugin + +test: all testserver testp4 testsvn + @echo "done testing" + +testp4: $(TESTSCRIPTS_GENERAL) ${P4TESTSCRIPTS} + @echo "Running perforce tests" + @./Test/run_tests.sh ${TESTSERVER_TARGET} ./${P4PLUGIN_TARGET} nonverbose ${^} + +testsvn: + + +testserver: $(TESTSERVER_TARGET) + @mkdir -p Build/$(PLATFORM) P4Plugin: $(P4PLUGIN_TARGET) - mkdir -p Build/$(PLATFORM) + @mkdir -p Build/$(PLATFORM) cp $(P4PLUGIN_TARGET) Build/$(PLATFORM) SvnPlugin: $(SVNPLUGIN_TARGET) - mkdir -p Build/$(PLATFORM) + @mkdir -p Build/$(PLATFORM) cp $(SVNPLUGIN_TARGET) Build/$(PLATFORM) Common: $(COMMON_MODULES) @@ -37,12 +53,18 @@ Common: $(COMMON_MODULES) Common/%.o : Common/%.cpp $(COMMON_INCLS) $(CXX) $(CXXFLAGS) $(INCLUDE) -c $< -o $@ +Test/Source/%.o : Test/Source/%.cpp $(TESTSERVER_INCLS) + $(CXX) $(CXXFLAGS) -c $< -o $@ + P4Plugin/Source/%.o : P4Plugin/Source/%.cpp $(COMMON_INCLS) $(P4PLUGIN_INCLS) $(CXX) $(CXXFLAGS) $(P4PLUGIN_INCLUDE) -c $< -o $@ SvnPlugin/Source/%.o : SvnPlugin/Source/%.cpp $(COMMON_INCLS) $(SVNPLUGIN_INCLS) $(CXX) $(CXXFLAGS) $(SVNPLUGIN_INCLUDE) -c $< -o $@ +$(TESTSERVER_TARGET): $(TESTSERVER_MODULES) + $(CXX) -arch i386 -o $@ $^ + $(P4PLUGIN_TARGET): $(COMMON_MODULES) $(P4PLUGIN_MODULES) $(CXX) -arch i386 -o $@ -framework Cocoa $^ ./P4Plugin/Source/r12.2/lib/osx32/libssl.a ./P4Plugin/Source/r12.2/lib/osx32/libcrypto.a -L./P4Plugin/Source/r12.2/lib/osx32 $(P4PLUGIN_LINK) diff --git a/Makefile.srcs b/Makefile.srcs index ccbfafe..d0ef0ac 100644 --- a/Makefile.srcs +++ b/Makefile.srcs @@ -24,6 +24,11 @@ COMMON_INCLS = ./Common/Changes.h \ ./Common/Log.h \ ./Common/POpen.h +TESTSERVER_SRCS = ./Test/Source/ExternalProcess_Posix.cpp \ + ./Test/Source/TestServer.cpp +TESTSERVER_INCLS = ./Test/Source/ExternalProcess.h +TESTSCRIPTS_GENERAL = ./Test/ProtocolNegotiation.txt + P4PLUGIN_SRCS = ./P4Plugin/Source/P4Plugin_Posix.cpp \ ./P4Plugin/Source/P4AddCommand.cpp \ ./P4Plugin/Source/P4ChangeDescriptionCommand.cpp \ @@ -67,6 +72,7 @@ P4PLUGIN_INCLS = ./P4Plugin/Source/P4Command.h \ P4PLUGIN_LINK = -lclient -lrpc -lsupp P4PLUGIN_INCLUDE = -I./Common -I./P4Plugin/Source/r12.2/include/p4 -I./P4Plugin/Source +P4TESTSCRIPTS = ./Test/Perforce/Traits.txt SVNPLUGIN_SRCS = ./SvnPlugin/Source/SvnPlugin_OSX.cpp \ ./SvnPlugin/Source/SvnTask.cpp @@ -96,3 +102,5 @@ SVNPLUGIN_INCLS = ./SvnPlugin/Source/AllSvnCommands.h \ SVNPLUGIN_LINK = SVNPLUGIN_INCLUDE = -I./Common -I./SvnPlugin/Source +SVNTESTSCRIPTS = + diff --git a/Test/Perforce/Traits.txt b/Test/Perforce/Traits.txt new file mode 100644 index 0000000..c17e241 --- /dev/null +++ b/Test/Perforce/Traits.txt @@ -0,0 +1,30 @@ +c:pluginConfig pluginTraits +-- +o1:4 +o8:requiresNetwork +o8:enablesCheckout +o8:enablesLocking +o8:enablesRevertUnchanged +o1:4 +o1:vcPerforceUsername +o8:Username +o8:The perforce user name +o1: +o1:1 +o1:vcPerforcePassword +o8:Password +o8:The perforce password +o1: +o1:2 +o1:vcPerforceWorkspace +o8:Workspace +o8:The perforce workspace/client +o1: +o1:1 +o1:vcPerforceServer +o8:Server +o8:The perforce server using format: hostname:port. Port hostname defaults to 'perforce' and port defaults to 1666 +o1:perforce +o1:0 +r1:end of response +-- diff --git a/Test/ProtocolNegotiation.txt b/Test/ProtocolNegotiation.txt new file mode 100644 index 0000000..12a5d2b --- /dev/null +++ b/Test/ProtocolNegotiation.txt @@ -0,0 +1,5 @@ +c:pluginConfig pluginVersions 1 +-- +o8:1 +r1:end of response +-- diff --git a/Test/Source/ExternalProcess.h b/Test/Source/ExternalProcess.h new file mode 100644 index 0000000..4cd95e7 --- /dev/null +++ b/Test/Source/ExternalProcess.h @@ -0,0 +1,81 @@ +#pragma once + +#if _WIN32 +#include "PlatformDependent/Win/WinUtils.h" +#endif + +#include +#include + +enum ExternalProcessState { + EPSTATE_NotRunning, + EPSTATE_TimeoutReading, + EPSTATE_TimeoutWriting, + EPSTATE_BrokenPipe +}; + + +class ExternalProcessException +{ +public: + ExternalProcessException(ExternalProcessState st, const std::string& msg = "") : m_State(st), m_Msg(msg) {} + const std::string& Message() const throw() { return m_Msg; } + const char* what() const throw() { return m_Msg.c_str(); } // standard +private: + const ExternalProcessState m_State; + const std::string m_Msg; +}; + + +class ExternalProcess +{ +public: + ExternalProcess(const std::string& app, const std::vector& arguments); + ~ExternalProcess(); + bool Launch(); + bool IsRunning(); + bool Write(const std::string& data); + std::string ReadLine(); + std::string PeekLine(); + void Shutdown(); + void SetReadTimeout(double secs); + double GetReadTimeout(); + void SetWriteTimeout(double secs); + double GetWriteTimeout(); +private: + void Cleanup(); + + bool m_LineBufferValid; + std::string m_LineBuffer; + std::string m_ApplicationPath; + std::vector m_Arguments; + double m_ReadTimeout; + double m_WriteTimeout; + +#if UNITY_WIN + PROCESS_INFORMATION m_Task; + + OVERLAPPED m_Overlapped; + HANDLE m_NamedPipe; + HANDLE m_Event; // Read or Write event + + // Main task writes to m_Task_stdin_wr and child task reads from m_Task_stdin_rd + winutils::AutoHandle m_Task_stdin_rd; + winutils::AutoHandle m_Task_stdin_wr; + + // Main task reads m_Task_stdout_rd and child task writes to m_Task_stdout_wr + winutils::AutoHandle m_Task_stdout_rd; + winutils::AutoHandle m_Task_stdout_wr; + + std::string m_Buffer; + +#else // posix + + pid_t m_Pid; + int m_ExitCode; + FILE *m_Input, + *m_Output; + bool m_Exited; + std::string ReadLine (FILE *file, std::string &buffer, bool removeFromBuffer); +#endif +}; diff --git a/Test/Source/ExternalProcess_Posix.cpp b/Test/Source/ExternalProcess_Posix.cpp new file mode 100644 index 0000000..6a71c71 --- /dev/null +++ b/Test/Source/ExternalProcess_Posix.cpp @@ -0,0 +1,221 @@ +#include "ExternalProcess.h" + +#include +#include + +#include +#include +#include +#include + +ExternalProcess::ExternalProcess(const std::string& app, const std::vector& arguments) : +m_LineBuffer(""), +m_LineBufferValid (false), +m_Pid(0), +m_Input(NULL), +m_Output(NULL), +m_ExitCode(-1), +m_Exited(false), +m_ApplicationPath(app), +m_Arguments(arguments), +m_ReadTimeout (120.0) +{ +} + +ExternalProcess::~ExternalProcess() +{ + Shutdown (); + fclose (m_Input); + fclose (m_Output); +} + +bool ExternalProcess::Launch() +{ + int readpipe[2]; // file descriptors for the parent to read and the child to write + int writepipe[2]; // file descriptors for the parent to write and the child to read + + // Setup communication pipeline first + if (pipe(readpipe) || pipe(writepipe)) + { + std::cerr << "Pipe error!" << std::endl; + return false; + } + + // Attempt to fork and check for errors + m_Pid = fork (); + + if (m_Pid == -1) + { + std::cerr << "fork error" << std::endl; // something went wrong + return false; + } + + if (m_Pid != 0) { + // A positive (non-negative) PID indicates the parent process + m_Input = fdopen (readpipe[0], "r"); + m_Output = fdopen (writepipe[1], "w"); + close (readpipe[1]); + close (writepipe[0]); + + if (!(m_Input && m_Output)) { + std::cerr << "error in fdopen()" << std::endl; + fclose (m_Input); + fclose (m_Output); + return false; + } + + return true; + } else { + // A zero PID indicates that this is the child process + dup2 (writepipe[0], STDIN_FILENO); // Replace stdin with the IN side of the pipe + dup2 (readpipe[1], STDOUT_FILENO); // Replace stdout with the OUT side of the pipe + close (readpipe[0]); + close (writepipe[1]); + + // Extract binary name from the path: + std::string bin = m_ApplicationPath; + int lastSlash = m_ApplicationPath.rfind ('/'); + + if (lastSlash != std::string::npos) + bin = m_ApplicationPath.substr (lastSlash + 1); + + std::vector args; + args.push_back (bin.c_str ()); + + for (std::vector::const_iterator it = m_Arguments.begin (), + end = m_Arguments.end (); it != end; ++it) + args.push_back (it->c_str ()); + + args.push_back (NULL); + + // Replace the child fork with a new process + int ret = execvp (m_ApplicationPath.c_str (), const_cast (&args[0])); + if( -1 == ret ) { + std::cerr << "Error: execvp(" << m_ApplicationPath << ") : " << strerror(errno) << std::endl; + exit (ret); + } + } + + return true; +} + +bool ExternalProcess::IsRunning() +{ + if (m_Exited) + return false; + + int status; + int retval = waitpid (m_Pid, &status, WNOHANG | WUNTRACED | WCONTINUED); + if (0 == retval) // State hasn't changed since last time + return true; + + if (WIFEXITED (status)) { + m_Exited = true; + m_ExitCode = WEXITSTATUS (status); + } else if (WIFSIGNALED (status)) { + m_Exited = true; + m_ExitCode = WTERMSIG (status); + } + return !m_Exited; +} + +bool ExternalProcess::Write(const std::string& data) +{ + fprintf (m_Output, "%s", data.c_str ()); + fflush (m_Output); + return true; +} + +std::string ExternalProcess::ReadLine (FILE *file, std::string &buffer, bool removeFromBuffer) +{ + int readyFDs, + fd = fileno (file); + fd_set fdReadSet; + std::string::size_type i = buffer.find('\n'); + int retries = 3; + struct timeval timeout; + timeout.tv_sec = (time_t) m_ReadTimeout; + timeout.tv_usec = (suseconds_t) ((m_ReadTimeout - timeout.tv_sec) * 1000000.0); + + while (retries > 0) + { + if (!IsRunning()) + throw ExternalProcessException(EPSTATE_NotRunning, "Trying to read from process that is not running"); + + if (i == std::string::npos) + { + // No newline found - read more data + + FD_ZERO(&fdReadSet); + FD_SET(fd, &fdReadSet); + readyFDs = select (fd + 1, &fdReadSet, NULL, NULL, &timeout); + + if ( readyFDs == -1 ) { + if (errno == EAGAIN) { + --retries; + continue; + } + throw ExternalProcessException(EPSTATE_BrokenPipe, "Error polling external process"); + } else if (readyFDs == 0 || !FD_ISSET (fd, &fdReadSet)) { + throw ExternalProcessException(EPSTATE_BrokenPipe, "Got unknown write ready file description while write from external process"); + } + + char charBuffer[BUFSIZ]; + ssize_t bytesread = read (fd, charBuffer, BUFSIZ-1); + if (0 >= bytesread) + throw ExternalProcessException(EPSTATE_BrokenPipe, "Error reading external process - read 0 bytes"); + + charBuffer[bytesread] = '\0'; + buffer += charBuffer; + retries = 3; + } + + i = buffer.find ('\n'); + if (std::string::npos != i) { + std::string line = buffer.substr (0, i); + if (removeFromBuffer) + buffer.erase (0, i+1); + return line; + } + } + + throw ExternalProcessException(EPSTATE_BrokenPipe, "Failed waiting for read from external process"); +} + +std::string ExternalProcess::ReadLine() +{ + return ReadLine (m_Input, m_LineBuffer, true); +} + +std::string ExternalProcess::PeekLine() +{ + return ReadLine (m_Input, m_LineBuffer, false); +} + +void ExternalProcess::Shutdown() +{ + kill (m_Pid, SIGINT); + usleep (100000); + if (IsRunning ()) + kill (m_Pid, SIGKILL); +} + +void ExternalProcess::SetReadTimeout(double secs) +{ + m_ReadTimeout = secs; +} + +double ExternalProcess::GetReadTimeout() +{ + return m_ReadTimeout; +} + +void ExternalProcess::SetWriteTimeout(double secs) +{ + m_WriteTimeout = secs; +} + +double ExternalProcess::GetWriteTimeout() +{ + return m_WriteTimeout; +} diff --git a/Test/Source/ExternalProcess_WIN.cpp b/Test/Source/ExternalProcess_WIN.cpp new file mode 100644 index 0000000..3b72ed0 --- /dev/null +++ b/Test/Source/ExternalProcess_WIN.cpp @@ -0,0 +1,420 @@ +#include "UnityPrefix.h" +#include "Editor/Platform/Interface/ExternalProcess.h" +#include "Runtime/Utilities/File.h" +#include "PlatformDependent/Win/PathUnicodeConversion.h" +#include +#include +#include + +using namespace std; + + +ExternalProcess::ExternalProcess(const std::string& app, const std::vector& arguments) +: m_LineBufferValid(false), m_ApplicationPath(app), m_Arguments(arguments), + m_ReadTimeout(30.0), m_WriteTimeout(30.0) +{ + ZeroMemory( &m_Task, sizeof(m_Task) ); + m_Task.hProcess = INVALID_HANDLE_VALUE; + m_Event = INVALID_HANDLE_VALUE; + m_NamedPipe = INVALID_HANDLE_VALUE; +} + +ExternalProcess::~ExternalProcess() +{ + Shutdown(); +} + +bool ExternalProcess::Launch() +{ + // Most of this is from MSDN's "Creating a Child Process with Redirected Input and Output", + // except for the parts where the code example is wrong :) + // + // See http://www.codeproject.com/KB/threads/redir.aspx for details on where it's wrong... + + // Make sure the plugin is not running + Shutdown(); + m_Buffer.clear(); + + // LogString(string("Launching external process ") + m_ApplicationPath); + + // Search path if app is not absolute name + wchar_t appWide[kDefaultPathBufferSize]; + ConvertUnityPathName( m_ApplicationPath.c_str(), appWide, kDefaultPathBufferSize ); + if( !IsAbsoluteFilePath(m_ApplicationPath) ) + { + wchar_t pathWide[kDefaultPathBufferSize]; + memset(pathWide, 0, sizeof(pathWide)); + wchar_t* namePart; + if( SearchPathW( NULL, appWide, NULL, kDefaultPathBufferSize, pathWide, &namePart ) > 0 ) + { + memcpy( appWide, pathWide, sizeof(pathWide) ); + } + } + + // Now create a child process + STARTUPINFOW siStartInfo; + ZeroMemory( &m_Task, sizeof(m_Task) ); + ZeroMemory( &siStartInfo, sizeof(siStartInfo) ); + siStartInfo.cb = sizeof(siStartInfo); + siStartInfo.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; +// siStartInfo.hStdOutput = INVALID_HANDLE_VALUE; // m_Task_stdout_wr.handle; +// siStartInfo.hStdInput = INVALID_HANDLE_VALUE; // m_Task_stdin_rd.handle; + siStartInfo.wShowWindow = SW_HIDE; + + // generate an argument list + std::wstring argumentString = std::wstring(L"\"") + appWide + L'"'; + std::wstring argWide; + for( int i = 0; i < m_Arguments.size(); ++i ) { + argumentString += L' '; + + std::string arg = m_Arguments[ i ]; + arg = QuoteString( arg ); + ConvertUTF8ToWideString( arg, argWide ); + argumentString += argWide; + } + + wchar_t* argumentBuffer = new wchar_t[argumentString.size()+1]; + memcpy( argumentBuffer, argumentString.c_str(), (argumentString.size()+1)*sizeof(wchar_t) ); + + // launch process + wchar_t directoryWide[kDefaultPathBufferSize]; + ConvertUnityPathName( File::GetCurrentDirectory().c_str(), directoryWide, kDefaultPathBufferSize ); + + BOOL processResult = CreateProcessW( + appWide, // application + argumentBuffer, // command line + NULL, // process security attributes + NULL, // primary thread security attributes + TRUE, // handles are inherited + 0, // creation flags + NULL, // use parent's environment + File::GetCurrentDirectory().empty() ? NULL : directoryWide, // directory + &siStartInfo, // STARTUPINFO pointer + &m_Task ); // receives PROCESS_INFORMATION + + delete[] argumentBuffer; + + if( processResult == FALSE ) + { + LogString("Error creating external process: " + winutils::ErrorCodeToMsg(GetLastError())); + return false; + } + + if (!CloseHandle(m_Task.hThread)) + LogString("Error closing external process thread: " + winutils::ErrorCodeToMsg(GetLastError())); + + // + // Setup named pipe to external process + // + wchar_t pipeWide[kDefaultPathBufferSize]; + ConvertUnityPathName("\\\\.\\pipe\\UnityVCS", pipeWide, kDefaultPathBufferSize); + + memset(&m_Overlapped, 0, sizeof(m_Overlapped)); + m_Event = CreateEvent(NULL, FALSE, FALSE, NULL); + m_Overlapped.hEvent = m_Event; + m_NamedPipe = CreateNamedPipeW(pipeWide, + /* FILE_FLAG_FIRST_PIPE_INSTANCE | */ FILE_FLAG_OVERLAPPED | PIPE_ACCESS_DUPLEX, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + PIPE_UNLIMITED_INSTANCES, 16384, 16384, 0, NULL); + if (m_NamedPipe == INVALID_HANDLE_VALUE) + { + LogString("Error creating named pipe: " + winutils::ErrorCodeToMsg(GetLastError())); + Cleanup(); + return false; + } + + int ret = ConnectNamedPipe(m_NamedPipe, &m_Overlapped); + + if (ret != 0) + { + LogString("Error connecting named pipe: " + winutils::ErrorCodeToMsg(GetLastError())); + Cleanup(); + return false; + } + + switch (GetLastError()) { + case ERROR_PIPE_CONNECTED: + return true; // connected to external process + break; + case ERROR_IO_PENDING: + { + // Wait for connect for 30 secs + const int waitConnectTimeoutMs = 60000; + + switch (WaitForSingleObject(m_Event, waitConnectTimeoutMs)) + { + case WAIT_OBJECT_0: + { + DWORD dwIgnore; + ret = GetOverlappedResult(m_NamedPipe, &m_Overlapped, &dwIgnore, FALSE); + if (!ret) + { + LogString("Error waiting for named pipe connect: " + winutils::ErrorCodeToMsg(GetLastError())); + Cleanup(); + return false; + } + return true; // connected to external process + } + case WAIT_TIMEOUT: + LogStringMsg("Timed out connecting pipe to external process: %s", m_ApplicationPath.c_str()); + break; + case WAIT_FAILED: + LogString("Wait timout while connecting to named pipe:" + winutils::ErrorCodeToMsg(GetLastError())); + break; + default: + LogString("Unknown error while connecting pipe to external process"); + break; + } + break; + } + default: + LogString("Unknown state while connecting pipe to external process"); + break; + } + Cleanup(); + return false; +} + +bool ExternalProcess::IsRunning() +{ + if (m_Task.hProcess == INVALID_HANDLE_VALUE) + return false; + + DWORD result = WaitForSingleObject(m_Task.hProcess, 0); + + if (result == WAIT_FAILED) + LogString("Error checking run state of external process: " + winutils::ErrorCodeToMsg(GetLastError())); + + return result == WAIT_TIMEOUT; +} + +// Copies the char array into a string, but ignores all \r +static void ConvertToString(CHAR* buffer, std::string& result) +{ + result.clear(); + + for(int i = 0; ; ) { + CHAR c = buffer[i]; + switch (c) { + case '\r': + break; + case 0: + return; + default: + result += (char)c; + } + i++; + } +} + +string ExternalProcess::ReadLine() +{ + if (m_LineBufferValid) + { + m_LineBufferValid = false; + return m_LineBuffer; + } + + const DWORD kBufferSize = 1024; + static char buffer[kBufferSize + 1]; + + DWORD waitReadTimeoutMs = (DWORD)(m_ReadTimeout * 1000.0); + + while (true) + { + if (!IsRunning()) + { + throw ExternalProcessException(EPSTATE_NotRunning, "Trying to read from process that is not running"); + } + + std::string::size_type i = m_Buffer.find("\n"); + + if ( i != std::string::npos) + { + std::string result(m_Buffer, 0, i); + ++i; // Eat \n + if (i >= m_Buffer.length()) + m_Buffer.clear(); + else + m_Buffer = m_Buffer.substr(i); + return result; + } + + DWORD bytesRead = 0; + bool success = ReadFile( m_NamedPipe, buffer, kBufferSize, &bytesRead, &m_Overlapped ); + if ( success ) + { + if (bytesRead == 0) + throw ExternalProcessException(EPSTATE_BrokenPipe, "Error reading external process - read 0 bytes"); + } + else + { + switch (GetLastError()) + { + case ERROR_IO_PENDING: + // Wait for data to be readable on pipe + switch (WaitForSingleObject(m_Event, waitReadTimeoutMs)) + { + case WAIT_OBJECT_0: + { + bool success = GetOverlappedResult( m_NamedPipe, // handle to pipe + &m_Overlapped, // OVERLAPPED structure + &bytesRead, // bytes transferred + FALSE); // do not wait + if (!success) + { + LogString("Error waiting for read external process: " + winutils::ErrorCodeToMsg(GetLastError())); + throw ExternalProcessException(EPSTATE_BrokenPipe, "Error waiting for read in external process"); + } + + if (bytesRead == 0) + throw ExternalProcessException(EPSTATE_BrokenPipe, "Error reading any data from external process"); + + break; + } + case WAIT_TIMEOUT: + throw ExternalProcessException(EPSTATE_TimeoutReading, "Timeout reading from external process"); + case WAIT_FAILED: + LogString("Timout waiting for read external process: " + winutils::ErrorCodeToMsg(GetLastError())); + throw ExternalProcessException(EPSTATE_BrokenPipe, "Failed waiting for read from external process"); + default: + throw ExternalProcessException(EPSTATE_BrokenPipe, "Unknown error during read of external process"); + } + break; + default: + LogString("Error reading externalprocess: " + winutils::ErrorCodeToMsg(GetLastError())); + throw ExternalProcessException(EPSTATE_BrokenPipe, "Error read polling external process"); + } + } + + buffer[bytesRead] = 0; + + string newData; + ConvertToString(buffer, newData); + m_Buffer += newData; + } + + throw ExternalProcessException(EPSTATE_TimeoutReading, "Too many retries while reading from external process. This cannot happen."); + return string(); +} + +string ExternalProcess::PeekLine() +{ + m_LineBuffer = ReadLine(); + m_LineBufferValid = true; + return m_LineBuffer; +} + + +bool ExternalProcess::Write(const std::string& data) +{ + const CHAR* buf = data.c_str(); + DWORD written = 0; + size_t toWrite = data.length(); + DWORD waitWriteTimeoutMs = (DWORD)(m_WriteTimeout * 1000.0); + + while (true) + { + if (!IsRunning()) + { + throw ExternalProcessException(EPSTATE_NotRunning, "Trying to write non running external process"); + } + + bool success = WriteFile(m_NamedPipe, buf, toWrite, &written, &m_Overlapped); + + if ( success ) + { + if (toWrite != written) + throw ExternalProcessException(EPSTATE_BrokenPipe, "Error writing external process - not all bytes written"); + break; + } + else + { + if (GetLastError() == ERROR_IO_PENDING) + { + // Wait for data to be readable on pipe + switch (WaitForSingleObject(m_Event, waitWriteTimeoutMs)) + { + case WAIT_OBJECT_0: + { + bool success = GetOverlappedResult( m_NamedPipe, // handle to pipe + &m_Overlapped, // OVERLAPPED structure + &written, // bytes transferred + FALSE); // do not wait + if (!success) + { + LogString("Error waiting to write external process: " + winutils::ErrorCodeToMsg(GetLastError())); + throw ExternalProcessException(EPSTATE_BrokenPipe, "Error waiting for write in external process"); + } + + if (written != toWrite) + throw ExternalProcessException(EPSTATE_BrokenPipe, "Error writing all data to external process"); + + return true; + } + case WAIT_TIMEOUT: + throw ExternalProcessException(EPSTATE_TimeoutReading, "Timeout writing to external process"); + case WAIT_FAILED: + LogString("Failed waiting to write external process: " + winutils::ErrorCodeToMsg(GetLastError())); + throw ExternalProcessException(EPSTATE_BrokenPipe, "Failed waiting for write to external process"); + default: + throw ExternalProcessException(EPSTATE_BrokenPipe, "Unknown error during write to external process"); + } + } + else + { + LogString("Error writing external process: " + winutils::ErrorCodeToMsg(GetLastError())); + throw ExternalProcessException(EPSTATE_BrokenPipe, "Error write polling external process"); + } + } + } + + return true; +} + +void ExternalProcess::Shutdown() +{ + // Wait until exit is needed in order for the task to properly deallocate + if (IsRunning() && m_NamedPipe != INVALID_HANDLE_VALUE) + { + FlushFileBuffers(m_NamedPipe); + DisconnectNamedPipe(m_NamedPipe); + } + + Cleanup(); +} + +void ExternalProcess::SetReadTimeout(double secs) +{ + m_ReadTimeout = secs; +} + +double ExternalProcess::GetReadTimeout() +{ + return m_ReadTimeout; +} + +void ExternalProcess::SetWriteTimeout(double secs) +{ + m_WriteTimeout = secs; +} + +double ExternalProcess::GetWriteTimeout() +{ + return m_WriteTimeout; +} + +void ExternalProcess::Cleanup() +{ + if (m_Event != INVALID_HANDLE_VALUE && !CloseHandle(m_Event)) + LogString("Error cleaning up external process event: " + winutils::ErrorCodeToMsg(GetLastError())); + m_Event = INVALID_HANDLE_VALUE; + + if (m_NamedPipe != INVALID_HANDLE_VALUE && !CloseHandle(m_NamedPipe)) + LogString("Error cleaning up external process named pipe: " + winutils::ErrorCodeToMsg(GetLastError())); + m_NamedPipe = INVALID_HANDLE_VALUE; + + if (m_Task.hProcess != INVALID_HANDLE_VALUE && !TerminateProcess(m_Task.hProcess, 1) && !CloseHandle(m_Task.hProcess)) + LogString("Error cleaning up external process: " + winutils::ErrorCodeToMsg(GetLastError())); + m_Task.hProcess = INVALID_HANDLE_VALUE; +} diff --git a/Test/Source/TestServer.cpp b/Test/Source/TestServer.cpp new file mode 100644 index 0000000..27c8145 --- /dev/null +++ b/Test/Source/TestServer.cpp @@ -0,0 +1,145 @@ +#include "ExternalProcess.h" +#include +#include + +using namespace std; + +void printStatus(bool ok); + +int main(int argc, char* argv[]) +{ + bool verbose = argc > 3 ? string(argv[3]) == "verbose" : false; + + if (verbose) + cout << "Pllugin : " << argv[1] << endl; + + cout << "Testing " << argv[2] << " "; + if (verbose) + cout << endl; + + vector arguments; + ExternalProcess p(argv[1], arguments); + p.Launch(); + + ifstream testscript(argv[2]); + + const int BUFSIZE = 4096; + char buf[BUFSIZE]; + + const string restartline = ""; + const string commanddelim = "--"; + const string expectdelim = "--"; + const string matchtoken = "re:"; + const string regextoken = "==:"; + const string exittoken = ""; + + bool ok = true; + while (!testscript.eof()) + { + // Just forward the command to the plugin + if (restartline == buf) + { + if (verbose) + cout << "Restarting plugin"; + p = ExternalProcess(argv[1], arguments); + p.Launch(); + continue; + } + + while (testscript.getline(buf, BUFSIZE)) + { + string command(buf); + + if (verbose) + cout << command << endl; + + if (command.find(commanddelim) == 0) + break; // done command lines + + if (command.find(exittoken) == 0) + return 0; + + if (command.find(restartline) == 0) + { + p = ExternalProcess(argv[1], arguments); + p.Launch(); + continue; + } + + p.Write(buf); + p.Write("\n"); + } + + while (testscript.getline(buf, BUFSIZE)) + { + string expect(buf); + + if (verbose) + cout << expect << endl; + + if (expect.find(expectdelim) == 0) + break; // done expect lines + + if (expect.find(exittoken) == 0) + return 0; + + if (expect.find(restartline) == 0) + { + p = ExternalProcess(argv[1], arguments); + p.Launch(); + continue; + } + + string msg = p.ReadLine(); + if (expect.find(regextoken) == 0) + { + // TODO: implement regex match + } + else + { + // Optional match token + if (expect.find(matchtoken) == 0) + expect = expect.substr(0, matchtoken.length()); + + if (expect != msg) + { + ok = false; + printStatus(ok); + cerr << "Output fail: expected '" << expect << "'" << endl; + cerr << " got '" << msg << "'" << endl; + + // Read as much as possible from plugin and stop + p.SetReadTimeout(0.3); + try + { + cerr << " reading as much as possible from plugin:" << endl; + do { + string l = p.ReadLine(); + cerr << l << endl; + } while (true); + + } catch (...) + { + return 1; + } + } + } + } + } + + printStatus(ok); + + return 0; +} + +void printStatus(bool ok) +{ + const char * redColor = "\033[;1;31m"; + const char * greenColor = "\033[;1;32m"; + const char * endColor = "\033[0m"; + + if (ok) + cout << greenColor << "OK" << endColor << endl; + else + cout << redColor << "Failed" << endColor << endl; +} diff --git a/Test/run_tests.bat b/Test/run_tests.bat new file mode 100644 index 0000000..6fa59fd --- /dev/null +++ b/Test/run_tests.bat @@ -0,0 +1 @@ +TODO: Make bat file diff --git a/Test/run_tests.sh b/Test/run_tests.sh new file mode 100755 index 0000000..084acb7 --- /dev/null +++ b/Test/run_tests.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +SERVEREXEC=$1 +PLUGINEXEC=$2 +VERBOSE=$3 +TESTS=${*:4} + +for i in $TESTS ; do + $SERVEREXEC $PLUGINEXEC "$i" $VERBOSE; +done