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
9 changes: 9 additions & 0 deletions Core/Foundation/Include/Babylon/StandardStreamLogger.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ namespace Babylon::StandardStreamLogger
* forwards to OutputDebugString while preserving the original stream destination.
* Other Unix platforms already expose standard streams and leave them unchanged.
*
* Private descriptors are non-inheritable, and the original standard-stream
* inheritance flags are preserved. Applications must serialize concurrent
* child-process creation with Start()/Stop(): redirection and flag restoration
* are not a single atomic operation. Apple additionally 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.
*
* Returns false if a platform stream could not be redirected. Repeated calls are
* idempotent.
*/
Expand Down
58 changes: 58 additions & 0 deletions Core/Foundation/Source/StandardStreamLoggerLines.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#pragma once

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

namespace Babylon::StandardStreamLogger::Detail
{
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
27 changes: 19 additions & 8 deletions Core/Foundation/Source/StandardStreamLogger_PosixOps.inl
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

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)
Expand All @@ -32,6 +34,11 @@ int64_t OsWrite(int fd, const void* data, size_t 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 +55,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 +75,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;
}
50 changes: 9 additions & 41 deletions Core/Foundation/Source/StandardStreamLogger_Shared.inl
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
// int64_t OsWrite(int fd, const void* data, size_t size);
// int OsCreatePipe(int fds[2]);
// bool OsOccupyTarget(int target);
// size_t OsMaxPlatformLineSize(bool isError);
// void OsWritePlatform(bool isError, const std::string& line);
// bool OsOnStartChannel(ChannelPlatformState& state, int target, bool isError);
// bool OsOnRedirected(ChannelPlatformState& state, int target);
// bool OsOnRestore(ChannelPlatformState& state, int target);

#include "StandardStreamLoggerLines.h"

#include <array>
#include <cerrno>
#include <cstdint>
Expand Down Expand Up @@ -70,20 +73,12 @@ namespace
return true;
}

void EmitLine(Stream stream, std::string line)
{
if (!line.empty() && line.back() == '\r')
{
line.pop_back();
}
OsWritePlatform(stream == Stream::Error, line);
}

void Drain(int readFd, int originalFd, Stream stream)
{
// Cap mirrored lines below typical platform limits (~4 KiB for
// OutputDebugStringA / logcat / os_log). Leave headroom under 4096.
constexpr size_t MAX_PLATFORM_LINE_SIZE{3800};
const size_t maxLineSize = OsMaxPlatformLineSize(stream == Stream::Error);
const auto emit = [stream](const std::string& line) {
OsWritePlatform(stream == Stream::Error, line);
};
std::array<char, 1024> buffer{};
std::string pending{};

Expand Down Expand Up @@ -111,37 +106,10 @@ 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 (;;)
{
const size_t newline = pending.find('\n', start);
if (newline != std::string::npos)
{
EmitLine(stream, pending.substr(start, newline - start));
start = newline + 1;
}
else if (pending.size() - start >= MAX_PLATFORM_LINE_SIZE)
{
EmitLine(stream, pending.substr(start, MAX_PLATFORM_LINE_SIZE));
start += MAX_PLATFORM_LINE_SIZE;
}
else
{
break;
}
}
if (start != 0)
{
pending.erase(0, start);
}
Babylon::StandardStreamLogger::Detail::EmitPendingLines(pending, maxLineSize, false, emit);
}

if (!pending.empty())
{
EmitLine(stream, std::move(pending));
}
Babylon::StandardStreamLogger::Detail::EmitPendingLines(pending, maxLineSize, true, emit);
(void)OsClose(readFd);
if (originalFd >= 0)
{
Expand Down
Loading
Loading