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