// // $Id$ // // SPLogger.m // sequel-pro // // Created by Rowan Beentje on 17/06/2009. // Copyright 2009 Rowan Beentje. All rights reserved. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // More info at <http://code.google.com/p/sequel-pro/> #import "SPLogger.h" #include <pwd.h> #include <stdio.h> #include <dirent.h> #include <sys/dir.h> #include <sys/types.h> static SPLogger *logger = nil; @interface SPLogger (PrivateAPI) - (void)_initLogFile; - (void)_outputTimeString; int _isSPLeaksLog(struct direct *entry); @end /** * This is a small class intended to aid in user issue debugging; by including * the header file, and using [[SPLogger logger] log:@"String with format", ...] * a file will be created on the user's desktop including timestamps and * the log message. * This allows use of fine-grained and detailed logging, without asking the user * to copy text from a console log via NSLog. * As each log line must by synched to disk as soon as it is received, for safety, * this class can add a performance hit when lots of logging is used. */ @implementation SPLogger @synthesize dumpLeaksOnTermination; @synthesize removeOldLeakDumpsOnTermination; /* * Returns the shared logger object. */ + (SPLogger *)logger { @synchronized(self) { if (logger == nil) { logger = [[super allocWithZone:NULL] init]; } } return logger; } #pragma mark - #pragma mark Initialisation and teardown + (id)allocWithZone:(NSZone *)zone { @synchronized(self) { return [[self logger] retain]; } return nil; } - (id)init { if ((self = [super init])) { initializedSuccessfully = YES; [self setDumpLeaksOnTermination:NO]; [self setRemoveOldLeakDumpsOnTermination:YES]; } return self; } #pragma mark - #pragma mark Logging functions - (void)log:(NSString *)theString, ... { if (!initializedSuccessfully) return; if (!logFileHandle) [self _initLogFile]; // Extract any supplied arguments and build the formatted log string va_list arguments; va_start(arguments, theString); NSString *logString = [[NSString alloc] initWithFormat:theString arguments:arguments]; va_end(arguments); // Write the log line, forcing an immediate write to disk to ensure logging, and // synchronised to allow use across multiple executables or their frameworks. [logFileHandle synchronizeFile]; [logFileHandle seekToEndOfFile]; [logFileHandle writeData:[[NSString stringWithFormat:@"%@ %@\n", [[NSDate date] descriptionWithCalendarFormat:@"%H:%M:%S" timeZone:nil locale:[[NSUserDefaults standardUserDefaults] dictionaryRepresentation]], logString] dataUsingEncoding:NSUTF8StringEncoding]]; [logFileHandle synchronizeFile]; [logString release]; } - (void)dumpLeaks { if ([self dumpLeaksOnTermination]) { char *lgn; struct passwd *pw; boolean_t hdir = FALSE; // Determine where to write the log to if ((lgn = getlogin()) == NULL || (pw = getpwnam(lgn)) == NULL) { fprintf(stderr, "Unable to get user info, falling back to /tmp\n"); } else { hdir = TRUE; } // If required remove old logs if ([self removeOldLeakDumpsOnTermination]) { int cnt, cnt2, i; struct direct **files; cnt = scandir("/tmp", &files, _isSPLeaksLog, NULL); char fpath[32], fpath2[32], fpath3[64]; for (i = 0; i < cnt; i++) { snprintf(fpath, sizeof(fpath), "/tmp/%s", files[i]->d_name); if (remove(fpath) != 0) { printf("Unable to remove Sequel Pro leaks log '%s'\n", files[i]->d_name); } } free(&files); if (hdir) { snprintf(fpath2, sizeof(fpath2), "%s/Desktop", pw->pw_dir); cnt2 = scandir(fpath2, &files, _isSPLeaksLog, NULL); for (i = 0; i < cnt2; i++) { snprintf(fpath3, sizeof(fpath3), "%s/%s", fpath2, files[i]->d_name); if (remove(fpath3) != 0) { printf("Unable to remove Sequel Pro leaks log '%s'\n", files[i]->d_name); } } } } size_t len; FILE *fp, *fp2; char cmd[32], file[64], buf[512]; snprintf(cmd, sizeof(cmd), "/usr/bin/leaks %d", getpid()); snprintf(file, sizeof(file), (hdir) ? "%s/Desktop/sp.leaks.%d.log" : "%s/sp.leaks.%d.log", (hdir) ? pw->pw_dir : "/tmp", getpid()); // Write new leaks log if ((fp = popen(cmd, "r")) && (fp2 = fopen(file, "w"))) { while ((len = fread(buf, 1, sizeof(buf), fp))) { fwrite(buf, 1, len, fp2); } pclose(fp); } } } #pragma mark - #pragma mark Private API - (void)_initLogFile { NSArray *paths = NSSearchPathForDirectoriesInDomains (NSDesktopDirectory, NSUserDomainMask, YES); NSString *logFilePath = [NSString stringWithFormat:@"%@/Sequel Pro Debug Log.log", [paths objectAtIndex:0]]; NSFileManager *fileManager = [NSFileManager defaultManager]; // Check if the debug file exists, and is writable if ([fileManager fileExistsAtPath:logFilePath]) { if (![fileManager isWritableFileAtPath:logFilePath]) { initializedSuccessfully = NO; NSRunAlertPanel(@"Logging error", @"Log file exists but is not writeable; no debug log will be generated!", @"OK", nil, nil); } // Otherwise try creating one } else { if (![fileManager createFileAtPath:logFilePath contents:[NSData data] attributes:nil]) { initializedSuccessfully = NO; NSRunAlertPanel(@"Logging error", @"Could not create log file for writing; no debug log will be generated!", @"OK", nil, nil); } } // Get a file handle to the file if possible if (initializedSuccessfully) { logFileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath]; if (!logFileHandle) { initializedSuccessfully = NO; NSRunAlertPanel(@"Logging error", @"Could not open log file for writing; no debug log will be generated!", @"OK", nil, nil); } else { [logFileHandle retain]; [logFileHandle seekToEndOfFile]; NSString *bundleName = [fileManager displayNameAtPath:[[NSBundle mainBundle] bundlePath]]; NSMutableString *logStart = [NSMutableString stringWithString:@"\n\n\n==========================================================================\n\n"]; [logStart appendFormat:@"%@ (r%ld)\n", bundleName, (long)[[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"] integerValue]]; [logFileHandle writeData:[logStart dataUsingEncoding:NSUTF8StringEncoding]]; } } } - (void)_outputTimeString { if (!initializedSuccessfully) return; [logFileHandle writeData:[[NSString stringWithFormat:@"Launched at %@\n\n", [[NSDate date] description]] dataUsingEncoding:NSUTF8StringEncoding]]; } int _isSPLeaksLog(struct direct *entry) { return (strstr(entry->d_name, "sp.leaks") != NULL); } @end