Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Core/Foundation/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ set(SOURCES
"Source/DelayedTaskScheduler.h"
"Source/PerfTrace.cpp"
"Source/StandardStreamLogger.cpp"
"Source/StandardStreamLoggerLines.h"
"Source/StandardStreamLoggerPlatform.h")

# .inl bodies are #include'd by the platform TUs (not separate translation units).
Expand Down
19 changes: 19 additions & 0 deletions Core/Foundation/Include/Babylon/StandardStreamLogger.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,25 @@ namespace Babylon::StandardStreamLogger
* forwards to OutputDebugString while preserving the original stream destination.
* Other Unix platforms already expose standard streams and leave them unchanged.
*
* Persistent private descriptors are non-inheritable, and the original standard-stream
* inheritance flags are preserved (including the Windows CRT no-inherit state
* when the original handle is non-inheritable). Applications must serialize
* concurrent child-process creation with Start()/Stop(): redirection and flag
* restoration are not a single atomic operation. Windows also uses a temporary
* CRT mode probe and reopens non-inheritable standard descriptors; serialize
* concurrent CRT descriptor allocation with Start()/Stop() as well. Apple
* lacks atomic close-on-exec pipe creation.
*
* Platform diagnostics split long lines to fit their sink's size limit.
* Chunking does not affect the tee to the original stream destination.
* Windows preserves CRT translation modes for targets with valid handles. Already-translated
* pipe bytes are teed without another translation; UTF-16 diagnostics are
* decoded to UTF-8 before line splitting (malformed input uses U+FFFD).
*
* Other Windows CRT descriptor flags, including _O_APPEND, are not retained
* in the private copies or restored target descriptors. Applications requiring
* CRT append semantics must not use Windows forwarding.
*
* Returns false if a platform stream could not be redirected. Repeated calls are
* idempotent.
*/
Expand Down
131 changes: 131 additions & 0 deletions Core/Foundation/Source/StandardStreamLoggerLines.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#pragma once

#include <cassert>
#include <cstddef>
#include <cstdint>
#include <string>

namespace Babylon::StandardStreamLogger::Detail
{
// Windows UTF-16 text descriptors still tee their original bytes; only the
// diagnostic copy is decoded. Retain incomplete code units/pairs between reads.
inline void AppendUtf16LE(std::string& output, std::string& pending, bool flush)
{
const auto codeUnit = [&pending](size_t offset) -> uint32_t {
return static_cast<unsigned char>(pending[offset]) |
(static_cast<uint32_t>(static_cast<unsigned char>(pending[offset + 1])) << 8);
};
size_t offset{};
while (offset + 1 < pending.size())
{
uint32_t value = codeUnit(offset);
size_t consumed{2};
if (value >= 0xD800 && value <= 0xDBFF)
{
if (offset + 3 >= pending.size())
{
if (!flush)
{
break;
}
value = 0xFFFD;
consumed = pending.size() - offset;
}
else if (codeUnit(offset + 2) >= 0xDC00 && codeUnit(offset + 2) <= 0xDFFF)
{
value = 0x10000 + ((value - 0xD800) << 10) + (codeUnit(offset + 2) - 0xDC00);
consumed = 4;
}
else
{
value = 0xFFFD;
}
}
else if (value >= 0xDC00 && value <= 0xDFFF)
{
value = 0xFFFD;
}
offset += consumed;

if (value <= 0x7F)
{
output.push_back(static_cast<char>(value));
}
else if (value <= 0x7FF)
{
output.push_back(static_cast<char>(0xC0 | (value >> 6)));
output.push_back(static_cast<char>(0x80 | (value & 0x3F)));
}
else
{
if (value > 0xFFFF)
{
output.push_back(static_cast<char>(0xF0 | (value >> 18)));
output.push_back(static_cast<char>(0x80 | ((value >> 12) & 0x3F)));
}
else
{
output.push_back(static_cast<char>(0xE0 | (value >> 12)));
}
output.push_back(static_cast<char>(0x80 | ((value >> 6) & 0x3F)));
output.push_back(static_cast<char>(0x80 | (value & 0x3F)));
}
}
if (flush && offset < pending.size())
{
output.append("\xEF\xBF\xBD");
offset = pending.size();
}
pending.erase(0, offset);
}

template<typename Emit>
void EmitPendingLines(std::string& pending, size_t maxLineSize, bool flush, Emit&& emit)
{
assert(maxLineSize >= 4);
size_t start{};
while (start < pending.size())
{
const size_t newline = pending.find('\n', start);
const size_t end = newline == std::string::npos ? pending.size() : newline;
size_t size = end - start;
if (size != 0 && pending[end - 1] == '\r')
{
if (newline != std::string::npos || flush)
{
--size;
}
else if (size == maxLineSize + 1)
{
// The next read may complete CRLF after an exactly full line.
break;
}
}
if (size > maxLineSize)
{
size = maxLineSize;
while (size != 0 && (static_cast<unsigned char>(pending[start + size]) & 0xC0) == 0x80)
{
--size;
}
// Standard streams can contain invalid UTF-8; still make progress.
if (size == 0)
{
size = maxLineSize;
}
emit(pending.substr(start, size));
start += size;
}
else if (newline != std::string::npos || flush)
{
emit(pending.substr(start, size));
start = end + (newline != std::string::npos ? 1 : 0);
}
else
{
break;
}
}
pending.erase(0, start);
}
}
5 changes: 5 additions & 0 deletions Core/Foundation/Source/StandardStreamLogger_Android.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ namespace
// POSIX fd helpers (dup/pipe/CLOEXEC/devnull); sink is OsWritePlatform below.
#include "StandardStreamLogger_PosixOps.inl"

