From 76ef338c7a06726eb52fd5f183d621fcfbe1e3bf Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Thu, 27 Aug 2026 10:36:42 -0700 Subject: [PATCH 1/7] Add cross-platform standard stream forwarding Add an opt-in Foundation utility that tees process stdout and stderr to platform diagnostics while preserving the original stream destinations. Use it in the Android unit-test host and cover lifecycle and tee behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09 --- Core/Foundation/CMakeLists.txt | 8 +- .../Include/Babylon/StandardStreamLogger.h | 28 + .../Source/StandardStreamLogger.cpp | 546 ++++++++++++++++++ .../Android/app/src/main/cpp/CMakeLists.txt | 1 + .../Android/app/src/main/cpp/JNI.cpp | 14 +- Tests/UnitTests/CMakeLists.txt | 1 + .../UnitTests/Shared/StandardStreamLogger.cpp | 197 +++++++ 7 files changed, 789 insertions(+), 6 deletions(-) create mode 100644 Core/Foundation/Include/Babylon/StandardStreamLogger.h create mode 100644 Core/Foundation/Source/StandardStreamLogger.cpp create mode 100644 Tests/UnitTests/Shared/StandardStreamLogger.cpp diff --git a/Core/Foundation/CMakeLists.txt b/Core/Foundation/CMakeLists.txt index d089d429..a87c629d 100644 --- a/Core/Foundation/CMakeLists.txt +++ b/Core/Foundation/CMakeLists.txt @@ -2,8 +2,10 @@ set(SOURCES "Include/Babylon/Api.h" "Include/Babylon/DebugTrace.h" "Include/Babylon/PerfTrace.h" + "Include/Babylon/StandardStreamLogger.h" "Source/DebugTrace.cpp" - "Source/PerfTrace.cpp") + "Source/PerfTrace.cpp" + "Source/StandardStreamLogger.cpp") add_library(Foundation ${SOURCES}) @@ -16,5 +18,9 @@ target_link_libraries(Foundation PRIVATE napi-extensions PRIVATE arcana) +if(ANDROID) + target_link_libraries(Foundation PRIVATE log) +endif() + set_property(TARGET Foundation PROPERTY FOLDER Core) source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) diff --git a/Core/Foundation/Include/Babylon/StandardStreamLogger.h b/Core/Foundation/Include/Babylon/StandardStreamLogger.h new file mode 100644 index 00000000..7d14c08e --- /dev/null +++ b/Core/Foundation/Include/Babylon/StandardStreamLogger.h @@ -0,0 +1,28 @@ +#pragma once + +#include "Api.h" + +namespace Babylon::StandardStreamLogger +{ + /** + * Starts process-wide standard-stream forwarding. + * + * Android forwards to logcat, Apple platforms forward to os_log, and Windows + * forwards to OutputDebugString while preserving the original stream destination. + * Other Unix platforms already expose standard streams and leave them unchanged. + * + * Returns false if a platform stream could not be redirected. Repeated calls are + * idempotent. + */ + bool BABYLON_API Start(); + + /** + * Flushes pending output, restores the original streams, and stops forwarding. + * + * Returns false if an original stream could not be restored or pending output + * could not be drained before the shutdown timeout. Repeated calls are idempotent. + */ + bool BABYLON_API Stop(); + + bool BABYLON_API IsStarted(); +} diff --git a/Core/Foundation/Source/StandardStreamLogger.cpp b/Core/Foundation/Source/StandardStreamLogger.cpp new file mode 100644 index 00000000..e5f2f279 --- /dev/null +++ b/Core/Foundation/Source/StandardStreamLogger.cpp @@ -0,0 +1,546 @@ +#include "StandardStreamLogger.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#include +#include +#elif defined(__ANDROID__) +#include +#include +#include +#elif defined(__APPLE__) +#include +#include +#include +#endif + +namespace +{ +#if defined(_WIN32) || defined(__ANDROID__) || defined(__APPLE__) + enum class Stream + { + Output, + Error, + }; + + struct Channel + { + int Target{-1}; + int Original{-1}; +#if defined(_WIN32) + DWORD StandardHandle{}; + HANDLE OriginalHandle{INVALID_HANDLE_VALUE}; + bool OriginalHandleUsesTarget{}; +#endif + std::future Completion{}; + std::thread Reader{}; + }; + +#if defined(_WIN32) + void IgnoreInvalidParameter( + const wchar_t*, + const wchar_t*, + const wchar_t*, + unsigned int, + uintptr_t) + { + } + + int Duplicate(int fd) + { + return ::_dup(fd); + } + + int DuplicateTo(int source, int target) + { + return ::_dup2(source, target); + } + + int Close(int fd) + { + return ::_close(fd); + } + + int64_t Read(int fd, void* data, size_t size) + { + return ::_read(fd, data, static_cast(size)); + } + + int64_t Write(int fd, const void* data, size_t size) + { + return ::_write(fd, data, static_cast(size)); + } + + int CreatePipe(int fds[2]) + { + return ::_pipe(fds, 4096, _O_BINARY | _O_NOINHERIT); + } + + 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; + } +#else + int Duplicate(int fd) + { + return ::dup(fd); + } + + int DuplicateTo(int source, int target) + { + return ::dup2(source, target) < 0 ? -1 : 0; + } + + int Close(int fd) + { + return ::close(fd); + } + + int64_t Read(int fd, void* data, size_t size) + { + return ::read(fd, data, size); + } + + int64_t Write(int fd, const void* data, size_t size) + { + return ::write(fd, data, size); + } + + int CreatePipe(int fds[2]) + { + if (::pipe(fds) != 0) + { + return -1; + } + if (::fcntl(fds[0], F_SETFD, FD_CLOEXEC) != 0) + { + const int error = errno; + (void)::close(fds[0]); + (void)::close(fds[1]); + errno = error; + return -1; + } + return 0; + } +#endif + + void WritePlatform(Stream stream, const std::string& line) + { +#if defined(_WIN32) + (void)stream; + std::string output{line}; + output.push_back('\n'); + ::OutputDebugStringA(output.c_str()); +#elif defined(__ANDROID__) + const int priority = stream == Stream::Error ? ANDROID_LOG_ERROR : ANDROID_LOG_INFO; + __android_log_write(priority, "JsRuntimeHost", line.c_str()); +#elif defined(__APPLE__) + const os_log_type_t type = stream == Stream::Error ? OS_LOG_TYPE_ERROR : OS_LOG_TYPE_DEFAULT; + os_log_with_type(OS_LOG_DEFAULT, type, "%{public}s", line.c_str()); +#endif + } + + bool WriteAll(int fd, const char* data, size_t size) + { + while (size != 0) + { + const auto written = Write(fd, data, size); + if (written > 0) + { + data += written; + size -= static_cast(written); + continue; + } + if (written < 0 && errno == EINTR) + { + continue; + } + return false; + } + return true; + } + + void EmitLine(Stream stream, std::string line) + { + if (!line.empty() && line.back() == '\r') + { + line.pop_back(); + } + WritePlatform(stream, line); + } + + void Drain(int readFd, int originalFd, Stream stream) + { + constexpr size_t MAX_PLATFORM_LINE_SIZE{3800}; + std::array buffer{}; + std::string pending{}; + + for (;;) + { + const auto count = Read(readFd, buffer.data(), buffer.size()); + if (count == 0) + { + break; + } + if (count < 0) + { + if (errno == EINTR) + { + continue; + } + break; + } + + const size_t size = static_cast(count); + if (originalFd >= 0) + { + (void)WriteAll(originalFd, buffer.data(), size); + } + + pending.append(buffer.data(), size); + for (;;) + { + const size_t newline = pending.find('\n'); + if (newline != std::string::npos) + { + EmitLine(stream, pending.substr(0, newline)); + pending.erase(0, newline + 1); + } + else if (pending.size() >= MAX_PLATFORM_LINE_SIZE) + { + EmitLine(stream, pending.substr(0, MAX_PLATFORM_LINE_SIZE)); + pending.erase(0, MAX_PLATFORM_LINE_SIZE); + } + else + { + break; + } + } + } + + if (!pending.empty()) + { + EmitLine(stream, std::move(pending)); + } + (void)Close(readFd); + if (originalFd >= 0) + { + (void)Close(originalFd); + } + } + + bool OccupyTarget(int target) + { +#if defined(_WIN32) + const int nullFd = ::_open("NUL", _O_WRONLY | _O_BINARY); +#else + const int nullFd = ::open("/dev/null", O_WRONLY); +#endif + if (nullFd < 0) + { + return false; + } + if (nullFd == target) + { + return true; + } + + const bool duplicated = DuplicateTo(nullFd, target) == 0; + (void)Close(nullFd); + return duplicated; + } + +#if defined(_WIN32) + bool RestoreStandardHandle(const Channel& channel) + { + HANDLE handle = channel.OriginalHandle; + if (channel.OriginalHandleUsesTarget) + { + const intptr_t restoredHandle = GetOsHandle(channel.Target); + if (restoredHandle == -1) + { + return false; + } + handle = reinterpret_cast(restoredHandle); + } + return ::SetStdHandle(channel.StandardHandle, handle) != FALSE; + } +#endif + + bool StartChannel(Channel& channel, int target, Stream stream) + { + channel.Target = target; +#if defined(_WIN32) + channel.StandardHandle = stream == Stream::Error ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE; + channel.OriginalHandle = ::GetStdHandle(channel.StandardHandle); + const intptr_t targetHandle = GetOsHandle(target); + channel.OriginalHandleUsesTarget = + targetHandle != -1 && + channel.OriginalHandle != nullptr && + channel.OriginalHandle != INVALID_HANDLE_VALUE && + channel.OriginalHandle == reinterpret_cast(targetHandle); +#endif + errno = 0; +#if defined(_WIN32) + const auto previousHandler = ::_set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); +#endif + channel.Original = Duplicate(target); +#if defined(_WIN32) + (void)::_set_thread_local_invalid_parameter_handler(previousHandler); +#endif + if (channel.Original < 0 && errno != EBADF) + { + channel = {}; + return false; + } + if (channel.Original < 0 && !OccupyTarget(target)) + { + channel = {}; + return false; + } + + int pipeFds[2]{-1, -1}; + if (CreatePipe(pipeFds) != 0) + { + if (channel.Original >= 0) + { + (void)Close(channel.Original); + } + else + { + (void)Close(target); + } + channel = {}; + return false; + } + + if (DuplicateTo(pipeFds[1], target) != 0) + { + (void)Close(pipeFds[0]); + (void)Close(pipeFds[1]); + if (channel.Original >= 0) + { + (void)Close(channel.Original); + } + else + { + (void)Close(target); + } + channel = {}; + return false; + } + (void)Close(pipeFds[1]); + +#if defined(_WIN32) + const intptr_t pipeHandle = GetOsHandle(target); + if (pipeHandle == -1 || !::SetStdHandle(channel.StandardHandle, reinterpret_cast(pipeHandle))) + { + if (channel.Original >= 0) + { + (void)DuplicateTo(channel.Original, target); + (void)Close(channel.Original); + } + else + { + (void)Close(target); + } + (void)RestoreStandardHandle(channel); + (void)Close(pipeFds[0]); + channel = {}; + return false; + } +#endif + + int readerOriginal{-1}; + if (channel.Original >= 0) + { + readerOriginal = Duplicate(channel.Original); + if (readerOriginal < 0) + { + (void)DuplicateTo(channel.Original, target); + (void)Close(channel.Original); +#if defined(_WIN32) + (void)RestoreStandardHandle(channel); +#endif + (void)Close(pipeFds[0]); + channel = {}; + return false; + } + } + + std::promise completed{}; + channel.Completion = completed.get_future(); + try + { + channel.Reader = std::thread{ + [readFd = pipeFds[0], originalFd = readerOriginal, stream, completed = std::move(completed)]() mutable { + Drain(readFd, originalFd, stream); + completed.set_value(); + }}; + } + catch (const std::system_error&) + { + if (channel.Original >= 0) + { + (void)DuplicateTo(channel.Original, target); + (void)Close(channel.Original); + } + else + { + (void)Close(target); + } +#if defined(_WIN32) + (void)RestoreStandardHandle(channel); +#endif + (void)Close(pipeFds[0]); + if (readerOriginal >= 0) + { + (void)Close(readerOriginal); + } + channel = {}; + return false; + } + return true; + } + + bool StopChannel(Channel& channel) + { + bool restored{true}; + if (channel.Original >= 0) + { + restored = DuplicateTo(channel.Original, channel.Target) == 0; + if (!restored) + { + (void)Close(channel.Target); + } + } + else + { + restored = Close(channel.Target) == 0; + } + +#if defined(_WIN32) + restored = RestoreStandardHandle(channel) && restored; +#endif + + if (channel.Original >= 0) + { + (void)Close(channel.Original); + } + + if (channel.Reader.joinable()) + { + if (channel.Completion.wait_for(std::chrono::seconds{2}) == std::future_status::ready) + { + channel.Reader.join(); + } + else + { + channel.Reader.detach(); + restored = false; + } + } + channel = {}; + return restored; + } +#endif + + std::mutex g_mutex{}; + bool g_started{}; + bool g_exitHandlerRegistered{}; +#if defined(_WIN32) || defined(__ANDROID__) || defined(__APPLE__) + Channel g_stdout{}; + Channel g_stderr{}; +#endif +} + +namespace Babylon::StandardStreamLogger +{ + bool Start() + { + std::lock_guard lock{g_mutex}; + if (g_started) + { + return true; + } + +#if defined(_WIN32) || defined(__ANDROID__) || defined(__APPLE__) + std::cout.flush(); + std::cerr.flush(); + std::fflush(stdout); + std::fflush(stderr); + + if (!StartChannel(g_stdout, 1, Stream::Output)) + { + return false; + } + if (!StartChannel(g_stderr, 2, Stream::Error)) + { + (void)StopChannel(g_stdout); + return false; + } +#endif + + if (!g_exitHandlerRegistered) + { + if (std::atexit([] { + (void)Babylon::StandardStreamLogger::Stop(); + }) != 0) + { +#if defined(_WIN32) || defined(__ANDROID__) || defined(__APPLE__) + (void)StopChannel(g_stdout); + (void)StopChannel(g_stderr); +#endif + return false; + } + g_exitHandlerRegistered = true; + } + + g_started = true; + return true; + } + + bool Stop() + { + std::lock_guard lock{g_mutex}; + if (!g_started) + { + return true; + } + + bool stopped{true}; +#if defined(_WIN32) || defined(__ANDROID__) || defined(__APPLE__) + std::cout.flush(); + std::cerr.flush(); + std::fflush(stdout); + std::fflush(stderr); + stopped = StopChannel(g_stdout); + stopped = StopChannel(g_stderr) && stopped; +#endif + g_started = false; + return stopped; + } + + bool IsStarted() + { + std::lock_guard lock{g_mutex}; + return g_started; + } +} diff --git a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt index 0af5caa8..5f7c83a0 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt @@ -18,6 +18,7 @@ npm(install --silent WORKING_DIRECTORY ${TESTS_DIR}) add_library(UnitTestsJNI SHARED JNI.cpp + ${UNIT_TESTS_DIR}/Shared/StandardStreamLogger.cpp ${UNIT_TESTS_DIR}/Shared/Shared.h ${UNIT_TESTS_DIR}/Shared/Shared.cpp) diff --git a/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp b/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp index 4415ce87..60d40657 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp +++ b/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp @@ -2,8 +2,8 @@ #include #include #include -#include #include "Babylon/DebugTrace.h" +#include "Babylon/StandardStreamLogger.h" #include extern "C" JNIEXPORT jint JNICALL @@ -14,11 +14,15 @@ Java_com_jsruntimehost_unittests_Native_javaScriptTests(JNIEnv* env, jclass claz throw std::runtime_error{"Failed to get Java VM"}; } + if (!Babylon::StandardStreamLogger::Start()) + { + __android_log_write(ANDROID_LOG_ERROR, "JsRuntimeHost", "Failed to start standard-stream forwarding."); + return -1; + } + jclass webSocketClass{env->FindClass("com/jsruntimehost/unittests/WebSocket")}; java::websocket::WebSocketClient::InitializeJavaWebSocketClass(webSocketClass, env); - android::StdoutLogger::Start(); - android::global::Initialize(javaVM, context); Babylon::DebugTrace::EnableDebugTrace(true); @@ -26,8 +30,8 @@ Java_com_jsruntimehost_unittests_Native_javaScriptTests(JNIEnv* env, jclass claz auto testResult = RunTests(); - android::StdoutLogger::Stop(); + const bool loggerStopped = Babylon::StandardStreamLogger::Stop(); java::websocket::WebSocketClient::DestructJavaWebSocketClass(env); - return testResult; + return loggerStopped ? testResult : -1; } diff --git a/Tests/UnitTests/CMakeLists.txt b/Tests/UnitTests/CMakeLists.txt index 2dbc7619..b8446eb2 100644 --- a/Tests/UnitTests/CMakeLists.txt +++ b/Tests/UnitTests/CMakeLists.txt @@ -8,6 +8,7 @@ set(TYPE_SCRIPTS file(GLOB ASSETS "${CMAKE_CURRENT_SOURCE_DIR}/Assets/*") set(SOURCES + "Shared/StandardStreamLogger.cpp" "Shared/Shared.cpp" "Shared/Shared.h") diff --git a/Tests/UnitTests/Shared/StandardStreamLogger.cpp b/Tests/UnitTests/Shared/StandardStreamLogger.cpp new file mode 100644 index 00000000..0361a504 --- /dev/null +++ b/Tests/UnitTests/Shared/StandardStreamLogger.cpp @@ -0,0 +1,197 @@ +#include +#include + +#include +#include +#include + +#if defined(_WIN32) +#include +#include +#else +#include +#endif + +namespace +{ +#if defined(_WIN32) + int DuplicateFileDescriptor(int fd) + { + return ::_dup(fd); + } + + int DuplicateFileDescriptorTo(int source, int target) + { + return ::_dup2(source, target); + } + + int CloseFileDescriptor(int fd) + { + return ::_close(fd); + } + + int FileDescriptor(FILE* file) + { + return ::_fileno(file); + } +#else + int DuplicateFileDescriptor(int fd) + { + return ::dup(fd); + } + + int DuplicateFileDescriptorTo(int source, int target) + { + return ::dup2(source, target) < 0 ? -1 : 0; + } + + int CloseFileDescriptor(int fd) + { + return ::close(fd); + } + + int FileDescriptor(FILE* file) + { + return ::fileno(file); + } +#endif + + class StdoutCapture + { + public: + StdoutCapture() + { + std::fflush(stdout); + m_original = DuplicateFileDescriptor(1); +#if defined(_WIN32) + m_originalStdHandle = ::GetStdHandle(STD_OUTPUT_HANDLE); + const intptr_t originalFdHandle = ::_get_osfhandle(1); + m_originalStdHandleUsesTarget = + originalFdHandle != -1 && + m_originalStdHandle != nullptr && + m_originalStdHandle != INVALID_HANDLE_VALUE && + m_originalStdHandle == reinterpret_cast(originalFdHandle); +#endif + m_file = std::tmpfile(); + if (m_original < 0 || m_file == nullptr || DuplicateFileDescriptorTo(FileDescriptor(m_file), 1) != 0) + { + Restore(); + return; + } +#if defined(_WIN32) + const intptr_t handle = ::_get_osfhandle(1); + if (handle == -1 || !::SetStdHandle(STD_OUTPUT_HANDLE, reinterpret_cast(handle))) + { + Restore(); + return; + } +#endif + m_valid = true; + } + + ~StdoutCapture() + { + Restore(); + } + + bool Valid() const + { + return m_valid; + } + + std::string ReadAndRestore() + { + std::fflush(stdout); + std::rewind(m_file); + + std::string result{}; + std::array buffer{}; + for (;;) + { + const size_t size = std::fread(buffer.data(), 1, buffer.size(), m_file); + result.append(buffer.data(), size); + if (size != buffer.size()) + { + break; + } + } + + Restore(); + return result; + } + + private: + void Restore() + { + if (m_original >= 0) + { + std::fflush(stdout); + const bool restored = DuplicateFileDescriptorTo(m_original, 1) == 0; + (void)CloseFileDescriptor(m_original); + m_original = -1; +#if defined(_WIN32) + if (restored) + { + HANDLE handle = m_originalStdHandle; + if (m_originalStdHandleUsesTarget) + { + const intptr_t restoredFdHandle = ::_get_osfhandle(1); + handle = restoredFdHandle == -1 + ? INVALID_HANDLE_VALUE + : reinterpret_cast(restoredFdHandle); + } + (void)::SetStdHandle(STD_OUTPUT_HANDLE, handle); + } +#endif + } + if (m_file != nullptr) + { + std::fclose(m_file); + m_file = nullptr; + } + m_valid = false; + } + + FILE* m_file{}; + int m_original{-1}; + bool m_valid{}; +#if defined(_WIN32) + HANDLE m_originalStdHandle{INVALID_HANDLE_VALUE}; + bool m_originalStdHandleUsesTarget{}; +#endif + }; +} + +TEST(StandardStreamLogger, Lifecycle) +{ + 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 bool started = Babylon::StandardStreamLogger::Start(); + const bool isStarted = Babylon::StandardStreamLogger::IsStarted(); + const bool secondStart = Babylon::StandardStreamLogger::Start(); + + std::fputs("StandardStreamLogger stdout test", stdout); + std::fflush(stdout); + std::fputs("StandardStreamLogger stderr test\n", stderr); + std::fflush(stderr); + + const bool stopped = Babylon::StandardStreamLogger::Stop(); + const std::string captured = capture.ReadAndRestore(); + + EXPECT_TRUE(started); + EXPECT_TRUE(isStarted); + EXPECT_TRUE(secondStart); + EXPECT_TRUE(stopped); + EXPECT_FALSE(Babylon::StandardStreamLogger::IsStarted()); + EXPECT_TRUE(Babylon::StandardStreamLogger::Stop()); + EXPECT_EQ(captured, "StandardStreamLogger stdout test"); +} From f8b86190a3078c11bf6abf603aea79060b3b0d1e Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Thu, 27 Aug 2026 10:58:04 -0700 Subject: [PATCH 2/7] Fix StandardStreamLogger UWP and iOS CI failures Use CreatePipe/_open_osfhandle and _sopen_s so the Windows path builds on UWP, and stop writing to stderr in the lifecycle test because iOS CI captures simctl launch stderr as the process exit code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09 --- .../Source/StandardStreamLogger.cpp | 41 +++++++++++++++++-- .../UnitTests/Shared/StandardStreamLogger.cpp | 5 ++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/Core/Foundation/Source/StandardStreamLogger.cpp b/Core/Foundation/Source/StandardStreamLogger.cpp index e5f2f279..7f6c00c0 100644 --- a/Core/Foundation/Source/StandardStreamLogger.cpp +++ b/Core/Foundation/Source/StandardStreamLogger.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #elif defined(__ANDROID__) #include #include @@ -87,7 +88,36 @@ namespace int CreatePipe(int fds[2]) { - return ::_pipe(fds, 4096, _O_BINARY | _O_NOINHERIT); + // UWP's CRT does not expose _pipe. CreatePipe + _open_osfhandle works on + // desktop Win32 and UWP, and keeps the ends non-inheritable. + SECURITY_ATTRIBUTES attributes{}; + attributes.nLength = sizeof(attributes); + attributes.bInheritHandle = FALSE; + + HANDLE readHandle{INVALID_HANDLE_VALUE}; + HANDLE writeHandle{INVALID_HANDLE_VALUE}; + if (!::CreatePipe(&readHandle, &writeHandle, &attributes, 4096)) + { + return -1; + } + + fds[0] = ::_open_osfhandle(reinterpret_cast(readHandle), _O_BINARY); + if (fds[0] < 0) + { + (void)::CloseHandle(readHandle); + (void)::CloseHandle(writeHandle); + return -1; + } + + fds[1] = ::_open_osfhandle(reinterpret_cast(writeHandle), _O_BINARY); + if (fds[1] < 0) + { + (void)::_close(fds[0]); + (void)::CloseHandle(writeHandle); + return -1; + } + + return 0; } intptr_t GetOsHandle(int fd) @@ -249,14 +279,19 @@ namespace bool OccupyTarget(int target) { #if defined(_WIN32) - const int nullFd = ::_open("NUL", _O_WRONLY | _O_BINARY); + // 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) + { + return false; + } #else const int nullFd = ::open("/dev/null", O_WRONLY); -#endif if (nullFd < 0) { return false; } +#endif if (nullFd == target) { return true; diff --git a/Tests/UnitTests/Shared/StandardStreamLogger.cpp b/Tests/UnitTests/Shared/StandardStreamLogger.cpp index 0361a504..7f0c71e6 100644 --- a/Tests/UnitTests/Shared/StandardStreamLogger.cpp +++ b/Tests/UnitTests/Shared/StandardStreamLogger.cpp @@ -179,10 +179,11 @@ TEST(StandardStreamLogger, Lifecycle) const bool isStarted = Babylon::StandardStreamLogger::IsStarted(); const bool secondStart = Babylon::StandardStreamLogger::Start(); + // Only exercise stdout here. iOS CI captures simctl launch stderr as the + // process exit code (`2> /tmp/exitCode`), so writing to stderr would corrupt + // that handshake even when the tests themselves succeed. std::fputs("StandardStreamLogger stdout test", stdout); std::fflush(stdout); - std::fputs("StandardStreamLogger stderr test\n", stderr); - std::fflush(stderr); const bool stopped = Babylon::StandardStreamLogger::Stop(); const std::string captured = capture.ReadAndRestore(); From 227d45a9cccaabfdd0d1cb17cce2afeb0866427d Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Thu, 27 Aug 2026 11:51:45 -0700 Subject: [PATCH 3/7] Mark both POSIX pipe ends CLOEXEC in StandardStreamLogger Avoid a concurrent exec inheriting the write end and delaying Drain EOF on Stop. Windows already creates non-inheritable pipe ends. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09 --- Core/Foundation/Source/StandardStreamLogger.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Core/Foundation/Source/StandardStreamLogger.cpp b/Core/Foundation/Source/StandardStreamLogger.cpp index 7f6c00c0..20b819b3 100644 --- a/Core/Foundation/Source/StandardStreamLogger.cpp +++ b/Core/Foundation/Source/StandardStreamLogger.cpp @@ -159,7 +159,10 @@ namespace { return -1; } - if (::fcntl(fds[0], F_SETFD, FD_CLOEXEC) != 0) + // Mark both ends CLOEXEC. Leaving the write end inheritable would let a + // concurrent exec keep the pipe open and delay Drain()'s EOF on Stop(). + if (::fcntl(fds[0], F_SETFD, FD_CLOEXEC) != 0 || + ::fcntl(fds[1], F_SETFD, FD_CLOEXEC) != 0) { const int error = errno; (void)::close(fds[0]); From e29f716f18aee6cd2458b11cf6cde53e0d62bfd0 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Thu, 27 Aug 2026 12:43:42 -0700 Subject: [PATCH 4/7] Address Copilot follow-ups on StandardStreamLogger Keep timed-out drain futures so Start() cannot install a second tee while a detached drain is still alive. Consume pending lines with a start index to avoid quadratic erase, document the 3800-byte platform line cap and IsStarted() contract, and log Android Stop() failures to logcat. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09 --- .../Include/Babylon/StandardStreamLogger.h | 13 +- .../Source/StandardStreamLogger.cpp | 199 +++++++++++------- .../Android/app/src/main/cpp/JNI.cpp | 13 +- 3 files changed, 147 insertions(+), 78 deletions(-) diff --git a/Core/Foundation/Include/Babylon/StandardStreamLogger.h b/Core/Foundation/Include/Babylon/StandardStreamLogger.h index 7d14c08e..ebcf3002 100644 --- a/Core/Foundation/Include/Babylon/StandardStreamLogger.h +++ b/Core/Foundation/Include/Babylon/StandardStreamLogger.h @@ -24,5 +24,14 @@ namespace Babylon::StandardStreamLogger */ bool BABYLON_API Stop(); - bool BABYLON_API IsStarted(); -} + /** + * Returns whether Start() has successfully begun process-wide forwarding and + * Stop() has not yet completed. + * + * This is the logical started flag, not a live probe of the underlying file + * descriptors. On platforms that leave stdout/stderr unchanged (plain Linux + * and other non-Android Unix hosts), Start() still succeeds and IsStarted() + * reports true even though no redirection was installed. + */ + bool BABYLON_API IsStarted(); + } diff --git a/Core/Foundation/Source/StandardStreamLogger.cpp b/Core/Foundation/Source/StandardStreamLogger.cpp index 20b819b3..63052014 100644 --- a/Core/Foundation/Source/StandardStreamLogger.cpp +++ b/Core/Foundation/Source/StandardStreamLogger.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #if defined(_WIN32) #include @@ -51,6 +52,10 @@ namespace std::thread Reader{}; }; + // Drain futures retained after Stop() times out and detaches the reader. + // Start() reaps these before installing a new redirection. + std::vector> g_outstandingDrains{}; + #if defined(_WIN32) void IgnoreInvalidParameter( const wchar_t*, @@ -221,63 +226,74 @@ namespace void Drain(int readFd, int originalFd, Stream stream) { - constexpr size_t MAX_PLATFORM_LINE_SIZE{3800}; - std::array buffer{}; - std::string pending{}; + // Cap mirrored lines below typical platform limits: + // OutputDebugStringA (~4 KiB practical), Android logcat (~4 KiB), + // and Apple os_log payload limits. Leave headroom under 4096. + constexpr size_t MAX_PLATFORM_LINE_SIZE{3800}; + std::array buffer{}; + std::string pending{}; - for (;;) - { - const auto count = Read(readFd, buffer.data(), buffer.size()); - if (count == 0) - { - break; - } - if (count < 0) + for (;;) { - if (errno == EINTR) + const auto count = Read(readFd, buffer.data(), buffer.size()); + if (count == 0) { - continue; + break; + } + if (count < 0) + { + if (errno == EINTR) + { + continue; + } + break; } - break; - } - - const size_t size = static_cast(count); - if (originalFd >= 0) - { - (void)WriteAll(originalFd, buffer.data(), size); - } - pending.append(buffer.data(), size); - for (;;) - { - const size_t newline = pending.find('\n'); - if (newline != std::string::npos) + const size_t size = static_cast(count); + if (originalFd >= 0) { - EmitLine(stream, pending.substr(0, newline)); - pending.erase(0, newline + 1); + (void)WriteAll(originalFd, buffer.data(), size); } - else if (pending.size() >= MAX_PLATFORM_LINE_SIZE) + + pending.append(buffer.data(), size); + + // 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 (;;) { - EmitLine(stream, pending.substr(0, MAX_PLATFORM_LINE_SIZE)); - pending.erase(0, MAX_PLATFORM_LINE_SIZE); + 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; + } } - else + if (start != 0) { - break; + pending.erase(0, start); } } - } - if (!pending.empty()) - { - EmitLine(stream, std::move(pending)); - } - (void)Close(readFd); - if (originalFd >= 0) - { - (void)Close(originalFd); + if (!pending.empty()) + { + EmitLine(stream, std::move(pending)); + } + (void)Close(readFd); + if (originalFd >= 0) + { + (void)Close(originalFd); + } } - } bool OccupyTarget(int target) { @@ -491,23 +507,54 @@ namespace } else { - channel.Reader.detach(); - restored = false; + // Detach so Stop can return, but keep the future so Start() + // can refuse a restart until this drain actually finishes. + // Otherwise a second Start() would spin up concurrent drains + // and duplicate/out-of-order platform logging. + g_outstandingDrains.push_back(std::move(channel.Completion)); + channel.Reader.detach(); + restored = false; + } + } + channel = {}; + return restored; } - } - channel = {}; - return restored; - } -#endif - std::mutex g_mutex{}; - bool g_started{}; - bool g_exitHandlerRegistered{}; -#if defined(_WIN32) || defined(__ANDROID__) || defined(__APPLE__) - Channel g_stdout{}; - Channel g_stderr{}; -#endif -} + // Drop completed drains; optionally wait up to `timeout` for the rest. + // Returns true only when no outstanding drains remain. + bool ReapOutstandingDrains(std::chrono::milliseconds timeout) + { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (!g_outstandingDrains.empty()) + { + auto& front = g_outstandingDrains.front(); + const auto remaining = deadline - std::chrono::steady_clock::now(); + if (remaining <= std::chrono::milliseconds::zero()) + { + if (front.wait_for(std::chrono::milliseconds::zero()) != std::future_status::ready) + { + return false; + } + } + else if (front.wait_for(remaining) != std::future_status::ready) + { + return false; + } + + g_outstandingDrains.erase(g_outstandingDrains.begin()); + } + return true; + } + #endif + + std::mutex g_mutex{}; + bool g_started{}; + bool g_exitHandlerRegistered{}; + #if defined(_WIN32) || defined(__ANDROID__) || defined(__APPLE__) + Channel g_stdout{}; + Channel g_stderr{}; + #endif + } namespace Babylon::StandardStreamLogger { @@ -520,21 +567,29 @@ namespace Babylon::StandardStreamLogger } #if defined(_WIN32) || defined(__ANDROID__) || defined(__APPLE__) - std::cout.flush(); - std::cerr.flush(); - std::fflush(stdout); - std::fflush(stderr); + // A prior Stop() may have detached drain threads after timeout. Do not + // redirect again until those finish; otherwise concurrent drains can + // duplicate platform logs against the restored (or newly teed) streams. + if (!ReapOutstandingDrains(std::chrono::seconds{2})) + { + return false; + } - if (!StartChannel(g_stdout, 1, Stream::Output)) - { - return false; - } - if (!StartChannel(g_stderr, 2, Stream::Error)) - { - (void)StopChannel(g_stdout); - return false; - } -#endif + std::cout.flush(); + std::cerr.flush(); + std::fflush(stdout); + std::fflush(stderr); + + if (!StartChannel(g_stdout, 1, Stream::Output)) + { + return false; + } + if (!StartChannel(g_stderr, 2, Stream::Error)) + { + (void)StopChannel(g_stdout); + return false; + } + #endif if (!g_exitHandlerRegistered) { diff --git a/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp b/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp index 60d40657..85612af3 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp +++ b/Tests/UnitTests/Android/app/src/main/cpp/JNI.cpp @@ -31,7 +31,12 @@ Java_com_jsruntimehost_unittests_Native_javaScriptTests(JNIEnv* env, jclass claz auto testResult = RunTests(); const bool loggerStopped = Babylon::StandardStreamLogger::Stop(); - - java::websocket::WebSocketClient::DestructJavaWebSocketClass(env); - return loggerStopped ? testResult : -1; -} + if (!loggerStopped) + { + __android_log_write(ANDROID_LOG_ERROR, "JsRuntimeHost", + "Failed to stop standard-stream forwarding (restore or drain timeout)."); + } + + java::websocket::WebSocketClient::DestructJavaWebSocketClass(env); + return loggerStopped ? testResult : -1; + } From ac36b14b4a3568eaa82a61db7614f397a42e4af3 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Fri, 28 Aug 2026 13:03:43 -0700 Subject: [PATCH 5/7] Split StandardStreamLogger into platform TUs Match AppRuntime: shared API + one JSRUNTIMEHOST_PLATFORM implementation. Windows (Win32/UWP), Android, Apple, and Unix each get their own source file; Android/Apple share the POSIX tee body via an .inl. Also fix IsStarted() doc comment indentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09 --- Core/Foundation/CMakeLists.txt | 23 +- .../Include/Babylon/StandardStreamLogger.h | 22 +- .../Source/StandardStreamLogger.cpp | 600 +----------------- .../Source/StandardStreamLoggerPlatform.h | 16 + .../Source/StandardStreamLogger_Android.cpp | 17 + .../Source/StandardStreamLogger_Apple.cpp | 17 + .../Source/StandardStreamLogger_Posix.inl | 397 ++++++++++++ .../Source/StandardStreamLogger_Unix.cpp | 18 + .../Source/StandardStreamLogger_Windows.cpp | 490 ++++++++++++++ 9 files changed, 999 insertions(+), 601 deletions(-) create mode 100644 Core/Foundation/Source/StandardStreamLoggerPlatform.h create mode 100644 Core/Foundation/Source/StandardStreamLogger_Android.cpp create mode 100644 Core/Foundation/Source/StandardStreamLogger_Apple.cpp create mode 100644 Core/Foundation/Source/StandardStreamLogger_Posix.inl create mode 100644 Core/Foundation/Source/StandardStreamLogger_Unix.cpp create mode 100644 Core/Foundation/Source/StandardStreamLogger_Windows.cpp diff --git a/Core/Foundation/CMakeLists.txt b/Core/Foundation/CMakeLists.txt index a87c629d..dec300eb 100644 --- a/Core/Foundation/CMakeLists.txt +++ b/Core/Foundation/CMakeLists.txt @@ -5,12 +5,31 @@ set(SOURCES "Include/Babylon/StandardStreamLogger.h" "Source/DebugTrace.cpp" "Source/PerfTrace.cpp" - "Source/StandardStreamLogger.cpp") + "Source/StandardStreamLogger.cpp" + "Source/StandardStreamLoggerPlatform.h") + +# Match AppRuntime: shared API TU + one platform TU selected by JSRUNTIMEHOST_PLATFORM. +# Windows (Win32/UWP) and POSIX (Android/Apple) keep their own redirect implementations; +# plain Unix is a successful no-op because stdout/stderr already reach the environment. +if(JSRUNTIMEHOST_PLATFORM STREQUAL "Win32" OR JSRUNTIMEHOST_PLATFORM STREQUAL "UWP") + list(APPEND SOURCES "Source/StandardStreamLogger_Windows.cpp") +elseif(JSRUNTIMEHOST_PLATFORM STREQUAL "Android") + list(APPEND SOURCES + "Source/StandardStreamLogger_Android.cpp" + "Source/StandardStreamLogger_Posix.inl") +elseif(JSRUNTIMEHOST_PLATFORM STREQUAL "iOS" OR JSRUNTIMEHOST_PLATFORM STREQUAL "macOS") + list(APPEND SOURCES + "Source/StandardStreamLogger_Apple.cpp" + "Source/StandardStreamLogger_Posix.inl") +else() + list(APPEND SOURCES "Source/StandardStreamLogger_Unix.cpp") +endif() add_library(Foundation ${SOURCES}) target_include_directories(Foundation PRIVATE "Include/Babylon" + PRIVATE "Source" INTERFACE "Include") target_link_libraries(Foundation @@ -23,4 +42,4 @@ if(ANDROID) endif() set_property(TARGET Foundation PROPERTY FOLDER Core) -source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) \ No newline at end of file diff --git a/Core/Foundation/Include/Babylon/StandardStreamLogger.h b/Core/Foundation/Include/Babylon/StandardStreamLogger.h index ebcf3002..765fb149 100644 --- a/Core/Foundation/Include/Babylon/StandardStreamLogger.h +++ b/Core/Foundation/Include/Babylon/StandardStreamLogger.h @@ -24,14 +24,14 @@ namespace Babylon::StandardStreamLogger */ bool BABYLON_API Stop(); - /** - * Returns whether Start() has successfully begun process-wide forwarding and - * Stop() has not yet completed. - * - * This is the logical started flag, not a live probe of the underlying file - * descriptors. On platforms that leave stdout/stderr unchanged (plain Linux - * and other non-Android Unix hosts), Start() still succeeds and IsStarted() - * reports true even though no redirection was installed. - */ - bool BABYLON_API IsStarted(); - } + /** + * Returns whether Start() has successfully begun process-wide forwarding and + * Stop() has not yet completed. + * + * This is the logical started flag, not a live probe of the underlying file + * descriptors. On platforms that leave stdout/stderr unchanged (plain Linux + * and other non-Android Unix hosts), Start() still succeeds and IsStarted() + * reports true even though no redirection was installed. + */ + bool BABYLON_API IsStarted(); +} \ No newline at end of file diff --git a/Core/Foundation/Source/StandardStreamLogger.cpp b/Core/Foundation/Source/StandardStreamLogger.cpp index 63052014..1b7327ab 100644 --- a/Core/Foundation/Source/StandardStreamLogger.cpp +++ b/Core/Foundation/Source/StandardStreamLogger.cpp @@ -1,560 +1,15 @@ #include "StandardStreamLogger.h" +#include "StandardStreamLoggerPlatform.h" -#include -#include -#include -#include #include -#include -#include -#include #include -#include -#include -#include -#include -#include - -#if defined(_WIN32) -#include -#include -#include -#include -#elif defined(__ANDROID__) -#include -#include -#include -#elif defined(__APPLE__) -#include -#include -#include -#endif namespace { -#if defined(_WIN32) || defined(__ANDROID__) || defined(__APPLE__) - enum class Stream - { - Output, - Error, - }; - - struct Channel - { - int Target{-1}; - int Original{-1}; -#if defined(_WIN32) - DWORD StandardHandle{}; - HANDLE OriginalHandle{INVALID_HANDLE_VALUE}; - bool OriginalHandleUsesTarget{}; -#endif - std::future Completion{}; - std::thread Reader{}; - }; - - // Drain futures retained after Stop() times out and detaches the reader. - // Start() reaps these before installing a new redirection. - std::vector> g_outstandingDrains{}; - -#if defined(_WIN32) - void IgnoreInvalidParameter( - const wchar_t*, - const wchar_t*, - const wchar_t*, - unsigned int, - uintptr_t) - { - } - - int Duplicate(int fd) - { - return ::_dup(fd); - } - - int DuplicateTo(int source, int target) - { - return ::_dup2(source, target); - } - - int Close(int fd) - { - return ::_close(fd); - } - - int64_t Read(int fd, void* data, size_t size) - { - return ::_read(fd, data, static_cast(size)); - } - - int64_t Write(int fd, const void* data, size_t size) - { - return ::_write(fd, data, static_cast(size)); - } - - int CreatePipe(int fds[2]) - { - // UWP's CRT does not expose _pipe. CreatePipe + _open_osfhandle works on - // desktop Win32 and UWP, and keeps the ends non-inheritable. - SECURITY_ATTRIBUTES attributes{}; - attributes.nLength = sizeof(attributes); - attributes.bInheritHandle = FALSE; - - HANDLE readHandle{INVALID_HANDLE_VALUE}; - HANDLE writeHandle{INVALID_HANDLE_VALUE}; - if (!::CreatePipe(&readHandle, &writeHandle, &attributes, 4096)) - { - return -1; - } - - fds[0] = ::_open_osfhandle(reinterpret_cast(readHandle), _O_BINARY); - if (fds[0] < 0) - { - (void)::CloseHandle(readHandle); - (void)::CloseHandle(writeHandle); - return -1; - } - - fds[1] = ::_open_osfhandle(reinterpret_cast(writeHandle), _O_BINARY); - if (fds[1] < 0) - { - (void)::_close(fds[0]); - (void)::CloseHandle(writeHandle); - return -1; - } - - return 0; - } - - 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; - } -#else - int Duplicate(int fd) - { - return ::dup(fd); - } - - int DuplicateTo(int source, int target) - { - return ::dup2(source, target) < 0 ? -1 : 0; - } - - int Close(int fd) - { - return ::close(fd); - } - - int64_t Read(int fd, void* data, size_t size) - { - return ::read(fd, data, size); - } - - int64_t Write(int fd, const void* data, size_t size) - { - return ::write(fd, data, size); - } - - int CreatePipe(int fds[2]) - { - if (::pipe(fds) != 0) - { - return -1; - } - // Mark both ends CLOEXEC. Leaving the write end inheritable would let a - // concurrent exec keep the pipe open and delay Drain()'s EOF on Stop(). - if (::fcntl(fds[0], F_SETFD, FD_CLOEXEC) != 0 || - ::fcntl(fds[1], F_SETFD, FD_CLOEXEC) != 0) - { - const int error = errno; - (void)::close(fds[0]); - (void)::close(fds[1]); - errno = error; - return -1; - } - return 0; - } -#endif - - void WritePlatform(Stream stream, const std::string& line) - { -#if defined(_WIN32) - (void)stream; - std::string output{line}; - output.push_back('\n'); - ::OutputDebugStringA(output.c_str()); -#elif defined(__ANDROID__) - const int priority = stream == Stream::Error ? ANDROID_LOG_ERROR : ANDROID_LOG_INFO; - __android_log_write(priority, "JsRuntimeHost", line.c_str()); -#elif defined(__APPLE__) - const os_log_type_t type = stream == Stream::Error ? OS_LOG_TYPE_ERROR : OS_LOG_TYPE_DEFAULT; - os_log_with_type(OS_LOG_DEFAULT, type, "%{public}s", line.c_str()); -#endif - } - - bool WriteAll(int fd, const char* data, size_t size) - { - while (size != 0) - { - const auto written = Write(fd, data, size); - if (written > 0) - { - data += written; - size -= static_cast(written); - continue; - } - if (written < 0 && errno == EINTR) - { - continue; - } - return false; - } - return true; - } - - void EmitLine(Stream stream, std::string line) - { - if (!line.empty() && line.back() == '\r') - { - line.pop_back(); - } - WritePlatform(stream, line); - } - - void Drain(int readFd, int originalFd, Stream stream) - { - // Cap mirrored lines below typical platform limits: - // OutputDebugStringA (~4 KiB practical), Android logcat (~4 KiB), - // and Apple os_log payload limits. Leave headroom under 4096. - constexpr size_t MAX_PLATFORM_LINE_SIZE{3800}; - std::array buffer{}; - std::string pending{}; - - for (;;) - { - const auto count = Read(readFd, buffer.data(), buffer.size()); - if (count == 0) - { - break; - } - if (count < 0) - { - if (errno == EINTR) - { - continue; - } - break; - } - - const size_t size = static_cast(count); - if (originalFd >= 0) - { - (void)WriteAll(originalFd, buffer.data(), size); - } - - pending.append(buffer.data(), size); - - // 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); - } - } - - if (!pending.empty()) - { - EmitLine(stream, std::move(pending)); - } - (void)Close(readFd); - if (originalFd >= 0) - { - (void)Close(originalFd); - } - } - - bool OccupyTarget(int target) - { -#if defined(_WIN32) - // 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) - { - return false; - } -#else - const int nullFd = ::open("/dev/null", O_WRONLY); - if (nullFd < 0) - { - return false; - } -#endif - if (nullFd == target) - { - return true; - } - - const bool duplicated = DuplicateTo(nullFd, target) == 0; - (void)Close(nullFd); - return duplicated; - } - -#if defined(_WIN32) - bool RestoreStandardHandle(const Channel& channel) - { - HANDLE handle = channel.OriginalHandle; - if (channel.OriginalHandleUsesTarget) - { - const intptr_t restoredHandle = GetOsHandle(channel.Target); - if (restoredHandle == -1) - { - return false; - } - handle = reinterpret_cast(restoredHandle); - } - return ::SetStdHandle(channel.StandardHandle, handle) != FALSE; - } -#endif - - bool StartChannel(Channel& channel, int target, Stream stream) - { - channel.Target = target; -#if defined(_WIN32) - channel.StandardHandle = stream == Stream::Error ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE; - channel.OriginalHandle = ::GetStdHandle(channel.StandardHandle); - const intptr_t targetHandle = GetOsHandle(target); - channel.OriginalHandleUsesTarget = - targetHandle != -1 && - channel.OriginalHandle != nullptr && - channel.OriginalHandle != INVALID_HANDLE_VALUE && - channel.OriginalHandle == reinterpret_cast(targetHandle); -#endif - errno = 0; -#if defined(_WIN32) - const auto previousHandler = ::_set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); -#endif - channel.Original = Duplicate(target); -#if defined(_WIN32) - (void)::_set_thread_local_invalid_parameter_handler(previousHandler); -#endif - if (channel.Original < 0 && errno != EBADF) - { - channel = {}; - return false; - } - if (channel.Original < 0 && !OccupyTarget(target)) - { - channel = {}; - return false; - } - - int pipeFds[2]{-1, -1}; - if (CreatePipe(pipeFds) != 0) - { - if (channel.Original >= 0) - { - (void)Close(channel.Original); - } - else - { - (void)Close(target); - } - channel = {}; - return false; - } - - if (DuplicateTo(pipeFds[1], target) != 0) - { - (void)Close(pipeFds[0]); - (void)Close(pipeFds[1]); - if (channel.Original >= 0) - { - (void)Close(channel.Original); - } - else - { - (void)Close(target); - } - channel = {}; - return false; - } - (void)Close(pipeFds[1]); - -#if defined(_WIN32) - const intptr_t pipeHandle = GetOsHandle(target); - if (pipeHandle == -1 || !::SetStdHandle(channel.StandardHandle, reinterpret_cast(pipeHandle))) - { - if (channel.Original >= 0) - { - (void)DuplicateTo(channel.Original, target); - (void)Close(channel.Original); - } - else - { - (void)Close(target); - } - (void)RestoreStandardHandle(channel); - (void)Close(pipeFds[0]); - channel = {}; - return false; - } -#endif - - int readerOriginal{-1}; - if (channel.Original >= 0) - { - readerOriginal = Duplicate(channel.Original); - if (readerOriginal < 0) - { - (void)DuplicateTo(channel.Original, target); - (void)Close(channel.Original); -#if defined(_WIN32) - (void)RestoreStandardHandle(channel); -#endif - (void)Close(pipeFds[0]); - channel = {}; - return false; - } - } - - std::promise completed{}; - channel.Completion = completed.get_future(); - try - { - channel.Reader = std::thread{ - [readFd = pipeFds[0], originalFd = readerOriginal, stream, completed = std::move(completed)]() mutable { - Drain(readFd, originalFd, stream); - completed.set_value(); - }}; - } - catch (const std::system_error&) - { - if (channel.Original >= 0) - { - (void)DuplicateTo(channel.Original, target); - (void)Close(channel.Original); - } - else - { - (void)Close(target); - } -#if defined(_WIN32) - (void)RestoreStandardHandle(channel); -#endif - (void)Close(pipeFds[0]); - if (readerOriginal >= 0) - { - (void)Close(readerOriginal); - } - channel = {}; - return false; - } - return true; - } - - bool StopChannel(Channel& channel) - { - bool restored{true}; - if (channel.Original >= 0) - { - restored = DuplicateTo(channel.Original, channel.Target) == 0; - if (!restored) - { - (void)Close(channel.Target); - } - } - else - { - restored = Close(channel.Target) == 0; - } - -#if defined(_WIN32) - restored = RestoreStandardHandle(channel) && restored; -#endif - - if (channel.Original >= 0) - { - (void)Close(channel.Original); - } - - if (channel.Reader.joinable()) - { - if (channel.Completion.wait_for(std::chrono::seconds{2}) == std::future_status::ready) - { - channel.Reader.join(); - } - else - { - // Detach so Stop can return, but keep the future so Start() - // can refuse a restart until this drain actually finishes. - // Otherwise a second Start() would spin up concurrent drains - // and duplicate/out-of-order platform logging. - g_outstandingDrains.push_back(std::move(channel.Completion)); - channel.Reader.detach(); - restored = false; - } - } - channel = {}; - return restored; - } - - // Drop completed drains; optionally wait up to `timeout` for the rest. - // Returns true only when no outstanding drains remain. - bool ReapOutstandingDrains(std::chrono::milliseconds timeout) - { - const auto deadline = std::chrono::steady_clock::now() + timeout; - while (!g_outstandingDrains.empty()) - { - auto& front = g_outstandingDrains.front(); - const auto remaining = deadline - std::chrono::steady_clock::now(); - if (remaining <= std::chrono::milliseconds::zero()) - { - if (front.wait_for(std::chrono::milliseconds::zero()) != std::future_status::ready) - { - return false; - } - } - else if (front.wait_for(remaining) != std::future_status::ready) - { - return false; - } - - g_outstandingDrains.erase(g_outstandingDrains.begin()); - } - return true; - } - #endif - - std::mutex g_mutex{}; - bool g_started{}; - bool g_exitHandlerRegistered{}; - #if defined(_WIN32) || defined(__ANDROID__) || defined(__APPLE__) - Channel g_stdout{}; - Channel g_stderr{}; - #endif - } + std::mutex g_mutex{}; + bool g_started{}; + bool g_exitHandlerRegistered{}; +} namespace Babylon::StandardStreamLogger { @@ -566,30 +21,10 @@ namespace Babylon::StandardStreamLogger return true; } -#if defined(_WIN32) || defined(__ANDROID__) || defined(__APPLE__) - // A prior Stop() may have detached drain threads after timeout. Do not - // redirect again until those finish; otherwise concurrent drains can - // duplicate platform logs against the restored (or newly teed) streams. - if (!ReapOutstandingDrains(std::chrono::seconds{2})) - { - return false; - } - - std::cout.flush(); - std::cerr.flush(); - std::fflush(stdout); - std::fflush(stderr); - - if (!StartChannel(g_stdout, 1, Stream::Output)) - { - return false; - } - if (!StartChannel(g_stderr, 2, Stream::Error)) - { - (void)StopChannel(g_stdout); - return false; - } - #endif + if (!Platform::Start()) + { + return false; + } if (!g_exitHandlerRegistered) { @@ -597,10 +32,7 @@ namespace Babylon::StandardStreamLogger (void)Babylon::StandardStreamLogger::Stop(); }) != 0) { -#if defined(_WIN32) || defined(__ANDROID__) || defined(__APPLE__) - (void)StopChannel(g_stdout); - (void)StopChannel(g_stderr); -#endif + (void)Platform::Stop(); return false; } g_exitHandlerRegistered = true; @@ -618,15 +50,7 @@ namespace Babylon::StandardStreamLogger return true; } - bool stopped{true}; -#if defined(_WIN32) || defined(__ANDROID__) || defined(__APPLE__) - std::cout.flush(); - std::cerr.flush(); - std::fflush(stdout); - std::fflush(stderr); - stopped = StopChannel(g_stdout); - stopped = StopChannel(g_stderr) && stopped; -#endif + const bool stopped = Platform::Stop(); g_started = false; return stopped; } @@ -636,4 +60,4 @@ namespace Babylon::StandardStreamLogger std::lock_guard lock{g_mutex}; return g_started; } -} +} \ No newline at end of file diff --git a/Core/Foundation/Source/StandardStreamLoggerPlatform.h b/Core/Foundation/Source/StandardStreamLoggerPlatform.h new file mode 100644 index 00000000..b0e11c90 --- /dev/null +++ b/Core/Foundation/Source/StandardStreamLoggerPlatform.h @@ -0,0 +1,16 @@ +#pragma once + +// Internal platform hooks for StandardStreamLogger. +// Each JSRUNTIMEHOST_PLATFORM TU implements these; the shared API TU owns the +// process-wide mutex / started flag and is the only public entry point. + +namespace Babylon::StandardStreamLogger::Platform +{ + // Install stdout/stderr redirection and drain threads. + // Not synchronized — the shared API holds the process-wide mutex. + bool Start(); + + // Flush, restore original streams, and join (or timeout-detach) drains. + // Not synchronized — the shared API holds the process-wide mutex. + bool Stop(); +} diff --git a/Core/Foundation/Source/StandardStreamLogger_Android.cpp b/Core/Foundation/Source/StandardStreamLogger_Android.cpp new file mode 100644 index 00000000..ddd1f0f6 --- /dev/null +++ b/Core/Foundation/Source/StandardStreamLogger_Android.cpp @@ -0,0 +1,17 @@ +#include "StandardStreamLoggerPlatform.h" + +#include +#include + +// Android mirrors drained lines to logcat. The tee/redirect machinery is shared +// with Apple via StandardStreamLogger_Posix.inl. +#define SSL_WRITE_PLATFORM(stream, line) \ + do \ + { \ + const int priority = (stream) == Stream::Error \ + ? ANDROID_LOG_ERROR \ + : ANDROID_LOG_INFO; \ + __android_log_write(priority, "JsRuntimeHost", (line).c_str()); \ + } while (0) + +#include "StandardStreamLogger_Posix.inl" \ No newline at end of file diff --git a/Core/Foundation/Source/StandardStreamLogger_Apple.cpp b/Core/Foundation/Source/StandardStreamLogger_Apple.cpp new file mode 100644 index 00000000..b0120896 --- /dev/null +++ b/Core/Foundation/Source/StandardStreamLogger_Apple.cpp @@ -0,0 +1,17 @@ +#include "StandardStreamLoggerPlatform.h" + +#include +#include + +// Apple mirrors drained lines to os_log. The tee/redirect machinery is shared +// with Android via StandardStreamLogger_Posix.inl. +#define SSL_WRITE_PLATFORM(stream, line) \ + do \ + { \ + const os_log_type_t type = (stream) == Stream::Error \ + ? OS_LOG_TYPE_ERROR \ + : OS_LOG_TYPE_DEFAULT; \ + os_log_with_type(OS_LOG_DEFAULT, type, "%{public}s", (line).c_str()); \ + } while (0) + +#include "StandardStreamLogger_Posix.inl" \ No newline at end of file diff --git a/Core/Foundation/Source/StandardStreamLogger_Posix.inl b/Core/Foundation/Source/StandardStreamLogger_Posix.inl new file mode 100644 index 00000000..bae2606f --- /dev/null +++ b/Core/Foundation/Source/StandardStreamLogger_Posix.inl @@ -0,0 +1,397 @@ +// Shared POSIX redirection body for Android and Apple. +// The including TU must define SSL_WRITE_PLATFORM(stream, line) before include. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace +{ + enum class Stream + { + Output, + Error, + }; + + struct Channel + { + int Target{-1}; + int Original{-1}; + std::future Completion{}; + std::thread Reader{}; + }; + + std::vector> g_outstandingDrains{}; + Channel g_stdout{}; + Channel g_stderr{}; + + int Duplicate(int fd) + { + return ::dup(fd); + } + + int DuplicateTo(int source, int target) + { + return ::dup2(source, target) < 0 ? -1 : 0; + } + + int Close(int fd) + { + return ::close(fd); + } + + int64_t Read(int fd, void* data, size_t size) + { + return ::read(fd, data, size); + } + + int64_t Write(int fd, const void* data, size_t size) + { + return ::write(fd, data, size); + } + + int CreatePipe(int fds[2]) + { + if (::pipe(fds) != 0) + { + return -1; + } + // Mark both ends CLOEXEC. Leaving the write end inheritable would let a + // concurrent exec keep the pipe open and delay Drain()'s EOF on Stop(). + if (::fcntl(fds[0], F_SETFD, FD_CLOEXEC) != 0 || + ::fcntl(fds[1], F_SETFD, FD_CLOEXEC) != 0) + { + const int error = errno; + (void)::close(fds[0]); + (void)::close(fds[1]); + errno = error; + return -1; + } + return 0; + } + + void WritePlatform(Stream stream, const std::string& line) + { + SSL_WRITE_PLATFORM(stream, line); + } + + bool WriteAll(int fd, const char* data, size_t size) + { + while (size != 0) + { + const auto written = Write(fd, data, size); + if (written > 0) + { + data += written; + size -= static_cast(written); + continue; + } + if (written < 0 && errno == EINTR) + { + continue; + } + return false; + } + return true; + } + + void EmitLine(Stream stream, std::string line) + { + if (!line.empty() && line.back() == '\r') + { + line.pop_back(); + } + WritePlatform(stream, line); + } + + void Drain(int readFd, int originalFd, Stream stream) + { + // Cap mirrored lines below typical platform limits: + // Android logcat (~4 KiB) and Apple os_log payloads. Leave headroom under 4096. + constexpr size_t MAX_PLATFORM_LINE_SIZE{3800}; + std::array buffer{}; + std::string pending{}; + + for (;;) + { + const auto count = Read(readFd, buffer.data(), buffer.size()); + if (count == 0) + { + break; + } + if (count < 0) + { + if (errno == EINTR) + { + continue; + } + break; + } + + const size_t size = static_cast(count); + if (originalFd >= 0) + { + (void)WriteAll(originalFd, buffer.data(), size); + } + + pending.append(buffer.data(), size); + + 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); + } + } + + if (!pending.empty()) + { + EmitLine(stream, std::move(pending)); + } + (void)Close(readFd); + if (originalFd >= 0) + { + (void)Close(originalFd); + } + } + + bool OccupyTarget(int target) + { + const int nullFd = ::open("/dev/null", O_WRONLY); + if (nullFd < 0) + { + return false; + } + if (nullFd == target) + { + return true; + } + + const bool duplicated = DuplicateTo(nullFd, target) == 0; + (void)Close(nullFd); + return duplicated; + } + + bool StartChannel(Channel& channel, int target, Stream stream) + { + channel.Target = target; + errno = 0; + channel.Original = Duplicate(target); + if (channel.Original < 0 && errno != EBADF) + { + channel = {}; + return false; + } + if (channel.Original < 0 && !OccupyTarget(target)) + { + channel = {}; + return false; + } + + int pipeFds[2]{-1, -1}; + if (CreatePipe(pipeFds) != 0) + { + if (channel.Original >= 0) + { + (void)Close(channel.Original); + } + else + { + (void)Close(target); + } + channel = {}; + return false; + } + + if (DuplicateTo(pipeFds[1], target) != 0) + { + (void)Close(pipeFds[0]); + (void)Close(pipeFds[1]); + if (channel.Original >= 0) + { + (void)Close(channel.Original); + } + else + { + (void)Close(target); + } + channel = {}; + return false; + } + (void)Close(pipeFds[1]); + + int readerOriginal{-1}; + if (channel.Original >= 0) + { + readerOriginal = Duplicate(channel.Original); + if (readerOriginal < 0) + { + (void)DuplicateTo(channel.Original, target); + (void)Close(channel.Original); + (void)Close(pipeFds[0]); + channel = {}; + return false; + } + } + + std::promise completed{}; + channel.Completion = completed.get_future(); + try + { + channel.Reader = std::thread{ + [readFd = pipeFds[0], originalFd = readerOriginal, stream, completed = std::move(completed)]() mutable { + Drain(readFd, originalFd, stream); + completed.set_value(); + }}; + } + catch (const std::system_error&) + { + if (channel.Original >= 0) + { + (void)DuplicateTo(channel.Original, target); + (void)Close(channel.Original); + } + else + { + (void)Close(target); + } + (void)Close(pipeFds[0]); + if (readerOriginal >= 0) + { + (void)Close(readerOriginal); + } + channel = {}; + return false; + } + return true; + } + + bool StopChannel(Channel& channel) + { + bool restored{true}; + if (channel.Original >= 0) + { + restored = DuplicateTo(channel.Original, channel.Target) == 0; + if (!restored) + { + (void)Close(channel.Target); + } + } + else + { + restored = Close(channel.Target) == 0; + } + + if (channel.Original >= 0) + { + (void)Close(channel.Original); + } + + if (channel.Reader.joinable()) + { + if (channel.Completion.wait_for(std::chrono::seconds{2}) == std::future_status::ready) + { + channel.Reader.join(); + } + else + { + g_outstandingDrains.push_back(std::move(channel.Completion)); + channel.Reader.detach(); + restored = false; + } + } + channel = {}; + return restored; + } + + bool ReapOutstandingDrains(std::chrono::milliseconds timeout) + { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (!g_outstandingDrains.empty()) + { + auto& front = g_outstandingDrains.front(); + const auto remaining = deadline - std::chrono::steady_clock::now(); + if (remaining <= std::chrono::milliseconds::zero()) + { + if (front.wait_for(std::chrono::milliseconds::zero()) != std::future_status::ready) + { + return false; + } + } + else if (front.wait_for(remaining) != std::future_status::ready) + { + return false; + } + + g_outstandingDrains.erase(g_outstandingDrains.begin()); + } + return true; + } +} + +namespace Babylon::StandardStreamLogger::Platform +{ + bool Start() + { + if (!ReapOutstandingDrains(std::chrono::seconds{2})) + { + return false; + } + + std::cout.flush(); + std::cerr.flush(); + std::fflush(stdout); + std::fflush(stderr); + + if (!StartChannel(g_stdout, 1, Stream::Output)) + { + return false; + } + if (!StartChannel(g_stderr, 2, Stream::Error)) + { + (void)StopChannel(g_stdout); + return false; + } + return true; + } + + bool Stop() + { + std::cout.flush(); + std::cerr.flush(); + std::fflush(stdout); + std::fflush(stderr); + bool stopped = StopChannel(g_stdout); + stopped = StopChannel(g_stderr) && stopped; + return stopped; + } +} \ No newline at end of file diff --git a/Core/Foundation/Source/StandardStreamLogger_Unix.cpp b/Core/Foundation/Source/StandardStreamLogger_Unix.cpp new file mode 100644 index 00000000..32ff735e --- /dev/null +++ b/Core/Foundation/Source/StandardStreamLogger_Unix.cpp @@ -0,0 +1,18 @@ +#include "StandardStreamLoggerPlatform.h" + +// Plain Unix already exposes stdout/stderr to the process environment (terminals, +// journald, etc.), so there is nothing to redirect. Start/Stop are successful +// no-ops; the shared API still flips IsStarted() for a uniform host contract. + +namespace Babylon::StandardStreamLogger::Platform +{ + bool Start() + { + return true; + } + + bool Stop() + { + return true; + } +} diff --git a/Core/Foundation/Source/StandardStreamLogger_Windows.cpp b/Core/Foundation/Source/StandardStreamLogger_Windows.cpp new file mode 100644 index 00000000..195832e9 --- /dev/null +++ b/Core/Foundation/Source/StandardStreamLogger_Windows.cpp @@ -0,0 +1,490 @@ +#include "StandardStreamLoggerPlatform.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace +{ + enum class Stream + { + Output, + Error, + }; + + struct Channel + { + int Target{-1}; + int Original{-1}; + DWORD StandardHandle{}; + HANDLE OriginalHandle{INVALID_HANDLE_VALUE}; + bool OriginalHandleUsesTarget{}; + std::future Completion{}; + std::thread Reader{}; + }; + + // Drain futures retained after Stop() times out and detaches the reader. + // Start() reaps these before installing a new redirection. + std::vector> g_outstandingDrains{}; + Channel g_stdout{}; + Channel g_stderr{}; + + void IgnoreInvalidParameter( + const wchar_t*, + const wchar_t*, + const wchar_t*, + unsigned int, + uintptr_t) + { + } + + int Duplicate(int fd) + { + return ::_dup(fd); + } + + int DuplicateTo(int source, int target) + { + return ::_dup2(source, target); + } + + int Close(int fd) + { + return ::_close(fd); + } + + int64_t Read(int fd, void* data, size_t size) + { + return ::_read(fd, data, static_cast(size)); + } + + int64_t Write(int fd, const void* data, size_t size) + { + return ::_write(fd, data, static_cast(size)); + } + + int CreatePipe(int fds[2]) + { + // UWP's CRT does not expose _pipe. CreatePipe + _open_osfhandle works on + // desktop Win32 and UWP, and keeps the ends non-inheritable. + SECURITY_ATTRIBUTES attributes{}; + attributes.nLength = sizeof(attributes); + attributes.bInheritHandle = FALSE; + + HANDLE readHandle{INVALID_HANDLE_VALUE}; + HANDLE writeHandle{INVALID_HANDLE_VALUE}; + if (!::CreatePipe(&readHandle, &writeHandle, &attributes, 4096)) + { + return -1; + } + + fds[0] = ::_open_osfhandle(reinterpret_cast(readHandle), _O_BINARY); + if (fds[0] < 0) + { + (void)::CloseHandle(readHandle); + (void)::CloseHandle(writeHandle); + return -1; + } + + fds[1] = ::_open_osfhandle(reinterpret_cast(writeHandle), _O_BINARY); + if (fds[1] < 0) + { + (void)::_close(fds[0]); + (void)::CloseHandle(writeHandle); + return -1; + } + + return 0; + } + + 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; + } + + void WritePlatform(Stream /*stream*/, const std::string& line) + { + std::string output{line}; + output.push_back('\n'); + ::OutputDebugStringA(output.c_str()); + } + + bool WriteAll(int fd, const char* data, size_t size) + { + while (size != 0) + { + const auto written = Write(fd, data, size); + if (written > 0) + { + data += written; + size -= static_cast(written); + continue; + } + if (written < 0 && errno == EINTR) + { + continue; + } + return false; + } + return true; + } + + void EmitLine(Stream stream, std::string line) + { + if (!line.empty() && line.back() == '\r') + { + line.pop_back(); + } + WritePlatform(stream, line); + } + + void Drain(int readFd, int originalFd, Stream stream) + { + // Cap mirrored lines below typical platform limits: + // OutputDebugStringA (~4 KiB practical). Leave headroom under 4096. + constexpr size_t MAX_PLATFORM_LINE_SIZE{3800}; + std::array buffer{}; + std::string pending{}; + + for (;;) + { + const auto count = Read(readFd, buffer.data(), buffer.size()); + if (count == 0) + { + break; + } + if (count < 0) + { + if (errno == EINTR) + { + continue; + } + break; + } + + const size_t size = static_cast(count); + if (originalFd >= 0) + { + (void)WriteAll(originalFd, buffer.data(), size); + } + + pending.append(buffer.data(), size); + + // 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); + } + } + + if (!pending.empty()) + { + EmitLine(stream, std::move(pending)); + } + (void)Close(readFd); + if (originalFd >= 0) + { + (void)Close(originalFd); + } + } + + bool OccupyTarget(int target) + { + // 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) + { + return false; + } + if (nullFd == target) + { + return true; + } + + const bool duplicated = DuplicateTo(nullFd, target) == 0; + (void)Close(nullFd); + return duplicated; + } + + bool RestoreStandardHandle(const Channel& channel) + { + HANDLE handle = channel.OriginalHandle; + if (channel.OriginalHandleUsesTarget) + { + const intptr_t restoredHandle = GetOsHandle(channel.Target); + if (restoredHandle == -1) + { + return false; + } + handle = reinterpret_cast(restoredHandle); + } + return ::SetStdHandle(channel.StandardHandle, handle) != FALSE; + } + + bool StartChannel(Channel& channel, int target, Stream stream) + { + channel.Target = target; + channel.StandardHandle = stream == Stream::Error ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE; + channel.OriginalHandle = ::GetStdHandle(channel.StandardHandle); + const intptr_t targetHandle = GetOsHandle(target); + channel.OriginalHandleUsesTarget = + targetHandle != -1 && + channel.OriginalHandle != nullptr && + channel.OriginalHandle != INVALID_HANDLE_VALUE && + channel.OriginalHandle == reinterpret_cast(targetHandle); + + errno = 0; + const auto previousHandler = ::_set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); + channel.Original = Duplicate(target); + (void)::_set_thread_local_invalid_parameter_handler(previousHandler); + + if (channel.Original < 0 && errno != EBADF) + { + channel = {}; + return false; + } + if (channel.Original < 0 && !OccupyTarget(target)) + { + channel = {}; + return false; + } + + int pipeFds[2]{-1, -1}; + if (CreatePipe(pipeFds) != 0) + { + if (channel.Original >= 0) + { + (void)Close(channel.Original); + } + else + { + (void)Close(target); + } + channel = {}; + return false; + } + + if (DuplicateTo(pipeFds[1], target) != 0) + { + (void)Close(pipeFds[0]); + (void)Close(pipeFds[1]); + if (channel.Original >= 0) + { + (void)Close(channel.Original); + } + else + { + (void)Close(target); + } + channel = {}; + return false; + } + (void)Close(pipeFds[1]); + + const intptr_t pipeHandle = GetOsHandle(target); + if (pipeHandle == -1 || !::SetStdHandle(channel.StandardHandle, reinterpret_cast(pipeHandle))) + { + if (channel.Original >= 0) + { + (void)DuplicateTo(channel.Original, target); + (void)Close(channel.Original); + } + else + { + (void)Close(target); + } + (void)RestoreStandardHandle(channel); + (void)Close(pipeFds[0]); + channel = {}; + return false; + } + + int readerOriginal{-1}; + if (channel.Original >= 0) + { + readerOriginal = Duplicate(channel.Original); + if (readerOriginal < 0) + { + (void)DuplicateTo(channel.Original, target); + (void)Close(channel.Original); + (void)RestoreStandardHandle(channel); + (void)Close(pipeFds[0]); + channel = {}; + return false; + } + } + + std::promise completed{}; + channel.Completion = completed.get_future(); + try + { + channel.Reader = std::thread{ + [readFd = pipeFds[0], originalFd = readerOriginal, stream, completed = std::move(completed)]() mutable { + Drain(readFd, originalFd, stream); + completed.set_value(); + }}; + } + catch (const std::system_error&) + { + if (channel.Original >= 0) + { + (void)DuplicateTo(channel.Original, target); + (void)Close(channel.Original); + } + else + { + (void)Close(target); + } + (void)RestoreStandardHandle(channel); + (void)Close(pipeFds[0]); + if (readerOriginal >= 0) + { + (void)Close(readerOriginal); + } + channel = {}; + return false; + } + return true; + } + + bool StopChannel(Channel& channel) + { + bool restored{true}; + if (channel.Original >= 0) + { + restored = DuplicateTo(channel.Original, channel.Target) == 0; + if (!restored) + { + (void)Close(channel.Target); + } + } + else + { + restored = Close(channel.Target) == 0; + } + + restored = RestoreStandardHandle(channel) && restored; + + if (channel.Original >= 0) + { + (void)Close(channel.Original); + } + + if (channel.Reader.joinable()) + { + if (channel.Completion.wait_for(std::chrono::seconds{2}) == std::future_status::ready) + { + channel.Reader.join(); + } + else + { + // Detach so Stop can return, but keep the future so Start() + // can refuse a restart until this drain actually finishes. + g_outstandingDrains.push_back(std::move(channel.Completion)); + channel.Reader.detach(); + restored = false; + } + } + channel = {}; + return restored; + } + + bool ReapOutstandingDrains(std::chrono::milliseconds timeout) + { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (!g_outstandingDrains.empty()) + { + auto& front = g_outstandingDrains.front(); + const auto remaining = deadline - std::chrono::steady_clock::now(); + if (remaining <= std::chrono::milliseconds::zero()) + { + if (front.wait_for(std::chrono::milliseconds::zero()) != std::future_status::ready) + { + return false; + } + } + else if (front.wait_for(remaining) != std::future_status::ready) + { + return false; + } + + g_outstandingDrains.erase(g_outstandingDrains.begin()); + } + return true; + } +} + +namespace Babylon::StandardStreamLogger::Platform +{ + bool Start() + { + if (!ReapOutstandingDrains(std::chrono::seconds{2})) + { + return false; + } + + std::cout.flush(); + std::cerr.flush(); + std::fflush(stdout); + std::fflush(stderr); + + if (!StartChannel(g_stdout, 1, Stream::Output)) + { + return false; + } + if (!StartChannel(g_stderr, 2, Stream::Error)) + { + (void)StopChannel(g_stdout); + return false; + } + return true; + } + + bool Stop() + { + std::cout.flush(); + std::cerr.flush(); + std::fflush(stdout); + std::fflush(stderr); + bool stopped = StopChannel(g_stdout); + stopped = StopChannel(g_stderr) && stopped; + return stopped; + } +} \ No newline at end of file From 1c4519a3dbd376371a1f69fc24fd893e5275fb5f Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Fri, 28 Aug 2026 14:50:24 -0700 Subject: [PATCH 6/7] Share StandardStreamLogger tee body across platforms Pull Drain/StartChannel/StopChannel into StandardStreamLogger_Shared.inl and leave each platform TU as thin OS ops (fd/pipe/null, diagnostic sink, and Windows StdHandle hooks). Android/Apple share POSIX fd helpers via StandardStreamLogger_PosixOps.inl. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09 --- Core/Foundation/CMakeLists.txt | 12 +- .../Source/StandardStreamLogger_Android.cpp | 27 +- .../Source/StandardStreamLogger_Apple.cpp | 27 +- .../Source/StandardStreamLogger_PosixOps.inl | 83 ++++ ...ix.inl => StandardStreamLogger_Shared.inl} | 184 ++++---- .../Source/StandardStreamLogger_Windows.cpp | 429 +++--------------- 6 files changed, 250 insertions(+), 512 deletions(-) create mode 100644 Core/Foundation/Source/StandardStreamLogger_PosixOps.inl rename Core/Foundation/Source/{StandardStreamLogger_Posix.inl => StandardStreamLogger_Shared.inl} (66%) diff --git a/Core/Foundation/CMakeLists.txt b/Core/Foundation/CMakeLists.txt index dec300eb..6b7f7ff1 100644 --- a/Core/Foundation/CMakeLists.txt +++ b/Core/Foundation/CMakeLists.txt @@ -6,21 +6,21 @@ set(SOURCES "Source/DebugTrace.cpp" "Source/PerfTrace.cpp" "Source/StandardStreamLogger.cpp" - "Source/StandardStreamLoggerPlatform.h") + "Source/StandardStreamLoggerPlatform.h" + "Source/StandardStreamLogger_Shared.inl") -# Match AppRuntime: shared API TU + one platform TU selected by JSRUNTIMEHOST_PLATFORM. -# Windows (Win32/UWP) and POSIX (Android/Apple) keep their own redirect implementations; -# plain Unix is a successful no-op because stdout/stderr already reach the environment. +# Shared tee body (StandardStreamLogger_Shared.inl) + thin OS-ops platform TUs. +# Match AppRuntime: one JSRUNTIMEHOST_PLATFORM implementation is compiled in. if(JSRUNTIMEHOST_PLATFORM STREQUAL "Win32" OR JSRUNTIMEHOST_PLATFORM STREQUAL "UWP") list(APPEND SOURCES "Source/StandardStreamLogger_Windows.cpp") elseif(JSRUNTIMEHOST_PLATFORM STREQUAL "Android") list(APPEND SOURCES "Source/StandardStreamLogger_Android.cpp" - "Source/StandardStreamLogger_Posix.inl") + "Source/StandardStreamLogger_PosixOps.inl") elseif(JSRUNTIMEHOST_PLATFORM STREQUAL "iOS" OR JSRUNTIMEHOST_PLATFORM STREQUAL "macOS") list(APPEND SOURCES "Source/StandardStreamLogger_Apple.cpp" - "Source/StandardStreamLogger_Posix.inl") + "Source/StandardStreamLogger_PosixOps.inl") else() list(APPEND SOURCES "Source/StandardStreamLogger_Unix.cpp") endif() diff --git a/Core/Foundation/Source/StandardStreamLogger_Android.cpp b/Core/Foundation/Source/StandardStreamLogger_Android.cpp index ddd1f0f6..110c6d7c 100644 --- a/Core/Foundation/Source/StandardStreamLogger_Android.cpp +++ b/Core/Foundation/Source/StandardStreamLogger_Android.cpp @@ -1,17 +1,22 @@ #include "StandardStreamLoggerPlatform.h" #include +#include +#include #include -// Android mirrors drained lines to logcat. The tee/redirect machinery is shared -// with Apple via StandardStreamLogger_Posix.inl. -#define SSL_WRITE_PLATFORM(stream, line) \ - do \ - { \ - const int priority = (stream) == Stream::Error \ - ? ANDROID_LOG_ERROR \ - : ANDROID_LOG_INFO; \ - __android_log_write(priority, "JsRuntimeHost", (line).c_str()); \ - } while (0) +#include +#include -#include "StandardStreamLogger_Posix.inl" \ No newline at end of file +namespace +{ +#include "StandardStreamLogger_PosixOps.inl" + + void OsWritePlatform(bool isError, const std::string& line) + { + const int priority = isError ? ANDROID_LOG_ERROR : ANDROID_LOG_INFO; + __android_log_write(priority, "JsRuntimeHost", line.c_str()); + } +} + +#include "StandardStreamLogger_Shared.inl" \ No newline at end of file diff --git a/Core/Foundation/Source/StandardStreamLogger_Apple.cpp b/Core/Foundation/Source/StandardStreamLogger_Apple.cpp index b0120896..2fbe82e2 100644 --- a/Core/Foundation/Source/StandardStreamLogger_Apple.cpp +++ b/Core/Foundation/Source/StandardStreamLogger_Apple.cpp @@ -1,17 +1,22 @@ #include "StandardStreamLoggerPlatform.h" #include +#include +#include #include -// Apple mirrors drained lines to os_log. The tee/redirect machinery is shared -// with Android via StandardStreamLogger_Posix.inl. -#define SSL_WRITE_PLATFORM(stream, line) \ - do \ - { \ - const os_log_type_t type = (stream) == Stream::Error \ - ? OS_LOG_TYPE_ERROR \ - : OS_LOG_TYPE_DEFAULT; \ - os_log_with_type(OS_LOG_DEFAULT, type, "%{public}s", (line).c_str()); \ - } while (0) +#include +#include -#include "StandardStreamLogger_Posix.inl" \ No newline at end of file +namespace +{ +#include "StandardStreamLogger_PosixOps.inl" + + void OsWritePlatform(bool isError, const std::string& line) + { + const os_log_type_t type = isError ? OS_LOG_TYPE_ERROR : OS_LOG_TYPE_DEFAULT; + os_log_with_type(OS_LOG_DEFAULT, type, "%{public}s", line.c_str()); + } +} + +#include "StandardStreamLogger_Shared.inl" \ No newline at end of file diff --git a/Core/Foundation/Source/StandardStreamLogger_PosixOps.inl b/Core/Foundation/Source/StandardStreamLogger_PosixOps.inl new file mode 100644 index 00000000..ef215102 --- /dev/null +++ b/Core/Foundation/Source/StandardStreamLogger_PosixOps.inl @@ -0,0 +1,83 @@ +// POSIX fd primitives shared by Android and Apple. Included inside an anonymous +// namespace that already provides OsWritePlatform. + +struct ChannelPlatformState +{ +}; + +int OsDuplicate(int fd) +{ + return ::dup(fd); +} + +int OsDuplicateTo(int source, int target) +{ + return ::dup2(source, target) < 0 ? -1 : 0; +} + +int OsClose(int fd) +{ + return ::close(fd); +} + +int64_t OsRead(int fd, void* data, size_t size) +{ + return ::read(fd, data, size); +} + +int64_t OsWrite(int fd, const void* data, size_t size) +{ + return ::write(fd, data, size); +} + +int OsCreatePipe(int fds[2]) +{ + if (::pipe(fds) != 0) + { + return -1; + } + // Mark both ends CLOEXEC. Leaving the write end inheritable would let a + // concurrent exec keep the pipe open and delay Drain()'s EOF on Stop(). + if (::fcntl(fds[0], F_SETFD, FD_CLOEXEC) != 0 || + ::fcntl(fds[1], F_SETFD, FD_CLOEXEC) != 0) + { + const int error = errno; + (void)::close(fds[0]); + (void)::close(fds[1]); + errno = error; + return -1; + } + return 0; +} + +bool OsOccupyTarget(int target) +{ + const int nullFd = ::open("/dev/null", O_WRONLY); + if (nullFd < 0) + { + return false; + } + if (nullFd == target) + { + return true; + } + + const bool duplicated = OsDuplicateTo(nullFd, target) == 0; + (void)OsClose(nullFd); + return duplicated; +} + +bool OsOnStartChannel(ChannelPlatformState&, int, bool) +{ + return true; +} + +bool OsOnRedirected(ChannelPlatformState&, int) +{ + return true; +} + +bool OsOnRestore(ChannelPlatformState&, int) +{ + return true; +} \ No newline at end of file diff --git a/Core/Foundation/Source/StandardStreamLogger_Posix.inl b/Core/Foundation/Source/StandardStreamLogger_Shared.inl similarity index 66% rename from Core/Foundation/Source/StandardStreamLogger_Posix.inl rename to Core/Foundation/Source/StandardStreamLogger_Shared.inl index bae2606f..db765b1c 100644 --- a/Core/Foundation/Source/StandardStreamLogger_Posix.inl +++ b/Core/Foundation/Source/StandardStreamLogger_Shared.inl @@ -1,5 +1,18 @@ -// Shared POSIX redirection body for Android and Apple. -// The including TU must define SSL_WRITE_PLATFORM(stream, line) before include. +// Shared stdout/stderr tee + drain implementation. +// +// Platform TUs define these in the enclosing anonymous namespace, then include: +// struct ChannelPlatformState { ... }; +// int OsDuplicate(int fd); +// int OsDuplicateTo(int source, int target); +// 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); +// void OsWritePlatform(bool isError, const std::string& line); +// bool OsOnStartChannel(ChannelPlatformState& state, int target, bool isError); +// bool OsOnRedirected(ChannelPlatformState& state, int target); +// bool OsOnRestore(ChannelPlatformState& state, int target); #include #include @@ -14,9 +27,6 @@ #include #include -#include -#include - namespace { enum class Stream @@ -29,69 +39,22 @@ namespace { int Target{-1}; int Original{-1}; + ChannelPlatformState Platform{}; std::future Completion{}; std::thread Reader{}; }; + // Drain futures retained after Stop() times out and detaches the reader. + // Start() reaps these before installing a new redirection. std::vector> g_outstandingDrains{}; Channel g_stdout{}; Channel g_stderr{}; - int Duplicate(int fd) - { - return ::dup(fd); - } - - int DuplicateTo(int source, int target) - { - return ::dup2(source, target) < 0 ? -1 : 0; - } - - int Close(int fd) - { - return ::close(fd); - } - - int64_t Read(int fd, void* data, size_t size) - { - return ::read(fd, data, size); - } - - int64_t Write(int fd, const void* data, size_t size) - { - return ::write(fd, data, size); - } - - int CreatePipe(int fds[2]) - { - if (::pipe(fds) != 0) - { - return -1; - } - // Mark both ends CLOEXEC. Leaving the write end inheritable would let a - // concurrent exec keep the pipe open and delay Drain()'s EOF on Stop(). - if (::fcntl(fds[0], F_SETFD, FD_CLOEXEC) != 0 || - ::fcntl(fds[1], F_SETFD, FD_CLOEXEC) != 0) - { - const int error = errno; - (void)::close(fds[0]); - (void)::close(fds[1]); - errno = error; - return -1; - } - return 0; - } - - void WritePlatform(Stream stream, const std::string& line) - { - SSL_WRITE_PLATFORM(stream, line); - } - bool WriteAll(int fd, const char* data, size_t size) { while (size != 0) { - const auto written = Write(fd, data, size); + const auto written = OsWrite(fd, data, size); if (written > 0) { data += written; @@ -113,20 +76,20 @@ namespace { line.pop_back(); } - WritePlatform(stream, line); + OsWritePlatform(stream == Stream::Error, line); } void Drain(int readFd, int originalFd, Stream stream) { - // Cap mirrored lines below typical platform limits: - // Android logcat (~4 KiB) and Apple os_log payloads. Leave headroom under 4096. + // 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}; std::array buffer{}; std::string pending{}; for (;;) { - const auto count = Read(readFd, buffer.data(), buffer.size()); + const auto count = OsRead(readFd, buffer.data(), buffer.size()); if (count == 0) { break; @@ -148,6 +111,8 @@ namespace pending.append(buffer.data(), size); + // 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 (;;) { @@ -177,88 +142,103 @@ namespace { EmitLine(stream, std::move(pending)); } - (void)Close(readFd); + (void)OsClose(readFd); if (originalFd >= 0) { - (void)Close(originalFd); + (void)OsClose(originalFd); } } - bool OccupyTarget(int target) + void RollbackRedirect(Channel& channel, int pipeReadFd, int readerOriginal) { - const int nullFd = ::open("/dev/null", O_WRONLY); - if (nullFd < 0) + if (channel.Original >= 0) { - return false; + (void)OsDuplicateTo(channel.Original, channel.Target); + (void)OsClose(channel.Original); } - if (nullFd == target) + else { - return true; + (void)OsClose(channel.Target); } - - const bool duplicated = DuplicateTo(nullFd, target) == 0; - (void)Close(nullFd); - return duplicated; + (void)OsOnRestore(channel.Platform, channel.Target); + if (pipeReadFd >= 0) + { + (void)OsClose(pipeReadFd); + } + if (readerOriginal >= 0) + { + (void)OsClose(readerOriginal); + } + channel = {}; } bool StartChannel(Channel& channel, int target, Stream stream) { channel.Target = target; + if (!OsOnStartChannel(channel.Platform, target, stream == Stream::Error)) + { + channel = {}; + return false; + } + errno = 0; - channel.Original = Duplicate(target); + channel.Original = OsDuplicate(target); if (channel.Original < 0 && errno != EBADF) { channel = {}; return false; } - if (channel.Original < 0 && !OccupyTarget(target)) + if (channel.Original < 0 && !OsOccupyTarget(target)) { channel = {}; return false; } int pipeFds[2]{-1, -1}; - if (CreatePipe(pipeFds) != 0) + if (OsCreatePipe(pipeFds) != 0) { if (channel.Original >= 0) { - (void)Close(channel.Original); + (void)OsClose(channel.Original); } else { - (void)Close(target); + (void)OsClose(target); } channel = {}; return false; } - if (DuplicateTo(pipeFds[1], target) != 0) + if (OsDuplicateTo(pipeFds[1], target) != 0) { - (void)Close(pipeFds[0]); - (void)Close(pipeFds[1]); + (void)OsClose(pipeFds[0]); + (void)OsClose(pipeFds[1]); if (channel.Original >= 0) { - (void)Close(channel.Original); + (void)OsClose(channel.Original); } else { - (void)Close(target); + (void)OsClose(target); } channel = {}; return false; } - (void)Close(pipeFds[1]); + (void)OsClose(pipeFds[1]); + + if (!OsOnRedirected(channel.Platform, target)) + { + RollbackRedirect(channel, pipeFds[0], -1); + return false; + } int readerOriginal{-1}; if (channel.Original >= 0) { - readerOriginal = Duplicate(channel.Original); + readerOriginal = OsDuplicate(channel.Original); if (readerOriginal < 0) { - (void)DuplicateTo(channel.Original, target); - (void)Close(channel.Original); - (void)Close(pipeFds[0]); - channel = {}; + RollbackRedirect(channel, pipeFds[0], -1); return false; } } @@ -275,21 +255,7 @@ namespace } catch (const std::system_error&) { - if (channel.Original >= 0) - { - (void)DuplicateTo(channel.Original, target); - (void)Close(channel.Original); - } - else - { - (void)Close(target); - } - (void)Close(pipeFds[0]); - if (readerOriginal >= 0) - { - (void)Close(readerOriginal); - } - channel = {}; + RollbackRedirect(channel, pipeFds[0], readerOriginal); return false; } return true; @@ -300,20 +266,22 @@ namespace bool restored{true}; if (channel.Original >= 0) { - restored = DuplicateTo(channel.Original, channel.Target) == 0; + restored = OsDuplicateTo(channel.Original, channel.Target) == 0; if (!restored) { - (void)Close(channel.Target); + (void)OsClose(channel.Target); } } else { - restored = Close(channel.Target) == 0; + restored = OsClose(channel.Target) == 0; } + restored = OsOnRestore(channel.Platform, channel.Target) && restored; + if (channel.Original >= 0) { - (void)Close(channel.Original); + (void)OsClose(channel.Original); } if (channel.Reader.joinable()) @@ -324,6 +292,8 @@ namespace } else { + // Detach so Stop can return, but keep the future so Start() + // can refuse a restart until this drain actually finishes. g_outstandingDrains.push_back(std::move(channel.Completion)); channel.Reader.detach(); restored = false; diff --git a/Core/Foundation/Source/StandardStreamLogger_Windows.cpp b/Core/Foundation/Source/StandardStreamLogger_Windows.cpp index 195832e9..e2ff1559 100644 --- a/Core/Foundation/Source/StandardStreamLogger_Windows.cpp +++ b/Core/Foundation/Source/StandardStreamLogger_Windows.cpp @@ -1,17 +1,8 @@ #include "StandardStreamLoggerPlatform.h" -#include #include #include -#include -#include -#include -#include #include -#include -#include -#include -#include #include #include @@ -20,29 +11,6 @@ namespace { - enum class Stream - { - Output, - Error, - }; - - struct Channel - { - int Target{-1}; - int Original{-1}; - DWORD StandardHandle{}; - HANDLE OriginalHandle{INVALID_HANDLE_VALUE}; - bool OriginalHandleUsesTarget{}; - std::future Completion{}; - std::thread Reader{}; - }; - - // Drain futures retained after Stop() times out and detaches the reader. - // Start() reaps these before installing a new redirection. - std::vector> g_outstandingDrains{}; - Channel g_stdout{}; - Channel g_stderr{}; - void IgnoreInvalidParameter( const wchar_t*, const wchar_t*, @@ -52,32 +20,43 @@ namespace { } - int Duplicate(int fd) + // Win32/UWP need to keep GetStdHandle/SetStdHandle in sync with CRT fds. + struct ChannelPlatformState { - return ::_dup(fd); + DWORD StandardHandle{}; + HANDLE OriginalHandle{INVALID_HANDLE_VALUE}; + bool OriginalHandleUsesTarget{}; + }; + + int OsDuplicate(int fd) + { + const auto previousHandler = ::_set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); + const int duplicated = ::_dup(fd); + (void)::_set_thread_local_invalid_parameter_handler(previousHandler); + return duplicated; } - int DuplicateTo(int source, int target) + int OsDuplicateTo(int source, int target) { return ::_dup2(source, target); } - int Close(int fd) + int OsClose(int fd) { return ::_close(fd); } - int64_t Read(int fd, void* data, size_t size) + int64_t OsRead(int fd, void* data, size_t size) { return ::_read(fd, data, static_cast(size)); } - int64_t Write(int fd, const void* data, size_t size) + int64_t OsWrite(int fd, const void* data, size_t size) { return ::_write(fd, data, static_cast(size)); } - int CreatePipe(int fds[2]) + int OsCreatePipe(int fds[2]) { // UWP's CRT does not expose _pipe. CreatePipe + _open_osfhandle works on // desktop Win32 and UWP, and keeps the ends non-inheritable. @@ -111,121 +90,7 @@ namespace return 0; } - 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; - } - - void WritePlatform(Stream /*stream*/, const std::string& line) - { - std::string output{line}; - output.push_back('\n'); - ::OutputDebugStringA(output.c_str()); - } - - bool WriteAll(int fd, const char* data, size_t size) - { - while (size != 0) - { - const auto written = Write(fd, data, size); - if (written > 0) - { - data += written; - size -= static_cast(written); - continue; - } - if (written < 0 && errno == EINTR) - { - continue; - } - return false; - } - return true; - } - - void EmitLine(Stream stream, std::string line) - { - if (!line.empty() && line.back() == '\r') - { - line.pop_back(); - } - WritePlatform(stream, line); - } - - void Drain(int readFd, int originalFd, Stream stream) - { - // Cap mirrored lines below typical platform limits: - // OutputDebugStringA (~4 KiB practical). Leave headroom under 4096. - constexpr size_t MAX_PLATFORM_LINE_SIZE{3800}; - std::array buffer{}; - std::string pending{}; - - for (;;) - { - const auto count = Read(readFd, buffer.data(), buffer.size()); - if (count == 0) - { - break; - } - if (count < 0) - { - if (errno == EINTR) - { - continue; - } - break; - } - - const size_t size = static_cast(count); - if (originalFd >= 0) - { - (void)WriteAll(originalFd, buffer.data(), size); - } - - pending.append(buffer.data(), size); - - // 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); - } - } - - if (!pending.empty()) - { - EmitLine(stream, std::move(pending)); - } - (void)Close(readFd); - if (originalFd >= 0) - { - (void)Close(originalFd); - } - } - - bool OccupyTarget(int target) + bool OsOccupyTarget(int target) { // Prefer the secure CRT form; UWP treats the deprecated _open as an error. int nullFd{-1}; @@ -238,253 +103,63 @@ namespace return true; } - const bool duplicated = DuplicateTo(nullFd, target) == 0; - (void)Close(nullFd); + const bool duplicated = OsDuplicateTo(nullFd, target) == 0; + (void)OsClose(nullFd); return duplicated; } - bool RestoreStandardHandle(const Channel& channel) + void OsWritePlatform(bool /*isError*/, const std::string& line) { - HANDLE handle = channel.OriginalHandle; - if (channel.OriginalHandleUsesTarget) - { - const intptr_t restoredHandle = GetOsHandle(channel.Target); - if (restoredHandle == -1) - { - return false; - } - handle = reinterpret_cast(restoredHandle); - } - return ::SetStdHandle(channel.StandardHandle, handle) != FALSE; + std::string output{line}; + output.push_back('\n'); + ::OutputDebugStringA(output.c_str()); } - bool StartChannel(Channel& channel, int target, Stream stream) + intptr_t GetOsHandle(int fd) { - channel.Target = target; - channel.StandardHandle = stream == Stream::Error ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE; - channel.OriginalHandle = ::GetStdHandle(channel.StandardHandle); - const intptr_t targetHandle = GetOsHandle(target); - channel.OriginalHandleUsesTarget = - targetHandle != -1 && - channel.OriginalHandle != nullptr && - channel.OriginalHandle != INVALID_HANDLE_VALUE && - channel.OriginalHandle == reinterpret_cast(targetHandle); - - errno = 0; const auto previousHandler = ::_set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); - channel.Original = Duplicate(target); + const intptr_t handle = ::_get_osfhandle(fd); (void)::_set_thread_local_invalid_parameter_handler(previousHandler); + return handle; + } - if (channel.Original < 0 && errno != EBADF) - { - channel = {}; - return false; - } - if (channel.Original < 0 && !OccupyTarget(target)) - { - channel = {}; - return false; - } - - int pipeFds[2]{-1, -1}; - if (CreatePipe(pipeFds) != 0) - { - if (channel.Original >= 0) - { - (void)Close(channel.Original); - } - else - { - (void)Close(target); - } - channel = {}; - return false; - } - - if (DuplicateTo(pipeFds[1], target) != 0) - { - (void)Close(pipeFds[0]); - (void)Close(pipeFds[1]); - if (channel.Original >= 0) - { - (void)Close(channel.Original); - } - else - { - (void)Close(target); - } - channel = {}; - return false; - } - (void)Close(pipeFds[1]); - - const intptr_t pipeHandle = GetOsHandle(target); - if (pipeHandle == -1 || !::SetStdHandle(channel.StandardHandle, reinterpret_cast(pipeHandle))) - { - if (channel.Original >= 0) - { - (void)DuplicateTo(channel.Original, target); - (void)Close(channel.Original); - } - else - { - (void)Close(target); - } - (void)RestoreStandardHandle(channel); - (void)Close(pipeFds[0]); - channel = {}; - return false; - } - - int readerOriginal{-1}; - if (channel.Original >= 0) - { - readerOriginal = Duplicate(channel.Original); - if (readerOriginal < 0) - { - (void)DuplicateTo(channel.Original, target); - (void)Close(channel.Original); - (void)RestoreStandardHandle(channel); - (void)Close(pipeFds[0]); - channel = {}; - return false; - } - } - - std::promise completed{}; - channel.Completion = completed.get_future(); - try - { - channel.Reader = std::thread{ - [readFd = pipeFds[0], originalFd = readerOriginal, stream, completed = std::move(completed)]() mutable { - Drain(readFd, originalFd, stream); - completed.set_value(); - }}; - } - catch (const std::system_error&) - { - if (channel.Original >= 0) - { - (void)DuplicateTo(channel.Original, target); - (void)Close(channel.Original); - } - else - { - (void)Close(target); - } - (void)RestoreStandardHandle(channel); - (void)Close(pipeFds[0]); - if (readerOriginal >= 0) - { - (void)Close(readerOriginal); - } - channel = {}; - return false; - } + bool OsOnStartChannel(ChannelPlatformState& state, int target, bool isError) + { + state.StandardHandle = isError ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE; + state.OriginalHandle = ::GetStdHandle(state.StandardHandle); + const intptr_t targetHandle = GetOsHandle(target); + state.OriginalHandleUsesTarget = + targetHandle != -1 && + state.OriginalHandle != nullptr && + state.OriginalHandle != INVALID_HANDLE_VALUE && + state.OriginalHandle == reinterpret_cast(targetHandle); return true; } - bool StopChannel(Channel& channel) + bool OsOnRedirected(ChannelPlatformState& state, int target) { - bool restored{true}; - if (channel.Original >= 0) - { - restored = DuplicateTo(channel.Original, channel.Target) == 0; - if (!restored) - { - (void)Close(channel.Target); - } - } - else - { - restored = Close(channel.Target) == 0; - } - - restored = RestoreStandardHandle(channel) && restored; - - if (channel.Original >= 0) - { - (void)Close(channel.Original); - } - - if (channel.Reader.joinable()) + const intptr_t pipeHandle = GetOsHandle(target); + if (pipeHandle == -1) { - if (channel.Completion.wait_for(std::chrono::seconds{2}) == std::future_status::ready) - { - channel.Reader.join(); - } - else - { - // Detach so Stop can return, but keep the future so Start() - // can refuse a restart until this drain actually finishes. - g_outstandingDrains.push_back(std::move(channel.Completion)); - channel.Reader.detach(); - restored = false; - } + return false; } - channel = {}; - return restored; + return ::SetStdHandle(state.StandardHandle, reinterpret_cast(pipeHandle)) != FALSE; } - bool ReapOutstandingDrains(std::chrono::milliseconds timeout) + bool OsOnRestore(ChannelPlatformState& state, int target) { - const auto deadline = std::chrono::steady_clock::now() + timeout; - while (!g_outstandingDrains.empty()) + HANDLE handle = state.OriginalHandle; + if (state.OriginalHandleUsesTarget) { - auto& front = g_outstandingDrains.front(); - const auto remaining = deadline - std::chrono::steady_clock::now(); - if (remaining <= std::chrono::milliseconds::zero()) - { - if (front.wait_for(std::chrono::milliseconds::zero()) != std::future_status::ready) - { - return false; - } - } - else if (front.wait_for(remaining) != std::future_status::ready) + const intptr_t restoredHandle = GetOsHandle(target); + if (restoredHandle == -1) { return false; } - - g_outstandingDrains.erase(g_outstandingDrains.begin()); + handle = reinterpret_cast(restoredHandle); } - return true; + return ::SetStdHandle(state.StandardHandle, handle) != FALSE; } } -namespace Babylon::StandardStreamLogger::Platform -{ - bool Start() - { - if (!ReapOutstandingDrains(std::chrono::seconds{2})) - { - return false; - } - - std::cout.flush(); - std::cerr.flush(); - std::fflush(stdout); - std::fflush(stderr); - - if (!StartChannel(g_stdout, 1, Stream::Output)) - { - return false; - } - if (!StartChannel(g_stderr, 2, Stream::Error)) - { - (void)StopChannel(g_stdout); - return false; - } - return true; - } - - bool Stop() - { - std::cout.flush(); - std::cerr.flush(); - std::fflush(stdout); - std::fflush(stderr); - bool stopped = StopChannel(g_stdout); - stopped = StopChannel(g_stderr) && stopped; - return stopped; - } -} \ No newline at end of file +#include "StandardStreamLogger_Shared.inl" \ No newline at end of file From 60c6b5380b570a3b4da1c37c09f24ed0b7ce4206 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Fri, 28 Aug 2026 16:45:30 -0700 Subject: [PATCH 7/7] Clarify StandardStreamLogger .inl CMake listing Android/Apple already #include PosixOps.inl; the CMake entries are IDE-only. Mark .inl files HEADER_FILE_ONLY and document that so they are not mistaken for separate translation units. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09 --- Core/Foundation/CMakeLists.txt | 21 ++++++++++++------- .../Source/StandardStreamLogger_Android.cpp | 1 + .../Source/StandardStreamLogger_Apple.cpp | 1 + 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/Core/Foundation/CMakeLists.txt b/Core/Foundation/CMakeLists.txt index 6b7f7ff1..7e4eac35 100644 --- a/Core/Foundation/CMakeLists.txt +++ b/Core/Foundation/CMakeLists.txt @@ -6,7 +6,11 @@ set(SOURCES "Source/DebugTrace.cpp" "Source/PerfTrace.cpp" "Source/StandardStreamLogger.cpp" - "Source/StandardStreamLoggerPlatform.h" + "Source/StandardStreamLoggerPlatform.h") + +# .inl bodies are #include'd by the platform TUs (not separate translation units). +# List them in SOURCES only so IDEs/source_group show them next to the .cpp files. +set(STREAM_LOGGER_INCLUDES "Source/StandardStreamLogger_Shared.inl") # Shared tee body (StandardStreamLogger_Shared.inl) + thin OS-ops platform TUs. @@ -14,19 +18,22 @@ set(SOURCES if(JSRUNTIMEHOST_PLATFORM STREQUAL "Win32" OR JSRUNTIMEHOST_PLATFORM STREQUAL "UWP") list(APPEND SOURCES "Source/StandardStreamLogger_Windows.cpp") elseif(JSRUNTIMEHOST_PLATFORM STREQUAL "Android") - list(APPEND SOURCES - "Source/StandardStreamLogger_Android.cpp" - "Source/StandardStreamLogger_PosixOps.inl") + list(APPEND SOURCES "Source/StandardStreamLogger_Android.cpp") + list(APPEND STREAM_LOGGER_INCLUDES "Source/StandardStreamLogger_PosixOps.inl") elseif(JSRUNTIMEHOST_PLATFORM STREQUAL "iOS" OR JSRUNTIMEHOST_PLATFORM STREQUAL "macOS") - list(APPEND SOURCES - "Source/StandardStreamLogger_Apple.cpp" - "Source/StandardStreamLogger_PosixOps.inl") + list(APPEND SOURCES "Source/StandardStreamLogger_Apple.cpp") + list(APPEND STREAM_LOGGER_INCLUDES "Source/StandardStreamLogger_PosixOps.inl") else() list(APPEND SOURCES "Source/StandardStreamLogger_Unix.cpp") endif() +list(APPEND SOURCES ${STREAM_LOGGER_INCLUDES}) + add_library(Foundation ${SOURCES}) +# Ensure generators never try to compile the .inl include bodies on their own. +set_source_files_properties(${STREAM_LOGGER_INCLUDES} PROPERTIES HEADER_FILE_ONLY TRUE) + target_include_directories(Foundation PRIVATE "Include/Babylon" PRIVATE "Source" diff --git a/Core/Foundation/Source/StandardStreamLogger_Android.cpp b/Core/Foundation/Source/StandardStreamLogger_Android.cpp index 110c6d7c..ef1d0052 100644 --- a/Core/Foundation/Source/StandardStreamLogger_Android.cpp +++ b/Core/Foundation/Source/StandardStreamLogger_Android.cpp @@ -10,6 +10,7 @@ namespace { +// POSIX fd helpers (dup/pipe/CLOEXEC/devnull); sink is OsWritePlatform below. #include "StandardStreamLogger_PosixOps.inl" void OsWritePlatform(bool isError, const std::string& line) diff --git a/Core/Foundation/Source/StandardStreamLogger_Apple.cpp b/Core/Foundation/Source/StandardStreamLogger_Apple.cpp index 2fbe82e2..5c514d88 100644 --- a/Core/Foundation/Source/StandardStreamLogger_Apple.cpp +++ b/Core/Foundation/Source/StandardStreamLogger_Apple.cpp @@ -10,6 +10,7 @@ namespace { +// POSIX fd helpers (dup/pipe/CLOEXEC/devnull); sink is OsWritePlatform below. #include "StandardStreamLogger_PosixOps.inl" void OsWritePlatform(bool isError, const std::string& line)