libgaim.framework [386] which has extensive debugging in the upnp module to try to...
[adiumx.git] / Source / AILoggerPlugin.m
blob5b750cf0c983436c06aabde062a750925ecdc65b
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 "AILogViewerWindowController.h"
22 #import "AIMDLogViewerWindowController.h"
23 #import "AIXMLAppender.h"
24 #import <Adium/AIContentControllerProtocol.h>
25 #import <Adium/AIInterfaceControllerProtocol.h>
26 #import <Adium/AIChatControllerProtocol.h>
27 #import <Adium/AILoginControllerProtocol.h>
28 #import <Adium/AIMenuControllerProtocol.h>
29 #import <Adium/AIPreferenceControllerProtocol.h>
30 #import <Adium/AIToolbarControllerProtocol.h>
31 #import <Adium/AIAccount.h>
32 #import <Adium/AIChat.h>
33 #import <Adium/AIContentMessage.h>
34 #import <Adium/AIContentStatus.h>
35 #import <Adium/AIContentEvent.h>
36 #import <Adium/AIHTMLDecoder.h>
37 #import <Adium/AIListContact.h>
38 #import <Adium/AIService.h>
39 #import <AIUtilities/AIAttributedStringAdditions.h>
40 #import <AIUtilities/AIDictionaryAdditions.h>
41 #import <AIUtilities/AIFileManagerAdditions.h>
42 #import <AIUtilities/AIMenuAdditions.h>
43 #import <AIUtilities/AIStringAdditions.h>
44 #import <AIUtilities/AIToolbarUtilities.h>
45 #import <AIUtilities/AIApplicationAdditions.h>
46 #import <AIUtilities/AIDateFormatterAdditions.h>
47 #import <AIUtilities/AIImageAdditions.h>
48 #import <AIUtilities/NSCalendarDate+ISO8601Unparsing.h>
49 #import <AIUtilities/NSCalendarDate+ISO8601Parsing.h>
51 #import "AILogFileUpgradeWindowController.h"
53 #import "AdiumSpotlightImporter.h"
55 #define LOG_INDEX_NAME                          @"Logs.index"
56 #define DIRTY_LOG_ARRAY_NAME            @"DirtyLogs.plist"
57 #define KEY_LOG_INDEX_VERSION           @"Log Index Version"
59 #define LOG_INDEX_STATUS_INTERVAL       20      //Interval before updating the log indexing status
60 #define LOG_CLEAN_SAVE_INTERVAL         500     //Number of logs to index continuously before saving the dirty array and index
62 #define LOG_VIEWER                                      AILocalizedString(@"Chat Transcripts Viewer",nil)
63 #define VIEW_LOGS_WITH_CONTACT          AILocalizedString(@"View Chat Transcripts",nil)
65 #define CURRENT_LOG_VERSION                     7       //Version of the log index.  Increase this number to reset everyone's index.
67 #define LOG_VIEWER_IDENTIFIER           @"LogViewer"
69 #define XML_LOGGING_NAMESPACE           @"http://purl.org/net/ulf/ns/0.4-02"
70 #define NEW_LOGFILE_TIMEOUT                     600             //10 minutes
72 #define ENABLE_PROXIMITY_SEARCH         TRUE
74 @interface AILoggerPlugin (PRIVATE)
75 - (void)configureMenuItems;
76 - (SKIndexRef)createLogIndex;
77 - (void)closeLogIndex;
78 - (void)resetLogIndex;
79 - (NSString *)_logIndexPath;
80 - (void)loadDirtyLogArray;
81 - (void)_saveDirtyLogArray;
82 - (NSString *)_dirtyLogArrayPath;
83 - (void)_dirtyAllLogsThread;
84 - (void)_cleanDirtyLogsThread;
86 - (void)upgradeLogExtensions;
88 - (NSString *)keyForChat:(AIChat *)chat;
89 - (AIXMLAppender *)existingAppenderForChat:(AIChat *)chat;
90 - (AIXMLAppender *)appenderForChat:(AIChat *)chat;
91 - (void)closeAppenderForChat:(AIChat *)chat;
92 @end
94 static NSString     *logBasePath = nil;     //The base directory of all logs
95 Class LogViewerWindowControllerClass = NULL;
96 @implementation AILoggerPlugin
99 - (void)installPlugin
101         LogViewerWindowControllerClass = ([NSApp isOnTigerOrBetter] ?
102                                                                           [AIMDLogViewerWindowController class] :
103                                                                           [AILogViewerWindowController class]);
105     //Init
106         observingContent = NO;
108         activeAppenders = [[NSMutableDictionary alloc] init];
109         appenderCloseTimers = [[NSMutableDictionary alloc] init];
110         
111         xhtmlDecoder = [[AIHTMLDecoder alloc] initWithHeaders:NO
112                                                                                                  fontTags:YES
113                                                                                         closeFontTags:YES
114                                                                                                 colorTags:YES
115                                                                                                 styleTags:YES
116                                                                                    encodeNonASCII:YES
117                                                                                          encodeSpaces:NO
118                                                                                 attachmentsAsText:YES
119                                                                 onlyIncludeOutgoingImages:NO
120                                                                                    simpleTagsOnly:NO
121                                                                                    bodyBackground:NO];
122         [xhtmlDecoder setGeneratesStrictXHTML:YES];
123         [xhtmlDecoder setUsesAttachmentTextEquivalents:YES];
124         
125         statusTranslation = [[NSDictionary alloc] initWithObjectsAndKeys:
126                 @"away",@"away",
127                 @"online",@"return_away",
128                 @"online",@"online",
129                 @"offline",@"offline",
130                 @"idle",@"idle",
131                 @"available",@"return_idle",
132                 @"away",@"away_message",
133                 nil];
135         //Setup our preferences
136         [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:LOGGING_DEFAULT_PREFS 
137                                                                                                                                                 forClass:[self class]] 
138                                                                                   forGroup:PREF_GROUP_LOGGING];
140         //Install the log viewer menu items
141         [self configureMenuItems];
142         
143         //Create a logs directory
144         logBasePath = [[[[[adium loginController] userDirectory] stringByAppendingPathComponent:PATH_LOGS] stringByExpandingTildeInPath] retain];
145         [[NSFileManager defaultManager] createDirectoriesForPath:logBasePath];
147         //Observe preference changes
148         [[adium preferenceController] addObserver:self
149                                                                    forKeyPath:PREF_KEYPATH_LOGGER_ENABLE
150                                                                           options:NSKeyValueObservingOptionNew
151                                                                           context:NULL];
152         [self observeValueForKeyPath:PREF_KEYPATH_LOGGER_ENABLE
153                             ofObject:[adium preferenceController]
154                               change:nil
155                              context:NULL];
157         //Toolbar item
158         NSToolbarItem   *toolbarItem;
159         toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:LOG_VIEWER_IDENTIFIER
160                                                                                                                   label:AILocalizedString(@"Transcripts",nil)
161                                                        paletteLabel:AILocalizedString(@"View Chat Transcripts",nil)
162                                                             toolTip:AILocalizedString(@"View previous conversations with this contact or chat",nil)
163                                                              target:self
164                                                     settingSelector:@selector(setImage:)
165                                                         itemContent:[NSImage imageNamed:@"LogViewer" forClass:[self class]]
166                                                              action:@selector(showLogViewerToSelectedContact:)
167                                                                menu:nil];
168         [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"ListObject"];
170         dirtyLogArray = nil;
171         index_Content = nil;
172         stopIndexingThreads = NO;
173         suspendDirtyArraySave = NO;             
174         indexingThreadLock = [[NSLock alloc] init];
175         dirtyLogLock = [[NSLock alloc] init];
176         logAccessLock = [[NSLock alloc] init];
177         
178         //Init index searching
179         [self initLogIndexing];
180         
181         [self upgradeLogExtensions];
182         
183         [[adium notificationCenter] addObserver:self
184                                                                    selector:@selector(showLogNotification:)
185                                                                            name:Adium_ShowLogAtPath
186                                                                          object:nil];
189 - (void)uninstallPlugin
191         [activeAppenders release];
192         [appenderCloseTimers release];
193         [xhtmlDecoder release];
194         [statusTranslation release];
196         [[adium preferenceController] removeObserver:self forKeyPath:PREF_KEYPATH_LOGGER_ENABLE];
199 //Update for the new preferences
200 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
202         BOOL    newLogValue;
203         logHTML = YES;
205         //Start/Stop logging
206         newLogValue = [[object valueForKeyPath:keyPath] boolValue];
207         if (newLogValue != observingContent) {
208                 observingContent = newLogValue;
209                                 
210                 if (!observingContent) { //Stop Logging
211                         [[adium notificationCenter] removeObserver:self name:Content_ContentObjectAdded object:nil];
212                         
213                         [[adium notificationCenter] removeObserver:self name:Chat_DidOpen object:nil];                  
214                         [[adium notificationCenter] removeObserver:self name:Chat_WillClose object:nil];
216                 } else { //Start Logging
217                         [[adium notificationCenter] addObserver:self 
218                                                                                    selector:@selector(contentObjectAdded:) 
219                                                                                            name:Content_ContentObjectAdded 
220                                                                                          object:nil];
221                                                                                          
222                         [[adium notificationCenter] addObserver:self
223                                                                                    selector:@selector(chatOpened:)
224                                                                                            name:Chat_DidOpen
225                                                                                          object:nil];
226                                                                                          
227                         [[adium notificationCenter] addObserver:self
228                                                                                    selector:@selector(chatClosed:)
229                                                                                            name:Chat_WillClose
230                                                                                          object:nil];
231                         [[adium notificationCenter] addObserver:self
232                                                                                    selector:@selector(chatWillDelete:)
233                                                                                            name:ChatLog_WillDelete
234                                                                                          object:nil];
235                 }
236         }
240 //Logging Paths --------------------------------------------------------------------------------------------------------
241 + (NSString *)logBasePath
243         return logBasePath;
246 //Returns the RELATIVE path to the folder where the log should be written
247 + (NSString *)relativePathForLogWithObject:(NSString *)object onAccount:(AIAccount *)account
248 {       
249         return [NSString stringWithFormat:@"%@.%@/%@", [account serviceID], [[account UID] safeFilenameString], object];
252 + (NSString *)fileNameForLogWithObject:(NSString *)object onDate:(NSDate *)date
254         NSParameterAssert(date != nil);
255         NSParameterAssert(object != nil);
256         NSString    *dateString = [date descriptionWithCalendarFormat:@"%Y-%m-%dT%H.%M.%S%z" timeZone:nil locale:nil];
257         
258         NSAssert2(dateString != nil, @"Date string was invalid for the chatlog for %@ on %@", object, date);
259                 
260         return [NSString stringWithFormat:@"%@ (%@).chatlog", object, dateString];
263 + (NSString *)fullPathForLogOfChat:(AIChat *)chat onDate:(NSDate *)date
265         NSString        *objectUID = [chat name];
266         AIAccount       *account = [chat account];
268         if (!objectUID) objectUID = [[chat listObject] UID];
269         objectUID = [objectUID safeFilenameString];
271         NSString        *fileName = [self fileNameForLogWithObject:objectUID onDate:date];
272         NSString        *absolutePath = [logBasePath stringByAppendingPathComponent:[self relativePathForLogWithObject:objectUID onAccount:account]];
273         NSString        *fullPath = [absolutePath stringByAppendingPathComponent:fileName];
275         return fullPath;
278 //Menu Items -----------------------------------------------------------------------------------------------------------
279 #pragma mark Menu Items
280 //Configure the log viewer menu items
281 - (void)configureMenuItems
283     logViewerMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:LOG_VIEWER 
284                                                                                                                                                           target:self
285                                                                                                                                                           action:@selector(showLogViewer:)
286                                                                                                                                            keyEquivalent:@"L"] autorelease];
287     [[adium menuController] addMenuItem:logViewerMenuItem toLocation:LOC_Window_Auxiliary];
289     viewContactLogsMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:VIEW_LOGS_WITH_CONTACT
290                                                                                                                                                                         target:self
291                                                                                                                                                                         action:@selector(showLogViewerToSelectedContact:) 
292                                                                                                                                                          keyEquivalent:@"l"] autorelease];
293     [[adium menuController] addMenuItem:viewContactLogsMenuItem toLocation:LOC_Contact_Info];
295     viewContactLogsContextMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:VIEW_LOGS_WITH_CONTACT
296                                                                                                                                                                                    target:self
297                                                                                                                                                                                    action:@selector(showLogViewerToSelectedContextContact:) 
298                                                                                                                                                                         keyEquivalent:@""] autorelease];
299     [[adium menuController] addContextualMenuItem:viewContactLogsContextMenuItem toLocation:Context_Contact_Manage];
302 //Enable/Disable our view log menus
303 - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
305     if (menuItem == viewContactLogsMenuItem) {
306         AIListObject    *selectedObject = [[adium interfaceController] selectedListObject];
307                 return selectedObject && [selectedObject isKindOfClass:[AIListContact class]];
309     } else if (menuItem == viewContactLogsContextMenuItem) {
310         AIListObject    *selectedObject = [[adium menuController] currentContextMenuObject];            
311                 return selectedObject && [selectedObject isKindOfClass:[AIListContact class]];
312                 
313     }
314         
315     return YES;
319  * @brief Show the log viewer for no contact
321  * Invoked from the Window menu
322  */
323 - (void)showLogViewer:(id)sender
325     [LogViewerWindowControllerClass openForContact:nil  
326                                                                                  plugin:self];  
330  * @brief Show the log viewer, displaying only the selected contact's logs
332  * Invoked from the Contact menu
333  */
334 - (void)showLogViewerToSelectedContact:(id)sender
336     AIListObject   *selectedObject = [[adium interfaceController] selectedListObject];
337     [LogViewerWindowControllerClass openForContact:([selectedObject isKindOfClass:[AIListContact class]] ?
338                                                                                                  (AIListContact *)selectedObject : 
339                                                                                                  nil)  
340                                                                                  plugin:self];
343 - (void)showLogViewerForLogAtPath:(NSString *)inPath
345         [LogViewerWindowControllerClass openLogAtPath:inPath plugin:self];
348 - (void)showLogNotification:(NSNotification *)inNotification
350         [self showLogViewerForLogAtPath:[inNotification object]];
354  * @brief Show the log viewer, displaying only the selected contact's logs
356  * Invoked from a contextual menu
357  */
358 - (void)showLogViewerToSelectedContextContact:(id)sender
360         AIListObject* object = [[adium menuController] currentContextMenuObject];
361         if ([object isKindOfClass:[AIListContact class]]) {
362                 [NSApp activateIgnoringOtherApps:YES];
363                 [[[LogViewerWindowControllerClass openForContact:(AIListContact *)object plugin:self] window]
364                                                                          makeKeyAndOrderFront:nil];
365         }
369 //Logging --------------------------------------------------------------------------------------------------------------
370 #pragma mark Logging
371 //Log any content that is sent or received
372 - (void)contentObjectAdded:(NSNotification *)notification
374         AIContentMessage        *content = [[notification userInfo] objectForKey:@"AIContentObject"];
375         if ([content postProcessContent]) {
376                 AIChat                          *chat = [notification object];
378                 //Don't log chats for temporary accounts
379                 if ([[chat account] isTemporary]) return;       
380                 
381                 BOOL                    dirty = NO;
382                 NSString                *contentType = [content type];
383                 NSString                *date = [[[content date] dateWithCalendarFormat:nil timeZone:nil] ISO8601DateString];
385                 if ([contentType isEqualToString:CONTENT_MESSAGE_TYPE]) {
386                         NSMutableArray *attributeKeys = [NSMutableArray arrayWithObjects:@"sender", @"time", nil];
387                         NSMutableArray *attributeValues = [NSMutableArray arrayWithObjects:[[content source] UID], date, nil];
388                         
389                         if([content isAutoreply])
390                         {
391                                 [attributeKeys addObject:@"auto"];
392                                 [attributeValues addObject:@"true"];
393                         }
394                         
395                         [[self appenderForChat:chat] addElementWithName:@"message" 
396                                                   escapedContent:[xhtmlDecoder encodeHTML:[content message] imagesPath:nil]
397                                                    attributeKeys:attributeKeys
398                                                  attributeValues:attributeValues];
399                         dirty = YES;
400                 } else {
401                         //XXX: Yucky hack. This is here because we get status and event updates for metas, not for individual contacts. Or something like that.
402                         AIListObject    *retardedMetaObject = [content source];
403                         AIListObject    *actualObject = nil;
404                         AIListContact   *participatingListObject = nil;
405                         
406                         NSEnumerator    *enumerator = [[chat participatingListObjects] objectEnumerator];
407                         
408                         while ((participatingListObject = [enumerator nextObject])) {
409                                 if ([participatingListObject parentContact] == retardedMetaObject) {
410                                         actualObject = participatingListObject;
411                                         break;
412                                 }
413                         }
414                         
415                         //If we can't find it for some reason, we probably shouldn't attempt logging.
416                         if (actualObject) {
417                                 if ([contentType isEqualToString:CONTENT_STATUS_TYPE]) {
418                                         NSString *translatedStatus = [statusTranslation objectForKey:[(AIContentStatus *)content status]];
419                                         if(translatedStatus == nil)
420                                                 AILog(@"AILogger: Don't know how to translate status: %@", [(AIContentStatus *)content status]);
421                                         else {
422                                                 [[self appenderForChat:chat] addElementWithName:@"status"
423                                                                           escapedContent:([(AIContentStatus *)content loggedMessage] ? [xhtmlDecoder encodeHTML:[(AIContentStatus *)content loggedMessage] imagesPath:nil] : nil)
424                                                                            attributeKeys:[NSArray arrayWithObjects:@"type", @"sender", @"time", nil]
425                                                                          attributeValues:[NSArray arrayWithObjects:
426                                                                                  translatedStatus, 
427                                                                                  [actualObject UID], 
428                                                                                  date,
429                                                                                  nil]];
430                                                 dirty = YES;
431                                         }
433                                 } else if ([contentType isEqualToString:CONTENT_EVENT_TYPE]) {
434                                         [[self appenderForChat:chat] addElementWithName:@"event"
435                                                                   escapedContent:[xhtmlDecoder encodeHTML:[content message] imagesPath:nil]
436                                                                    attributeKeys:[NSArray arrayWithObjects:@"type", @"sender", @"time", nil]
437                                                                  attributeValues:[NSArray arrayWithObjects:[(AIContentEvent *)content eventType], [[content source] UID], date, nil]];
438                                         dirty = YES;
439                                 }
440                         }
441                 }
442                 //Don't create a new one if not needed
443                 AIXMLAppender *appender = [self existingAppenderForChat:chat];
444                 if (dirty && appender)
445                         [self markLogDirtyAtPath:[appender path] forChat:chat];
446         }
449 - (void)chatOpened:(NSNotification *)notification
451         AIChat  *chat = [notification object];
453         //Don't log chats for temporary accounts
454         if ([[chat account] isTemporary]) return;       
457 - (void)chatClosed:(NSNotification *)notification
459         AIChat  *chat = [notification object];
461         //Don't log chats for temporary accounts
462         if ([[chat account] isTemporary]) return;
463         
464         //Use this method so we don't create a new appender for chat close events
465         AIXMLAppender *appender = [self existingAppenderForChat:chat];
466         
467         //If there is an appender, add the windowClose event
468         if (appender) {
469                 [appender addElementWithName:@"event"
470                                                          content:nil
471                                            attributeKeys:[NSArray arrayWithObjects:@"type", @"sender", @"time", nil]
472                                          attributeValues:[NSArray arrayWithObjects:@"windowClosed", [[chat account] UID], [[[chat dateOpened] dateWithCalendarFormat:nil timeZone:nil] ISO8601DateString], nil]];
474                 [self closeAppenderForChat:chat];
476                 [self markLogDirtyAtPath:[appender path] forChat:chat];
477         }
480 //Ugly method. Shouldn't this notification post an AIChat, not an AIChatLog?
481 - (void)chatWillDelete:(NSNotification *)notification
483         AIChatLog *chatLog = [notification object];
484         NSString *chatID = [NSString stringWithFormat:@"%@.%@-%@", [chatLog serviceClass], [chatLog from], [chatLog to]];
485         AIXMLAppender *appender = [activeAppenders objectForKey:chatID];
486         
487         if (appender != nil) {
488                 if ([[appender path] hasSuffix:[chatLog path]]) {
489                         [activeAppenders removeObjectForKey:chatID];
490                         [[appenderCloseTimers objectForKey:chatID] invalidate];
491                         [appenderCloseTimers removeObjectForKey:chatID];
492                 }
493         }
496 - (NSString *)keyForChat:(AIChat *)chat
498         AIAccount *account = [chat account];
499         NSString *chatID = [chat isGroupChat] ? [chat name] : [[chat listObject] UID];
500         
501         return [NSString stringWithFormat:@"%@.%@-%@", [account serviceID], [account UID], chatID];
504 - (AIXMLAppender *)existingAppenderForChat:(AIChat *)chat
506         //Look up the key for this chat and use it to try to retrieve the appender
507         NSString *chatKey = [self keyForChat:chat];
508         return [activeAppenders objectForKey:chatKey];  
511 - (AIXMLAppender *)appenderForChat:(AIChat *)chat
513         //Check if there is already an appender for this chat
514         AIXMLAppender   *appender = [self existingAppenderForChat:chat];
515         NSString                *chatKey = [self keyForChat:chat];
516         NSDate                  *chatDate = [chat dateOpened];
518         //If there's an appender scheduled to be closed for this chat, invalidate the timer, since we're using it now
519         if (appender && [appenderCloseTimers objectForKey:chatKey]) {
520                 [[appenderCloseTimers objectForKey:chatKey] invalidate];
521                 [appenderCloseTimers removeObjectForKey:chatKey];
522         //If there isn't already an appender, create a new one and add it to the dictionary
523         } else if (!appender) {
524                 NSString                *fullPath = [AILoggerPlugin fullPathForLogOfChat:chat onDate:chatDate];
526                 appender = [AIXMLAppender documentWithPath:fullPath];
527                 [appender initializeDocumentWithRootElementName:@"chat"
528                                                                                   attributeKeys:[NSArray arrayWithObjects:@"xmlns", @"account", @"service", nil]
529                                                                                 attributeValues:[NSArray arrayWithObjects:
530                                                                                         XML_LOGGING_NAMESPACE,
531                                                                                         [[chat account] UID],
532                                                                                         [[chat account] serviceID],
533                                                                                         nil]];
534                 
535                 //Add the window opened event now
536                 [appender addElementWithName:@"event"
537                                          content:nil
538                            attributeKeys:[NSArray arrayWithObjects:@"type", @"sender", @"time", nil]
539                          attributeValues:[NSArray arrayWithObjects:@"windowOpened", [[chat account] UID], [[chatDate dateWithCalendarFormat:nil timeZone:nil] ISO8601DateString], nil]];
541                 [activeAppenders setObject:appender forKey:chatKey];
542                 
543                 [self markLogDirtyAtPath:[appender path] forChat:chat];
544         }
545         
546         return appender;
549 - (void)closeAppenderForChat:(AIChat *)chat
551         //Create a new timer to fire after the timeout period, which will close the appender
552         NSString *chatKey = [self keyForChat:chat];
553         NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:NEW_LOGFILE_TIMEOUT 
554                                                                                                           target:self 
555                                                                                                         selector:@selector(finishClosingAppender:) 
556                                                                                                         userInfo:chatKey
557                                                                                                          repeats:NO];
558         //Add it to the appenderCloseTimers dictionary
559         [appenderCloseTimers setObject:timer forKey:chatKey];
562 - (void)finishClosingAppender:(NSTimer *)timer
564         //Remove the appender, closing its file descriptor upon dealloc
565         [activeAppenders removeObjectForKey:[timer userInfo]];
566         //Remove the timer, it's invalid anyway and not very useful
567         [appenderCloseTimers removeObjectForKey:[timer userInfo]];
571 //Display a warning to the user that logging failed, and disable logging to prevent additional warnings
572 //XXX not currently used. We may want to shift these strings for use when xml logging fails, so I'm not removing them -eds
574 - (void)displayErrorAndDisableLogging
576         NSRunAlertPanel(AILocalizedString(@"Unable to write log", nil),
577                                         [NSString stringWithFormat:
578                                                 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],
579                                         AILocalizedString(@"OK", nil), nil, nil);
581         //Disable logging
582         [[adium preferenceController] setPreference:[NSNumber numberWithBool:NO]
583                                              forKey:KEY_LOGGER_ENABLE
584                                               group:PREF_GROUP_LOGGING];
588 #pragma mark Message History
590 NSCalendarDate* getDateFromPath(NSString *path)
592         NSRange openParenRange, closeParenRange;
594         if ([path hasSuffix:@".chatlog"] && (openParenRange = [path rangeOfString:@"(" options:NSBackwardsSearch]).location != NSNotFound) {
595                 openParenRange = NSMakeRange(openParenRange.location, [path length] - openParenRange.location);
596                 if ((closeParenRange = [path rangeOfString:@")" options:0 range:openParenRange]).location != NSNotFound) {
597                         //Add and subtract one to remove the parenthesis
598                         NSString *dateString = [path substringWithRange:NSMakeRange(openParenRange.location + 1, (closeParenRange.location - openParenRange.location))];
599                         NSCalendarDate *date = [NSCalendarDate calendarDateWithString:dateString timeSeparator:'.'];
600                         return date;
601                 }
602         }
603         return nil;
606 int sortPaths(NSString *path1, NSString *path2, void *context)
608         NSCalendarDate *date1 = getDateFromPath(path1);
609         NSCalendarDate *date2 = getDateFromPath(path2);
610         
611         if(!date1 && !date2)
612                 return NSOrderedSame;
613         else if (date1 && date2)
614                 return [date2 compare:date1];
615         else
616                 return date2 ? NSOrderedDescending : NSOrderedAscending;
619 + (NSArray *)sortedArrayOfLogFilesForChat:(AIChat *)chat
621         NSString *baseLogPath = [[self fullPathForLogOfChat:chat onDate:[NSDate date]] stringByDeletingLastPathComponent];
622         NSArray *files = [[NSFileManager defaultManager] directoryContentsAtPath:baseLogPath];  
623         if (files) {
624                 return [files sortedArrayUsingFunction:&sortPaths context:NULL];
625         }
626         return nil;
629 #pragma mark Upgrade code
630 - (void)upgradeLogExtensions
632         if (![[[adium preferenceController] preferenceForKey:@"Log Extensions Updated" group:PREF_GROUP_LOGGING] boolValue]) {
633                 /* This could all be a simple NSDirectoryEnumerator call on basePath, but we wouldn't be able to show progress,
634                 * and this could take a bit.
635                 */
636                 NSFileManager   *defaultManager = [NSFileManager defaultManager];
637                 NSArray                 *accountFolders = [defaultManager directoryContentsAtPath:logBasePath];
638                 NSEnumerator    *accountFolderEnumerator = [accountFolders objectEnumerator];
639                 NSString                *accountFolderName;
640                 
641                 NSMutableSet    *pathsToContactFolders = [NSMutableSet set];
642                 while ((accountFolderName = [accountFolderEnumerator nextObject])) {
643                         NSString                *contactBasePath = [logBasePath stringByAppendingPathComponent:accountFolderName];
644                         NSArray                 *contactFolders = [defaultManager directoryContentsAtPath:contactBasePath];
645                         
646                         NSEnumerator    *contactFolderEnumerator = [contactFolders objectEnumerator];
647                         NSString                *contactFolderName;
648                         
649                         while ((contactFolderName = [contactFolderEnumerator nextObject])) {
650                                 [pathsToContactFolders addObject:[contactBasePath stringByAppendingPathComponent:contactFolderName]];
651                         }
652                 }
653                 
654                 unsigned                contactsToProcess = [pathsToContactFolders count];
655                 unsigned                processed = 0;
656                 
657                 if (contactsToProcess) {
658                         AILogFileUpgradeWindowController *upgradeWindowController;
659                         
660                         upgradeWindowController = [[AILogFileUpgradeWindowController alloc] initWithWindowNibName:@"LogFileUpgrade"];
661                         [[upgradeWindowController window] makeKeyAndOrderFront:nil];
663                         NSEnumerator    *pathsToContactFoldersEnumerator = [pathsToContactFolders objectEnumerator];
664                         NSString                *pathToContactFolder;
665                         while ((pathToContactFolder = [pathsToContactFoldersEnumerator nextObject])) {
666                                 NSDirectoryEnumerator *enumerator = [defaultManager enumeratorAtPath:pathToContactFolder];
667                                 NSString        *file;
668                                 
669                                 while ((file = [enumerator nextObject])) {
670                                         if (([[file pathExtension] isEqualToString:@"html"]) ||
671                                                 ([[file pathExtension] isEqualToString:@"adiumLog"]) ||
672                                                 (([[file pathExtension] isEqualToString:@"bak"]) && ([file hasSuffix:@".html.bak"] || 
673                                                                                                                                                          [file hasSuffix:@".adiumLog.bak"]))) {
674                                                 NSString *fullFile = [pathToContactFolder stringByAppendingPathComponent:file];
675                                                 NSString *newFile = [[fullFile stringByDeletingPathExtension] stringByAppendingPathExtension:@"AdiumHTMLLog"];
676                                                 
677                                                 [defaultManager movePath:fullFile
678                                                                                   toPath:newFile
679                                                                                  handler:self];
680                                         }
681                                 }
682                                 
683                                 processed++;
684                                 [upgradeWindowController setProgress:(processed*100.0)/contactsToProcess];
685                         }
686                         
687                         [upgradeWindowController close];
688                         [upgradeWindowController release];
689                 }
690                 
691                 [[adium preferenceController] setPreference:[NSNumber numberWithBool:YES]
692                                                                                          forKey:@"Log Extensions Updated"
693                                                                                           group:PREF_GROUP_LOGGING];
694         }
697 - (BOOL)fileManager:(NSFileManager *)manager shouldProceedAfterError:(NSDictionary *)errorInfo
699         NSLog(@"Error: %@",errorInfo);
700         
701         return NO;
704 //Log Indexing ---------------------------------------------------------------------------------------------------------
705 #pragma mark Log Indexing
706 /***** Everything below this point is related to log index generation and access ****/
708 /* For the log content searching, we are required to re-index a log whenever it changes.  The solution below to
709  * this problem is along the lines of:
710  *              - Keep an array of logs that need to be re-indexed
711  *              - Whenever a log is changed, add it to this array
712  *              - When the log viewer is opened, re-index all the logs in the array
713  */
716  * @brief Initialize log indexing
717  */
718 - (void)initLogIndexing
720         //Load the list of logs that need re-indexing
721         [self loadDirtyLogArray];
725  * @brief Prepare the log index for searching.
727  * Must call before attempting to use the logSearchIndex.
728  */
729 - (void)prepareLogContentSearching
731     /* Load the index and start indexing to make it current
732          * If we're going to need to re-index all our logs from scratch, it will make
733          * things faster if we start with a fresh log index as well.
734          */
735         if (!dirtyLogArray) {
736                 [self resetLogIndex];
737         }
739         //Load the contentIndex immediately; this will clear dirtyLogArray if necessary
740         [self logContentIndex];
742         stopIndexingThreads = NO;
743         if (!dirtyLogArray) {
744                 [self dirtyAllLogs];
745         } else {
746                 [self cleanDirtyLogs];
747         }
750 //Close down and clean up the log index  (Call when finished using the logSearchIndex)
751 - (void)cleanUpLogContentSearching
753         [self stopIndexingThreads];
754         [self closeLogIndex];
757 //Returns the Search Kit index for log content searching
758 - (SKIndexRef)logContentIndex
760         SKIndexRef      returnIndex;
762         [logAccessLock lock];
763         if (!index_Content) index_Content = [self createLogIndex];
764         returnIndex = (SKIndexRef)[[(NSObject *)index_Content retain] autorelease];
765         [logAccessLock unlock];
767         return returnIndex;
770 //Mark a log as needing a re-index
771 - (void)markLogDirtyAtPath:(NSString *)path forChat:(AIChat *)chat
773         NSString    *dirtyKey = [@"LogIsDirty_" stringByAppendingString:path];
774         
775         if (![chat integerStatusObjectForKey:dirtyKey]) {
776                 //Add to dirty array (Lock to ensure that no one changes its content while we are)
777                 [dirtyLogLock lock];
778                 if (path != nil) {
779                         if (!dirtyLogArray) dirtyLogArray = [[NSMutableArray alloc] init];
781                         if (![dirtyLogArray containsObject:path]) {
782                                 [dirtyLogArray addObject:path];
783                         }
784                 }
785                 [dirtyLogLock unlock];
787                 //Save the dirty array immedientally
788                 [self _saveDirtyLogArray];
789                 
790                 //Flag the chat with 'LogIsDirty' for this filename.  On the next message we can quickly check this flag.
791                 [chat setStatusObject:[NSNumber numberWithBool:YES]
792                                            forKey:dirtyKey
793                                            notify:NotifyNever];
794         }       
797 - (void)markLogDirtyAtPath:(NSString *)path
799         if(!path) return;
800         [dirtyLogLock lock];
801         if (!dirtyLogArray) dirtyLogArray = [[NSMutableArray alloc] init];
803         if (![dirtyLogArray containsObject:path]) {
804                 [dirtyLogArray addObject:path];
805         }
806         [dirtyLogLock unlock];  
809 //Get the current status of indexing.  Returns NO if indexing is not occuring
810 - (BOOL)getIndexingProgress:(int *)indexNumber outOf:(int *)total
812         //logsIndexed + 1 is the log we are currently indexing
813         if (indexNumber) *indexNumber = (logsIndexed + 1 <= logsToIndex) ? logsIndexed + 1 : logsToIndex;
814         if (total) *total = logsToIndex;
815         return (logsToIndex > 0);
819 //Log index ------------------------------------------------------------------------------------------------------------
820 //Search kit index used to searching log content
821 #pragma mark Log Index
823  * @brief Create the log index
825  * Should be called within logAccessLock being locked
826  */
827 - (SKIndexRef)createLogIndex
829     NSString    *logIndexPath = [self _logIndexPath];
830     NSURL       *logIndexPathURL = [NSURL fileURLWithPath:logIndexPath];
831         SKIndexRef      newIndex = NULL;
833     if ([[NSFileManager defaultManager] fileExistsAtPath:logIndexPath]) {
834                 newIndex = SKIndexOpenWithURL((CFURLRef)logIndexPathURL, (CFStringRef)@"Content", true);
835                 AILog(@"Opened index %x from %@",newIndex,logIndexPathURL);
836     }
837     if (!newIndex) {
838                 NSDictionary *textAnalysisProperties;
839                 
840                 if ([NSApp isOnTigerOrBetter]) {
841                         textAnalysisProperties = [NSDictionary dictionaryWithObjectsAndKeys:
842                                 [NSNumber numberWithInt:0], kSKMaximumTerms,
843 #if ENABLE_PROXIMITY_SEARCH
844                                 kCFBooleanTrue, kSKProximityIndexing, 
845 #endif
846                                 nil];
848                 } else {
849                         textAnalysisProperties = nil;
850                 }
852                 //Create the index if one doesn't exist
853                 [[NSFileManager defaultManager] createDirectoriesForPath:[logIndexPath stringByDeletingLastPathComponent]];
854                 
855                 newIndex = SKIndexCreateWithURL((CFURLRef)logIndexPathURL,
856                                                                                 (CFStringRef)@"Content", 
857                                                                                 kSKIndexInverted,
858                                                                                 (CFDictionaryRef)textAnalysisProperties);
859                 AILog(@"Created a new log index %x at %@ with textAnalysisProperties %@",newIndex,logIndexPathURL,textAnalysisProperties);
860                 //Clear the dirty log array in case it was loaded (this can happen if the user mucks with the cache directory)
861                 [[NSFileManager defaultManager] removeFileAtPath:[self _dirtyLogArrayPath] handler:NULL];
862                 [dirtyLogArray release]; dirtyLogArray = nil;
863     }
865         return newIndex;
868 - (void)releaseIndex:(SKIndexRef)inIndex
870     NSAutoreleasePool   *pool = [[NSAutoreleasePool alloc] init];
871         CFRelease(inIndex);
872         [pool release];
875 //Close the log index
876 - (void)closeLogIndex
878         [logAccessLock lock];
879         if (index_Content) {
880                 [NSThread detachNewThreadSelector:@selector(releaseIndex:)
881                                                                  toTarget:self
882                                                            withObject:(id)index_Content];
883                 index_Content = nil;
884         }
885         [logAccessLock unlock];
888 //Delete the log index
889 - (void)resetLogIndex
891         if ([[NSFileManager defaultManager] fileExistsAtPath:[self _logIndexPath]]) {
892                 [[NSFileManager defaultManager] removeFileAtPath:[self _logIndexPath] handler:NULL];
893         }       
895         if ([[NSFileManager defaultManager] fileExistsAtPath:[self _dirtyLogArrayPath]]) {
896                 [[NSFileManager defaultManager] removeFileAtPath:[self _dirtyLogArrayPath] handler:NULL];
897         }
900 //Path of log index file
901 - (NSString *)_logIndexPath
903     return [[adium cachesPath] stringByAppendingPathComponent:LOG_INDEX_NAME];
907 //Dirty Log Array ------------------------------------------------------------------------------------------------------
908 //Stores the absolute paths of logs that need to be re-indexed
909 #pragma mark Dirty Log Array
910 //Load the dirty log array
911 - (void)loadDirtyLogArray
913         if (!dirtyLogArray) {
914                 int logVersion = [[[adium preferenceController] preferenceForKey:KEY_LOG_INDEX_VERSION
915                                                                                                                                    group:PREF_GROUP_LOGGING] intValue];
917                 //If the log version has changed, we reset the index and don't load the dirty array (So all the logs are marked dirty)
918                 if (logVersion >= CURRENT_LOG_VERSION) {
919                         [dirtyLogLock lock];
920                         dirtyLogArray = [[NSMutableArray alloc] initWithContentsOfFile:[self _dirtyLogArrayPath]];
921                         [dirtyLogLock unlock];
922                 } else {
923                         [self resetLogIndex];
924                         [[adium preferenceController] setPreference:[NSNumber numberWithInt:CURRENT_LOG_VERSION]
925                                                              forKey:KEY_LOG_INDEX_VERSION
926                                                               group:PREF_GROUP_LOGGING];
927                 }
928         }
931 //Save the dirty lod array
932 - (void)_saveDirtyLogArray
934     if (dirtyLogArray && !suspendDirtyArraySave) {
935                 [dirtyLogLock lock];
936                 [dirtyLogArray writeToFile:[self _dirtyLogArrayPath] atomically:NO];
937                 [dirtyLogLock unlock];
938     }
941 //Path of the dirty log array file
942 - (NSString *)_dirtyLogArrayPath
944     return [[adium cachesPath] stringByAppendingPathComponent:DIRTY_LOG_ARRAY_NAME];
948 //Threaded Indexing ----------------------------------------------------------------------------------------------------
949 #pragma mark Threaded Indexing
950 //Stop any indexing related threads
951 - (void)stopIndexingThreads
953     //Let any indexing threads know it's time to stop, and wait for them to finish.
954     stopIndexingThreads = YES;
957 //The following methods will be run in a separate thread to avoid blocking the interface during index operations
958 //THREAD: Flag every log as dirty (Do this when there is no log index)
959 - (void)dirtyAllLogs
961     //Reset and rebuild the dirty array
962     [dirtyLogArray release]; dirtyLogArray = [[NSMutableArray alloc] init];
963         //[self _dirtyAllLogsThread];
964         [NSThread detachNewThreadSelector:@selector(_dirtyAllLogsThread) toTarget:self withObject:nil];
966 - (void)_dirtyAllLogsThread
968     NSAutoreleasePool   *pool = [[NSAutoreleasePool alloc] init];
969     NSEnumerator                *fromEnumerator, *toEnumerator, *logEnumerator;
970     NSString                    *fromName;
971     AILogFromGroup              *fromGroup = nil;
972     AILogToGroup                *toGroup;
973     AIChatLog                   *theLog;    
975     [indexingThreadLock lock];
976     suspendDirtyArraySave = YES;    //Prevent saving of the dirty array until we're finished building it
977     
978     //Create a fresh dirty log array
979     [dirtyLogLock lock];
980     [dirtyLogArray release]; dirtyLogArray = [[NSMutableArray alloc] init];
981     [dirtyLogLock unlock];
982         
983     //Process each from folder
984     fromEnumerator = [[[[NSFileManager defaultManager] directoryContentsAtPath:logBasePath] objectEnumerator] retain];
985     while ((fromName = [[fromEnumerator nextObject] retain])) {
986                 fromGroup = [[AILogFromGroup alloc] initWithPath:fromName fromUID:fromName serviceClass:nil];
988                 //Walk through every 'to' group
989                 toEnumerator = [[[fromGroup toGroupArray] objectEnumerator] retain];
990                 while (!stopIndexingThreads && (toGroup = [[toEnumerator nextObject] retain])) {
991                         //Walk through every log
992                         logEnumerator = [toGroup logEnumerator];
993                         while ((theLog = [logEnumerator nextObject]) && !stopIndexingThreads) {
994                                 //Add this log's path to our dirty array.  The dirty array is guarded with a lock
995                                 //since it will be accessed from outside this thread as well
996                                 [dirtyLogLock lock];
997                                 if (theLog != nil) {
998                                         [dirtyLogArray addObject:[logBasePath stringByAppendingPathComponent:[theLog path]]];
999                                 }
1000                                 [dirtyLogLock unlock];
1001                         }
1002                         
1003                         //Flush our pool
1004                         [pool release]; pool = [[NSAutoreleasePool alloc] init];
1005                         
1006                         [toGroup release];
1007                 }
1008                 [toEnumerator release];
1009                 
1010                 [fromGroup release];
1011                 [fromName release];
1012     }
1013     [fromEnumerator release];
1014         
1015     //Save the dirty array we just built
1016     if (!stopIndexingThreads) {
1017                 [self _saveDirtyLogArray];
1018                 suspendDirtyArraySave = NO; //Re-allow saving of the dirty array
1019     }
1020     
1021     //Begin cleaning the logs (If the log viewer is open)
1022     if ([LogViewerWindowControllerClass existingWindowController]) {
1023                 [self cleanDirtyLogs];
1024     }
1025     
1026     [indexingThreadLock unlock];
1027     [pool release];
1031  * @brief Index all dirty logs
1033  * Indexing will occur on a thread
1034  */
1035 - (void)cleanDirtyLogs
1037     //Reset the cleaning progress
1038     [dirtyLogLock lock];
1039     logsToIndex = [dirtyLogArray count];
1040     [dirtyLogLock unlock];
1041     logsIndexed = 0;
1043         [NSThread detachNewThreadSelector:@selector(_cleanDirtyLogsThread:) toTarget:self withObject:(id)[self logContentIndex]];
1046 - (void)didCleanDirtyLogs
1048         //Update our progress
1049         if (!stopIndexingThreads) {
1050                 logsToIndex = 0;
1051                 [[LogViewerWindowControllerClass existingWindowController] logIndexingProgressUpdate];
1052         }
1053         
1054         //Clear the dirty status of all open chats so they will be marked dirty if they receive another message
1055         NSEnumerator *enumerator = [[[adium chatController] openChats] objectEnumerator];
1056         AIChat           *chat;
1057         
1058         while ((chat = [enumerator nextObject])) {
1059                 NSString *existingAppenderPath = [[self existingAppenderForChat:chat] path];
1060                 if (existingAppenderPath) {
1061                         NSString *dirtyKey = [@"LogIsDirty_" stringByAppendingString:existingAppenderPath];
1062                         
1063                         if ([chat integerStatusObjectForKey:dirtyKey]) {
1064                                 [chat setStatusObject:nil
1065                                                            forKey:dirtyKey
1066                                                            notify:NotifyNever];
1067                         }
1068                 }
1069         }
1072 - (void)_cleanDirtyLogsThread:(SKIndexRef)searchIndex
1074     NSAutoreleasePool   *pool = [[NSAutoreleasePool alloc] init];
1076         //Ensure log indexing (in an old thread) isn't already going on and just waiting to stop
1077         [indexingThreadLock lock]; [indexingThreadLock unlock];
1078         
1079     [indexingThreadLock lock];
1081     //Start cleaning (If we're still supposed to go)
1082     if (!stopIndexingThreads) {
1083                 UInt32  lastUpdate = TickCount();
1084                 int             unsavedChanges = 0;
1086                 AILog(@"Cleaning %i dirty logs", [dirtyLogArray count]);
1088                 //Scan until we're done or told to stop
1089                 while (!stopIndexingThreads) {
1090                         NSString        *logPath = nil;
1091                         
1092                         //Get the next dirty log
1093                         [dirtyLogLock lock];
1094                         if ([dirtyLogArray count]) {
1095                                 logPath = [[[dirtyLogArray lastObject] retain] autorelease]; //retain to prevent deallocation when removing from the array
1096                                 [dirtyLogArray removeLastObject];
1097                         }
1098                         [dirtyLogLock unlock];
1100                         if (logPath) {
1101                                 SKDocumentRef   document;
1102                                 
1103                                 document = SKDocumentCreateWithURL((CFURLRef)[NSURL fileURLWithPath:logPath]);
1104                                 if (document) {
1105                                         /* We _could_ use SKIndexAddDocument() and depend on our Spotlight plugin for importing.
1106                                          * However, this has three problems:
1107                                          *      1. Slower, especially to start initial indexing, which is the most common use case since the log viewer
1108                                          *         indexes recently-modified ("dirty") logs when it opens.
1109                                          *  2. Sometimes logs don't appear to be associated with the right URI type and therefore don't get indexed.
1110                                          *  3. On 10.3, this means that logs' markup is indexed in addition to their text, which is undesireable.
1111                                          */
1112                                         CFStringRef documentText = CopyTextContentForFile(NULL, (CFStringRef)logPath);
1113                                         if (documentText) {
1114                                                 SKIndexAddDocumentWithText(searchIndex,
1115                                                                                                    document,
1116                                                                                                    documentText,
1117                                                                                                    YES);
1118                                                 CFRelease(documentText);
1119                                         }
1120                                         CFRelease(document);
1121                                 } else {
1122                                         NSLog(@"Could not create document for %@ [%@]",logPath,[NSURL fileURLWithPath:logPath]);
1123                                 }
1124                                 
1125                                 //Update our progress
1126                                 logsIndexed++;
1127                                 if (lastUpdate == 0 || TickCount() > lastUpdate + LOG_INDEX_STATUS_INTERVAL) {
1128                                         [[LogViewerWindowControllerClass existingWindowController]
1129                                             performSelectorOnMainThread:@selector(logIndexingProgressUpdate) 
1130                                                              withObject:nil
1131                                                           waitUntilDone:NO];
1132                                         lastUpdate = TickCount();
1133                                 }
1134                                 
1135                                 //Save the dirty array
1136                                 if (unsavedChanges++ > LOG_CLEAN_SAVE_INTERVAL) {
1137                                         [self _saveDirtyLogArray];
1139                                         unsavedChanges = 0;
1141                                         //Flush ram
1142                                         [pool release]; pool = [[NSAutoreleasePool alloc] init];
1143                                 }
1144                                 
1145                         } else {
1146                                 break; //Exit when we run out of logs
1147                         }
1148                 }
1149                 
1150                 //Save the slimmed down dirty log array
1151                 if (unsavedChanges) {
1152                         [self _saveDirtyLogArray];
1153                 }
1155                 SKIndexFlush(searchIndex);
1157                 [self performSelectorOnMainThread:@selector(didCleanDirtyLogs)
1158                                                            withObject:nil
1159                                                         waitUntilDone:NO];
1160     }
1162         [indexingThreadLock unlock];
1164     [pool release];
1167 - (NSLock *)logAccessLock
1169         return logAccessLock;
1172 - (void)_removePathsFromIndexThread:(NSDictionary *)userInfo
1174         NSAutoreleasePool   *pool = [[NSAutoreleasePool alloc] init];
1176         [indexingThreadLock lock];
1178         SKIndexRef logSearchIndex = (SKIndexRef)[userInfo objectForKey:@"SKIndexRef"];
1179         NSEnumerator *enumerator = [[userInfo objectForKey:@"Paths"] objectEnumerator];
1180         NSString         *logPath;
1181         
1182         while ((logPath = [enumerator nextObject])) {
1183                 SKDocumentRef document = SKDocumentCreateWithURL((CFURLRef)[NSURL fileURLWithPath:logPath]);
1184                 if (document) {
1185                         SKIndexRemoveDocument(logSearchIndex, document);
1186                         CFRelease(document);
1187                 }
1188         }
1190         [indexingThreadLock unlock];
1192         [pool release];
1195 - (void)removePathsFromIndex:(NSSet *)paths
1197         [NSThread detachNewThreadSelector:@selector(_removePathsFromIndexThread:)
1198                                                          toTarget:self
1199                                                    withObject:[NSDictionary dictionaryWithObjectsAndKeys:
1200                                                            (id)[self logContentIndex], @"SKIndexRef",
1201                                                            paths, @"Paths",
1202                                                            nil]];
1206 @end