2006-05-16 bradeeoh <beidson@apple.com>
[WebKit-https.git] / WebKit / Misc / WebFileDatabase.m
1 /*
2  * Copyright (C) 2005 Apple Computer, Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 #import <WebKit/WebFileDatabase.h>
30 #import <WebKit/WebKitLogging.h>
31 #import <WebKit/WebLRUFileList.h>
32 #import <WebKit/WebNSFileManagerExtras.h>
33
34 #import <fcntl.h>
35 #import <fts.h>
36 #import <pthread.h>
37 #import <string.h>
38 #import <sys/stat.h>
39 #import <sys/types.h>
40 #import <sys/mman.h>
41
42 #if ERROR_DISABLED
43 #define BEGIN_EXCEPTION_HANDLER
44 #define END_EXCEPTION_HANDLER
45 #else
46 #define BEGIN_EXCEPTION_HANDLER NS_DURING
47 #define END_EXCEPTION_HANDLER \
48     NS_HANDLER \
49         LOG_ERROR("Uncaught exception: %@ [%@] [%@]", [localException class], [localException reason], [localException userInfo]); \
50     NS_ENDHANDLER
51 #endif
52
53 static pthread_once_t databaseInitControl = PTHREAD_ONCE_INIT;
54 static NSNumber *WebFileDirectoryPOSIXPermissions;
55 static NSNumber *WebFilePOSIXPermissions;
56 static NSRunLoop *syncRunLoop;
57
58 #define UniqueFilePathSize (34)
59 static void UniqueFilePathForKey(id key, char *buffer);
60
61 #define MinThreadPriority (10)
62 static int SetThreadPriority(int priority);
63
64 typedef enum
65 {
66     WebFileDatabaseSetObjectOp,
67     WebFileDatabaseRemoveObjectOp,
68 } WebFileDatabaseOpcode;
69
70 enum
71 {
72     MAX_UNSIGNED_LENGTH = 20, // long enough to hold the string representation of a 64-bit unsigned number
73     SYNC_IDLE_THRESHOLD = 10,
74 };
75
76 // interface WebFileDatabaseOp -------------------------------------------------------------
77
78 @interface WebFileDatabaseOp : NSObject
79 {
80     WebFileDatabaseOpcode opcode;
81     id key;
82     id object; 
83 }
84
85 +(id)opWithCode:(WebFileDatabaseOpcode)opcode key:(id)key object:(id)object;
86 -(id)initWithCode:(WebFileDatabaseOpcode)opcode key:(id)key object:(id)object;
87
88 -(WebFileDatabaseOpcode)opcode;
89 -(id)key;
90 -(id)object;
91 -(void)perform:(WebFileDatabase *)target;
92
93 @end
94
95
96 // implementation WebFileDatabaseOp -------------------------------------------------------------
97
98 @implementation WebFileDatabaseOp
99
100 +(id)opWithCode:(WebFileDatabaseOpcode)theOpcode key:(id)theKey object:(id)theObject
101 {
102     return [[[WebFileDatabaseOp alloc] initWithCode:theOpcode key:theKey object:theObject] autorelease];
103 }
104
105 -(id)initWithCode:(WebFileDatabaseOpcode)theOpcode key:(id)theKey object:(id)theObject
106 {
107     ASSERT(theKey);
108
109     if ((self = [super init])) {
110         
111         opcode = theOpcode;
112         key = [theKey retain];
113         object = [theObject retain];
114         
115         return self;
116     }
117   
118     return nil;
119 }
120
121 -(WebFileDatabaseOpcode)opcode
122 {
123     return opcode;
124 }
125
126 -(id)key
127 {
128     return key;
129 }
130
131 -(id)object
132 {
133     return object;
134 }
135
136 -(void)perform:(WebFileDatabase *)target
137 {
138     ASSERT(target);
139
140     switch (opcode) {
141         case WebFileDatabaseSetObjectOp:
142             [target performSetObject:object forKey:key];
143             break;
144         case WebFileDatabaseRemoveObjectOp:
145             [target performRemoveObjectForKey:key];
146             break;
147         default:
148             ASSERT_NOT_REACHED();
149             break;
150     }
151 }
152
153 -(void)dealloc
154 {
155     [key release];
156     [object release];
157     
158     [super dealloc];
159 }
160
161 @end
162
163
164 // interface WebFileDatabasePrivate -----------------------------------------------------------
165
166 @interface WebFileDatabase (WebFileDatabasePrivate)
167
168 -(void)_createLRUList:(id)arg;
169 -(void)_truncateToSizeLimit:(unsigned)size;
170
171 @end
172
173 // implementation WebFileDatabasePrivate ------------------------------------------------------
174
175 @implementation WebFileDatabase (WebFileDatabasePrivate)
176
177 static int SetThreadPriority(int priority) 
178 {
179     struct sched_param sp;
180
181     memset(&sp, 0, sizeof(struct sched_param));
182     sp.sched_priority=priority;
183     if (pthread_setschedparam(pthread_self(), SCHED_OTHER, &sp) == -1) {
184         LOG_ERROR("Failed to change priority.");
185         return -1;
186     }
187     return 0;
188 }
189
190 static void UniqueFilePathForKey(id key, char *buffer)
191 {
192     const char *s;
193     UInt32 hash1;
194     UInt32 hash2;
195     CFIndex len;
196     CFIndex cnt;
197     
198     s = [[[[key description] lowercaseString] stringByStandardizingPath] UTF8String];
199     len = strlen(s);
200
201     // compute first hash    
202     hash1 = len;
203     for (cnt = 0; cnt < len; cnt++) {
204         hash1 += (hash1 << 8) + s[cnt];
205     }
206     hash1 += (hash1 << (len & 31));
207
208     // compute second hash    
209     hash2 = len;
210     for (cnt = 0; cnt < len; cnt++) {
211         hash2 = (37 * hash2) ^ s[cnt];
212     }
213
214     snprintf(buffer, UniqueFilePathSize, "%.2lu/%.2lu/%.10lu-%.10lu.cache", ((hash1 & 0xff) >> 4), ((hash2 & 0xff) >> 4), hash1, hash2);
215 }
216
217 -(void)_createLRUList:(id)arg
218 {
219     SetThreadPriority(MinThreadPriority + 1); // make this a little higher priority than the syncRunLoop thread
220
221     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
222
223     BEGIN_EXCEPTION_HANDLER
224     
225     WebLRUFileList *fileList = WebLRUFileListCreate();
226     WebLRUFileListRebuildFileDataUsingRootDirectory(fileList, [path fileSystemRepresentation]);
227     lru = fileList;
228
229     END_EXCEPTION_HANDLER
230
231     LOG(FileDatabaseActivity, "lru list created");
232
233     [pool drain];
234 }
235
236 -(void)_truncateToSizeLimit:(unsigned)size
237 {
238     NSFileManager *defaultManager;
239     
240     if (!lru || size > [self usage]) {
241         return;
242     }
243
244     if (size == 0) {
245         [self removeAllObjects];
246     }
247     else {
248         defaultManager = [NSFileManager defaultManager];
249         [mutex lock];
250         while ([self usage] > size) {
251             char uniqueKey[UniqueFilePathSize];
252             if (!WebLRUFileListGetPathOfOldestFile(lru, uniqueKey, UniqueFilePathSize)) {
253                 break;
254             }
255             NSString *filePath = [[NSString alloc] initWithFormat:@"%@/%s", path, uniqueKey];
256             [defaultManager _webkit_removeFileOnlyAtPath:filePath];
257             [filePath release];
258             WebLRUFileListRemoveOldestFileFromList(lru);
259         }
260         [mutex unlock];
261     }
262 }
263
264 @end
265
266
267 // implementation WebFileDatabase -------------------------------------------------------------
268
269 @implementation WebFileDatabase
270
271 // creation functions ---------------------------------------------------------------------------
272 #pragma mark creation functions
273
274 +(void)_syncLoop:(id)arg
275 {
276     SetThreadPriority(MinThreadPriority);
277
278     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
279     NSPort *placeholderPort;
280
281     BEGIN_EXCEPTION_HANDLER
282
283     syncRunLoop = [NSRunLoop currentRunLoop];
284
285     while (YES) {
286         BEGIN_EXCEPTION_HANDLER
287         // we specifically use an NSRunLoop here to get autorelease pool support
288         placeholderPort = [NSPort port];
289         [syncRunLoop addPort:placeholderPort forMode:NSDefaultRunLoopMode];
290         [syncRunLoop run];
291         [syncRunLoop removePort:placeholderPort forMode:NSDefaultRunLoopMode];
292         END_EXCEPTION_HANDLER
293     }
294
295     END_EXCEPTION_HANDLER
296
297     [pool drain];
298 }
299
300 static void databaseInit()
301 {
302     // set file perms to owner read/write/execute only
303     WebFileDirectoryPOSIXPermissions = [[NSNumber numberWithInt:(WEB_UREAD | WEB_UWRITE | WEB_UEXEC)] retain];
304
305     // set file perms to owner read/write only
306     WebFilePOSIXPermissions = [[NSNumber numberWithInt:(WEB_UREAD | WEB_UWRITE)] retain];
307
308     [NSThread detachNewThreadSelector:@selector(_syncLoop:) toTarget:[WebFileDatabase class] withObject:nil];
309 }
310
311 -(id)initWithPath:(NSString *)thePath
312 {
313     pthread_once(&databaseInitControl, databaseInit);
314
315     [super initWithPath:thePath];
316     
317     if (self == nil || thePath == nil) {
318         [self release];
319         return nil;
320     }
321
322     ops = [[NSMutableArray alloc] init];
323     setCache = [[NSMutableDictionary alloc] init];
324     removeCache = [[NSMutableSet alloc] init];
325     timer = nil;
326     mutex = [[NSRecursiveLock alloc] init];
327     
328     return self;
329 }
330
331 // WebFileDatabase objects are never released, so we need no dealloc implementation.
332
333 -(void)setTimer
334 {
335     if (timer == nil) {
336         NSDate *fireDate = [[NSDate alloc] initWithTimeIntervalSinceNow:SYNC_IDLE_THRESHOLD];
337         timer = [[NSTimer alloc] initWithFireDate:fireDate interval:SYNC_IDLE_THRESHOLD target:self selector:@selector(lazySync:) userInfo:nil repeats:YES];
338         [fireDate release];
339         [syncRunLoop addTimer:timer forMode:NSDefaultRunLoopMode];
340     }
341 }
342
343 // database functions ---------------------------------------------------------------------------
344 #pragma mark database functions
345
346 -(void)setObject:(id)object forKey:(id)key
347 {
348     WebFileDatabaseOp *op;
349
350     ASSERT(object);
351     ASSERT(key);
352
353     touch = CFAbsoluteTimeGetCurrent();
354
355     LOG(FileDatabaseActivity, "%p - %@", object, key);
356     
357     [mutex lock];
358     
359     [setCache setObject:object forKey:key];
360     op = [[WebFileDatabaseOp alloc] initWithCode:WebFileDatabaseSetObjectOp key:key object:object];
361     [ops addObject:op];
362     [op release];
363     [self setTimer];
364     
365     [mutex unlock];
366 }
367
368 -(void)removeObjectForKey:(id)key
369 {
370     WebFileDatabaseOp *op;
371
372     ASSERT(key);
373
374     touch = CFAbsoluteTimeGetCurrent();
375     
376     [mutex lock];
377     
378     [removeCache addObject:key];
379     op = [[WebFileDatabaseOp alloc] initWithCode:WebFileDatabaseRemoveObjectOp key:key object:nil];
380     [ops addObject:op];
381     [op release];
382     [self setTimer];
383     
384     [mutex unlock];
385 }
386
387 -(void)removeAllObjects
388 {
389     touch = CFAbsoluteTimeGetCurrent();
390
391     [mutex lock];
392     [setCache removeAllObjects];
393     [removeCache removeAllObjects];
394     [ops removeAllObjects];
395     [self close];
396     [[NSFileManager defaultManager] _webkit_backgroundRemoveFileAtPath:path];
397     [self open];
398     [mutex unlock];
399
400     LOG(FileDatabaseActivity, "removeAllObjects");
401 }
402
403 -(id)objectForKey:(id)key
404 {
405     volatile id result;
406     
407     ASSERT(key);
408
409     touch = CFAbsoluteTimeGetCurrent();
410
411     // check caches
412     [mutex lock];
413     if ([removeCache containsObject:key]) {
414         [mutex unlock];
415         return nil;
416     }
417     if ((result = [setCache objectForKey:key])) {
418         [mutex unlock];
419         return result;
420     }
421     [mutex unlock];
422
423     // go to disk
424     char uniqueKey[UniqueFilePathSize];
425     UniqueFilePathForKey(key, uniqueKey);
426     NSString *filePath = [[NSString alloc] initWithFormat:@"%@/%s", path, uniqueKey];
427     NSData *data = [[NSData alloc] initWithContentsOfFile:filePath];
428     NSUnarchiver * volatile unarchiver = nil;
429
430     NS_DURING
431         if (data) {
432             unarchiver = [[NSUnarchiver alloc] initForReadingWithData:data];
433             if (unarchiver) {
434                 id fileKey = [unarchiver decodeObject];
435                 if ([fileKey isEqual:key]) {
436                     id object = [unarchiver decodeObject];
437                     if (object) {
438                         // Decoded objects go away when the unarchiver does, so we need to
439                         // retain this so we can return it to our caller.
440                         result = [[object retain] autorelease];
441                         if (lru) {
442                             // if we can't update the list yet, that's too bad
443                             // but not critically bad
444                             WebLRUFileListTouchFileWithPath(lru, uniqueKey);
445                         }
446                         LOG(FileDatabaseActivity, "read disk cache file - %@", key);
447                     }
448                 }
449             }
450         }
451     NS_HANDLER
452         LOG(FileDatabaseActivity, "cannot unarchive cache file - %@", key);
453         result = nil;
454     NS_ENDHANDLER
455
456     [unarchiver release];
457     [data release];
458     [filePath release];
459
460     LOG(Timing, "getting value for %@ took %f", key, (CFAbsoluteTimeGetCurrent() - touch));
461     
462     return result;
463 }
464
465 -(void)performSetObject:(id)object forKey:(id)key
466 {
467     NSString *filePath;
468     NSMutableData *data;
469     NSDictionary *attributes;
470     NSDictionary *directoryAttributes;
471     NSArchiver *archiver;
472     NSFileManager *defaultManager;
473     char uniqueKey[UniqueFilePathSize];
474     BOOL result;
475
476     ASSERT(object);
477     ASSERT(key);
478
479     UniqueFilePathForKey(key, uniqueKey);
480
481     LOG(FileDatabaseActivity, "%@ - %s", key, uniqueKey);
482
483     data = [NSMutableData data];
484     archiver = [[NSArchiver alloc] initForWritingWithMutableData:data];
485     [archiver encodeObject:key];
486     [archiver encodeObject:object];
487     
488     attributes = [NSDictionary dictionaryWithObjectsAndKeys:
489         NSUserName(), NSFileOwnerAccountName,
490         WebFilePOSIXPermissions, NSFilePosixPermissions,
491         NULL
492     ];
493
494     directoryAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
495         NSUserName(), NSFileOwnerAccountName,
496         WebFileDirectoryPOSIXPermissions, NSFilePosixPermissions,
497         NULL
498     ];
499
500     defaultManager = [NSFileManager defaultManager];
501
502     filePath = [[NSString alloc] initWithFormat:@"%@/%s", path, uniqueKey];
503     attributes = [defaultManager fileAttributesAtPath:filePath traverseLink:YES];
504
505     // update usage and truncate before writing file
506     // this has the effect of _always_ keeping disk usage under sizeLimit by clearing away space in anticipation of the write.
507     WebLRUFileListSetFileData(lru, uniqueKey, [data length], CFAbsoluteTimeGetCurrent());
508     [self _truncateToSizeLimit:[self sizeLimit]];
509
510     result = [defaultManager _webkit_createFileAtPathWithIntermediateDirectories:filePath contents:data attributes:attributes directoryAttributes:directoryAttributes];
511
512     if (!result) {
513         WebLRUFileListRemoveFileWithPath(lru, uniqueKey);
514     }
515
516     [archiver release];
517     [filePath release];    
518 }
519
520 -(void)performRemoveObjectForKey:(id)key
521 {
522     NSString *filePath;
523     char uniqueKey[UniqueFilePathSize];
524     
525     ASSERT(key);
526
527     LOG(FileDatabaseActivity, "%@", key);
528
529     UniqueFilePathForKey(key, uniqueKey);
530     filePath = [[NSString alloc] initWithFormat:@"%@/%s", path, uniqueKey];
531     [[NSFileManager defaultManager] _webkit_removeFileOnlyAtPath:filePath];
532     WebLRUFileListRemoveFileWithPath(lru, uniqueKey);
533     [filePath release];
534 }
535
536 // database management functions ---------------------------------------------------------------------------
537 #pragma mark database management functions
538
539 -(void)open
540 {
541     NSFileManager *manager;
542     NSDictionary *attributes;
543     BOOL isDir;
544     
545     if (!isOpen) {
546         manager = [NSFileManager defaultManager];
547         if ([manager fileExistsAtPath:path isDirectory:&isDir]) {
548             if (isDir) {
549                 isOpen = YES;
550             }
551         }
552         else {
553             attributes = [NSDictionary dictionaryWithObjectsAndKeys:
554                 [NSDate date], @"NSFileModificationDate",
555                 NSUserName(), @"NSFileOwnerAccountName",
556                 WebFileDirectoryPOSIXPermissions, @"NSFilePosixPermissions",
557                 NULL
558             ];
559             
560             isOpen = [manager _webkit_createDirectoryAtPathWithIntermediateDirectories:path attributes:attributes];
561         }
562
563         // remove any leftover turds
564         [manager _webkit_backgroundRemoveLeftoverFiles:path];
565         
566         if (isOpen) {
567             [NSThread detachNewThreadSelector:@selector(_createLRUList:) toTarget:self withObject:nil];
568         }
569     }
570 }
571
572 -(void)close
573 {
574     if (isOpen) {
575         isOpen = NO;
576         if (lru) {
577             WebLRUFileListRelease(lru);
578             lru = NULL;
579         }
580     }
581 }
582
583 -(void)lazySync:(NSTimer *)theTimer
584 {
585     if (!lru) {
586         // wait for lru to finish getting created        
587         return;
588     }
589
590 #ifndef NDEBUG
591     CFTimeInterval mark = CFAbsoluteTimeGetCurrent();
592 #endif
593
594     LOG(FileDatabaseActivity, ">>> BEFORE lazySync\n%@", WebLRUFileListDescription(lru));
595
596     WebFileDatabaseOp *op;
597
598     ASSERT(theTimer);
599
600     while (touch + SYNC_IDLE_THRESHOLD < CFAbsoluteTimeGetCurrent() && [ops count] > 0) {
601         [mutex lock];
602
603         if (timer) {
604             [timer invalidate];
605             [timer autorelease];
606             timer = nil;
607         }
608         
609         op = [ops lastObject];
610         if (op) {
611             [op retain];
612             [ops removeLastObject];
613             [op perform:self];
614             [setCache removeObjectForKey:[op key]];
615             [removeCache removeObject:[op key]];
616             [op release];
617         }
618
619         [mutex unlock];
620     }
621
622     // come back later to finish the work...
623     if ([ops count] > 0) {
624         [mutex lock];
625         [self setTimer];
626         [mutex unlock];
627     }
628
629 #ifndef NDEBUG
630     if (lru)
631         LOG(FileDatabaseActivity, "<<< AFTER lazySync\n%@", WebLRUFileListDescription(lru));
632
633     CFTimeInterval now = CFAbsoluteTimeGetCurrent();
634     LOG(FileDatabaseActivity, "lazySync ran in %.3f secs.", now - mark);
635 #endif
636 }
637
638 -(void)sync
639 {
640     NSArray *array;
641
642     if (!lru) {
643         // wait for lru to finish getting created        
644         return;
645     }
646
647     touch = CFAbsoluteTimeGetCurrent();
648
649     LOG(FileDatabaseActivity, ">>> BEFORE sync\n%@", WebLRUFileListDescription(lru));
650     
651     [mutex lock];
652     array = [ops copy];
653     [ops removeAllObjects];
654     [timer invalidate];
655     [timer autorelease];
656     timer = nil;
657     [setCache removeAllObjects];
658     [removeCache removeAllObjects];
659     [mutex unlock];
660
661     [array makeObjectsPerformSelector:@selector(perform:) withObject:self];
662     [array release];
663
664     LOG(FileDatabaseActivity, "<<< AFTER sync\n%@", WebLRUFileListDescription(lru));
665 }
666
667 -(unsigned)count
668 {
669     if (lru)
670         return WebLRUFileListCountItems(lru);
671     
672     return 0;
673 }
674
675 -(unsigned)usage
676 {
677     if (lru)
678         return WebLRUFileListGetTotalSize(lru);
679     
680     return 0;
681 }
682
683 -(void)setSizeLimit:(unsigned)limit
684 {
685     sizeLimit = limit;
686     if (limit < [self usage]) {
687         [self _truncateToSizeLimit:limit];
688     }
689 }
690
691 @end