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
20 changes: 16 additions & 4 deletions NativeScript/runtime/Console.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ namespace {
struct ConsoleTimersState {
robin_hood::unordered_map<std::string, double> startedAt;
};

// Errors get the error log type so log readers can tell severity apart. The
// default handle is kept on purpose: a subsystem would show up in the text of
// syslog-style output, which older CLIs parse.
void LogConsoleMessage(bool isError, const std::string& prefix,
const std::string& message) {
#if TNS_HAVE_OS_LOG
TNS_WriteToOsLog(message.c_str(),
isError ? OS_LOG_TYPE_ERROR : OS_LOG_TYPE_DEFAULT, prefix);
#else
Log("%s%s", prefix.c_str(), message.c_str());
#endif
}
} // namespace

void Console::Init(Local<Context> context) {
Expand Down Expand Up @@ -117,9 +130,8 @@ void Console::LogCallback(const FunctionCallbackInfo<Value>& args) {

ConsoleAPIType method = VerbosityToInspectorMethod(verbosityLevel);
SendToDevToolsFrontEnd(method, args);
std::string msgWithVerbosity =
"CONSOLE " + verbosityLevelUpper + ": " + msgToLog;
Log("%s", msgWithVerbosity.c_str());
LogConsoleMessage(verbosityLevel == "error",
"CONSOLE " + verbosityLevelUpper + ": ", msgToLog);

if (RuntimeConfig.IsDebug && Runtime::showErrorDisplay() &&
verbosityLevel == "error" && hasStackTrace) {
Expand Down Expand Up @@ -158,7 +170,7 @@ void Console::AssertCallback(const FunctionCallbackInfo<Value>& args) {
std::string log = ss.str();

SendToDevToolsFrontEnd(ConsoleAPIType::kAssert, args);
Log("%s", log.c_str());
LogConsoleMessage(true, "", log);
}
}

Expand Down
43 changes: 40 additions & 3 deletions NativeScript/runtime/Helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ extern "C" void NSLog(CFStringRef format, ...);
#define TNS_HAVE_OS_LOG 0
#endif

#include "LogChunks.h"

namespace tns {

inline v8::Local<v8::String> ToV8String(v8::Isolate* isolate, const std::string& value) {
Expand Down Expand Up @@ -353,6 +355,41 @@ void LogBacktrace(int skip = 1);
// "%{public}s" argument to os_log. When os_log is not available we
// fall back to NSLog (Objective-C) or CF/NSLog bridge in pure C++ builds.

// The log format (see LogChunks.h) the CLI asked for, or 0 for plain split lines.
int LogFormat();

// Unique per split message in the process, for the headers of format 1.
uint32_t NextLogMessageId();

#if TNS_HAVE_OS_LOG
// Logs the two parts as one entry.
static inline void TNS_WriteOsLogEntry(os_log_type_t type, std::string_view first,
std::string_view second) {
os_log_with_type(OS_LOG_DEFAULT, type, "%{public}.*s%{public}.*s",
static_cast<int>(first.size()), first.data(), static_cast<int>(second.size()),
second.data());
}

static inline void TNS_WriteToOsLog(const char* message, os_log_type_t type = OS_LOG_TYPE_DEFAULT,
std::string_view prefix = "") {
std::string_view text(message);
if (prefix.size() + text.size() <= kMaxLogChunkBytes) {
TNS_WriteOsLogEntry(type, prefix, text);
} else if (LogFormat() >= 1) {
ForEachFramedLogChunk(prefix, text, NextLogMessageId(),
[&](std::string_view header, std::string_view piece) {
TNS_WriteOsLogEntry(type, header, piece);
});
} else {
// Repeated on every piece, so that each stays a complete log line.
ForEachLogChunk(text, kMaxLogChunkBytes - prefix.size(),
[&](std::string_view chunk, LogChunkBreak) {
TNS_WriteOsLogEntry(type, prefix, chunk);
});
}
}
#endif

#ifdef __OBJC__
// Overload for Objective-C string literals (NSString*)
static inline void TNS_FormatAndLog(NSString* fmt, ...) {
Expand All @@ -373,7 +410,7 @@ static inline void TNS_FormatAndLog(NSString* fmt, ...) {
const char* cStr = [formattedString UTF8String];
if (cStr) {
#if TNS_HAVE_OS_LOG
os_log(OS_LOG_DEFAULT, "%{public}s", cStr);
TNS_WriteToOsLog(cStr);
#else
NSLog(@"%s", cStr);
#endif
Expand Down Expand Up @@ -406,7 +443,7 @@ static inline void TNS_FormatAndLog(const char* fmt, ...) {
if (needed < STACK_BUF_SIZE) {
// Message fit into stack buffer — single formatting pass.
#if TNS_HAVE_OS_LOG
os_log(OS_LOG_DEFAULT, "%{public}s", stack_buf);
TNS_WriteToOsLog(stack_buf);
#else
// Fall back to NSLog. Use Objective-C API when compiling as ObjC++.
#ifdef __OBJC__
Expand All @@ -421,7 +458,7 @@ static inline void TNS_FormatAndLog(const char* fmt, ...) {
vsnprintf(buffer.data(), buffer.size(), fmt, ap);

#if TNS_HAVE_OS_LOG
os_log(OS_LOG_DEFAULT, "%{public}s", buffer.data());
TNS_WriteToOsLog(buffer.data());
#else
// Fall back to NSLog. Use Objective-C API when compiling as ObjC++.
#ifdef __OBJC__
Expand Down
30 changes: 30 additions & 0 deletions NativeScript/runtime/Helpers.mm
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include <algorithm>
#include <atomic>
#include <cerrno>
#include <mutex>
Expand Down Expand Up @@ -493,6 +494,35 @@ void WriteDebugLine(tns::LogCategory category, const char* message) {
}
}

static bool IsDevNull(int fd) {
struct stat target;
struct stat devNull;
return fstat(fd, &target) == 0 && stat("/dev/null", &devNull) == 0 &&
S_ISCHR(target.st_mode) && target.st_rdev == devNull.st_rdev;
}

// A console attached to the app would show the headers, so they also need stdout
// to go nowhere, as it does when the CLI launches the app.
int tns::LogFormat() {
if (RuntimeConfig.ApplicationPath.empty()) {
return 0;
}

static const int format = [] {
id value = Runtime::GetAppConfigValue("logFormat");
if (![value isKindOfClass:[NSNumber class]] || !IsDevNull(STDOUT_FILENO)) {
return 0;
}
return std::clamp([value intValue], 0, kNewestLogFormat);
}();
return format;
}

uint32_t tns::NextLogMessageId() {
static std::atomic<uint32_t> lastMessageId{0};
return lastMessageId.fetch_add(1, std::memory_order_relaxed) + 1;
}

void tns::EmitDebugLog(LogCategory category, const char* format, ...) {
va_list ap;
va_start(ap, format);
Expand Down
144 changes: 144 additions & 0 deletions NativeScript/runtime/LogChunks.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#ifndef LogChunks_h
#define LogChunks_h

#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <string>
#include <string_view>
#include <vector>

namespace tns {

// os_log replaces whatever exceeds its buffer with "<…>". The buffer also holds
// a few bytes of bookkeeping per argument, which left 996 bytes of text with two
// arguments on iPadOS 26.7, so stay well below that.
constexpr size_t kMaxLogChunkBytes = 900;

// How a chunk connects to the one after it.
enum class LogChunkBreak { kNone, kJoin, kLine };

// Calls emit with consecutive pieces of message, each at most maxBytes long and
// never ending inside a UTF-8 sequence. A piece ends at the last line break that
// fits, so that lines stay intact whenever they can; that line break is dropped,
// since every piece is logged as a line of its own.
template <typename Emit>
void ForEachLogChunk(std::string_view message, size_t maxBytes, Emit&& emit) {
while (message.size() > maxBytes) {
size_t end = message.rfind('\n', maxBytes);
size_t next = end + 1;
LogChunkBreak chunkBreak = LogChunkBreak::kLine;

if (end == std::string_view::npos || end == 0) {
end = maxBytes;
while (end > 0 && (static_cast<unsigned char>(message[end]) & 0xC0) == 0x80) {
end--;
}
// Not UTF-8 at all: any cut is as good as another.
if (end == 0) {
end = maxBytes;
}
next = end;
chunkBreak = LogChunkBreak::kJoin;
}

emit(message.substr(0, end), chunkBreak);
message.remove_prefix(next);
}

emit(message, LogChunkBreak::kNone);
}

// The newest value of the logFormat key in the app's package.json that the
// runtime understands. The CLI writes the newest one it understands.
//
// Format 1 logs each piece of a split message as "[ns <id> <index>/<count>] "
// followed by the piece. <id> is unique per message in the process (hex) and
// <index> counts from 1. The pieces are consecutive slices of the message and
// only the first carries the message's prefix, so they join back into the
// message as they are. Messages that fit in one entry are logged without a
// header.
constexpr int kNewestLogFormat = 1;

// Room for the longest header: ids up to 8 hex digits and counts up to 7
// decimal ones (messages of several GB).
constexpr size_t kMaxLogChunkHeaderBytes = 40;

inline std::string_view FormatLogChunkHeader(char (&buffer)[kMaxLogChunkHeaderBytes], uint32_t id,
size_t index, size_t count) {
int length = snprintf(buffer, sizeof(buffer), "[ns %x %zu/%zu] ", id, index, count);
if (length < 0) {
return {};
}
return std::string_view(buffer, static_cast<size_t>(length) < sizeof(buffer)
? static_cast<size_t>(length)
: sizeof(buffer) - 1);
}

inline bool IsLogWhitespace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f';
}

// Calls emit with consecutive slices of text, each at most maxBytes long and
// never ending inside a UTF-8 sequence. The unified log strips whitespace from
// the end of every entry, so a slice never ends in whitespace: it is left for
// the start of the next slice, behind that slice's header. Only a run of
// whitespace longer than maxBytes still loses what it ends with. A slice ends
// before the last line break that fits, so that lines stay intact whenever they
// can.
template <typename Emit>
void ForEachLogSlice(std::string_view text, size_t maxBytes, Emit&& emit) {
auto backOverWhitespace = [&](size_t end) {
while (end > 0 && IsLogWhitespace(text[end - 1])) {
end--;
}
return end;
};

while (text.size() > maxBytes) {
size_t lineBreak = text.rfind('\n', maxBytes);
size_t end = lineBreak == std::string_view::npos ? 0 : backOverWhitespace(lineBreak);

if (end == 0) {
end = maxBytes;
while (end > 0 && (static_cast<unsigned char>(text[end]) & 0xC0) == 0x80) {
end--;
}
// Not UTF-8 at all: any cut is as good as another.
if (end == 0) {
end = maxBytes;
}
if (size_t beforeWhitespace = backOverWhitespace(end)) {
end = beforeWhitespace;
}
}

emit(text.substr(0, end));
text.remove_prefix(end);
}

emit(text);
}

// Calls emit with the header and text of each piece of prefix + message, split
// in format 1 as the message with the given id.
template <typename Emit>
void ForEachFramedLogChunk(std::string_view prefix, std::string_view message, uint32_t id,
Emit&& emit) {
std::string text;
text.reserve(prefix.size() + message.size());
text.append(prefix).append(message);

std::vector<std::string_view> pieces;
ForEachLogSlice(text, kMaxLogChunkBytes - kMaxLogChunkHeaderBytes,
[&](std::string_view piece) { pieces.push_back(piece); });

for (size_t i = 0; i < pieces.size(); i++) {
char buffer[kMaxLogChunkHeaderBytes];
emit(FormatLogChunkHeader(buffer, id, i + 1, pieces.size()), pieces[i]);
}
}

} // namespace tns

#endif /* LogChunks_h */
2 changes: 2 additions & 0 deletions v8ios.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,7 @@
C2DDEB6A229EAC8200345BFE /* ArgConverter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ArgConverter.mm; sourceTree = "<group>"; };
C2DDEB6B229EAC8200345BFE /* Console.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Console.cpp; sourceTree = "<group>"; };
C2DDEB6D229EAC8200345BFE /* Helpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Helpers.h; sourceTree = "<group>"; };
A1C0DE5100000000000000A1 /* LogChunks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LogChunks.h; sourceTree = "<group>"; };
C2DDEB6E229EAC8200345BFE /* Metadata.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Metadata.h; sourceTree = "<group>"; };
C2DDEB6F229EAC8200345BFE /* ArrayAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ArrayAdapter.h; sourceTree = "<group>"; };
C2DDEB70229EAC8200345BFE /* Metadata.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = Metadata.mm; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1497,6 +1498,7 @@
C266569D22B282BA00EE15CC /* FunctionReference.h */,
C266569C22B282BA00EE15CC /* FunctionReference.cpp */,
C2DDEB6D229EAC8200345BFE /* Helpers.h */,
A1C0DE5100000000000000A1 /* LogChunks.h */,
C2DDEB7D229EAC8200345BFE /* Helpers.mm */,
AA5DBFD12EC191F8008D12F9 /* HttpLoader.h */,
AA5DBFD22EC191F8008D12F9 /* HttpLoader.mm */,
Expand Down
Loading