* Adium is the legal property of its developers, whose names are listed in the copyright file included * with this source distribution. * 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. #import "AILoggerPlugin.h" #import "AILogFromGroup.h" #import "AILogViewerWindowController.h" #import "AIXMLAppender.h" #import <Adium/AIXMLElement.h> #import <Adium/AIContentControllerProtocol.h> #import <Adium/AIInterfaceControllerProtocol.h> #import <Adium/AIChatControllerProtocol.h> #import <Adium/AILoginControllerProtocol.h> #import <Adium/AIMenuControllerProtocol.h> #import <Adium/AIToolbarControllerProtocol.h> #import <Adium/AIAccount.h> #import <Adium/AIContentMessage.h> #import <Adium/AIContentNotification.h> #import <Adium/AIContentStatus.h> #import <Adium/AIContentEvent.h> #import <Adium/AIContentContext.h> #import <Adium/AIHTMLDecoder.h> #import <Adium/AIListContact.h> #import <Adium/AIListBookmark.h> #import <Adium/AIService.h> #import <AIUtilities/AIAttributedStringAdditions.h> #import <AIUtilities/AIDictionaryAdditions.h> #import <AIUtilities/AIFileManagerAdditions.h> #import <AIUtilities/AIMenuAdditions.h> #import <AIUtilities/AIStringAdditions.h> #import <AIUtilities/AIToolbarUtilities.h> #import <AIUtilities/AIDateFormatterAdditions.h> #import <AIUtilities/AIImageAdditions.h> #import <AIUtilities/NSCalendarDate+ISO8601Unparsing.h> #import <AIUtilities/NSCalendarDate+ISO8601Parsing.h> #import <libkern/OSAtomic.h> #import "AILogFileUpgradeWindowController.h" #import "AdiumSpotlightImporter.h" #define LOG_INDEX_NAME @"Logs.index" #define KEY_LOG_INDEX_VERSION @"Log Index Version" #define DIRTY_LOG_SET_NAME @"DirtyLogs.plist" #define KEY_LOG_INDEX_VERSION @"Log Index Version" //Version of the log index. Increase this number to reset everyone's index. #define CURRENT_LOG_VERSION 10 #define LOG_INDEX_STATUS_INTERVAL 20 #define LOG_CLEAN_SAVE_INTERVAL 2000 #define NEW_LOGFILE_TIMEOUT 600 #define LOG_VIEWER AILocalizedString(@"Chat Transcript Viewer",nil) #define VIEW_LOGS_WITH_CONTACT AILocalizedString(@"View Chat Transcripts",nil) #define LOG_VIEWER_IDENTIFIER @"LogViewer" #define ENABLE_PROXIMITY_SEARCH TRUE #pragma mark Private Interface NSData * CopyDataForURL ( CFStringRef contentTypeUTI , NSURL * urlToFile ); CFStringRef CopyTextContentForFileData ( CFStringRef contentTypeUTI , NSURL * urlToFile , NSData * fileData ); @interface AILoggerPlugin () + ( NSString * ) pathForLogsLikeChat : ( AIChat * ) chat ; + ( NSString * ) fullPathForLogOfChat: ( AIChat * ) chat onDate: ( NSDate * ) date ; + ( NSString * ) nameForLogWithObject: ( NSString * ) object onDate: ( NSDate * ) date ; - ( void ) _configureMenuItems ; - ( void ) _initLogIndexing ; - ( void ) _upgradeLogExtensions ; - ( void ) _upgradeLogPermissions ; - ( void ) _reimportLogsToSpotlightIfNeeded ; - ( void ) showLogViewer: ( id ) sender ; - ( void ) showLogViewerToSelectedContact: ( id ) sender ; - ( void ) showLogViewerToSelectedContextContact: ( id ) sender ; - ( void ) showLogViewerForActiveChat: ( id ) sender ; - ( void ) showLogViewerForGroupChat: ( id ) sender ; - ( void ) showLogViewerAndReindex: ( id ) sender ; - ( void ) showLogNotification: ( NSNotification * ) inNotification ; - ( void ) _showLogViewerForLogAtPath: ( NSString * ) inPath ; - ( void ) contentObjectAdded: ( NSNotification * ) notification ; - ( void ) chatOpened: ( NSNotification * ) notification ; - ( void ) chatClosed: ( NSNotification * ) notification ; - ( void ) chatWillDelete: ( NSNotification * ) notification ; - ( AIXMLAppender * ) _appenderForChat: ( AIChat * ) chat ; - ( AIXMLAppender * ) _existingAppenderForChat: ( AIChat * ) chat ; - ( NSString * ) keyForChat: ( AIChat * ) chat ; - ( void ) closeAppenderForChat: ( AIChat * ) chat ; - ( void ) finishClosingAppender: ( NSString * ) chatKey ; - ( NSString * ) _logIndexPath ; - ( NSString * ) _dirtyLogSetPath ; - ( void ) _loadDirtyLogSet ; - ( void ) _cancelClosingLogIndex ; // Log Indexing Internals - ( void ) _didCleanDirtyLogs ; - ( void ) _saveDirtyLogSet ; - ( void ) _markLogDirtyAtPath: ( NSString * ) path forChat: ( AIChat * ) chat ; - ( void ) _flushIndex: ( SKIndexRef ) inIndex ; @property ( retain , readwrite ) NSMutableDictionary * activeAppenders ; @property ( retain , readwrite ) AIHTMLDecoder * xhtmlDecoder ; @property ( retain , readwrite ) NSDictionary * statusTranslation ; @property ( retain , readwrite ) NSMutableSet * dirtyLogSet ; @property ( assign , readwrite ) BOOL logHTML ; @property ( assign , readwrite ) BOOL indexingAllowed ; @property ( assign , readwrite ) BOOL loggingEnabled ; @property ( assign , readwrite ) BOOL canCloseIndex ; @property ( assign , readwrite ) BOOL canSaveDirtyLogSet ; @property ( assign , readwrite ) BOOL indexIsFlushing ; @property ( assign , readwrite ) BOOL isIndexing ; @property ( assign , readwrite ) SInt64 logsToIndex ; @property ( assign , readwrite ) SInt64 logsIndexed ; #pragma mark Private Function Prototypes void runWithAutoreleasePool ( dispatch_block_t block ); static inline dispatch_block_t blockWithAutoreleasePool ( dispatch_block_t block ); NSCalendarDate * getDateFromPath ( NSString * path ); NSComparisonResult sortPaths ( NSString * path1 , NSString * path2 , void * context ); #pragma mark Static Globals //The base directory of all logs static NSString * logBasePath = nil ; //If the usual Logs folder path refers to an alias file, this is that path, and logBasePath is the destination of the alias; otherwise, this is nil and logBasePath is the usual Logs folder path. static NSString * logBaseAliasPath = nil ; static dispatch_queue_t defaultDispatchQueue ; static dispatch_queue_t dirtyLogSetMutationQueue ; static dispatch_queue_t searchIndexQueue ; static dispatch_queue_t activeAppendersMutationQueue ; static dispatch_queue_t addToSearchKitQueue ; static dispatch_queue_t ioQueue ; static dispatch_group_t logIndexingGroup ; static dispatch_group_t closingIndexGroup ; static dispatch_group_t logAppendingGroup ; static dispatch_group_t loggerPluginGroup ; static dispatch_semaphore_t jobSemaphore ; static dispatch_semaphore_t logLoadingPrefetchSemaphore ; //limit prefetching log data to N-1 ahead @implementation AILoggerPlugin @synthesize dirtyLogSet , indexingAllowed , loggingEnabled , logsToIndex , logsIndexed , canCloseIndex , canSaveDirtyLogSet , activeAppenders , logHTML , xhtmlDecoder , statusTranslation , isIndexing , indexIsFlushing ; #pragma mark Public Methods #pragma mark Overridden AIPlugin Methods userTriggeredReindex = NO ; self . indexingAllowed = YES ; self . canCloseIndex = YES ; self . loggingEnabled = NO ; self . canSaveDirtyLogSet = YES ; self . indexIsFlushing = NO ; self . activeAppenders = [ NSMutableDictionary dictionary ]; self . dirtyLogSet = [ NSMutableSet set ]; defaultDispatchQueue = dispatch_get_global_queue ( DISPATCH_QUEUE_PRIORITY_LOW , 0 ); dirtyLogSetMutationQueue = dispatch_queue_create ( "im.adium.AILoggerPlugin.dirtyLogSetMutationQueue" , 0 ); searchIndexQueue = dispatch_queue_create ( "im.adium.AILoggerPlugin.searchIndexFlushingQueue" , 0 ); activeAppendersMutationQueue = dispatch_queue_create ( "im.adium.AILoggerPlugin.activeAppendersMutationQueue" , 0 ); addToSearchKitQueue = dispatch_queue_create ( "im.adium.AILoggerPlugin.searchIndexAddingQueue" , 0 ); logIndexingGroup = dispatch_group_create (); closingIndexGroup = dispatch_group_create (); logAppendingGroup = dispatch_group_create (); loggerPluginGroup = dispatch_group_create (); ioQueue = dispatch_queue_create ( "im.adium.AILoggerPlugin.ioQueue" , 0 ); NSUInteger cpuCount = [[ NSProcessInfo processInfo ] activeProcessorCount ]; jobSemaphore = dispatch_semaphore_create ( 3 * cpuCount ); logLoadingPrefetchSemaphore = dispatch_semaphore_create ( 3 * cpuCount + 1 ); //prefetch one log self . xhtmlDecoder = [[[ AIHTMLDecoder alloc ] initWithHeaders : NO onlyIncludeOutgoingImages : NO allowJavascriptURLs : YES ] autorelease ]; [ self . xhtmlDecoder setGeneratesStrictXHTML : YES ]; self . statusTranslation = [ NSDictionary dictionaryWithObjectsAndKeys : @"online" , @"return_away" , @"available" , @"return_idle" , [ adium . preferenceController registerDefaults : [ NSDictionary dictionaryNamed : LOGGING_DEFAULT_PREFS forClass : [ self class ]] forGroup : PREF_GROUP_LOGGING ]; [ self _configureMenuItems ]; static dispatch_once_t setLogBasePath ; dispatch_once ( & setLogBasePath , ^ { logBasePath = [[[[ adium . loginController userDirectory ] stringByAppendingPathComponent : PATH_LOGS ] stringByExpandingTildeInPath ] retain ]; [[ NSFileManager defaultManager ] createDirectoryAtPath : logBasePath withIntermediateDirectories : YES attributes : nil error : NULL ]; //Observe preference changes [ adium . preferenceController addObserver : self forKeyPath : PREF_KEYPATH_LOGGER_ENABLE options : NSKeyValueObservingOptionNew [ self observeValueForKeyPath : PREF_KEYPATH_LOGGER_ENABLE ofObject : adium . preferenceController NSToolbarItem * toolbarItem ; toolbarItem = [ AIToolbarUtilities toolbarItemWithIdentifier : LOG_VIEWER_IDENTIFIER label : AILocalizedString ( @"Transcripts" , nil ) paletteLabel : AILocalizedString ( @"View Chat Transcripts" , nil ) toolTip : AILocalizedString ( @"View previous conversations with this contact or chat" , nil ) settingSelector : @selector ( setImage : ) itemContent :[ NSImage imageNamed : @"msg-log-viewer" forClass : [ self class ] loadLazily : YES ] action : @selector ( showLogViewerForActiveChat : ) [ adium . toolbarController registerToolbarItem : toolbarItem forToolbarType : @"ListObject" ]; [ self _upgradeLogExtensions ]; [ self _upgradeLogPermissions ]; [ self _reimportLogsToSpotlightIfNeeded ]; [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( showLogNotification : ) name : AIShowLogAtPathNotification [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( showLogViewerAndReindex : ) name : AIShowLogViewerAndReindexNotification dispatch_group_wait ( closingIndexGroup , DISPATCH_TIME_FOREVER ); [[ NSNotificationCenter defaultCenter ] removeObserver : self ]; [ adium . preferenceController removeObserver : self forKeyPath : PREF_KEYPATH_LOGGER_ENABLE ]; dispatch_group_wait ( logIndexingGroup , DISPATCH_TIME_FOREVER ); dispatch_group_wait ( closingIndexGroup , DISPATCH_TIME_FOREVER ); dispatch_group_wait ( logAppendingGroup , DISPATCH_TIME_FOREVER ); dispatch_group_wait ( loggerPluginGroup , DISPATCH_TIME_FOREVER ); self . activeAppenders = nil ; self . statusTranslation = nil ; dispatch_release ( dirtyLogSetMutationQueue ); dirtyLogSetMutationQueue = nil ; dispatch_release ( searchIndexQueue ); searchIndexQueue = nil ; dispatch_release ( activeAppendersMutationQueue ); activeAppendersMutationQueue = nil ; dispatch_release ( logIndexingGroup ); logIndexingGroup = nil ; dispatch_release ( closingIndexGroup ); closingIndexGroup = nil ; dispatch_release ( addToSearchKitQueue ); addToSearchKitQueue = nil ; dispatch_release ( logAppendingGroup ); logAppendingGroup = nil ; dispatch_release ( ioQueue ); ioQueue = nil ; dispatch_release ( jobSemaphore ); jobSemaphore = nil ; dispatch_release ( loggerPluginGroup ); loggerPluginGroup = nil ; #pragma mark AILoggerPlugin Plubic Methods + ( NSString * ) logBasePath static dispatch_once_t didResolveLogBaseAlias ; dispatch_once ( & didResolveLogBaseAlias , ^ { OSStatus err = FSPathMakeRef (( UInt8 * )[ logBasePath UTF8String ], & ref , & isDir ); NSLog ( @"Warning: Couldn't obtain FSRef for transcripts folder: %s (%ld)" , GetMacOSStatusCommentString ( err ), ( long ) err ); Boolean wasAliased_nobodyCares ; err = FSResolveAliasFile ( & ref , /*resolveAliasChains*/ true , & isDir , & wasAliased_nobodyCares ); NSLog ( @"Warning: Couldn't resolve alias to transcripts folder: %s (%ld)" , GetMacOSStatusCommentString ( err ), ( long ) err ); NSURL * logBaseURL = [( NSURL * ) CFURLCreateFromFSRef ( kCFAllocatorDefault , & ref ) autorelease ]; logBaseAliasPath = logBasePath ; logBasePath = [[ logBaseURL path ] copy ]; + ( NSString * ) relativePathForLogWithObject: ( NSString * ) object onAccount: ( AIAccount * ) account return [ NSString stringWithFormat : @"%@.%@/%@" , account . service . serviceID , [ account . UID safeFilenameString ], object ]; + ( NSArray * ) sortedArrayOfLogFilesForChat: ( AIChat * ) chat NSArray * files = [[ NSFileManager defaultManager ] contentsOfDirectoryAtPath : [ self pathForLogsLikeChat : chat ] error : NULL ]; NSMutableArray * dates = [ NSMutableArray arrayWithCapacity : files . count ]; for ( NSString * path in files ) { id date = getDateFromPath ( path ); [ dates addObject : date ?: [ NSNull null ]]; NSDictionary * cache = [ NSDictionary dictionaryWithObjects : dates forKeys : files ]; return ( files ? [ files sortedArrayUsingFunction :& sortPaths context : cache ] : nil ); - ( void ) prepareLogContentSearching __block __typeof__ ( self ) bself = self ; dispatch_async ( defaultDispatchQueue , ^ { /* Load the index and start indexing to make it current * If we're going to need to re-index all our logs from scratch, it will make * things faster if we start with a fresh log index as well. BOOL reindex = ! [[ NSFileManager defaultManager ] fileExistsAtPath : [ bself _dirtyLogSetPath ]]; if ( ! userTriggeredReindex ) { - ( void ) cleanUpLogContentSearching __block __typeof__ ( self ) bself = self ; dispatch_group_async ( loggerPluginGroup , defaultDispatchQueue , ^ { - ( SKIndexRef ) logContentIndex /* We shouldn't have to lock here except in createLogIndex. However, a 'window period' exists after an SKIndex has been closed via SKIndexClose() * in which an attempt to load the index from disk returns NULL (presumably because it's still being written-to asynchronously). We therefore lock * around the full access to make the process reliable. The documentation says that SKIndex is thread-safe, but that seems to assume that you keep * a single instance of SKIndex open at all times... which is a major memory hit for a large index of a significant number of logs. We only keep the index * open as long as the transcript viewer window is open. [ self _cancelClosingLogIndex ]; __block __typeof__ ( self ) bself = self ; dispatch_sync ( searchIndexQueue , blockWithAutoreleasePool ( ^ { NSString * logIndexPath = [ bself _logIndexPath ]; NSURL * logIndexURL = [ NSURL fileURLWithPath : logIndexPath ]; if ([[ NSFileManager defaultManager ] fileExistsAtPath : logIndexPath ]) { _index = SKIndexOpenWithURL (( CFURLRef ) logIndexURL , ( CFStringRef ) @"Content" , true ); AILogWithSignature ( @"Opened index %x from %@" , _index , logIndexURL ); //It appears our index was somehow corrupt, since it exists but it could not be opened. Remove it so we can create a new one. AILogWithSignature ( @"*** Warning: The Chat Transcript searching index at %@ was corrupt. Removing it and starting fresh; transcripts will be re-indexed automatically." , [[ NSFileManager defaultManager ] removeItemAtPath : logIndexPath error : NULL ]; NSDictionary * textAnalysisProperties ; textAnalysisProperties = [ NSDictionary dictionaryWithObjectsAndKeys : [ NSNumber numberWithInteger : 0 ], kSKMaximumTerms , [ NSNumber numberWithInteger : 2 ], kSKMinTermLength , #if ENABLE_PROXIMITY_SEARCH kCFBooleanTrue , kSKProximityIndexing , //Create the index if one doesn't exist or it couldn't be opened. [[ NSFileManager defaultManager ] createDirectoryAtPath : [ logIndexPath stringByDeletingLastPathComponent ] withIntermediateDirectories : YES attributes : nil error : NULL ]; _index = SKIndexCreateWithURL (( CFURLRef ) logIndexURL , ( CFDictionaryRef ) textAnalysisProperties ); AILogWithSignature ( @"Created a new log index %x at %@ with textAnalysisProperties %@. Will reindex all logs." , _index , logIndexURL , textAnalysisProperties ); //Clear the dirty log set in case it was loaded (this can happen if the user mucks with the cache directory) [[ NSFileManager defaultManager ] removeItemAtPath : [ bself _dirtyLogSetPath ] error : NULL ]; dispatch_sync ( dirtyLogSetMutationQueue , ^ { [ bself -> dirtyLogSet removeAllObjects ]; [ bself _flushIndex : _index ]; AILogWithSignature ( @"AILoggerPlugin warning: SKIndexCreateWithURL(%@, %@, %lu, %@) returned NULL" , logIndexURL , @"Content" , ( unsigned long ) kSKIndexInverted , textAnalysisProperties ); bself -> logIndex = _index ; if ( logIndex ) CFRetain ( logIndex ); - ( void ) markLogDirtyAtPath: ( NSString * ) path __block __typeof__ ( self ) bself = self ; dispatch_sync ( dirtyLogSetMutationQueue , ^ { if ( path && ! [ bself . dirtyLogSet containsObject : path ]) { [ bself . dirtyLogSet addObject : path ]; __block __typeof__ ( self ) bself = self ; dispatch_group_async ( loggerPluginGroup , defaultDispatchQueue , ^ { bself . indexingAllowed = NO ; dispatch_group_wait ( logIndexingGroup , DISPATCH_TIME_FOREVER ); bself . indexingAllowed = YES ; AILogWithSignature ( @"Canceling indexing operations." ); - ( void ) removePathsFromIndex: ( NSSet * ) paths __block __typeof__ ( self ) bself = self ; dispatch_group_async ( loggerPluginGroup , defaultDispatchQueue , blockWithAutoreleasePool ( ^ { SKIndexRef logSearchIndex = [ bself logContentIndex ]; AILogWithSignature ( @"AILoggerPlugin warning: logSearchIndex is NULL, but we wanted to remove documents." ); for ( NSString * logPath in paths ) { SKDocumentRef document = SKDocumentCreateWithURL (( CFURLRef )[ NSURL fileURLWithPath : logPath ]); SKIndexRemoveDocument ( logSearchIndex , document ); CFRelease ( logSearchIndex ); #pragma mark Private Methods #pragma mark Private Functions void runWithAutoreleasePool ( dispatch_block_t block ) NSAutoreleasePool * pool = [ NSAutoreleasePool new ]; static inline dispatch_block_t blockWithAutoreleasePool ( dispatch_block_t block ) runWithAutoreleasePool ( block ); NSCalendarDate * getDateFromPath ( NSString * path ) NSRange openParenRange , closeParenRange ; if ([ path hasSuffix : @".chatlog" ] && ( openParenRange = [ path rangeOfString : @"(" options : NSBackwardsSearch ]). location != NSNotFound ) { openParenRange = NSMakeRange ( openParenRange . location , [ path length ] - openParenRange . location ); if (( closeParenRange = [ path rangeOfString : @")" options : 0 range : openParenRange ]). location != NSNotFound ) { //Add and subtract one to remove the parenthesis NSString * dateString = [ path substringWithRange : NSMakeRange ( openParenRange . location + 1 , ( closeParenRange . location - openParenRange . location ))]; NSCalendarDate * date = [ NSCalendarDate calendarDateWithString : dateString timeSeparator : '.' ]; NSComparisonResult sortPaths ( NSString * path1 , NSString * path2 , void * context ) NSDictionary * cache = ( NSDictionary * ) context ; id date1 = [ cache objectForKey : path1 ]; id date2 = [ cache objectForKey : path2 ]; NSNull * n = [ NSNull null ]; return [ date2 compare : date1 ]; return date2 ? NSOrderedDescending : NSOrderedAscending ; #pragma mark Private Class Methods + ( NSString * ) pathForLogsLikeChat: ( AIChat * ) chat NSString * objectUID = chat . name ; AIAccount * account = chat . account ; if ( ! objectUID ) objectUID = chat . listObject . UID ; objectUID = [ objectUID safeFilenameString ]; return [[ self logBasePath ] stringByAppendingPathComponent : [ self relativePathForLogWithObject : objectUID onAccount : account ]]; + ( NSString * ) fullPathForLogOfChat: ( AIChat * ) chat onDate: ( NSDate * ) date NSString * objectUID = chat . name ; AIAccount * account = chat . account ; if ( ! objectUID ) objectUID = chat . listObject . UID ; objectUID = [ objectUID safeFilenameString ]; NSString * absolutePath = [[ self logBasePath ] stringByAppendingPathComponent : [ self relativePathForLogWithObject : objectUID onAccount : account ]]; NSString * name = [ self nameForLogWithObject : objectUID onDate : date ]; NSString * fullPath = [[ absolutePath stringByAppendingPathComponent : [ name stringByAppendingPathExtension : @"chatlog" ]] stringByAppendingPathComponent :[ name stringByAppendingPathExtension : @"xml" ]]; + ( NSString * ) nameForLogWithObject: ( NSString * ) object onDate: ( NSDate * ) date NSParameterAssert ( date != nil ); NSParameterAssert ( object != nil ); NSString * dateString = [ date descriptionWithCalendarFormat : @"%Y-%m-%dT%H.%M.%S%z" timeZone : nil locale : nil ]; NSAssert2 ( dateString != nil , @"Date string was invalid for the chatlog for %@ on %@" , object , date ); return [ NSString stringWithFormat : @"%@ (%@)" , object , dateString ]; #pragma mark Private Instace Methods #pragma mark Installation Methods - ( void ) _configureMenuItems logViewerMenuItem = [[[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] action : @selector ( showLogViewer : ) keyEquivalent : @"L" ] autorelease ]; [ adium . menuController addMenuItem : logViewerMenuItem toLocation : LOC_Window_Auxiliary ]; viewContactLogsMenuItem = [[[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] initWithTitle : VIEW_LOGS_WITH_CONTACT action : @selector ( showLogViewerToSelectedContact : ) keyEquivalent : @"l" ] autorelease ]; [ adium . menuController addMenuItem : viewContactLogsMenuItem toLocation : LOC_Contact_Info ]; viewContactLogsContextMenuItem = [[[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] initWithTitle : VIEW_LOGS_WITH_CONTACT action : @selector ( showLogViewerToSelectedContextContact : ) keyEquivalent : @"" ] autorelease ]; [ adium . menuController addContextualMenuItem : viewContactLogsContextMenuItem toLocation : Context_Contact_Manage ]; viewGroupLogsContextMenuItem = [[[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] initWithTitle : VIEW_LOGS_WITH_CONTACT action : @selector ( showLogViewerForGroupChat : ) keyEquivalent : @"" ] autorelease ]; [ adium . menuController addContextualMenuItem : viewGroupLogsContextMenuItem toLocation : Context_GroupChat_Manage ]; // Enable/Disable our view log menus - ( BOOL ) validateMenuItem: ( NSMenuItem * ) menuItem if ( menuItem == viewContactLogsMenuItem ) { AIListObject * selectedObject = adium . interfaceController . selectedListObject ; return adium . interfaceController . activeChat || ( selectedObject && [ selectedObject isKindOfClass : [ AIListContact class ]]); } else if ( menuItem == viewContactLogsContextMenuItem ) { AIListObject * selectedObject = adium . menuController . currentContextMenuObject ; return ! adium . interfaceController . activeChat . isGroupChat || ( selectedObject && [ selectedObject isKindOfClass : [ AIListContact class ]]); - ( void ) _upgradeLogExtensions if ( ! [[ adium . preferenceController preferenceForKey : @"Log Extensions Updated" group : PREF_GROUP_LOGGING ] boolValue ]) { /* This could all be a simple NSDirectoryEnumerator call on basePath, but we wouldn't be able to show progress, * and this could take a bit. NSMutableSet * pathsToContactFolders = [ NSMutableSet set ]; for ( NSString * accountFolderName in [[ NSFileManager defaultManager ] contentsOfDirectoryAtPath : [[ self class ] logBasePath ] error : NULL ]) { NSString * contactBasePath = [ logBasePath stringByAppendingPathComponent : accountFolderName ]; for ( NSString * contactFolderName in [[ NSFileManager defaultManager ] contentsOfDirectoryAtPath : contactBasePath error : NULL ]) { [ pathsToContactFolders addObject : [ contactBasePath stringByAppendingPathComponent : contactFolderName ]]; NSUInteger contactsToProcess = [ pathsToContactFolders count ]; NSUInteger processed = 0 ; AILogFileUpgradeWindowController * upgradeWindowController = [[ AILogFileUpgradeWindowController alloc ] initWithWindowNibName : @"LogFileUpgrade" ]; [[ upgradeWindowController window ] makeKeyAndOrderFront : nil ]; for ( NSString * pathToContactFolder in pathsToContactFolders ) { for ( NSString * file in [[ NSFileManager defaultManager ] contentsOfDirectoryAtPath : pathToContactFolder error : NULL ]) { if (([[ file pathExtension ] isEqualToString : @"html" ]) || ([[ file pathExtension ] isEqualToString : @"adiumLog" ]) || (([[ file pathExtension ] isEqualToString : @"bak" ]) && ([ file hasSuffix : @".html.bak" ] || [ file hasSuffix : @".adiumLog.bak" ]))) { NSString * fullFile = [ pathToContactFolder stringByAppendingPathComponent : file ]; NSString * newFile = [[ fullFile stringByDeletingPathExtension ] stringByAppendingPathExtension : @"AdiumHTMLLog" ]; [[ NSFileManager defaultManager ] moveItemAtPath : fullFile AILogWithSignature ( @"%@" , [ err localizedDescription ]); [ upgradeWindowController setProgress : ( processed * 100.0 ) / contactsToProcess ]; [ upgradeWindowController close ]; [ upgradeWindowController release ]; [ adium . preferenceController setPreference : [ NSNumber numberWithBool : YES ] forKey : @"Log Extensions Updated" group : PREF_GROUP_LOGGING ]; - ( void ) _upgradeLogPermissions if ([[ adium . preferenceController preferenceForKey : @"Log Permissions Updated" group : PREF_GROUP_LOGGING ] boolValue ]) /* This is based off of -upgradeLogExtensions. Refer to that. */ NSMutableSet * pathsToContactFolders = [ NSMutableSet set ]; for ( NSString * accountFolderName in [[ NSFileManager defaultManager ] contentsOfDirectoryAtPath : [[ self class ] logBasePath ] error : NULL ]) { //??? isn't this just going to be the same as accountFolderName? NSString * contactBasePath = [[[ self class ] logBasePath ] stringByAppendingPathComponent : accountFolderName ]; // Set permissions to prohibit access from other users [[ NSFileManager defaultManager ] setAttributes : [ NSDictionary dictionaryWithObject : [ NSNumber numberWithUnsignedLong : 0700UL ] forKey : NSFilePosixPermissions ] ofItemAtPath : contactBasePath for ( NSString * contactFolderName in [[ NSFileManager defaultManager ] contentsOfDirectoryAtPath : contactBasePath error : NULL ]) { NSString * contactFolderPath = [ contactBasePath stringByAppendingPathComponent : contactFolderName ]; // Set permissions to prohibit access from other users [[ NSFileManager defaultManager ] setAttributes : [ NSDictionary dictionaryWithObject : [ NSNumber numberWithUnsignedLong : 0700UL ] forKey : NSFilePosixPermissions ] ofItemAtPath : contactFolderPath // We'll traverse the contact directories themselves next [ pathsToContactFolders addObject : contactFolderPath ]; NSUInteger contactsToProcess = [ pathsToContactFolders count ]; NSUInteger processed = 0 ; AILogFileUpgradeWindowController * upgradeWindowController = [[ AILogFileUpgradeWindowController alloc ] initWithWindowNibName : @"LogFileUpgrade" ]; [[ upgradeWindowController window ] makeKeyAndOrderFront : nil ]; for ( NSString * pathToContactFolder in pathsToContactFolders ) { for ( NSString * file in [[ NSFileManager defaultManager ] contentsOfDirectoryAtPath : pathToContactFolder error : NULL ]) { NSString * fullFile = [ pathToContactFolder stringByAppendingPathComponent : file ]; // Some chat logs are bundles [[ NSFileManager defaultManager ] fileExistsAtPath : fullFile isDirectory :& isDir ]; [[ NSFileManager defaultManager ] setAttributes : [ NSDictionary dictionaryWithObject : [ NSNumber numberWithUnsignedLong : 0600UL ] forKey : NSFilePosixPermissions ] [[ NSFileManager defaultManager ] setAttributes : [ NSDictionary dictionaryWithObject : [ NSNumber numberWithUnsignedLong : 0700UL ] forKey : NSFilePosixPermissions ] // We have to enumerate this directory, too, only not as deep for ( NSString * contentFile in [[ NSFileManager defaultManager ] contentsOfDirectoryAtPath : fullFile error : NULL ]) { [[ NSFileManager defaultManager ] setAttributes : [ NSDictionary dictionaryWithObject : [ NSNumber numberWithUnsignedLong : 0600UL ] forKey : NSFilePosixPermissions ] [ upgradeWindowController setProgress : ( processed * 100.0 ) / contactsToProcess ]; [ upgradeWindowController close ]; [ upgradeWindowController release ]; [ adium . preferenceController setPreference : [ NSNumber numberWithBool : YES ] forKey : @"Log Permissions Updated" group : PREF_GROUP_LOGGING ]; * @brief Instruct spotlight to reimport logs * Adium 1.0.2 and earlier had a bug which made spotlight import not work properly. * New logs are properly indexed, but previously created logs are not. On first launch of Adium 1.1, * Adium will tell Spotlight to reimport those old logs. * We also reindex in Adium 1.3.3 to help capture logs in Mac OS X 10.5.6 which indexes logs in our default location. - ( void ) _reimportLogsToSpotlightIfNeeded if ( ! [[ NSUserDefaults standardUserDefaults ] boolForKey : @"Adium 1.3.3:Reimported Spotlight Logs" ]) { arguments = [ NSArray arrayWithObjects : [[[[[[ NSBundle mainBundle ] bundlePath ] stringByAppendingPathComponent : @"Contents" ] stringByAppendingPathComponent : @"Library" ] stringByAppendingPathComponent : @"Spotlight" ] stringByAppendingPathComponent : [ @"AdiumSpotlightImporter" stringByAppendingPathExtension : @"mdimporter" ]], [ NSTask launchedTaskWithLaunchPath : @"/usr/bin/mdimport" arguments : arguments ]; @catch ( NSException * e ) { NSLog ( @"Exception caught while reimporting Spotlight logs: %@" , e ); [[ NSUserDefaults standardUserDefaults ] setBool : YES forKey : @"Adium 1.3.3:Reimported Spotlight Logs" ]; #pragma mark KeyValueObserving - ( void ) observeValueForKeyPath: ( NSString * ) keyPath ofObject: ( id ) object change: ( NSDictionary * ) change context: ( void * ) context newLogValue = [[ object valueForKeyPath : keyPath ] boolValue ]; if ( newLogValue != self . loggingEnabled ) { self . loggingEnabled = newLogValue ; if ( ! self . loggingEnabled ) { //Stop Logging [[ NSNotificationCenter defaultCenter ] removeObserver : self name : Content_ContentObjectAdded object : nil ]; [[ NSNotificationCenter defaultCenter ] removeObserver : self name : Chat_DidOpen object : nil ]; [[ NSNotificationCenter defaultCenter ] removeObserver : self name : Chat_WillClose object : nil ]; [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( contentObjectAdded : ) name : Content_ContentObjectAdded [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( chatOpened : ) [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( chatClosed : ) [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( chatWillDelete : ) #pragma mark Action Methods * @brief Show the log viewer for no contact * Invoked from the Window menu - ( void ) showLogViewer: ( id ) sender [ AILogViewerWindowController openForContact : nil * @brief Show the log viewer, displaying only the selected contact's logs * Invoked from the Contact menu - ( void ) showLogViewerToSelectedContact: ( id ) sender BOOL openForSelectedObject = YES ; if ( sender == viewContactLogsMenuItem ) { AIChat * activeChat = adium . interfaceController . activeChat ; if ( activeChat . isGroupChat ) { [ AILogViewerWindowController openForChatName : activeChat . name withAccount : activeChat . account plugin : self ]; openForSelectedObject = NO ; if ( openForSelectedObject ) { AIListObject * selectedObject = adium . interfaceController . selectedListObject ; if ([ selectedObject isKindOfClass : [ AIListBookmark class ]]) { [ AILogViewerWindowController openForChatName : (( AIListBookmark * ) selectedObject ). name withAccount :(( AIListBookmark * ) selectedObject ). account [ AILogViewerWindowController openForContact : ([ selectedObject isKindOfClass : [ AIListContact class ]] ? ( AIListContact * ) selectedObject : * @brief Show the log viewer, displaying only the selected contact's logs * Invoked from a contextual menu - ( void ) showLogViewerToSelectedContextContact: ( id ) sender AIListObject * object = adium . menuController . currentContextMenuObject ; AILogViewerWindowController * windowController = nil ; if ([ object isKindOfClass : [ AIListBookmark class ]]) { windowController = [ AILogViewerWindowController openForChatName : (( AIListBookmark * ) object ). name withAccount :(( AIListBookmark * ) object ). account } else if ([ object isKindOfClass : [ AIListContact class ]]) { windowController = [ AILogViewerWindowController openForContact : ( AIListContact * ) object plugin : self ]; [ NSApp activateIgnoringOtherApps : YES ]; [[ windowController window ] makeKeyAndOrderFront : nil ]; * @brief Show the log viewer for the active chat * This is called when a chat is definitely in focus, i.e. the toolbar item. - ( void ) showLogViewerForActiveChat: ( id ) sender AIChat * activeChat = adium . interfaceController . activeChat ; if ( activeChat . isGroupChat ) { [ AILogViewerWindowController openForChatName : activeChat . name withAccount : activeChat . account plugin : self ]; [ AILogViewerWindowController openForContact : activeChat . listObject * @brief Show the log viewer with the menu context chat * Opens the log window for a specific AIChat which the context menu is currently referencing. * This is called by the group chat's context menu to open its logs. - ( void ) showLogViewerForGroupChat: ( id ) sender AIChat * contextChat = adium . menuController . currentContextMenuChat ; [ NSApp activateIgnoringOtherApps : YES ]; [ AILogViewerWindowController openForChatName : contextChat . name withAccount : contextChat . account - ( void ) showLogViewerAndReindex: ( id ) sender userTriggeredReindex = YES ; [ self showLogViewer : nil ]; userTriggeredReindex = NO ; - ( void ) showLogNotification: ( NSNotification * ) inNotification [ self _showLogViewerForLogAtPath : [ inNotification object ]]; - ( void ) _showLogViewerForLogAtPath: ( NSString * ) inPath [ AILogViewerWindowController openLogAtPath : inPath plugin : self ]; //Log any content that is sent or received - ( void ) contentObjectAdded: ( NSNotification * ) notification AIContentMessage * content = [[ notification userInfo ] objectForKey : @"AIContentObject" ]; if ([ content postProcessContent ]) { AIChat * chat = [ notification object ]; if ( ! [ chat shouldLog ]) return ; __block __typeof__ ( self ) bself = self ; dispatch_group_async ( logAppendingGroup , dispatch_get_main_queue (), blockWithAutoreleasePool ( ^ { NSString * contentType = [ content type ]; NSString * date = [[[ content date ] dateWithCalendarFormat : nil timeZone : nil ] ISO8601DateString ]; if ([ contentType isEqualToString : CONTENT_MESSAGE_TYPE ] || [ contentType isEqualToString : CONTENT_CONTEXT_TYPE ]) { NSMutableArray * attributeKeys = [ NSMutableArray arrayWithObjects : @"sender" , @"time" , nil ]; NSMutableArray * attributeValues = [ NSMutableArray arrayWithObjects : [[ content source ] UID ], date , nil ]; AIXMLAppender * appender = [ self _appenderForChat : chat ]; if ([ content isAutoreply ]) { [ attributeKeys addObject : @"auto" ]; [ attributeValues addObject : @"true" ]; NSString * displayName = [ chat displayNameForContact : content . source ]; if ( ! [[[ content source ] UID ] isEqualToString : displayName ]) { [ attributeKeys addObject : @"alias" ]; [ attributeValues addObject : displayName ]; AIXMLElement * messageElement = [[[ AIXMLElement alloc ] initWithName : @"message" ] autorelease ]; [ messageElement addEscapedObject : [ xhtmlDecoder encodeHTML : [ content message ] imagesPath :[ appender . path stringByDeletingLastPathComponent ]]]; [ messageElement setAttributeNames : attributeKeys values : attributeValues ]; [ appender appendElement : messageElement ]; //XXX: Yucky hack. This is here because we get status and event updates for metas, not for individual contacts. Or something like that. AIListObject * retardedMetaObject = [ content source ]; AIListObject * actualObject = nil ; for ( AIListContact * participatingListObject in chat ) { if ([ participatingListObject parentContact ] == retardedMetaObject ) { actualObject = participatingListObject ; //If we can't find it for some reason, we probably shouldn't attempt logging, unless source was nil. if ([ contentType isEqualToString : CONTENT_STATUS_TYPE ] && actualObject ) { NSString * translatedStatus = [ statusTranslation objectForKey : [( AIContentStatus * ) content status ]]; if ( translatedStatus == nil ) { AILogWithSignature ( @"AILogger: Don't know how to translate status: %@" , [( AIContentStatus * ) content status ]); NSMutableArray * attributeKeys = [ NSMutableArray arrayWithObjects : @"type" , @"sender" , @"time" , nil ]; NSMutableArray * attributeValues = [ NSMutableArray arrayWithObjects : if ( ! [ actualObject . UID isEqualToString : actualObject . displayName ]) { [ attributeKeys addObject : @"alias" ]; [ attributeValues addObject : actualObject . displayName ]; AIXMLElement * statusElement = [[[ AIXMLElement alloc ] initWithName : @"status" ] autorelease ]; [ statusElement addEscapedObject : ([( AIContentStatus * ) content loggedMessage ] ? [ xhtmlDecoder encodeHTML : [( AIContentStatus * ) content loggedMessage ] imagesPath : nil ] : [ statusElement setAttributeNames : attributeKeys values : attributeValues ]; [[ bself _appenderForChat : chat ] appendElement : statusElement ]; } else if ([ contentType isEqualToString : CONTENT_EVENT_TYPE ] || [ contentType isEqualToString : CONTENT_NOTIFICATION_TYPE ]) { NSMutableArray * attributeKeys = nil , * attributeValues = nil ; attributeKeys = [ NSMutableArray arrayWithObjects : @"type" , @"sender" , @"time" , nil ]; attributeValues = [ NSMutableArray arrayWithObjects : [( AIContentEvent * ) content eventType ], [[ content source ] UID ], date , nil ]; attributeKeys = [ NSMutableArray arrayWithObjects : @"type" , @"time" , nil ]; attributeValues = [ NSMutableArray arrayWithObjects : [( AIContentEvent * ) content eventType ], date , nil ]; AIXMLAppender * appender = [ self _appenderForChat : chat ]; if ( content . source && ! [[[ content source ] UID ] isEqualToString : [[ content source ] displayName ]]) { [ attributeKeys addObject : @"alias" ]; [ attributeValues addObject : [[ content source ] displayName ]]; AIXMLElement * statusElement = [[[ AIXMLElement alloc ] initWithName : @"status" ] autorelease ]; [ statusElement addEscapedObject : [ xhtmlDecoder encodeHTML : [ content message ] imagesPath :[[ appender path ] stringByDeletingLastPathComponent ]]]; [ statusElement setAttributeNames : attributeKeys values : attributeValues ]; [ appender appendElement : statusElement ]; //Don't create a new one if not needed AIXMLAppender * appender = [ self _existingAppenderForChat : chat ]; [ bself _markLogDirtyAtPath : [ appender path ] forChat : chat ]; - ( void ) chatOpened: ( NSNotification * ) notification AIChat * chat = [ notification object ]; if ( ! [ chat shouldLog ]) return ; //Try reusing the appender object AIXMLAppender * appender = [ self _existingAppenderForChat : chat ]; //If there is an appender, add the windowOpened event /* Ensure a timeout isn't set for closing the appender, since we're now using it. * This gives us the desired behavior - if chat #2 opens before the timeout on the * log file, then we want to keep the log continuous until the user has closed the [ NSObject cancelPreviousPerformRequestsWithTarget : self selector : @selector ( finishClosingAppender : ) object :[ self keyForChat : chat ]]; // Print the windowOpened event in the log AIXMLElement * eventElement = [[[ AIXMLElement alloc ] initWithName : @"event" ] autorelease ]; [ eventElement setAttributeNames : [ NSArray arrayWithObjects : @"type" , @"sender" , @"time" , nil ] values :[ NSArray arrayWithObjects : @"windowOpened" , chat . account . UID , [[[ NSDate date ] dateWithCalendarFormat : nil timeZone : nil ] ISO8601DateString ], nil ]]; [ appender appendElement : eventElement ]; [ self _markLogDirtyAtPath : [ appender path ] forChat : chat ]; - ( void ) chatClosed: ( NSNotification * ) notification AIChat * chat = [ notification object ]; if ( ! [ chat shouldLog ]) return ; //Use this method so we don't create a new appender for chat close events AIXMLAppender * appender = [ self _existingAppenderForChat : chat ]; //If there is an appender, add the windowClose event AIXMLElement * eventElement = [[[ AIXMLElement alloc ] initWithName : @"event" ] autorelease ]; [ eventElement setAttributeNames : [ NSArray arrayWithObjects : @"type" , @"sender" , @"time" , nil ] values :[ NSArray arrayWithObjects : @"windowClosed" , chat . account . UID , [[[ NSDate date ] dateWithCalendarFormat : nil timeZone : nil ] ISO8601DateString ], nil ]]; [ appender appendElement : eventElement ]; [ self closeAppenderForChat : chat ]; [ self _markLogDirtyAtPath : [ appender path ] forChat : chat ]; //Ugly method. Shouldn't this notification post an AIChat, not an AIChatLog? - ( void ) chatWillDelete: ( NSNotification * ) notification AIChatLog * chatLog = [ notification object ]; NSString * chatID = [ NSString stringWithFormat : @"%@.%@-%@" , [ chatLog serviceClass ], [ chatLog from ], [ chatLog to ]]; AIXMLAppender * appender = [ activeAppenders objectForKey : chatID ]; if ([[ appender path ] hasSuffix : [ chatLog relativePath ]]) { [ NSObject cancelPreviousPerformRequestsWithTarget : self selector : @selector ( finishClosingAppender : ) [ self finishClosingAppender : chatID ]; #pragma mark Logging Internals - ( AIXMLAppender * ) _appenderForChat: ( AIChat * ) chat //Check if there is already an appender for this chat AIXMLAppender * appender = [ self _existingAppenderForChat : chat ]; //Ensure a timeout isn't set for closing the appender, since we're now using it [ NSObject cancelPreviousPerformRequestsWithTarget : self selector : @selector ( finishClosingAppender : ) object :[ self keyForChat : chat ]]; //If there isn't already an appender, create a new one and add it to the dictionary NSDate * chatDate = [ chat dateOpened ]; NSString * fullPath = [ AILoggerPlugin fullPathForLogOfChat : chat onDate : chatDate ]; AIXMLElement * rootElement = [[[ AIXMLElement alloc ] initWithName : @"chat" ] autorelease ]; [ rootElement setAttributeNames : [ NSArray arrayWithObjects : @"xmlns" , @"account" , @"service" , @"adiumversion" , @"buildid" , nil ] values :[ NSArray arrayWithObjects : chat . account . service . serviceID , [[ NSBundle mainBundle ] objectForInfoDictionaryKey : @"CFBundleShortVersionString" ], [[ NSBundle mainBundle ] objectForInfoDictionaryKey : @"AIBuildIdentifier" ], appender = [ AIXMLAppender documentWithPath : fullPath rootElement : rootElement ]; //Add the window opened event now AIXMLElement * eventElement = [[[ AIXMLElement alloc ] initWithName : @"event" ] autorelease ]; [ eventElement setAttributeNames : [ NSArray arrayWithObjects : @"type" , @"sender" , @"time" , nil ] values :[ NSArray arrayWithObjects : @"windowOpened" , chat . account . UID , [[[ NSDate date ] dateWithCalendarFormat : nil timeZone : nil ] ISO8601DateString ], nil ]]; [ appender appendElement : eventElement ]; [ activeAppenders setObject : appender forKey : [ self keyForChat : chat ]]; [ self _markLogDirtyAtPath : [ appender path ] forChat : chat ]; - ( AIXMLAppender * ) _existingAppenderForChat: ( AIChat * ) chat //Look up the key for this chat and use it to try to retrieve the appender return [ activeAppenders objectForKey : [ self keyForChat : chat ]]; - ( NSString * ) keyForChat: ( AIChat * ) chat AIAccount * account = chat . account ; NSString * chatID = ( chat . isGroupChat ? [ chat identifier ] : chat . listObject . UID ); return [ NSString stringWithFormat : @"%@.%@-%@" , account . service . serviceID , account . UID , chatID ]; - ( void ) closeAppenderForChat: ( AIChat * ) chat //Create a new timer to fire after the timeout period, which will close the appender NSString * chatKey = [ self keyForChat : chat ]; [ NSObject cancelPreviousPerformRequestsWithTarget : self selector : @selector ( finishClosingAppender : ) [ self performSelector : @selector ( finishClosingAppender : ) afterDelay : NEW_LOGFILE_TIMEOUT ]; - ( void ) finishClosingAppender: ( NSString * ) chatKey //Remove the appender, closing its file descriptor upon dealloc [ activeAppenders removeObjectForKey : chatKey ]; #pragma mark Log Indexing - ( NSString * ) _logIndexPath return [[ adium cachesPath ] stringByAppendingPathComponent : LOG_INDEX_NAME ]; - ( NSString * ) _dirtyLogSetPath return [[ adium cachesPath ] stringByAppendingPathComponent : DIRTY_LOG_SET_NAME ]; if ([ self . dirtyLogSet count ] == 0 ) { NSInteger logVersion = [[ adium . preferenceController preferenceForKey : KEY_LOG_INDEX_VERSION group : PREF_GROUP_LOGGING ] integerValue ]; //If the log version has changed, we reset the index and don't load the dirty set (So all the logs are marked dirty) if ( logVersion >= CURRENT_LOG_VERSION ) { __block __typeof__ ( self ) bself = self ; dispatch_sync ( dirtyLogSetMutationQueue , ^ { [ bself . dirtyLogSet addObjectsFromArray : [ NSArray arrayWithContentsOfFile : [ bself _dirtyLogSetPath ]]]; AILogWithSignature ( @"Loaded dirty log set with %i logs" ,[ bself . dirtyLogSet count ]); AILogWithSignature ( @"**** Log version upgrade. Resetting" ); [ adium . preferenceController setPreference : [ NSNumber numberWithInteger : CURRENT_LOG_VERSION ] forKey : KEY_LOG_INDEX_VERSION group : PREF_GROUP_LOGGING ]; if ([[ NSFileManager defaultManager ] fileExistsAtPath : [ self _logIndexPath ]]) { [[ NSFileManager defaultManager ] removeItemAtPath : [ self _logIndexPath ] error : NULL ]; if ([[ NSFileManager defaultManager ] fileExistsAtPath : [ self _dirtyLogSetPath ]]) { [[ NSFileManager defaultManager ] removeItemAtPath : [ self _dirtyLogSetPath ] error : NULL ]; - ( void ) _cancelClosingLogIndex __block __typeof__ ( self ) bself = self ; dispatch_async ( defaultDispatchQueue , ^ { bself . canCloseIndex = NO ; dispatch_group_wait ( closingIndexGroup , DISPATCH_TIME_FOREVER ); bself . canCloseIndex = YES ; __block __typeof__ ( self ) bself = self ; dispatch_sync ( dirtyLogSetMutationQueue , ^ { [ bself . dirtyLogSet removeAllObjects ]; dispatch_group_async ( loggerPluginGroup , defaultDispatchQueue , blockWithAutoreleasePool ( ^ { dispatch_group_wait ( logIndexingGroup , DISPATCH_TIME_FOREVER ); dispatch_group_wait ( closingIndexGroup , DISPATCH_TIME_FOREVER ); dispatch_group_wait ( logAppendingGroup , DISPATCH_TIME_FOREVER ); bself . canSaveDirtyLogSet = NO ; //Process each from folder NSString * _logBasePath = [[ bself class ] logBasePath ]; NSArray * fromNames = [[ NSFileManager defaultManager ] contentsOfDirectoryAtPath : _logBasePath for ( NSString * fromName in fromNames ) { AILogFromGroup * fromGroup = [[ AILogFromGroup alloc ] initWithPath : fromName for ( AILogToGroup * toGroup in [ fromGroup toGroupArray ]) { NSAutoreleasePool * innerPool = [[ NSAutoreleasePool alloc ] init ]; for ( AIChatLog * theLog in [ toGroup logEnumerator ]) { dispatch_sync ( dirtyLogSetMutationQueue , ^ { [ bself . dirtyLogSet addObject : [ _logBasePath stringByAppendingPathComponent : [ theLog relativePath ]]]; AILogWithSignature ( @"Finished dirtying all logs" ); bself . canSaveDirtyLogSet = YES ; [ bself _saveDirtyLogSet ]; if ( bself . indexingAllowed && [ AILogViewerWindowController existingWindowController ]) { * @brief Index all dirty logs * Indexing will occur on a thread __block SInt64 _remainingLogs = 0 ; //Do nothing if we're paused if ( ! self . indexingAllowed ) return ; //Reset the cleaning progress __block __typeof__ ( self ) bself = self ; __block NSMutableSet * localLogSet = nil ; dispatch_sync ( dirtyLogSetMutationQueue , ^ { localLogSet = [[ self . dirtyLogSet mutableCopy ] autorelease ]; OSAtomicCompareAndSwap64Barrier ( bself -> logsToIndex , [ localLogSet count ], ( int64_t * ) & ( bself -> logsToIndex )); OSAtomicCompareAndSwap64Barrier ( _remainingLogs , bself -> logsToIndex , ( int64_t * ) & _remainingLogs ); if ( self . logsToIndex == 0 ){ dispatch_async ( defaultDispatchQueue , ^ { OSAtomicCompareAndSwap64Barrier ( logsIndexed , 0 , ( int64_t * ) & logsIndexed ); [ bself _didCleanDirtyLogs ]; __block SKIndexRef searchIndex = [ self logContentIndex ]; AILogWithSignature ( @"*** Warning: Could not open searchIndex in -[%@ _cleanDirtyLogs]. That shouldn't happen!" , self ); OSAtomicCompareAndSwap64Barrier ( logsIndexed , 0 , ( int64_t * ) & logsIndexed ); if ( self . indexingAllowed ) { __block UInt32 lastUpdate = TickCount (); __block SInt32 unsavedChanges = 0 ; AILogWithSignature ( @"Cleaning %i dirty logs" , [ localLogSet count ]); dispatch_group_async ( loggerPluginGroup , searchIndexQueue , blockWithAutoreleasePool ( ^ { dispatch_group_enter ( logIndexingGroup ); while ( _remainingLogs > 0 && bself . indexingAllowed ) { NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; __block NSString * __logPath = nil ; dispatch_sync ( dirtyLogSetMutationQueue , ^ { if ([ localLogSet count ]) { __logPath = [[[ localLogSet anyObject ] retain ] autorelease ]; [ bself . dirtyLogSet removeObject : __logPath ]; [ localLogSet removeObject : __logPath ]; logPath = [[ __logPath copy ] autorelease ]; NSURL * logURL = [ NSURL fileURLWithPath : logPath ]; NSAssert ( logURL != nil , @"Converting path to url failed" ); dispatch_semaphore_wait ( logLoadingPrefetchSemaphore , DISPATCH_TIME_FOREVER ); dispatch_group_async ( logIndexingGroup , ioQueue , blockWithAutoreleasePool ( ^ { __block SKDocumentRef document = SKDocumentCreateWithURL (( CFURLRef ) logURL ); if ( document && bself . indexingAllowed ) { /* We _could_ use SKIndexAddDocument() and depend on our Spotlight plugin for importing. * However, this has three problems: * 1. Slower, especially to start initial indexing, which is the most common use case since the log viewer * indexes recently-modified ("dirty") logs when it opens. * 2. Sometimes logs don't appear to be associated with the right URI type and therefore don't get indexed. * 3. On 10.3, this means that logs' markup is indexed in addition to their text, which is undesireable. NSData * documentData = CopyDataForURL ( NULL , logURL ); dispatch_semaphore_wait ( jobSemaphore , DISPATCH_TIME_FOREVER ); dispatch_group_async ( logIndexingGroup , defaultDispatchQueue , blockWithAutoreleasePool ( ^ { __block CFStringRef documentText = CopyTextContentForFileData ( NULL , logURL , documentData ); dispatch_group_async ( logIndexingGroup , defaultDispatchQueue , blockWithAutoreleasePool ( ^ { if ( documentText && CFStringGetLength ( documentText ) > 0 && bself . indexingAllowed ) { static dispatch_queue_t skQueue = nil ; static dispatch_once_t onceToken ; dispatch_once ( & onceToken , ^ { skQueue = dispatch_queue_create ( "im.adium.AILoggerPlugin._cleanDirtyLogs.skQueue" , 0 ); dispatch_group_async ( logIndexingGroup , skQueue , ^ { SKIndexAddDocumentWithText ( searchIndex , document , documentText , YES ); OSAtomicIncrement64Barrier (( int64_t * ) & ( bself -> logsIndexed )); OSAtomicDecrement64Barrier (( int64_t * ) & _remainingLogs ); if ( lastUpdate == 0 || TickCount () > lastUpdate + LOG_INDEX_STATUS_INTERVAL || _remainingLogs == 0 ) { dispatch_async ( dispatch_get_main_queue (), ^ { [[ AILogViewerWindowController existingWindowController ] logIndexingProgressUpdate ]; UInt32 tick = TickCount (); OSAtomicCompareAndSwap32Barrier ( lastUpdate , tick , ( int32_t * ) & lastUpdate ); OSAtomicIncrement32Barrier (( int32_t * ) & unsavedChanges ); if ( unsavedChanges > LOG_CLEAN_SAVE_INTERVAL ) { [ bself _saveDirtyLogSet ]; OSAtomicCompareAndSwap32Barrier ( unsavedChanges , 0 , ( int32_t * ) & unsavedChanges ); dispatch_semaphore_signal ( jobSemaphore ); } else if ( documentText ) { dispatch_semaphore_signal ( jobSemaphore ); OSAtomicIncrement64Barrier (( int64_t * ) & ( bself -> logsIndexed )); OSAtomicDecrement64Barrier (( int64_t * ) & _remainingLogs ); dispatch_semaphore_signal ( jobSemaphore ); dispatch_semaphore_signal ( logLoadingPrefetchSemaphore ); AILogWithSignature ( @"Could not create document for %@ [%@]" , logPath , logURL ); OSAtomicIncrement64Barrier (( int64_t * ) & ( bself -> logsIndexed )); OSAtomicDecrement64Barrier (( int64_t * ) & _remainingLogs ); dispatch_semaphore_signal ( jobSemaphore ); dispatch_semaphore_signal ( logLoadingPrefetchSemaphore ); [ pool release ]; pool = nil ; [ bself _saveDirtyLogSet ]; dispatch_group_enter ( closingIndexGroup ); dispatch_group_leave ( logIndexingGroup ); dispatch_group_notify ( logIndexingGroup , searchIndexQueue , ^ { dispatch_async ( dispatch_get_main_queue (), ^ { [[ AILogViewerWindowController existingWindowController ] logIndexingProgressUpdate ]; [ bself _flushIndex : searchIndex ]; AILogWithSignature ( @"After cleaning dirty logs, the search index has a max ID of %i and a count of %i" , SKIndexGetMaximumDocumentID ( searchIndex ), SKIndexGetDocumentCount ( searchIndex )); [ bself _didCleanDirtyLogs ]; dispatch_group_leave ( closingIndexGroup ); #pragma mark Log Indexing Internals - ( void ) _didCleanDirtyLogs NSLog ( @"_didCleanDirtyLogs" ); __block __typeof__ ( self ) bself = self ; dispatch_sync ( dirtyLogSetMutationQueue , ^ { OSAtomicCompareAndSwap64Barrier ( bself -> logsToIndex , [ bself -> dirtyLogSet count ], ( int64_t * ) & ( bself -> logsToIndex )); //Clear the dirty status of all open chats so they will be marked dirty if they receive another message for ( AIChat * chat in adium . chatController . openChats ) { NSString * existingAppenderPath = [[ self _existingAppenderForChat : chat ] path ]; if ( existingAppenderPath ) { NSString * dirtyKey = [ @"LogIsDirty_" stringByAppendingString : existingAppenderPath ]; if ([ chat integerValueForProperty : dirtyKey ]) { dispatch_async ( dispatch_get_main_queue (), ^ { [[ AILogViewerWindowController existingWindowController ] logIndexingProgressUpdate ]; __block __typeof__ ( self ) bself = self ; dispatch_group_async ( loggerPluginGroup , dirtyLogSetMutationQueue , ^ { NSSet * _dirtySet = [[ bself . dirtyLogSet copy ] autorelease ]; AILogWithSignature ( @"Saving %lu dirty logs" , _dirtySet . count ); if ([ _dirtySet count ] > 0 && bself . canSaveDirtyLogSet ) { dispatch_async ( ioQueue , ^ { [[ _dirtySet allObjects ] writeToFile : [ bself _dirtyLogSetPath ] - ( void ) _markLogDirtyAtPath: ( NSString * ) path forChat: ( AIChat * ) chat NSParameterAssert ( path != nil ); NSParameterAssert ( chat != nil ); __block __typeof__ ( self ) bself = self ; dispatch_async ( defaultDispatchQueue , ^ { dispatch_async ( dirtyLogSetMutationQueue , ^ { if ( path && ! [ bself . dirtyLogSet containsObject : path ]) { [ bself . dirtyLogSet addObject : path ]; dispatch_group_async ( loggerPluginGroup , defaultDispatchQueue , blockWithAutoreleasePool ( ^ { [ bself _saveDirtyLogSet ]; __block __typeof__ ( self ) bself = self ; dispatch_group_wait ( logIndexingGroup , DISPATCH_TIME_FOREVER ); dispatch_group_async ( closingIndexGroup , searchIndexQueue , ^ { [ bself _flushIndex : bself -> logIndex ]; if ( bself . canCloseIndex ) { AILogWithSignature ( @"**** %@ Releasing its index %p (%d)" , bself , bself -> logIndex , CFGetRetainCount ( bself -> logIndex )); SKIndexClose ( bself -> logIndex ); - ( void ) _flushIndex: ( SKIndexRef ) inIndex NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; self . indexIsFlushing = YES ; AILogWithSignature ( @"**** Flushing index %p" , inIndex ); AILogWithSignature ( @"**** Finished flushing index %p" , inIndex ); self . indexIsFlushing = NO ;