diff --git a/src/vs/base/node/storage.ts b/src/vs/base/node/storage.ts index c77c674e964..0ef368833a7 100644 --- a/src/vs/base/node/storage.ts +++ b/src/vs/base/node/storage.ts @@ -6,7 +6,7 @@ import { Database, Statement } from 'vscode-sqlite3'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; -import { ThrottledDelayer, timeout } from 'vs/base/common/async'; +import { ThrottledDelayer, timeout, always } from 'vs/base/common/async'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { mapToString, setToString } from 'vs/base/common/map'; import { basename } from 'path'; @@ -411,26 +411,38 @@ export class SQLiteStorageDatabase implements IStorageDatabase { return this.whenOpened.then(result => { return new Promise((resolve, reject) => { - result.db.close(error => { - if (error) { - this.handleSQLiteError(error, `[storage ${this.name}] close(): ${error}`); + result.db.close(closeError => { + if (closeError) { + this.handleSQLiteError(closeError, `[storage ${this.name}] close(): ${closeError}`); + } - return reject(error); + if (result.path === SQLiteStorageDatabase.IN_MEMORY_PATH) { + return resolve(); // return early for in-memory DBs + } + + if (this.isCorrupt) { + // If the DB is corrupt, make sure to rename the file so that we can start + // from a fresh DB or a previous backup on the next startup and not be stuck + // with a corrupt DB for ever. + this.logger.error(`[storage ${this.name}] close(): removing corrupt DB and trying to restore backup`); + + return always(rename(result.path, this.toCorruptPath(result.path)) + .then(() => rename(this.toBackupPath(result.path), result.path)), () => closeError ? reject(closeError) : resolve()); + } + + if (closeError) { + return reject(closeError); } // If the DB closed successfully and we are not running in-memory // and the DB did not get corrupted during runtime, make a backup // of the DB so that we can use it as fallback in case the actual // DB becomes corrupt. - if (result.path !== SQLiteStorageDatabase.IN_MEMORY_PATH && !this.isCorrupt) { - return this.backup(result).then(resolve, error => { - this.logger.error(`[storage ${this.name}] backup(): ${error}`); + return this.backup(result).then(resolve, error => { + this.logger.error(`[storage ${this.name}] backup(): ${error}`); - return resolve(); // ignore failing backup - }); - } - - return resolve(); + return resolve(); // ignore failing backup + }); }); }); }); @@ -512,6 +524,14 @@ export class SQLiteStorageDatabase implements IStorageDatabase { .then(() => this.doOpen(path)); } + private handleSQLiteError(error: Error & { code?: string }, msg: string): void { + if (error.code === 'SQLITE_CORRUPT' || error.code === 'SQLITE_NOTADB') { + this.isCorrupt = true; + } + + this.logger.error(msg); + } + private toCorruptPath(path: string): string { const randomSuffix = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 4); @@ -652,14 +672,6 @@ export class SQLiteStorageDatabase implements IStorageDatabase { stmt.removeListener('error', statementErrorListener); }); } - - private handleSQLiteError(error: Error & { code?: string }, msg: string): void { - if (error.code === 'SQLITE_CORRUPT' || error.code === 'SQLITE_NOTADB') { - this.isCorrupt = true; - } - - this.logger.error(msg); - } } class SQLiteStorageDatabaseLogger { diff --git a/src/vs/base/test/node/storage/storage.test.ts b/src/vs/base/test/node/storage/storage.test.ts index ab5e7124249..603d22ea0e8 100644 --- a/src/vs/base/test/node/storage/storage.test.ts +++ b/src/vs/base/test/node/storage/storage.test.ts @@ -8,7 +8,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { join } from 'path'; import { tmpdir } from 'os'; import { equal, ok } from 'assert'; -import { mkdirp, del, writeFile } from 'vs/base/node/pfs'; +import { mkdirp, del, writeFile, exists } from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import { isWindows } from 'vs/base/common/platform'; @@ -427,7 +427,7 @@ suite('SQLite Storage Library', () => { await del(storageDir, tmpdir()); }); - test('basics (corrupt DB does not backup)', async () => { + test('basics (DB that becomes corrupt during runtime restores backup on close())', async () => { if (isWindows) { await Promise.resolve(); // Windows will fail to write to open DB due to locking @@ -449,6 +449,8 @@ suite('SQLite Storage Library', () => { await storage.updateItems({ insert: items }); await storage.close(); + equal(await exists(`${storagePath}.backup`), true); + storage = new SQLiteStorageDatabase(storagePath); await storage.getItems(); @@ -462,6 +464,8 @@ suite('SQLite Storage Library', () => { await storage.close(); + equal(await exists(`${storagePath}.backup`), false); + storage = new SQLiteStorageDatabase(storagePath); const storedItems = await storage.getItems();