1 /* 2 * Copyright (C) 2008, 2009, 2010 Apple 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 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY 14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR 17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26 #include "config.h" 27 #include "StorageAreaSync.h" 28 29 #if ENABLE(DOM_STORAGE) 30 31 #include "EventNames.h" 32 #include "FileSystem.h" 33 #include "HTMLElement.h" 34 #include "SQLiteFileSystem.h" 35 #include "SQLiteStatement.h" 36 #include "SecurityOrigin.h" 37 #include "StorageAreaImpl.h" 38 #include "StorageSyncManager.h" 39 #include "StorageTracker.h" 40 #include "SuddenTermination.h" 41 #include <wtf/text/CString.h> 42 43 namespace WebCore { 44 45 // If the StorageArea undergoes rapid changes, don't sync each change to disk. 46 // Instead, queue up a batch of items to sync and actually do the sync at the following interval. 47 static const double StorageSyncInterval = 1.0; 48 49 // A sane limit on how many items we'll schedule to sync all at once. This makes it 50 // much harder to starve the rest of LocalStorage and the OS's IO subsystem in general. 51 static const int MaxiumItemsToSync = 100; 52 53 inline StorageAreaSync::StorageAreaSync(PassRefPtr<StorageSyncManager> storageSyncManager, PassRefPtr<StorageAreaImpl> storageArea, const String& databaseIdentifier) 54 : m_syncTimer(this, &StorageAreaSync::syncTimerFired) 55 , m_itemsCleared(false) 56 , m_finalSyncScheduled(false) 57 , m_storageArea(storageArea) 58 , m_syncManager(storageSyncManager) 59 , m_databaseIdentifier(databaseIdentifier.crossThreadString()) 60 , m_clearItemsWhileSyncing(false) 61 , m_syncScheduled(false) 62 , m_syncInProgress(false) 63 , m_databaseOpenFailed(false) 64 , m_syncCloseDatabase(false) 65 , m_importComplete(false) 66 { 67 ASSERT(isMainThread()); 68 ASSERT(m_storageArea); 69 ASSERT(m_syncManager); 70 } 71 72 PassRefPtr<StorageAreaSync> StorageAreaSync::create(PassRefPtr<StorageSyncManager> storageSyncManager, PassRefPtr<StorageAreaImpl> storageArea, const String& databaseIdentifier) 73 { 74 RefPtr<StorageAreaSync> area = adoptRef(new StorageAreaSync(storageSyncManager, storageArea, databaseIdentifier)); 75 76 // FIXME: If it can't import, then the default WebKit behavior should be that of private browsing, 77 // not silently ignoring it. https://bugs.webkit.org/show_bug.cgi?id=25894 78 if (!area->m_syncManager->scheduleImport(area.get())) 79 area->m_importComplete = true; 80 81 return area.release(); 82 } 83 84 StorageAreaSync::~StorageAreaSync() 85 { 86 ASSERT(isMainThread()); 87 ASSERT(!m_syncTimer.isActive()); 88 ASSERT(m_finalSyncScheduled); 89 } 90 91 void StorageAreaSync::scheduleFinalSync() 92 { 93 ASSERT(isMainThread()); 94 // FIXME: We do this to avoid races, but it'd be better to make things safe without blocking. 95 blockUntilImportComplete(); 96 m_storageArea = 0; // This is done in blockUntilImportComplete() but this is here as a form of documentation that we must be absolutely sure the ref count cycle is broken. 97 98 if (m_syncTimer.isActive()) 99 m_syncTimer.stop(); 100 else { 101 // The following is balanced by the call to enableSuddenTermination in the 102 // syncTimerFired function. 103 disableSuddenTermination(); 104 } 105 // FIXME: This is synchronous. We should do it on the background process, but 106 // we should do it safely. 107 m_finalSyncScheduled = true; 108 syncTimerFired(&m_syncTimer); 109 m_syncManager->scheduleDeleteEmptyDatabase(this); 110 } 111 112 void StorageAreaSync::scheduleItemForSync(const String& key, const String& value) 113 { 114 ASSERT(isMainThread()); 115 ASSERT(!m_finalSyncScheduled); 116 117 m_changedItems.set(key, value); 118 if (!m_syncTimer.isActive()) { 119 m_syncTimer.startOneShot(StorageSyncInterval); 120 121 // The following is balanced by the call to enableSuddenTermination in the 122 // syncTimerFired function. 123 disableSuddenTermination(); 124 } 125 } 126 127 void StorageAreaSync::scheduleClear() 128 { 129 ASSERT(isMainThread()); 130 ASSERT(!m_finalSyncScheduled); 131 132 m_changedItems.clear(); 133 m_itemsCleared = true; 134 if (!m_syncTimer.isActive()) { 135 m_syncTimer.startOneShot(StorageSyncInterval); 136 137 // The following is balanced by the call to enableSuddenTermination in the 138 // syncTimerFired function. 139 disableSuddenTermination(); 140 } 141 } 142 143 void StorageAreaSync::scheduleCloseDatabase() 144 { 145 ASSERT(isMainThread()); 146 ASSERT(!m_finalSyncScheduled); 147 148 if (!m_database.isOpen()) 149 return; 150 151 m_syncCloseDatabase = true; 152 153 if (!m_syncTimer.isActive()) { 154 m_syncTimer.startOneShot(StorageSyncInterval); 155 156 // The following is balanced by the call to enableSuddenTermination in the 157 // syncTimerFired function. 158 disableSuddenTermination(); 159 } 160 } 161 162 void StorageAreaSync::syncTimerFired(Timer<StorageAreaSync>*) 163 { 164 ASSERT(isMainThread()); 165 166 bool partialSync = false; 167 { 168 MutexLocker locker(m_syncLock); 169 170 // Do not schedule another sync if we're still trying to complete the 171 // previous one. But, if we're shutting down, schedule it anyway. 172 if (m_syncInProgress && !m_finalSyncScheduled) { 173 ASSERT(!m_syncTimer.isActive()); 174 m_syncTimer.startOneShot(StorageSyncInterval); 175 return; 176 } 177 178 if (m_itemsCleared) { 179 m_itemsPendingSync.clear(); 180 m_clearItemsWhileSyncing = true; 181 m_itemsCleared = false; 182 } 183 184 HashMap<String, String>::iterator changed_it = m_changedItems.begin(); 185 HashMap<String, String>::iterator changed_end = m_changedItems.end(); 186 for (int count = 0; changed_it != changed_end; ++count, ++changed_it) { 187 if (count >= MaxiumItemsToSync && !m_finalSyncScheduled) { 188 partialSync = true; 189 break; 190 } 191 m_itemsPendingSync.set(changed_it->first.crossThreadString(), changed_it->second.crossThreadString()); 192 } 193 194 if (partialSync) { 195 // We can't do the fast path of simply clearing all items, so we'll need to manually 196 // remove them one by one. Done under lock since m_itemsPendingSync is modified by 197 // the background thread. 198 HashMap<String, String>::iterator pending_it = m_itemsPendingSync.begin(); 199 HashMap<String, String>::iterator pending_end = m_itemsPendingSync.end(); 200 for (; pending_it != pending_end; ++pending_it) 201 m_changedItems.remove(pending_it->first); 202 } 203 204 if (!m_syncScheduled) { 205 m_syncScheduled = true; 206 207 // The following is balanced by the call to enableSuddenTermination in the 208 // performSync function. 209 disableSuddenTermination(); 210 211 m_syncManager->scheduleSync(this); 212 } 213 } 214 215 if (partialSync) { 216 // If we didn't finish syncing, then we need to finish the job later. 217 ASSERT(!m_syncTimer.isActive()); 218 m_syncTimer.startOneShot(StorageSyncInterval); 219 } else { 220 // The following is balanced by the calls to disableSuddenTermination in the 221 // scheduleItemForSync, scheduleClear, and scheduleFinalSync functions. 222 enableSuddenTermination(); 223 224 m_changedItems.clear(); 225 } 226 } 227 228 void StorageAreaSync::openDatabase(OpenDatabaseParamType openingStrategy) 229 { 230 ASSERT(!isMainThread()); 231 ASSERT(!m_database.isOpen()); 232 ASSERT(!m_databaseOpenFailed); 233 234 String databaseFilename = m_syncManager->fullDatabaseFilename(m_databaseIdentifier); 235 236 if (!fileExists(databaseFilename) && openingStrategy == SkipIfNonExistent) 237 return; 238 239 if (databaseFilename.isEmpty()) { 240 LOG_ERROR("Filename for local storage database is empty - cannot open for persistent storage"); 241 markImported(); 242 m_databaseOpenFailed = true; 243 return; 244 } 245 246 // A StorageTracker thread may have been scheduled to delete the db we're 247 // reopening, so cancel possible deletion. 248 StorageTracker::tracker().cancelDeletingOrigin(m_databaseIdentifier); 249 250 if (!m_database.open(databaseFilename)) { 251 LOG_ERROR("Failed to open database file %s for local storage", databaseFilename.utf8().data()); 252 markImported(); 253 m_databaseOpenFailed = true; 254 return; 255 } 256 257 if (!m_database.executeCommand("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value TEXT NOT NULL ON CONFLICT FAIL)")) { 258 LOG_ERROR("Failed to create table ItemTable for local storage"); 259 markImported(); 260 m_databaseOpenFailed = true; 261 return; 262 } 263 264 StorageTracker::tracker().setOriginDetails(m_databaseIdentifier, databaseFilename); 265 } 266 267 void StorageAreaSync::performImport() 268 { 269 ASSERT(!isMainThread()); 270 ASSERT(!m_database.isOpen()); 271 272 openDatabase(SkipIfNonExistent); 273 if (!m_database.isOpen()) { 274 markImported(); 275 return; 276 } 277 278 SQLiteStatement query(m_database, "SELECT key, value FROM ItemTable"); 279 if (query.prepare() != SQLResultOk) { 280 LOG_ERROR("Unable to select items from ItemTable for local storage"); 281 markImported(); 282 return; 283 } 284 285 HashMap<String, String> itemMap; 286 287 int result = query.step(); 288 while (result == SQLResultRow) { 289 itemMap.set(query.getColumnText(0), query.getColumnText(1)); 290 result = query.step(); 291 } 292 293 if (result != SQLResultDone) { 294 LOG_ERROR("Error reading items from ItemTable for local storage"); 295 markImported(); 296 return; 297 } 298 299 HashMap<String, String>::iterator it = itemMap.begin(); 300 HashMap<String, String>::iterator end = itemMap.end(); 301 302 for (; it != end; ++it) 303 m_storageArea->importItem(it->first, it->second); 304 305 markImported(); 306 } 307 308 void StorageAreaSync::markImported() 309 { 310 MutexLocker locker(m_importLock); 311 m_importComplete = true; 312 m_importCondition.signal(); 313 } 314 315 // FIXME: In the future, we should allow use of StorageAreas while it's importing (when safe to do so). 316 // Blocking everything until the import is complete is by far the simplest and safest thing to do, but 317 // there is certainly room for safe optimization: Key/length will never be able to make use of such an 318 // optimization (since the order of iteration can change as items are being added). Get can return any 319 // item currently in the map. Get/remove can work whether or not it's in the map, but we'll need a list 320 // of items the import should not overwrite. Clear can also work, but it'll need to kill the import 321 // job first. 322 void StorageAreaSync::blockUntilImportComplete() 323 { 324 ASSERT(isMainThread()); 325 326 // Fast path. We set m_storageArea to 0 only after m_importComplete being true. 327 if (!m_storageArea) 328 return; 329 330 MutexLocker locker(m_importLock); 331 while (!m_importComplete) 332 m_importCondition.wait(m_importLock); 333 m_storageArea = 0; 334 } 335 336 void StorageAreaSync::sync(bool clearItems, const HashMap<String, String>& items) 337 { 338 ASSERT(!isMainThread()); 339 340 if (items.isEmpty() && !clearItems) 341 return; 342 if (m_databaseOpenFailed) 343 return; 344 if (!m_database.isOpen()) 345 openDatabase(CreateIfNonExistent); 346 if (!m_database.isOpen()) 347 return; 348 349 // Closing this db because it is about to be deleted by StorageTracker. 350 // The delete will be cancelled if StorageAreaSync needs to reopen the db 351 // to write new items created after the request to delete the db. 352 if (m_syncCloseDatabase) { 353 m_syncCloseDatabase = false; 354 m_database.close(); 355 return; 356 } 357 358 // If the clear flag is set, then we clear all items out before we write any new ones in. 359 if (clearItems) { 360 SQLiteStatement clear(m_database, "DELETE FROM ItemTable"); 361 if (clear.prepare() != SQLResultOk) { 362 LOG_ERROR("Failed to prepare clear statement - cannot write to local storage database"); 363 return; 364 } 365 366 int result = clear.step(); 367 if (result != SQLResultDone) { 368 LOG_ERROR("Failed to clear all items in the local storage database - %i", result); 369 return; 370 } 371 } 372 373 SQLiteStatement insert(m_database, "INSERT INTO ItemTable VALUES (?, ?)"); 374 if (insert.prepare() != SQLResultOk) { 375 LOG_ERROR("Failed to prepare insert statement - cannot write to local storage database"); 376 return; 377 } 378 379 SQLiteStatement remove(m_database, "DELETE FROM ItemTable WHERE key=?"); 380 if (remove.prepare() != SQLResultOk) { 381 LOG_ERROR("Failed to prepare delete statement - cannot write to local storage database"); 382 return; 383 } 384 385 HashMap<String, String>::const_iterator end = items.end(); 386 387 for (HashMap<String, String>::const_iterator it = items.begin(); it != end; ++it) { 388 // Based on the null-ness of the second argument, decide whether this is an insert or a delete. 389 SQLiteStatement& query = it->second.isNull() ? remove : insert; 390 391 query.bindText(1, it->first); 392 393 // If the second argument is non-null, we're doing an insert, so bind it as the value. 394 if (!it->second.isNull()) 395 query.bindText(2, it->second); 396 397 int result = query.step(); 398 if (result != SQLResultDone) { 399 LOG_ERROR("Failed to update item in the local storage database - %i", result); 400 break; 401 } 402 403 query.reset(); 404 } 405 } 406 407 void StorageAreaSync::performSync() 408 { 409 ASSERT(!isMainThread()); 410 411 bool clearItems; 412 HashMap<String, String> items; 413 { 414 MutexLocker locker(m_syncLock); 415 416 ASSERT(m_syncScheduled); 417 418 clearItems = m_clearItemsWhileSyncing; 419 m_itemsPendingSync.swap(items); 420 421 m_clearItemsWhileSyncing = false; 422 m_syncScheduled = false; 423 m_syncInProgress = true; 424 } 425 426 sync(clearItems, items); 427 428 { 429 MutexLocker locker(m_syncLock); 430 m_syncInProgress = false; 431 } 432 433 // The following is balanced by the call to disableSuddenTermination in the 434 // syncTimerFired function. 435 enableSuddenTermination(); 436 } 437 438 void StorageAreaSync::deleteEmptyDatabase() 439 { 440 ASSERT(!isMainThread()); 441 if (!m_database.isOpen()) 442 return; 443 444 SQLiteStatement query(m_database, "SELECT COUNT(*) FROM ItemTable"); 445 if (query.prepare() != SQLResultOk) { 446 LOG_ERROR("Unable to count number of rows in ItemTable for local storage"); 447 return; 448 } 449 450 int result = query.step(); 451 if (result != SQLResultRow) { 452 LOG_ERROR("No results when counting number of rows in ItemTable for local storage"); 453 return; 454 } 455 456 int count = query.getColumnInt(0); 457 if (!count) { 458 query.finalize(); 459 m_database.close(); 460 if (StorageTracker::tracker().isActive()) 461 StorageTracker::tracker().deleteOrigin(m_databaseIdentifier); 462 else { 463 String databaseFilename = m_syncManager->fullDatabaseFilename(m_databaseIdentifier); 464 if (!SQLiteFileSystem::deleteDatabaseFile(databaseFilename)) 465 LOG_ERROR("Failed to delete database file %s\n", databaseFilename.utf8().data()); 466 } 467 } 468 } 469 470 void StorageAreaSync::scheduleSync() 471 { 472 syncTimerFired(&m_syncTimer); 473 } 474 475 } // namespace WebCore 476 477 #endif // ENABLE(DOM_STORAGE) 478