Fixed a bunch of unit tests to restore state after they complete.
[adiumx.git] / Source / AILoggerPlugin.m
blob61f3cdca2dd0b1352c2bf79b985d15b452b2f2e3
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import "AILoggerPlugin.h"
18 #import "AIChatLog.h"
19 #import "AILogFromGroup.h"
20 #import "AILogToGroup.h"
21 #import "AIMDLogViewerWindowController.h"
22 #import "AIXMLAppender.h"
23 #import <Adium/AIContentControllerProtocol.h>
24 #import <Adium/AIInterfaceControllerProtocol.h>
25 #import <Adium/AIChatControllerProtocol.h>
26 #import <Adium/AILoginControllerProtocol.h>
27 #import <Adium/AIMenuControllerProtocol.h>
28 #import <Adium/AIPreferenceControllerProtocol.h>
29 #import <Adium/AIToolbarControllerProtocol.h>
30 #import <Adium/AIAccount.h>
31 #import <Adium/AIChat.h>
32 #import <Adium/AIContentMessage.h>
33 #import <Adium/AIContentStatus.h>
34 #import <Adium/AIContentEvent.h>
35 #import <Adium/AIHTMLDecoder.h>
36 #import <Adium/AIListContact.h>
37 #import <Adium/AIService.h>
38 #import <AIUtilities/AIAttributedStringAdditions.h>
39 #import <AIUtilities/AIDictionaryAdditions.h>
40 #import <AIUtilities/AIFileManagerAdditions.h>
41 #import <AIUtilities/AIMenuAdditions.h>
42 #import <AIUtilities/AIStringAdditions.h>
43 #import <AIUtilities/AIToolbarUtilities.h>
44 #import <AIUtilities/AIApplicationAdditions.h>
45 #import <AIUtilities/AIDateFormatterAdditions.h>
46 #import <AIUtilities/AIImageAdditions.h>
47 #import <AIUtilities/NSCalendarDate+ISO8601Unparsing.h>
48 #import <AIUtilities/NSCalendarDate+ISO8601Parsing.h>
50 #import "AILogFileUpgradeWindowController.h"
52 #import "AdiumSpotlightImporter.h"
54 #define LOG_INDEX_NAME                          @"Logs.index"
55 #define DIRTY_LOG_ARRAY_NAME            @"DirtyLogs.plist"
56 #define KEY_LOG_INDEX_VERSION           @"Log Index Version"
58 #define LOG_INDEX_STATUS_INTERVAL       20      //Interval before updating the log indexing status
59 #define LOG_CLEAN_SAVE_INTERVAL         500     //Number of logs to index continuously before saving the dirty array and index
61 #define LOG_VIEWER                                      AILocalizedString(@"Chat Transcript Viewer",nil)
62 #define VIEW_LOGS_WITH_CONTACT          AILocalizedString(@"View Chat Transcripts",nil)
64 #define CURRENT_LOG_VERSION                     8       //Version of the log index.  Increase this number to reset everyone's index.
66 #define LOG_VIEWER_IDENTIFIER           @"LogViewer"
68 #define NEW_LOGFILE_TIMEOUT                     600             //10 minutes
70 #define ENABLE_PROXIMITY_SEARCH         TRUE
72 @interface AILoggerPlugin (PRIVATE)
73 - (void)configureMenuItems;
74 - (SKIndexRef)createLogIndex;
75 - (void)closeLogIndex;
76 - (void)resetLogIndex;
77 - (NSString *)_logIndexPath;
78 - (void)loadDirtyLogArray;
79 - (void)_saveDirtyLogArray;
80 - (NSString *)_dirtyLogArrayPath;
81 - (void)_dirtyAllLogsThread;
82 - (void)_cleanDirtyLogsThread;
84 - (void)upgradeLogExtensions;
85 - (void)reimportLogsToSpotlightIfNeeded;
87 - (NSString *)keyForChat:(AIChat *)chat;
88 - (AIXMLAppender *)existingAppenderForChat:(AIChat *)chat;
89 - (AIXMLAppender *)appenderForChat:(AIChat *)chat;
90 - (void)closeAppenderForChat:(AIChat *)chat;
91 - (void)finishClosingAppender:(NSString *)chatKey;
92 @end
94 static NSString     *logBasePath = nil;     //The base directory of all logs
95 Class LogViewerWindowControllerClass = NULL;
96 @implementation AILoggerPlugin
99 - (void)installPlugin
101         //XXX: this should be refactored now that we can rely on Tiger.
102         LogViewerWindowControllerClass = [AIMDLogViewerWindowController class];
104     //Init
105         observingContent = NO;
107         activeAppenders = [[NSMutableDictionary alloc] init];
108         
109         xhtmlDecoder = [[AIHTMLDecoder alloc] initWithHeaders:NO
110                                                                                                  fontTags:YES
111                                                                                         closeFontTags:YES
112                                                                                                 colorTags:YES
113                                                                                                 styleTags:YES
114                                                                                    encodeNonASCII:YES
115                                                                                          encodeSpaces:NO
116                                                                                 attachmentsAsText:YES
117                                                                 onlyIncludeOutgoingImages:NO
118                                                                                    simpleTagsOnly:NO
119                                                                                    bodyBackground:NO];
120         [xhtmlDecoder setGeneratesStrictXHTML:YES];
121         [xhtmlDecoder setUsesAttachmentTextEquivalents:YES];
122         
123         statusTranslation = [[NSDictionary alloc] initWithObjectsAndKeys:
124                 @"away",@"away",
125                 @"online",@"return_away",
126                 @"online",@"online",
127                 @"offline",@"offline",
128                 @"idle",@"idle",
129                 @"available",@"return_idle",
130                 @"away",@"away_message",
131                 nil];
133         //Setup our preferences
134         [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:LOGGING_DEFAULT_PREFS 
135                                                                                                                                                 forClass:[self class]] 
136                                                                                   forGroup:PREF_GROUP_LOGGING];
138         //Install the log viewer menu items
139         [self configureMenuItems];
140         
141         //Create a logs directory
142         logBasePath = [[[[[adium loginController] userDirectory] stringByAppendingPathComponent:PATH_LOGS] stringByExpandingTildeInPath] retain];
143         [[NSFileManager defaultManager] createDirectoriesForPath:logBasePath];
145         //Observe preference changes
146         [[adium preferenceController] addObserver:self
147                                                                    forKeyPath:PREF_KEYPATH_LOGGER_ENABLE
148                                                                           options:NSKeyValueObservingOptionNew
149                                                                           context:NULL];
150         [self observeValueForKeyPath:PREF_KEYPATH_LOGGER_ENABLE
151                             ofObject:[adium preferenceController]
152                               change:nil
153                              context:NULL];
155         //Toolbar item
156         NSToolbarItem   *toolbarItem;
157         toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:LOG_VIEWER_IDENTIFIER
158                                                                                                                   label:AILocalizedString(@"Transcripts",nil)
159                                                        paletteLabel:AILocalizedString(@"View Chat Transcripts",nil)
160                                                             toolTip:AILocalizedString(@"View previous conversations with this contact or chat",nil)
161                                                              target:self
162                                                     settingSelector:@selector(setImage:)
163                                                         itemContent:[NSImage imageNamed:@"LogViewer" forClass:[self class]]
164                                                              action:@selector(showLogViewerToSelectedContact:)
165                                                                menu:nil];
166         [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"ListObject"];
168         dirtyLogArray = nil;
169         index_Content = nil;
170         stopIndexingThreads = NO;
171         suspendDirtyArraySave = NO;             
172         indexingThreadLock = [[NSLock alloc] init];
173         dirtyLogLock = [[NSLock alloc] init];
174         logAccessLock = [[NSLock alloc] init];
175         
176         //Init index searching
177         [self initLogIndexing];
178         
179         [self upgradeLogExtensions];
180         [self reimportLogsToSpotlightIfNeeded];
182         [[adium notificationCenter] addObserver:self
183                                                                    selector:@selector(showLogNotification:)
184                                                                            name:AIShowLogAtPathNotification
185                                                                          object:nil];
188 - (void)uninstallPlugin
190         [activeAppenders release]; activeAppenders = nil;
191         [xhtmlDecoder release]; xhtmlDecoder = nil;
192         [statusTranslation release]; statusTranslation = nil;
194         [NSObject cancelPreviousPerformRequestsWithTarget:self];
196         [[adium notificationCenter] removeObserver:self];
197         [[adium preferenceController] removeObserver:self forKeyPath:PREF_KEYPATH_LOGGER_ENABLE];
200 //Update for the new preferences
201 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
203         BOOL    newLogValue;
204         logHTML = YES;
206         //Start/Stop logging
207         newLogValue = [[object valueForKeyPath:keyPath] boolValue];
208         if (newLogValue != observingContent) {
209                 observingContent = newLogValue;
210                                 
211                 if (!observingContent) { //Stop Logging
212                         [[adium notificationCenter] removeObserver:self name:Content_ContentObjectAdded object:nil];
213                         
214                         [[adium notificationCenter] removeObserver:self name:Chat_DidOpen object:nil];                  
215                         [[adium notificationCenter] removeObserver:self name:Chat_WillClose object:nil];
217                 } else { //Start Logging
218                         [[adium notificationCenter] addObserver:self 
219                                                                                    selector:@selector(contentObjectAdded:) 
220                                                                                            name:Content_ContentObjectAdded 
221                                                                                          object:nil];
222                                                                                          
223                         [[adium notificationCenter] addObserver:self
224                                                                                    selector:@selector(chatOpened:)
225                                                                                            name:Chat_DidOpen
226                                                                                          object:nil];
227                                                                                          
228                         [[adium notificationCenter] addObserver:self
229                                                                                    selector:@selector(chatClosed:)
230                                                                                            name:Chat_WillClose
231                                                                                          object:nil];
232                         [[adium notificationCenter] addObserver:self
233                                                                                    selector:@selector(chatWillDelete:)
234                                                                                            name:ChatLog_WillDelete
235                                                                                          object:nil];
236                 }
237         }
241 //Logging Paths --------------------------------------------------------------------------------------------------------
242 + (NSString *)logBasePath
244         return logBasePath;
247 //Returns the RELATIVE path to the folder where the log should be written
248 + (NSString *)relativePathForLogWithObject:(NSString *)object onAccount:(AIAccount *)account
249 {       
250         return [NSString stringWithFormat:@"%@.%@/%@", [account serviceID], [[account UID] safeFilenameString], object];
253 + (NSString *)fileNameForLogWithObject:(NSString *)object onDate:(NSDate *)date
255         NSParameterAssert(date != nil);
256         NSParameterAssert(object != nil);
257         NSString    *dateString = [date descriptionWithCalendarFormat:@"%Y-%m-%dT%H.%M.%S%z" timeZone:nil locale:nil];
258         
259         NSAssert2(dateString != nil, @"Date string was invalid for the chatlog for %@ on %@", object, date);
260                 
261         return [NSString stringWithFormat:@"%@ (%@).chatlog", object, dateString];
264 + (NSString *)fullPathForLogOfChat:(AIChat *)chat onDate:(NSDate *)date
266         NSString        *objectUID = [chat name];
267         AIAccount       *account = [chat account];
269         if (!objectUID) objectUID = [[chat listObject] UID];
270         objectUID = [objectUID safeFilenameString];
272         NSString        *fileName = [self fileNameForLogWithObject:objectUID onDate:date];
273         NSString        *absolutePath = [logBasePath stringByAppendingPathComponent:[self relativePathForLogWithObject:objectUID onAccount:account]];
274         NSString        *fullPath = [absolutePath stringByAppendingPathComponent:fileName];
276         return fullPath;
279 //Menu Items -----------------------------------------------------------------------------------------------------------
280 #pragma mark Menu Items
281 //Configure the log viewer menu items
282 - (void)configureMenuItems
284     logViewerMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:LOG_VIEWER 
285                                                                                                                                                           target:self
286                                                                                                                                                           action:@selector(showLogViewer:)
287                                                                                                                                            keyEquivalent:@"L"] autorelease];
288     [[adium menuController] addMenuItem:logViewerMenuItem toLocation:LOC_Window_Auxiliary];
290     viewContactLogsMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:VIEW_LOGS_WITH_CONTACT
291                                                                                                                                                                         target:self
292                                                                                                                                                                         action:@selector(showLogViewerToSelectedContact:) 
293                                                                                                                                                          keyEquivalent:@"l"] autorelease];
294     [[adium menuController] addMenuItem:viewContactLogsMenuItem toLocation:LOC_Contact_Info];
296     viewContactLogsContextMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:VIEW_LOGS_WITH_CONTACT
297                                                                                                                                                                                    target:self
298                                                                                                                                                                                    action:@selector(showLogViewerToSelectedContextContact:) 
299                                                                                                                                                                         keyEquivalent:@""] autorelease];
300     [[adium menuController] addContextualMenuItem:viewContactLogsContextMenuItem toLocation:Context_Contact_Manage];
303 //Enable/Disable our view log menus
304 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
306     if (menuItem == viewContactLogsMenuItem) {
307         AIListObject    *selectedObject = [[adium interfaceController] selectedListObject];
308                 return selectedObject && [selectedObject isKindOfClass:[AIListContact class]];
310     } else if (menuItem == viewContactLogsContextMenuItem) {
311         AIListObject    *selectedObject = [[adium menuController] currentContextMenuObject];            
312                 return selectedObject && [selectedObject isKindOfClass:[AIListContact class]];
313                 
314     }
315         
316     return YES;
320  * @brief Show the log viewer for no contact
322  * Invoked from the Window menu
323  */
324 - (void)showLogViewer:(id)sender
326     [LogViewerWindowControllerClass openForContact:nil  
327                                                                                  plugin:self];  
331  * @brief Show the log viewer, displaying only the selected contact's logs
333  * Invoked from the Contact menu
334  */
335 - (void)showLogViewerToSelectedContact:(id)sender
337     AIListObject   *selectedObject = [[adium interfaceController] selectedListObject];
338     [LogViewerWindowControllerClass openForContact:([selectedObject isKindOfClass:[AIListContact class]] ?
339                                                                                                  (AIListContact *)selectedObject : 
340                                                                                                  nil)  
341                                                                                  plugin:self];
344 - (void)showLogViewerForLogAtPath:(NSString *)inPath
346         [LogViewerWindowControllerClass openLogAtPath:inPath plugin:self];
349 - (void)showLogNotification:(NSNotification *)inNotification
351         [self showLogViewerForLogAtPath:[inNotification object]];
355  * @brief Show the log viewer, displaying only the selected contact's logs
357  * Invoked from a contextual menu
358  */
359 - (void)showLogViewerToSelectedContextContact:(id)sender
361         AIListObject* object = [[adium menuController] currentContextMenuObject];
362         if ([object isKindOfClass:[AIListContact class]]) {
363                 [NSApp activateIgnoringOtherApps:YES];
364                 [[[LogViewerWindowControllerClass openForContact:(AIListContact *)object plugin:self] window]
365                                                                          makeKeyAndOrderFront:nil];
366         }
370 //Logging --------------------------------------------------------------------------------------------------------------
371 #pragma mark Logging
372 //Log any content that is sent or received
373 - (void)contentObjectAdded:(NSNotification *)notification
375         AIContentMessage        *content = [[notification userInfo] objectForKey:@"AIContentObject"];
376         if ([content postProcessContent]) {
377                 AIChat                          *chat = [notification object];
379                 //Don't log chats for temporary accounts
380                 if ([[chat account] isTemporary]) return;       
381                 
382                 BOOL                    dirty = NO;
383                 NSString                *contentType = [content type];
384                 NSString                *date = [[[content date] dateWithCalendarFormat:nil timeZone:nil] ISO8601DateString];
386                 if ([contentType isEqualToString:CONTENT_MESSAGE_TYPE]) {
387                         NSMutableArray *attributeKeys = [NSMutableArray arrayWithObjects:@"sender", @"time", nil];
388                         NSMutableArray *attributeValues = [NSMutableArray arrayWithObjects:[[content source] UID], date, nil];
389                         
390                         if([content isAutoreply])
391                         {
392                                 [attributeKeys addObject:@"auto"];
393                                 [attributeValues addObject:@"true"];
394                         }
395                         
396                         [[self appenderForChat:chat] addElementWithName:@"message" 
397                                                   escapedContent:[xhtmlDecoder encodeHTML:[content message] imagesPath:nil]
398                                                    attributeKeys:attributeKeys
399                                                  attributeValues:attributeValues];
400                         dirty = YES;
401                 } else {
402                         //XXX: Yucky hack. This is here because we get status and event updates for metas, not for individual contacts. Or something like that.
403                         AIListObject    *retardedMetaObject = [content source];
404                         AIListObject    *actualObject = nil;
405                         AIListContact   *participatingListObject = nil;
406                         
407                         NSEnumerator    *enumerator = [[chat containedObjects] objectEnumerator];
408                         
409                         while ((participatingListObject = [enumerator nextObject])) {
410                                 if ([participatingListObject parentContact] == retardedMetaObject) {
411                                         actualObject = participatingListObject;
412                                         break;
413                                 }
414                         }
415                         
416                         //If we can't find it for some reason, we probably shouldn't attempt logging.
417                         if (actualObject) {
418                                 if ([contentType isEqualToString:CONTENT_STATUS_TYPE]) {
419                                         NSString *translatedStatus = [statusTranslation objectForKey:[(AIContentStatus *)content status]];
420                                         if (translatedStatus == nil) {
421                                                 AILog(@"AILogger: Don't know how to translate status: %@", [(AIContentStatus *)content status]);
422                                         } else {
423                                                 [[self appenderForChat:chat] addElementWithName:@"status"
424                                                                           escapedContent:([(AIContentStatus *)content loggedMessage] ? [xhtmlDecoder encodeHTML:[(AIContentStatus *)content loggedMessage] imagesPath:nil] : nil)
425                                                                            attributeKeys:[NSArray arrayWithObjects:@"type", @"sender", @"time", nil]
426                                                                          attributeValues:[NSArray arrayWithObjects:
427                                                                                  translatedStatus, 
428                                                                                  [actualObject UID], 
429                                                                                  date,
430                                                                                  nil]];
431                                                 dirty = YES;
432                                         }
434                                 } else if ([contentType isEqualToString:CONTENT_EVENT_TYPE]) {
435                                         [[self appenderForChat:chat] addElementWithName:@"event"
436                                                                   escapedContent:[xhtmlDecoder encodeHTML:[content message] imagesPath:nil]
437                                                                    attributeKeys:[NSArray arrayWithObjects:@"type", @"sender", @"time", nil]
438                                                                  attributeValues:[NSArray arrayWithObjects:[(AIContentEvent *)content eventType], [[content source] UID], date, nil]];
439                                         dirty = YES;
440                                 }
441                         }
442                 }
443                 //Don't create a new one if not needed
444                 AIXMLAppender *appender = [self existingAppenderForChat:chat];
445                 if (dirty && appender)
446                         [self markLogDirtyAtPath:[appender path] forChat:chat];
447         }
450 - (void)chatOpened:(NSNotification *)notification
452         AIChat  *chat = [notification object];
454         //Don't log chats for temporary accounts
455         if ([[chat account] isTemporary]) return;       
456         
457         //Try reusing the appender object
458         AIXMLAppender *appender = [self existingAppenderForChat:chat];
459         
460         //If there is an appender, add the windowOpened event
461         if (appender) {
462                 /* Ensure a timeout isn't set for closing the appender, since we're now using it.
463                  * This gives us the desired behavior - if chat #2 opens before the timeout on the
464                  * log file, then we want to keep the log continuous until the user has closed the
465                  * window. 
466                  */
467                 [NSObject cancelPreviousPerformRequestsWithTarget:self
468                                                                                                  selector:@selector(finishClosingAppender:) 
469                                                                                                  object:[self keyForChat:chat]];
470                                                                                                    
471                 // Print the windowOpened event in the log
472                 [appender addElementWithName:@"event"
473                                                          content:nil
474                                            attributeKeys:[NSArray arrayWithObjects:@"type", @"sender", @"time", nil]
475                                          attributeValues:[NSArray arrayWithObjects:@"windowOpened", [[chat account] UID], [[[NSDate date] dateWithCalendarFormat:nil timeZone:nil] ISO8601DateString], nil]];
477                 [self markLogDirtyAtPath:[appender path] forChat:chat];
478         }
481 - (void)chatClosed:(NSNotification *)notification
483         AIChat  *chat = [notification object];
485         //Don't log chats for temporary accounts
486         if ([[chat account] isTemporary]) return;
487         
488         //Use this method so we don't create a new appender for chat close events
489         AIXMLAppender *appender = [self existingAppenderForChat:chat];
490         
491         //If there is an appender, add the windowClose event
492         if (appender) {
493                 [appender addElementWithName:@"event"
494                                                          content:nil
495                                            attributeKeys:[NSArray arrayWithObjects:@"type", @"sender", @"time", nil]
496                                          attributeValues:[NSArray arrayWithObjects:@"windowClosed", [[chat account] UID], [[[NSDate date] dateWithCalendarFormat:nil timeZone:nil] ISO8601DateString], nil]];
498                 [self closeAppenderForChat:chat];
500                 [self markLogDirtyAtPath:[appender path] forChat:chat];
501         }
504 //Ugly method. Shouldn't this notification post an AIChat, not an AIChatLog?
505 - (void)chatWillDelete:(NSNotification *)notification
507         AIChatLog *chatLog = [notification object];
508         NSString *chatID = [NSString stringWithFormat:@"%@.%@-%@", [chatLog serviceClass], [chatLog from], [chatLog to]];
509         AIXMLAppender *appender = [activeAppenders objectForKey:chatID];
510         
511         if (appender) {
512                 if ([[appender path] hasSuffix:[chatLog path]]) {
513                         [NSObject cancelPreviousPerformRequestsWithTarget:self
514                                                                                                          selector:@selector(finishClosingAppender:) 
515                                                                                                            object:chatID];
516                         [self finishClosingAppender:chatID];
517                 }
518         }
521 - (NSString *)keyForChat:(AIChat *)chat
523         AIAccount *account = [chat account];
524         NSString *chatID = ([chat isGroupChat] ? [chat identifier] : [[chat listObject] UID]);
526         return [NSString stringWithFormat:@"%@.%@-%@", [account serviceID], [account UID], chatID];
529 - (AIXMLAppender *)existingAppenderForChat:(AIChat *)chat
531         //Look up the key for this chat and use it to try to retrieve the appender
532         return [activeAppenders objectForKey:[self keyForChat:chat]];   
535 - (AIXMLAppender *)appenderForChat:(AIChat *)chat
537         //Check if there is already an appender for this chat
538         AIXMLAppender   *appender = [self existingAppenderForChat:chat];
540         if (appender) {
541                 //Ensure a timeout isn't set for closing the appender, since we're now using it
542                 [NSObject cancelPreviousPerformRequestsWithTarget:self
543                                                                                                  selector:@selector(finishClosingAppender:) 
544                                                                                                    object:[self keyForChat:chat]];
545         } else {
546                 //If there isn't already an appender, create a new one and add it to the dictionary
547                 NSDate                  *chatDate = [chat dateOpened];
548                 NSString                *fullPath = [AILoggerPlugin fullPathForLogOfChat:chat onDate:chatDate];
550                 appender = [AIXMLAppender documentWithPath:fullPath];
551                 [appender initializeDocumentWithRootElementName:@"chat"
552                                                                                   attributeKeys:[NSArray arrayWithObjects:@"xmlns", @"account", @"service", nil]
553                                                                                 attributeValues:[NSArray arrayWithObjects:
554                                                                                         XML_LOGGING_NAMESPACE,
555                                                                                         [[chat account] UID],
556                                                                                         [[chat account] serviceID],
557                                                                                         nil]];
558                 
559                 //Add the window opened event now
560                 [appender addElementWithName:@"event"
561                                          content:nil
562                            attributeKeys:[NSArray arrayWithObjects:@"type", @"sender", @"time", nil]
563                          attributeValues:[NSArray arrayWithObjects:@"windowOpened", [[chat account] UID], [[chatDate dateWithCalendarFormat:nil timeZone:nil] ISO8601DateString], nil]];
565                 [activeAppenders setObject:appender forKey:[self keyForChat:chat]];
566                 
567                 [self markLogDirtyAtPath:[appender path] forChat:chat];
568         }
569         
570         return appender;
573 - (void)closeAppenderForChat:(AIChat *)chat
575         //Create a new timer to fire after the timeout period, which will close the appender
576         NSString *chatKey = [self keyForChat:chat];
577         [NSObject cancelPreviousPerformRequestsWithTarget:self
578                                                                                          selector:@selector(finishClosingAppender:) 
579                                                                                            object:chatKey];
580         [self performSelector:@selector(finishClosingAppender:) 
581                            withObject:chatKey
582                            afterDelay:NEW_LOGFILE_TIMEOUT];
585 - (void)finishClosingAppender:(NSString *)chatKey
587         //Remove the appender, closing its file descriptor upon dealloc
588         [activeAppenders removeObjectForKey:chatKey];
592 //Display a warning to the user that logging failed, and disable logging to prevent additional warnings
593 //XXX not currently used. We may want to shift these strings for use when xml logging fails, so I'm not removing them -eds
595 - (void)displayErrorAndDisableLogging
597         NSRunAlertPanel(AILocalizedString(@"Unable to write log", nil),
598                                         [NSString stringWithFormat:
599                                                 AILocalizedString(@"Adium was unable to write the log file for this conversation. Please ensure you have appropriate file permissions to write to your log directory (%@) for and then re-enable logging in the General preferences.", nil), logBasePath],
600                                         AILocalizedString(@"OK", nil), nil, nil);
602         //Disable logging
603         [[adium preferenceController] setPreference:[NSNumber numberWithBool:NO]
604                                              forKey:KEY_LOGGER_ENABLE
605                                               group:PREF_GROUP_LOGGING];
609 #pragma mark Message History
611 NSCalendarDate* getDateFromPath(NSString *path)
613         NSRange openParenRange, closeParenRange;
615         if ([path hasSuffix:@".chatlog"] && (openParenRange = [path rangeOfString:@"(" options:NSBackwardsSearch]).location != NSNotFound) {
616                 openParenRange = NSMakeRange(openParenRange.location, [path length] - openParenRange.location);
617                 if ((closeParenRange = [path rangeOfString:@")" options:0 range:openParenRange]).location != NSNotFound) {
618                         //Add and subtract one to remove the parenthesis
619                         NSString *dateString = [path substringWithRange:NSMakeRange(openParenRange.location + 1, (closeParenRange.location - openParenRange.location))];
620                         NSCalendarDate *date = [NSCalendarDate calendarDateWithString:dateString timeSeparator:'.'];
621                         return date;
622                 }
623         }
624         return nil;
627 int sortPaths(NSString *path1, NSString *path2, void *context)
629         NSCalendarDate *date1 = getDateFromPath(path1);
630         NSCalendarDate *date2 = getDateFromPath(path2);
631         
632         if(!date1 && !date2)
633                 return NSOrderedSame;
634         else if (date1 && date2)
635                 return [date2 compare:date1];
636         else
637                 return date2 ? NSOrderedDescending : NSOrderedAscending;
640 + (NSArray *)sortedArrayOfLogFilesForChat:(AIChat *)chat
642         NSString *baseLogPath = [[self fullPathForLogOfChat:chat onDate:[NSDate date]] stringByDeletingLastPathComponent];
643         NSArray *files = [[NSFileManager defaultManager] directoryContentsAtPath:baseLogPath];  
644         if (files) {
645                 return [files sortedArrayUsingFunction:&sortPaths context:NULL];
646         }
647         return nil;
650 #pragma mark Upgrade code
651 - (void)upgradeLogExtensions
653         if (![[[adium preferenceController] preferenceForKey:@"Log Extensions Updated" group:PREF_GROUP_LOGGING] boolValue]) {
654                 /* This could all be a simple NSDirectoryEnumerator call on basePath, but we wouldn't be able to show progress,
655                 * and this could take a bit.
656                 */
657                 NSFileManager   *defaultManager = [NSFileManager defaultManager];
658                 NSArray                 *accountFolders = [defaultManager directoryContentsAtPath:logBasePath];
659                 NSEnumerator    *accountFolderEnumerator = [accountFolders objectEnumerator];
660                 NSString                *accountFolderName;
661                 
662                 NSMutableSet    *pathsToContactFolders = [NSMutableSet set];
663                 while ((accountFolderName = [accountFolderEnumerator nextObject])) {
664                         NSString                *contactBasePath = [logBasePath stringByAppendingPathComponent:accountFolderName];
665                         NSArray                 *contactFolders = [defaultManager directoryContentsAtPath:contactBasePath];
666                         
667                         NSEnumerator    *contactFolderEnumerator = [contactFolders objectEnumerator];
668                         NSString                *contactFolderName;
669                         
670                         while ((contactFolderName = [contactFolderEnumerator nextObject])) {
671                                 [pathsToContactFolders addObject:[contactBasePath stringByAppendingPathComponent:contactFolderName]];
672                         }
673                 }
674                 
675                 unsigned                contactsToProcess = [pathsToContactFolders count];
676                 unsigned                processed = 0;
677                 
678                 if (contactsToProcess) {
679                         AILogFileUpgradeWindowController *upgradeWindowController;
680                         
681                         upgradeWindowController = [[AILogFileUpgradeWindowController alloc] initWithWindowNibName:@"LogFileUpgrade"];
682                         [[upgradeWindowController window] makeKeyAndOrderFront:nil];
684                         NSEnumerator    *pathsToContactFoldersEnumerator = [pathsToContactFolders objectEnumerator];
685                         NSString                *pathToContactFolder;
686                         while ((pathToContactFolder = [pathsToContactFoldersEnumerator nextObject])) {
687                                 NSDirectoryEnumerator *enumerator = [defaultManager enumeratorAtPath:pathToContactFolder];
688                                 NSString        *file;
689                                 
690                                 while ((file = [enumerator nextObject])) {
691                                         if (([[file pathExtension] isEqualToString:@"html"]) ||
692                                                 ([[file pathExtension] isEqualToString:@"adiumLog"]) ||
693                                                 (([[file pathExtension] isEqualToString:@"bak"]) && ([file hasSuffix:@".html.bak"] || 
694                                                                                                                                                          [file hasSuffix:@".adiumLog.bak"]))) {
695                                                 NSString *fullFile = [pathToContactFolder stringByAppendingPathComponent:file];
696                                                 NSString *newFile = [[fullFile stringByDeletingPathExtension] stringByAppendingPathExtension:@"AdiumHTMLLog"];
697                                                 
698                                                 [defaultManager movePath:fullFile
699                                                                                   toPath:newFile
700                                                                                  handler:self];
701                                         }
702                                 }
703                                 
704                                 processed++;
705                                 [upgradeWindowController setProgress:(processed*100.0)/contactsToProcess];
706                         }
707                         
708                         [upgradeWindowController close];
709                         [upgradeWindowController release];
710                 }
711                 
712                 [[adium preferenceController] setPreference:[NSNumber numberWithBool:YES]
713                                                                                          forKey:@"Log Extensions Updated"
714                                                                                           group:PREF_GROUP_LOGGING];
715         }
718 - (BOOL)fileManager:(NSFileManager *)manager shouldProceedAfterError:(NSDictionary *)errorInfo
720         NSLog(@"Error: %@",errorInfo);
721         
722         return NO;
726  * @brief Instruct spotlight to reimport logs
728  * Adium 1.0.2 and earlier had a bug which made spotlight import not work properly.
729  * New logs are properly indexed, but previously created logs are not. On first launch of Adium 1.1,
730  * Adium will tell Spotlight to reimport those old logs.
731  */
732 - (void)reimportLogsToSpotlightIfNeeded
734         if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Adium 1.1:Reimported Spotlight Logs"]) {
735                 NSArray *arguments;
737                 arguments = [NSArray arrayWithObjects:
738                         @"-r",
739                         [[[[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Contents"] stringByAppendingPathComponent:@"Library"]
740                                 stringByAppendingPathComponent:@"Spotlight"] stringByAppendingPathComponent:[@"AdiumSpotlightImporter" stringByAppendingPathExtension:@"mdimporter"]],
741                         nil];
742                 [NSTask launchedTaskWithLaunchPath:@"/usr/bin/mdimport" arguments:arguments];
744                 [[NSUserDefaults standardUserDefaults] setBool:YES
745                                                                                                 forKey:@"Adium 1.1:Reimported Spotlight Logs"];
746         }
749 //Log Indexing ---------------------------------------------------------------------------------------------------------
750 #pragma mark Log Indexing
751 /***** Everything below this point is related to log index generation and access ****/
753 /* For the log content searching, we are required to re-index a log whenever it changes.  The solution below to
754  * this problem is along the lines of:
755  *              - Keep an array of logs that need to be re-indexed
756  *              - Whenever a log is changed, add it to this array
757  *              - When the log viewer is opened, re-index all the logs in the array
758  */
761  * @brief Initialize log indexing
762  */
763 - (void)initLogIndexing
765         //Load the list of logs that need re-indexing
766         [self loadDirtyLogArray];
770  * @brief Prepare the log index for searching.
772  * Must call before attempting to use the logSearchIndex.
773  */
774 - (void)prepareLogContentSearching
776     /* Load the index and start indexing to make it current
777          * If we're going to need to re-index all our logs from scratch, it will make
778          * things faster if we start with a fresh log index as well.
779          */
780         if (!dirtyLogArray) {
781                 [self resetLogIndex];
782         }
784         //Load the contentIndex immediately; this will clear dirtyLogArray if necessary
785         [self logContentIndex];
787         stopIndexingThreads = NO;
788         if (!dirtyLogArray) {
789                 [self dirtyAllLogs];
790         } else {
791                 [self cleanDirtyLogs];
792         }
795 //Close down and clean up the log index  (Call when finished using the logSearchIndex)
796 - (void)cleanUpLogContentSearching
798         [self stopIndexingThreads];
799         [self closeLogIndex];
802 //Returns the Search Kit index for log content searching
803 - (SKIndexRef)logContentIndex
805         SKIndexRef      returnIndex;
807         [logAccessLock lock];
808         if (!index_Content) index_Content = [self createLogIndex];
809         returnIndex = (SKIndexRef)[[(NSObject *)index_Content retain] autorelease];
810         [logAccessLock unlock];
812         return returnIndex;
815 //Mark a log as needing a re-index
816 - (void)markLogDirtyAtPath:(NSString *)path forChat:(AIChat *)chat
818         NSString    *dirtyKey = [@"LogIsDirty_" stringByAppendingString:path];
819         
820         if (![chat integerStatusObjectForKey:dirtyKey]) {
821                 //Add to dirty array (Lock to ensure that no one changes its content while we are)
822                 [dirtyLogLock lock];
823                 if (path != nil) {
824                         if (!dirtyLogArray) dirtyLogArray = [[NSMutableArray alloc] init];
826                         if (![dirtyLogArray containsObject:path]) {
827                                 [dirtyLogArray addObject:path];
828                         }
829                 }
830                 [dirtyLogLock unlock];
832                 //Save the dirty array immedientally
833                 [self _saveDirtyLogArray];
834                 
835                 //Flag the chat with 'LogIsDirty' for this filename.  On the next message we can quickly check this flag.
836                 [chat setStatusObject:[NSNumber numberWithBool:YES]
837                                            forKey:dirtyKey
838                                            notify:NotifyNever];
839         }       
842 - (void)markLogDirtyAtPath:(NSString *)path
844         if(!path) return;
845         [dirtyLogLock lock];
846         if (!dirtyLogArray) dirtyLogArray = [[NSMutableArray alloc] init];
848         if (![dirtyLogArray containsObject:path]) {
849                 [dirtyLogArray addObject:path];
850         }
851         [dirtyLogLock unlock];  
854 //Get the current status of indexing.  Returns NO if indexing is not occuring
855 - (BOOL)getIndexingProgress:(int *)indexNumber outOf:(int *)total
857         //logsIndexed + 1 is the log we are currently indexing
858         if (indexNumber) *indexNumber = (logsIndexed + 1 <= logsToIndex) ? logsIndexed + 1 : logsToIndex;
859         if (total) *total = logsToIndex;
860         return (logsToIndex > 0);
864 //Log index ------------------------------------------------------------------------------------------------------------
865 //Search kit index used to searching log content
866 #pragma mark Log Index
868  * @brief Create the log index
870  * Should be called within logAccessLock being locked
871  */
872 - (SKIndexRef)createLogIndex
874     NSString    *logIndexPath = [self _logIndexPath];
875     NSURL       *logIndexPathURL = [NSURL fileURLWithPath:logIndexPath];
876         SKIndexRef      newIndex = NULL;
878     if ([[NSFileManager defaultManager] fileExistsAtPath:logIndexPath]) {
879                 newIndex = SKIndexOpenWithURL((CFURLRef)logIndexPathURL, (CFStringRef)@"Content", true);
880                 AILog(@"Opened index %x from %@",newIndex,logIndexPathURL);
881                 
882                 if (!newIndex) {
883                         //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.
884                         NSLog(@"*** Warning: The Chat Transcript searching index at %@ was corrupt. Removing it and starting fresh; transcripts will be re-indexed automatically.",
885                                   logIndexPath);
886                         [[NSFileManager defaultManager] removeFileAtPath:logIndexPath handler:NULL];
887                 }
888     }
889     if (!newIndex) {
890                 NSDictionary *textAnalysisProperties;
891                 
892                 textAnalysisProperties = [NSDictionary dictionaryWithObjectsAndKeys:
893                         [NSNumber numberWithInt:0], kSKMaximumTerms,
894                         [NSNumber numberWithInt:2], kSKMinTermLength,
895 #if ENABLE_PROXIMITY_SEARCH
896                         kCFBooleanTrue, kSKProximityIndexing, 
897 #endif
898                         nil];
900                 //Create the index if one doesn't exist or it couldn't be opened.
901                 [[NSFileManager defaultManager] createDirectoriesForPath:[logIndexPath stringByDeletingLastPathComponent]];
903                 newIndex = SKIndexCreateWithURL((CFURLRef)logIndexPathURL,
904                                                                                 (CFStringRef)@"Content", 
905                                                                                 kSKIndexInverted,
906                                                                                 (CFDictionaryRef)textAnalysisProperties);
907                 if (newIndex) {
908                         AILog(@"Created a new log index %x at %@ with textAnalysisProperties %@",newIndex,logIndexPathURL,textAnalysisProperties);
909                         //Clear the dirty log array in case it was loaded (this can happen if the user mucks with the cache directory)
910                         [[NSFileManager defaultManager] removeFileAtPath:[self _dirtyLogArrayPath] handler:NULL];
911                         [dirtyLogArray release]; dirtyLogArray = nil;
912                 } else {
913                         AILog(@"AILoggerPlugin warning: SKIndexCreateWithURL() returned NULL");
914                 }
915     }
917         return newIndex;
920 - (void)releaseIndex:(SKIndexRef)inIndex
922     NSAutoreleasePool   *pool = [[NSAutoreleasePool alloc] init];
923         [logAccessLock lock];
924         if (inIndex) {
925                 AILog(@"**** Flushing index %p",inIndex);
926                 SKIndexFlush(inIndex);
927                 CFRelease(inIndex);
928                 AILog(@"**** Finished flushing index %p, and released it",inIndex);
929         }
930         [logAccessLock unlock];
931         [pool release];
934 //Close the log index
935 - (void)closeLogIndex
937         [logAccessLock lock];
938         if (index_Content) {
939                 [NSThread detachNewThreadSelector:@selector(releaseIndex:)
940                                                                  toTarget:self
941                                                            withObject:(id)index_Content];
942                 index_Content = nil;
943                 AILog(@"**** index_Content is now nil", nil);
944         }
945         [logAccessLock unlock];
948 //Delete the log index
949 - (void)resetLogIndex
951         if ([[NSFileManager defaultManager] fileExistsAtPath:[self _logIndexPath]]) {
952                 [[NSFileManager defaultManager] removeFileAtPath:[self _logIndexPath] handler:NULL];
953         }       
955         if ([[NSFileManager defaultManager] fileExistsAtPath:[self _dirtyLogArrayPath]]) {
956                 [[NSFileManager defaultManager] removeFileAtPath:[self _dirtyLogArrayPath] handler:NULL];
957         }
960 //Path of log index file
961 - (NSString *)_logIndexPath
963     return [[adium cachesPath] stringByAppendingPathComponent:LOG_INDEX_NAME];
967 //Dirty Log Array ------------------------------------------------------------------------------------------------------
968 //Stores the absolute paths of logs that need to be re-indexed
969 #pragma mark Dirty Log Array
970 //Load the dirty log array
971 - (void)loadDirtyLogArray
973         if (!dirtyLogArray) {
974                 int logVersion = [[[adium preferenceController] preferenceForKey:KEY_LOG_INDEX_VERSION
975                                                                                                                                    group:PREF_GROUP_LOGGING] intValue];
977                 //If the log version has changed, we reset the index and don't load the dirty array (So all the logs are marked dirty)
978                 if (logVersion >= CURRENT_LOG_VERSION) {
979                         [dirtyLogLock lock];
980                         dirtyLogArray = [[NSMutableArray alloc] initWithContentsOfFile:[self _dirtyLogArrayPath]];
981                         AILog(@"Got dirty logs %@",dirtyLogArray);
982                         [dirtyLogLock unlock];
983                 } else {
984                         AILog(@"**** Log version upgrade. Resetting");
985                         [self resetLogIndex];
986                         [[adium preferenceController] setPreference:[NSNumber numberWithInt:CURRENT_LOG_VERSION]
987                                                              forKey:KEY_LOG_INDEX_VERSION
988                                                               group:PREF_GROUP_LOGGING];
989                 }
990         }
993 //Save the dirty lod array
994 - (void)_saveDirtyLogArray
996     if (dirtyLogArray && !suspendDirtyArraySave) {
997                 [dirtyLogLock lock];
998                 [dirtyLogArray writeToFile:[self _dirtyLogArrayPath] atomically:NO];
999                 [dirtyLogLock unlock];
1000     }
1003 //Path of the dirty log array file
1004 - (NSString *)_dirtyLogArrayPath
1006     return [[adium cachesPath] stringByAppendingPathComponent:DIRTY_LOG_ARRAY_NAME];
1010 //Threaded Indexing ----------------------------------------------------------------------------------------------------
1011 #pragma mark Threaded Indexing
1012 //Stop any indexing related threads
1013 - (void)stopIndexingThreads
1015     //Let any indexing threads know it's time to stop, and wait for them to finish.
1016     stopIndexingThreads = YES;
1019 //The following methods will be run in a separate thread to avoid blocking the interface during index operations
1020 //THREAD: Flag every log as dirty (Do this when there is no log index)
1021 - (void)dirtyAllLogs
1023     //Reset and rebuild the dirty array
1024     [dirtyLogArray release]; dirtyLogArray = [[NSMutableArray alloc] init];
1025         //[self _dirtyAllLogsThread];
1026         [NSThread detachNewThreadSelector:@selector(_dirtyAllLogsThread) toTarget:self withObject:nil];
1028 - (void)_dirtyAllLogsThread
1030     NSAutoreleasePool   *pool = [[NSAutoreleasePool alloc] init];
1031     NSEnumerator                *fromEnumerator, *toEnumerator, *logEnumerator;
1032     NSString                    *fromName;
1033     AILogFromGroup              *fromGroup = nil;
1034     AILogToGroup                *toGroup;
1035     AIChatLog                   *theLog;    
1037     [indexingThreadLock lock];
1038     suspendDirtyArraySave = YES;    //Prevent saving of the dirty array until we're finished building it
1039     
1040     //Create a fresh dirty log array
1041     [dirtyLogLock lock];
1042     [dirtyLogArray release]; dirtyLogArray = [[NSMutableArray alloc] init];
1043     [dirtyLogLock unlock];
1044         
1045     //Process each from folder
1046     fromEnumerator = [[[[NSFileManager defaultManager] directoryContentsAtPath:logBasePath] objectEnumerator] retain];
1047     while ((fromName = [[fromEnumerator nextObject] retain])) {
1048                 fromGroup = [[AILogFromGroup alloc] initWithPath:fromName fromUID:fromName serviceClass:nil];
1050                 //Walk through every 'to' group
1051                 toEnumerator = [[[fromGroup toGroupArray] objectEnumerator] retain];
1052                 while ((toGroup = [[toEnumerator nextObject] retain])) {
1053                         //Walk through every log
1054                         logEnumerator = [toGroup logEnumerator];
1055                         while ((theLog = [logEnumerator nextObject])) {
1056                                 //Add this log's path to our dirty array.  The dirty array is guarded with a lock
1057                                 //since it will be accessed from outside this thread as well
1058                                 [dirtyLogLock lock];
1059                                 if (theLog != nil) {
1060                                         [dirtyLogArray addObject:[logBasePath stringByAppendingPathComponent:[theLog path]]];
1061                                 }
1062                                 [dirtyLogLock unlock];
1063                         }
1064                         
1065                         //Flush our pool
1066                         [pool release]; pool = [[NSAutoreleasePool alloc] init];
1067                         
1068                         [toGroup release];
1069                 }
1070                 [toEnumerator release];
1071                 
1072                 [fromGroup release];
1073                 [fromName release];
1074     }
1075     [fromEnumerator release];
1076         
1077         AILog(@"Finished dritying all logs");
1078         
1079     //Save the dirty array we just built
1080         [self _saveDirtyLogArray];
1081         suspendDirtyArraySave = NO; //Re-allow saving of the dirty array
1082     
1083     //Begin cleaning the logs (If the log viewer is open)
1084     if (!stopIndexingThreads && [LogViewerWindowControllerClass existingWindowController]) {
1085                 [self cleanDirtyLogs];
1086     }
1087     
1088     [indexingThreadLock unlock];
1089     [pool release];
1093  * @brief Index all dirty logs
1095  * Indexing will occur on a thread
1096  */
1097 - (void)cleanDirtyLogs
1099         //Do nothing if we're paused
1100         if (logIndexingPauses) return;
1102     //Reset the cleaning progress
1103     [dirtyLogLock lock];
1104     logsToIndex = [dirtyLogArray count];
1105     [dirtyLogLock unlock];
1106     logsIndexed = 0;
1107         AILog(@"cleanDirtyLogs: logsToIndex is %i",logsToIndex);
1108         if (logsToIndex > 0) {
1109                 [NSThread detachNewThreadSelector:@selector(_cleanDirtyLogsThread:) toTarget:self withObject:(id)[self logContentIndex]];
1110         }
1113 - (void)didCleanDirtyLogs
1115         //Update our progress
1116         logsToIndex = 0;
1118         [[LogViewerWindowControllerClass existingWindowController] logIndexingProgressUpdate];
1120         //Clear the dirty status of all open chats so they will be marked dirty if they receive another message
1121         NSEnumerator *enumerator = [[[adium chatController] openChats] objectEnumerator];
1122         AIChat           *chat;
1124         while ((chat = [enumerator nextObject])) {
1125                 NSString *existingAppenderPath = [[self existingAppenderForChat:chat] path];
1126                 if (existingAppenderPath) {
1127                         NSString *dirtyKey = [@"LogIsDirty_" stringByAppendingString:existingAppenderPath];
1129                         if ([chat integerStatusObjectForKey:dirtyKey]) {
1130                                 [chat setStatusObject:nil
1131                                                            forKey:dirtyKey
1132                                                            notify:NotifyNever];
1133                         }
1134                 }
1135         }
1138 - (void)pauseIndexing
1140         if (logsToIndex) {
1141                 [self stopIndexingThreads];
1142                 logsToIndex = 0;
1143                 logIndexingPauses++;
1144                 AILog(@"Pausing %i",logIndexingPauses);
1145         }
1148 - (void)resumeIndexing
1150         if (logIndexingPauses)
1151                 logIndexingPauses--;
1152         AILog(@"Told to resume; log indexing paauses is now %i",logIndexingPauses);
1153         if (logIndexingPauses == 0) {
1154                 stopIndexingThreads = NO;
1155                 [self cleanDirtyLogs];
1156         }
1159 - (void)_cleanDirtyLogsThread:(SKIndexRef)searchIndex
1161     NSAutoreleasePool   *pool = [[NSAutoreleasePool alloc] init];
1163         //Ensure log indexing (in an old thread) isn't already going on and just waiting to stop
1164         [indexingThreadLock lock]; [indexingThreadLock unlock];
1166         //If log indexing was already in progress, we can just cancel since it is now complete
1167         if (logsToIndex == 0) {
1168                 AILog(@"Nothing to clean!");
1169                 [self performSelectorOnMainThread:@selector(didCleanDirtyLogs)
1170                                                            withObject:nil
1171                                                         waitUntilDone:NO];
1172                 [pool release];
1173                 return;
1174         }
1176         if (!searchIndex) {
1177                 NSLog(@"*** Warning: Called -[%@ _cleanDirtyLogsThread:] with a NULL searchIndex. That shouldn't happen!", self);
1178                 [pool release];
1179                 return;
1180         }
1182     [indexingThreadLock lock];
1183         
1184         CFRetain(searchIndex);
1186     //Start cleaning (If we're still supposed to go)
1187     if (!stopIndexingThreads) {
1188                 UInt32  lastUpdate = TickCount();
1189                 int             unsavedChanges = 0;
1191                 AILog(@"Cleaning %i dirty logs", [dirtyLogArray count]);
1193                 //Scan until we're done or told to stop
1194                 while (!stopIndexingThreads) {
1195                         NSString        *logPath = nil;
1196                         
1197                         //Get the next dirty log
1198                         [dirtyLogLock lock];
1199                         if ([dirtyLogArray count]) {
1200                                 logPath = [[[dirtyLogArray lastObject] retain] autorelease]; //retain to prevent deallocation when removing from the array
1201                                 [dirtyLogArray removeLastObject];
1202                         }
1203                         [dirtyLogLock unlock];
1205                         if (logPath) {
1206                                 SKDocumentRef   document;
1207                                 
1208                                 document = SKDocumentCreateWithURL((CFURLRef)[NSURL fileURLWithPath:logPath]);
1209                                 if (document) {
1210                                         /* We _could_ use SKIndexAddDocument() and depend on our Spotlight plugin for importing.
1211                                          * However, this has three problems:
1212                                          *      1. Slower, especially to start initial indexing, which is the most common use case since the log viewer
1213                                          *         indexes recently-modified ("dirty") logs when it opens.
1214                                          *  2. Sometimes logs don't appear to be associated with the right URI type and therefore don't get indexed.
1215                                          *  3. On 10.3, this means that logs' markup is indexed in addition to their text, which is undesireable.
1216                                          */
1217                                         CFStringRef documentText = CopyTextContentForFile(NULL, (CFStringRef)logPath);
1218                                         if (documentText) {
1219                                                 SKIndexAddDocumentWithText(searchIndex,
1220                                                                                                    document,
1221                                                                                                    documentText,
1222                                                                                                    YES);
1223                                                 CFRelease(documentText);
1224                                         }
1225                                         CFRelease(document);
1226                                 } else {
1227                                         NSLog(@"Could not create document for %@ [%@]",logPath,[NSURL fileURLWithPath:logPath]);
1228                                 }
1229                                 
1230                                 //Update our progress
1231                                 logsIndexed++;
1232                                 if (lastUpdate == 0 || TickCount() > lastUpdate + LOG_INDEX_STATUS_INTERVAL) {
1233                                         [[LogViewerWindowControllerClass existingWindowController]
1234                                             performSelectorOnMainThread:@selector(logIndexingProgressUpdate) 
1235                                                              withObject:nil
1236                                                           waitUntilDone:NO];
1237                                         lastUpdate = TickCount();
1238                                 }
1239                                 
1240                                 //Save the dirty array
1241                                 if (unsavedChanges++ > LOG_CLEAN_SAVE_INTERVAL) {
1242                                         [self _saveDirtyLogArray];
1244                                         unsavedChanges = 0;
1246                                         //Flush ram
1247                                         [pool release]; pool = [[NSAutoreleasePool alloc] init];
1248                                 }
1249                                 
1250                         } else {
1251                                 break; //Exit when we run out of logs
1252                         }
1253                 }
1254                 
1255                 //Save the slimmed down dirty log array
1256                 if (unsavedChanges) {
1257                         [self _saveDirtyLogArray];
1258                 }
1260                 [logAccessLock lock];
1261                 SKIndexFlush(searchIndex);
1262                 AILog(@"After cleaning dirty logs, the search index has a max ID of %i and a count of %i",
1263                           SKIndexGetMaximumDocumentID(searchIndex),
1264                           SKIndexGetDocumentCount(searchIndex));
1265                 [logAccessLock unlock];
1266                 
1267                 
1268                 [self performSelectorOnMainThread:@selector(didCleanDirtyLogs)
1269                                                            withObject:nil
1270                                                         waitUntilDone:NO];
1271     }
1273         CFRelease(searchIndex);
1275         [indexingThreadLock unlock];
1277     [pool release];
1280 - (NSLock *)logAccessLock
1282         return logAccessLock;
1285 - (void)_removePathsFromIndexThread:(NSDictionary *)userInfo
1287         NSAutoreleasePool   *pool = [[NSAutoreleasePool alloc] init];
1289         [indexingThreadLock lock];
1291         SKIndexRef logSearchIndex = (SKIndexRef)[userInfo objectForKey:@"SKIndexRef"];
1292         NSEnumerator *enumerator = [[userInfo objectForKey:@"Paths"] objectEnumerator];
1293         NSString         *logPath;
1294         
1295         while ((logPath = [enumerator nextObject])) {
1296                 SKDocumentRef document = SKDocumentCreateWithURL((CFURLRef)[NSURL fileURLWithPath:logPath]);
1297                 if (document) {
1298                         SKIndexRemoveDocument(logSearchIndex, document);
1299                         CFRelease(document);
1300                 }
1301         }
1303         [indexingThreadLock unlock];
1305         [pool release];
1308 - (void)removePathsFromIndex:(NSSet *)paths
1310         [NSThread detachNewThreadSelector:@selector(_removePathsFromIndexThread:)
1311                                                          toTarget:self
1312                                                    withObject:[NSDictionary dictionaryWithObjectsAndKeys:
1313                                                            (id)[self logContentIndex], @"SKIndexRef",
1314                                                            paths, @"Paths",
1315                                                            nil]];
1319 @end