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..0eddc294 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,27 @@ 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. +// Whether split messages should carry markers for the CLI to stitch them with. +bool ShouldMarkLogChunks(); + +#if TNS_HAVE_OS_LOG +// The prefix is repeated on every chunk, so that each stays a complete log line. +static inline void TNS_WriteToOsLog(const char* message, os_log_type_t type = OS_LOG_TYPE_DEFAULT, + std::string_view prefix = "") { + ForEachLogChunk(message, kMaxLogChunkBytes - prefix.size(), + [&](std::string_view chunk, LogChunkBreak chunkBreak) { + const char* marker = ""; + if (chunkBreak != LogChunkBreak::kNone && ShouldMarkLogChunks()) { + marker = chunkBreak == LogChunkBreak::kLine ? kLogChunkLineMarker + : kLogChunkJoinMarker; + } + os_log_with_type(OS_LOG_DEFAULT, type, "%{public}.*s%{public}.*s%{public}s", + static_cast(prefix.size()), prefix.data(), + static_cast(chunk.size()), chunk.data(), marker); + }); +} +#endif + #ifdef __OBJC__ // Overload for Objective-C string literals (NSString*) static inline void TNS_FormatAndLog(NSString* fmt, ...) { @@ -373,7 +396,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 +429,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 +444,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..e9bf22ad 100644 --- a/NativeScript/runtime/Helpers.mm +++ b/NativeScript/runtime/Helpers.mm @@ -493,6 +493,28 @@ 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; +} + +// Syslog-style output shows the markers as visible text and an attached console +// shows them as they are, so they need a CLI that asked for them and no console. +bool tns::ShouldMarkLogChunks() { + if (RuntimeConfig.ApplicationPath.empty()) { + return false; + } + + static const bool enabled = [] { + id value = Runtime::GetAppConfigValue("logChunkMarkers"); + return [value isKindOfClass:[NSNumber class]] && [value boolValue] && + IsDevNull(STDOUT_FILENO); + }(); + return enabled; +} + 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..54c772f7 --- /dev/null +++ b/NativeScript/runtime/LogChunks.h @@ -0,0 +1,55 @@ +#ifndef LogChunks_h +#define LogChunks_h + +#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 }; + +// Invisible, so that the chunks read normally wherever they are not stitched. +// Control characters would not do: os_log escapes them. +constexpr const char* kLogChunkJoinMarker = "\u2060"; +constexpr const char* kLogChunkLineMarker = "\u2061"; + +// 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); +} + +} // 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 */,