size_t OsMaxPlatformLineSize(bool /*isError*/)
{
return 3800;
}

void OsWritePlatform(bool isError, const std::string& line)
{
const int priority = isError ? ANDROID_LOG_ERROR : ANDROID_LOG_INFO;
Expand Down
6 changes: 6 additions & 0 deletions Core/Foundation/Source/StandardStreamLogger_Apple.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ namespace
// POSIX fd helpers (dup/pipe/CLOEXEC/devnull); sink is OsWritePlatform below.
#include "StandardStreamLogger_PosixOps.inl"

size_t OsMaxPlatformLineSize(bool isError)
{
// Reserve the terminator within os_log's persisted dynamic-content budget.
return isError ? 255 : 1023;
}

void OsWritePlatform(bool isError, const std::string& line)
{
const os_log_type_t type = isError ? OS_LOG_TYPE_ERROR : OS_LOG_TYPE_DEFAULT;
Expand Down
34 changes: 25 additions & 9 deletions Core/Foundation/Source/StandardStreamLogger_PosixOps.inl
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@

struct ChannelPlatformState
{
int OriginalDescriptorFlags{};
bool OriginalDescriptorOpen{};
};

int OsDuplicate(int fd)
{
return ::dup(fd);
return ::fcntl(fd, F_DUPFD_CLOEXEC, 0);
}

int OsDuplicateTo(int source, int target)
int OsDuplicateTo(int source, int target, const ChannelPlatformState* /*state*/ = nullptr)
{
return ::dup2(source, target) < 0 ? -1 : 0;
}
Expand All @@ -30,8 +32,18 @@ int64_t OsWrite(int fd, const void* data, size_t size)
return ::write(fd, data, size);
}

void OsAppendPlatformBytes(ChannelPlatformState&, std::string& pending, const char* data, size_t size, bool)
{
pending.append(data, size);
}

int OsCreatePipe(int fds[2])
{
#if defined(__ANDROID__)
return ::pipe2(fds, O_CLOEXEC);
#else
// The portable fallback requires callers to serialize Start() with
// fork/exec to avoid inheritance between pipe() and fcntl().
if (::pipe(fds) != 0)
{
return -1;
Expand All @@ -48,11 +60,12 @@ int OsCreatePipe(int fds[2])
return -1;
}
return 0;
#endif
}

bool OsOccupyTarget(int target)
{
const int nullFd = ::open("/dev/null", O_WRONLY);
const int nullFd = ::open("/dev/null", O_WRONLY | O_CLOEXEC);
if (nullFd < 0)
{
return false;
Expand All @@ -67,17 +80,20 @@ bool OsOccupyTarget(int target)
return duplicated;
}

bool OsOnStartChannel(ChannelPlatformState&, int, bool)
bool OsOnStartChannel(ChannelPlatformState& state, int target, bool)
{
return true;
state.OriginalDescriptorFlags = ::fcntl(target, F_GETFD);
state.OriginalDescriptorOpen = state.OriginalDescriptorFlags >= 0;
return state.OriginalDescriptorOpen || errno == EBADF;
}

bool OsOnRedirected(ChannelPlatformState&, int)
bool OsOnRedirected(ChannelPlatformState& state, int target)
{
return true;
const int flags = state.OriginalDescriptorOpen ? state.OriginalDescriptorFlags : FD_CLOEXEC;
return ::fcntl(target, F_SETFD, flags) == 0;
}

bool OsOnRestore(ChannelPlatformState&, int)
bool OsOnRestore(ChannelPlatformState& state, int target)
{
return true;
return !state.OriginalDescriptorOpen || ::fcntl(target, F_SETFD, state.OriginalDescriptorFlags) == 0;
}
Loading
Loading