From 9d514e65f00dde0e309f33591f31fbf7f132a005 Mon Sep 17 00:00:00 2001 From: jsmall-nvidia Date: Wed, 12 Jun 2019 09:05:40 -0400 Subject: Runtime execution of Visual Studio Compiler (#978) * Work in progress to be able to invoke VS from within code. * First pass at windows version of refactor of OSProcessSpawner * Closer to getting VS path lookup working. * Make OSString assignable/ctor able * Work out program files directory directly, so don't have to expand %%. * WIP: Improve handling of process spawning. * Add support for splitting input by line. * * Correctly locates visual studio install * Added functionality to invoke vs via cmd * Add option to execute the command line. * Handle in ProcessUtil for windows -> WinHandle. * Rename files slang-win-visual-studio-util.cpp/.h and slang-process-util.h * First pass at unix/linux version of ProcessUtil. * Fix reading Visual Studio path from the registry. * Get compiling on linux with. * Fix vcvarsall.bat name * Use ProcessUtil to execute external code. * Remove OSProcessSpawner. * Remove includes for "os.h" where no longer needed. * Fix tabbing issue in premake5.lua Remove test code from slang-test-main.cpp * Fix premake4.lua tabbing issue. * Small fixes to slang-process-util.h Init ExecuteResult on Win execute. * Improve comments. * Fix bug in StringUtil::calcLines - with oddly terminated source input being able to read past end. Make slang-generate use StringUtil over it's own impl. * Fix off by one bug in working out Visual Studio version. * Fix bug in calculating Visual Studio Version * Fix compilation on linux with string parameter being passed to messageFormat. * Remove erroneous use of kOSError codes - use Result. --- premake5.lua | 6 + source/core/core.vcxproj | 4 + source/core/core.vcxproj.filters | 12 + source/core/slang-process-util.h | 61 +++ source/core/slang-string-util.cpp | 42 ++ source/core/slang-string-util.h | 3 + source/core/slang-string.cpp | 27 +- source/core/slang-string.h | 43 +- source/core/unix/slang-unix-process-util.cpp | 234 ++++++++++ source/core/windows/slang-win-process-util.cpp | 338 +++++++++++++++ .../core/windows/slang-win-visual-studio-util.cpp | 339 +++++++++++++++ source/core/windows/slang-win-visual-studio-util.h | 50 +++ tools/slang-generate/main.cpp | 45 +- tools/slang-test/options.cpp | 2 +- tools/slang-test/os.cpp | 469 --------------------- tools/slang-test/os.h | 65 +-- tools/slang-test/slang-test-main.cpp | 335 ++++++++------- tools/slang-test/test-context.cpp | 2 +- tools/slang-test/test-reporter.cpp | 40 +- 19 files changed, 1357 insertions(+), 760 deletions(-) create mode 100644 source/core/slang-process-util.h create mode 100644 source/core/unix/slang-unix-process-util.cpp create mode 100644 source/core/windows/slang-win-process-util.cpp create mode 100644 source/core/windows/slang-win-visual-studio-util.cpp create mode 100644 source/core/windows/slang-win-visual-studio-util.h diff --git a/premake5.lua b/premake5.lua index fcd83eb4f..03b05adfc 100644 --- a/premake5.lua +++ b/premake5.lua @@ -441,6 +441,12 @@ standardProject "core" warnings "Extra" flags { "FatalWarnings" } + if isTargetWindows then + addSourceDir "source/core/windows" + else + addSourceDir "source/core/unix" + end + -- We need the core library to be relocatable to be able to link with slang.so filter { "system:linux" } buildoptions{"-fPIC"} diff --git a/source/core/core.vcxproj b/source/core/core.vcxproj index a8e92949f..684b5592c 100644 --- a/source/core/core.vcxproj +++ b/source/core/core.vcxproj @@ -186,6 +186,7 @@ + @@ -202,6 +203,7 @@ + @@ -223,6 +225,8 @@ + + diff --git a/source/core/core.vcxproj.filters b/source/core/core.vcxproj.filters index 8656ab49b..eec57c50b 100644 --- a/source/core/core.vcxproj.filters +++ b/source/core/core.vcxproj.filters @@ -57,6 +57,9 @@ Header Files + + Header Files + Header Files @@ -105,6 +108,9 @@ Header Files + + Header Files + @@ -164,6 +170,12 @@ Source Files + + Source Files + + + Source Files + diff --git a/source/core/slang-process-util.h b/source/core/slang-process-util.h new file mode 100644 index 000000000..19ae7c0d1 --- /dev/null +++ b/source/core/slang-process-util.h @@ -0,0 +1,61 @@ +// slang-process-util.h +#ifndef SLANG_PROCESS_UTIL_H +#define SLANG_PROCESS_UTIL_H + +#include "slang-string.h" +#include "slang-list.h" + +namespace Slang { + +struct CommandLine +{ + enum class ExecutableType + { + Unknown, ///< The executable is not specified + Path, ///< The executable is set as a path + Filename, ///< The executable is set as a filename + }; + + void addArg(const String& in) { m_args.add(in); } + void addArgs(const String* args, Int argsCount) { m_args.addRange(args, argsCount); } + void setExecutablePath(const String& path) { m_executableType = ExecutableType::Path; m_executable = path; } + void setExecutableFilename(const String& filename) { m_executableType = ExecutableType::Filename; m_executable = filename; } + + /// Ctor + CommandLine():m_executableType(ExecutableType::Unknown) {} + + ExecutableType m_executableType; ///< How the executable is specified + String m_executable; ///< Executable to run + List m_args; ///< The parameters to pass +}; + +struct ExecuteResult +{ + void init() + { + resultCode = 0; + standardOutput = String(); + standardError = String(); + } + + typedef int ResultCode; + ResultCode resultCode; + Slang::String standardOutput; + Slang::String standardError; +}; + +struct ProcessUtil +{ + /// Get the suffix used on this platform + static UnownedStringSlice getExecutableSuffix(); + + /// Output how the command line is executed on the target (with escaping and the such like) + static String getCommandLineString(const CommandLine& commandLine); + + /// Execute the command line + static SlangResult execute(const CommandLine& commandLine, ExecuteResult& outExecuteResult); +}; + +} + +#endif // SLANG_PROCESS_UTIL_H diff --git a/source/core/slang-string-util.cpp b/source/core/slang-string-util.cpp index 11d6846f0..e571bbc99 100644 --- a/source/core/slang-string-util.cpp +++ b/source/core/slang-string-util.cpp @@ -199,6 +199,48 @@ ComPtr StringUtil::createStringBlob(const String& string) return (fromChar == toChar || string.indexOf(fromChar) == Index(-1)) ? string : calcCharReplaced(string.getUnownedSlice(), fromChar, toChar); } +/* static */void StringUtil::calcLines(const UnownedStringSlice& textIn, List& outLines) +{ + char const* begin = textIn.begin(); + char const* end = textIn.end(); + + char const* cursor = begin; + const char* lineStart = cursor; + + while (cursor < end) + { + int c = *cursor++; + switch (c) + { + case '\r': case '\n': + { + outLines.add(UnownedStringSlice(lineStart, cursor - 1)); + + // When we see a line-break character we need + // to record the line break, but we also need + // to deal with the annoying issue of encodings, + // where a multi-byte sequence might encode + // the line break. + + if (cursor < end) + { + int d = *cursor; + if ((c ^ d) == ('\r' ^ '\n')) + cursor++; + } + lineStart = cursor; + break; + } + default: + break; + } + } + + if (cursor > lineStart) + { + outLines.add(UnownedStringSlice(lineStart, cursor)); + } +} } // namespace Slang diff --git a/source/core/slang-string-util.h b/source/core/slang-string-util.h index fcae23bb3..dc0d7e546 100644 --- a/source/core/slang-string-util.h +++ b/source/core/slang-string-util.h @@ -77,6 +77,9 @@ struct StringUtil /// Create a blob from a string static ComPtr createStringBlob(const String& string); + + /// Given text, splits into lines stored in outLines. NOTE! That lines is only valid as long as textIn remains valid + static void calcLines(const UnownedStringSlice& textIn, List& lines); }; } // namespace Slang diff --git a/source/core/slang-string.cpp b/source/core/slang-string.cpp index 64b8e4dc1..54703def1 100644 --- a/source/core/slang-string.cpp +++ b/source/core/slang-string.cpp @@ -14,8 +14,8 @@ namespace Slang // OSString OSString::OSString() - : m_begin(0) - , m_end(0) + : m_begin(nullptr) + , m_end(nullptr) {} OSString::OSString(wchar_t* begin, wchar_t* end) @@ -23,7 +23,7 @@ namespace Slang , m_end(end) {} - OSString::~OSString() + void OSString::_releaseBuffer() { if (m_begin) { @@ -31,6 +31,27 @@ namespace Slang } } + void OSString::set(const wchar_t* begin, const wchar_t* end) + { + if (m_begin) + { + delete[] m_begin; + m_begin = nullptr; + m_end = nullptr; + } + const size_t len = end - begin; + if (len > 0) + { + // TODO(JS): The allocation is only done this way to be compatible with the buffer being detached from an array + // This is unfortunate, because it means that the allocation stores the size (and alignment fix), which is a shame because we know the size + m_begin = new wchar_t[len + 1]; + memcpy(m_begin, begin, len * sizeof(wchar_t)); + // Zero terminate + m_begin[len] = 0; + m_end = m_begin + len; + } + } + static const wchar_t kEmptyOSString[] = { 0 }; wchar_t const* OSString::begin() const diff --git a/source/core/slang-string.h b/source/core/slang-string.h index 1cd9e5413..4975e9ec6 100644 --- a/source/core/slang-string.h +++ b/source/core/slang-string.h @@ -292,9 +292,43 @@ namespace Slang class OSString { public: + /// Default OSString(); + /// NOTE! This assumes that begin is a new wchar_t[] buffer, and it will + /// now be owned by the OSString OSString(wchar_t* begin, wchar_t* end); - ~OSString(); + /// Move Ctor + OSString(OSString&& rhs): + m_begin(rhs.m_begin), + m_end(rhs.m_end) + { + rhs.m_begin = nullptr; + rhs.m_end = nullptr; + } + // Copy Ctor + OSString(const OSString& rhs) : + m_begin(nullptr), + m_end(nullptr) + { + set(rhs.m_begin, rhs.m_end); + } + + /// = + void operator=(const OSString& rhs) { set(rhs.m_begin, rhs.m_end); } + void operator=(OSString&& rhs) + { + auto begin = m_begin; + auto end = m_end; + m_begin = rhs.m_begin; + m_end = rhs.m_end; + rhs.m_begin = begin; + rhs.m_end = end; + } + + ~OSString() { _releaseBuffer(); } + + size_t getLength() const { return (m_end - m_begin); } + void set(const wchar_t* begin, const wchar_t* end); operator wchar_t const*() const { @@ -305,8 +339,11 @@ namespace Slang wchar_t const* end() const; private: - wchar_t* m_begin; - wchar_t* m_end; + + void _releaseBuffer(); + + wchar_t* m_begin; ///< First character. This is a new wchar_t[] buffer + wchar_t* m_end; ///< Points to terminating 0 }; /*! diff --git a/source/core/unix/slang-unix-process-util.cpp b/source/core/unix/slang-unix-process-util.cpp new file mode 100644 index 000000000..7795463b7 --- /dev/null +++ b/source/core/unix/slang-unix-process-util.cpp @@ -0,0 +1,234 @@ +// slang-unix-process-util.cpp +#include "../slang-process-util.h" + +#include "../slang-common.h" +#include "../slang-string-util.h" + +#include +#include + +//#include +#include +#include +#include +#include +#include +#include + +namespace Slang { + + +/* static */UnownedStringSlice ProcessUtil::getExecutableSuffix() +{ + return UnownedStringSlice::fromLiteral(""); +} + +static void _appendEscaped(const UnownedStringSlice& slice, StringBuilder& out) +{ + // TODO(JS): This escaping is not complete... ! + if (slice.indexOf(' ') >= 0 || slice.indexOf('"') >= 0) + { + out << "\""; + + const char* cur = slice.begin(); + const char* end = slice.end(); + + while (cur < end) + { + char c = *cur++; + switch (c) + { + case '\"': + { + // Escape quotes. + out << "\\\""; + break; + } + default: + out.append(c); + } + } + + out << "\""; + + return; + } + + out << slice; +} + +/* static */String ProcessUtil::getCommandLineString(const CommandLine& commandLine) +{ + StringBuilder cmd; + _appendEscaped(commandLine.m_executable.getUnownedSlice(), cmd); + for (const auto& arg : commandLine.m_args) + { + cmd << " "; + _appendEscaped(arg.getUnownedSlice(), cmd); + } + return cmd.ToString(); +} + +/* static */SlangResult ProcessUtil::execute(const CommandLine& commandLine, ExecuteResult& outExecuteResult) +{ + outExecuteResult.init(); + + List argPtrs; + // Add the command + argPtrs.add(commandLine.m_executable.getBuffer()); + // Add all the args + for (auto arg : commandLine.m_args) + { + argPtrs.add(arg.getBuffer()); + } + // Terminate with a null + argPtrs.add(nullptr); + + int stdoutPipe[2]; + int stderrPipe[2]; + + if (pipe(stdoutPipe) == -1) + return SLANG_FAIL; + + if (pipe(stderrPipe) == -1) + return SLANG_FAIL; + + pid_t childProcessID = fork(); + if (childProcessID == -1) + return SLANG_FAIL; + + if (childProcessID == 0) + { + // We are the child process. + + dup2(stdoutPipe[1], STDOUT_FILENO); + dup2(stderrPipe[1], STDERR_FILENO); + + close(stdoutPipe[0]); + close(stdoutPipe[1]); + + close(stderrPipe[0]); + close(stderrPipe[1]); + + execvp(argPtrs[0], (char* const*)&argPtrs[0]); + + // If we get here, then `exec` failed + fprintf(stderr, "error: `exec` failed\n"); + return SLANG_FAIL; + } + else + { + // We are the parent process + + close(stdoutPipe[1]); + close(stderrPipe[1]); + + int stdoutFD = stdoutPipe[0]; + int stderrFD = stderrPipe[0]; + + pollfd pollInfos[2]; + nfds_t pollInfoCount = 2; + + pollInfos[0].fd = stdoutFD; + pollInfos[0].events = POLLIN; + pollInfos[0].revents = 0; + pollInfos[1].fd = stderrFD; + pollInfos[1].events = POLLIN; + pollInfos[1].revents = 0; + + int remainingCount = 2; + int iterations = 0; + while (remainingCount) + { + // Safeguard against infinite loop: + iterations++; + if (iterations > 10000) + { + fprintf(stderr, "poll(): %d iterations\n", iterations); + return SLANG_FAIL; + } + + // Set a timeout of ten seconds; + // we really shouldn't wait too long... + int pollTimeout = 10000; + int pollResult = poll(pollInfos, pollInfoCount, pollTimeout); + if (pollResult <= 0) + { + // If there was a signal that got in + // the way, then retry... + if (pollResult == -1 && errno == EINTR) + continue; + + // timeout or error... + return SLANG_FAIL; + } + + enum { kBufferSize = 1024 }; + char buffer[kBufferSize]; + + if (pollInfos[0].revents) + { + auto count = read(stdoutFD, buffer, kBufferSize); + if (count <= 0) + { + // end-of-file + close(stdoutFD); + pollInfos[0].fd = -1; + remainingCount--; + } + + outExecuteResult.standardOutput.append(buffer, buffer + count); + } + + if (pollInfos[1].revents) + { + auto count = read(stderrFD, buffer, kBufferSize); + if (count <= 0) + { + // end-of-file + close(stderrFD); + pollInfos[1].fd = -1; + remainingCount--; + } + + outExecuteResult.standardError.append(buffer, buffer + count); + } + } + + int childStatus = 0; + iterations = 0; + for (;;) + { + // Safeguard against infinite loop: + iterations++; + if (iterations > 10000) + { + fprintf(stderr, "waitpid(): %d iterations\n", iterations); + return SLANG_FAIL; + } + + pid_t terminatedProcessID = waitpid(childProcessID, &childStatus, 0); + if (terminatedProcessID == -1) + { + return SLANG_FAIL; + } + + if (terminatedProcessID == childProcessID) + { + if (WIFEXITED(childStatus)) + { + outExecuteResult.resultCode = (int)(int8_t)WEXITSTATUS(childStatus); + } + else + { + outExecuteResult.resultCode = 1; + } + return SLANG_OK; + } + } + } + + return SLANG_FAIL; +} + +} // namespace Slang diff --git a/source/core/windows/slang-win-process-util.cpp b/source/core/windows/slang-win-process-util.cpp new file mode 100644 index 000000000..424b87128 --- /dev/null +++ b/source/core/windows/slang-win-process-util.cpp @@ -0,0 +1,338 @@ +// slang-win-process-util.cpp +#include "../slang-process-util.h" + +#include "../slang-string.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 + +namespace Slang { + +namespace { // anonymous + +struct ThreadInfo +{ + HANDLE file; + String output; +}; + +// 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; + } + } + + /// 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; +}; + +} // anonymous + +static DWORD WINAPI _readerThreadProc(LPVOID threadParam) +{ + ThreadInfo* info = (ThreadInfo*)threadParam; + HANDLE file = info->file; + + static const int kChunkSize = 1024; + char buffer[kChunkSize]; + + StringBuilder outputBuilder; + + // We need to re-write the output to deal with line + // endings, so we check for paired '\r' and '\n' + // characters, which may span chunks. + int prevChar = -1; + + for (;;) + { + DWORD bytesRead = 0; + BOOL readResult = ReadFile(file, buffer, kChunkSize, &bytesRead, nullptr); + + const DWORD lastError = GetLastError(); + if (lastError == ERROR_BROKEN_PIPE) + { + break; + } + + if (!readResult) + { + break; + } + + + // walk the buffer and rewrite to eliminate '\r' '\n' pairs + char* readCursor = buffer; + char const* end = buffer + bytesRead; + char* writeCursor = buffer; + + while (readCursor != end) + { + int p = prevChar; + int c = *readCursor++; + prevChar = c; + switch (c) + { + case '\r': case '\n': + // swallow input if '\r' and '\n' appear in sequence + if ((p ^ c) == ('\r' ^ '\n')) + { + // but don't swallow the next byte + prevChar = -1; + continue; + } + // always replace '\r' with '\n' + c = '\n'; + break; + + default: + break; + } + + *writeCursor++ = (char)c; + } + bytesRead = (DWORD)(writeCursor - buffer); + + // Note: Current "core" implementation gives no way to know + // the length of the buffer, so we ultimately have + // to just assume null termination... + outputBuilder.Append(buffer, bytesRead); + } + + info->output = outputBuilder.ProduceString(); + + return 0; +} + + +/* static */UnownedStringSlice ProcessUtil::getExecutableSuffix() +{ + return UnownedStringSlice::fromLiteral(".exe"); +} + +static void _appendEscaped(const UnownedStringSlice& slice, StringBuilder& out) +{ + // TODO(JS): This escaping is not complete... ! + if (slice.indexOf(' ') >= 0 || slice.indexOf('"') >= 0) + { + out << "\""; + + const char* cur = slice.begin(); + const char* end = slice.end(); + + while (cur < end) + { + char c= *cur++; + switch (c) + { + case '\"': + { + // Escape quotes. + out << "\\\""; + break; + } + default: + out.append(c); + } + } + + out << "\""; + + return; + } + + out << slice; +} + +/* static */String ProcessUtil::getCommandLineString(const CommandLine& commandLine) +{ + StringBuilder cmd; + _appendEscaped(commandLine.m_executable.getUnownedSlice(), cmd); + for (const auto& arg : commandLine.m_args) + { + cmd << " "; + _appendEscaped(arg.getUnownedSlice(), cmd); + } + return cmd.ToString(); +} + +#define SLANG_RETURN_FAIL_ON_FALSE(x) if (!(x)) return SLANG_FAIL; + +/* static */SlangResult ProcessUtil::execute(const CommandLine& commandLine, ExecuteResult& outExecuteResult) +{ + outExecuteResult.init(); + + SECURITY_ATTRIBUTES securityAttributes; + securityAttributes.nLength = sizeof(securityAttributes); + securityAttributes.lpSecurityDescriptor = nullptr; + securityAttributes.bInheritHandle = true; + + WinHandle childStdOutRead; + WinHandle childStdErrRead; + WinHandle childStdInWrite; + + // Now we can actually get around to starting a process + PROCESS_INFORMATION processInfo; + ZeroMemory(&processInfo, sizeof(processInfo)); + { + WinHandle childStdOutWrite; + WinHandle childStdErrWrite; + WinHandle childStdInRead; + + { + WinHandle childStdOutReadTmp; + WinHandle childStdErrReadTmp; + WinHandle childStdInWriteTmp; + // create stdout pipe for child process + SLANG_RETURN_FAIL_ON_FALSE(CreatePipe(childStdOutReadTmp.writeRef(), childStdOutWrite.writeRef(), &securityAttributes, 0)); + // create stderr pipe for child process + SLANG_RETURN_FAIL_ON_FALSE(CreatePipe(childStdErrReadTmp.writeRef(), childStdErrWrite.writeRef(), &securityAttributes, 0)); + // create stdin pipe for child process + SLANG_RETURN_FAIL_ON_FALSE(CreatePipe(childStdInRead.writeRef(), childStdInWriteTmp.writeRef(), &securityAttributes, 0)); + + HANDLE currentProcess = GetCurrentProcess(); + + // 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; + _appendEscaped(commandLine.m_executable.getUnownedSlice(), cmd); + + pathBuffer = cmd.toWString(); + path = pathBuffer.begin(); + } + + // Produce the command line string + String cmdString = getCommandLineString(commandLine); + OSString cmdStringBuffer = cmdString.toWString(); + + // 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, + CREATE_NO_WINDOW, + nullptr, // TODO: allow specifying environment variables? + nullptr, + &startupInfo, + &processInfo); + + if (!success) + { + DWORD err = GetLastError(); + SLANG_UNUSED(err); + + return SLANG_FAIL; + } + + // close handles we are now done with + CloseHandle(processInfo.hThread); + } + + // Create a thread to read from the child's stdout. + ThreadInfo stdOutThreadInfo; + stdOutThreadInfo.file = childStdOutRead; + WinHandle stdOutThread = CreateThread(nullptr, 0, &_readerThreadProc, (LPVOID)&stdOutThreadInfo, 0, nullptr); + + // Create a thread to read from the child's stderr. + ThreadInfo stdErrThreadInfo; + stdErrThreadInfo.file = childStdErrRead; + WinHandle stdErrThread = CreateThread(nullptr, 0, &_readerThreadProc, (LPVOID)&stdErrThreadInfo, 0, nullptr); + + // wait for the process to exit + // TODO: set a timeout as a safety measure... + WaitForSingleObject(processInfo.hProcess, INFINITE); + + // get exit code for process + // https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess + + DWORD childExitCode = 0; + if (!GetExitCodeProcess(processInfo.hProcess, &childExitCode)) + { + // TODO(JS): Do we want to close here? It seems plausible because just because reading the exit code failed, doesn't mean the handle is closed + CloseHandle(processInfo.hProcess); + + return SLANG_FAIL; + } + + // wait for the reader threads + WaitForSingleObject(stdOutThread, INFINITE); + WaitForSingleObject(stdErrThread, INFINITE); + + CloseHandle(processInfo.hProcess); + + outExecuteResult.standardOutput = stdOutThreadInfo.output; + outExecuteResult.standardError = stdErrThreadInfo.output; + outExecuteResult.resultCode = childExitCode; + + return SLANG_OK; +} + +} diff --git a/source/core/windows/slang-win-visual-studio-util.cpp b/source/core/windows/slang-win-visual-studio-util.cpp new file mode 100644 index 000000000..edf33110c --- /dev/null +++ b/source/core/windows/slang-win-visual-studio-util.cpp @@ -0,0 +1,339 @@ +#include "slang-win-visual-studio-util.h" + +#include "../slang-common.h" +#include "../slang-process-util.h" +#include "../slang-string-util.h" + +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# define NOMINMAX +# include +# undef WIN32_LEAN_AND_MEAN +# undef NOMINMAX + +# include + +#endif + +// The method used to invoke VS was originally inspired by some ideas in +// https://github.com/RuntimeCompiledCPlusPlus/RuntimeCompiledCPlusPlus/ + +namespace Slang { + +// Information on VS versioning can be found here +// https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#Internal_version_numbering + + +namespace { // anonymous + +typedef WinVisualStudioUtil::Version Version; + +struct RegistryInfo +{ + const char* regName; ///< The name of the entry in the registry + const char* pathFix; ///< With the value from the registry how to fix the path +}; + +struct VersionInfo +{ + Version version; ///< The version + const char* name; ///< The name of the registry key +}; + +} // anonymous + + +static SlangResult _readRegistryKey(const char* path, const char* keyName, String& outString) +{ + // https://docs.microsoft.com/en-us/windows/desktop/api/winreg/nf-winreg-regopenkeyexa + HKEY key; + LONG ret = RegOpenKeyExA(HKEY_LOCAL_MACHINE, path, 0, KEY_READ | KEY_WOW64_32KEY, &key); + if (ret != ERROR_SUCCESS) + { + return SLANG_FAIL; + } + + char value[MAX_PATH]; + DWORD size = MAX_PATH; + + // https://docs.microsoft.com/en-us/windows/desktop/api/winreg/nf-winreg-regqueryvalueexa + ret = RegQueryValueExA(key, keyName, nullptr, nullptr, (LPBYTE)value, &size); + RegCloseKey(key); + + if (ret != ERROR_SUCCESS) + { + return SLANG_FAIL; + } + + outString = value; + return SLANG_OK; +} + +// Make easier to set up the array +static Version _makeVersion(int main, int dot = 0) { return WinVisualStudioUtil::makeVersion(main, dot); } + +VersionInfo _makeVersionInfo(const char* name, int high, int dot = 0) +{ + VersionInfo info; + info.name = name; + info.version = WinVisualStudioUtil::makeVersion(high, dot); + return info; +} + +static const VersionInfo s_versionInfos[] = +{ + _makeVersionInfo("VS 2005", 8), + _makeVersionInfo("VS 2008", 9), + _makeVersionInfo("VS 2010", 10), + _makeVersionInfo("VS 2012", 11), + _makeVersionInfo("VS 2013", 12), + _makeVersionInfo("VS 2015", 14), + _makeVersionInfo("VS 2017", 15), + _makeVersionInfo("VS 2019", 16), +}; + +// When trying to figure out how this stuff works by running regedit - care is needed, +// because what regedit displays varies on which version of regedit is used. +// In order to use the registry paths used here it's necessary to use Start/Run with +// %systemroot%\syswow64\regedit to view 32 bit keys + +static const RegistryInfo s_regInfos[] = +{ + {"SOFTWARE\\Microsoft\\VisualStudio\\SxS\\VC7", "" }, + {"SOFTWARE\\Microsoft\\VisualStudio\\SxS\\VS7", "VC\\Auxiliary\\Build\\" }, +}; + +static bool _canUseVSWhere(Version version) +{ + // If greater than 15.0 we can use vswhere tool + return (int(version) >= int(_makeVersion(15))); +} + +static int _getRegistryKeyIndex(Version version) +{ + if (int(version) >= int(_makeVersion(15))) + { + return 1; + } + return 0; +} + +/* static */void WinVisualStudioUtil::getVersions(List& outVersions) +{ + const int count = SLANG_COUNT_OF(s_versionInfos); + outVersions.setCount(count); + + Version* dst = outVersions.begin(); + for (int i = 0; i < count; ++i) + { + dst[i] = s_versionInfos[i].version; + } +} + +/* static */WinVisualStudioUtil::Version WinVisualStudioUtil::getCompiledVersion() +{ + // Get the version of visual studio used to compile this source + const uint32_t version = _MSC_VER; + + switch (version) + { + case 1400: return _makeVersion(8); + case 1500: return _makeVersion(9); + case 1600: return _makeVersion(10); + case 1700: return _makeVersion(11); + case 1800: return _makeVersion(12); + case 1900: + { + return _makeVersion(14); + } + case 1911: + case 1912: + case 1913: + case 1914: + case 1915: + case 1916: + { + return _makeVersion(15); + } + case 1920: + { + return _makeVersion(16); + } + default: + { + int lastKnownVersion = 1920; + if (version > lastKnownVersion) + { + // Its an unknown newer version + return Version::Future; + } + break; + } + } + + // Unknown version + return Version::Unknown; +} + +static SlangResult _find(int versionIndex, WinVisualStudioUtil::VersionPath& outPath) +{ + const auto& versionInfo = s_versionInfos[versionIndex]; + + auto version = versionInfo.version; + + outPath.version = version; + outPath.vcvarsPath = String(); + + if (_canUseVSWhere(version)) + { + CommandLine cmd; + + // Lookup directly %ProgramFiles(x86)% path + // https://docs.microsoft.com/en-us/windows/desktop/api/shlobj_core/nf-shlobj_core-shgetfolderpatha + HWND hwnd = GetConsoleWindow(); + + char programFilesPath[_MAX_PATH]; + SHGetFolderPathA(hwnd, CSIDL_PROGRAM_FILESX86, NULL, 0, programFilesPath); + + String vswherePath = programFilesPath; + vswherePath.append("\\Microsoft Visual Studio\\Installer\\vswhere"); + + cmd.setExecutableFilename(vswherePath); + + StringBuilder versionName; + WinVisualStudioUtil::append(version, versionName); + + String args[] = { "-version", versionName, "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "-property", "installationPath" }; + cmd.addArgs(args, SLANG_COUNT_OF(args)); + + ExecuteResult exeRes; + if (SLANG_SUCCEEDED(ProcessUtil::execute(cmd, exeRes))) + { + // We need to chopoff CR/LF if there is one + List lines; + StringUtil::calcLines(exeRes.standardOutput.getUnownedSlice(), lines); + + if (lines.getCount()) + { + outPath.vcvarsPath = lines[0]; + outPath.vcvarsPath.append("\\VC\\Auxiliary\\Build\\"); + return SLANG_OK; + } + } + } + + const Int keyIndex = _getRegistryKeyIndex(version); + if (keyIndex >= 0) + { + SLANG_ASSERT(keyIndex < SLANG_COUNT_OF(s_regInfos)); + + // Try reading the key + const auto& keyInfo = s_regInfos[keyIndex]; + + StringBuilder keyName; + WinVisualStudioUtil::append(versionInfo.version, keyName); + + String value; + if (SLANG_SUCCEEDED(_readRegistryKey(keyInfo.regName, keyName.getBuffer(), value))) + { + outPath.vcvarsPath = value; + return SLANG_OK; + } + } + + return SLANG_FAIL; +} + +/* static */SlangResult WinVisualStudioUtil::find(List& outVersionPaths) +{ + outVersionPaths.clear(); + + const int versionCount = SLANG_COUNT_OF(s_versionInfos); + + for (int i = versionCount - 1; i >= 0; --i) + { + VersionPath versionPath; + if (SLANG_SUCCEEDED(_find(i, versionPath))) + { + outVersionPaths.add(versionPath); + } + } + + return SLANG_OK; +} + +/* static */SlangResult WinVisualStudioUtil::find(Version version, VersionPath& outPath) +{ + const int versionCount = SLANG_COUNT_OF(s_versionInfos); + + for (int i = 0; i < versionCount; ++i) + { + const auto& versionInfo = s_versionInfos[i]; + if (versionInfo.version == version) + { + return _find(i, outPath); + } + } + return SLANG_FAIL; +} + +/* static */SlangResult WinVisualStudioUtil::executeCompiler(const VersionPath& versionPath, const CommandLine& commandLine, ExecuteResult& outResult) +{ + // To invoke cl we need to run the suitable vcvars. In order to run this we have to have MS CommandLine. + // So here we build up a cl command line that is run by first running vcvars, and then executing cl with the parameters as passed to commandLine + + StringBuilder builder; + CommandLine cmdLine; + + cmdLine.setExecutableFilename("cmd.exe"); + { + String options[] = { "/q", "/c", "@prompt", "$" }; + cmdLine.addArgs(options, SLANG_COUNT_OF(options)); + } + + cmdLine.addArg("&&"); + + { + StringBuilder path; + path << versionPath.vcvarsPath; + path << "\\vcvarsall.bat"; + cmdLine.addArg(path); + } + +#if SLANG_PTR_IS_32 + cmdLine.addArg("x86"); +#else + cmdLine.addArg("x86_amd64"); +#endif + + cmdLine.addArg("&&"); + cmdLine.addArg("cl"); + + // Append the command line options + cmdLine.addArgs(commandLine.m_args.getBuffer(), commandLine.m_args.getCount()); + + return ProcessUtil::execute(cmdLine, outResult); +} + +/* static */void WinVisualStudioUtil::append(Version version, StringBuilder& outBuilder) +{ + switch (version) + { + case Version::Unknown: + { + outBuilder << "unknown"; + } + case Version::Future: + { + outBuilder << "future"; + break; + } + default: + { + outBuilder << (int(version) / 10) << "." << (int(version) % 10); + break; + } + } +} + +} // namespace Slang diff --git a/source/core/windows/slang-win-visual-studio-util.h b/source/core/windows/slang-win-visual-studio-util.h new file mode 100644 index 000000000..aec4493df --- /dev/null +++ b/source/core/windows/slang-win-visual-studio-util.h @@ -0,0 +1,50 @@ +#ifndef SLANG_WIN_VISUAL_STUDIO_UTIL_H +#define SLANG_WIN_VISUAL_STUDIO_UTIL_H + +#include "../slang-list.h" +#include "../slang-string.h" + +#include "../slang-process-util.h" + +namespace Slang { + +struct WinVisualStudioUtil +{ + enum class Version: uint32_t + { + Unknown = 0, ///< This is an unknown (and not later) version + Future = 0xff * 10, ///< This is a version 'from the future' - that isn't specifically known. Will be treated as latest + }; + + struct VersionPath + { + Version version; ///< The visual studio version + String vcvarsPath; ///< The path to vcvars bat files, that need to be executed before executing the compiler + }; + + /// Find all the installations + static SlangResult find(List& outVersionPaths); + + /// Given a version find it's path + static SlangResult find(Version version, VersionPath& outPath); + + /// Run visual studio on specified path with the parameters specified on the command line. Output placed in outResult. + static SlangResult executeCompiler(const VersionPath& versionPath, const CommandLine& commandLine, ExecuteResult& outResult); + + /// Get all the known version numbers + static void getVersions(List& outVersions); + + /// Gets the msc compiler used to compile this version. Returning Version(0) means unknown + static Version getCompiledVersion(); + + /// Create a version from a high and low indices + static Version makeVersion(int high, int low = 0) { SLANG_ASSERT(low >= 0 && low <= 9); return Version(high * 10 + low); } + + /// Convert a version number into a string + static void append(Version version, StringBuilder& outBuilder); + +}; + +} // namespace Slang + +#endif diff --git a/tools/slang-generate/main.cpp b/tools/slang-generate/main.cpp index ddd087072..046b2063c 100644 --- a/tools/slang-generate/main.cpp +++ b/tools/slang-generate/main.cpp @@ -7,6 +7,7 @@ #include "../../source/core/slang-list.h" #include "../../source/core/slang-string.h" +#include "../../source/core/slang-string-util.h" using namespace Slang; @@ -601,7 +602,7 @@ void emitCodeNodes( } // Given line starts and a location, find the line number. Returns -1 if not found -static Index _findLineIndex(const List& lineBreaks, const char* location) +static Index _findLineIndex(const List& lineBreaks, const char* location) { if (location == nullptr) { @@ -615,7 +616,7 @@ static Index _findLineIndex(const List& lineBreaks, const char* loc while (lo + 1 < hi) { const auto mid = (hi + lo) >> 1; - const auto midOffset = lineBreaks[mid]; + const auto midOffset = lineBreaks[mid].begin(); if (midOffset <= location) { lo = mid; @@ -629,50 +630,14 @@ static Index _findLineIndex(const List& lineBreaks, const char* loc return lo; } -static void _calcLineBreaks(const UnownedStringSlice& content, List& outLineStarts) -{ - char const* begin = content.begin(); - char const* end = content.end(); - - char const* cursor = begin; - - // Treat the beginning of the file as a line break - outLineStarts.add(cursor); - - while (cursor != end) - { - int c = *cursor++; - switch (c) - { - case '\r': case '\n': - { - // When we see a line-break character we need - // to record the line break, but we also need - // to deal with the annoying issue of encodings, - // where a multi-byte sequence might encode - // the line break. - - int d = *cursor; - if ((c^d) == ('\r' ^ '\n')) - cursor++; - - outLineStarts.add(cursor); - break; - } - default: - break; - } - } -} - void emitTemplateNodes( SourceFile* sourceFile, FILE* stream, Node* node) { // Work out - List lineBreaks; - _calcLineBreaks(sourceFile->text, lineBreaks); + List lineBreaks; + StringUtil::calcLines(sourceFile->text, lineBreaks); Node* prev = nullptr; for (auto nn = node; nn; prev = nn, nn = nn->next) diff --git a/tools/slang-test/options.cpp b/tools/slang-test/options.cpp index 3e985b493..4c8108f4a 100644 --- a/tools/slang-test/options.cpp +++ b/tools/slang-test/options.cpp @@ -1,8 +1,8 @@ // test-context.cpp #include "options.h" -#include "os.h" #include "../../source/core/slang-string-util.h" +#include "../../source/core/slang-io.h" #include #include diff --git a/tools/slang-test/os.cpp b/tools/slang-test/os.cpp index a938a71e7..eceeef2d7 100644 --- a/tools/slang-test/os.cpp +++ b/tools/slang-test/os.cpp @@ -123,266 +123,6 @@ OSFindFilesResult osFindChildDirectories( return result; } -// OSProcessSpawner - -struct OSProcessSpawner_ReaderThreadInfo -{ - HANDLE file; - String output; -}; - -static DWORD WINAPI osReaderThreadProc(LPVOID threadParam) -{ - OSProcessSpawner_ReaderThreadInfo* info = (OSProcessSpawner_ReaderThreadInfo*)threadParam; - HANDLE file = info->file; - - static const int kChunkSize = 1024; - char buffer[kChunkSize]; - - StringBuilder outputBuilder; - - // We need to re-write the output to deal with line - // endings, so we check for paired '\r' and '\n' - // characters, which may span chunks. - int prevChar = -1; - - for (;;) - { - DWORD bytesRead = 0; - BOOL readResult = ReadFile(file, buffer, kChunkSize, &bytesRead, nullptr); - - if (!readResult || GetLastError() == ERROR_BROKEN_PIPE) - { - break; - } - - // walk the buffer and rewrite to eliminate '\r' '\n' pairs - char* readCursor = buffer; - char const* end = buffer + bytesRead; - char* writeCursor = buffer; - - while (readCursor != end) - { - int p = prevChar; - int c = *readCursor++; - prevChar = c; - switch (c) - { - case '\r': case '\n': - // swallow input if '\r' and '\n' appear in sequence - if ((p ^ c) == ('\r' ^ '\n')) - { - // but don't swallow the next byte - prevChar = -1; - continue; - } - // always replace '\r' with '\n' - c = '\n'; - break; - - default: - break; - } - - *writeCursor++ = (char)c; - } - bytesRead = (DWORD)(writeCursor - buffer); - - // Note: Current "core" implementation gives no way to know - // the length of the buffer, so we ultimately have - // to just assume null termination... - outputBuilder.Append(buffer, bytesRead); - } - - info->output = outputBuilder.ProduceString(); - - return 0; -} - -void OSProcessSpawner::pushExecutableName( - Slang::String executableName) -{ - executableName_ = executableName; - commandLine_.Append(executableName); - isExecutablePath_ = false; -} - -void OSProcessSpawner::pushExecutablePath( - Slang::String executablePath) -{ - executableName_ = executablePath; - commandLine_.Append(executablePath); - isExecutablePath_ = true; -} - -void OSProcessSpawner::pushArgument( - Slang::String argument) -{ - // TODO(tfoley): handle cases where arguments need some escaping - commandLine_.Append(" "); - commandLine_.Append(argument); - - argumentList_.add(argument); -} - -Slang::String OSProcessSpawner::getCommandLine() -{ - return commandLine_; -} - -OSError OSProcessSpawner::spawnAndWaitForCompletion() -{ - SECURITY_ATTRIBUTES securityAttributes; - securityAttributes.nLength = sizeof(securityAttributes); - securityAttributes.lpSecurityDescriptor = nullptr; - securityAttributes.bInheritHandle = true; - - // create stdout pipe for child process - HANDLE childStdOutReadTmp = nullptr; - HANDLE childStdOutWrite = nullptr; - if (!CreatePipe(&childStdOutReadTmp, &childStdOutWrite, &securityAttributes, 0)) - { - return kOSError_OperationFailed; - } - - // create stderr pipe for child process - HANDLE childStdErrReadTmp = nullptr; - HANDLE childStdErrWrite = nullptr; - if (!CreatePipe(&childStdErrReadTmp, &childStdErrWrite, &securityAttributes, 0)) - { - return kOSError_OperationFailed; - } - - // create stdin pipe for child process - HANDLE childStdInRead = nullptr; - HANDLE childStdInWriteTmp = nullptr; - if (!CreatePipe(&childStdInRead, &childStdInWriteTmp, &securityAttributes, 0)) - { - return kOSError_OperationFailed; - } - - HANDLE currentProcess = GetCurrentProcess(); - - // create a non-inheritable duplicate of the stdout reader - HANDLE childStdOutRead = nullptr; - if (!DuplicateHandle( - currentProcess, childStdOutReadTmp, - currentProcess, &childStdOutRead, - 0, FALSE, DUPLICATE_SAME_ACCESS)) - { - return kOSError_OperationFailed; - } - if (!CloseHandle(childStdOutReadTmp)) - { - return kOSError_OperationFailed; - } - - // create a non-inheritable duplicate of the stderr reader - HANDLE childStdErrRead = nullptr; - if (!DuplicateHandle( - currentProcess, childStdErrReadTmp, - currentProcess, &childStdErrRead, - 0, FALSE, DUPLICATE_SAME_ACCESS)) - { - return kOSError_OperationFailed; - } - if (!CloseHandle(childStdErrReadTmp)) - { - return kOSError_OperationFailed; - } - - // create a non-inheritable duplicate of the stdin writer - HANDLE childStdInWrite = nullptr; - if (!DuplicateHandle( - currentProcess, childStdInWriteTmp, - currentProcess, &childStdInWrite, - 0, FALSE, DUPLICATE_SAME_ACCESS)) - { - return kOSError_OperationFailed; - } - if (!CloseHandle(childStdInWriteTmp)) - { - return kOSError_OperationFailed; - } - - // Now we can actually get around to starting a process - PROCESS_INFORMATION processInfo; - ZeroMemory(&processInfo, sizeof(processInfo)); - - // 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; - - // `CreateProcess` requires write access to this, for some reason... - BOOL success = CreateProcessW( - isExecutablePath_ ? executableName_.toWString().begin() : nullptr, - (LPWSTR)commandLine_.ToString().toWString().begin(), - nullptr, - nullptr, - true, - CREATE_NO_WINDOW, - nullptr, // TODO: allow specifying environment variables? - nullptr, - &startupInfo, - &processInfo); - if (!success) - { - return kOSError_OperationFailed; - } - - // close handles we are now done with - CloseHandle(processInfo.hThread); - CloseHandle(childStdOutWrite); - CloseHandle(childStdErrWrite); - CloseHandle(childStdInRead); - - // Create a thread to read from the child's stdout. - OSProcessSpawner_ReaderThreadInfo stdOutThreadInfo; - stdOutThreadInfo.file = childStdOutRead; - HANDLE stdOutThread = CreateThread(nullptr, 0, &osReaderThreadProc, (LPVOID)&stdOutThreadInfo, 0, nullptr); - - // Create a thread to read from the child's stderr. - OSProcessSpawner_ReaderThreadInfo stdErrThreadInfo; - stdErrThreadInfo.file = childStdErrRead; - HANDLE stdErrThread = CreateThread(nullptr, 0, &osReaderThreadProc, (LPVOID)&stdErrThreadInfo, 0, nullptr); - - // wait for the process to exit - // TODO: set a timeout as a safety measure... - WaitForSingleObject(processInfo.hProcess, INFINITE); - - // get exit code for process - DWORD childExitCode = 0; - if (!GetExitCodeProcess(processInfo.hProcess, &childExitCode)) - { - return kOSError_OperationFailed; - } - - // wait for the reader threads - WaitForSingleObject(stdOutThread, INFINITE); - WaitForSingleObject(stdErrThread, INFINITE); - - CloseHandle(processInfo.hProcess); - CloseHandle(childStdOutRead); - CloseHandle(childStdErrRead); - CloseHandle(childStdInWrite); - - standardOutput_ = stdOutThreadInfo.output; - standardError_ = stdErrThreadInfo.output; - resultCode_ = childExitCode; - - return kOSError_None; -} - -char const* osGetExecutableSuffix() -{ - return ".exe"; -} - #else static bool advance(OSFindFilesResult& result) @@ -475,213 +215,4 @@ OSFindFilesResult osFindChildDirectories( return result; } -// OSProcessSpawner - -void OSProcessSpawner::pushExecutableName( - Slang::String executableName) -{ - executableName_ = executableName; - arguments_.add(executableName); - isExecutablePath_ = false; -} - -void OSProcessSpawner::pushExecutablePath( - Slang::String executablePath) -{ - executableName_ = executablePath; - arguments_.add(executablePath); - isExecutablePath_ = true; -} - -void OSProcessSpawner::pushArgument( - Slang::String argument) -{ - arguments_.add(argument); - argumentList_.add(argument); -} - -Slang::String OSProcessSpawner::getCommandLine() -{ - Slang::UInt argCount = arguments_.getCount(); - - Slang::StringBuilder sb; - for(Slang::UInt ii = 0; ii < argCount; ++ii) - { - if(ii != 0) sb << " "; - sb << arguments_[ii]; - } - return sb.ProduceString(); -} - -OSError OSProcessSpawner::spawnAndWaitForCompletion() -{ - List argPtrs; - for(auto arg : arguments_) - { - argPtrs.add(arg.getBuffer()); - } - argPtrs.add(NULL); - - int stdoutPipe[2]; - int stderrPipe[2]; - - if(pipe(stdoutPipe) == -1) - return kOSError_OperationFailed; - - if(pipe(stderrPipe) == -1) - return kOSError_OperationFailed; - - pid_t childProcessID = fork(); - if (childProcessID == -1) - return kOSError_OperationFailed; - - if(childProcessID == 0) - { - // We are the child process. - - dup2(stdoutPipe[1], STDOUT_FILENO); - dup2(stderrPipe[1], STDERR_FILENO); - - close(stdoutPipe[0]); - close(stdoutPipe[1]); - - close(stderrPipe[0]); - close(stderrPipe[1]); - - execvp( - argPtrs[0], - (char* const*) &argPtrs[0]); - - // If we get here, then `exec` failed - fprintf(stderr, "error: `exec` failed\n"); - exit(1); - } - else - { - // We are the parent process - - close(stdoutPipe[1]); - close(stderrPipe[1]); - - int stdoutFD = stdoutPipe[0]; - int stderrFD = stderrPipe[0]; - - pollfd pollInfos[2]; - nfds_t pollInfoCount = 2; - - pollInfos[0].fd = stdoutFD; - pollInfos[0].events = POLLIN; - pollInfos[0].revents = 0; - pollInfos[1].fd = stderrFD; - pollInfos[1].events = POLLIN; - pollInfos[1].revents = 0; - - int remainingCount = 2; - int iterations = 0; - while(remainingCount) - { - // Safeguard against infinite loop: - iterations++; - if (iterations > 10000) - { - fprintf(stderr, "poll(): %d iterations\n", iterations); - return kOSError_OperationFailed; - } - - // Set a timeout of ten seconds; - // we really shouldn't wait too long... - int pollTimeout = 10000; - int pollResult = poll(pollInfos, pollInfoCount, pollTimeout); - if (pollResult <= 0) - { - // If there was a signal that got in - // the way, then retry... - if(pollResult == -1 && errno == EINTR) - continue; - - // timeout or error... - return kOSError_OperationFailed; - } - - enum { kBufferSize = 1024 }; - char buffer[kBufferSize]; - - if(pollInfos[0].revents) - { - auto count = read(stdoutFD, buffer, kBufferSize); - if (count <= 0) - { - // end-of-file - close(stdoutFD); - pollInfos[0].fd = -1; - remainingCount--; - } - - standardOutput_.append( - buffer, buffer + count); - } - - if(pollInfos[1].revents) - { - auto count = read(stderrFD, buffer, kBufferSize); - if (count <= 0) - { - // end-of-file - close(stderrFD); - pollInfos[1].fd = -1; - remainingCount--; - } - - standardError_.append( - buffer, buffer + count); - } - } - - int childStatus = 0; - iterations = 0; - for(;;) - { - // Safeguard against infinite loop: - iterations++; - if (iterations > 10000) - { - fprintf(stderr, "waitpid(): %d iterations\n", iterations); - return kOSError_OperationFailed; - } - - - pid_t terminatedProcessID = waitpid( - childProcessID, - &childStatus, - 0); - if (terminatedProcessID == -1) - { - return kOSError_OperationFailed; - } - - if(terminatedProcessID == childProcessID) - { - if(WIFEXITED(childStatus)) - { - resultCode_ = (int)(int8_t)WEXITSTATUS(childStatus); - } - else - { - resultCode_ = 1; - } - - return kOSError_None; - } - } - - } - - return kOSError_OperationFailed; -} - -char const* osGetExecutableSuffix() -{ - return ""; -} - #endif diff --git a/tools/slang-test/os.h b/tools/slang-test/os.h index 3e6d93942..9dd20c62d 100644 --- a/tools/slang-test/os.h +++ b/tools/slang-test/os.h @@ -1,3 +1,6 @@ +#ifndef SLANG_OS_H +#define SLANG_OS_H + // os.h #include "../../source/core/slang-io.h" @@ -21,7 +24,7 @@ #include #include -#include +//#include #include #include #include @@ -129,62 +132,4 @@ OSFindFilesResult osFindFilesInDirectoryMatchingPattern( OSFindFilesResult osFindFilesInDirectory( Slang::String directoryPath); - -// An `OSProcessSpawner` can be used to launch a process, and handles -// putting together the arguments in the form required by the target -// platform, as well as capturing any output from the process (both -// standard output and standard error) as strings. -struct OSProcessSpawner -{ - // Set the executable name for the process to be spawned. - // Note: this call must be made before any arguments are pushed. - void pushExecutableName( - Slang::String executableName); - - // Set the executable name for the process to be spawned. - // Note: this call must be made before any arguments are pushed. - void pushExecutablePath( - Slang::String executablePath); - - // Append an argument for the process to be spawned. - void pushArgument( - Slang::String argument); - - // Get a printable version of the command line - // that will be run (can be used for debugging) - Slang::String getCommandLine(); - - // Attempt to spawn the process, and wait for it to complete. - // Returns an error if the attempt to spawn and/or wait fails, - // but returns `kOSError_None` if the process is run to completion, - // whether or not the process returns "successfully" (with a zero - // result code); - OSError spawnAndWaitForCompletion(); - - // If the process is successfully spawned and completes, then - // the user can query the result code that the process produce - // on exit, along with the output it wrote to stdout and stderr. - typedef int ResultCode; - ResultCode getResultCode() { return resultCode_; } - Slang::String const& getStandardOutput() { return standardOutput_; } - Slang::String const& getStandardError() { return standardError_; } - - // "private" data follows - Slang::String standardOutput_; - Slang::String standardError_; - ResultCode resultCode_; - Slang::String executableName_; -#ifdef WIN32 - Slang::StringBuilder commandLine_; -#else - Slang::List arguments_; -#endif - - // Only holds the argumements in order - Slang::List argumentList_; - - // Is the executable specified by path, rather than just by name? - bool isExecutablePath_; -}; - -char const* osGetExecutableSuffix(); +#endif // SLANG_OS_H diff --git a/tools/slang-test/slang-test-main.cpp b/tools/slang-test/slang-test-main.cpp index f030dc8aa..dac6ff9fe 100644 --- a/tools/slang-test/slang-test-main.cpp +++ b/tools/slang-test/slang-test-main.cpp @@ -18,6 +18,8 @@ using namespace Slang; #include "options.h" #include "slangc-tool.h" +#include "../../source/core/slang-process-util.h" + #define STB_IMAGE_IMPLEMENTATION #include "external/stb/stb_image.h" @@ -93,7 +95,7 @@ typedef TestResult(*TestCallback)(TestContext* context, TestInput& input); // Globals // Pre declare -static void _addRenderTestOptions(const Options& options, OSProcessSpawner& spawner); +static void _addRenderTestOptions(const Options& options, CommandLine& cmdLine); /* !!!!!!!!!!!!!!!!!!!!!!!!!!!!! Functions !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ @@ -382,51 +384,46 @@ TestResult gatherTestsForFile( return TestResult::Pass; } -OSError spawnAndWaitExe(TestContext* context, const String& testPath, OSProcessSpawner& spawner) +Result spawnAndWaitExe(TestContext* context, const String& testPath, const CommandLine& cmdLine, ExecuteResult& outRes) { const auto& options = context->options; if (options.shouldBeVerbose) { - String commandLine = spawner.getCommandLine(); + String commandLine = ProcessUtil::getCommandLineString(cmdLine); context->reporter->messageFormat(TestMessageType::Info, "%s\n", commandLine.begin()); } - OSError err = spawner.spawnAndWaitForCompletion(); - if (err != kOSError_None) + Result res = ProcessUtil::execute(cmdLine, outRes); + if (SLANG_FAILED(res)) { // fprintf(stderr, "failed to run test '%S'\n", testPath.ToWString()); context->reporter->messageFormat(TestMessageType::RunError, "failed to run test '%S'", testPath.toWString().begin()); } - return err; + return res; } -OSError spawnAndWaitSharedLibrary(TestContext* context, const String& testPath, OSProcessSpawner& spawner) +Result spawnAndWaitSharedLibrary(TestContext* context, const String& testPath, const CommandLine& cmdLine, ExecuteResult& outRes) { const auto& options = context->options; - String exeName = Path::getFileNameWithoutExt(spawner.executableName_); + String exeName = Path::getFileNameWithoutExt(cmdLine.m_executable); if (options.shouldBeVerbose) { - StringBuilder builder; - - builder << "slang-test"; + CommandLine testCmdLine; + testCmdLine.setExecutableFilename("slang-test"); if (options.binDir.getLength()) { - builder << " -bindir " << options.binDir; + testCmdLine.addArg("-bindir"); + testCmdLine.addArg(options.binDir); } - builder << " " << exeName; + testCmdLine.addArg(exeName); + testCmdLine.m_args.addRange(cmdLine.m_args); - // TODO(js): Potentially this should handle escaping parameters for the command line if need be - const auto& argList = spawner.argumentList_; - for (Index i = 0; i < argList.getCount(); ++i) - { - builder << " " << argList[i]; - } - - context->reporter->messageFormat(TestMessageType::Info, "%s\n", builder.begin()); + String testCmdLineString = ProcessUtil::getCommandLineString(testCmdLine); + context->reporter->messageFormat(TestMessageType::Info, "%s\n", testCmdLineString.getBuffer()); } auto func = context->getInnerMainFunc(context->options.binDir, exeName); @@ -452,24 +449,24 @@ OSError spawnAndWaitSharedLibrary(TestContext* context, const String& testPath, List args; args.add(exeName.getBuffer()); - for (Index i = 0; i < spawner.argumentList_.getCount(); ++i) + for (Index i = 0; i < cmdLine.m_args.getCount(); ++i) { - args.add(spawner.argumentList_[i].getBuffer()); + args.add(cmdLine.m_args[i].getBuffer()); } SlangResult res = func(&stdWriters, context->getSession(), int(args.getCount()), args.begin()); StdWriters::setSingleton(prevStdWriters); - spawner.standardError_ = stdErrorString; - spawner.standardOutput_ = stdOutString; + outRes.standardError = stdErrorString; + outRes.standardOutput = stdOutString; - spawner.resultCode_ = (int)TestToolUtil::getReturnCode(res); + outRes.resultCode = (int)TestToolUtil::getReturnCode(res); - return kOSError_None; + return SLANG_OK; } - return kOSError_OperationFailed; + return SLANG_FAIL; } @@ -580,10 +577,10 @@ static SlangCompileTarget _getCompileTarget(const UnownedStringSlice& name) return SLANG_TARGET_UNKNOWN; } -static SlangResult _extractRenderTestRequirements(OSProcessSpawner& spawner, TestRequirements* ioRequirements) +static SlangResult _extractRenderTestRequirements(const CommandLine& cmdLine, TestRequirements* ioRequirements) { - const auto& args = spawner.argumentList_; - + const auto& args = cmdLine.m_args; + // TODO(JS): // This is rather convoluted in that it has to work out from the command line parameters passed // to render-test what renderer will be used. @@ -697,11 +694,11 @@ static SlangResult _extractRenderTestRequirements(OSProcessSpawner& spawner, Tes return SLANG_OK; } -static SlangResult _extractSlangCTestRequirements(OSProcessSpawner& spawner, TestRequirements* ioRequirements) +static SlangResult _extractSlangCTestRequirements(const CommandLine& cmdLine, TestRequirements* ioRequirements) { // This determines what the requirements are for a slangc like command line - const auto& args = spawner.argumentList_; - + const auto& args = cmdLine.m_args; + // First check pass through { String passThrough; @@ -724,27 +721,27 @@ static SlangResult _extractSlangCTestRequirements(OSProcessSpawner& spawner, Tes } -static SlangResult _extractReflectionTestRequirements(OSProcessSpawner& spawner, TestRequirements* ioRequirements) +static SlangResult _extractReflectionTestRequirements(const CommandLine& cmdLine, TestRequirements* ioRequirements) { // There are no specialized constraints for a reflection test return SLANG_OK; } -static SlangResult _extractTestRequirements(OSProcessSpawner& spawner, TestRequirements* ioInfo) +static SlangResult _extractTestRequirements(const CommandLine& cmdLine, TestRequirements* ioInfo) { - String exeName = Path::getFileNameWithoutExt(spawner.executableName_); + String exeName = Path::getFileNameWithoutExt(cmdLine.m_executable); if (exeName == "render-test") { - return _extractRenderTestRequirements(spawner, ioInfo); + return _extractRenderTestRequirements(cmdLine, ioInfo); } else if (exeName == "slangc") { - return _extractSlangCTestRequirements(spawner, ioInfo); + return _extractSlangCTestRequirements(cmdLine, ioInfo); } else if (exeName == "slang-reflection-test") { - return _extractReflectionTestRequirements(spawner, ioInfo); + return _extractReflectionTestRequirements(cmdLine, ioInfo); } SLANG_ASSERT(!"Unknown tool type"); @@ -768,20 +765,21 @@ static RenderApiFlags _getAvailableRenderApiFlags(TestContext* context) if (RenderApiUtil::calcHasApi(apiType)) { // Try starting up the device - OSProcessSpawner spawner; - spawner.pushExecutablePath(Path::combine(context->options.binDir, String("render-test") + osGetExecutableSuffix())); - _addRenderTestOptions(context->options, spawner); + CommandLine cmdLine; + cmdLine.setExecutablePath(Path::combine(context->options.binDir, String("render-test") + ProcessUtil::getExecutableSuffix())); + _addRenderTestOptions(context->options, cmdLine); // We just want to see if the device can be started up - spawner.pushArgument("-only-startup"); + cmdLine.addArg("-only-startup"); // Select what api to use StringBuilder builder; builder << "-" << RenderApiUtil::getApiName(apiType); - spawner.pushArgument(builder); + cmdLine.addArg(builder); // Run the render-test tool and see if the device could startup - if (spawnAndWaitSharedLibrary(context, "device-startup", spawner) == OSError::kOSError_None - && TestToolUtil::getReturnCodeFromInt(spawner.resultCode_) == ToolReturnCode::Success) + ExecuteResult exeRes; + if (SLANG_SUCCEEDED(spawnAndWaitSharedLibrary(context, "device-startup", cmdLine, exeRes)) + && TestToolUtil::getReturnCodeFromInt(exeRes.resultCode) == ToolReturnCode::Success) { availableRenderApiFlags |= RenderApiFlags(1) << int(apiType); } @@ -795,17 +793,17 @@ static RenderApiFlags _getAvailableRenderApiFlags(TestContext* context) return context->availableRenderApiFlags; } -ToolReturnCode getReturnCode(OSProcessSpawner& spawner) +ToolReturnCode getReturnCode(const ExecuteResult& exeRes) { - return TestToolUtil::getReturnCodeFromInt(spawner.getResultCode()); + return TestToolUtil::getReturnCodeFromInt(exeRes.resultCode); } -ToolReturnCode spawnAndWait(TestContext* context, const String& testPath, SpawnType spawnType, OSProcessSpawner& spawner) +ToolReturnCode spawnAndWait(TestContext* context, const String& testPath, SpawnType spawnType, const CommandLine& cmdLine, ExecuteResult& outExeRes) { if (context->isCollectingRequirements()) { // If we just want info... don't bother running anything - const SlangResult res = _extractTestRequirements(spawner, context->testRequirements); + const SlangResult res = _extractTestRequirements(cmdLine, context->testRequirements); // Keep compiler happy on release SLANG_UNUSED(res); SLANG_ASSERT(SLANG_SUCCEEDED(res)); @@ -815,37 +813,37 @@ ToolReturnCode spawnAndWait(TestContext* context, const String& testPath, SpawnT const auto& options = context->options; - OSError spawnResult = kOSError_OperationFailed; + SlangResult spawnResult = SLANG_FAIL; switch (spawnType) { case SpawnType::UseExe: { - spawnResult = spawnAndWaitExe(context, testPath, spawner); + spawnResult = spawnAndWaitExe(context, testPath, cmdLine, outExeRes); break; } case SpawnType::UseSharedLibrary: { - spawnResult = spawnAndWaitSharedLibrary(context, testPath, spawner); + spawnResult = spawnAndWaitSharedLibrary(context, testPath, cmdLine, outExeRes); break; } default: break; } - if (spawnResult != kOSError_None) + if (SLANG_FAILED(spawnResult)) { return ToolReturnCode::FailedToRun; } - return getReturnCode(spawner); + return getReturnCode(outExeRes); } -String getOutput(OSProcessSpawner& spawner) +String getOutput(const ExecuteResult& exeRes) { - OSProcessSpawner::ResultCode resultCode = spawner.getResultCode(); - - String standardOuptut = spawner.getStandardOutput(); - String standardError = spawner.getStandardError(); - + ExecuteResult::ResultCode resultCode = exeRes.resultCode; + + String standardOuptut = exeRes.standardOutput; + String standardError = exeRes.standardError; + // We construct a single output string that captures the results StringBuilder actualOutputBuilder; actualOutputBuilder.Append("result code = "); @@ -897,13 +895,13 @@ String findExpectedPath(const TestInput& input, const char* postFix) return ""; } -static void _initSlangCompiler(TestContext* context, OSProcessSpawner& spawnerOut) +static void _initSlangCompiler(TestContext* context, CommandLine& ioCmdLine) { - spawnerOut.pushExecutablePath(Path::combine(context->options.binDir, String("slangc") + osGetExecutableSuffix())); + ioCmdLine.setExecutablePath(Path::combine(context->options.binDir, String("slangc") + ProcessUtil::getExecutableSuffix())); if (context->options.verbosePaths) { - spawnerOut.pushArgument("-verbose-paths"); + ioCmdLine.addArg("-verbose-paths"); } } @@ -933,24 +931,25 @@ TestResult runSimpleTest(TestContext* context, TestInput& input) auto filePath999 = input.filePath; auto outputStem = input.outputStem; - OSProcessSpawner spawner; - _initSlangCompiler(context, spawner); + CommandLine cmdLine; + _initSlangCompiler(context, cmdLine); - spawner.pushArgument(filePath999); + cmdLine.addArg(filePath999); for( auto arg : input.testOptions->args ) { - spawner.pushArgument(arg); + cmdLine.addArg(arg); } - - TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, spawner)); + + ExecuteResult exeRes; + TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, cmdLine, exeRes)); if (context->isCollectingRequirements()) { return TestResult::Pass; } - String actualOutput = getOutput(spawner); + String actualOutput = getOutput(exeRes); String expectedOutputPath = outputStem + ".expected"; String expectedOutput; @@ -1009,24 +1008,25 @@ TestResult runReflectionTest(TestContext* context, TestInput& input) auto filePath = input.filePath; auto outputStem = input.outputStem; - OSProcessSpawner spawner; - - spawner.pushExecutablePath(Path::combine(options.binDir, String("slang-reflection-test") + osGetExecutableSuffix())); - spawner.pushArgument(filePath); + CommandLine cmdLine; + + cmdLine.setExecutablePath(Path::combine(options.binDir, String("slang-reflection-test") + ProcessUtil::getExecutableSuffix())); + cmdLine.addArg(filePath); for( auto arg : input.testOptions->args ) { - spawner.pushArgument(arg); + cmdLine.addArg(arg); } - TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, spawner)); + ExecuteResult exeRes; + TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, cmdLine, exeRes)); if (context->isCollectingRequirements()) { return TestResult::Pass; } - String actualOutput = getOutput(spawner); + String actualOutput = getOutput(exeRes); String expectedOutputPath = outputStem + ".expected"; String expectedOutput; @@ -1097,13 +1097,13 @@ TestResult runCrossCompilerTest(TestContext* context, TestInput& input) auto filePath = input.filePath; auto outputStem = input.outputStem; - OSProcessSpawner actualSpawner; - OSProcessSpawner expectedSpawner; + CommandLine actualCmdLine; + CommandLine expectedCmdLine; - _initSlangCompiler(context, actualSpawner); - _initSlangCompiler(context, expectedSpawner); + _initSlangCompiler(context, actualCmdLine); + _initSlangCompiler(context, expectedCmdLine); - actualSpawner.pushArgument(filePath); + actualCmdLine.addArg(filePath); // TODO(JS): This should no longer be needed with TestInfo accumulated for a test @@ -1124,23 +1124,23 @@ TestResult runCrossCompilerTest(TestContext* context, TestInput& input) { case SLANG_DXIL_ASM: { - expectedSpawner.pushArgument(filePath + ".hlsl"); - expectedSpawner.pushArgument("-pass-through"); - expectedSpawner.pushArgument("dxc"); + expectedCmdLine.addArg(filePath + ".hlsl"); + expectedCmdLine.addArg("-pass-through"); + expectedCmdLine.addArg("dxc"); break; } case SLANG_DXBC_ASM: { - expectedSpawner.pushArgument(filePath + ".hlsl"); - expectedSpawner.pushArgument("-pass-through"); - expectedSpawner.pushArgument("fxc"); + expectedCmdLine.addArg(filePath + ".hlsl"); + expectedCmdLine.addArg("-pass-through"); + expectedCmdLine.addArg("fxc"); break; } default: { - expectedSpawner.pushArgument(filePath + ".glsl"); - expectedSpawner.pushArgument("-pass-through"); - expectedSpawner.pushArgument("glslang"); + expectedCmdLine.addArg(filePath + ".glsl"); + expectedCmdLine.addArg("-pass-through"); + expectedCmdLine.addArg("glslang"); break; } } @@ -1148,16 +1148,17 @@ TestResult runCrossCompilerTest(TestContext* context, TestInput& input) for( auto arg : input.testOptions->args ) { - actualSpawner.pushArgument(arg); - expectedSpawner.pushArgument(arg); + actualCmdLine.addArg(arg); + expectedCmdLine.addArg(arg); } - TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, expectedSpawner)); + ExecuteResult expectedExeRes; + TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, expectedCmdLine, expectedExeRes)); String expectedOutput; if (context->isExecuting()) { - expectedOutput = getOutput(expectedSpawner); + expectedOutput = getOutput(expectedExeRes); String expectedOutputPath = outputStem + ".expected"; try { @@ -1169,14 +1170,15 @@ TestResult runCrossCompilerTest(TestContext* context, TestInput& input) } } - TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, actualSpawner)); + ExecuteResult actualExeRes; + TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, actualCmdLine, actualExeRes)); if (context->isCollectingRequirements()) { return TestResult::Pass; } - String actualOutput = getOutput(actualSpawner); + String actualOutput = getOutput(actualExeRes); TestResult result = TestResult::Pass; @@ -1190,7 +1192,7 @@ TestResult runCrossCompilerTest(TestContext* context, TestInput& input) // to catch situations where, e.g., command-line options parsing // caused the same error in both the Slang and glslang cases. // - if( actualSpawner.getResultCode() != 0 ) + if(actualExeRes.resultCode != 0 ) { result = TestResult::Fail; } @@ -1214,29 +1216,30 @@ TestResult generateHLSLBaseline(TestContext* context, TestInput& input) auto filePath999 = input.filePath; auto outputStem = input.outputStem; - OSProcessSpawner spawner; - _initSlangCompiler(context, spawner); + CommandLine cmdLine; + _initSlangCompiler(context, cmdLine); - spawner.pushArgument(filePath999); + cmdLine.addArg(filePath999); for( auto arg : input.testOptions->args ) { - spawner.pushArgument(arg); + cmdLine.addArg(arg); } - spawner.pushArgument("-target"); - spawner.pushArgument("dxbc-assembly"); - spawner.pushArgument("-pass-through"); - spawner.pushArgument("fxc"); + cmdLine.addArg("-target"); + cmdLine.addArg("dxbc-assembly"); + cmdLine.addArg("-pass-through"); + cmdLine.addArg("fxc"); - TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, spawner)); + ExecuteResult exeRes; + TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, cmdLine, exeRes)); if (context->isCollectingRequirements()) { return TestResult::Pass; } - String expectedOutput = getOutput(spawner); + String expectedOutput = getOutput(exeRes); String expectedOutputPath = outputStem + ".expected"; try { @@ -1262,24 +1265,25 @@ TestResult runHLSLComparisonTest(TestContext* context, TestInput& input) // need to execute the stand-alone Slang compiler on the file, and compare its output to what we expect - OSProcessSpawner spawner; - _initSlangCompiler(context, spawner); + CommandLine cmdLine; + _initSlangCompiler(context, cmdLine); - spawner.pushArgument(filePath999); + cmdLine.addArg(filePath999); for( auto arg : input.testOptions->args ) { - spawner.pushArgument(arg); + cmdLine.addArg(arg); } // TODO: The compiler should probably define this automatically... - spawner.pushArgument("-D"); - spawner.pushArgument("__SLANG__"); + cmdLine.addArg("-D"); + cmdLine.addArg("__SLANG__"); - spawner.pushArgument("-target"); - spawner.pushArgument("dxbc-assembly"); + cmdLine.addArg("-target"); + cmdLine.addArg("dxbc-assembly"); - TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, spawner)); + ExecuteResult exeRes; + TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, cmdLine, exeRes)); if (context->isCollectingRequirements()) { @@ -1289,11 +1293,11 @@ TestResult runHLSLComparisonTest(TestContext* context, TestInput& input) // We ignore output to stdout, and only worry about what the compiler // wrote to stderr. - OSProcessSpawner::ResultCode resultCode = spawner.getResultCode(); - - String standardOutput = spawner.getStandardOutput(); - String standardError = spawner.getStandardError(); - + ExecuteResult::ResultCode resultCode = exeRes.resultCode; + + String standardOutput = exeRes.standardOutput; + String standardError = exeRes.standardError; + // We construct a single output string that captures the results StringBuilder actualOutputBuilder; actualOutputBuilder.Append("result code = "); @@ -1364,42 +1368,43 @@ TestResult doGLSLComparisonTestRun(TestContext* context, auto filePath999 = input.filePath; auto outputStem = input.outputStem; - OSProcessSpawner spawner; - _initSlangCompiler(context, spawner); + CommandLine cmdLine; + _initSlangCompiler(context, cmdLine); - spawner.pushArgument(filePath999); + cmdLine.addArg(filePath999); if( langDefine ) { - spawner.pushArgument("-D"); - spawner.pushArgument(langDefine); + cmdLine.addArg("-D"); + cmdLine.addArg(langDefine); } if( passThrough ) { - spawner.pushArgument("-pass-through"); - spawner.pushArgument(passThrough); + cmdLine.addArg("-pass-through"); + cmdLine.addArg(passThrough); } - spawner.pushArgument("-target"); - spawner.pushArgument("spirv-assembly"); + cmdLine.addArg("-target"); + cmdLine.addArg("spirv-assembly"); for( auto arg : input.testOptions->args ) { - spawner.pushArgument(arg); + cmdLine.addArg(arg); } - TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, spawner)); + ExecuteResult exeRes; + TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, cmdLine, exeRes)); if (context->isCollectingRequirements()) { return TestResult::Pass; } - OSProcessSpawner::ResultCode resultCode = spawner.getResultCode(); + ExecuteResult::ResultCode resultCode = exeRes.resultCode; - String standardOuptut = spawner.getStandardOutput(); - String standardError = spawner.getStandardError(); + String standardOuptut = exeRes.standardOutput; + String standardError = exeRes.standardError; // We construct a single output string that captures the results StringBuilder outputBuilder; @@ -1458,12 +1463,12 @@ TestResult runGLSLComparisonTest(TestContext* context, TestInput& input) return TestResult::Pass; } -static void _addRenderTestOptions(const Options& options, OSProcessSpawner& spawner) +static void _addRenderTestOptions(const Options& options, CommandLine& ioCmdLine) { if (options.adapter.getLength()) { - spawner.pushArgument("-adapter"); - spawner.pushArgument(options.adapter); + ioCmdLine.addArg("-adapter"); + ioCmdLine.addArg(options.adapter); } } @@ -1473,25 +1478,25 @@ TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, cons auto filePath999 = input.filePath; auto outputStem = input.outputStem; - OSProcessSpawner spawner; + CommandLine cmdLine; - spawner.pushExecutablePath(Path::combine(context->options.binDir, String("render-test") + osGetExecutableSuffix())); - spawner.pushArgument(filePath999); + cmdLine.setExecutablePath(Path::combine(context->options.binDir, String("render-test") + ProcessUtil::getExecutableSuffix())); + cmdLine.addArg(filePath999); - _addRenderTestOptions(context->options, spawner); + _addRenderTestOptions(context->options, cmdLine); for (auto arg : input.testOptions->args) { - spawner.pushArgument(arg); + cmdLine.addArg(arg); } for (int i = 0; i < int(numLangOpts); ++i) { - spawner.pushArgument(langOpts[i]); + cmdLine.addArg(langOpts[i]); } - spawner.pushArgument("-o"); + cmdLine.addArg("-o"); auto actualOutputFile = outputStem + ".actual.txt"; - spawner.pushArgument(actualOutputFile); + cmdLine.addArg(actualOutputFile); if (context->isExecuting()) { @@ -1499,7 +1504,8 @@ TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, cons File::writeAllText(actualOutputFile, ""); } - TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, spawner)); + ExecuteResult exeRes; + TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, cmdLine, exeRes)); if (context->isCollectingRequirements()) { @@ -1512,7 +1518,7 @@ TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, cons return TestResult::Fail; } - auto actualOutput = getOutput(spawner); + auto actualOutput = getOutput(exeRes); auto expectedOutput = getExpectedOutput(outputStem); if (actualOutput != expectedOutput) { @@ -1591,34 +1597,35 @@ TestResult doRenderComparisonTestRun(TestContext* context, TestInput& input, cha auto filePath = input.filePath; auto outputStem = input.outputStem; - OSProcessSpawner spawner; + CommandLine cmdLine; - spawner.pushExecutablePath(Path::combine(context->options.binDir, String("render-test") + osGetExecutableSuffix())); - spawner.pushArgument(filePath); + cmdLine.setExecutablePath(Path::combine(context->options.binDir, String("render-test") + ProcessUtil::getExecutableSuffix())); + cmdLine.addArg(filePath); - _addRenderTestOptions(context->options, spawner); + _addRenderTestOptions(context->options, cmdLine); for( auto arg : input.testOptions->args ) { - spawner.pushArgument(arg); + cmdLine.addArg(arg); } - spawner.pushArgument(langOption); - spawner.pushArgument("-o"); - spawner.pushArgument(outputStem + outputKind + ".png"); + cmdLine.addArg(langOption); + cmdLine.addArg("-o"); + cmdLine.addArg(outputStem + outputKind + ".png"); - TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, spawner)); + ExecuteResult exeRes; + TEST_RETURN_ON_DONE(spawnAndWait(context, outputStem, input.spawnType, cmdLine, exeRes)); if (context->isCollectingRequirements()) { return TestResult::Pass; } - OSProcessSpawner::ResultCode resultCode = spawner.getResultCode(); - - String standardOutput = spawner.getStandardOutput(); - String standardError = spawner.getStandardError(); + ExecuteResult::ResultCode resultCode = exeRes.resultCode; + String standardOutput = exeRes.standardOutput; + String standardError = exeRes.standardError; + // We construct a single output string that captures the results StringBuilder outputBuilder; outputBuilder.Append("result code = "); diff --git a/tools/slang-test/test-context.cpp b/tools/slang-test/test-context.cpp index e4d1df449..0b17b3672 100644 --- a/tools/slang-test/test-context.cpp +++ b/tools/slang-test/test-context.cpp @@ -1,7 +1,7 @@ // test-context.cpp #include "test-context.h" -#include "os.h" +#include "../../source/core/slang-io.h" #include "../../source/core/slang-string-util.h" #include diff --git a/tools/slang-test/test-reporter.cpp b/tools/slang-test/test-reporter.cpp index d2402d61b..f7ab3cf1e 100644 --- a/tools/slang-test/test-reporter.cpp +++ b/tools/slang-test/test-reporter.cpp @@ -1,8 +1,8 @@ // test-reporter.cpp #include "test-reporter.h" -#include "os.h" #include "../../source/core/slang-string-util.h" +#include "../../source/core/slang-process-util.h" #include #include @@ -373,29 +373,31 @@ void TestReporter::_addResult(const TestInfo& info) break; } - OSProcessSpawner spawner; - spawner.pushExecutableName("appveyor"); - spawner.pushArgument("AddTest"); - spawner.pushArgument(info.name); - spawner.pushArgument("-FileName"); + CommandLine cmdLine; + cmdLine.setExecutableFilename("appveyor"); + cmdLine.addArg("AddTest"); + cmdLine.addArg(info.name); + cmdLine.addArg("-FileName"); // TODO: this isn't actually a file name in all cases - spawner.pushArgument(info.name); - spawner.pushArgument("-Framework"); - spawner.pushArgument("slang-test"); - spawner.pushArgument("-Outcome"); - spawner.pushArgument(resultString); - - auto err = spawner.spawnAndWaitForCompletion(); - - if (err != kOSError_None) + cmdLine.addArg(info.name); + cmdLine.addArg("-Framework"); + cmdLine.addArg("slang-test"); + cmdLine.addArg("-Outcome"); + cmdLine.addArg(resultString); + + ExecuteResult exeRes; + SlangResult res = ProcessUtil::execute(cmdLine, exeRes); + + if (SLANG_FAILED(res)) { messageFormat(TestMessageType::Info, "failed to add appveyor test results for '%S'\n", info.name.toWString().begin()); #if 0 - fprintf(stderr, "[%d] TEST RESULT: %s {%d} {%s} {%s}\n", err, spawner.commandLine_.getBuffer(), - spawner.getResultCode(), - spawner.getStandardOutput().begin(), - spawner.getStandardError().begin()); + String cmdLineString = ProcessUtil::getCommandLineString(cmdLine); + fprintf(stderr, "[%d] TEST RESULT: %s {%d} {%s} {%s}\n", err, cmdLineString.getBuffer(), + exeRes.resultCode, + exeRes.standardOutput.begin(), + exeRes.standardError.begin()); #endif } -- cgit v1.2.3