/* * LoggerClient.m * * version 1.5-RC2 22-NOV-2013 * * Main implementation of the NSLogger client side code * Part of NSLogger (client side) * https://github.com/fpillet/NSLogger * * BSD license follows (http://www.opensource.org/licenses/bsd-license.php) * * Copyright (c) 2010-2013 Florent Pillet All Rights Reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. Redistributions in * binary form must reproduce the above copyright notice, this list of * conditions and the following disclaimer in the documentation and/or other * materials provided with the distribution. Neither the name of Florent * Pillet nor the names of its contributors may be used to endorse or promote * products derived from this software without specific prior written * permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * */ #import #import #import #import "LoggerClient.h" #import "LoggerCommon.h" #if !TARGET_OS_IPHONE #import #import #import #import #elif ALLOW_COCOA_USE #import #endif #import /* -------------------------------------------------------------------------------- * IMPLEMENTATION NOTES: * * The logger runs in a separate thread. It is written * in straight C for maximum compatibility with all runtime environments * (does not use the Objective-C runtime, only uses unix and CoreFoundation * calls, except for get the thread name and device information, but these * can be disabled by setting ALLOW_COCOA_USE to 0). * * It is suitable for use in both Cocoa and low-level code. It does not activate * Cocoa multi-threading (no call to [NSThread detachNewThread...]). You can start * logging very early (as soon as your code starts running), logs will be * buffered and sent to the log viewer as soon as a connection is acquired. * This makes the logger suitable for use in conditions where you usually * don't have a connection to a remote machine yet (early wakeup, network * down, etc). * * When you call one of the public logging functions, the logger is designed * to return to your application as fast as possible. It enqueues logs to * send for processing by its own thread, while your application keeps running. * * The logger does buffer logs while not connected to a desktop * logger. It uses Bonjour to find a logger on the local network, and can * optionally connect to a remote logger identified by an IP address / port * or a Host Name / port. * * The logger can optionally output its logs to the console, like NSLog(). * * The logger can optionally buffer its logs to a file for which you specify the * full path. Upon connection to the desktop viewer, the file contents are * transmitted to the viewer prior to sending new logs. When the whole file * content has been transmitted, it is emptied. * * Multiple loggers can coexist at the same time. You can perfectly use a * logger for your debug traces, and another that connects remotely to help * diagnose issues while the application runs on your user's device. * * The logger can optionally capture stdout and stderr. When running an * application from the IDE, this will automatically capture everything that * goes into the debugger console log, and insert it in the stream of logs * sent to the viewer. * * Using the logger's flexible packet format, you can customize logging by * creating your own log types, and customize the desktop viewer to display * runtime information panels for your application. * -------------------------------------------------------------------------------- */ /* Logger internal debug flags */ // Set to 0 to disable internal debug completely // Set to 1 to activate console logs when running the logger itself // Set to 2 to see every logging call issued by the app, too #define LOGGER_DEBUG 0 #ifdef NSLog #undef NSLog #endif // Internal debugging stuff for the logger itself #if LOGGER_DEBUG #define LOGGERDBG LoggerDbg #if LOGGER_DEBUG > 1 #define LOGGERDBG2 LoggerDbg #else #define LOGGERDBG2(format, ...) do{}while(0) #endif // Internal logging function prototype static void LoggerDbg(CFStringRef format, ...); #else #define LOGGERDBG(format, ...) do{}while(0) #define LOGGERDBG2(format, ...) do{}while(0) #endif // small set of macros for proper ARC/non-ARC compilation support // with added cruft to support non-clang compilers #undef LOGGER_ARC_MACROS_DEFINED #if defined(__has_feature) #if __has_feature(objc_arc) #define CAST_TO_CFSTRING __bridge CFStringRef #define CAST_TO_NSSTRING __bridge NSString * #define CAST_TO_CFDATA __bridge CFDataRef #define RELEASE(obj) do{}while(0) #define LOGGER_ARC_MACROS_DEFINED #endif #endif #if !defined(LOGGER_ARC_MACROS_DEFINED) #define CAST_TO_CFSTRING CFStringRef #define CAST_TO_NSSTRING NSString * #define CAST_TO_CFDATA CFDataRef #define RELEASE(obj) [obj release] #endif #undef LOGGER_ARC_MACROS_DEFINED /* Local prototypes */ static void LoggerFlushAllOnExit(void); static void* LoggerWorkerThread(Logger *logger); static void LoggerWriteMoreData(Logger *logger); static void LoggerPushMessageToQueue(Logger *logger, CFDataRef message); // Bonjour management static void LoggerStartBonjourBrowsing(Logger *logger); static void LoggerStopBonjourBrowsing(Logger *logger); static BOOL LoggerBrowseBonjourForServices(Logger *logger, CFStringRef domainName); static void LoggerServiceBrowserCallBack(CFNetServiceBrowserRef browser, CFOptionFlags flags, CFTypeRef domainOrService, CFStreamError* error, void *info); // Reachability and reconnect timer static void LoggerRemoteSettingsChanged(Logger *logger); static void LoggerStartReachabilityChecking(Logger *logger); static void LoggerStopReachabilityChecking(Logger *logger); static void LoggerReachabilityCallBack(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info); static void LoggerStartReconnectTimer(Logger *logger); static void LoggerStopReconnectTimer(Logger *logger); static void LoggerTimedReconnectCallback(CFRunLoopTimerRef timer, void *info); // Connection & stream management static void LoggerTryConnect(Logger *logger); static void LoggerWriteStreamTerminated(Logger *logger); static void LoggerWriteStreamCallback(CFWriteStreamRef ws, CFStreamEventType event, void* info); // File buffering static void LoggerCreateBufferWriteStream(Logger *logger); static void LoggerCreateBufferReadStream(Logger *logger); static void LoggerEmptyBufferFile(Logger *logger); static void LoggerFileBufferingOptionsChanged(Logger *logger); static void LoggerFlushQueueToBufferStream(Logger *logger, BOOL firstEntryIsClientInfo); // Encoding functions static void LoggerPushClientInfoToFrontOfQueue(Logger *logger); static void LoggerMessageAddTimestampAndThreadID(CFMutableDataRef encoder); static CFMutableDataRef LoggerMessageCreate(int32_t seq); static void LoggerMessageAddInt32(CFMutableDataRef encoder, int32_t anInt, int key); #if __LP64__ static void LoggerMessageAddInt64(CFMutableDataRef data, int64_t anInt, int key); #endif static void LoggerMessageAddString(CFMutableDataRef encoder, CFStringRef aString, int key); static void LoggerMessageAddData(CFMutableDataRef encoder, CFDataRef theData, int key, int partType); static uint32_t LoggerMessageGetSeq(CFDataRef message); /* Static objects */ static CFMutableArrayRef sLoggersList; static Logger* volatile sDefaultLogger = NULL; static Boolean sAtexitFunctionSet = FALSE; static pthread_mutex_t sLoggersListMutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t sDefaultLoggerMutex = PTHREAD_MUTEX_INITIALIZER; // Console logging static void LoggerStartGrabbingConsole(Logger *logger); static void LoggerStopGrabbingConsole(Logger *logger); static Logger ** consoleGrabbersList = NULL; static unsigned consoleGrabbersListLength; static unsigned numActiveConsoleGrabbers = 0; static pthread_mutex_t consoleGrabbersMutex = PTHREAD_MUTEX_INITIALIZER; static pthread_t consoleGrabThread; static int sConsolePipes[4] = { -1, -1, -1, -1 }; static int sSTDOUT = -1, sSTDERR = -1; static int sSTDOUThadSIGPIPE, sSTDERRhadSIGPIPE; // ----------------------------------------------------------------------------- #pragma mark - #pragma mark Default logger // ----------------------------------------------------------------------------- void LoggerSetDefaultLogger(Logger *defaultLogger) { pthread_mutex_lock(&sDefaultLoggerMutex); sDefaultLogger = defaultLogger; pthread_mutex_unlock(&sDefaultLoggerMutex); } Logger *LoggerGetDefaultLogger(void) { Logger *l = sDefaultLogger; if (l == NULL) { pthread_mutex_lock(&sDefaultLoggerMutex); l = sDefaultLogger; if (l == NULL) l = LoggerInit(); pthread_mutex_unlock(&sDefaultLoggerMutex); } return l; } Logger *LoggerCheckDefaultLogger(void) { return sDefaultLogger; } // ----------------------------------------------------------------------------- #pragma mark - #pragma mark Initialization and setup // ----------------------------------------------------------------------------- Logger *LoggerInit(void) { LOGGERDBG(CFSTR("LoggerInit defaultLogger=%p"), sDefaultLogger); Logger *logger = (Logger *)malloc(sizeof(Logger)); bzero(logger, sizeof(Logger)); logger->logQueue = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); pthread_mutex_init(&logger->logQueueMutex, NULL); pthread_cond_init(&logger->logQueueEmpty, NULL); logger->bonjourServiceBrowsers = CFArrayCreateMutable(NULL, 4, &kCFTypeArrayCallBacks); logger->bonjourServices = CFArrayCreateMutable(NULL, 4, &kCFTypeArrayCallBacks); // for now we don't grow the send buffer, just use one page of memory which should be enouh // (bigger messages will be sent separately) logger->sendBuffer = (uint8_t *)malloc(4096); logger->sendBufferSize = 4096; logger->options = LOGGER_DEFAULT_OPTIONS; #if LOGGER_DEBUG // when debugging NSLogger itself, don't hijack the system console // as we are sending messages to it for display logger->options &= ~kLoggerOption_CaptureSystemConsole; #endif logger->quit = NO; // Add logger to the list of existing loggers // Set this logger as the default logger is none exist already pthread_mutex_lock(&sLoggersListMutex); if (sLoggersList == NULL) { CFArrayCallBacks callbacks; bzero(&callbacks, sizeof(callbacks)); sLoggersList = CFArrayCreateMutable(NULL, 0, &callbacks); } CFArrayAppendValue(sLoggersList, (const void *)logger); if (sDefaultLogger == NULL) sDefaultLogger = logger; // Configure a low level exit() callback that will flush all connected loggers if (!sAtexitFunctionSet) { atexit(&LoggerFlushAllOnExit); sAtexitFunctionSet = TRUE; } pthread_mutex_unlock(&sLoggersListMutex); return logger; } void LoggerSetOptions(Logger *logger, uint32_t options) { LOGGERDBG(CFSTR("LoggerSetOptions options=0x%08lx"), options); // If we choose to log to system console // make sure we are not configured to capture the system console // When debugging NSLogger itself, we never capture the system console either if (options & kLoggerOption_LogToConsole) options &= (uint32_t)~kLoggerOption_CaptureSystemConsole; if (logger == NULL) logger = LoggerGetDefaultLogger(); if (logger != NULL) logger->options = options; } void LoggerSetupBonjour(Logger *logger, CFStringRef bonjourServiceType, CFStringRef bonjourServiceName) { LOGGERDBG(CFSTR("LoggerSetupBonjour serviceType=%@ serviceName=%@"), bonjourServiceType, bonjourServiceName); if (logger == NULL) logger = LoggerGetDefaultLogger(); if (logger != NULL) { if (bonjourServiceType != NULL) CFRetain(bonjourServiceType); if (bonjourServiceName != NULL) CFRetain(bonjourServiceName); if (logger->bonjourServiceType != NULL) CFRelease(logger->bonjourServiceType); if (logger->bonjourServiceName != NULL) CFRelease(logger->bonjourServiceName); logger->bonjourServiceType = bonjourServiceType; logger->bonjourServiceName = bonjourServiceName; } } void LoggerSetViewerHost(Logger *logger, CFStringRef hostName, UInt32 port) { if (logger == NULL) logger = LoggerGetDefaultLogger(); if (logger == NULL) return; CFStringRef previousHost = logger->host; UInt32 previousPort = logger->port; logger->host = NULL; if (hostName != NULL) { logger->host = CFStringCreateCopy(NULL, hostName); logger->port = port; } if (logger->remoteOptionsChangedSource != NULL && (logger->port != previousPort || ((hostName == NULL) != (previousHost == NULL)) || (hostName != NULL && CFStringCompare(hostName, previousHost, kCFCompareCaseInsensitive) != kCFCompareEqualTo))) CFRunLoopSourceSignal(logger->remoteOptionsChangedSource); if (previousHost != NULL) CFRelease(previousHost); } void LoggerSetBufferFile(Logger *logger, CFStringRef absolutePath) { if (logger == NULL) { logger = LoggerGetDefaultLogger(); if (logger == NULL) return; } BOOL change = ((logger->bufferFile != NULL && absolutePath == NULL) || (logger->bufferFile == NULL && absolutePath != NULL) || (logger->bufferFile != NULL && absolutePath != NULL && CFStringCompare(logger->bufferFile, absolutePath, (CFStringCompareFlags) 0) != kCFCompareEqualTo)); if (change) { if (logger->bufferFile != NULL) { CFRelease(logger->bufferFile); logger->bufferFile = NULL; } if (absolutePath != NULL) logger->bufferFile = CFStringCreateCopy(NULL, absolutePath); if (logger->bufferFileChangedSource != NULL) CFRunLoopSourceSignal(logger->bufferFileChangedSource); } } Logger *LoggerStart(Logger *logger) { // will do nothing if logger is already started if (logger == NULL) logger = LoggerGetDefaultLogger(); if (logger != NULL) { if (logger->workerThread == NULL) { dispatch_once(&logger->workerThreadInit, ^{ // Start the work thread which performs the Bonjour search, // connects to the logging service and forwards the logs LOGGERDBG(CFSTR("LoggerStart logger=%p"), logger); pthread_create(&logger->workerThread, NULL, (void *(*)(void *))&LoggerWorkerThread, logger); // Grab console output if required if (logger->options & kLoggerOption_CaptureSystemConsole) LoggerStartGrabbingConsole(logger); }); } } else { LOGGERDBG2(CFSTR("-> could not create logger")); } return logger; } void LoggerStop(Logger *logger) { LOGGERDBG(CFSTR("LoggerStop")); pthread_mutex_lock(&sLoggersListMutex); if (logger == NULL || logger == sDefaultLogger) { logger = sDefaultLogger; sDefaultLogger = NULL; } if (sLoggersList != NULL && logger != NULL) { CFIndex where = CFArrayGetFirstIndexOfValue(sLoggersList, CFRangeMake(0, CFArrayGetCount(sLoggersList)), (void const *) logger); if (where != -1) CFArrayRemoveValueAtIndex(sLoggersList, where); } pthread_mutex_unlock(&sLoggersListMutex); if (logger != NULL) { if (logger->workerThread != NULL) { LoggerStopGrabbingConsole(logger); logger->quit = YES; pthread_join(logger->workerThread, NULL); } CFRelease(logger->bonjourServiceBrowsers); CFRelease(logger->bonjourServices); free(logger->sendBuffer); if (logger->host != NULL) CFRelease(logger->host); if (logger->bufferFile != NULL) CFRelease(logger->bufferFile); if (logger->bonjourServiceType != NULL) CFRelease(logger->bonjourServiceType); if (logger->bonjourServiceName != NULL) CFRelease(logger->bonjourServiceName); // to make sure potential errors are caught, set the whole structure // to a value that will make code crash if it tries using pointers to it. memset(logger, 0x55, sizeof(Logger)); free(logger); } } static void LoggerFlushAllOnExit() { // this function is automatically configured by NSLogger to flush all connected loggers // on exit. this guarantees that the developer sees the last messages issued by the application. // it is configured the first time a logger is initialized, so at the time we're being called // the loggers list is never NULL pthread_mutex_lock(&sLoggersListMutex); CFIndex numLoggers = CFArrayGetCount(sLoggersList); for (CFIndex i=0; i < numLoggers; i++) LoggerFlush((Logger *) CFArrayGetValueAtIndex(sLoggersList, i), NO); pthread_mutex_unlock(&sLoggersListMutex); } void LoggerFlush(Logger *logger, BOOL waitForConnection) { // Special case: if nothing has ever been logged, don't bother if (logger == NULL && sDefaultLogger == NULL) return; if (logger == NULL) logger = LoggerGetDefaultLogger(); if (logger != NULL && pthread_self() != logger->workerThread && (logger->connected || logger->bufferFile != NULL || waitForConnection)) // TODO: change this test { pthread_mutex_lock(&logger->logQueueMutex); if (CFArrayGetCount(logger->logQueue) > 0) pthread_cond_wait(&logger->logQueueEmpty, &logger->logQueueMutex); pthread_mutex_unlock(&logger->logQueueMutex); } } #if LOGGER_DEBUG static void LoggerDbg(CFStringRef format, ...) { // Internal debugging function // (what do you think, that we use the Logger to debug itself ??) if (format != NULL) { @autoreleasepool { va_list args; va_start(args, format); CFStringRef s = CFStringCreateWithFormatAndArguments(NULL, NULL, format, args); va_end(args); if (s != NULL) { CFShow(s); CFRelease(s); } } } } #endif // ----------------------------------------------------------------------------- #pragma mark - #pragma mark Main processing // ----------------------------------------------------------------------------- static BOOL LoggerPrepareRunloopSource(Logger *logger, CFRunLoopSourceRef *outRef, void *callback) { // first call will also create the thread's runloop CFRunLoopSourceContext context; bzero(&context, sizeof(context)); context.info = logger; context.perform = callback; *outRef = CFRunLoopSourceCreate(NULL, 0, &context); if (*outRef == NULL) { // This NSLog is intentional as this failure MUST be logged to console NSLog(@"*** NSLogger: worker thread failed creating runloop source"); return NO; } CFRunLoopAddSource(CFRunLoopGetCurrent(), *outRef, kCFRunLoopDefaultMode); return YES; } static void LoggerDisposeRunloopSource(CFRunLoopSourceRef *sourceRef) { if (*sourceRef != NULL) { CFRunLoopSourceInvalidate(*sourceRef); CFRelease(*sourceRef); *sourceRef = NULL; } } static void *LoggerWorkerThread(Logger *logger) { LOGGERDBG(CFSTR("Start LoggerWorkerThread")); #if !TARGET_OS_IPHONE // Register thread with Garbage Collector on Mac OS X if we're running an OS version that has GC void (*registerThreadWithCollector_fn)(void); registerThreadWithCollector_fn = (void(*)(void)) dlsym(RTLD_NEXT, "objc_registerThreadWithCollector"); if (registerThreadWithCollector_fn) (*registerThreadWithCollector_fn)(); #endif // Create the run loop source that signals when messages have been added to the runloop // this will directly trigger a WriteMoreData() call, which will or won't write depending // on whether we're connected and there's space available in the stream if (!LoggerPrepareRunloopSource(logger, &logger->messagePushedSource, &LoggerWriteMoreData)) { // Failing to create the runloop source for pushing messages is a major failure. // This NSLog is intentional. We WANT console output in this case NSLog(@"*** NSLogger: switching to console logging."); logger->options |= kLoggerOption_LogToConsole; logger->workerThread = NULL; return NULL; } // Create the buffering stream if needed if (logger->bufferFile != NULL) LoggerCreateBufferWriteStream(logger); // Create the runloop source that lets us know when file buffering options change LoggerPrepareRunloopSource(logger, &logger->bufferFileChangedSource, &LoggerFileBufferingOptionsChanged); // Create the runloop source that lets us know when remote (host, Bonjour) settings change LoggerPrepareRunloopSource(logger, &logger->remoteOptionsChangedSource, &LoggerRemoteSettingsChanged); // Start Reachability (when needed), which determines when we take the next step // (once Reachability status is known, we'll decide to either start Bonjour browsing or // try connecting to a direct host) LoggerStartReachabilityChecking(logger); // Run logging thread until LoggerStop() is called NSTimeInterval timeout = 0.10; while (!logger->quit) { int result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, true); if (result == kCFRunLoopRunFinished || result == kCFRunLoopRunStopped) break; if (result == kCFRunLoopRunHandledSource) { timeout = 0.0; continue; } timeout = fmax(1.0, fmin(0.10, timeout+0.0005)); } // Cleanup LoggerStopBonjourBrowsing(logger); LoggerStopReachabilityChecking(logger); LoggerStopReconnectTimer(logger); if (logger->logStream != NULL) { CFWriteStreamSetClient(logger->logStream, 0, NULL, NULL); CFWriteStreamClose(logger->logStream); CFRelease(logger->logStream); logger->logStream = NULL; } if (logger->bufferWriteStream == NULL && logger->bufferFile != NULL) { // If there are messages in the queue and LoggerStop() was called and // a buffer file was set just before LoggerStop() was called, flush // the log queue to the buffer file pthread_mutex_lock(&logger->logQueueMutex); CFIndex outstandingMessages = CFArrayGetCount(logger->logQueue); pthread_mutex_unlock(&logger->logQueueMutex); if (outstandingMessages) LoggerCreateBufferWriteStream(logger); } if (logger->bufferWriteStream != NULL) { CFWriteStreamClose(logger->bufferWriteStream); CFRelease(logger->bufferWriteStream); logger->bufferWriteStream = NULL; } LoggerDisposeRunloopSource(&logger->messagePushedSource); LoggerDisposeRunloopSource(&logger->bufferFileChangedSource); LoggerDisposeRunloopSource(&logger->remoteOptionsChangedSource); // if the client ever tries to log again against us, make sure that logs at least // go to console logger->options |= kLoggerOption_LogToConsole; logger->workerThread = NULL; LOGGERDBG(CFSTR("Stop LoggerWorkerThread")); return NULL; } static CFStringRef LoggerCreateStringRepresentationFromBinaryData(CFDataRef data) { CFMutableStringRef s = CFStringCreateMutable(NULL, 0); unsigned int offset = 0; unsigned int dataLen = (unsigned int)CFDataGetLength(data); char buffer[1+6+16*3+1+16+1+1+1]; buffer[0] = '\0'; const unsigned char *q = (unsigned char *)CFDataGetBytePtr(data); if (dataLen == 1) CFStringAppend(s, CFSTR("Raw data, 1 byte:\n")); else CFStringAppendFormat(s, NULL, CFSTR("Raw data, %u bytes:\n"), dataLen); while (dataLen) { int i, j, b = sprintf(buffer," %04x: ", offset); for (i=0; i < 16 && i < (int)dataLen; i++) sprintf(&buffer[b+3*i], "%02x ", (int)q[i]); for (j=i; j < 16; j++) strncat(buffer, " ", 3); b = (int)strlen(buffer); buffer[b++] = '\''; for (i=0; i < 16 && i < (int)dataLen; i++, q++) { if (*q >= 32 && *q < 128) buffer[b++] = (char)*q; else buffer[b++] = ' '; } for (j=i; j < 16; j++) buffer[b++] = ' '; buffer[b++] = '\''; buffer[b++] = '\n'; buffer[b] = 0; CFStringRef bufferStr = CFStringCreateWithBytesNoCopy(NULL, (const UInt8 *)buffer, (CFIndex)strlen(buffer), kCFStringEncodingISOLatin1, false, kCFAllocatorNull); CFStringAppend(s, bufferStr); CFRelease(bufferStr); dataLen -= (unsigned int)i; offset += (unsigned int)i; } return s; } static void LoggerLogToConsole(CFDataRef data) { // Decode and log a message to the console. Doing this from the worker thread // allow us to serialize logging, which is a benefit that NSLog() doesn't have. // Only drawback is that we have to decode our own message, but that is a minor hassle. if (data == NULL) { CFShow(CFSTR("LoggerLogToConsole: data is NULL")); return; } struct timeval timestamp; bzero(×tamp, sizeof(timestamp)); int type = LOGMSG_TYPE_LOG, contentsType = PART_TYPE_STRING; int imgWidth=0, imgHeight=0; CFStringRef message = NULL; CFStringRef thread = NULL; // decode message contents uint8_t *p = (uint8_t *)CFDataGetBytePtr(data) + 4; uint16_t partCount; memcpy(&partCount, p, 2); partCount = ntohs(partCount); p += 2; while (partCount--) { uint8_t partKey = *p++; uint8_t partType = *p++; uint32_t partSize; if (partType == PART_TYPE_INT16) partSize = 2; else if (partType == PART_TYPE_INT32) partSize = 4; else if (partType == PART_TYPE_INT64) partSize = 8; else { memcpy(&partSize, p, 4); p += 4; partSize = ntohl(partSize); } CFTypeRef part = NULL; uint32_t value32 = 0; uint64_t value64 = 0; if (partSize > 0) { if (partType == PART_TYPE_STRING) { // trim whitespace and newline at both ends of the string uint8_t *q = p; uint32_t l = partSize; while (l && (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r')) q++, l--; uint8_t *r = q + l - 1; while (l && (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r')) r--, l--; part = CFStringCreateWithBytesNoCopy(NULL, q, (CFIndex)l, kCFStringEncodingUTF8, false, kCFAllocatorNull); } else if (partType == PART_TYPE_BINARY) { part = CFDataCreateWithBytesNoCopy(NULL, p, (CFIndex)partSize, kCFAllocatorNull); } else if (partType == PART_TYPE_IMAGE) { // ignore image data, we can't log it to console } else if (partType == PART_TYPE_INT16) { value32 = ((uint32_t)p[0]) << 8 | (uint32_t)p[1]; } else if (partType == PART_TYPE_INT32) { memcpy(&value32, p, 4); value32 = ntohl(value32); } else if (partType == PART_TYPE_INT64) { memcpy(&value64, p, 8); value64 = CFSwapInt64BigToHost(value64); } p += partSize; } switch (partKey) { case PART_KEY_MESSAGE_TYPE: type = (int)value32; break; case PART_KEY_TIMESTAMP_S: // timestamp with seconds-level resolution timestamp.tv_sec = (partType == PART_TYPE_INT64) ? (__darwin_time_t)value64 : (__darwin_time_t)value32; break; case PART_KEY_TIMESTAMP_MS: // millisecond part of the timestamp (optional) timestamp.tv_usec = ((partType == PART_TYPE_INT64) ? (__darwin_suseconds_t)value64 : (__darwin_suseconds_t)value32) * 1000; break; case PART_KEY_TIMESTAMP_US: // microsecond part of the timestamp (optional) timestamp.tv_usec = (partType == PART_TYPE_INT64) ? (__darwin_suseconds_t)value64 : (__darwin_suseconds_t)value32; break; case PART_KEY_THREAD_ID: if (thread == NULL) // useless test, we know what we're doing but clang analyzer doesn't... { if (partType == PART_TYPE_INT32) thread = CFStringCreateWithFormat(NULL, NULL, CFSTR("thread 0x%08x"), value32); else if (partType == PART_TYPE_INT64) thread = CFStringCreateWithFormat(NULL, NULL, CFSTR("thread 0x%qx"), value64); else if (partType == PART_TYPE_STRING && part != NULL) thread = CFRetain(part); } break; case PART_KEY_MESSAGE: if (part != NULL) { if (partType == PART_TYPE_STRING) message = CFRetain(part); else if (partType == PART_TYPE_BINARY) message = LoggerCreateStringRepresentationFromBinaryData(part); } contentsType = partType; break; case PART_KEY_IMAGE_WIDTH: imgWidth = (partType == PART_TYPE_INT32 ? (int)value32 : (int)value64); break; case PART_KEY_IMAGE_HEIGHT: imgHeight = (partType == PART_TYPE_INT32 ? (int)value32 : (int)value64); break; default: break; } if (part != NULL) CFRelease(part); } // Prepare the final representation and log to console CFMutableStringRef s = CFStringCreateMutable(NULL, 0); char buf[32]; struct tm t; gmtime_r(×tamp.tv_sec, &t); strftime(buf, sizeof(buf)-1, "%T", &t); CFStringRef ts = CFStringCreateWithBytesNoCopy( NULL, (const UInt8 *)buf, (CFIndex)strlen(buf), kCFStringEncodingASCII, false, kCFAllocatorNull); CFStringAppend(s, ts); CFRelease(ts); if (contentsType == PART_TYPE_IMAGE) message = CFStringCreateWithFormat(NULL, NULL, CFSTR(""), imgWidth, imgHeight); buf[0] = 0; if (thread != NULL && CFStringGetLength(thread) < 16) { int n = 16 - (int)CFStringGetLength(thread); memset(buf, ' ', (size_t)n); buf[n] = 0; } CFStringAppendFormat(s, NULL, CFSTR(".%04d %s%@ | %@"), (int)(timestamp.tv_usec / 1000), buf, (thread == NULL) ? CFSTR("") : thread, (message != NULL) ? message : CFSTR("")); if (thread != NULL) CFRelease(thread); if (message != NULL) CFRelease(message); if (type == LOGMSG_TYPE_LOG || type == LOGMSG_TYPE_MARK) CFShow(s); CFRelease(s); } static void LoggerWriteMoreData(Logger *logger) { uint32_t logToConsole = (logger->options & kLoggerOption_LogToConsole); if (!logger->connected) { if (logToConsole) { pthread_mutex_lock(&logger->logQueueMutex); while (CFArrayGetCount(logger->logQueue)) { LoggerLogToConsole((CFDataRef)CFArrayGetValueAtIndex(logger->logQueue, 0)); CFArrayRemoveValueAtIndex(logger->logQueue, 0); } pthread_mutex_unlock(&logger->logQueueMutex); pthread_cond_broadcast(&logger->logQueueEmpty); } else if (logger->bufferWriteStream != NULL) { LoggerFlushQueueToBufferStream(logger, NO); } else if (!(logger->options & kLoggerOption_BufferLogsUntilConnection)) { /* No client connected * User don't want to log to console * User don't want to log to file * and user don't want us to buffer it in memory * So let's just sack the whole queue */ pthread_mutex_lock(&logger->logQueueMutex); while (CFArrayGetCount(logger->logQueue)) { CFArrayRemoveValueAtIndex(logger->logQueue, 0); } pthread_mutex_unlock(&logger->logQueueMutex); pthread_cond_broadcast(&logger->logQueueEmpty); } return; } if (CFWriteStreamCanAcceptBytes(logger->logStream)) { // prepare archived data with log queue contents, unblock the queue as soon as possible CFMutableDataRef sendFirstItem = NULL; if (logger->sendBufferUsed == 0) { // pull more data from the log queue if (logger->bufferReadStream != NULL) { if (!CFReadStreamHasBytesAvailable(logger->bufferReadStream)) { CFReadStreamClose(logger->bufferReadStream); CFRelease(logger->bufferReadStream); logger->bufferReadStream = NULL; LoggerEmptyBufferFile(logger); } else { logger->sendBufferUsed = (NSUInteger)CFReadStreamRead(logger->bufferReadStream, logger->sendBuffer, (CFIndex)logger->sendBufferSize); } } else { pthread_mutex_lock(&logger->logQueueMutex); while (CFArrayGetCount(logger->logQueue)) { CFDataRef d = (CFDataRef)CFArrayGetValueAtIndex(logger->logQueue, 0); CFIndex dsize = CFDataGetLength(d); if ((logger->sendBufferUsed + (NSUInteger)dsize) > logger->sendBufferSize) break; memcpy(logger->sendBuffer + logger->sendBufferUsed, CFDataGetBytePtr(d), (size_t)dsize); logger->sendBufferUsed += (NSUInteger)dsize; if (logToConsole) LoggerLogToConsole(d); CFArrayRemoveValueAtIndex(logger->logQueue, 0); logger->incompleteSendOfFirstItem = NO; } pthread_mutex_unlock(&logger->logQueueMutex); } if (logger->sendBufferUsed == 0) { // are we done yet? pthread_mutex_lock(&logger->logQueueMutex); if (CFArrayGetCount(logger->logQueue) == 0) { pthread_mutex_unlock(&logger->logQueueMutex); pthread_cond_broadcast(&logger->logQueueEmpty); return; } // first item is too big to fit in a single packet, send it separately sendFirstItem = (CFMutableDataRef)CFArrayGetValueAtIndex(logger->logQueue, 0); logger->incompleteSendOfFirstItem = YES; pthread_mutex_unlock(&logger->logQueueMutex); logger->sendBufferOffset = 0; } } // send data over the socket. We try hard to be failsafe and if we have to send // data in fragments, we make sure that in case a disconnect occurs we restart // sending the whole message(s) if (logger->sendBufferUsed != 0) { CFIndex written = CFWriteStreamWrite(logger->logStream, logger->sendBuffer + logger->sendBufferOffset, (CFIndex)(logger->sendBufferUsed - logger->sendBufferOffset)); if (written < 0) { // We'll get an event if the stream closes on error. Don't discard the data, // it will be sent as soon as a connection is re-acquired. LOGGERDBG(CFSTR("CFWriteStreamWrite got %d result"),written); return; } if ((logger->sendBufferOffset + (NSUInteger)written) < logger->sendBufferUsed) { // everything couldn't be sent at once logger->sendBufferOffset += (NSUInteger)written; } else { logger->sendBufferUsed = 0; logger->sendBufferOffset = 0; } } else if (sendFirstItem) { CFIndex length = CFDataGetLength(sendFirstItem) - (CFIndex)logger->sendBufferOffset; CFIndex written = CFWriteStreamWrite(logger->logStream, CFDataGetBytePtr(sendFirstItem) + logger->sendBufferOffset, length); if (written < 0) { // We'll get an event if the stream closes on error return; } if (written < length) { // The output pipe is full, and the first item has not been sent completely // We need to reduce the remaining data on the first item so it can be taken // care of at the next iteration. We take advantage of the fact that each item // in the queue is actually a mutable data block // @@@ NOTE: IF WE GET DISCONNECTED WHILE DOING THIS, THINGS WILL GO WRONG // NEED TO UPDATE THIS LOGIC LOGGERDBG(CFSTR("Output pipe is full")); CFDataReplaceBytes((CFMutableDataRef)sendFirstItem, CFRangeMake(0, written), NULL, 0); return; } // we are done sending the first item in the queue, remove it now pthread_mutex_lock(&logger->logQueueMutex); CFArrayRemoveValueAtIndex(logger->logQueue, 0); logger->incompleteSendOfFirstItem = NO; pthread_mutex_unlock(&logger->logQueueMutex); logger->sendBufferOffset = 0; } pthread_mutex_lock(&logger->logQueueMutex); CFIndex remainingMsgs = CFArrayGetCount(logger->logQueue); pthread_mutex_unlock(&logger->logQueueMutex); if (remainingMsgs == 0) pthread_cond_broadcast(&logger->logQueueEmpty); } } // ----------------------------------------------------------------------------- #pragma mark - #pragma mark Console logs redirection support // ----------------------------------------------------------------------------- static void LoggerLogFromConsole(NSString *tag, int fd, int outfd) { const int BUFSIZE = 1000; UInt8 buf[BUFSIZE]; ssize_t bytes_read = 0; while ((bytes_read = read(fd, buf, BUFSIZE-1)) > 0) { // output received data to the original fd if (outfd != -1) write(outfd, buf, (size_t)bytes_read); if (buf[bytes_read-1] == '\n') --bytes_read; CFStringRef messageString = CFStringCreateWithBytes(NULL, buf, bytes_read, kCFStringEncodingUTF8, false); if (messageString != NULL) { CFArrayRef array = CFStringCreateArrayBySeparatingStrings(NULL, messageString, CFSTR("\n")); if (array != NULL) { pthread_mutex_lock(&consoleGrabbersMutex); CFIndex n = CFArrayGetCount(array); for (CFIndex m = 0; m < n; m++) { CFStringRef msg = (CFStringRef)CFArrayGetValueAtIndex(array, m); for (unsigned i = 0; i < consoleGrabbersListLength; i++) { if (consoleGrabbersList[i] != NULL) LogMessageTo(consoleGrabbersList[i], tag, 0, @"%@", msg); } } pthread_mutex_unlock(&consoleGrabbersMutex); CFRelease(array); } CFRelease(messageString); } } } static void *LoggerConsoleGrabThread(void *context) { #pragma unused (context) int fdout = sConsolePipes[0]; fcntl(fdout, F_SETFL, fcntl(fdout, F_GETFL, 0) | O_NONBLOCK); int fderr = sConsolePipes[2]; fcntl(fderr, F_SETFL, fcntl(fderr, F_GETFL, 0) | O_NONBLOCK); while (numActiveConsoleGrabbers != 0) { fd_set set; FD_ZERO(&set); FD_SET(fdout, &set); FD_SET(fderr, &set); int ret = select(fderr + 1, &set, NULL, NULL, NULL); if (ret <= 0) { // ==0: time expired without activity // < 0: error occurred break; } if (FD_ISSET(fdout, &set)) LoggerLogFromConsole(@"stdout", fdout, sSTDOUT); if (FD_ISSET(fderr, &set )) LoggerLogFromConsole(@"stderr", fderr, sSTDERR); } return NULL; } static void LoggerStartConsoleRedirection() { // keep the original pipes so we can still forward everything // (i.e. to the running IDE that needs to display or interpret console messages) // and remember the SIGPIPE settings, as we are going to clear them to prevent // the app from exiting when we close the pipes if (sSTDOUT == -1) { sSTDOUThadSIGPIPE = fcntl(STDOUT_FILENO, F_GETNOSIGPIPE); sSTDOUT = dup(STDOUT_FILENO); sSTDERRhadSIGPIPE = fcntl(STDERR_FILENO, F_GETNOSIGPIPE); sSTDERR = dup(STDERR_FILENO); } // create the pipes if (sConsolePipes[0] == -1) { if (pipe(sConsolePipes) != -1) { fcntl(sConsolePipes[0], F_SETNOSIGPIPE, 1); fcntl(sConsolePipes[1], F_SETNOSIGPIPE, 1); dup2(sConsolePipes[1], STDOUT_FILENO); } } if (sConsolePipes[2] == -1) { if (pipe(&sConsolePipes[2]) != -1) { fcntl(sConsolePipes[0], F_SETNOSIGPIPE, 1); fcntl(sConsolePipes[1], F_SETNOSIGPIPE, 1); dup2(sConsolePipes[3], STDERR_FILENO); } } pthread_create(&consoleGrabThread, NULL, &LoggerConsoleGrabThread, NULL); } static void LoggerStopConsoleRedirection() { // close the pipes - will force exiting the console logger thread // assume the console grabber mutex has been acquired dup2(sSTDOUT, STDOUT_FILENO); dup2(sSTDERR, STDERR_FILENO); close(sSTDOUT); close(sSTDERR); // restore sigpipe flag on standard streams fcntl(STDOUT_FILENO, F_SETNOSIGPIPE, &sSTDOUThadSIGPIPE); fcntl(STDERR_FILENO, F_SETNOSIGPIPE, &sSTDERRhadSIGPIPE); // close pipes, this will trigger an error in select() and a console grab thread exit if (sConsolePipes[0] != -1) { close(sConsolePipes[0]); close(sConsolePipes[1]); sConsolePipes[0] = -1; } if (sConsolePipes[2] != -1) { close(sConsolePipes[2]); close(sConsolePipes[1]); } sConsolePipes[0] = sConsolePipes[1] = sConsolePipes[2] = sConsolePipes[3] = -1; pthread_join(consoleGrabThread, NULL); } static void LoggerStartGrabbingConsole(Logger *logger) { if (!(logger->options & kLoggerOption_CaptureSystemConsole)) return; pthread_mutex_lock(&consoleGrabbersMutex); Boolean added = FALSE; for (unsigned i = 0; i < numActiveConsoleGrabbers; i++) { if (consoleGrabbersList[i] == NULL) { consoleGrabbersList[i] = logger; numActiveConsoleGrabbers++; added = TRUE; break; } } if (!added) { consoleGrabbersList = realloc(consoleGrabbersList, ++consoleGrabbersListLength * sizeof(Logger *)); consoleGrabbersList[numActiveConsoleGrabbers++] = logger; } LoggerStartConsoleRedirection(); // Start redirection if necessary pthread_mutex_unlock( &consoleGrabbersMutex ); } static void LoggerStopGrabbingConsole(Logger *logger) { if (numActiveConsoleGrabbers == 0) return; pthread_mutex_lock(&consoleGrabbersMutex); for (unsigned grabberIndex = 0; grabberIndex < consoleGrabbersListLength; grabberIndex++) { if (consoleGrabbersList[grabberIndex] == logger) { consoleGrabbersList[grabberIndex] = NULL; if (--numActiveConsoleGrabbers == 0) { consoleGrabbersListLength = 0; free(consoleGrabbersList); consoleGrabbersList = NULL; LoggerStopConsoleRedirection(); } break; } } pthread_mutex_unlock(&consoleGrabbersMutex); } // ----------------------------------------------------------------------------- #pragma mark - #pragma mark File buffering functions // ----------------------------------------------------------------------------- static void LoggerCreateBufferWriteStream(Logger *logger) { LOGGERDBG(CFSTR("LoggerCreateBufferWriteStream to file %@"), logger->bufferFile); CFURLRef fileURL = CFURLCreateWithFileSystemPath(NULL, logger->bufferFile, kCFURLPOSIXPathStyle, false); if (fileURL != NULL) { // Create write stream to file logger->bufferWriteStream = CFWriteStreamCreateWithFile(NULL, fileURL); CFRelease(fileURL); if (logger->bufferWriteStream != NULL) { // Set flag to append new data to buffer file CFWriteStreamSetProperty(logger->bufferWriteStream, kCFStreamPropertyAppendToFile, kCFBooleanTrue); // Open the buffer stream for writing if (!CFWriteStreamOpen(logger->bufferWriteStream)) { CFRelease(logger->bufferWriteStream); logger->bufferWriteStream = NULL; } else { // Write client info and flush the queue contents to buffer file LoggerPushClientInfoToFrontOfQueue(logger); LoggerFlushQueueToBufferStream(logger, YES); } } } if (logger->bufferWriteStream == NULL) { CFShow(CFSTR("NSLogger Warning: failed opening buffer file for writing:")); CFShow(logger->bufferFile); } } static void LoggerCreateBufferReadStream(Logger *logger) { LOGGERDBG(CFSTR("LoggerCreateBufferReadStream from file %@"), logger->bufferFile); CFURLRef fileURL = CFURLCreateWithFileSystemPath(NULL, logger->bufferFile, kCFURLPOSIXPathStyle, false); if (fileURL != NULL) { // Create read stream from file logger->bufferReadStream = CFReadStreamCreateWithFile(NULL, fileURL); CFRelease(fileURL); if (logger->bufferReadStream != NULL) { if (!CFReadStreamOpen(logger->bufferReadStream)) { CFRelease(logger->bufferReadStream); logger->bufferReadStream = NULL; } } } } static void LoggerEmptyBufferFile(Logger *logger) { // completely remove the buffer file from storage LOGGERDBG(CFSTR("LoggerEmptyBufferFile %@"), logger->bufferFile); if (logger->bufferFile != NULL) { CFIndex bufferSize = 1 + CFStringGetLength(logger->bufferFile) * 3; char *buffer = (char *)malloc((size_t)bufferSize); if (buffer != NULL) { if (CFStringGetFileSystemRepresentation(logger->bufferFile, buffer, bufferSize)) { // remove file unlink(buffer); } free(buffer); } } } static void LoggerFileBufferingOptionsChanged(Logger *logger) { // File buffering options changed (callback called on logger thread): // - close the current buffer file stream, if any // - create a new one, if needed LOGGERDBG(CFSTR("LoggerFileBufferingOptionsChanged bufferFile=%@"), logger->bufferFile); if (logger->bufferWriteStream != NULL) { CFWriteStreamClose(logger->bufferWriteStream); CFRelease(logger->bufferWriteStream); logger->bufferWriteStream = NULL; } if (logger->bufferFile != NULL) LoggerCreateBufferWriteStream(logger); } static void LoggerFlushQueueToBufferStream(Logger *logger, BOOL firstEntryIsClientInfo) { LOGGERDBG(CFSTR("LoggerFlushQueueToBufferStream")); pthread_mutex_lock(&logger->logQueueMutex); if (logger->incompleteSendOfFirstItem) { // drop anything being sent logger->sendBufferUsed = 0; logger->sendBufferOffset = 0; } logger->incompleteSendOfFirstItem = NO; // Write outstanding messages to the buffer file (streams don't detect disconnection // until the next write, where we could lose one or more messages) if (!firstEntryIsClientInfo && logger->sendBufferUsed) CFWriteStreamWrite(logger->bufferWriteStream, logger->sendBuffer + logger->sendBufferOffset, (CFIndex)(logger->sendBufferUsed - logger->sendBufferOffset)); int n = 0; while (CFArrayGetCount(logger->logQueue)) { CFDataRef data = CFArrayGetValueAtIndex(logger->logQueue, 0); CFIndex dataLength = CFDataGetLength(data); CFIndex written = CFWriteStreamWrite(logger->bufferWriteStream, CFDataGetBytePtr(data), dataLength); if (written != dataLength) { // couldn't write all data to file, maybe storage run out of space? CFShow(CFSTR("NSLogger Error: failed flushing the whole queue to buffer file:")); CFShow(logger->bufferFile); break; } CFArrayRemoveValueAtIndex(logger->logQueue, 0); if (n == 0 && firstEntryIsClientInfo && logger->sendBufferUsed) { // try hard: write any outstanding messages to the buffer file, after the client info CFWriteStreamWrite(logger->bufferWriteStream, logger->sendBuffer + logger->sendBufferOffset, (CFIndex)(logger->sendBufferUsed - logger->sendBufferOffset)); } n++; } logger->sendBufferUsed = 0; logger->sendBufferOffset = 0; pthread_mutex_unlock(&logger->logQueueMutex); pthread_cond_broadcast(&logger->logQueueEmpty); } // ----------------------------------------------------------------------------- #pragma mark - #pragma mark Bonjour browsing // ----------------------------------------------------------------------------- static void LoggerStartBonjourBrowsing(Logger *logger) { if (!logger->targetReachable || logger->bonjourDomainBrowser != NULL || !(logger->options & kLoggerOption_BrowseBonjour)) return; LOGGERDBG(CFSTR("LoggerStartBonjourBrowsing")); if (logger->options & kLoggerOption_BrowseOnlyLocalDomain) { LOGGERDBG(CFSTR("Logger configured to search only the local domain, searching for services on: local.")); if (!LoggerBrowseBonjourForServices(logger, CFSTR("local.")) && logger->host == NULL) { LOGGERDBG(CFSTR("*** Logger: could not browse for services in domain local., no remote host configured: reverting to console logging. ***")); logger->options |= kLoggerOption_LogToConsole; } } else { LOGGERDBG(CFSTR("Logger configured to search all domains, browsing for domains first")); CFNetServiceClientContext context = {0, (void *)logger, NULL, NULL, NULL}; CFRunLoopRef runLoop = CFRunLoopGetCurrent(); logger->bonjourDomainBrowser = CFNetServiceBrowserCreate(NULL, &LoggerServiceBrowserCallBack, &context); CFNetServiceBrowserScheduleWithRunLoop(logger->bonjourDomainBrowser, runLoop, kCFRunLoopCommonModes); if (!CFNetServiceBrowserSearchForDomains(logger->bonjourDomainBrowser, false, NULL)) { // An error occurred, revert to console logging if there is no remote host LOGGERDBG(CFSTR("*** Logger: could not browse for domains, reverting to console logging. ***")); CFNetServiceBrowserUnscheduleFromRunLoop(logger->bonjourDomainBrowser, runLoop, kCFRunLoopCommonModes); CFRelease(logger->bonjourDomainBrowser); logger->bonjourDomainBrowser = NULL; if (logger->host == NULL) logger->options |= kLoggerOption_LogToConsole; } } } static void LoggerStopBonjourBrowsing(Logger *logger) { LOGGERDBG(CFSTR("LoggerStopBonjourBrowsing")); // stop browsing for domains if (logger->bonjourDomainBrowser != NULL) { CFNetServiceBrowserStopSearch(logger->bonjourDomainBrowser, NULL); CFNetServiceBrowserUnscheduleFromRunLoop(logger->bonjourDomainBrowser, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); CFNetServiceBrowserInvalidate(logger->bonjourDomainBrowser); CFRelease(logger->bonjourDomainBrowser); logger->bonjourDomainBrowser = NULL; } // stop browsing for services CFIndex idx; for (idx = 0; idx < CFArrayGetCount(logger->bonjourServiceBrowsers); idx++) { CFNetServiceBrowserRef browser = (CFNetServiceBrowserRef)CFArrayGetValueAtIndex(logger->bonjourServiceBrowsers, idx); CFNetServiceBrowserStopSearch(browser, NULL); CFNetServiceBrowserUnscheduleFromRunLoop(browser, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); CFNetServiceBrowserInvalidate(browser); } CFArrayRemoveAllValues(logger->bonjourServiceBrowsers); // Forget all services CFArrayRemoveAllValues(logger->bonjourServices); } static BOOL LoggerBrowseBonjourForServices(Logger *logger, CFStringRef domainName) { BOOL result = NO; CFNetServiceClientContext context = {0, (void *)logger, NULL, NULL, NULL}; CFRunLoopRef runLoop = CFRunLoopGetCurrent(); CFNetServiceBrowserRef browser = CFNetServiceBrowserCreate(NULL, (CFNetServiceBrowserClientCallBack)&LoggerServiceBrowserCallBack, &context); CFNetServiceBrowserScheduleWithRunLoop(browser, runLoop, kCFRunLoopCommonModes); CFStreamError error; // try to use the user-specfied service type if any, fallback on our // default service type CFStringRef serviceType = logger->bonjourServiceType; if (serviceType == NULL) { if (logger->options & kLoggerOption_UseSSL) serviceType = LOGGER_SERVICE_TYPE_SSL; else serviceType = LOGGER_SERVICE_TYPE; } if (!CFNetServiceBrowserSearchForServices(browser, domainName, serviceType, &error)) { LOGGERDBG(CFSTR("Logger can't start search on domain: %@ (error %d)"), domainName, error.error); CFNetServiceBrowserUnscheduleFromRunLoop(browser, runLoop, kCFRunLoopCommonModes); CFNetServiceBrowserInvalidate(browser); } else { LOGGERDBG(CFSTR("Logger started search for services of type %@ in domain %@"), serviceType, domainName); CFArrayAppendValue(logger->bonjourServiceBrowsers, browser); result = YES; } CFRelease(browser); return result; } static void LoggerServiceBrowserCallBack (CFNetServiceBrowserRef browser, CFOptionFlags flags, CFTypeRef domainOrService, CFStreamError* error, void* info) { #pragma unused (browser) #pragma unused (error) LOGGERDBG(CFSTR("LoggerServiceBrowserCallback browser=%@ flags=0x%04x domainOrService=%@ error=%d"), browser, flags, domainOrService, error==NULL ? 0 : error->error); Logger *logger = (Logger *)info; assert(logger != NULL); if (flags & kCFNetServiceFlagRemove) { if (!(flags & kCFNetServiceFlagIsDomain)) { CFNetServiceRef service = (CFNetServiceRef)domainOrService; CFIndex idx; for (idx = 0; idx < CFArrayGetCount(logger->bonjourServices); idx++) { if (CFArrayGetValueAtIndex(logger->bonjourServices, idx) == service) { CFNetServiceUnscheduleFromRunLoop(service, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); CFNetServiceClientContext context = {0, NULL, NULL, NULL, NULL}; CFNetServiceSetClient(service, NULL, &context); CFNetServiceCancel(service); CFArrayRemoveValueAtIndex(logger->bonjourServices, idx); break; } } } } else { if (flags & kCFNetServiceFlagIsDomain) { // start searching for services in this domain LoggerBrowseBonjourForServices(logger, (CFStringRef)domainOrService); } else { // a service has been found LOGGERDBG(CFSTR("Logger found service: %@"), domainOrService); CFNetServiceRef service = (CFNetServiceRef)domainOrService; if (service != NULL) { // if the user has specified that Logger shall only connect to the specified // Bonjour service name, check it now. This makes things easier in a teamwork // environment where multiple instances of NSLogger viewer may run on the // same network if (logger->bonjourServiceName != NULL) { LOGGERDBG(CFSTR("-> looking for services of name %@"), logger->bonjourServiceName); CFStringRef name = CFNetServiceGetName(service); if (name == NULL || kCFCompareEqualTo != CFStringCompare(name, logger->bonjourServiceName, kCFCompareCaseInsensitive | kCFCompareDiacriticInsensitive)) { LOGGERDBG(CFSTR("-> service name %@ does not match requested service name, ignoring."), name, logger->bonjourServiceName); return; } } else { // If the desktop viewer we found requested that only clients looking for its name can connect, // honor the request and do not connect. This helps with teams having multiple devices and multiple // desktops with NSLogger installed to avoid unwanted logs coming to a specific viewer // To indicate that the desktop only wants clients that are looking for its specific name, // the desktop sets the TXT record to be a dictionary containing the @"filterClients" key with value @"1" CFDataRef txtData = CFNetServiceGetTXTData(service); if (txtData != NULL) { CFDictionaryRef txtDict = CFNetServiceCreateDictionaryWithTXTData(NULL, txtData); if (txtDict != NULL) { const void *value = CFDictionaryGetValue(txtDict, CFSTR("filterClients")); Boolean mismatch = (value != NULL && CFGetTypeID((CFTypeRef)value) == CFStringGetTypeID() && CFStringCompare((CFStringRef)value, CFSTR("1"), 0) != kCFCompareEqualTo); CFRelease(txtDict); if (mismatch) { LOGGERDBG(CFSTR("-> service %@ requested that only clients looking for it do connect."), name, logger->bonjourServiceName); return; } } } } CFArrayAppendValue(logger->bonjourServices, service); LoggerTryConnect(logger); } } } } // ----------------------------------------------------------------------------- #pragma mark - #pragma mark Reachability & Connectivity Management // ----------------------------------------------------------------------------- static void LoggerRemoteSettingsChanged(Logger *logger) { // this is a callback for a runloop source, called on the logger thread // Always terminate any ongoing connection first LoggerWriteStreamTerminated(logger); if (logger->host == NULL && !(logger->options & kLoggerOption_BrowseBonjour)) { // developer doesn't want any network connection LoggerStopBonjourBrowsing(logger); LoggerStopReconnectTimer(logger); LoggerStopReachabilityChecking(logger); } else { // we may already have Reachability or Bonjour browsing running, // the calls do nothing if they are not needed LoggerStartReachabilityChecking(logger); if (logger->targetReachable) { if (logger->options & kLoggerOption_BrowseBonjour) LoggerStartBonjourBrowsing(logger); else LoggerStopBonjourBrowsing(logger); } LoggerTryConnect(logger); } } static void LoggerStartReachabilityChecking(Logger *logger) { if (logger->reachability == NULL) { if (logger->host != NULL) { // reachability targeted to the configured host LOGGERDBG(CFSTR("Starting SCNetworkReachability to wait for host %@ to be reachable"), logger->host); CFIndex length = CFStringGetLength(logger->host) * 3; char *buffer = (char *)malloc((size_t)length + 1); CFStringGetBytes(logger->host, CFRangeMake(0, CFStringGetLength(logger->host)), kCFStringEncodingUTF8, '?', false, (UInt8 *)buffer, length, &length); buffer[length] = 0; logger->reachability = SCNetworkReachabilityCreateWithName(NULL, buffer); free(buffer); } else { // reachability for generic connection to the internet LOGGERDBG(CFSTR("Starting SCNetworkReachability to wait for internet to be reachable"), logger->host); struct sockaddr_in addr; bzero(&addr, sizeof(addr)); addr.sin_len = (__uint8_t) sizeof(addr); addr.sin_family = AF_INET; logger->reachability = SCNetworkReachabilityCreateWithAddress(NULL, (const struct sockaddr *)&addr); } SCNetworkReachabilityContext context = {0, logger, NULL, NULL, NULL}; SCNetworkReachabilitySetCallback(logger->reachability, &LoggerReachabilityCallBack, &context); SCNetworkReachabilityScheduleWithRunLoop(logger->reachability, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); // arm the callback if (SCNetworkReachabilityGetFlags(logger->reachability, &logger->reachabilityFlags)) LoggerReachabilityCallBack(logger->reachability, logger->reachabilityFlags, logger); } } static void LoggerStopReachabilityChecking(Logger *logger) { if (logger->reachability != NULL) { LOGGERDBG(CFSTR("Stopping SCNetworkReachability")); SCNetworkReachabilityUnscheduleFromRunLoop(logger->reachability, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); CFRelease((CFTypeRef)logger->reachability); logger->reachability = NULL; } LoggerStopReconnectTimer(logger); logger->targetReachable = NO; } static void LoggerReachabilityCallBack(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info) { #pragma unused (target) Logger *logger = (Logger *)info; LOGGERDBG(CFSTR("LoggerReachabilityCallBack called with flags=0x%08lx"), flags); SCNetworkReachabilityFlags oldFlags = logger->reachabilityFlags; logger->reachabilityFlags = flags; if (flags & kSCNetworkReachabilityFlagsReachable) { // target host or internet became reachable LOGGERDBG(CFSTR("-> target became reachable")); logger->targetReachable = YES; // in the event a network transition occurred without network loss (i.e. WiFi -> 3G), // preemptively disconnect. In many cases, if the network stays up, we will never receive // a disconnection (possibly due to SSH ?) if (flags != oldFlags && logger->logStream != NULL) LoggerWriteStreamTerminated(logger); else LoggerTryConnect(logger); // will start Bonjour browsing if needed } else if (logger->connected || logger->logStream != NULL) { // lost internet connecton. Force a disconnect, we'll wait for the connection to become // available again LOGGERDBG(CFSTR("-> target became unreachable")); logger->targetReachable = NO; if (flags != oldFlags && logger->logStream != NULL) LoggerWriteStreamTerminated(logger); LoggerStopBonjourBrowsing(logger); LoggerStopReconnectTimer(logger); } } static void LoggerStartReconnectTimer(Logger *logger) { // start a timer that will try to reconnect every 5 seconds if (logger->reconnectTimer == NULL && (logger->host != NULL || (logger->options & kLoggerOption_BrowseBonjour))) { LOGGERDBG(CFSTR("Starting the reconnect timer")); CFRunLoopTimerContext timerCtx = { .version = 0, .info = logger, .retain = NULL, .release = NULL, .copyDescription = NULL }; logger->reconnectTimer = CFRunLoopTimerCreate(NULL, CFAbsoluteTimeGetCurrent() + 5, 5, // reconnect interval 0, 0, &LoggerTimedReconnectCallback, &timerCtx); if (logger->reconnectTimer != NULL) { LOGGERDBG(CFSTR("Starting the TimedReconnect timer to regularly retry the connection")); CFRunLoopAddTimer(CFRunLoopGetCurrent(), logger->reconnectTimer, kCFRunLoopCommonModes); } } } static void LoggerStopReconnectTimer(Logger *logger) { if (logger->reconnectTimer != NULL) { LOGGERDBG(CFSTR("Stopping the reconnect timer")); CFRunLoopTimerInvalidate(logger->reconnectTimer); CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), logger->reconnectTimer, kCFRunLoopCommonModes); CFRelease(logger->reconnectTimer); logger->reconnectTimer = NULL; } } static void LoggerTimedReconnectCallback(CFRunLoopTimerRef timer, void *info) { #pragma unused (timer) Logger *logger = (Logger *)info; assert(logger != NULL); LOGGERDBG(CFSTR("LoggerTimedReconnectCallback")); if (logger->logStream == NULL) { LOGGERDBG(CFSTR("-> trying to reconnect to host %@"), logger->host); LoggerTryConnect(logger); } else { LOGGERDBG(CFSTR("-> timer not needed anymore, removing it form runloop")); LoggerStopReconnectTimer(logger); } } // ----------------------------------------------------------------------------- #pragma mark - #pragma mark Stream management // ----------------------------------------------------------------------------- static BOOL LoggerConfigureAndOpenStream(Logger *logger) { // configure and open stream LOGGERDBG(CFSTR("LoggerConfigureAndOpenStream configuring and opening log stream")); CFStreamClientContext context = {0, (void *)logger, NULL, NULL, NULL}; if (CFWriteStreamSetClient(logger->logStream, (kCFStreamEventOpenCompleted | kCFStreamEventCanAcceptBytes | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered), &LoggerWriteStreamCallback, &context)) { if (logger->options & kLoggerOption_UseSSL) { // Configure stream to require a SSL connection LOGGERDBG(CFSTR("-> configuring SSL")); const void *SSLKeys[] = { kCFStreamSSLLevel, kCFStreamSSLValidatesCertificateChain, kCFStreamSSLIsServer, kCFStreamSSLPeerName }; const void *SSLValues[] = { kCFStreamSocketSecurityLevelNegotiatedSSL, kCFBooleanFalse, // no certificate chain validation (we use a self-signed certificate) kCFBooleanFalse, // not a server kCFNull }; #if TARGET_OS_IPHONE // workaround for TLS in iOS 5 as per TN2287 // see http://developer.apple.com/library/ios/#technotes/tn2287/_index.html#//apple_ref/doc/uid/DTS40011309 // if we are running iOS 5 or later, use a special mode that allows the stack to downgrade gracefully #if ALLOW_COCOA_USE @autoreleasepool { NSString *versionString = [[UIDevice currentDevice] systemVersion]; if ([versionString compare:@"5.0" options:NSNumericSearch] != NSOrderedAscending) SSLValues[0] = CFSTR("kCFStreamSocketSecurityLevelTLSv1_0SSLv3"); } #else // we can't find out, assume we _may_ be on iOS 5 but can't be certain // go for SSLv3 which works without the TLS 1.2 / 1.1 / 1.0 downgrade issue SSLValues[0] = kCFStreamSocketSecurityLevelSSLv3; #endif #endif CFDictionaryRef SSLDict = CFDictionaryCreate(NULL, SSLKeys, SSLValues, 4, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFWriteStreamSetProperty(logger->logStream, kCFStreamPropertySSLSettings, SSLDict); CFRelease(SSLDict); } CFWriteStreamScheduleWithRunLoop(logger->logStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); if (CFWriteStreamOpen(logger->logStream)) { LOGGERDBG(CFSTR("-> stream open attempt, waiting for open completion")); return YES; } LOGGERDBG(CFSTR("-> stream open failed.")); CFWriteStreamSetClient(logger->logStream, kCFStreamEventNone, NULL, NULL); if (CFWriteStreamGetStatus(logger->logStream) == kCFStreamStatusOpen) CFWriteStreamClose(logger->logStream); CFWriteStreamUnscheduleFromRunLoop(logger->logStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); } else { LOGGERDBG(CFSTR("-> stream set client failed.")); } CFRelease(logger->logStream); logger->logStream = NULL; return NO; } static void LoggerTryConnect(Logger *logger) { // Core function that attempts connection to found Bonjour services and configured Host LOGGERDBG(CFSTR("LoggerTryConnect, %d services registered, current stream=%@"), CFArrayGetCount(logger->bonjourServices), logger->logStream); // If we already have a connection established or being attempted, stop here if (logger->logStream != NULL) { LOGGERDBG(CFSTR("-> another connection is opened or in progress, giving up for now")); return; } // If reachability status is not known yet, just wait if (logger->targetReachable == NO) { LOGGERDBG(CFSTR("-> not sure target is reachable, let's wait and see")); return; } // If there are discovered Bonjour services, try them now while (CFArrayGetCount(logger->bonjourServices)) { CFNetServiceRef service = (CFNetServiceRef)CFArrayGetValueAtIndex(logger->bonjourServices, 0); LOGGERDBG(CFSTR("-> Trying to open write stream to service %@"), service); CFStreamCreatePairWithSocketToNetService(NULL, service, NULL, &logger->logStream); CFArrayRemoveValueAtIndex(logger->bonjourServices, 0); if (logger->logStream == NULL) { // create pair failed LOGGERDBG(CFSTR("-> failed.")); } else if (LoggerConfigureAndOpenStream(logger)) { // open is now in progress return; } } // If there is a host to directly connect to, try it now (this will happen before // Bonjour kicks in, Bonjour being handled as a fallback solution if direct Host // fails) if (logger->host != NULL) { LOGGERDBG(CFSTR("-> Trying to open direct connection to host %@ port %u"), logger->host, logger->port); CFStreamCreatePairWithSocketToHost(NULL, logger->host, logger->port, NULL, &logger->logStream); if (logger->logStream == NULL) { // Create stream failed LOGGERDBG(CFSTR("-> failed.")); if (logger->logStream != NULL) { CFRelease(logger->logStream); logger->logStream = NULL; } } else if (LoggerConfigureAndOpenStream(logger)) { // open is now in progress return; } LoggerStartReconnectTimer(logger); } // Finally, if Bonjour is enabled and not started yet, start it now. if (logger->options & kLoggerOption_BrowseBonjour) { if (logger->bonjourDomainBrowser == NULL || CFArrayGetCount(logger->bonjourServiceBrowsers) == 0) { LoggerStopBonjourBrowsing(logger); LoggerStartBonjourBrowsing(logger); } } } static void LoggerWriteStreamTerminated(Logger *logger) { LOGGERDBG(CFSTR("LoggerWriteStreamTerminated called")); if (logger->connected) { LOGGERDBG(CFSTR("-> Logger DISCONNECTED")); logger->connected = NO; } if (logger->logStream != NULL) { LOGGERDBG(CFSTR("-> disposing the write stream")); CFWriteStreamSetClient(logger->logStream, 0, NULL, NULL); CFWriteStreamUnscheduleFromRunLoop(logger->logStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); CFWriteStreamClose(logger->logStream); CFRelease(logger->logStream); logger->logStream = NULL; } if (logger->bufferReadStream != NULL) { // In the case the connection drops before we have flushed the // whole contents of the file, we choose to keep it integrally // and retransmit it when reconnecting to the viewer. The reason // of this choice is that we may have transmitted only part of // a message, and this may cause errors on the desktop side. LOGGERDBG(CFSTR("-> closing the bufferReadStream")); CFReadStreamClose(logger->bufferReadStream); CFRelease(logger->bufferReadStream); logger->bufferReadStream = NULL; } if (logger->bufferFile != NULL && logger->bufferWriteStream == NULL) LoggerCreateBufferWriteStream(logger); // ensure that any current block on LoggerFlush() gets unblocked pthread_cond_broadcast(&logger->logQueueEmpty); // tryConnect will take care of setting up the reconnect timer if needed if (logger->targetReachable && (logger->host != NULL || (logger->options & kLoggerOption_BrowseBonjour))) LoggerTryConnect(logger); } static void LoggerWriteStreamCallback(CFWriteStreamRef ws, CFStreamEventType event, void* info) { Logger *logger = (Logger *)info; assert(ws == logger->logStream); switch (event) { case kCFStreamEventOpenCompleted: // A stream open was complete. Cancel all bonjour browsing, // service resolution and connection attempts, and try to // write existing buffer contents LOGGERDBG(CFSTR("Logger CONNECTED")); logger->connected = YES; LoggerStopBonjourBrowsing(logger); LoggerStopReconnectTimer(logger); if (logger->bufferWriteStream != NULL) { // now that a connection is acquired, we can stop logging to a file CFWriteStreamClose(logger->bufferWriteStream); CFRelease(logger->bufferWriteStream); logger->bufferWriteStream = NULL; } if (logger->bufferFile != NULL) { // if a buffer file was defined, try to read its contents LoggerCreateBufferReadStream(logger); } LoggerPushClientInfoToFrontOfQueue(logger); LoggerWriteMoreData(logger); break; case kCFStreamEventCanAcceptBytes: LoggerWriteMoreData(logger); break; case kCFStreamEventErrorOccurred: { CFErrorRef error = CFWriteStreamCopyError(ws); LOGGERDBG(CFSTR("Logger stream error: %@"), error); CFRelease(error); // Fall-thru } case kCFStreamEventEndEncountered: LoggerWriteStreamTerminated(logger); break; // avoid warnings when building; cover all enum cases. case kCFStreamEventNone: case kCFStreamEventHasBytesAvailable: break; } } // ----------------------------------------------------------------------------- #pragma mark - #pragma mark Internal encoding functions // ----------------------------------------------------------------------------- static uint8_t *LoggerMessagePrepareForPart(CFMutableDataRef encoder, uint32_t requiredExtraBytes) { // Ensure a data block has the required storage capacity, update the total size and part count // then return a pointer for fast storage of the data uint8_t *p = CFDataGetMutableBytePtr(encoder); CFIndex size = CFDataGetLength(encoder); uint32_t oldSize = ntohl(*(uint32_t *)p); uint32_t newSize = oldSize + requiredExtraBytes; if ((newSize + 4) > size) { // grow by 64 bytes chunks CFDataSetLength(encoder, (newSize + 4 + 64) & ~63); p = CFDataGetMutableBytePtr(encoder); } *((uint32_t *)p) = htonl(newSize); p += 4; *((uint16_t *)p) = htons(ntohs(*(uint16_t *)p) + 1); // return a pointer to where new data must be put return p + oldSize; } static void LoggerMessageAddTimestamp(CFMutableDataRef encoder) { struct timeval t; if (gettimeofday(&t, NULL) == 0) { #if __LP64__ LoggerMessageAddInt64(encoder, t.tv_sec, PART_KEY_TIMESTAMP_S); LoggerMessageAddInt64(encoder, t.tv_usec, PART_KEY_TIMESTAMP_US); #else LoggerMessageAddInt32(encoder, t.tv_sec, PART_KEY_TIMESTAMP_S); LoggerMessageAddInt32(encoder, t.tv_usec, PART_KEY_TIMESTAMP_US); #endif } else { time_t ts = time(NULL); #if __LP64__ LoggerMessageAddInt64(encoder, ts, PART_KEY_TIMESTAMP_S); #else LoggerMessageAddInt32(encoder, ts, PART_KEY_TIMESTAMP_S); #endif } } static void LoggerMessageAddTimestampAndThreadID(CFMutableDataRef encoder) { LoggerMessageAddTimestamp(encoder); BOOL hasThreadName = NO; #if ALLOW_COCOA_USE // Getting the thread number is tedious, to say the least. Since there is // no direct way to get it, we have to do it sideways. Note that it can be dangerous // to use any Cocoa call when in a multithreaded application that only uses non-Cocoa threads // and for which Cocoa's multithreading has not been activated. We test for this case. BOOL inMainThread = [NSThread isMainThread]; if (inMainThread) { hasThreadName = YES; LoggerMessageAddString(encoder, CFSTR("Main thread"), PART_KEY_THREAD_ID); } else if ([NSThread isMultiThreaded]) { NSThread *thread = [NSThread currentThread]; NSString *name = [thread name]; if (![name length]) { // use the thread dictionary to store and retrieve the computed thread name NSMutableDictionary *threadDict = [thread threadDictionary]; name = [threadDict objectForKey:@"__$NSLoggerThreadName$__"]; if (name == nil) { @autoreleasepool { // optimize CPU use by computing the thread name once and storing it back // in the thread dictionary name = [thread description]; NSRange range = [name rangeOfString:@"num = "]; if (range.location != NSNotFound) { name = [NSString stringWithFormat:@"Thread %@", [name substringWithRange:NSMakeRange(range.location + range.length, [name length] - range.location - range.length - 1)]]; [threadDict setObject:name forKey:@"__$NSLoggerThreadName$__"]; } } } } if (name != nil) { LoggerMessageAddString(encoder, (CAST_TO_CFSTRING)name, PART_KEY_THREAD_ID); hasThreadName = YES; } } #endif if (!hasThreadName) { #if __LP64__ LoggerMessageAddInt64(encoder, (int64_t)pthread_self(), PART_KEY_THREAD_ID); #else LoggerMessageAddInt32(encoder, (int32_t)pthread_self(), PART_KEY_THREAD_ID); #endif } } static CFMutableDataRef LoggerMessageCreate(int32_t seq) { CFMutableDataRef encoder = CFDataCreateMutable(NULL, 0); if (encoder != NULL) { CFDataIncreaseLength(encoder, 64); uint8_t *p = CFDataGetMutableBytePtr(encoder); if (p != NULL) { // directly write the sequence number as first part of the message // so we find it quickly when inserting new messages in the queue if (seq) { p[3] = 8; // size 0x00000008 in big endian p[5] = 1; // part count 0x0001 p[6] = (uint8_t)PART_KEY_MESSAGE_SEQ; p[7] = (uint8_t)PART_TYPE_INT32; *(uint32_t *)(p + 8) = htonl(seq); // ARMv6 and later, x86 processors do just fine with unaligned accesses } else { // empty message with a 0 part count p[3] = 2; } } LoggerMessageAddTimestampAndThreadID(encoder); } return encoder; } static void LoggerMessageFinalize(CFMutableDataRef encoder) { // Finalize a message by reducing the CFData size to the actual used size if (encoder != NULL) { uint32_t *p = (uint32_t *)CFDataGetBytePtr(encoder); if (p != NULL) CFDataSetLength(encoder, ntohl(*p) + 4); } } static void LoggerMessageAddInt32(CFMutableDataRef encoder, int32_t anInt, int key) { uint8_t *p = LoggerMessagePrepareForPart(encoder, 6); if (p != NULL) { *p++ = (uint8_t)key; *p++ = (uint8_t)PART_TYPE_INT32; *(uint32_t *)p = htonl(anInt); // ARMv6 and later, x86 processors do just fine with unaligned accesses } } #if __LP64__ static void LoggerMessageAddInt64(CFMutableDataRef encoder, int64_t anInt, int key) { uint8_t *p = LoggerMessagePrepareForPart(encoder, 10); if (p != NULL) { *p++ = (uint8_t)key; *p++ = (uint8_t)PART_TYPE_INT64; uint32_t *q = (uint32_t *)p; *q++ = htonl((uint32_t)(anInt >> 32)); // ARMv6 and later, x86 processors do just fine with unaligned accesses *q = htonl((uint32_t)anInt); } } #endif static void LoggerMessageAddCString(CFMutableDataRef data, const char *aString, int key) { if (aString == NULL || *aString == 0) return; // convert to UTF-8 int len = (int)strlen(aString); uint8_t *buf = malloc((size_t)(2 * len)); if (buf != NULL) { int i, n = 0; for (i = 0; i < len; i++) { uint8_t c = (uint8_t)(*aString++); if (c < 0x80) buf[n++] = c; else { buf[n++] = 0xC0 | (c >> 6); buf[n++] = (c & 0x6F) | 0x80; } } if (n) { uint8_t *p = LoggerMessagePrepareForPart(data, (uint32_t)n+6); if (p != NULL) { *p++ = (uint8_t)key; *p++ = (uint8_t)PART_TYPE_STRING; *(uint32_t *)p = htonl(n); // ARMv6 and later, x86 processors do just fine with unaligned accesses memcpy(p + 4, buf, (size_t)n); } } free(buf); } } static void LoggerMessageAddString(CFMutableDataRef encoder, CFStringRef aString, int key) { if (aString == NULL) aString = CFSTR(""); // All strings are UTF-8 encoded uint32_t partSize = 0; uint8_t *bytes = NULL; CFIndex stringLength = CFStringGetLength(aString); CFIndex bytesLength = stringLength * 4; if (stringLength) { bytes = (uint8_t *)malloc((size_t)bytesLength + 4); if (bytes != NULL) { CFStringGetBytes(aString, CFRangeMake(0, stringLength), kCFStringEncodingUTF8, '?', false, bytes, bytesLength, &bytesLength); partSize = (uint32_t)bytesLength; } } uint8_t *p = LoggerMessagePrepareForPart(encoder, 6 + partSize); if (p != NULL) { *p++ = (uint8_t)key; *p++ = (uint8_t)PART_TYPE_STRING; *(uint32_t *)p = htonl(partSize); // ARMv6 and later, x86 processors do just fine with unaligned accesses if (partSize && bytes != NULL) memcpy(p + 4, bytes, (size_t)partSize); } if (bytes != NULL) free(bytes); } static void LoggerMessageAddData(CFMutableDataRef encoder, CFDataRef theData, int key, int partType) { if (theData != NULL) { CFIndex dataLength = CFDataGetLength(theData); uint8_t *p = LoggerMessagePrepareForPart(encoder, (uint32_t)dataLength + 6); if (p != NULL) { *p++ = (uint8_t)key; *p++ = (uint8_t)partType; *((uint32_t *)p) = htonl(dataLength); // ARMv6 and later, x86 processors do just fine with unaligned accesses if (dataLength) memcpy(p + 4, CFDataGetBytePtr(theData), (size_t)dataLength); } } } static uint32_t LoggerMessageGetSeq(CFDataRef message) { // Extract the sequence number from a message. When pushing messages to the queue, // we use this to guarantee the logging order according to the seq# // Since we now store the seq as first component, we only have to check whether // the first part is the sequence number, and extract it. uint8_t *p = (uint8_t *)CFDataGetBytePtr(message) + 4; uint16_t partCount = ntohs(*(uint16_t *)p); if (partCount) { if (p[2] == PART_KEY_MESSAGE_SEQ) return ntohl(*(uint32_t *)(p+4)); // ARMv6 and later, x86 processors do just fine with unaligned accesses } return 0; } // ----------------------------------------------------------------------------- #pragma mark - #pragma mark Private logging functions // ----------------------------------------------------------------------------- static void LoggerPushClientInfoToFrontOfQueue(Logger *logger) { // Extract client information from the main bundle, as well as platform info, // and assemble it to a message that will be put in front of the queue // Helps desktop viewer display who's talking to it // Note that we must be called from the logger work thread, as we don't // run through the message port to transmit this message to the queue CFBundleRef bundle = CFBundleGetMainBundle(); if (bundle == NULL) return; CFMutableDataRef encoder = LoggerMessageCreate(0); if (encoder != NULL) { LoggerMessageAddInt32(encoder, LOGMSG_TYPE_CLIENTINFO, PART_KEY_MESSAGE_TYPE); CFStringRef version = (CFStringRef)CFBundleGetValueForInfoDictionaryKey(bundle, kCFBundleVersionKey); if (version != NULL && CFGetTypeID(version) == CFStringGetTypeID()) LoggerMessageAddString(encoder, version, PART_KEY_CLIENT_VERSION); CFStringRef name = (CFStringRef)CFBundleGetValueForInfoDictionaryKey(bundle, kCFBundleNameKey); if (name != NULL) LoggerMessageAddString(encoder, name, PART_KEY_CLIENT_NAME); #if TARGET_OS_IPHONE && ALLOW_COCOA_USE if ([NSThread isMultiThreaded] || [NSThread isMainThread]) { @autoreleasepool { UIDevice *device = [UIDevice currentDevice]; LoggerMessageAddString(encoder, (CAST_TO_CFSTRING)device.name, PART_KEY_UNIQUEID); LoggerMessageAddString(encoder, (CAST_TO_CFSTRING)device.systemVersion, PART_KEY_OS_VERSION); LoggerMessageAddString(encoder, (CAST_TO_CFSTRING)device.systemName, PART_KEY_OS_NAME); LoggerMessageAddString(encoder, (CAST_TO_CFSTRING)device.model, PART_KEY_CLIENT_MODEL); } } #elif TARGET_OS_MAC CFStringRef osName = NULL, osVersion = NULL; #if ALLOW_COCOA_USE // Read the OS version without using deprecated Gestalt calls @autoreleasepool { @try { NSString* versionString = [[NSDictionary dictionaryWithContentsOfFile: @"/System/Library/CoreServices/SystemVersion.plist"] objectForKey: @"ProductVersion"]; if ([versionString length]) { osName = CFSTR("Mac OS X"); osVersion = CFRetain((CAST_TO_CFSTRING)versionString); } } @catch (NSException *exc) { } } #endif if (osVersion == NULL) { // Not allowed to call into Cocoa ? use the Darwin version string struct utsname u; if (uname(&u) == 0) { osName = CFStringCreateWithCString(NULL, u.sysname, kCFStringEncodingUTF8); osVersion = CFStringCreateWithCString(NULL, u.release, kCFStringEncodingUTF8); } else { osName = CFSTR("Mac OS X"); osVersion = CFSTR(""); } } LoggerMessageAddString(encoder, osVersion, PART_KEY_OS_VERSION); LoggerMessageAddString(encoder, osName, PART_KEY_OS_NAME); CFRelease(osVersion); CFRelease(osName); char buf[64]; size_t len; int ncpu = 0; bzero(buf, sizeof(buf)); len = sizeof(buf)-1; sysctlbyname("hw.model", buf, &len, NULL, 0); len = sizeof(ncpu); sysctlbyname("hw.ncpu", &ncpu, &len, NULL, 0); sprintf(buf+strlen(buf), " - %d * ", ncpu); len = sizeof(buf)-strlen(buf)-1; sysctlbyname("hw.machine", buf+strlen(buf), &len, NULL, 0); CFStringRef s = CFStringCreateWithCString(NULL, buf, kCFStringEncodingASCII); LoggerMessageAddString(encoder, s, PART_KEY_CLIENT_MODEL); CFRelease(s); #endif LoggerMessageFinalize(encoder); pthread_mutex_lock(&logger->logQueueMutex); CFArrayInsertValueAtIndex(logger->logQueue, logger->incompleteSendOfFirstItem ? 1 : 0, encoder); pthread_mutex_unlock(&logger->logQueueMutex); CFRelease(encoder); } } static void LoggerPushMessageToQueue(Logger *logger, CFDataRef message) { // Add the message to the log queue and signal the runLoop source that will trigger // a send on the worker thread. pthread_mutex_lock(&logger->logQueueMutex); CFIndex idx = CFArrayGetCount(logger->logQueue); if (idx) { // to prevent out-of-order messages (as much as possible), we try to transmit messages in the // order their sequence number was generated. Since the seq is generated first-thing, // we can provide fine-grained ordering that gives a reasonable idea of the order // the logging calls were made (useful for precise information about multithreading code) uint32_t lastSeq, seq = LoggerMessageGetSeq(message); do { lastSeq = LoggerMessageGetSeq(CFArrayGetValueAtIndex(logger->logQueue, idx-1)); } while (lastSeq > seq && --idx > 0); } if (idx >= 0) CFArrayInsertValueAtIndex(logger->logQueue, idx, message); else CFArrayAppendValue(logger->logQueue, message); pthread_mutex_unlock(&logger->logQueueMutex); if (logger->messagePushedSource != NULL) { // One case where the pushed source may be NULL is if the client code // immediately starts logging without initializing the logger first. // In this case, the worker thread has not completed startup, so we don't need // to fire the runLoop source CFRunLoopSourceSignal(logger->messagePushedSource); } else if (logger->workerThread == NULL && (logger->options & kLoggerOption_LogToConsole)) { // In this case, a failure creating the message runLoop source forces us // to always log to console pthread_mutex_lock(&logger->logQueueMutex); while (CFArrayGetCount(logger->logQueue)) { LoggerLogToConsole(CFArrayGetValueAtIndex(logger->logQueue, 0)); CFArrayRemoveValueAtIndex(logger->logQueue, 0); } pthread_mutex_unlock(&logger->logQueueMutex); pthread_cond_broadcast(&logger->logQueueEmpty); // in case other threads are waiting for a flush } } static void LogMessageRawTo_internal(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *message) { // Variant of the LogMessage function that doesn't perform any variable arguments formatting logger = LoggerStart(logger); // start if needed if (logger != NULL) { int32_t seq = OSAtomicIncrement32Barrier(&logger->messageSeq); LOGGERDBG2(CFSTR("%ld LogMessage"), seq); CFMutableDataRef encoder = LoggerMessageCreate(seq); if (encoder != NULL) { LoggerMessageAddInt32(encoder, LOGMSG_TYPE_LOG, PART_KEY_MESSAGE_TYPE); if (domain != nil && [domain length]) LoggerMessageAddString(encoder, (CAST_TO_CFSTRING)domain, PART_KEY_TAG); if (level) LoggerMessageAddInt32(encoder, level, PART_KEY_LEVEL); if (filename != NULL) LoggerMessageAddCString(encoder, filename, PART_KEY_FILENAME); if (lineNumber) LoggerMessageAddInt32(encoder, lineNumber, PART_KEY_LINENUMBER); if (functionName != NULL) LoggerMessageAddCString(encoder, functionName, PART_KEY_FUNCTIONNAME); if (message != nil) LoggerMessageAddString(encoder, (CAST_TO_CFSTRING)message, PART_KEY_MESSAGE); else LoggerMessageAddString(encoder, CFSTR(""), PART_KEY_MESSAGE); LoggerMessageFinalize(encoder); LoggerPushMessageToQueue(logger, encoder); CFRelease(encoder); } else { LOGGERDBG2(CFSTR("-> failed creating encoder")); } } } static void LogMessageTo_internal(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *format, va_list args) { logger = LoggerStart(logger); // start if needed if (logger != NULL) { int32_t seq = OSAtomicIncrement32Barrier(&logger->messageSeq); LOGGERDBG2(CFSTR("%ld LogMessage"), seq); CFMutableDataRef encoder = LoggerMessageCreate(seq); if (encoder != NULL) { LoggerMessageAddInt32(encoder, LOGMSG_TYPE_LOG, PART_KEY_MESSAGE_TYPE); if (domain != nil && [domain length]) LoggerMessageAddString(encoder, (CAST_TO_CFSTRING)domain, PART_KEY_TAG); if (level) LoggerMessageAddInt32(encoder, level, PART_KEY_LEVEL); if (filename != NULL) LoggerMessageAddCString(encoder, filename, PART_KEY_FILENAME); if (lineNumber) LoggerMessageAddInt32(encoder, lineNumber, PART_KEY_LINENUMBER); if (functionName != NULL) LoggerMessageAddCString(encoder, functionName, PART_KEY_FUNCTIONNAME); #if ALLOW_COCOA_USE // Go though NSString to avoid low-level logging of CF datastructures (i.e. too detailed NSDictionary, etc) NSString *msgString = [[NSString alloc] initWithFormat:format arguments:args]; if (msgString != nil) { LoggerMessageAddString(encoder, (CAST_TO_CFSTRING)msgString, PART_KEY_MESSAGE); RELEASE(msgString); } #else CFStringRef msgString = CFStringCreateWithFormatAndArguments(NULL, NULL, (CFStringRef)format, args); if (msgString != NULL) { LoggerMessageAddString(encoder, msgString, PART_KEY_MESSAGE); CFRelease(msgString); } #endif LoggerMessageFinalize(encoder); LoggerPushMessageToQueue(logger, encoder); CFRelease(encoder); } else { LOGGERDBG2(CFSTR("-> failed creating encoder")); } } } static void LogImageTo_internal(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, int width, int height, NSData *data) { logger = LoggerStart(logger); // start if needed if (logger != NULL) { int32_t seq = OSAtomicIncrement32Barrier(&logger->messageSeq); LOGGERDBG2(CFSTR("%ld LogImage"), seq); CFMutableDataRef encoder = LoggerMessageCreate(seq); if (encoder != NULL) { LoggerMessageAddInt32(encoder, LOGMSG_TYPE_LOG, PART_KEY_MESSAGE_TYPE); if (domain != nil && [domain length]) LoggerMessageAddString(encoder, (CAST_TO_CFSTRING)domain, PART_KEY_TAG); if (level) LoggerMessageAddInt32(encoder, level, PART_KEY_LEVEL); if (width && height) { LoggerMessageAddInt32(encoder, width, PART_KEY_IMAGE_WIDTH); LoggerMessageAddInt32(encoder, height, PART_KEY_IMAGE_HEIGHT); } if (filename != NULL) LoggerMessageAddCString(encoder, filename, PART_KEY_FILENAME); if (lineNumber) LoggerMessageAddInt32(encoder, lineNumber, PART_KEY_LINENUMBER); if (functionName != NULL) LoggerMessageAddCString(encoder, functionName, PART_KEY_FUNCTIONNAME); LoggerMessageAddData(encoder, (CAST_TO_CFDATA)data, PART_KEY_MESSAGE, PART_TYPE_IMAGE); LoggerMessageFinalize(encoder); LoggerPushMessageToQueue(logger, encoder); CFRelease(encoder); } else { LOGGERDBG2(CFSTR("-> failed creating encoder")); } } } static void LogDataTo_internal(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSData *data) { logger = LoggerStart(logger); // start if needed if (logger != NULL) { int32_t seq = OSAtomicIncrement32Barrier(&logger->messageSeq); LOGGERDBG2(CFSTR("%ld LogData"), seq); CFMutableDataRef encoder = LoggerMessageCreate(seq); if (encoder != NULL) { LoggerMessageAddInt32(encoder, LOGMSG_TYPE_LOG, PART_KEY_MESSAGE_TYPE); if (domain != nil && [domain length]) LoggerMessageAddString(encoder, (CAST_TO_CFSTRING)domain, PART_KEY_TAG); if (level) LoggerMessageAddInt32(encoder, level, PART_KEY_LEVEL); if (filename != NULL) LoggerMessageAddCString(encoder, filename, PART_KEY_FILENAME); if (lineNumber) LoggerMessageAddInt32(encoder, lineNumber, PART_KEY_LINENUMBER); if (functionName != NULL) LoggerMessageAddCString(encoder, functionName, PART_KEY_FUNCTIONNAME); LoggerMessageAddData(encoder, (CAST_TO_CFDATA)data, PART_KEY_MESSAGE, PART_TYPE_BINARY); LoggerMessageFinalize(encoder); LoggerPushMessageToQueue(logger, encoder); CFRelease(encoder); } else { LOGGERDBG2(CFSTR("-> failed creating encoder")); } } } static void LogStartBlockTo_internal(Logger *logger, NSString *format, va_list args) { logger = LoggerStart(logger); // start if needed if (logger) { int32_t seq = OSAtomicIncrement32Barrier(&logger->messageSeq); LOGGERDBG2(CFSTR("%ld LogStartBlock"), seq); CFMutableDataRef encoder = LoggerMessageCreate(seq); if (encoder != NULL) { LoggerMessageAddInt32(encoder, LOGMSG_TYPE_BLOCKSTART, PART_KEY_MESSAGE_TYPE); if (format != nil) { CFStringRef msgString = CFStringCreateWithFormatAndArguments(NULL, NULL, (CAST_TO_CFSTRING)format, args); if (msgString != NULL) { LoggerMessageAddString(encoder, msgString, PART_KEY_MESSAGE); CFRelease(msgString); } } LoggerMessageFinalize(encoder); LoggerPushMessageToQueue(logger, encoder); CFRelease(encoder); } } } // ----------------------------------------------------------------------------- #pragma mark - #pragma mark Public logging functions // ----------------------------------------------------------------------------- void LogMessageRaw(NSString *message) { LogMessageRawTo_internal(NULL, NULL, 0, NULL, nil, 0, message); } void LogMessageRawF(const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *message) { LogMessageRawTo_internal(NULL, filename, lineNumber, functionName, domain, level, message); } void LogMessageRawToF(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *message) { LogMessageRawTo_internal(logger, filename, lineNumber, functionName, domain, level, message); } void LogMessageCompat(NSString *format, ...) { va_list args; va_start(args, format); LogMessageTo_internal(NULL, NULL, 0, NULL, nil, 0, format, args); va_end(args); } void LogMessageTo(Logger *logger, NSString *domain, int level, NSString *format, ...) { va_list args; va_start(args, format); LogMessageTo_internal(logger, NULL, 0, NULL, domain, level, format, args); va_end(args); } void LogMessageToF(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *format, ...) { va_list args; va_start(args, format); LogMessageTo_internal(logger, filename, lineNumber, functionName, domain, level, format, args); va_end(args); } void LogMessageTo_va(Logger *logger, NSString *domain, int level, NSString *format, va_list args) { LogMessageTo_internal(logger, NULL, 0, NULL, domain, level, format, args); } void LogMessageToF_va(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *format, va_list args) { LogMessageTo_internal(logger, filename, lineNumber, functionName, domain, level, format, args); } void LogMessage(NSString *domain, int level, NSString *format, ...) { va_list args; va_start(args, format); LogMessageTo_internal(NULL, NULL, 0, NULL, domain, level, format, args); va_end(args); } void LogMessageF(const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *format, ...) { va_list args; va_start(args, format); LogMessageTo_internal(NULL, filename, lineNumber, functionName, domain, level, format, args); va_end(args); } void LogMessage_va(NSString *domain, int level, NSString *format, va_list args) { LogMessageTo_internal(NULL, NULL, 0, NULL, domain, level, format, args); } void LogMessageF_va(const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSString *format, va_list args) { LogMessageTo_internal(NULL, filename, lineNumber, functionName, domain, level, format, args); } void LogData(NSString *domain, int level, NSData *data) { LogDataTo_internal(NULL, NULL, 0, NULL, domain, level, data); } void LogDataF(const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSData *data) { LogDataTo_internal(NULL, filename, lineNumber, functionName, domain, level, data); } void LogDataTo(Logger *logger, NSString *domain, int level, NSData *data) { LogDataTo_internal(logger, NULL, 0, NULL, domain, level, data); } void LogDataToF(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, NSData *data) { LogDataTo_internal(logger, filename, lineNumber, functionName, domain, level, data); } void LogImageData(NSString *domain, int level, int width, int height, NSData *data) { LogImageTo_internal(NULL, NULL, 0, NULL, domain, level, width, height, data); } void LogImageDataF(const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, int width, int height, NSData *data) { LogImageTo_internal(NULL, filename, lineNumber, functionName, domain, level, width, height, data); } void LogImageDataTo(Logger *logger, NSString *domain, int level, int width, int height, NSData *data) { LogImageTo_internal(logger, NULL, 0, NULL, domain, level, width, height, data); } void LogImageDataToF(Logger *logger, const char *filename, int lineNumber, const char *functionName, NSString *domain, int level, int width, int height, NSData *data) { LogImageTo_internal(logger, filename, lineNumber, functionName, domain, level, width, height, data); } void LogStartBlock(NSString *format, ...) { va_list args; va_start(args, format); LogStartBlockTo_internal(NULL, format, args); va_end(args); } void LogStartBlockTo(Logger *logger, NSString *format, ...) { va_list args; va_start(args, format); LogStartBlockTo_internal(logger, format, args); va_end(args); } void LogEndBlockTo(Logger *logger) { logger = LoggerStart(logger); if (logger) { if (logger->options & kLoggerOption_LogToConsole) return; int32_t seq = OSAtomicIncrement32Barrier(&logger->messageSeq); LOGGERDBG2(CFSTR("%ld LogEndBlock"), seq); CFMutableDataRef encoder = LoggerMessageCreate(seq); if (encoder != NULL) { LoggerMessageAddInt32(encoder, LOGMSG_TYPE_BLOCKEND, PART_KEY_MESSAGE_TYPE); LoggerMessageFinalize(encoder); LoggerPushMessageToQueue(logger, encoder); CFRelease(encoder); } else { LOGGERDBG2(CFSTR("-> failed creating encoder")); } } } void LogEndBlock(void) { LogEndBlockTo(NULL); } void LogMarkerTo(Logger *logger, NSString *text) { logger = LoggerStart(logger); // start if needed if (logger != NULL) { int32_t seq = OSAtomicIncrement32Barrier(&logger->messageSeq); LOGGERDBG2(CFSTR("%ld LogMarker"), seq); CFMutableDataRef encoder = LoggerMessageCreate(seq); if (encoder != NULL) { LoggerMessageAddInt32(encoder, LOGMSG_TYPE_MARK, PART_KEY_MESSAGE_TYPE); if (text == nil) { CFDateFormatterRef df = CFDateFormatterCreate(NULL, NULL, kCFDateFormatterShortStyle, kCFDateFormatterMediumStyle); CFStringRef str = CFDateFormatterCreateStringWithAbsoluteTime(NULL, df, CFAbsoluteTimeGetCurrent()); CFRelease(df); if (str != NULL) { LoggerMessageAddString(encoder, str, PART_KEY_MESSAGE); CFRelease(str); } } else { LoggerMessageAddString(encoder, (CAST_TO_CFSTRING)text, PART_KEY_MESSAGE); } LoggerMessageFinalize(encoder); LoggerPushMessageToQueue(logger, encoder); CFRelease(encoder); } else { LOGGERDBG2(CFSTR("-> failed creating encoder")); } } } void LogMarker(NSString *text) { LogMarkerTo(NULL, text); }