From 8a9e518371df03b3f382e0fe869da83751fdda0b Mon Sep 17 00:00:00 2001 From: jsmall-nvidia Date: Wed, 10 Nov 2021 17:33:22 -0500 Subject: Interprocess communication via pipes (#2009) * #include an absolute path didn't work - because paths were taken to always be relative. * Use 'Process' to communicate with an command line tool. * Remove slang-win-stream * Tidy up windows ProcessUtil. * First version of BufferedReadStream. * Windows working IPC for steams. * Test proxy count option. * Split Process/ProcessUtil. Process is platform dependant. ProcessUtil are functions that are platform independent. * First implementation of Unix Process interface. * Unix process compiles on cygwin. * Fix typo in unix process. * Separate unix pipe stream error of invalid access, from pipe availability. * Fix in standard line extraction. * Make fd non blocking. * Fix issues with Windows Process streams. * Added UnixPipe. * Some fixes around UnixPipeStream. * Make a unix stream closed explicit. * Hack to debug linux process/stream. * Revert to old linux pipe handling. * Pass executable path for unit tests. Split out CommandLine into own source. * Small improvements in process/command line. * Check process behavior with crash. * Make stderr and stdout unbuffered for crash testing. * Only turn disable buffering in crash test. * Disable crash test on CI. * Fix crash on clang/linux. * Enable crash test. Remove _appendBuffer as can use StreamUtil functionality. --- source/core/windows/slang-win-process.cpp | 495 ++++++++++++++++++++++++++++++ 1 file changed, 495 insertions(+) create mode 100644 source/core/windows/slang-win-process.cpp (limited to 'source/core/windows/slang-win-process.cpp') diff --git a/source/core/windows/slang-win-process.cpp b/source/core/windows/slang-win-process.cpp new file mode 100644 index 000000000..57eb209c3 --- /dev/null +++ b/source/core/windows/slang-win-process.cpp @@ -0,0 +1,495 @@ +// slang-win-process-util.cpp +#include "../slang-process.h" + +#include "../slang-string.h" +#include "../slang-string-escape-util.h" +#include "../slang-string-util.h" +#include "../slang-process-util.h" + +#include "../../../slang-com-helper.h" + +#ifdef _WIN32 +// Include Windows header in a way that minimized namespace pollution. +// TODO: We could try to avoid including this at all, but it would +// mean trying to hide certain struct layouts, which would add +// more dynamic allocation. +# define WIN32_LEAN_AND_MEAN +# define NOMINMAX +# include +# undef WIN32_LEAN_AND_MEAN +# undef NOMINMAX +#endif + +#include +#include + +#ifndef SLANG_RETURN_FAIL_ON_FALSE +# define SLANG_RETURN_FAIL_ON_FALSE(x) if (!(x)) return SLANG_FAIL; +#endif + +namespace Slang { + +// Has behavior very similar to unique_ptr - assignment is a move. +class WinHandle +{ +public: + /// Detach the encapsulated handle. Returns the handle (which now must be externally handled) + HANDLE detach() { HANDLE handle = m_handle; m_handle = nullptr; return handle; } + + /// Return as a handle + operator HANDLE() const { return m_handle; } + + /// Assign + void operator=(HANDLE handle) { setNull(); m_handle = handle; } + void operator=(WinHandle&& rhs) { HANDLE handle = m_handle; m_handle = rhs.m_handle; rhs.m_handle = handle; } + + /// Get ready for writing + SLANG_FORCE_INLINE HANDLE* writeRef() { setNull(); return &m_handle; } + /// Get for read access + SLANG_FORCE_INLINE const HANDLE* readRef() const { return &m_handle; } + + void setNull() + { + if (m_handle) + { + CloseHandle(m_handle); + m_handle = nullptr; + } + } + bool isNull() const { return m_handle == nullptr; } + + /// Ctor + WinHandle(HANDLE handle = nullptr) :m_handle(handle) {} + WinHandle(WinHandle&& rhs) :m_handle(rhs.m_handle) { rhs.m_handle = nullptr; } + + /// Dtor + ~WinHandle() { setNull(); } + +private: + + WinHandle(const WinHandle&) = delete; + void operator=(const WinHandle& rhs) = delete; + + HANDLE m_handle; +}; + +/* A simple Stream implementation of a File HANDLE (or Pipe). Note that currently does not allow getPosition/seek/atEnd */ +class WinPipeStream : public Stream +{ +public: + typedef WinPipeStream ThisType; + + // Stream + virtual Int64 getPosition() SLANG_OVERRIDE { return 0; } + virtual SlangResult seek(SeekOrigin origin, Int64 offset) SLANG_OVERRIDE { SLANG_UNUSED(origin); SLANG_UNUSED(offset); return SLANG_E_NOT_AVAILABLE; } + virtual SlangResult read(void* buffer, size_t length, size_t& outReadBytes) SLANG_OVERRIDE; + virtual SlangResult write(const void* buffer, size_t length) SLANG_OVERRIDE; + virtual bool isEnd() SLANG_OVERRIDE { return m_streamHandle.isNull(); } + virtual bool canRead() SLANG_OVERRIDE { return _has(FileAccess::Read) && !m_streamHandle.isNull(); } + virtual bool canWrite() SLANG_OVERRIDE { return _has(FileAccess::Write) && !m_streamHandle.isNull(); } + virtual void close() SLANG_OVERRIDE; + virtual SlangResult flush() SLANG_OVERRIDE; + + WinPipeStream(HANDLE handle, FileAccess access, bool isOwned = true); + + ~WinPipeStream() { close(); } + +protected: + + bool _has(FileAccess access) const { return (Index(access) & Index(m_access)) != 0; } + + SlangResult _updateState(BOOL res); + + FileAccess m_access = FileAccess::None; + WinHandle m_streamHandle; + bool m_isOwned; +}; + + +class WinProcess : public Process +{ +public: + + virtual bool isTerminated() SLANG_OVERRIDE; + virtual void waitForTermination() SLANG_OVERRIDE; + + WinProcess(HANDLE handle, Stream* const* streams) : + m_processHandle(handle) + { + for (Index i = 0; i < Index(Process::StreamType::CountOf); ++i) + { + m_streams[i] = streams[i]; + } + } + +protected: + + void _hasTerminated(); + WinHandle m_processHandle; ///< If not set the process has terminated +}; + +/* !!!!!!!!!!!!!!!!!!!!!!!!!!! WinPipeStream !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ + +WinPipeStream::WinPipeStream(HANDLE handle, FileAccess access, bool isOwned) : + m_streamHandle(handle), + m_access(access), + m_isOwned(isOwned) +{ + // It might be handy to get information about the handle + // https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-getnamedpipeinfo + + { + DWORD flags, outBufferSize, inBufferSize, maxInstances; + // It appears that by default windows pipe buffer size is 4k. + if (GetNamedPipeInfo(handle, &flags, &outBufferSize, &inBufferSize, &maxInstances)) + { + } + } +} + +SlangResult WinPipeStream::_updateState(BOOL res) +{ + if (res) + { + return SLANG_OK; + } + else + { + const auto err = GetLastError(); + + if (err == ERROR_BROKEN_PIPE) + { + m_streamHandle.setNull(); + return SLANG_OK; + } + + SLANG_UNUSED(err); + return SLANG_FAIL; + } +} + +SlangResult WinPipeStream::read(void* buffer, size_t length, size_t& outReadBytes) +{ + outReadBytes = 0; + if (!_has(FileAccess::Read)) + { + return SLANG_E_NOT_AVAILABLE; + } + + if (m_streamHandle.isNull()) + { + return SLANG_OK; + } + + // Check if there is any data, so won't block + { + DWORD bytesRead = 0; + DWORD totalBytes = 0; + DWORD remainingBytes = 0; + + // Works on anonymous pipes too + // https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe + + SLANG_RETURN_ON_FAIL(_updateState(PeekNamedPipe(m_streamHandle, nullptr, DWORD(0), &bytesRead, &totalBytes, &remainingBytes))); + // If there is nothing to read we are done + // If we don't do this ReadFile will *block* if there is nothing available + if (totalBytes == 0) + { + return SLANG_OK; + } + } + + { + DWORD bytesRead = 0; + SLANG_RETURN_ON_FAIL(_updateState(ReadFile(m_streamHandle, buffer, DWORD(length), &bytesRead, nullptr))); + + outReadBytes = size_t(bytesRead); + } + + return SLANG_OK; +} + +SlangResult WinPipeStream::write(const void* buffer, size_t length) +{ + if (!_has(FileAccess::Write)) + { + return SLANG_E_NOT_AVAILABLE; + } + + if (m_streamHandle.isNull()) + { + // Writing to closed stream + return SLANG_FAIL; + } + + DWORD numWritten = 0; + BOOL writeResult = WriteFile(m_streamHandle, buffer, DWORD(length), &numWritten, nullptr); + + if (!writeResult || numWritten != length) + { + return SLANG_FAIL; + } + + return SLANG_OK; +} + +void WinPipeStream::close() +{ + if (!m_isOwned) + { + // If we don't own it just detach it + m_streamHandle.detach(); + } + m_streamHandle.setNull(); +} + +SlangResult WinPipeStream::flush() +{ + if ((Index(m_access) & Index(FileAccess::Write)) == 0 || m_streamHandle.isNull()) + { + return SLANG_E_NOT_AVAILABLE; + } + + // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-flushfilebuffers + + if (!FlushFileBuffers(m_streamHandle)) + { + auto err = GetLastError(); + SLANG_UNUSED(err); + } + return SLANG_OK; +} + +/* !!!!!!!!!!!!!!!!!!!!!!!!!!! WinProcess !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ + +void WinProcess::_hasTerminated() +{ + if (!m_processHandle.isNull()) + { + // get exit code for process + // https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess + + DWORD childExitCode = 0; + if (GetExitCodeProcess(m_processHandle, &childExitCode)) + { + m_returnValue = int32_t(childExitCode); + } + m_processHandle.setNull(); + } +} + +void WinProcess::waitForTermination() +{ + if (m_processHandle.isNull()) + { + return; + } + + // wait for the process to exit + // TODO: set a timeout as a safety measure... + WaitForSingleObject(m_processHandle, INFINITE); + + _hasTerminated(); +} + +bool WinProcess::isTerminated() +{ + if (m_processHandle.isNull()) + { + return true; + } + + auto res = WaitForSingleObject(m_processHandle, 0); + + if (res == WAIT_TIMEOUT) + { + return false; + } + _hasTerminated(); + return true; +} + +/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ + +/* static */StringEscapeHandler* Process::getEscapeHandler() +{ + return StringEscapeUtil::getHandler(StringEscapeUtil::Style::Space); +} + +/* static */UnownedStringSlice Process::getExecutableSuffix() +{ + return UnownedStringSlice::fromLiteral(".exe"); +} + +/* static */SlangResult Process::getStdStream(StreamType type, RefPtr& out) +{ + switch (type) + { + case StreamType::StdIn: + { + const HANDLE stdinHandle = GetStdHandle(STD_INPUT_HANDLE); + out = new WinPipeStream(stdinHandle, FileAccess::Read, false); + return SLANG_OK; + } + } + + return SLANG_FAIL; +} + +/* static */SlangResult Process::create(const CommandLine& commandLine, Process::Flags flags, RefPtr& outProcess) +{ + WinHandle childStdOutRead; + WinHandle childStdErrRead; + WinHandle childStdInWrite; + + WinHandle processHandle; + { + WinHandle childStdOutWrite; + WinHandle childStdErrWrite; + WinHandle childStdInRead; + + SECURITY_ATTRIBUTES securityAttributes; + securityAttributes.nLength = sizeof(securityAttributes); + securityAttributes.lpSecurityDescriptor = nullptr; + securityAttributes.bInheritHandle = true; + + // 0 means use the 'system default' + //const DWORD bufferSize = 64 * 1024; + const DWORD bufferSize = 0; + + { + WinHandle childStdOutReadTmp; + WinHandle childStdErrReadTmp; + WinHandle childStdInWriteTmp; + // create stdout pipe for child process + SLANG_RETURN_FAIL_ON_FALSE(CreatePipe(childStdOutReadTmp.writeRef(), childStdOutWrite.writeRef(), &securityAttributes, bufferSize)); + // create stderr pipe for child process + SLANG_RETURN_FAIL_ON_FALSE(CreatePipe(childStdErrReadTmp.writeRef(), childStdErrWrite.writeRef(), &securityAttributes, bufferSize)); + // create stdin pipe for child process + SLANG_RETURN_FAIL_ON_FALSE(CreatePipe(childStdInRead.writeRef(), childStdInWriteTmp.writeRef(), &securityAttributes, bufferSize)); + + const HANDLE currentProcess = GetCurrentProcess(); + + // https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-duplicatehandle + + // create a non-inheritable duplicate of the stdout reader + SLANG_RETURN_FAIL_ON_FALSE(DuplicateHandle(currentProcess, childStdOutReadTmp, currentProcess, childStdOutRead.writeRef(), 0, FALSE, DUPLICATE_SAME_ACCESS)); + // create a non-inheritable duplicate of the stderr reader + SLANG_RETURN_FAIL_ON_FALSE(DuplicateHandle(currentProcess, childStdErrReadTmp, currentProcess, childStdErrRead.writeRef(), 0, FALSE, DUPLICATE_SAME_ACCESS)); + // create a non-inheritable duplicate of the stdin writer + SLANG_RETURN_FAIL_ON_FALSE(DuplicateHandle(currentProcess, childStdInWriteTmp, currentProcess, childStdInWrite.writeRef(), 0, FALSE, DUPLICATE_SAME_ACCESS)); + } + + // TODO: switch to proper wide-character versions of these... + STARTUPINFOW startupInfo; + ZeroMemory(&startupInfo, sizeof(startupInfo)); + startupInfo.cb = sizeof(startupInfo); + startupInfo.hStdError = childStdErrWrite; + startupInfo.hStdOutput = childStdOutWrite; + startupInfo.hStdInput = childStdInRead; + startupInfo.dwFlags = STARTF_USESTDHANDLES; + + OSString pathBuffer; + LPCWSTR path = nullptr; + + if (commandLine.m_executableType == CommandLine::ExecutableType::Path) + { + StringBuilder cmd; + StringEscapeUtil::appendMaybeQuoted(getEscapeHandler(), commandLine.m_executable.getUnownedSlice(), cmd); + + pathBuffer = cmd.toWString(); + path = pathBuffer.begin(); + } + + // Produce the command line string + String cmdString = commandLine.toString(); + OSString cmdStringBuffer = cmdString.toWString(); + + // Now we can actually get around to starting a process + PROCESS_INFORMATION processInfo; + ZeroMemory(&processInfo, sizeof(processInfo)); + + // https://docs.microsoft.com/en-us/windows/win32/procthread/process-creation-flags + + DWORD createFlags = CREATE_NO_WINDOW; + + if (flags & Process::Flag::AttachDebugger) + { + createFlags |= CREATE_SUSPENDED; + } + + // https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-createprocessa + // `CreateProcess` requires write access to this, for some reason... + BOOL success = CreateProcessW( + path, + (LPWSTR)cmdStringBuffer.begin(), + nullptr, + nullptr, + true, + createFlags, + nullptr, // TODO: allow specifying environment variables? + nullptr, + &startupInfo, + &processInfo); + + if (!success) + { + DWORD err = GetLastError(); + SLANG_UNUSED(err); + return SLANG_FAIL; + } + + if (flags & Process::Flag::AttachDebugger) + { + // Lets see if we can set up to debug + // https://docs.microsoft.com/en-us/windows/win32/debug/debugging-a-running-process + + //DebugActiveProcess(processInfo.dwProcessId); + + // Resume the thread + ResumeThread(processInfo.hThread); + } + + // close handles we are now done with + CloseHandle(processInfo.hThread); + + // Save the process handle + processHandle = processInfo.hProcess; + } + + RefPtr streams[Index(Process::StreamType::CountOf)]; + streams[Index(Process::StreamType::ErrorOut)] = new WinPipeStream(childStdErrRead.detach(), FileAccess::Read); + streams[Index(Process::StreamType::StdOut)] = new WinPipeStream(childStdOutRead.detach(), FileAccess::Read); + streams[Index(Process::StreamType::StdIn)] = new WinPipeStream(childStdInWrite.detach(), FileAccess::Write); + + outProcess = new WinProcess(processHandle.detach(), streams[0].readRef()); + return SLANG_OK; +} + +/* static */void Process::sleepCurrentThread(Index timeInMs) +{ + ::Sleep(DWORD(timeInMs)); +} + +static uint64_t _getClockFrequency() +{ + LARGE_INTEGER timerFrequency; + QueryPerformanceFrequency(&timerFrequency); + return timerFrequency.QuadPart; +} + +static const uint64_t g_frequency = _getClockFrequency(); + +/* static */uint64_t Process::getClockFrequency() +{ + return g_frequency; +} + +/* static */uint64_t Process::getClockTick() +{ + LARGE_INTEGER counter; + QueryPerformanceCounter(&counter); + return counter.QuadPart; +} + +} // namespace Slang -- cgit v1.2.3