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#include "EventNames.h" 30#include "FileSystem.h" 31#include "HTMLElement.h" 32#include "SQLiteFileSystem.h" 33#include "SQLiteStatement.h" 34#include "SQLiteTransaction.h" 35#include "SecurityOrigin.h" 36#include "StorageAreaImpl.h" 37#include "StorageSyncManager.h" 38#include "StorageTracker.h" 39#include "SuddenTermination.h" 40#include <wtf/Functional.h> 41#include <wtf/MainThread.h> 42#include <wtf/text/CString.h> 43 44namespace WebCore { 45 46// If the StorageArea undergoes rapid changes, don't sync each change to disk. 47// Instead, queue up a batch of items to sync and actually do the sync at the following interval. 48static const double StorageSyncInterval = 1.0; 49 50// A sane limit on how many items we'll schedule to sync all at once. This makes it 51// much harder to starve the rest of LocalStorage and the OS's IO subsystem in general. 52static const int MaxiumItemsToSync = 100; 53 54inline StorageAreaSync::StorageAreaSync(PassRefPtr<StorageSyncManager> storageSyncManager, PassRefPtr<StorageAreaImpl> storageArea, const String& databaseIdentifier) 55 : m_syncTimer(this, &StorageAreaSync::syncTimerFired) 56 , m_itemsCleared(false) 57 , m_finalSyncScheduled(false) 58 , m_storageArea(storageArea) 59 , m_syncManager(storageSyncManager) 60 , m_databaseIdentifier(databaseIdentifier.isolatedCopy()) 61 , m_clearItemsWhileSyncing(false) 62 , m_syncScheduled(false) 63 , m_syncInProgress(false) 64 , m_databaseOpenFailed(false) 65 , m_syncCloseDatabase(false) 66 , m_importComplete(false) 67{ 68 ASSERT(isMainThread()); 69 ASSERT(m_storageArea); 70 ASSERT(m_syncManager); 71 72 // FIXME: If it can't import, then the default WebKit behavior should be that of private browsing, 73 // not silently ignoring it. https://bugs.webkit.org/show_bug.cgi?id=25894 74 m_syncManager->dispatch(bind(&StorageAreaSync::performImport, this)); 75} 76 77PassRefPtr<StorageAreaSync> StorageAreaSync::create(PassRefPtr<StorageSyncManager> storageSyncManager, PassRefPtr<StorageAreaImpl> storageArea, const String& databaseIdentifier) 78{ 79 RefPtr<StorageAreaSync> area = adoptRef(new StorageAreaSync(storageSyncManager, storageArea, databaseIdentifier)); 80 81 return area.release(); 82} 83 84StorageAreaSync::~StorageAreaSync() 85{ 86 ASSERT(isMainThread()); 87 ASSERT(!m_syncTimer.isActive()); 88 ASSERT(m_finalSyncScheduled); 89} 90 91void 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 110 m_syncManager->dispatch(bind(&StorageAreaSync::deleteEmptyDatabase, this)); 111} 112 113void StorageAreaSync::scheduleItemForSync(const String& key, const String& value) 114{ 115 ASSERT(isMainThread()); 116 ASSERT(!m_finalSyncScheduled); 117 118 m_changedItems.set(key, value); 119 if (!m_syncTimer.isActive()) { 120 m_syncTimer.startOneShot(StorageSyncInterval); 121 122 // The following is balanced by the call to enableSuddenTermination in the 123 // syncTimerFired function. 124 disableSuddenTermination(); 125 } 126} 127 128void StorageAreaSync::scheduleClear() 129{ 130 ASSERT(isMainThread()); 131 ASSERT(!m_finalSyncScheduled); 132 133 m_changedItems.clear(); 134 m_itemsCleared = true; 135 if (!m_syncTimer.isActive()) { 136 m_syncTimer.startOneShot(StorageSyncInterval); 137 138 // The following is balanced by the call to enableSuddenTermination in the 139 // syncTimerFired function. 140 disableSuddenTermination(); 141 } 142} 143 144void StorageAreaSync::scheduleCloseDatabase() 145{ 146 ASSERT(isMainThread()); 147 ASSERT(!m_finalSyncScheduled); 148 149 if (!m_database.isOpen()) 150 return; 151 152 m_syncCloseDatabase = true; 153 154 if (!m_syncTimer.isActive()) { 155 m_syncTimer.startOneShot(StorageSyncInterval); 156 157 // The following is balanced by the call to enableSuddenTermination in the 158 // syncTimerFired function. 159 disableSuddenTermination(); 160 } 161} 162 163void StorageAreaSync::syncTimerFired(Timer<StorageAreaSync>*) 164{ 165 ASSERT(isMainThread()); 166 167 bool partialSync = false; 168 { 169 MutexLocker locker(m_syncLock); 170 171 // Do not schedule another sync if we're still trying to complete the 172 // previous one. But, if we're shutting down, schedule it anyway. 173 if (m_syncInProgress && !m_finalSyncScheduled) { 174 ASSERT(!m_syncTimer.isActive()); 175 m_syncTimer.startOneShot(StorageSyncInterval); 176 return; 177 } 178 179 if (m_itemsCleared) { 180 m_itemsPendingSync.clear(); 181 m_clearItemsWhileSyncing = true; 182 m_itemsCleared = false; 183 } 184 185 HashMap<String, String>::iterator changed_it = m_changedItems.begin(); 186 HashMap<String, String>::iterator changed_end = m_changedItems.end(); 187 for (int count = 0; changed_it != changed_end; ++count, ++changed_it) { 188 if (count >= MaxiumItemsToSync && !m_finalSyncScheduled) { 189 partialSync = true; 190 break; 191 } 192 m_itemsPendingSync.set(changed_it->key.isolatedCopy(), changed_it->value.isolatedCopy()); 193 } 194 195 if (partialSync) { 196 // We can't do the fast path of simply clearing all items, so we'll need to manually 197 // remove them one by one. Done under lock since m_itemsPendingSync is modified by 198 // the background thread. 199 HashMap<String, String>::iterator pending_it = m_itemsPendingSync.begin(); 200 HashMap<String, String>::iterator pending_end = m_itemsPendingSync.end(); 201 for (; pending_it != pending_end; ++pending_it) 202 m_changedItems.remove(pending_it->key); 203 } 204 205 if (!m_syncScheduled) { 206 m_syncScheduled = true; 207 208 // The following is balanced by the call to enableSuddenTermination in the 209 // performSync function. 210 disableSuddenTermination(); 211 212 m_syncManager->dispatch(bind(&StorageAreaSync::performSync, this)); 213 } 214 } 215 216 if (partialSync) { 217 // If we didn't finish syncing, then we need to finish the job later. 218 ASSERT(!m_syncTimer.isActive()); 219 m_syncTimer.startOneShot(StorageSyncInterval); 220 } else { 221 // The following is balanced by the calls to disableSuddenTermination in the 222 // scheduleItemForSync, scheduleClear, and scheduleFinalSync functions. 223 enableSuddenTermination(); 224 225 m_changedItems.clear(); 226 } 227} 228 229void StorageAreaSync::openDatabase(OpenDatabaseParamType openingStrategy) 230{ 231 ASSERT(!isMainThread()); 232 ASSERT(!m_database.isOpen()); 233 ASSERT(!m_databaseOpenFailed); 234 235 String databaseFilename = m_syncManager->fullDatabaseFilename(m_databaseIdentifier); 236 237 if (!fileExists(databaseFilename) && openingStrategy == SkipIfNonExistent) 238 return; 239 240 if (databaseFilename.isEmpty()) { 241 LOG_ERROR("Filename for local storage database is empty - cannot open for persistent storage"); 242 markImported(); 243 m_databaseOpenFailed = true; 244 return; 245 } 246 247 // A StorageTracker thread may have been scheduled to delete the db we're 248 // reopening, so cancel possible deletion. 249 StorageTracker::tracker().cancelDeletingOrigin(m_databaseIdentifier); 250 251 if (!m_database.open(databaseFilename)) { 252 LOG_ERROR("Failed to open database file %s for local storage", databaseFilename.utf8().data()); 253 markImported(); 254 m_databaseOpenFailed = true; 255 return; 256 } 257 258 migrateItemTableIfNeeded(); 259 260 if (!m_database.executeCommand("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)")) { 261 LOG_ERROR("Failed to create table ItemTable for local storage"); 262 markImported(); 263 m_databaseOpenFailed = true; 264 return; 265 } 266 267 StorageTracker::tracker().setOriginDetails(m_databaseIdentifier, databaseFilename); 268} 269 270void StorageAreaSync::migrateItemTableIfNeeded() 271{ 272 if (!m_database.tableExists("ItemTable")) 273 return; 274 275 { 276 SQLiteStatement query(m_database, "SELECT value FROM ItemTable LIMIT 1"); 277 // this query isn't ever executed. 278 if (query.isColumnDeclaredAsBlob(0)) 279 return; 280 } 281 282 // alter table for backward compliance, change the value type from TEXT to BLOB. 283 static const char* commands[] = { 284 "DROP TABLE IF EXISTS ItemTable2", 285 "CREATE TABLE ItemTable2 (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)", 286 "INSERT INTO ItemTable2 SELECT * from ItemTable", 287 "DROP TABLE ItemTable", 288 "ALTER TABLE ItemTable2 RENAME TO ItemTable", 289 0, 290 }; 291 292 SQLiteTransaction transaction(m_database, false); 293 transaction.begin(); 294 for (size_t i = 0; commands[i]; ++i) { 295 if (!m_database.executeCommand(commands[i])) { 296 LOG_ERROR("Failed to migrate table ItemTable for local storage when executing: %s", commands[i]); 297 transaction.rollback(); 298 299 // finally it will try to keep a backup of ItemTable for the future restoration. 300 // NOTICE: this will essentially DELETE the current database, but that's better 301 // than continually hitting this case and never being able to use the local storage. 302 // if this is ever hit, it's definitely a bug. 303 ASSERT_NOT_REACHED(); 304 if (!m_database.executeCommand("ALTER TABLE ItemTable RENAME TO Backup_ItemTable")) 305 LOG_ERROR("Failed to save ItemTable after migration job failed."); 306 307 return; 308 } 309 } 310 transaction.commit(); 311} 312 313void StorageAreaSync::performImport() 314{ 315 ASSERT(!isMainThread()); 316 ASSERT(!m_database.isOpen()); 317 318 openDatabase(SkipIfNonExistent); 319 if (!m_database.isOpen()) { 320 markImported(); 321 return; 322 } 323 324 SQLiteStatement query(m_database, "SELECT key, value FROM ItemTable"); 325 if (query.prepare() != SQLResultOk) { 326 LOG_ERROR("Unable to select items from ItemTable for local storage"); 327 markImported(); 328 return; 329 } 330 331 HashMap<String, String> itemMap; 332 333 int result = query.step(); 334 while (result == SQLResultRow) { 335 itemMap.set(query.getColumnText(0), query.getColumnBlobAsString(1)); 336 result = query.step(); 337 } 338 339 if (result != SQLResultDone) { 340 LOG_ERROR("Error reading items from ItemTable for local storage"); 341 markImported(); 342 return; 343 } 344 345 m_storageArea->importItems(itemMap); 346 347 markImported(); 348} 349 350void StorageAreaSync::markImported() 351{ 352 MutexLocker locker(m_importLock); 353 m_importComplete = true; 354 m_importCondition.signal(); 355} 356 357// FIXME: In the future, we should allow use of StorageAreas while it's importing (when safe to do so). 358// Blocking everything until the import is complete is by far the simplest and safest thing to do, but 359// there is certainly room for safe optimization: Key/length will never be able to make use of such an 360// optimization (since the order of iteration can change as items are being added). Get can return any 361// item currently in the map. Get/remove can work whether or not it's in the map, but we'll need a list 362// of items the import should not overwrite. Clear can also work, but it'll need to kill the import 363// job first. 364void StorageAreaSync::blockUntilImportComplete() 365{ 366 ASSERT(isMainThread()); 367 368 // Fast path. We set m_storageArea to 0 only after m_importComplete being true. 369 if (!m_storageArea) 370 return; 371 372 MutexLocker locker(m_importLock); 373 while (!m_importComplete) 374 m_importCondition.wait(m_importLock); 375 m_storageArea = 0; 376} 377 378void StorageAreaSync::sync(bool clearItems, const HashMap<String, String>& items) 379{ 380 ASSERT(!isMainThread()); 381 382 if (items.isEmpty() && !clearItems && !m_syncCloseDatabase) 383 return; 384 if (m_databaseOpenFailed) 385 return; 386 387 if (!m_database.isOpen() && m_syncCloseDatabase) { 388 m_syncCloseDatabase = false; 389 return; 390 } 391 392 if (!m_database.isOpen()) 393 openDatabase(CreateIfNonExistent); 394 if (!m_database.isOpen()) 395 return; 396 397 // Closing this db because it is about to be deleted by StorageTracker. 398 // The delete will be cancelled if StorageAreaSync needs to reopen the db 399 // to write new items created after the request to delete the db. 400 if (m_syncCloseDatabase) { 401 m_syncCloseDatabase = false; 402 m_database.close(); 403 return; 404 } 405 406 // If the clear flag is set, then we clear all items out before we write any new ones in. 407 if (clearItems) { 408 SQLiteStatement clear(m_database, "DELETE FROM ItemTable"); 409 if (clear.prepare() != SQLResultOk) { 410 LOG_ERROR("Failed to prepare clear statement - cannot write to local storage database"); 411 return; 412 } 413 414 int result = clear.step(); 415 if (result != SQLResultDone) { 416 LOG_ERROR("Failed to clear all items in the local storage database - %i", result); 417 return; 418 } 419 } 420 421 SQLiteStatement insert(m_database, "INSERT INTO ItemTable VALUES (?, ?)"); 422 if (insert.prepare() != SQLResultOk) { 423 LOG_ERROR("Failed to prepare insert statement - cannot write to local storage database"); 424 return; 425 } 426 427 SQLiteStatement remove(m_database, "DELETE FROM ItemTable WHERE key=?"); 428 if (remove.prepare() != SQLResultOk) { 429 LOG_ERROR("Failed to prepare delete statement - cannot write to local storage database"); 430 return; 431 } 432 433 HashMap<String, String>::const_iterator end = items.end(); 434 435 SQLiteTransaction transaction(m_database); 436 transaction.begin(); 437 for (HashMap<String, String>::const_iterator it = items.begin(); it != end; ++it) { 438 // Based on the null-ness of the second argument, decide whether this is an insert or a delete. 439 SQLiteStatement& query = it->value.isNull() ? remove : insert; 440 441 query.bindText(1, it->key); 442 443 // If the second argument is non-null, we're doing an insert, so bind it as the value. 444 if (!it->value.isNull()) 445 query.bindBlob(2, it->value); 446 447 int result = query.step(); 448 if (result != SQLResultDone) { 449 LOG_ERROR("Failed to update item in the local storage database - %i", result); 450 break; 451 } 452 453 query.reset(); 454 } 455 transaction.commit(); 456} 457 458void StorageAreaSync::performSync() 459{ 460 ASSERT(!isMainThread()); 461 462 bool clearItems; 463 HashMap<String, String> items; 464 { 465 MutexLocker locker(m_syncLock); 466 467 ASSERT(m_syncScheduled); 468 469 clearItems = m_clearItemsWhileSyncing; 470 m_itemsPendingSync.swap(items); 471 472 m_clearItemsWhileSyncing = false; 473 m_syncScheduled = false; 474 m_syncInProgress = true; 475 } 476 477 sync(clearItems, items); 478 479 { 480 MutexLocker locker(m_syncLock); 481 m_syncInProgress = false; 482 } 483 484 // The following is balanced by the call to disableSuddenTermination in the 485 // syncTimerFired function. 486 enableSuddenTermination(); 487} 488 489void StorageAreaSync::deleteEmptyDatabase() 490{ 491 ASSERT(!isMainThread()); 492 if (!m_database.isOpen()) 493 return; 494 495 SQLiteStatement query(m_database, "SELECT COUNT(*) FROM ItemTable"); 496 if (query.prepare() != SQLResultOk) { 497 LOG_ERROR("Unable to count number of rows in ItemTable for local storage"); 498 return; 499 } 500 501 int result = query.step(); 502 if (result != SQLResultRow) { 503 LOG_ERROR("No results when counting number of rows in ItemTable for local storage"); 504 return; 505 } 506 507 int count = query.getColumnInt(0); 508 if (!count) { 509 query.finalize(); 510 m_database.close(); 511 if (StorageTracker::tracker().isActive()) 512 StorageTracker::tracker().deleteOriginWithIdentifier(m_databaseIdentifier); 513 else { 514 String databaseFilename = m_syncManager->fullDatabaseFilename(m_databaseIdentifier); 515 if (!SQLiteFileSystem::deleteDatabaseFile(databaseFilename)) 516 LOG_ERROR("Failed to delete database file %s\n", databaseFilename.utf8().data()); 517 } 518 } 519} 520 521void StorageAreaSync::scheduleSync() 522{ 523 syncTimerFired(&m_syncTimer); 524} 525 526} // namespace WebCore 527