Oolite 1.91.0.7604-240417-a536cbe
Loading...
Searching...
No Matches
OOLogOutputHandler.m
Go to the documentation of this file.
1/*
2
3OOLogOutputHandler.m
4By Jens Ayton
5
6
7Copyright (C) 2007-2013 Jens Ayton and contributors
8
9Permission is hereby granted, free of charge, to any person obtaining a copy
10of this software and associated documentation files (the "Software"), to deal
11in the Software without restriction, including without limitation the rights
12to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13copies of the Software, and to permit persons to whom the Software is
14furnished to do so, subject to the following conditions:
15
16The above copyright notice and this permission notice shall be included in all
17copies or substantial portions of the Software.
18
19THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25SOFTWARE.
26
27*/
28
29
30#define OOLOG_POISON_NSLOG 0
31
33#import "OOLogging.h"
34#import "OOAsyncQueue.h"
35#include <stdlib.h>
36#include <stdio.h>
39
40
41#undef NSLog // We need to be able to call the real NSLog.
42
43
44#if OOLITE_MAC_OS_X
45
46#include <dlfcn.h>
47
48#ifndef NDEBUG
49
50#define SET_CRASH_REPORTER_INFO 1
51
52// Function to set "Application Specific Information" field in crash reporter log in Leopard.
53// Extremely unsupported, so not used in release builds.
54static void InitCrashReporterInfo(void);
55static void SetCrashReporterInfo(const char *info);
57
58#endif
59
60
61typedef void (*LogCStringFunctionProc)(const char *string, unsigned length, BOOL withSyslogBanner);
64
67
68static void LoadLogCStringFunctions(void);
69static void OONSLogCStringFunction(const char *string, unsigned length, BOOL withSyslogBanner);
70
71static NSString *GetAppName(void);
72
74
75#elif OOLITE_GNUSTEP
76
77static void OONSLogPrintfHandler(NSString *message);
78
79#else
80#error Unknown platform!
81#endif
82
83static BOOL DirectoryExistCreatingIfNecessary(NSString *path);
84
85
86#define kFlushInterval 2.0 // Lower bound on interval between explicit log file flushes.
87
88
89@interface OOAsyncLogger: NSObject
90{
91@private
93 NSConditionLock *threadStateMonitor;
94 NSFileHandle *logFile;
95 NSTimer *flushTimer;
96}
97
98- (void)asyncLogMessage:(NSString *)message;
99- (void)endLogging;
100
101- (void)changeFile;
102
103// Internal
104- (BOOL)startLogging;
105- (void)loggerThread;
106- (void)flushLog;
107
108@end
109
110
111static BOOL sInited = NO;
112static BOOL sWriteToStderr = YES;
113static BOOL sWriteToStdout = NO;
114static BOOL sSaturated = NO;
116static NSString *sLogFileName = @"Latest.log";
117
118
120{
121 if (sInited) return;
122
123#if SET_CRASH_REPORTER_INFO
125#endif
126
127 sLogger = [[OOAsyncLogger alloc] init];
128 sInited = YES;
129
130 if (sLogger != nil)
131 {
132 sWriteToStderr = [[NSUserDefaults standardUserDefaults] boolForKey:@"logging-echo-to-stderr"];
133 }
134 else
135 {
136 sWriteToStderr = YES;
137 }
138
139#if OOLITE_MAC_OS_X
141 if (_NSSetLogCStringFunction != NULL)
142 {
145 }
146 else
147 {
148 OOLog(@"logging.nsLogFilter.install.failed", @"Failed to install NSLog() filter; system messages will not be logged in log file.");
149 }
150#elif GNUSTEP
151 NSRecursiveLock *lock = GSLogLock();
152 [lock lock];
153 _NSLog_printf_handler = OONSLogPrintfHandler;
154 [lock unlock];
155#endif
156
158}
159
160
162{
163 if (sInited)
164 {
165 sWriteToStderr = YES;
166 sInited = NO;
167
168 [sLogger endLogging];
170
171#if OOLITE_MAC_OS_X
173 {
176 }
177#elif GNUSTEP
178 NSRecursiveLock *lock = GSLogLock();
179 [lock lock];
180 _NSLog_printf_handler = NULL;
181 [lock unlock];
182#endif
183 }
184}
185
194
195void OOLogOutputHandlerPrint(NSString *string)
196{
197 if (sInited && sLogger != nil && !sWriteToStdout) [sLogger asyncLogMessage:string];
198
199 BOOL doCStringStuff = sWriteToStderr || sWriteToStdout;
200#if SET_CRASH_REPORTER_INFO
201 doCStringStuff = doCStringStuff || sCrashReporterInfoAvailable;
202#endif
203
204 if (doCStringStuff)
205 {
206 const char *cStr = [[string stringByAppendingString:@"\n"] UTF8String];
207 if (sWriteToStdout)
208 fputs(cStr, stdout);
209 else if (sWriteToStderr)
210 fputs(cStr, stderr);
211
212#if SET_CRASH_REPORTER_INFO
214#endif
215 }
216
217}
218
219
221{
222 return [OOLogHandlerGetLogBasePath() stringByAppendingPathComponent:sLogFileName];
223}
224
225
226void OOLogOutputHandlerChangeLogFile(NSString *newLogName)
227{
228 if (![sLogFileName isEqual:newLogName])
229 {
230 sLogFileName = [newLogName copy];
231 [sLogger changeFile];
232 }
233}
234
235
236enum
237{
241
242
243@implementation OOAsyncLogger
244
245- (id)init
246{
247 BOOL OK = YES;
248 NSString *logPath = nil;
249 NSString *oldPath = nil;
250 NSFileManager *fmgr = nil;
251
252 self = [super init];
253 if (self == nil) OK = NO;
254
255 if (OK)
256 {
257 fmgr = [NSFileManager defaultManager];
258 logPath = OOLogHandlerGetLogPath();
259
260 // If there is an existing file, move it to Previous.log.
261 if ([fmgr fileExistsAtPath:logPath])
262 {
263 oldPath = [OOLogHandlerGetLogBasePath() stringByAppendingPathComponent:@"Previous.log"];
264 [fmgr oo_removeItemAtPath:oldPath];
265 if (![fmgr oo_moveItemAtPath:logPath toPath:oldPath])
266 {
267 if (![fmgr oo_removeItemAtPath:logPath])
268 {
269 NSLog(@"Log setup: could not move or delete existing log at %@, will log to stdout instead.", logPath);
270 OK = NO;
271 }
272 }
273 }
274 }
275
276 if (OK) OK = [self startLogging];
277
278 if (!OK) DESTROY(self);
279
280 return self;
281}
282
283
284- (void)dealloc
285{
289 // We don't own a reference to flushTimer.
290
291 [super dealloc];
292}
293
294
295- (BOOL)startLogging
296{
297 BOOL OK = YES;
298 NSString *logPath = nil;
299 NSFileManager *fmgr = nil;
300
301 fmgr = [NSFileManager defaultManager];
302
303 if (OK)
304 {
305 messageQueue = [[OOAsyncQueue alloc] init];
306 if (messageQueue == nil) OK = NO;
307 }
308
309 if (OK)
310 {
311 // set up threadStateMonitor -- used as a binary semaphore of sorts to check when the worker thread starts and stops.
312 threadStateMonitor = [[NSConditionLock alloc] initWithCondition:kConditionReadyToDealloc];
313 if (threadStateMonitor == nil) OK = NO;
314 [threadStateMonitor setName:@"OOLogOutputHandler thread state monitor"];
315 }
316
317 if (OK)
318 {
319 // Create work thread to actually handle messages.
320 // This needs to be done early to avoid messy state if something goes wrong.
321 [NSThread detachNewThreadSelector:@selector(loggerThread) toTarget:self withObject:nil];
322 // Wait for it to start.
323 if (![threadStateMonitor lockWhenCondition:kConditionWorking beforeDate:[NSDate dateWithTimeIntervalSinceNow:5.0]])
324 {
325 // If it doesn't signal a start within five seconds, assume something's wrong.
326 // Send kill signal, just in case it comes to life...
327 [messageQueue enqueue:@"die"];
328 // ...and stop -dealloc from waiting for thread death
329 [threadStateMonitor release];
331 OK = NO;
332 }
333 [threadStateMonitor unlockWithCondition:kConditionWorking];
334 }
335
336 if (OK)
337 {
338 logPath = OOLogHandlerGetLogPath();
339 OK = (logPath != nil);
340 }
341
342 if (OK)
343 {
344 // Create shiny new log file
345 OK = [fmgr createFileAtPath:logPath contents:nil attributes:nil];
346 if (OK)
347 {
348 logFile = [[NSFileHandle fileHandleForWritingAtPath:logPath] retain];
349 OK = (logFile != nil);
350 }
351 if (!OK)
352 {
353 NSLog(@"Log setup: could not open log at %@, will log to stdout instead.", logPath);
354 OK = NO;
355 }
356 }
357
358 return OK;
359}
360
361
362- (void)endLogging
363{
364 NSString *postamble = nil;
365
367 {
368 // We're fully inited; write postamble, wait for worker thread to terminate cleanly, and close file.
369 postamble = [NSString stringWithFormat:@"\nClosing log at %@.", [NSDate date]];
370 [self asyncLogMessage:postamble];
371 [messageQueue enqueue:@"die"]; // Kill message
372 [threadStateMonitor lockWhenCondition:kConditionReadyToDealloc];
373 [threadStateMonitor unlock];
374
375 [logFile closeFile];
376 }
377}
378
379
380- (void)changeFile
381{
382 [self endLogging];
383 if (![self startLogging]) sWriteToStderr = YES;
384}
385
386
387- (void)asyncLogMessage:(NSString *)message
388{
389 // Don't log of saturated flag is set.
390 if (sSaturated) return;
391
392 if (message != nil)
393 {
394 message = [message stringByAppendingString:@"\n"];
395
396#if OOLITE_WINDOWS
397 // Convert Unix line endings to Windows ones.
398 NSArray *messageComponents = [message componentsSeparatedByString:@"\n"];
399 message = [messageComponents componentsJoinedByString:@"\r\n"];
400#endif
401
402 [messageQueue enqueue:[message dataUsingEncoding:NSUTF8StringEncoding]];
403
404 if (flushTimer == nil)
405 {
406 // No pending flush
407 flushTimer = [NSTimer scheduledTimerWithTimeInterval:kFlushInterval target:self selector:@selector(flushLog) userInfo:nil repeats:NO];
408 }
409 }
410}
411
412
413- (void)flushLog
414{
415 flushTimer = nil;
416 [messageQueue enqueue:@"flush"];
417}
418
419
420- (void)loggerThread
421{
422 id message = nil;
423 NSAutoreleasePool *rootPool = nil, *pool = nil;
424 NSUInteger size = 0;
425
426 rootPool = [[NSAutoreleasePool alloc] init];
427 [NSThread ooSetCurrentThreadName:@"OOLogOutputHandler logging thread"];
428
429 // Signal readiness
430 [messageQueue retain];
431 [threadStateMonitor lock];
432 [threadStateMonitor unlockWithCondition:kConditionWorking];
433
434 @try
435 {
436 for (;;)
437 {
438 pool = [[NSAutoreleasePool alloc] init];
439
440 message = [messageQueue dequeue];
441
442 if (!sSaturated && [message isKindOfClass:[NSData class]])
443 {
444 size += [message length];
445 if (size > 1 << 30) // 1 GiB
446 {
447 sSaturated = YES;
448#if OOLITE_WINDOWS
449 message = @"\r\n\r\n\r\n***** LOG TRUNCATED DUE TO EXCESSIVE LENGTH *****\r\n";
450#else
451 message = @"\n\n\n***** LOG TRUNCATED DUE TO EXCESSIVE LENGTH *****\n";
452#endif
453 message = [message dataUsingEncoding:NSUTF8StringEncoding];
454 }
455
456 [logFile writeData:message];
457 }
458 else if ([message isEqual:@"flush"])
459 {
460 [logFile synchronizeFile];
461 }
462 else if ([message isEqual:@"die"])
463 {
464 break;
465 }
466
467 [pool release];
468 }
469 }
470 @catch (NSException *exception) {}
471 [pool release];
472
473 // Clean up; after this, ivars are out of bounds.
474 [messageQueue release];
475 [threadStateMonitor lock];
476 [threadStateMonitor unlockWithCondition:kConditionReadyToDealloc];
477
478 [rootPool release];
479}
480
481@end
482
483
484#if OOLITE_MAC_OS_X
485
486/* LoadLogCStringFunctions()
487
488 We wish to make NSLogv() call our custom function OONSLogCStringFunction()
489 rather than printing to stdout, by calling _NSSetLogCStringFunction().
490 Additionally, in order to close the logger cleanly, we wish to be able to
491 restore the standard logger, which requires us to call
492 _NSLogCStringFunction(). These functions are private.
493 _NSLogCStringFunction() is undocumented. _NSSetLogCStringFunction() is
494 documented at http://docs.info.apple.com/article.html?artnum=70081 ,
495 with the warning:
496
497 Be aware that this code references private APIs; this is an
498 unsupported workaround and users should use these instructions at
499 their own risk. Apple will not guarantee or provide support for
500 this procedure.
501
502 The approach taken here is to load the functions dynamically. This makes
503 us safe in the case of Apple removing the functions. In the unlikely event
504 that they change the functions' paramters without renaming them, we would
505 have a problem.
506*/
507static void LoadLogCStringFunctions(void)
508{
509 CFBundleRef foundationBundle = NULL;
510 LogCStringFunctionGetterProc getter = NULL;
511 LogCStringFunctionSetterProc setter = NULL;
512
513 foundationBundle = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.Foundation"));
514 if (foundationBundle != NULL)
515 {
516 getter = CFBundleGetFunctionPointerForName(foundationBundle, CFSTR("_NSLogCStringFunction"));
517 setter = CFBundleGetFunctionPointerForName(foundationBundle, CFSTR("_NSSetLogCStringFunction"));
518
519 if (getter != NULL && setter != NULL)
520 {
521 _NSLogCStringFunction = getter;
523 }
524 }
525}
526
527
528static void OONSLogCStringFunction(const char *string, unsigned length, BOOL withSyslogBanner)
529{
530 if (OOLogWillDisplayMessagesInClass(@"system"))
531 {
532 OOLogWithFunctionFileAndLine(@"system", NULL, NULL, 0, @"%s", string);
533 }
534}
535
536#elif OOLITE_GNUSTEP
537
538static void OONSLogPrintfHandler(NSString *message)
539{
540 if (OOLogWillDisplayMessagesInClass(@"gnustep"))
541 {
542 OOLogWithFunctionFileAndLine(@"gnustep", NULL, NULL, 0, @"%@", message);
543 }
544}
545
546#endif
547
548
549static BOOL DirectoryExistCreatingIfNecessary(NSString *path)
550{
551 BOOL exists, directory;
552 NSFileManager *fmgr = [NSFileManager defaultManager];
553
554 exists = [fmgr fileExistsAtPath:path isDirectory:&directory];
555
556 if (exists && !directory)
557 {
558 NSLog(@"Log setup: expected %@ to be a folder, but it is a file.", path);
559 return NO;
560 }
561 if (!exists)
562 {
563 if (![fmgr oo_createDirectoryAtPath:path attributes:nil])
564 {
565 NSLog(@"Log setup: could not create folder %@.", path);
566 return NO;
567 }
568 }
569
570 return YES;
571}
572
573
574#if OOLITE_MAC_OS_X
575
576static void ExcludeFromTimeMachine(NSString *path);
577
578
580{
581 static NSString *basePath = nil;
582
583 if (basePath == nil)
584 {
585 // ~/Library
586 basePath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0];
587
588 // ~/Library/Logs
589 basePath = [basePath stringByAppendingPathComponent:@"Logs"];
590 if (!DirectoryExistCreatingIfNecessary(basePath)) return nil;
591
592 // ~/Library/Logs/Oolite
593 basePath = [basePath stringByAppendingPathComponent:GetAppName()];
594 if (!DirectoryExistCreatingIfNecessary(basePath)) return nil;
595 ExcludeFromTimeMachine(basePath);
596
597 [basePath retain];
598 }
599
600 return basePath;
601}
602
603
604static void ExcludeFromTimeMachine(NSString *path)
605{
606 OSStatus (*CSBackupSetItemExcluded)(NSURL *item, Boolean exclude, Boolean excludeByPath) = NULL;
607 CFBundleRef carbonCoreBundle = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.CoreServices.CarbonCore"));
608 if (carbonCoreBundle)
609 {
610 CSBackupSetItemExcluded = CFBundleGetFunctionPointerForName(carbonCoreBundle, CFSTR("CSBackupSetItemExcluded"));
611 if (CSBackupSetItemExcluded != NULL)
612 {
613 (void)CSBackupSetItemExcluded([NSURL fileURLWithPath:path], YES, NO);
614 }
615 }
616}
617
618
619static NSString *GetAppName(void)
620{
621 static NSString *appName = nil;
622 NSBundle *bundle = nil;
623
624 if (appName == nil)
625 {
626 bundle = [NSBundle mainBundle];
627 appName = [bundle objectForInfoDictionaryKey:@"CFBundleName"];
628 if (appName == nil) appName = [bundle bundleIdentifier];
629 if (appName == nil) appName = @"<unknown application>";
630 [appName retain];
631 }
632
633 return appName;
634}
635
636#elif OOLITE_LINUX
637
638NSString *OOLogHandlerGetLogBasePath(void)
639{
640 static NSString *basePath = nil;
641
642 if (basePath == nil)
643 {
644 // ~
645 basePath = NSHomeDirectory();
646
647 // ~/.Oolite
648 basePath = [basePath stringByAppendingPathComponent:@".Oolite"];
649 if (!DirectoryExistCreatingIfNecessary(basePath)) return nil;
650
651 // ~/.Oolite/Logs
652 basePath = [basePath stringByAppendingPathComponent:@"Logs"];
653 if (!DirectoryExistCreatingIfNecessary(basePath)) return nil;
654
655 [basePath retain];
656 }
657
658 return basePath;
659}
660
661#elif OOLITE_WINDOWS
662
663NSString *OOLogHandlerGetLogBasePath(void)
664{
665 static NSString *basePath = nil;
666
667 if (basePath == nil)
668 {
669 // <Install path>\Oolite
670 basePath = NSHomeDirectory();
671
672 // <Install path>\Oolite\Logs
673 basePath = [basePath stringByAppendingPathComponent:@"Logs"];
674 if (!DirectoryExistCreatingIfNecessary(basePath)) return nil;
675
676 [basePath retain];
677 }
678
679 return basePath;
680}
681
682#endif
683
684
685#if SET_CRASH_REPORTER_INFO
686
687static char **sCrashReporterInfo = NULL;
688static char *sOldCrashReporterInfo = NULL;
690
691// Evil hackery based on http://www.allocinit.net/blog/2008/01/04/application-specific-information-in-leopard-crash-reports/
692static void InitCrashReporterInfo(void)
693{
694 sCrashReporterInfo = dlsym(RTLD_DEFAULT, "__crashreporter_info__");
695 if (sCrashReporterInfo != NULL)
696 {
697 sCrashReporterInfoLock = [[NSLock alloc] init];
699 {
701 }
702 else
703 {
704 sCrashReporterInfo = NULL;
705 }
706 }
707}
708
709static void SetCrashReporterInfo(const char *info)
710{
711 char *copy = NULL, *old = NULL;
712
713 /* Don't do anything if setup failed or the string is NULL or empty.
714 (The NULL and empty checks may not be desirable in other uses.)
715 */
716 if (!sCrashReporterInfoAvailable || info == NULL || *info == '\0') return;
717
718 // Copy the string, which we assume to be dynamic...
719 copy = strdup(info);
720 if (copy == NULL) return;
721
722 /* ...and swap it in.
723 Note that we keep a separate pointer to the old value, in case
724 something else overwrites __crashreporter_info__.
725 */
726 [sCrashReporterInfoLock lock];
727 *sCrashReporterInfo = copy;
730 [sCrashReporterInfoLock unlock];
731
732 // Delete our old string.
733 if (old != NULL) free(old);
734}
735
736#endif
#define DESTROY(x)
Definition OOCocoa.h:77
static void SetCrashReporterInfo(const char *info)
static LogCStringFunctionSetterProc _NSSetLogCStringFunction
@ kConditionReadyToDealloc
@ kConditionWorking
void OOLogOutputHandlerPrint(NSString *string)
static NSString * sLogFileName
static NSString * GetAppName(void)
static char * sOldCrashReporterInfo
void OOLogOutputHandlerStartLoggingToStdout()
NSString * OOLogHandlerGetLogPath(void)
void OOLogOutputHandlerClose(void)
static NSLock * sCrashReporterInfoLock
void OOLogOutputHandlerChangeLogFile(NSString *newLogName)
static LogCStringFunctionGetterProc _NSLogCStringFunction
static LogCStringFunctionProc sDefaultLogCStringFunction
static OOAsyncLogger * sLogger
LogCStringFunctionProc(* LogCStringFunctionGetterProc)(void)
NSString * OOLogHandlerGetLogBasePath(void)
static char ** sCrashReporterInfo
void OOLogOutputHandlerInit(void)
static BOOL sCrashReporterInfoAvailable
static void InitCrashReporterInfo(void)
static BOOL sSaturated
static BOOL sWriteToStderr
void(* LogCStringFunctionSetterProc)(LogCStringFunctionProc)
static void LoadLogCStringFunctions(void)
static BOOL sInited
static BOOL DirectoryExistCreatingIfNecessary(NSString *path)
static BOOL sWriteToStdout
static void ExcludeFromTimeMachine(NSString *path)
void(* LogCStringFunctionProc)(const char *string, unsigned length, BOOL withSyslogBanner)
void OOLogOutputHandlerStopLoggingToStdout()
static void OONSLogCStringFunction(const char *string, unsigned length, BOOL withSyslogBanner)
#define NSLog(format,...)
Definition OOLogging.h:137
BOOL OOLogWillDisplayMessagesInClass(NSString *inMessageClass)
Definition OOLogging.m:144
#define OOLog(class, format,...)
Definition OOLogging.h:88
void void OOLogWithFunctionFileAndLine(NSString *inMessageClass, const char *inFunction, const char *inFile, unsigned long inLine, NSString *inFormat,...) OO_TAKES_FORMAT_STRING(5
return nil
OOAsyncQueue * messageQueue
NSFileHandle * logFile
void asyncLogMessage:(NSString *message)
NSConditionLock * threadStateMonitor
voidpf void uLong size
Definition ioapi.h:134