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