Updating svn:mergeinfo
[adiumx.git] / Source / AILoggerPlugin.m
blob020a561472362b17372c5811f0bf229f130647cc
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 participatingListObjects] 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 opject
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]);
525         /*
526         NSLog(@"Chat: %@",chat);
527         NSLog(@"service ID is %@",[account serviceID]);
528         NSLog(@"Account UID is %@",[account UID]);
529         NSLog(@"chat ID is %@",chatID);
530         */
532         return [NSString stringWithFormat:@"%@.%@-%@", [account serviceID], [account UID], chatID];
535 - (AIXMLAppender *)existingAppenderForChat:(AIChat *)chat
537         //Look up the key for this chat and use it to try to retrieve the appender
538         return [activeAppenders objectForKey:[self keyForChat:chat]];   
541 - (AIXMLAppender *)appenderForChat:(AIChat *)chat
543         //Check if there is already an appender for this chat
544         AIXMLAppender   *appender = [self existingAppenderForChat:chat];
546         if (appender) {
547                 //Ensure a timeout isn't set for closing the appender, since we're now using it
548                 [NSObject cancelPreviousPerformRequestsWithTarget:self
549                                                                                                  selector:@selector(finishClosingAppender:) 
550                                                                                                    object:[self keyForChat:chat]];
551         } else {
552                 //If there isn't already an appender, create a new one and add it to the dictionary
553                 NSDate                  *chatDate = [chat dateOpened];
554                 NSString                *fullPath = [AILoggerPlugin fullPathForLogOfChat:chat onDate:chatDate];
556                 appender = [AIXMLAppender documentWithPath:fullPath];
557                 [appender initializeDocumentWithRootElementName:@"chat"
558                                                                                   attributeKeys:[NSArray arrayWithObjects:@"xmlns", @"account", @"service", nil]
559                                                                                 attributeValues:[NSArray arrayWithObjects:
560                                                                                         XML_LOGGING_NAMESPACE,
561                                                                                         [[chat account] UID],
562                                                                                         [[chat account] serviceID],
563                                                                                         nil]];
564                 
565                 //Add the window opened event now
566                 [appender addElementWithName:@"event"
567                                          content:nil
568                            attributeKeys:[NSArray arrayWithObjects:@"type", @"sender", @"time", nil]
569                          attributeValues:[NSArray arrayWithObjects:@"windowOpened", [[chat account] UID], [[chatDate dateWithCalendarFormat:nil timeZone:nil] ISO8601DateString], nil]];
571                 [activeAppenders setObject:appender forKey:[self keyForChat:chat]];
572                 
573                 [self markLogDirtyAtPath:[appender path] forChat:chat];
574         }
575         
576         return appender;
579 - (void)closeAppenderForChat:(AIChat *)chat
581         //Create a new timer to fire after the timeout period, which will close the appender
582         NSString *chatKey = [self keyForChat:chat];
583         [NSObject cancelPreviousPerformRequestsWithTarget:self
584                                                                                          selector:@selector(finishClosingAppender:) 
585                                                                                            object:chatKey];
586         [self performSelector:@selector(finishClosingAppender:) 
587                            withObject:chatKey
588                            afterDelay:NEW_LOGFILE_TIMEOUT];
591 - (void)finishClosingAppender:(NSString *)chatKey
593         //Remove the appender, closing its file descriptor upon dealloc
594         [activeAppenders removeObjectForKey:chatKey];
598 //Display a warning to the user that logging failed, and disable logging to prevent additional warnings
599 //XXX not currently used. We may want to shift these strings for use when xml logging fails, so I'm not removing them -eds
601 - (void)displayErrorAndDisableLogging
603         NSRunAlertPanel(AILocalizedString(@"Unable to write log", nil),
604                                         [NSString stringWithFormat:
605                                                 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],
606                                         AILocalizedString(@"OK", nil), nil, nil);
608         //Disable logging
609         [[adium preferenceController] setPreference:[NSNumber numberWithBool:NO]
610                                              forKey:KEY_LOGGER_ENABLE
611                                               group:PREF_GROUP_LOGGING];
615 #pragma mark Message History
617 NSCalendarDate* getDateFromPath(NSString *path)
619         NSRange openParenRange, closeParenRange;
621         if ([path hasSuffix:@".chatlog"] && (openParenRange = [path rangeOfString:@"(" options:NSBackwardsSearch]).location != NSNotFound) {
622                 openParenRange = NSMakeRange(openParenRange.location, [path length] - openParenRange.location);
623                 if ((closeParenRange = [path rangeOfString:@")" options:0 range:openParenRange]).location != NSNotFound) {
624                         //Add and subtract one to remove the parenthesis
625                         NSString *dateString = [path substringWithRange:NSMakeRange(openParenRange.location + 1, (closeParenRange.location - openParenRange.location))];
626                         NSCalendarDate *date = [NSCalendarDate calendarDateWithString:dateString timeSeparator:'.'];
627                         return date;
628                 }
629         }
630         return nil;
633 int sortPaths(NSString *path1, NSString *path2, void *context)
635         NSCalendarDate *date1 = getDateFromPath(path1);
636         NSCalendarDate *date2 = getDateFromPath(path2);
637         
638         if(!date1 && !date2)
639                 return NSOrderedSame;
640         else if (date1 && date2)
641                 return [date2 compare:date1];
642         else
643                 return date2 ? NSOrderedDescending : NSOrderedAscending;
646 + (NSArray *)sortedArrayOfLogFilesForChat:(AIChat *)chat
648         NSString *baseLogPath = [[self fullPathForLogOfChat:chat onDate:[NSDate date]] stringByDeletingLastPathComponent];
649         NSArray *files = [[NSFileManager defaultManager] directoryContentsAtPath:baseLogPath];  
650         if (files) {
651                 return [files sortedArrayUsingFunction:&sortPaths context:NULL];
652         }
653         return nil;
656 #pragma mark Upgrade code
657 - (void)upgradeLogExtensions
659         if (![[[adium preferenceController] preferenceForKey:@"Log Extensions Updated" group:PREF_GROUP_LOGGING] boolValue]) {
660                 /* This could all be a simple NSDirectoryEnumerator call on basePath, but we wouldn't be able to show progress,
661                 * and this could take a bit.
662                 */
663                 NSFileManager   *defaultManager = [NSFileManager defaultManager];
664                 NSArray                 *accountFolders = [defaultManager directoryContentsAtPath:logBasePath];
665                 NSEnumerator    *accountFolderEnumerator = [accountFolders objectEnumerator];
666                 NSString                *accountFolderName;
667                 
668                 NSMutableSet    *pathsToContactFolders = [NSMutableSet set];
669                 while ((accountFolderName = [accountFolderEnumerator nextObject])) {
670                         NSString                *contactBasePath = [logBasePath stringByAppendingPathComponent:accountFolderName];
671                         NSArray                 *contactFolders = [defaultManager directoryContentsAtPath:contactBasePath];
672                         
673                         NSEnumerator    *contactFolderEnumerator = [contactFolders objectEnumerator];
674                         NSString                *contactFolderName;
675                         
676                         while ((contactFolderName = [contactFolderEnumerator nextObject])) {
677                                 [pathsToContactFolders addObject:[contactBasePath stringByAppendingPathComponent:contactFolderName]];
678                         }
679                 }
680                 
681                 unsigned                contactsToProcess = [pathsToContactFolders count];
682                 unsigned                processed = 0;
683                 
684                 if (contactsToProcess) {
685                         AILogFileUpgradeWindowController *upgradeWindowController;
686                         
687                         upgradeWindowController = [[AILogFileUpgradeWindowController alloc] initWithWindowNibName:@"LogFileUpgrade"];
688                         [[upgradeWindowController window] makeKeyAndOrderFront:nil];
690                         NSEnumerator    *pathsToContactFoldersEnumerator = [pathsToContactFolders objectEnumerator];
691                         NSString                *pathToContactFolder;
692                         while ((pathToContactFolder = [pathsToContactFoldersEnumerator nextObject])) {
693                                 NSDirectoryEnumerator *enumerator = [defaultManager enumeratorAtPath:pathToContactFolder];
694                                 NSString        *file;
695                                 
696                                 while ((file = [enumerator nextObject])) {
697                                         if (([[file pathExtension] isEqualToString:@"html"]) ||
698                                                 ([[file pathExtension] isEqualToString:@"adiumLog"]) ||
699                                                 (([[file pathExtension] isEqualToString:@"bak"]) && ([file hasSuffix:@".html.bak"] || 
700                                                                                                                                                          [file hasSuffix:@".adiumLog.bak"]))) {
701                                                 NSString *fullFile = [pathToContactFolder stringByAppendingPathComponent:file];
702                                                 NSString *newFile = [[fullFile stringByDeletingPathExtension] stringByAppendingPathExtension:@"AdiumHTMLLog"];
703                                                 
704                                                 [defaultManager movePath:fullFile
705                                                                                   toPath:newFile
706                                                                                  handler:self];
707                                         }
708                                 }
709                                 
710                                 processed++;
711                                 [upgradeWindowController setProgress:(processed*100.0)/contactsToProcess];
712                         }
713                         
714                         [upgradeWindowController close];
715                         [upgradeWindowController release];
716                 }
717                 
718                 [[adium preferenceController] setPreference:[NSNumber numberWithBool:YES]
719                                                                                          forKey:@"Log Extensions Updated"
720                                                                                           group:PREF_GROUP_LOGGING];
721         }
724 - (BOOL)fileManager:(NSFileManager *)manager shouldProceedAfterError:(NSDictionary *)errorInfo
726         NSLog(@"Error: %@",errorInfo);
727         
728         return NO;
732  * @brief Instruct spotlight to reimport logs
734  * Adium 1.0.2 and earlier had a bug which made spotlight import not work properly.
735  * New logs are properly indexed, but previously created logs are not. On first launch of Adium 1.1,
736  * Adium will tell Spotlight to reimport those old logs.
737  */
738 - (void)reimportLogsToSpotlightIfNeeded
740         if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Adium 1.1:Reimported Spotlight Logs"]) {
741                 NSArray *arguments;
743                 arguments = [NSArray arrayWithObjects:
744                         @"-r",
745                         [[[[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Contents"] stringByAppendingPathComponent:@"Library"]
746                                 stringByAppendingPathComponent:@"Spotlight"] stringByAppendingPathComponent:[@"AdiumSpotlightImporter" stringByAppendingPathExtension:@"mdimporter"]],
747                         nil];
748                 [NSTask launchedTaskWithLaunchPath:@"/usr/bin/mdimport" arguments:arguments];
750                 [[NSUserDefaults standardUserDefaults] setBool:YES
751                                                                                                 forKey:@"Adium 1.1:Reimported Spotlight Logs"];
752         }
755 //Log Indexing ---------------------------------------------------------------------------------------------------------
756 #pragma mark Log Indexing
757 /***** Everything below this point is related to log index generation and access ****/
759 /* For the log content searching, we are required to re-index a log whenever it changes.  The solution below to
760  * this problem is along the lines of:
761  *              - Keep an array of logs that need to be re-indexed
762  *              - Whenever a log is changed, add it to this array
763  *              - When the log viewer is opened, re-index all the logs in the array
764  */
767  * @brief Initialize log indexing
768  */
769 - (void)initLogIndexing
771         //Load the list of logs that need re-indexing
772         [self loadDirtyLogArray];
776  * @brief Prepare the log index for searching.
778  * Must call before attempting to use the logSearchIndex.
779  */
780 - (void)prepareLogContentSearching
782     /* Load the index and start indexing to make it current
783          * If we're going to need to re-index all our logs from scratch, it will make
784          * things faster if we start with a fresh log index as well.
785          */
786         if (!dirtyLogArray) {
787                 [self resetLogIndex];
788         }
790         //Load the contentIndex immediately; this will clear dirtyLogArray if necessary
791         [self logContentIndex];
793         stopIndexingThreads = NO;
794         if (!dirtyLogArray) {
795                 [self dirtyAllLogs];
796         } else {
797                 [self cleanDirtyLogs];
798         }
801 //Close down and clean up the log index  (Call when finished using the logSearchIndex)
802 - (void)cleanUpLogContentSearching
804         [self stopIndexingThreads];
805         [self closeLogIndex];
808 //Returns the Search Kit index for log content searching
809 - (SKIndexRef)logContentIndex
811         SKIndexRef      returnIndex;
813         [logAccessLock lock];
814         if (!index_Content) index_Content = [self createLogIndex];
815         returnIndex = (SKIndexRef)[[(NSObject *)index_Content retain] autorelease];
816         [logAccessLock unlock];
818         return returnIndex;
821 //Mark a log as needing a re-index
822 - (void)markLogDirtyAtPath:(NSString *)path forChat:(AIChat *)chat
824         NSString    *dirtyKey = [@"LogIsDirty_" stringByAppendingString:path];
825         
826         if (![chat integerStatusObjectForKey:dirtyKey]) {
827                 //Add to dirty array (Lock to ensure that no one changes its content while we are)
828                 [dirtyLogLock lock];
829                 if (path != nil) {
830                         if (!dirtyLogArray) dirtyLogArray = [[NSMutableArray alloc] init];
832                         if (![dirtyLogArray containsObject:path]) {
833                                 [dirtyLogArray addObject:path];
834                         }
835                 }
836                 [dirtyLogLock unlock];
838                 //Save the dirty array immedientally
839                 [self _saveDirtyLogArray];
840                 
841                 //Flag the chat with 'LogIsDirty' for this filename.  On the next message we can quickly check this flag.
842                 [chat setStatusObject:[NSNumber numberWithBool:YES]
843                                            forKey:dirtyKey
844                                            notify:NotifyNever];
845         }       
848 - (void)markLogDirtyAtPath:(NSString *)path
850         if(!path) return;
851         [dirtyLogLock lock];
852         if (!dirtyLogArray) dirtyLogArray = [[NSMutableArray alloc] init];
854         if (![dirtyLogArray containsObject:path]) {
855                 [dirtyLogArray addObject:path];
856         }
857         [dirtyLogLock unlock];  
860 //Get the current status of indexing.  Returns NO if indexing is not occuring
861 - (BOOL)getIndexingProgress:(int *)indexNumber outOf:(int *)total
863         //logsIndexed + 1 is the log we are currently indexing
864         if (indexNumber) *indexNumber = (logsIndexed + 1 <= logsToIndex) ? logsIndexed + 1 : logsToIndex;
865         if (total) *total = logsToIndex;
866         return (logsToIndex > 0);
870 //Log index ------------------------------------------------------------------------------------------------------------
871 //Search kit index used to searching log content
872 #pragma mark Log Index
874  * @brief Create the log index
876  * Should be called within logAccessLock being locked
877  */
878 - (SKIndexRef)createLogIndex
880     NSString    *logIndexPath = [self _logIndexPath];
881     NSURL       *logIndexPathURL = [NSURL fileURLWithPath:logIndexPath];
882         SKIndexRef      newIndex = NULL;
884     if ([[NSFileManager defaultManager] fileExistsAtPath:logIndexPath]) {
885                 newIndex = SKIndexOpenWithURL((CFURLRef)logIndexPathURL, (CFStringRef)@"Content", true);
886                 AILog(@"Opened index %x from %@",newIndex,logIndexPathURL);
887                 
888                 if (!newIndex) {
889                         //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.
890                         NSLog(@"*** Warning: The Chat Transcript searching index at %@ was corrupt. Removing it and starting fresh; transcripts will be re-indexed automatically.",
891                                   logIndexPath);
892                         [[NSFileManager defaultManager] removeFileAtPath:logIndexPath handler:NULL];
893                 }
894     }
895     if (!newIndex) {
896                 NSDictionary *textAnalysisProperties;
897                 
898                 textAnalysisProperties = [NSDictionary dictionaryWithObjectsAndKeys:
899                         [NSNumber numberWithInt:0], kSKMaximumTerms,
900                         [NSNumber numberWithInt:2], kSKMinTermLength,
901 #if ENABLE_PROXIMITY_SEARCH
902                         kCFBooleanTrue, kSKProximityIndexing, 
903 #endif
904                         nil];
906                 //Create the index if one doesn't exist or it couldn't be opened.
907                 [[NSFileManager defaultManager] createDirectoriesForPath:[logIndexPath stringByDeletingLastPathComponent]];
909                 newIndex = SKIndexCreateWithURL((CFURLRef)logIndexPathURL,
910                                                                                 (CFStringRef)@"Content", 
911                                                                                 kSKIndexInverted,
912                                                                                 (CFDictionaryRef)textAnalysisProperties);
913                 if (newIndex) {
914                         AILog(@"Created a new log index %x at %@ with textAnalysisProperties %@",newIndex,logIndexPathURL,textAnalysisProperties);
915                         //Clear the dirty log array in case it was loaded (this can happen if the user mucks with the cache directory)
916                         [[NSFileManager defaultManager] removeFileAtPath:[self _dirtyLogArrayPath] handler:NULL];
917                         [dirtyLogArray release]; dirtyLogArray = nil;
918                 } else {
919                         AILog(@"AILoggerPlugin warning: SKIndexCreateWithURL() returned NULL");
920                 }
921     }
923         return newIndex;
926 - (void)releaseIndex:(SKIndexRef)inIndex
928     NSAutoreleasePool   *pool = [[NSAutoreleasePool alloc] init];
929         [logAccessLock lock];
930         if (inIndex) {
931                 AILog(@"**** Flushing index %p",inIndex);
932                 SKIndexFlush(inIndex);
933                 CFRelease(inIndex);
934                 AILog(@"**** Finished flushing index %p, and released it",inIndex);
935         }
936         [logAccessLock unlock];
937         [pool release];
940 //Close the log index
941 - (void)closeLogIndex
943         [logAccessLock lock];
944         if (index_Content) {
945                 [NSThread detachNewThreadSelector:@selector(releaseIndex:)
946                                                                  toTarget:self
947                                                            withObject:(id)index_Content];
948                 index_Content = nil;
949                 AILog(@"**** index_Content is now nil", nil);
950         }
951         [logAccessLock unlock];
954 //Delete the log index
955 - (void)resetLogIndex
957         if ([[NSFileManager defaultManager] fileExistsAtPath:[self _logIndexPath]]) {
958                 [[NSFileManager defaultManager] removeFileAtPath:[self _logIndexPath] handler:NULL];
959         }       
961         if ([[NSFileManager defaultManager] fileExistsAtPath:[self _dirtyLogArrayPath]]) {
962                 [[NSFileManager defaultManager] removeFileAtPath:[self _dirtyLogArrayPath] handler:NULL];
963         }
966 //Path of log index file
967 - (NSString *)_logIndexPath
969     return [[adium cachesPath] stringByAppendingPathComponent:LOG_INDEX_NAME];
973 //Dirty Log Array ------------------------------------------------------------------------------------------------------
974 //Stores the absolute paths of logs that need to be re-indexed
975 #pragma mark Dirty Log Array
976 //Load the dirty log array
977 - (void)loadDirtyLogArray
979         if (!dirtyLogArray) {
980                 int logVersion = [[[adium preferenceController] preferenceForKey:KEY_LOG_INDEX_VERSION
981                                                                                                                                    group:PREF_GROUP_LOGGING] intValue];
983                 //If the log version has changed, we reset the index and don't load the dirty array (So all the logs are marked dirty)
984                 if (logVersion >= CURRENT_LOG_VERSION) {
985                         [dirtyLogLock lock];
986                         dirtyLogArray = [[NSMutableArray alloc] initWithContentsOfFile:[self _dirtyLogArrayPath]];
987                         AILog(@"Got dirty logs %@",dirtyLogArray);
988                         [dirtyLogLock unlock];
989                 } else {
990                         AILog(@"**** Log version upgrade. Resetting");
991                         [self resetLogIndex];
992                         [[adium preferenceController] setPreference:[NSNumber numberWithInt:CURRENT_LOG_VERSION]
993                                                              forKey:KEY_LOG_INDEX_VERSION
994                                                               group:PREF_GROUP_LOGGING];
995                 }
996         }
999 //Save the dirty lod array
1000 - (void)_saveDirtyLogArray
1002     if (dirtyLogArray && !suspendDirtyArraySave) {
1003                 [dirtyLogLock lock];
1004                 [dirtyLogArray writeToFile:[self _dirtyLogArrayPath] atomically:NO];
1005                 [dirtyLogLock unlock];
1006     }
1009 //Path of the dirty log array file
1010 - (NSString *)_dirtyLogArrayPath
1012     return [[adium cachesPath] stringByAppendingPathComponent:DIRTY_LOG_ARRAY_NAME];
1016 //Threaded Indexing ----------------------------------------------------------------------------------------------------
1017 #pragma mark Threaded Indexing
1018 //Stop any indexing related threads
1019 - (void)stopIndexingThreads
1021     //Let any indexing threads know it's time to stop, and wait for them to finish.
1022     stopIndexingThreads = YES;
1025 //The following methods will be run in a separate thread to avoid blocking the interface during index operations
1026 //THREAD: Flag every log as dirty (Do this when there is no log index)
1027 - (void)dirtyAllLogs
1029     //Reset and rebuild the dirty array
1030     [dirtyLogArray release]; dirtyLogArray = [[NSMutableArray alloc] init];
1031         //[self _dirtyAllLogsThread];
1032         [NSThread detachNewThreadSelector:@selector(_dirtyAllLogsThread) toTarget:self withObject:nil];
1034 - (void)_dirtyAllLogsThread
1036     NSAutoreleasePool   *pool = [[NSAutoreleasePool alloc] init];
1037     NSEnumerator                *fromEnumerator, *toEnumerator, *logEnumerator;
1038     NSString                    *fromName;
1039     AILogFromGroup              *fromGroup = nil;
1040     AILogToGroup                *toGroup;
1041     AIChatLog                   *theLog;    
1043     [indexingThreadLock lock];
1044     suspendDirtyArraySave = YES;    //Prevent saving of the dirty array until we're finished building it
1045     
1046     //Create a fresh dirty log array
1047     [dirtyLogLock lock];
1048     [dirtyLogArray release]; dirtyLogArray = [[NSMutableArray alloc] init];
1049     [dirtyLogLock unlock];
1050         
1051     //Process each from folder
1052     fromEnumerator = [[[[NSFileManager defaultManager] directoryContentsAtPath:logBasePath] objectEnumerator] retain];
1053     while ((fromName = [[fromEnumerator nextObject] retain])) {
1054                 fromGroup = [[AILogFromGroup alloc] initWithPath:fromName fromUID:fromName serviceClass:nil];
1056                 //Walk through every 'to' group
1057                 toEnumerator = [[[fromGroup toGroupArray] objectEnumerator] retain];
1058                 while ((toGroup = [[toEnumerator nextObject] retain])) {
1059                         //Walk through every log
1060                         logEnumerator = [toGroup logEnumerator];
1061                         while ((theLog = [logEnumerator nextObject])) {
1062                                 //Add this log's path to our dirty array.  The dirty array is guarded with a lock
1063                                 //since it will be accessed from outside this thread as well
1064                                 [dirtyLogLock lock];
1065                                 if (theLog != nil) {
1066                                         [dirtyLogArray addObject:[logBasePath stringByAppendingPathComponent:[theLog path]]];
1067                                 }
1068                                 [dirtyLogLock unlock];
1069                         }
1070                         
1071                         //Flush our pool
1072                         [pool release]; pool = [[NSAutoreleasePool alloc] init];
1073                         
1074                         [toGroup release];
1075                 }
1076                 [toEnumerator release];
1077                 
1078                 [fromGroup release];
1079                 [fromName release];
1080     }
1081     [fromEnumerator release];
1082         
1083         AILog(@"Finished dritying all logs");
1084         
1085     //Save the dirty array we just built
1086         [self _saveDirtyLogArray];
1087         suspendDirtyArraySave = NO; //Re-allow saving of the dirty array
1088     
1089     //Begin cleaning the logs (If the log viewer is open)
1090     if (!stopIndexingThreads && [LogViewerWindowControllerClass existingWindowController]) {
1091                 [self cleanDirtyLogs];
1092     }
1093     
1094     [indexingThreadLock unlock];
1095     [pool release];
1099  * @brief Index all dirty logs
1101  * Indexing will occur on a thread
1102  */
1103 - (void)cleanDirtyLogs
1105         //Do nothing if we're paused
1106         if (logIndexingPauses) return;
1108     //Reset the cleaning progress
1109     [dirtyLogLock lock];
1110     logsToIndex = [dirtyLogArray count];
1111     [dirtyLogLock unlock];
1112     logsIndexed = 0;
1113         AILog(@"cleanDirtyLogs: logsToIndex is %i",logsToIndex);
1114         if (logsToIndex > 0) {
1115                 [NSThread detachNewThreadSelector:@selector(_cleanDirtyLogsThread:) toTarget:self withObject:(id)[self logContentIndex]];
1116         }
1119 - (void)didCleanDirtyLogs
1121         //Update our progress
1122         logsToIndex = 0;
1124         [[LogViewerWindowControllerClass existingWindowController] logIndexingProgressUpdate];
1126         //Clear the dirty status of all open chats so they will be marked dirty if they receive another message
1127         NSEnumerator *enumerator = [[[adium chatController] openChats] objectEnumerator];
1128         AIChat           *chat;
1130         while ((chat = [enumerator nextObject])) {
1131                 NSString *existingAppenderPath = [[self existingAppenderForChat:chat] path];
1132                 if (existingAppenderPath) {
1133                         NSString *dirtyKey = [@"LogIsDirty_" stringByAppendingString:existingAppenderPath];
1135                         if ([chat integerStatusObjectForKey:dirtyKey]) {
1136                                 [chat setStatusObject:nil
1137                                                            forKey:dirtyKey
1138                                                            notify:NotifyNever];
1139                         }
1140                 }
1141         }
1144 - (void)pauseIndexing
1146         if (logsToIndex) {
1147                 [self stopIndexingThreads];
1148                 logsToIndex = 0;
1149                 logIndexingPauses++;
1150                 AILog(@"Pausing %i",logIndexingPauses);
1151         }
1154 - (void)resumeIndexing
1156         if (logIndexingPauses)
1157                 logIndexingPauses--;
1158         AILog(@"Told to resume; log indexing paauses is now %i",logIndexingPauses);
1159         if (logIndexingPauses == 0) {
1160                 stopIndexingThreads = NO;
1161                 [self cleanDirtyLogs];
1162         }
1165 - (void)_cleanDirtyLogsThread:(SKIndexRef)searchIndex
1167     NSAutoreleasePool   *pool = [[NSAutoreleasePool alloc] init];
1169         //Ensure log indexing (in an old thread) isn't already going on and just waiting to stop
1170         [indexingThreadLock lock]; [indexingThreadLock unlock];
1172         //If log indexing was already in progress, we can just cancel since it is now complete
1173         if (logsToIndex == 0) {
1174                 AILog(@"Nothing to clean!");
1175                 [self performSelectorOnMainThread:@selector(didCleanDirtyLogs)
1176                                                            withObject:nil
1177                                                         waitUntilDone:NO];
1178                 [pool release];
1179                 return;
1180         }
1182         if (!searchIndex) {
1183                 NSLog(@"*** Warning: Called -[%@ _cleanDirtyLogsThread:] with a NULL searchIndex. That shouldn't happen!", self);
1184                 [pool release];
1185                 return;
1186         }
1188     [indexingThreadLock lock];
1189         
1190         CFRetain(searchIndex);
1192     //Start cleaning (If we're still supposed to go)
1193     if (!stopIndexingThreads) {
1194                 UInt32  lastUpdate = TickCount();
1195                 int             unsavedChanges = 0;
1197                 AILog(@"Cleaning %i dirty logs", [dirtyLogArray count]);
1199                 //Scan until we're done or told to stop
1200                 while (!stopIndexingThreads) {
1201                         NSString        *logPath = nil;
1202                         
1203                         //Get the next dirty log
1204                         [dirtyLogLock lock];
1205                         if ([dirtyLogArray count]) {
1206                                 logPath = [[[dirtyLogArray lastObject] retain] autorelease]; //retain to prevent deallocation when removing from the array
1207                                 [dirtyLogArray removeLastObject];
1208                         }
1209                         [dirtyLogLock unlock];
1211                         if (logPath) {
1212                                 SKDocumentRef   document;
1213                                 
1214                                 document = SKDocumentCreateWithURL((CFURLRef)[NSURL fileURLWithPath:logPath]);
1215                                 if (document) {
1216                                         /* We _could_ use SKIndexAddDocument() and depend on our Spotlight plugin for importing.
1217                                          * However, this has three problems:
1218                                          *      1. Slower, especially to start initial indexing, which is the most common use case since the log viewer
1219                                          *         indexes recently-modified ("dirty") logs when it opens.
1220                                          *  2. Sometimes logs don't appear to be associated with the right URI type and therefore don't get indexed.
1221                                          *  3. On 10.3, this means that logs' markup is indexed in addition to their text, which is undesireable.
1222                                          */
1223                                         CFStringRef documentText = CopyTextContentForFile(NULL, (CFStringRef)logPath);
1224                                         if (documentText) {
1225                                                 SKIndexAddDocumentWithText(searchIndex,
1226                                                                                                    document,
1227                                                                                                    documentText,
1228                                                                                                    YES);
1229                                                 CFRelease(documentText);
1230                                         }
1231                                         CFRelease(document);
1232                                 } else {
1233                                         NSLog(@"Could not create document for %@ [%@]",logPath,[NSURL fileURLWithPath:logPath]);
1234                                 }
1235                                 
1236                                 //Update our progress
1237                                 logsIndexed++;
1238                                 if (lastUpdate == 0 || TickCount() > lastUpdate + LOG_INDEX_STATUS_INTERVAL) {
1239                                         [[LogViewerWindowControllerClass existingWindowController]
1240                                             performSelectorOnMainThread:@selector(logIndexingProgressUpdate) 
1241                                                              withObject:nil
1242                                                           waitUntilDone:NO];
1243                                         lastUpdate = TickCount();
1244                                 }
1245                                 
1246                                 //Save the dirty array
1247                                 if (unsavedChanges++ > LOG_CLEAN_SAVE_INTERVAL) {
1248                                         [self _saveDirtyLogArray];
1250                                         unsavedChanges = 0;
1252                                         //Flush ram
1253                                         [pool release]; pool = [[NSAutoreleasePool alloc] init];
1254                                 }
1255                                 
1256                         } else {
1257                                 break; //Exit when we run out of logs
1258                         }
1259                 }
1260                 
1261                 //Save the slimmed down dirty log array
1262                 if (unsavedChanges) {
1263                         [self _saveDirtyLogArray];
1264                 }
1266                 [logAccessLock lock];
1267                 SKIndexFlush(searchIndex);
1268                 AILog(@"After cleaning dirty logs, the search index has a max ID of %i and a count of %i",
1269                           SKIndexGetMaximumDocumentID(searchIndex),
1270                           SKIndexGetDocumentCount(searchIndex));
1271                 [logAccessLock unlock];
1272                 
1273                 
1274                 [self performSelectorOnMainThread:@selector(didCleanDirtyLogs)
1275                                                            withObject:nil
1276                                                         waitUntilDone:NO];
1277     }
1279         CFRelease(searchIndex);
1281         [indexingThreadLock unlock];
1283     [pool release];
1286 - (NSLock *)logAccessLock
1288         return logAccessLock;
1291 - (void)_removePathsFromIndexThread:(NSDictionary *)userInfo
1293         NSAutoreleasePool   *pool = [[NSAutoreleasePool alloc] init];
1295         [indexingThreadLock lock];
1297         SKIndexRef logSearchIndex = (SKIndexRef)[userInfo objectForKey:@"SKIndexRef"];
1298         NSEnumerator *enumerator = [[userInfo objectForKey:@"Paths"] objectEnumerator];
1299         NSString         *logPath;
1300         
1301         while ((logPath = [enumerator nextObject])) {
1302                 SKDocumentRef document = SKDocumentCreateWithURL((CFURLRef)[NSURL fileURLWithPath:logPath]);
1303                 if (document) {
1304                         SKIndexRemoveDocument(logSearchIndex, document);
1305                         CFRelease(document);
1306                 }
1307         }
1309         [indexingThreadLock unlock];
1311         [pool release];
1314 - (void)removePathsFromIndex:(NSSet *)paths
1316         [NSThread detachNewThreadSelector:@selector(_removePathsFromIndexThread:)
1317                                                          toTarget:self
1318                                                    withObject:[NSDictionary dictionaryWithObjectsAndKeys:
1319                                                            (id)[self logContentIndex], @"SKIndexRef",
1320                                                            paths, @"Paths",
1321                                                            nil]];
1325 @end