diff --git a/Core/Foundation/CMakeLists.txt b/Core/Foundation/CMakeLists.txt index 04028562..f85322e7 100644 --- a/Core/Foundation/CMakeLists.txt +++ b/Core/Foundation/CMakeLists.txt @@ -8,6 +8,7 @@ set(SOURCES "Source/DelayedTaskScheduler.h" "Source/PerfTrace.cpp" "Source/StandardStreamLogger.cpp" + "Source/StandardStreamLoggerLines.h" "Source/StandardStreamLoggerPlatform.h") # .inl bodies are #include'd by the platform TUs (not separate translation units). diff --git a/Core/Foundation/Include/Babylon/StandardStreamLogger.h b/Core/Foundation/Include/Babylon/StandardStreamLogger.h index 765fb149..2ce786f8 100644 --- a/Core/Foundation/Include/Babylon/StandardStreamLogger.h +++ b/Core/Foundation/Include/Babylon/StandardStreamLogger.h @@ -11,6 +11,25 @@ namespace Babylon::StandardStreamLogger * forwards to OutputDebugString while preserving the original stream destination. * Other Unix platforms already expose standard streams and leave them unchanged. * + * Persistent private descriptors are non-inheritable, and the original standard-stream + * inheritance flags are preserved (including the Windows CRT no-inherit state + * when the original handle is non-inheritable). Applications must serialize + * concurrent child-process creation with Start()/Stop(): redirection and flag + * restoration are not a single atomic operation. Windows also uses a temporary + * CRT mode probe and reopens non-inheritable standard descriptors; serialize + * concurrent CRT descriptor allocation with Start()/Stop() as well. Apple + * lacks atomic close-on-exec pipe creation. + * + * Platform diagnostics split long lines to fit their sink's size limit. + * Chunking does not affect the tee to the original stream destination. + * Windows preserves CRT translation modes for targets with valid handles. Already-translated + * pipe bytes are teed without another translation; UTF-16 diagnostics are + * decoded to UTF-8 before line splitting (malformed input uses U+FFFD). + * + * Other Windows CRT descriptor flags, including _O_APPEND, are not retained + * in the private copies or restored target descriptors. Applications requiring + * CRT append semantics must not use Windows forwarding. + * * Returns false if a platform stream could not be redirected. Repeated calls are * idempotent. */ diff --git a/Core/Foundation/Source/StandardStreamLoggerLines.h b/Core/Foundation/Source/StandardStreamLoggerLines.h new file mode 100644 index 00000000..6b603485 --- /dev/null +++ b/Core/Foundation/Source/StandardStreamLoggerLines.h @@ -0,0 +1,131 @@ +#pragma once + +#include +#include +#include +#include + +namespace Babylon::StandardStreamLogger::Detail +{ + // Windows UTF-16 text descriptors still tee their original bytes; only the + // diagnostic copy is decoded. Retain incomplete code units/pairs between reads. + inline void AppendUtf16LE(std::string& output, std::string& pending, bool flush) + { + const auto codeUnit = [&pending](size_t offset) -> uint32_t { + return static_cast(pending[offset]) | + (static_cast(static_cast(pending[offset + 1])) << 8); + }; + size_t offset{}; + while (offset + 1 < pending.size()) + { + uint32_t value = codeUnit(offset); + size_t consumed{2}; + if (value >= 0xD800 && value <= 0xDBFF) + { + if (offset + 3 >= pending.size()) + { + if (!flush) + { + break; + } + value = 0xFFFD; + consumed = pending.size() - offset; + } + else if (codeUnit(offset + 2) >= 0xDC00 && codeUnit(offset + 2) <= 0xDFFF) + { + value = 0x10000 + ((value - 0xD800) << 10) + (codeUnit(offset + 2) - 0xDC00); + consumed = 4; + } + else + { + value = 0xFFFD; + } + } + else if (value >= 0xDC00 && value <= 0xDFFF) + { + value = 0xFFFD; + } + offset += consumed; + + if (value <= 0x7F) + { + output.push_back(static_cast(value)); + } + else if (value <= 0x7FF) + { + output.push_back(static_cast(0xC0 | (value >> 6))); + output.push_back(static_cast(0x80 | (value & 0x3F))); + } + else + { + if (value > 0xFFFF) + { + output.push_back(static_cast(0xF0 | (value >> 18))); + output.push_back(static_cast(0x80 | ((value >> 12) & 0x3F))); + } + else + { + output.push_back(static_cast(0xE0 | (value >> 12))); + } + output.push_back(static_cast(0x80 | ((value >> 6) & 0x3F))); + output.push_back(static_cast(0x80 | (value & 0x3F))); + } + } + if (flush && offset < pending.size()) + { + output.append("\xEF\xBF\xBD"); + offset = pending.size(); + } + pending.erase(0, offset); + } + + template + void EmitPendingLines(std::string& pending, size_t maxLineSize, bool flush, Emit&& emit) + { + assert(maxLineSize >= 4); + size_t start{}; + while (start < pending.size()) + { + const size_t newline = pending.find('\n', start); + const size_t end = newline == std::string::npos ? pending.size() : newline; + size_t size = end - start; + if (size != 0 && pending[end - 1] == '\r') + { + if (newline != std::string::npos || flush) + { + --size; + } + else if (size == maxLineSize + 1) + { + // The next read may complete CRLF after an exactly full line. + break; + } + } + if (size > maxLineSize) + { + size = maxLineSize; + while (size != 0 && (static_cast(pending[start + size]) & 0xC0) == 0x80) + { + --size; + } + // Standard streams can contain invalid UTF-8; still make progress. + if (size == 0) + { + size = maxLineSize; + } + emit(pending.substr(start, size)); + start += size; + } + else if (newline != std::string::npos || flush) + { + emit(pending.substr(start, size)); + start = end + (newline != std::string::npos ? 1 : 0); + } + else + { + break; + } + } + pending.erase(0, start); + } +} diff --git a/Core/Foundation/Source/StandardStreamLogger_Android.cpp b/Core/Foundation/Source/StandardStreamLogger_Android.cpp index ef1d0052..2ade6317 100644 --- a/Core/Foundation/Source/StandardStreamLogger_Android.cpp +++ b/Core/Foundation/Source/StandardStreamLogger_Android.cpp @@ -13,6 +13,11 @@ namespace // POSIX fd helpers (dup/pipe/CLOEXEC/devnull); sink is OsWritePlatform below. #include "StandardStreamLogger_PosixOps.inl" + size_t OsMaxPlatformLineSize(bool /*isError*/) + { + return 3800; + } + void OsWritePlatform(bool isError, const std::string& line) { const int priority = isError ? ANDROID_LOG_ERROR : ANDROID_LOG_INFO; diff --git a/Core/Foundation/Source/StandardStreamLogger_Apple.cpp b/Core/Foundation/Source/StandardStreamLogger_Apple.cpp index 5c514d88..0551b870 100644 --- a/Core/Foundation/Source/StandardStreamLogger_Apple.cpp +++ b/Core/Foundation/Source/StandardStreamLogger_Apple.cpp @@ -13,6 +13,12 @@ namespace // POSIX fd helpers (dup/pipe/CLOEXEC/devnull); sink is OsWritePlatform below. #include "StandardStreamLogger_PosixOps.inl" + size_t OsMaxPlatformLineSize(bool isError) + { + // Reserve the terminator within os_log's persisted dynamic-content budget. + return isError ? 255 : 1023; + } + void OsWritePlatform(bool isError, const std::string& line) { const os_log_type_t type = isError ? OS_LOG_TYPE_ERROR : OS_LOG_TYPE_DEFAULT; diff --git a/Core/Foundation/Source/StandardStreamLogger_PosixOps.inl b/Core/Foundation/Source/StandardStreamLogger_PosixOps.inl index ef215102..1281146e 100644 --- a/Core/Foundation/Source/StandardStreamLogger_PosixOps.inl +++ b/Core/Foundation/Source/StandardStreamLogger_PosixOps.inl @@ -3,14 +3,16 @@ struct ChannelPlatformState { + int OriginalDescriptorFlags{}; + bool OriginalDescriptorOpen{}; }; int OsDuplicate(int fd) { - return ::dup(fd); + return ::fcntl(fd, F_DUPFD_CLOEXEC, 0); } -int OsDuplicateTo(int source, int target) +int OsDuplicateTo(int source, int target, const ChannelPlatformState* /*state*/ = nullptr) { return ::dup2(source, target) < 0 ? -1 : 0; } @@ -30,8 +32,18 @@ int64_t OsWrite(int fd, const void* data, size_t size) return ::write(fd, data, size); } +void OsAppendPlatformBytes(ChannelPlatformState&, std::string& pending, const char* data, size_t size, bool) +{ + pending.append(data, size); +} + int OsCreatePipe(int fds[2]) { +#if defined(__ANDROID__) + return ::pipe2(fds, O_CLOEXEC); +#else + // The portable fallback requires callers to serialize Start() with + // fork/exec to avoid inheritance between pipe() and fcntl(). if (::pipe(fds) != 0) { return -1; @@ -48,11 +60,12 @@ int OsCreatePipe(int fds[2]) return -1; } return 0; +#endif } bool OsOccupyTarget(int target) { - const int nullFd = ::open("/dev/null", O_WRONLY); + const int nullFd = ::open("/dev/null", O_WRONLY | O_CLOEXEC); if (nullFd < 0) { return false; @@ -67,17 +80,20 @@ bool OsOccupyTarget(int target) return duplicated; } -bool OsOnStartChannel(ChannelPlatformState&, int, bool) +bool OsOnStartChannel(ChannelPlatformState& state, int target, bool) { - return true; + state.OriginalDescriptorFlags = ::fcntl(target, F_GETFD); + state.OriginalDescriptorOpen = state.OriginalDescriptorFlags >= 0; + return state.OriginalDescriptorOpen || errno == EBADF; } -bool OsOnRedirected(ChannelPlatformState&, int) +bool OsOnRedirected(ChannelPlatformState& state, int target) { - return true; + const int flags = state.OriginalDescriptorOpen ? state.OriginalDescriptorFlags : FD_CLOEXEC; + return ::fcntl(target, F_SETFD, flags) == 0; } -bool OsOnRestore(ChannelPlatformState&, int) +bool OsOnRestore(ChannelPlatformState& state, int target) { - return true; + return !state.OriginalDescriptorOpen || ::fcntl(target, F_SETFD, state.OriginalDescriptorFlags) == 0; } \ No newline at end of file diff --git a/Core/Foundation/Source/StandardStreamLogger_Shared.inl b/Core/Foundation/Source/StandardStreamLogger_Shared.inl index db765b1c..1958c3b0 100644 --- a/Core/Foundation/Source/StandardStreamLogger_Shared.inl +++ b/Core/Foundation/Source/StandardStreamLogger_Shared.inl @@ -3,17 +3,21 @@ // Platform TUs define these in the enclosing anonymous namespace, then include: // struct ChannelPlatformState { ... }; // int OsDuplicate(int fd); -// int OsDuplicateTo(int source, int target); +// int OsDuplicateTo(int source, int target, const ChannelPlatformState* state = nullptr); // int OsClose(int fd); // int64_t OsRead(int fd, void* data, size_t size); // int64_t OsWrite(int fd, const void* data, size_t size); // int OsCreatePipe(int fds[2]); // bool OsOccupyTarget(int target); +// size_t OsMaxPlatformLineSize(bool isError); // void OsWritePlatform(bool isError, const std::string& line); +// void OsAppendPlatformBytes(ChannelPlatformState& state, std::string& pending, const char* data, size_t size, bool flush); // bool OsOnStartChannel(ChannelPlatformState& state, int target, bool isError); // bool OsOnRedirected(ChannelPlatformState& state, int target); // bool OsOnRestore(ChannelPlatformState& state, int target); +#include "StandardStreamLoggerLines.h" + #include #include #include @@ -70,20 +74,12 @@ namespace return true; } - void EmitLine(Stream stream, std::string line) + void Drain(int readFd, int originalFd, Stream stream, ChannelPlatformState platform) { - if (!line.empty() && line.back() == '\r') - { - line.pop_back(); - } - OsWritePlatform(stream == Stream::Error, line); - } - - void Drain(int readFd, int originalFd, Stream stream) - { - // Cap mirrored lines below typical platform limits (~4 KiB for - // OutputDebugStringA / logcat / os_log). Leave headroom under 4096. - constexpr size_t MAX_PLATFORM_LINE_SIZE{3800}; + const size_t maxLineSize = OsMaxPlatformLineSize(stream == Stream::Error); + const auto emit = [stream](const std::string& line) { + OsWritePlatform(stream == Stream::Error, line); + }; std::array buffer{}; std::string pending{}; @@ -109,39 +105,13 @@ namespace (void)WriteAll(originalFd, buffer.data(), size); } - pending.append(buffer.data(), size); + OsAppendPlatformBytes(platform, pending, buffer.data(), size, false); - // Consume complete lines via a start index so we only memmove once - // per read batch instead of on every newline. - size_t start = 0; - for (;;) - { - const size_t newline = pending.find('\n', start); - if (newline != std::string::npos) - { - EmitLine(stream, pending.substr(start, newline - start)); - start = newline + 1; - } - else if (pending.size() - start >= MAX_PLATFORM_LINE_SIZE) - { - EmitLine(stream, pending.substr(start, MAX_PLATFORM_LINE_SIZE)); - start += MAX_PLATFORM_LINE_SIZE; - } - else - { - break; - } - } - if (start != 0) - { - pending.erase(0, start); - } + Babylon::StandardStreamLogger::Detail::EmitPendingLines(pending, maxLineSize, false, emit); } - if (!pending.empty()) - { - EmitLine(stream, std::move(pending)); - } + OsAppendPlatformBytes(platform, pending, buffer.data(), 0, true); + Babylon::StandardStreamLogger::Detail::EmitPendingLines(pending, maxLineSize, true, emit); (void)OsClose(readFd); if (originalFd >= 0) { @@ -153,7 +123,7 @@ namespace { if (channel.Original >= 0) { - (void)OsDuplicateTo(channel.Original, channel.Target); + (void)OsDuplicateTo(channel.Original, channel.Target, &channel.Platform); (void)OsClose(channel.Original); } else @@ -209,7 +179,7 @@ namespace return false; } - if (OsDuplicateTo(pipeFds[1], target) != 0) + if (OsDuplicateTo(pipeFds[1], target, &channel.Platform) != 0) { (void)OsClose(pipeFds[0]); (void)OsClose(pipeFds[1]); @@ -248,8 +218,8 @@ namespace try { channel.Reader = std::thread{ - [readFd = pipeFds[0], originalFd = readerOriginal, stream, completed = std::move(completed)]() mutable { - Drain(readFd, originalFd, stream); + [readFd = pipeFds[0], originalFd = readerOriginal, stream, platform = channel.Platform, completed = std::move(completed)]() mutable { + Drain(readFd, originalFd, stream, std::move(platform)); completed.set_value(); }}; } @@ -266,7 +236,7 @@ namespace bool restored{true}; if (channel.Original >= 0) { - restored = OsDuplicateTo(channel.Original, channel.Target) == 0; + restored = OsDuplicateTo(channel.Original, channel.Target, &channel.Platform) == 0; if (!restored) { (void)OsClose(channel.Target); diff --git a/Core/Foundation/Source/StandardStreamLogger_Windows.cpp b/Core/Foundation/Source/StandardStreamLogger_Windows.cpp index e2ff1559..0f97a37b 100644 --- a/Core/Foundation/Source/StandardStreamLogger_Windows.cpp +++ b/Core/Foundation/Source/StandardStreamLogger_Windows.cpp @@ -1,4 +1,5 @@ #include "StandardStreamLoggerPlatform.h" +#include "StandardStreamLoggerLines.h" #include #include @@ -11,6 +12,10 @@ namespace { + constexpr intptr_t NO_CONSOLE_FILENO{-2}; + // Some Windows SDKs hide HANDLE_FLAG_INHERIT from the app partition. + constexpr DWORD HANDLE_INHERIT_FLAG{0x00000001}; + void IgnoreInvalidParameter( const wchar_t*, const wchar_t*, @@ -25,20 +30,198 @@ namespace { DWORD StandardHandle{}; HANDLE OriginalHandle{INVALID_HANDLE_VALUE}; + DWORD OriginalDescriptorHandleFlags{}; + bool OriginalDescriptorOpen{}; bool OriginalHandleUsesTarget{}; + int OriginalMode{_O_BINARY}; + std::string PendingUtf16{}; }; - int OsDuplicate(int fd) + void SetErrnoFromWin32Error(DWORD error) + { + _doserrno = error; + switch (error) + { + case ERROR_INVALID_HANDLE: + errno = EBADF; + break; + case ERROR_TOO_MANY_OPEN_FILES: + errno = EMFILE; + break; + case ERROR_NOT_ENOUGH_MEMORY: + case ERROR_OUTOFMEMORY: + errno = ENOMEM; + break; + case ERROR_ACCESS_DENIED: + errno = EACCES; + break; + case ERROR_INVALID_PARAMETER: + errno = EINVAL; + break; + case ERROR_BROKEN_PIPE: + errno = EPIPE; + break; + default: + errno = EIO; + break; + } + } + + intptr_t GetOsHandle(int fd) { const auto previousHandler = ::_set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); - const int duplicated = ::_dup(fd); + const intptr_t handle = ::_get_osfhandle(fd); (void)::_set_thread_local_invalid_parameter_handler(previousHandler); + return handle; + } + + bool SetDescriptorInheritance(int fd, bool inherit) + { + const intptr_t handle = GetOsHandle(fd); + if (handle == -1) + { + return false; + } + if (!::SetHandleInformation( + reinterpret_cast(handle), + HANDLE_INHERIT_FLAG, + inherit ? HANDLE_INHERIT_FLAG : 0)) + { + SetErrnoFromWin32Error(::GetLastError()); + return false; + } + return true; + } + + int OsDuplicate(int fd) + { + const intptr_t sourceHandle = GetOsHandle(fd); + if (sourceHandle == -1) + { + return -1; + } + if (sourceHandle == NO_CONSOLE_FILENO) + { + errno = EBADF; + _doserrno = 0; + return -1; + } + + HANDLE handle{INVALID_HANDLE_VALUE}; + if (!::DuplicateHandle( + ::GetCurrentProcess(), reinterpret_cast(sourceHandle), + ::GetCurrentProcess(), &handle, 0, FALSE, DUPLICATE_SAME_ACCESS)) + { + SetErrnoFromWin32Error(::GetLastError()); + return -1; + } + + // Both the kernel handle and the CRT entry must be non-inheritable. + // Binary tees must not translate bytes that the target already translated. + // Other CRT flags, including _O_APPEND, cannot be copied through this API. + const int duplicated = ::_open_osfhandle(reinterpret_cast(handle), _O_BINARY | _O_NOINHERIT); + if (duplicated < 0) + { + const int error = errno; + const unsigned long dosError = _doserrno; + (void)::CloseHandle(handle); + errno = error; + _doserrno = dosError; + } return duplicated; } - int OsDuplicateTo(int source, int target) + int OsDuplicateTo(int source, int target, const ChannelPlatformState* state = nullptr) + { + if (state != nullptr && ::_setmode(source, state->OriginalMode) < 0) + { + return -1; + } + + const bool noinherit = + state != nullptr && + (!state->OriginalDescriptorOpen || + (state->OriginalDescriptorHandleFlags & HANDLE_INHERIT_FLAG) == 0); + if (!noinherit) + { + return ::_dup2(source, target); + } + + // _dup2 always clears the CRT's FNOINHERIT bit. Reopen the target + // through the CRT to restore it as well as the kernel handle flag. + const intptr_t sourceHandle = GetOsHandle(source); + if (sourceHandle == -1) + { + return -1; + } + HANDLE handle{INVALID_HANDLE_VALUE}; + if (!::DuplicateHandle( + ::GetCurrentProcess(), reinterpret_cast(sourceHandle), + ::GetCurrentProcess(), &handle, 0, FALSE, DUPLICATE_SAME_ACCESS)) + { + SetErrnoFromWin32Error(::GetLastError()); + return -1; + } + + const intptr_t targetHandle = GetOsHandle(target); + if (targetHandle == -1 && errno != EBADF) + { + (void)::CloseHandle(handle); + return -1; + } + // _close can report an invalid OS handle for UWP's -2 placeholder + // while still freeing its CRT descriptor slot. + if (targetHandle != -1 && ::_close(target) != 0 && + targetHandle != NO_CONSOLE_FILENO) + { + const int error = errno; + const unsigned long dosError = _doserrno; + (void)::CloseHandle(handle); + errno = error; + _doserrno = dosError; + return -1; + } + + const int reopened = ::_open_osfhandle( + reinterpret_cast(handle), _O_BINARY | _O_NOINHERIT); + if (reopened != target) + { + const int error = reopened < 0 ? errno : EBUSY; + const unsigned long dosError = reopened < 0 ? _doserrno : 0; + if (reopened < 0) + { + (void)::CloseHandle(handle); + } + else + { + (void)::_close(reopened); + } + errno = error; + _doserrno = dosError; + return -1; + } + + return ::_setmode(target, state->OriginalMode) < 0 ? -1 : 0; + } + + int GetDescriptorMode(int fd) { - return ::_dup2(source, target); + // Probe a temporary copy rather than changing a live standard stream. + // This short-lived _dup is covered by Start/Stop's child-creation exclusion. + const auto previousHandler = ::_set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); + const int probe = ::_dup(fd); + (void)::_set_thread_local_invalid_parameter_handler(previousHandler); + if (probe < 0) + { + return -1; + } + const int mode = ::_setmode(probe, _O_BINARY); + const int error = errno; + const unsigned long dosError = _doserrno; + (void)::_close(probe); + errno = error; + _doserrno = dosError; + return mode; } int OsClose(int fd) @@ -68,10 +251,11 @@ namespace HANDLE writeHandle{INVALID_HANDLE_VALUE}; if (!::CreatePipe(&readHandle, &writeHandle, &attributes, 4096)) { + SetErrnoFromWin32Error(::GetLastError()); return -1; } - fds[0] = ::_open_osfhandle(reinterpret_cast(readHandle), _O_BINARY); + fds[0] = ::_open_osfhandle(reinterpret_cast(readHandle), _O_BINARY | _O_NOINHERIT); if (fds[0] < 0) { (void)::CloseHandle(readHandle); @@ -79,7 +263,7 @@ namespace return -1; } - fds[1] = ::_open_osfhandle(reinterpret_cast(writeHandle), _O_BINARY); + fds[1] = ::_open_osfhandle(reinterpret_cast(writeHandle), _O_BINARY | _O_NOINHERIT); if (fds[1] < 0) { (void)::_close(fds[0]); @@ -94,8 +278,11 @@ namespace { // Prefer the secure CRT form; UWP treats the deprecated _open as an error. int nullFd{-1}; - if (::_sopen_s(&nullFd, "NUL", _O_WRONLY | _O_BINARY, _SH_DENYNO, 0) != 0) + const errno_t openError = + ::_sopen_s(&nullFd, "NUL", _O_WRONLY | _O_BINARY | _O_NOINHERIT, _SH_DENYNO, 0); + if (openError != 0) { + errno = openError; return false; } if (nullFd == target) @@ -103,11 +290,31 @@ namespace return true; } - const bool duplicated = OsDuplicateTo(nullFd, target) == 0; + ChannelPlatformState state{}; + const bool targetDuplicated = OsDuplicateTo(nullFd, target, &state) == 0; + const bool duplicated = + targetDuplicated && + SetDescriptorInheritance(target, false); + const int error = errno; + const unsigned long dosError = _doserrno; (void)OsClose(nullFd); + if (!duplicated) + { + if (targetDuplicated) + { + (void)OsClose(target); + } + errno = error; + _doserrno = dosError; + } return duplicated; } + size_t OsMaxPlatformLineSize(bool /*isError*/) + { + return 3800; + } + void OsWritePlatform(bool /*isError*/, const std::string& line) { std::string output{line}; @@ -115,12 +322,17 @@ namespace ::OutputDebugStringA(output.c_str()); } - intptr_t GetOsHandle(int fd) + void OsAppendPlatformBytes(ChannelPlatformState& state, std::string& pending, const char* data, size_t size, bool flush) { - const auto previousHandler = ::_set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); - const intptr_t handle = ::_get_osfhandle(fd); - (void)::_set_thread_local_invalid_parameter_handler(previousHandler); - return handle; + if (state.OriginalMode == _O_WTEXT || state.OriginalMode == _O_U16TEXT) + { + state.PendingUtf16.append(data, size); + Babylon::StandardStreamLogger::Detail::AppendUtf16LE(pending, state.PendingUtf16, flush); + } + else + { + pending.append(data, size); + } } bool OsOnStartChannel(ChannelPlatformState& state, int target, bool isError) @@ -128,8 +340,30 @@ namespace state.StandardHandle = isError ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE; state.OriginalHandle = ::GetStdHandle(state.StandardHandle); const intptr_t targetHandle = GetOsHandle(target); + if (targetHandle == -1) + { + return errno == EBADF; + } + if (targetHandle == NO_CONSOLE_FILENO) + { + return true; + } + + if (!::GetHandleInformation( + reinterpret_cast(targetHandle), + &state.OriginalDescriptorHandleFlags)) + { + SetErrnoFromWin32Error(::GetLastError()); + return false; + } + + state.OriginalMode = GetDescriptorMode(target); + if (state.OriginalMode < 0) + { + return false; + } + state.OriginalDescriptorOpen = true; state.OriginalHandleUsesTarget = - targetHandle != -1 && state.OriginalHandle != nullptr && state.OriginalHandle != INVALID_HANDLE_VALUE && state.OriginalHandle == reinterpret_cast(targetHandle); @@ -143,22 +377,48 @@ namespace { return false; } - return ::SetStdHandle(state.StandardHandle, reinterpret_cast(pipeHandle)) != FALSE; + + const bool inherit = + state.OriginalDescriptorOpen && + (state.OriginalDescriptorHandleFlags & HANDLE_INHERIT_FLAG) != 0; + if (!SetDescriptorInheritance(target, inherit)) + { + return false; + } + if (!::SetStdHandle(state.StandardHandle, reinterpret_cast(pipeHandle))) + { + SetErrnoFromWin32Error(::GetLastError()); + return false; + } + return true; } bool OsOnRestore(ChannelPlatformState& state, int target) { HANDLE handle = state.OriginalHandle; - if (state.OriginalHandleUsesTarget) + bool restored{true}; + if (state.OriginalDescriptorOpen) { const intptr_t restoredHandle = GetOsHandle(target); if (restoredHandle == -1) { return false; } - handle = reinterpret_cast(restoredHandle); + + const bool inherit = + (state.OriginalDescriptorHandleFlags & HANDLE_INHERIT_FLAG) != 0; + restored = SetDescriptorInheritance(target, inherit); + if (state.OriginalHandleUsesTarget) + { + handle = reinterpret_cast(restoredHandle); + } + } + if (!::SetStdHandle(state.StandardHandle, handle)) + { + SetErrnoFromWin32Error(::GetLastError()); + return false; } - return ::SetStdHandle(state.StandardHandle, handle) != FALSE; + return restored; } } diff --git a/Tests/UnitTests/CMakeLists.txt b/Tests/UnitTests/CMakeLists.txt index 69cc6cac..37773f59 100644 --- a/Tests/UnitTests/CMakeLists.txt +++ b/Tests/UnitTests/CMakeLists.txt @@ -30,6 +30,8 @@ set(SOURCES "Source/Tests.NodeApi.cpp" "Source/Tests.Scheduling.cpp" "Source/Tests.StandardStreamLogger.cpp" + "Source/StandardStreamLoggerLines.cpp" + "Source/StandardStreamLoggerPosix.cpp" "Source/Tests.TimeoutDispatcher.cpp") if(APPLE) @@ -58,6 +60,10 @@ if(APPLE) elseif(WIN32) set(SOURCES ${SOURCES} Source/Win32/App.cpp) + if(NOT WINDOWS_STORE) + set(SOURCES ${SOURCES} + "Source/StandardStreamLoggerWindows.cpp") + endif() elseif(UNIX AND NOT ANDROID) set(SOURCES ${SOURCES} Source/Linux/App.cpp) diff --git a/Tests/UnitTests/Source/Android/app/src/main/cpp/CMakeLists.txt b/Tests/UnitTests/Source/Android/app/src/main/cpp/CMakeLists.txt index aa933dba..251603a5 100644 --- a/Tests/UnitTests/Source/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Source/Android/app/src/main/cpp/CMakeLists.txt @@ -27,6 +27,8 @@ add_library(UnitTestsJNI SHARED ${UNIT_TESTS_DIR}/Source/Tests.NodeApi.cpp ${UNIT_TESTS_DIR}/Source/Tests.Scheduling.cpp ${UNIT_TESTS_DIR}/Source/Tests.StandardStreamLogger.cpp + ${UNIT_TESTS_DIR}/Source/StandardStreamLoggerLines.cpp + ${UNIT_TESTS_DIR}/Source/StandardStreamLoggerPosix.cpp ${UNIT_TESTS_DIR}/Source/Tests.TimeoutDispatcher.cpp) if(NAPI_JAVASCRIPT_ENGINE STREQUAL "V8") diff --git a/Tests/UnitTests/Source/StandardStreamLoggerLines.cpp b/Tests/UnitTests/Source/StandardStreamLoggerLines.cpp new file mode 100644 index 00000000..37fe93fc --- /dev/null +++ b/Tests/UnitTests/Source/StandardStreamLoggerLines.cpp @@ -0,0 +1,244 @@ +#include "../../../Core/Foundation/Source/StandardStreamLoggerLines.h" +#include + +#include +#include +#include +#include + +namespace +{ + class LineCapture + { + public: + explicit LineCapture(size_t limit) + : m_limit{limit} + { + } + + void Write(const std::string& data, bool flush = false) + { + m_pending += data; + Babylon::StandardStreamLogger::Detail::EmitPendingLines( + m_pending, m_limit, flush, [this](std::string line) { + EXPECT_LE(line.size(), m_limit); + Lines.push_back(std::move(line)); + }); + } + + std::string Join() const + { + std::string result; + for (const auto& line : Lines) + { + result += line; + } + return result; + } + + std::vector Lines; + + private: + size_t m_limit; + std::string m_pending; + }; + + constexpr std::array Limits{255, 1023, 3800}; + + std::string Utf16Bytes(const std::u16string& text) + { + std::string bytes; + for (const auto unit : text) + { + bytes.push_back(static_cast(unit & 0xFF)); + bytes.push_back(static_cast(unit >> 8)); + } + return bytes; + } +} + +TEST(StandardStreamLoggerLines, DecodesUtf16AcrossEveryReadBoundary) +{ + const auto bytes = Utf16Bytes(u"A\u00A9\u2603\U0001F600\r\n"); + const std::string expected = "A\xC2\xA9\xE2\x98\x83\xF0\x9F\x98\x80\r\n"; + for (size_t chunk = 1; chunk <= bytes.size(); ++chunk) + { + SCOPED_TRACE(chunk); + std::string pending; + std::string decoded; + for (size_t offset = 0; offset < bytes.size(); offset += chunk) + { + pending += bytes.substr(offset, chunk); + Babylon::StandardStreamLogger::Detail::AppendUtf16LE(decoded, pending, false); + EXPECT_LE(pending.size(), 3u); + } + Babylon::StandardStreamLogger::Detail::AppendUtf16LE(decoded, pending, true); + EXPECT_EQ(decoded, expected); + EXPECT_TRUE(pending.empty()); + } +} + +TEST(StandardStreamLoggerLines, BoundsDecodedUtf16ByUtf8Bytes) +{ + for (const auto limit : Limits) + { + LineCapture capture{limit}; + const auto bytes = Utf16Bytes(std::u16string(limit - 1, u'x') + u"\U0001F600\u2603\r\n"); + std::string pending; + for (size_t offset = 0; offset < bytes.size(); offset += 7) + { + pending += bytes.substr(offset, 7); + std::string decoded; + Babylon::StandardStreamLogger::Detail::AppendUtf16LE(decoded, pending, false); + capture.Write(decoded); + } + std::string decoded; + Babylon::StandardStreamLogger::Detail::AppendUtf16LE(decoded, pending, true); + capture.Write(decoded, true); + ASSERT_EQ(capture.Lines.size(), 2u); + EXPECT_EQ(capture.Lines[0], std::string(limit - 1, 'x')); + EXPECT_EQ(capture.Lines[1], "\xF0\x9F\x98\x80\xE2\x98\x83"); + } +} + +TEST(StandardStreamLoggerLines, ReplacesMalformedAndTruncatedUtf16OnFlush) +{ + const std::vector> cases{ + {std::string{"\x00", 1}, "\xEF\xBF\xBD"}, + {std::string{"\x00\xD8", 2}, "\xEF\xBF\xBD"}, + {std::string{"\x00\xD8\x00", 3}, "\xEF\xBF\xBD"}, + {std::string{"\x00\xDC", 2}, "\xEF\xBF\xBD"}, + {std::string{"\x00\xD8\x41\x00", 4}, "\xEF\xBF\xBD" + "A"}, + }; + for (const auto& [bytes, expected] : cases) + { + std::string pending; + std::string decoded; + for (const auto byte : bytes) + { + pending.push_back(byte); + Babylon::StandardStreamLogger::Detail::AppendUtf16LE(decoded, pending, false); + } + Babylon::StandardStreamLogger::Detail::AppendUtf16LE(decoded, pending, true); + EXPECT_EQ(decoded, expected); + EXPECT_TRUE(pending.empty()); + } +} + +TEST(StandardStreamLoggerLines, CompleteLineAcrossReadsIsBounded) +{ + LineCapture capture{3800}; + const std::string input = std::string(4095, 'x') + '\n'; + for (size_t offset = 0; offset < input.size(); offset += 1024) + { + capture.Write(input.substr(offset, 1024)); + } + ASSERT_EQ(capture.Lines.size(), 2); + EXPECT_EQ(capture.Lines[0].size(), 3800); + EXPECT_EQ(capture.Lines[1].size(), 295); + EXPECT_EQ(capture.Join(), std::string(4095, 'x')); +} + +TEST(StandardStreamLoggerLines, BoundsCompleteAndUnterminatedLinesForEverySink) +{ + for (const auto limit : Limits) + { + for (const size_t size : {limit - 1, limit, limit + 1, 2 * limit, 4 * limit + 3}) + { + for (const bool newline : {false, true}) + { + SCOPED_TRACE(::testing::Message() << limit << ", " << size << ", " << newline); + LineCapture capture{limit}; + const std::string content(size, 'x'); + const std::string input = content + (newline ? "\n" : ""); + for (size_t offset = 0; offset < input.size(); offset += 1024) + { + capture.Write(input.substr(offset, 1024)); + } + capture.Write({}, true); + EXPECT_EQ(capture.Join(), content); + EXPECT_EQ(capture.Lines.size(), (size + limit - 1) / limit); + for (const auto& line : capture.Lines) + { + EXPECT_FALSE(line.empty()); + } + } + } + } +} + +TEST(StandardStreamLoggerLines, ExactLimitDoesNotCreateAnExtraEmptyLine) +{ + for (const auto limit : Limits) + { + for (const auto* ending : {"\n", "\r\n"}) + { + LineCapture capture{limit}; + capture.Write(std::string(limit, 'x')); + for (const char ch : std::string{ending}) + { + capture.Write(std::string(1, ch)); + } + capture.Write({}, true); + ASSERT_EQ(capture.Lines.size(), 1); + EXPECT_EQ(capture.Lines[0], std::string(limit, 'x')); + } + LineCapture capture{limit}; + capture.Write(std::string(limit, 'x') + "\r\n"); + ASSERT_EQ(capture.Lines.size(), 1); + EXPECT_EQ(capture.Lines[0], std::string(limit, 'x')); + } +} + +TEST(StandardStreamLoggerLines, PreservesBlankLinesAndNormalizesLineEndings) +{ + LineCapture capture{255}; + capture.Write("\nfirst\r"); + capture.Write("\n\nsecond\ntail\r", true); + EXPECT_EQ(capture.Lines, (std::vector{"", "first", "", "second", "tail"})); +} + +TEST(StandardStreamLoggerLines, DoesNotDropCarriageReturnAtChunkBoundary) +{ + LineCapture capture{255}; + const std::string input = std::string(254, 'x') + "\rY"; + capture.Write(input + "\n"); + EXPECT_EQ(capture.Join(), input); +} + +TEST(StandardStreamLoggerLines, PreservesUtf8AcrossChunkAndReadBoundaries) +{ + for (const auto limit : Limits) + { + for (const auto* sequence : {"\xC2\xA9", "\xE2\x98\x83", "\xF0\x9F\x98\x80"}) + { + LineCapture capture{limit}; + const std::string content = std::string(limit - 1, 'x') + sequence + "tail"; + for (size_t offset = 0; offset < content.size(); offset += limit) + { + capture.Write(content.substr(offset, limit)); + } + capture.Write("\n"); + ASSERT_EQ(capture.Lines.size(), 2); + EXPECT_EQ(capture.Lines[0], std::string(limit - 1, 'x')); + EXPECT_EQ(capture.Lines[1], std::string{sequence} + "tail"); + EXPECT_EQ(capture.Join(), content); + } + } +} + +TEST(StandardStreamLoggerLines, InvalidUtf8StillMakesProgress) +{ + LineCapture capture{255}; + const std::string content(1024, '\x80'); + capture.Write(content + "\n"); + EXPECT_EQ(capture.Join(), content); +} + +TEST(StandardStreamLoggerLines, EmptyStreamProducesNoLines) +{ + LineCapture capture{255}; + capture.Write({}, true); + EXPECT_TRUE(capture.Lines.empty()); +} diff --git a/Tests/UnitTests/Source/StandardStreamLoggerPosix.cpp b/Tests/UnitTests/Source/StandardStreamLoggerPosix.cpp new file mode 100644 index 00000000..211c78a7 --- /dev/null +++ b/Tests/UnitTests/Source/StandardStreamLoggerPosix.cpp @@ -0,0 +1,100 @@ +#if !defined(_WIN32) + +#include + +#include +#include +#include +#include + +namespace +{ +#include "../../../Core/Foundation/Source/StandardStreamLogger_PosixOps.inl" + + struct Descriptor + { + int Value; + + ~Descriptor() + { + if (Value >= 0) + { + (void)OsClose(Value); + } + } + }; +} + +TEST(StandardStreamLoggerPosix, PrivateDuplicatesAndPipesAreCloseOnExec) +{ + Descriptor original{::open("/dev/null", O_WRONLY)}; + ASSERT_GE(original.Value, 0); + Descriptor copy{OsDuplicate(original.Value)}; + ASSERT_GE(copy.Value, 0); + EXPECT_EQ(::fcntl(copy.Value, F_GETFD), FD_CLOEXEC); + + int pipeFds[2]; + ASSERT_EQ(OsCreatePipe(pipeFds), 0); + Descriptor read{pipeFds[0]}; + Descriptor write{pipeFds[1]}; + EXPECT_EQ(::fcntl(read.Value, F_GETFD), FD_CLOEXEC); + EXPECT_EQ(::fcntl(write.Value, F_GETFD), FD_CLOEXEC); + ASSERT_EQ(OsWrite(write.Value, "x", 1), 1); + char value{}; + EXPECT_EQ(OsRead(read.Value, &value, 1), 1); + EXPECT_EQ(value, 'x'); +} + +TEST(StandardStreamLoggerPosix, PreservesInheritanceOnRedirectAndRestore) +{ + for (const int flags : {0, FD_CLOEXEC}) + { + Descriptor target{::open("/dev/null", O_WRONLY)}; + ASSERT_GE(target.Value, 0); + ASSERT_EQ(::fcntl(target.Value, F_SETFD, flags), 0); + ChannelPlatformState state{}; + ASSERT_TRUE(OsOnStartChannel(state, target.Value, false)); + Descriptor original{OsDuplicate(target.Value)}; + ASSERT_GE(original.Value, 0); + + int pipeFds[2]; + ASSERT_EQ(OsCreatePipe(pipeFds), 0); + Descriptor read{pipeFds[0]}; + Descriptor write{pipeFds[1]}; + ASSERT_EQ(OsDuplicateTo(write.Value, target.Value), 0); + ASSERT_TRUE(OsOnRedirected(state, target.Value)); + EXPECT_EQ(::fcntl(target.Value, F_GETFD), flags); + ASSERT_EQ(OsDuplicateTo(original.Value, target.Value), 0); + ASSERT_TRUE(OsOnRestore(state, target.Value)); + EXPECT_EQ(::fcntl(target.Value, F_GETFD), flags); + } +} + +TEST(StandardStreamLoggerPosix, SupportsAnInitiallyClosedTarget) +{ + Descriptor target{::open("/dev/null", O_WRONLY)}; + ASSERT_GE(target.Value, 0); + const int targetFd = target.Value; + ASSERT_EQ(OsClose(targetFd), 0); + target.Value = -1; + ChannelPlatformState state{}; + ASSERT_TRUE(OsOnStartChannel(state, targetFd, false)); + ASSERT_TRUE(OsOccupyTarget(targetFd)); + target.Value = targetFd; + ASSERT_TRUE(OsOnRedirected(state, target.Value)); + EXPECT_NE(::fcntl(target.Value, F_GETFD) & FD_CLOEXEC, 0); + ASSERT_EQ(OsClose(target.Value), 0); + target.Value = -1; + ASSERT_TRUE(OsOnRestore(state, targetFd)); + EXPECT_EQ(::fcntl(targetFd, F_GETFD), -1); + EXPECT_EQ(errno, EBADF); +} + +TEST(StandardStreamLoggerPosix, InvalidDuplicateReportsBadDescriptor) +{ + errno = 0; + EXPECT_EQ(OsDuplicate(-1), -1); + EXPECT_EQ(errno, EBADF); +} + +#endif diff --git a/Tests/UnitTests/Source/StandardStreamLoggerWindows.cpp b/Tests/UnitTests/Source/StandardStreamLoggerWindows.cpp new file mode 100644 index 00000000..c430146a --- /dev/null +++ b/Tests/UnitTests/Source/StandardStreamLoggerWindows.cpp @@ -0,0 +1,686 @@ +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) +namespace +{ + constexpr DWORD InheritHandleFlag{0x00000001}; + constexpr size_t DescriptorReservationCount{32}; + constexpr char SpawnProbeSwitch[]{"--standard-stream-logger-spawn-probe"}; + + void IgnoreInvalidParameter( + const wchar_t*, + const wchar_t*, + const wchar_t*, + unsigned int, + uintptr_t) + { + } + + intptr_t GetOsHandle(int fd) + { + const auto previousHandler = ::_set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); + const intptr_t handle = ::_get_osfhandle(fd); + (void)::_set_thread_local_invalid_parameter_handler(previousHandler); + return handle; + } + + int DescriptorInheritance(int fd) + { + DWORD flags{}; + const intptr_t handle = GetOsHandle(fd); + return handle != -1 && ::GetHandleInformation(reinterpret_cast(handle), &flags) + ? (flags & InheritHandleFlag) != 0 + : -1; + } + + bool SetDescriptorInheritance(int fd, bool inherit) + { + const intptr_t handle = GetOsHandle(fd); + return handle != -1 && ::SetHandleInformation( + reinterpret_cast(handle), InheritHandleFlag, inherit ? InheritHandleFlag : 0); + } + + bool BindNonInheritableDescriptor(int source, int target, int mode) + { + const intptr_t sourceHandle = GetOsHandle(source); + if (sourceHandle == -1) + { + return false; + } + HANDLE handle{INVALID_HANDLE_VALUE}; + if (!::DuplicateHandle( + ::GetCurrentProcess(), reinterpret_cast(sourceHandle), + ::GetCurrentProcess(), &handle, 0, FALSE, DUPLICATE_SAME_ACCESS)) + { + return false; + } + if (::_close(target) != 0) + { + (void)::CloseHandle(handle); + return false; + } + const int reopened = + ::_open_osfhandle(reinterpret_cast(handle), _O_BINARY | _O_NOINHERIT); + if (reopened != target) + { + if (reopened < 0) + { + (void)::CloseHandle(handle); + } + else + { + (void)::_close(reopened); + } + return false; + } + return ::_setmode(target, mode) >= 0; + } + + FILE* FileForDescriptor(int fd) + { + return fd == 1 ? stdout : stderr; + } + + DWORD StandardHandleForDescriptor(int fd) + { + return fd == 1 ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE; + } + + class StreamCapture + { + public: + explicit StreamCapture(int target, bool noinherit = false) + : m_target{target} + , m_standardHandleId{StandardHandleForDescriptor(target)} + { + std::fflush(FileForDescriptor(m_target)); + m_originalInheritance = DescriptorInheritance(m_target); + m_original = ::_dup(m_target); + m_originalStdHandle = ::GetStdHandle(m_standardHandleId); + const intptr_t originalHandle = GetOsHandle(m_target); + m_originalStdHandleUsesTarget = + originalHandle != -1 && + m_originalStdHandle != nullptr && + m_originalStdHandle != INVALID_HANDLE_VALUE && + m_originalStdHandle == reinterpret_cast(originalHandle); + + m_file = std::tmpfile(); + if (m_original < 0 || m_file == nullptr) + { + return; + } + + m_fileDescriptor = ::_fileno(m_file); + if (m_fileDescriptor < 0 || ::_setmode(m_fileDescriptor, _O_BINARY) < 0 || + (noinherit + ? !BindNonInheritableDescriptor(m_fileDescriptor, m_target, _O_BINARY) + : ::_dup2(m_fileDescriptor, m_target) != 0)) + { + return; + } + + if ((m_originalInheritance >= 0 && + !SetDescriptorInheritance(m_target, !noinherit && m_originalInheritance != 0)) || + !::SetStdHandle( + m_standardHandleId, + reinterpret_cast(GetOsHandle(m_target)))) + { + return; + } + + m_valid = true; + } + + ~StreamCapture() + { + RestoreTarget(); + CloseFile(); + } + + bool Valid() const + { + return m_valid; + } + + bool ReadAndRestore(std::string& output) + { + std::fflush(FileForDescriptor(m_target)); + const bool restored = RestoreTarget(); + + bool read{m_fileDescriptor >= 0 && ::_setmode(m_fileDescriptor, _O_BINARY) >= 0}; + if (read) + { + read = ::_lseeki64(m_fileDescriptor, 0, SEEK_SET) == 0; + } + + std::array buffer{}; + while (read) + { + const int count = ::_read(m_fileDescriptor, buffer.data(), static_cast(buffer.size())); + if (count < 0) + { + read = false; + break; + } + if (count == 0) + { + break; + } + output.append(buffer.data(), static_cast(count)); + } + + CloseFile(); + return restored && read; + } + + private: + bool RestoreTarget() + { + bool restored{true}; + if (m_original >= 0) + { + std::fflush(FileForDescriptor(m_target)); + bool descriptorRestored{}; + if (m_originalInheritance == 0) + { + const int originalMode = ::_setmode(m_original, _O_BINARY); + if (originalMode >= 0) + { + descriptorRestored = + BindNonInheritableDescriptor(m_original, m_target, originalMode); + } + } + else + { + descriptorRestored = ::_dup2(m_original, m_target) == 0; + } + restored = descriptorRestored; + if (descriptorRestored && m_originalInheritance >= 0) + { + restored = + SetDescriptorInheritance(m_target, m_originalInheritance != 0) && + restored; + } + + (void)::_close(m_original); + m_original = -1; + + if (descriptorRestored) + { + HANDLE handle = m_originalStdHandle; + if (m_originalStdHandleUsesTarget) + { + const intptr_t restoredHandle = GetOsHandle(m_target); + handle = restoredHandle == -1 + ? INVALID_HANDLE_VALUE + : reinterpret_cast(restoredHandle); + } + restored = + ::SetStdHandle(m_standardHandleId, handle) != FALSE && + restored; + } + } + m_valid = false; + return restored; + } + + void CloseFile() + { + if (m_file != nullptr) + { + std::fclose(m_file); + m_file = nullptr; + m_fileDescriptor = -1; + } + } + + int m_target; + DWORD m_standardHandleId; + FILE* m_file{}; + int m_fileDescriptor{-1}; + int m_original{-1}; + int m_originalInheritance{-1}; + HANDLE m_originalStdHandle{INVALID_HANDLE_VALUE}; + bool m_originalStdHandleUsesTarget{}; + bool m_valid{}; + }; + + struct ModeCase + { + const char* Name; + int Mode; + int ObservableMode; + bool UnicodeInput; + }; + + constexpr std::array ModeCases{{ + {"binary", _O_BINARY, _O_BINARY, false}, + {"text", _O_TEXT, _O_TEXT, false}, + {"utf8", _O_U8TEXT, _O_U8TEXT, true}, + {"utf16", _O_U16TEXT, _O_WTEXT, true}, + {"wtext", _O_WTEXT, _O_WTEXT, true}, + }}; + + bool WriteStage(int target, const ModeCase& mode, wchar_t stage) + { + if (!mode.UnicodeInput) + { + const std::array input{{static_cast(stage), '\n'}}; + return ::_write(target, input.data(), static_cast(input.size())) == + static_cast(input.size()); + } + + const std::array input{{stage, L'\x2603', L'\n'}}; + const auto size = static_cast(input.size() * sizeof(wchar_t)); + return ::_write(target, input.data(), size) == static_cast(size); + } + + void AppendUtf16(std::string& output, wchar_t value) + { + output.append(reinterpret_cast(&value), sizeof(value)); + } + + void AppendExpectedStage(std::string& output, const ModeCase& mode, wchar_t stage) + { + if (mode.Mode == _O_BINARY) + { + output.push_back(static_cast(stage)); + output.push_back('\n'); + return; + } + if (mode.Mode == _O_TEXT) + { + output.push_back(static_cast(stage)); + output.append("\r\n"); + return; + } + if (mode.Mode == _O_U8TEXT) + { + output.push_back(static_cast(stage)); + output.append("\xE2\x98\x83\r\n"); + return; + } + + AppendUtf16(output, stage); + AppendUtf16(output, L'\x2603'); + AppendUtf16(output, L'\r'); + AppendUtf16(output, L'\n'); + } + + struct ModeResult + { + bool WasAlreadyStarted{}; + bool CaptureValid{}; + bool ModeSet{}; + bool BeforeWritten{}; + bool Started{}; + int ActiveMode{-1}; + bool ActiveModeRestored{}; + bool DuringWritten{}; + bool Stopped{}; + int RestoredMode{-1}; + bool RestoredModeRestored{}; + bool AfterWritten{}; + bool CaptureRead{}; + std::string Captured{}; + }; + + ModeResult RunModeCase(int target, const ModeCase& mode) + { + ModeResult result{}; + result.WasAlreadyStarted = Babylon::StandardStreamLogger::IsStarted(); + if (result.WasAlreadyStarted) + { + return result; + } + + StreamCapture capture{target}; + result.CaptureValid = capture.Valid(); + if (!result.CaptureValid) + { + return result; + } + + result.ModeSet = ::_setmode(target, mode.Mode) >= 0; + if (result.ModeSet) + { + result.BeforeWritten = WriteStage(target, mode, L'B'); + } + + result.Started = Babylon::StandardStreamLogger::Start(); + if (result.Started) + { + result.ActiveMode = ::_setmode(target, mode.Mode); + if (result.ActiveMode >= 0) + { + result.ActiveModeRestored = ::_setmode(target, result.ActiveMode) >= 0; + } + result.DuringWritten = WriteStage(target, mode, L'D'); + } + + result.Stopped = Babylon::StandardStreamLogger::Stop(); + if (result.Started && result.Stopped) + { + result.RestoredMode = ::_setmode(target, mode.Mode); + if (result.RestoredMode >= 0) + { + result.RestoredModeRestored = ::_setmode(target, result.RestoredMode) >= 0; + } + result.AfterWritten = WriteStage(target, mode, L'A'); + } + + result.CaptureRead = capture.ReadAndRestore(result.Captured); + return result; + } + + std::vector AllocateDescriptorReservations(size_t count) + { + std::vector result{}; + result.reserve(count); + for (size_t index = 0; index < count; ++index) + { + const int fd = ::_dup(1); + if (fd < 0) + { + break; + } + result.push_back(fd); + } + return result; + } + + void CloseDescriptors(const std::vector& descriptors) + { + for (const int fd : descriptors) + { + (void)::_close(fd); + } + } + + std::vector FindOccupiedReservations( + const std::vector& reservations, + const std::vector& availableAfterStart) + { + std::vector result{}; + for (const int fd : reservations) + { + if (std::find(availableAfterStart.begin(), availableAfterStart.end(), fd) == + availableAfterStart.end()) + { + result.push_back(fd); + } + } + return result; + } + + struct SpawnResult + { + bool Started{}; + bool ExecutableFound{}; + bool Spawned{}; + bool Waited{}; + DWORD ExitCode{static_cast(-1)}; + bool Stopped{}; + bool AfterStopSpawned{}; + bool AfterStopWaited{}; + DWORD AfterStopExitCode{static_cast(-1)}; + std::vector PrivateDescriptors{}; + }; + + struct SpawnOutcome + { + bool Spawned{}; + bool Waited{}; + DWORD ExitCode{static_cast(-1)}; + }; + + SpawnOutcome ProbeSpawn(const std::wstring& executable, const std::vector& descriptors) + { + SpawnOutcome result{}; + std::vector argumentStorage{}; + argumentStorage.reserve(descriptors.size() + 2); + argumentStorage.push_back(executable); + argumentStorage.emplace_back(L"--standard-stream-logger-spawn-probe"); + for (const int fd : descriptors) + { + argumentStorage.push_back(std::to_wstring(fd)); + } + + std::vector arguments{}; + arguments.reserve(argumentStorage.size() + 1); + for (const auto& argument : argumentStorage) + { + arguments.push_back(argument.c_str()); + } + arguments.push_back(nullptr); + + const intptr_t child = ::_wspawnv( + _P_NOWAIT, argumentStorage.front().c_str(), arguments.data()); + result.Spawned = child != -1; + if (result.Spawned) + { + const HANDLE process = reinterpret_cast(child); + const DWORD wait = ::WaitForSingleObject(process, 10000); + result.Waited = wait == WAIT_OBJECT_0; + if (!result.Waited && wait == WAIT_TIMEOUT) + { + (void)::TerminateProcess(process, 0xFE); + (void)::WaitForSingleObject(process, 10000); + } + if (result.Waited) + { + (void)::GetExitCodeProcess(process, &result.ExitCode); + } + (void)::CloseHandle(process); + } + return result; + } + + SpawnResult RunSpawnCase(bool checkStandardDescriptors = false) + { + SpawnResult result{}; + if (Babylon::StandardStreamLogger::IsStarted()) + { + return result; + } + + const std::vector reservations = + AllocateDescriptorReservations(DescriptorReservationCount); + CloseDescriptors(reservations); + + std::vector executableBuffer(32768); + DWORD executableLength{}; + result.Started = Babylon::StandardStreamLogger::Start(); + if (result.Started) + { + const std::vector availableAfterStart = + AllocateDescriptorReservations(reservations.size()); + result.PrivateDescriptors = + FindOccupiedReservations(reservations, availableAfterStart); + CloseDescriptors(availableAfterStart); + + executableLength = ::GetModuleFileNameW( + nullptr, executableBuffer.data(), static_cast(executableBuffer.size())); + result.ExecutableFound = + executableLength != 0 && executableLength < executableBuffer.size(); + + if (result.ExecutableFound && !result.PrivateDescriptors.empty()) + { + std::vector descriptors = result.PrivateDescriptors; + if (checkStandardDescriptors) + { + descriptors.push_back(1); + descriptors.push_back(2); + } + const SpawnOutcome outcome = ProbeSpawn( + std::wstring{executableBuffer.data(), executableLength}, descriptors); + result.Spawned = outcome.Spawned; + result.Waited = outcome.Waited; + result.ExitCode = outcome.ExitCode; + } + } + + result.Stopped = Babylon::StandardStreamLogger::Stop(); + if (checkStandardDescriptors && result.Stopped && result.ExecutableFound) + { + const SpawnOutcome outcome = ProbeSpawn( + std::wstring{executableBuffer.data(), executableLength}, {1, 2}); + result.AfterStopSpawned = outcome.Spawned; + result.AfterStopWaited = outcome.Waited; + result.AfterStopExitCode = outcome.ExitCode; + } + return result; + } +} + +int RunStandardStreamLoggerSpawnProbe(int argc, char** argv) +{ + if (argc < 3 || std::strcmp(argv[1], SpawnProbeSwitch) != 0) + { + return 2; + } + + STARTUPINFOA startupInfo{}; + ::GetStartupInfoA(&startupInfo); + if (startupInfo.lpReserved2 == nullptr || startupInfo.cbReserved2 < sizeof(int)) + { + return 3; + } + + int handleCount{}; + std::memcpy(&handleCount, startupInfo.lpReserved2, sizeof(handleCount)); + if (handleCount < 0) + { + return 4; + } + + const size_t requiredSize = + sizeof(handleCount) + + static_cast(handleCount) * (sizeof(unsigned char) + sizeof(intptr_t)); + if (requiredSize > startupInfo.cbReserved2) + { + return 5; + } + + const auto* firstFlag = + reinterpret_cast(startupInfo.lpReserved2) + sizeof(handleCount); + const auto* firstHandle = firstFlag + handleCount; + for (int argument = 2; argument < argc; ++argument) + { + char* end{}; + errno = 0; + const long parsed = std::strtol(argv[argument], &end, 10); + if (errno != 0 || end == argv[argument] || *end != '\0' || parsed < 0 || parsed > INT_MAX) + { + return 6; + } + + const int fd = static_cast(parsed); + if (fd >= handleCount) + { + continue; + } + + intptr_t inheritedHandle{}; + std::memcpy( + &inheritedHandle, + firstHandle + static_cast(fd) * sizeof(inheritedHandle), + sizeof(inheritedHandle)); + if (firstFlag[fd] != 0 || + inheritedHandle != reinterpret_cast(INVALID_HANDLE_VALUE)) + { + return 1; + } + } + + return 0; +} + +TEST(StandardStreamLoggerWindows, PreservesTargetModesAndOriginalBytes) +{ + for (const int target : {1, 2}) + { + for (const ModeCase& mode : ModeCases) + { + const ModeResult result = RunModeCase(target, mode); + std::string expected{}; + AppendExpectedStage(expected, mode, L'B'); + AppendExpectedStage(expected, mode, L'D'); + AppendExpectedStage(expected, mode, L'A'); + + SCOPED_TRACE( + std::string{target == 1 ? "stdout " : "stderr "} + mode.Name); + EXPECT_FALSE(result.WasAlreadyStarted); + EXPECT_TRUE(result.CaptureValid); + EXPECT_TRUE(result.ModeSet); + EXPECT_TRUE(result.BeforeWritten); + EXPECT_TRUE(result.Started); + EXPECT_EQ(result.ActiveMode, mode.ObservableMode); + EXPECT_TRUE(result.ActiveModeRestored); + EXPECT_TRUE(result.DuringWritten); + EXPECT_TRUE(result.Stopped); + EXPECT_EQ(result.RestoredMode, mode.ObservableMode); + EXPECT_TRUE(result.RestoredModeRestored); + EXPECT_TRUE(result.AfterWritten); + EXPECT_TRUE(result.CaptureRead); + EXPECT_EQ(result.Captured, expected); + } + } +} + +TEST(StandardStreamLoggerWindows, PrivateDescriptorsAreNotSerializedBySpawn) +{ + const SpawnResult result = RunSpawnCase(); + + EXPECT_TRUE(result.Started); + EXPECT_FALSE(result.PrivateDescriptors.empty()); + EXPECT_TRUE(result.ExecutableFound); + EXPECT_TRUE(result.Spawned); + EXPECT_TRUE(result.Waited); + EXPECT_EQ(result.ExitCode, 0u); + EXPECT_TRUE(result.Stopped); +} + +TEST(StandardStreamLoggerWindows, NonInheritableStandardDescriptorsAreNotSerializedBySpawn) +{ + bool capturesValid{}; + SpawnResult result{}; + { + StreamCapture output{1, true}; + StreamCapture error{2, true}; + capturesValid = output.Valid() && error.Valid(); + if (capturesValid) + { + result = RunSpawnCase(true); + } + } + ASSERT_TRUE(capturesValid); + EXPECT_TRUE(result.Started); + EXPECT_FALSE(result.PrivateDescriptors.empty()); + EXPECT_TRUE(result.ExecutableFound); + EXPECT_TRUE(result.Spawned); + EXPECT_TRUE(result.Waited); + EXPECT_EQ(result.ExitCode, 0u); + EXPECT_TRUE(result.Stopped); + EXPECT_TRUE(result.AfterStopSpawned); + EXPECT_TRUE(result.AfterStopWaited); + EXPECT_EQ(result.AfterStopExitCode, 0u); +} +#endif diff --git a/Tests/UnitTests/Source/Tests.StandardStreamLogger.cpp b/Tests/UnitTests/Source/Tests.StandardStreamLogger.cpp index 7f0c71e6..e4c42523 100644 --- a/Tests/UnitTests/Source/Tests.StandardStreamLogger.cpp +++ b/Tests/UnitTests/Source/Tests.StandardStreamLogger.cpp @@ -9,12 +9,16 @@ #include #include #else +#include #include #endif namespace { #if defined(_WIN32) + // HANDLE_FLAG_INHERIT's documented value; the SDK hides the macro from UWP. + constexpr DWORD InheritHandleFlag{0x00000001}; + int DuplicateFileDescriptor(int fd) { return ::_dup(fd); @@ -34,6 +38,22 @@ namespace { return ::_fileno(file); } + + int DescriptorInheritance(int fd) + { + DWORD flags{}; + const intptr_t handle = ::_get_osfhandle(fd); + return handle != -1 && ::GetHandleInformation(reinterpret_cast(handle), &flags) + ? (flags & InheritHandleFlag) != 0 + : -1; + } + + bool SetDescriptorInheritance(int fd, bool inherit) + { + const intptr_t handle = ::_get_osfhandle(fd); + return handle != -1 && ::SetHandleInformation( + reinterpret_cast(handle), InheritHandleFlag, inherit ? InheritHandleFlag : 0); + } #else int DuplicateFileDescriptor(int fd) { @@ -54,6 +74,18 @@ namespace { return ::fileno(file); } + + int DescriptorInheritance(int fd) + { + const int flags = ::fcntl(fd, F_GETFD); + return flags >= 0 ? (flags & FD_CLOEXEC) == 0 : -1; + } + + bool SetDescriptorInheritance(int fd, bool inherit) + { + const int flags = ::fcntl(fd, F_GETFD); + return flags >= 0 && ::fcntl(fd, F_SETFD, inherit ? flags & ~FD_CLOEXEC : flags | FD_CLOEXEC) == 0; + } #endif class StdoutCapture @@ -62,6 +94,7 @@ namespace StdoutCapture() { std::fflush(stdout); + m_originalInheritance = DescriptorInheritance(1); m_original = DuplicateFileDescriptor(1); #if defined(_WIN32) m_originalStdHandle = ::GetStdHandle(STD_OUTPUT_HANDLE); @@ -127,6 +160,10 @@ namespace { std::fflush(stdout); const bool restored = DuplicateFileDescriptorTo(m_original, 1) == 0; + if (restored && m_originalInheritance >= 0) + { + (void)SetDescriptorInheritance(1, m_originalInheritance != 0); + } (void)CloseFileDescriptor(m_original); m_original = -1; #if defined(_WIN32) @@ -154,6 +191,7 @@ namespace FILE* m_file{}; int m_original{-1}; + int m_originalInheritance{-1}; bool m_valid{}; #if defined(_WIN32) HANDLE m_originalStdHandle{INVALID_HANDLE_VALUE}; @@ -196,3 +234,57 @@ TEST(StandardStreamLogger, Lifecycle) EXPECT_TRUE(Babylon::StandardStreamLogger::Stop()); EXPECT_EQ(captured, "StandardStreamLogger stdout test"); } + +TEST(StandardStreamLogger, PreservesDescriptorInheritance) +{ + if (Babylon::StandardStreamLogger::IsStarted()) + { + GTEST_SKIP() << "The platform host already owns standard-stream forwarding."; + } + + for (const bool inherit : {false, true}) + { + StdoutCapture capture{}; + if (!capture.Valid()) + { + GTEST_SKIP() << "The platform does not expose a writable temporary-file location."; + } + ASSERT_TRUE(SetDescriptorInheritance(1, inherit)); + + const bool started = Babylon::StandardStreamLogger::Start(); + const int redirectedInheritance = DescriptorInheritance(1); + const bool stopped = Babylon::StandardStreamLogger::Stop(); + const int restoredInheritance = DescriptorInheritance(1); + (void)capture.ReadAndRestore(); + + EXPECT_TRUE(started); + EXPECT_TRUE(stopped); + EXPECT_EQ(redirectedInheritance, inherit); + EXPECT_EQ(restoredInheritance, inherit); + } +} + +TEST(StandardStreamLogger, LargeOutputPreservesOriginalBytes) +{ + if (Babylon::StandardStreamLogger::IsStarted()) + { + GTEST_SKIP() << "The platform host already owns standard-stream forwarding."; + } + + StdoutCapture capture{}; + if (!capture.Valid()) + { + GTEST_SKIP() << "The platform does not expose a writable temporary-file location."; + } + const std::string input = std::string(8192, 'x') + "\r\n" + + std::string{"tail\0more", 9} + "\xE2\x98\x83\n"; + const bool started = Babylon::StandardStreamLogger::Start(); + const size_t written = std::fwrite(input.data(), 1, input.size(), stdout); + const bool stopped = Babylon::StandardStreamLogger::Stop(); + const std::string captured = capture.ReadAndRestore(); + + EXPECT_TRUE(started); + EXPECT_EQ(written, input.size()); + EXPECT_TRUE(stopped); + EXPECT_EQ(captured, input); +} diff --git a/Tests/UnitTests/Source/Win32/App.cpp b/Tests/UnitTests/Source/Win32/App.cpp index 47a9f84e..dab0b283 100644 --- a/Tests/UnitTests/Source/Win32/App.cpp +++ b/Tests/UnitTests/Source/Win32/App.cpp @@ -1,10 +1,22 @@ #include "../App.h" #include +#include #include "Babylon/DebugTrace.h" #include +#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) +int RunStandardStreamLoggerSpawnProbe(int argc, char** argv); +#endif + int main(int argc, char** argv) { +#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) + if (argc >= 2 && std::strcmp(argv[1], "--standard-stream-logger-spawn-probe") == 0) + { + return RunStandardStreamLoggerSpawnProbe(argc, argv); + } +#endif + SetConsoleOutputCP(CP_UTF8); Babylon::DebugTrace::EnableDebugTrace(true);