diff --git a/NativeScript/runtime/Console.cpp b/NativeScript/runtime/Console.cpp index ef8a3b58..3047b1e6 100644 --- a/NativeScript/runtime/Console.cpp +++ b/NativeScript/runtime/Console.cpp @@ -25,6 +25,19 @@ namespace { struct ConsoleTimersState { robin_hood::unordered_map 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) { @@ -117,9 +130,8 @@ void Console::LogCallback(const FunctionCallbackInfo& 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) { @@ -158,7 +170,7 @@ void Console::AssertCallback(const FunctionCallbackInfo& args) { std::string log = ss.str(); SendToDevToolsFrontEnd(ConsoleAPIType::kAssert, args); - Log("%s", log.c_str()); + LogConsoleMessage(true, "", log); } } diff --git a/NativeScript/runtime/Helpers.h b/NativeScript/runtime/Helpers.h index d4fd42b8..e30d3b90 100644 --- a/NativeScript/runtime/Helpers.h +++ b/NativeScript/runtime/Helpers.h @@ -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 ToV8String(v8::Isolate* isolate, const std::string& value) { @@ -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(first.size()), first.data(), static_cast(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, ...) { @@ -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 @@ -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__ @@ -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__ diff --git a/NativeScript/runtime/Helpers.mm b/NativeScript/runtime/Helpers.mm index 41d5361b..e95f2d80 100644 --- a/NativeScript/runtime/Helpers.mm +++ b/NativeScript/runtime/Helpers.mm @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -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 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); diff --git a/NativeScript/runtime/LogChunks.h b/NativeScript/runtime/LogChunks.h new file mode 100644 index 00000000..4e8e8906 --- /dev/null +++ b/NativeScript/runtime/LogChunks.h @@ -0,0 +1,144 @@ +#ifndef LogChunks_h +#define LogChunks_h + +#include +#include +#include +#include +#include +#include + +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 +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(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 /] " +// followed by the piece. is unique per message in the process (hex) and +// 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(length) < sizeof(buffer) + ? static_cast(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 +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(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 +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 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 */ diff --git a/v8ios.xcodeproj/project.pbxproj b/v8ios.xcodeproj/project.pbxproj index 40102693..7034da3d 100644 --- a/v8ios.xcodeproj/project.pbxproj +++ b/v8ios.xcodeproj/project.pbxproj @@ -818,6 +818,7 @@ C2DDEB6A229EAC8200345BFE /* ArgConverter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ArgConverter.mm; sourceTree = ""; }; C2DDEB6B229EAC8200345BFE /* Console.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Console.cpp; sourceTree = ""; }; C2DDEB6D229EAC8200345BFE /* Helpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Helpers.h; sourceTree = ""; }; + A1C0DE5100000000000000A1 /* LogChunks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LogChunks.h; sourceTree = ""; }; C2DDEB6E229EAC8200345BFE /* Metadata.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Metadata.h; sourceTree = ""; }; C2DDEB6F229EAC8200345BFE /* ArrayAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ArrayAdapter.h; sourceTree = ""; }; C2DDEB70229EAC8200345BFE /* Metadata.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = Metadata.mm; sourceTree = ""; }; @@ -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 */,