tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 59c2bad9f0fb05ef777ff974e7bbac6fd622bafb
parent 5d163df8ca6fa596e244440f2721becc238efa80
Author: Chris DuPuis <cdupuis@mozilla.com>
Date:   Fri,  7 Nov 2025 17:40:24 +0000

Bug 1997090 - reverse order of delete and create backup r=hsohaney

When there is reason to automatically regenerate the backup, the current behavior is to first delete old backup and then create a new one. This leaves a period where there is no backup file in existence, while the new backup is being generated.

To avoid leaving the user without a backup file, switch the order, so we first create the new backup file and then delete the old one.

Differential Revision: https://phabricator.services.mozilla.com/D271509

Diffstat:
Mbrowser/components/backup/BackupService.sys.mjs | 76++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mbrowser/components/backup/tests/xpcshell/test_BackupService_regeneration.js | 19+++----------------
Mbrowser/components/backup/tests/xpcshell/test_BackupService_retryHeuristic.js | 6+++---
3 files changed, 58 insertions(+), 43 deletions(-)

diff --git a/browser/components/backup/BackupService.sys.mjs b/browser/components/backup/BackupService.sys.mjs @@ -1195,16 +1195,10 @@ export class BackupService extends EventTarget { this.#postRecoveryResolver = resolve; this.#backupWriteAbortController = new AbortController(); this.#regenerationDebouncer = new lazy.DeferredTask(async () => { - if ( - !this.#backupWriteAbortController.signal.aborted && - this.archiveEnabledStatus.enabled - ) { - await this.deleteLastBackup(); - if (lazy.scheduledBackupsPref) { - await this.createBackupOnIdleDispatch({ - reason: "user deleted some data", - }); - } + if (!this.#backupWriteAbortController.signal.aborted) { + await this.createBackupOnIdleDispatch({ + reason: "user deleted some data", + }); } }, BackupService.REGENERATION_DEBOUNCE_RATE_MS); } @@ -4046,7 +4040,7 @@ export class BackupService extends EventTarget { * not been sent to the application for at least * IDLE_THRESHOLD_SECONDS_PREF_NAME seconds. */ - onIdle() { + async onIdle() { lazy.logConsole.debug("Saw idle callback"); if (!this.#takenMeasurements) { this.takeMeasurements(); @@ -4092,12 +4086,19 @@ export class BackupService extends EventTarget { // loop in the parent process isn't so busy with higher priority things. let expectedBackupTime = lastBackupDate + lazy.minimumTimeBetweenBackupsSeconds; - this.createBackupOnIdleDispatch({ - reason: - expectedBackupTime < this._startupTimeUnixSeconds - ? "missed" - : "idle", - }); + try { + await this.createBackupOnIdleDispatch({ + reason: + expectedBackupTime < this._startupTimeUnixSeconds + ? "missed" + : "idle", + }); + } catch (e) { + lazy.logConsole.error( + "createBackupOnIdleDispatch promise rejected", + e + ); + } } else { lazy.logConsole.debug( "Last backup was too recent. Not creating one for now." @@ -4121,10 +4122,11 @@ export class BackupService extends EventTarget { * is not busy with higher priority events. This is intentionally broken out * into its own method to make it easier to stub out in tests. * - * @param {...*} args - * Arguments to pass through to createBackup. + * @param {object} [options] + * @param {boolean} [options.deletePreviousBackup] + * @param {string} [options.reason] */ - createBackupOnIdleDispatch(...args) { + createBackupOnIdleDispatch({ deletePreviousBackup = true, reason }) { let now = Math.floor(Date.now() / 1000); let errorStateDebugInfo = Services.prefs.getStringPref( BACKUP_DEBUG_INFO_PREF_NAME, @@ -4143,15 +4145,27 @@ export class BackupService extends EventTarget { lazy.logConsole.debug( `We've already retried in the last ${lazy.minimumTimeBetweenBackupsSeconds}s. Waiting for next valid idleDispatch to try again.` ); - return; + return Promise.resolve(); } + // Determine path to old backup file + const oldBackupFile = this.#_state.lastBackupFileName; + const isScheduledBackupsEnabled = lazy.scheduledBackupsPref; - ChromeUtils.idleDispatch(() => { + let { backupPromise, resolve } = Promise.withResolvers(); + ChromeUtils.idleDispatch(async () => { lazy.logConsole.debug( "idleDispatch fired. Attempting to create a backup." ); + let oldBackupFilePath; + if (await this.#infalliblePathExists(lazy.backupDirPref)) { + oldBackupFilePath = PathUtils.join(lazy.backupDirPref, oldBackupFile); + } - this.createBackup(...args).catch(e => { + try { + if (isScheduledBackupsEnabled) { + await this.createBackup({ reason }); + } + } catch (e) { lazy.logConsole.debug( `There was an error creating backup on idle dispatch: ${e}` ); @@ -4162,8 +4176,22 @@ export class BackupService extends EventTarget { Services.prefs.setBoolPref(DISABLED_ON_IDLE_RETRY_PREF_NAME, true); Glean.browserBackup.backupThrottled.record(); } - }); + } finally { + // Now delete the old backup file, if it exists + if (deletePreviousBackup && oldBackupFilePath) { + lazy.logConsole.log( + "Attempting to delete last backup file at ", + oldBackupFilePath + ); + await IOUtils.remove(oldBackupFilePath, { + ignoreAbsent: true, + retryReadonly: true, + }); + resolve(); + } + } }); + return backupPromise; } /** diff --git a/browser/components/backup/tests/xpcshell/test_BackupService_regeneration.js b/browser/components/backup/tests/xpcshell/test_BackupService_regeneration.js @@ -129,16 +129,8 @@ async function expectRegeneration(taskFn, msg) { let bs = new BackupService(); - // Now we set up some stubs on the BackupService to detect calls to - // deleteLastBackup and createbackupOnIdleDispatch, which are both called - // on regeneration. - let deleteDeferred = Promise.withResolvers(); - sandbox.stub(bs, "deleteLastBackup").callsFake(() => { - Assert.ok(true, "Saw deleteLastBackup call"); - deleteDeferred.resolve(); - return Promise.resolve(); - }); - + // Now we set up a stub on the BackupService to detect calls to + // createbackupOnIdleDispatch let createBackupDeferred = Promise.withResolvers(); sandbox.stub(bs, "createBackupOnIdleDispatch").callsFake(options => { Assert.ok(true, "Saw createBackupOnIdleDispatch call"); @@ -162,11 +154,6 @@ async function expectRegeneration(taskFn, msg) { await taskFn(); - let regenerationPromises = [ - deleteDeferred.promise, - createBackupDeferred.promise, - ]; - // We'll wait for 1 second before considering the regeneration a bust. let timeoutPromise = new Promise((resolve, reject) => // eslint-disable-next-line mozilla/no-arbitrary-setTimeout @@ -176,7 +163,7 @@ async function expectRegeneration(taskFn, msg) { ); try { - await Promise.race([Promise.all(regenerationPromises), timeoutPromise]); + await Promise.race([createBackupDeferred.promise, timeoutPromise]); Assert.ok(true, msg); } catch (e) { Assert.ok(false, "Timed out waiting for regeneration."); diff --git a/browser/components/backup/tests/xpcshell/test_BackupService_retryHeuristic.js b/browser/components/backup/tests/xpcshell/test_BackupService_retryHeuristic.js @@ -81,7 +81,7 @@ add_task(async function test_retry_limit() { // ensure that there is no error code set Services.prefs.setIntPref(BACKUP_ERROR_CODE_PREF_NAME, ERRORS.NONE); - bs.createBackupOnIdleDispatch(); + bs.createBackupOnIdleDispatch({}); // #backupInProgress is set to true await bsInProgressStateUpdate(bs, true); @@ -121,7 +121,7 @@ add_task(async function test_retry_limit() { // check if it switched to no longer creating backups on idle const previousCalls = bs.createBackup.callCount; - bs.createBackupOnIdleDispatch(); + bs.createBackupOnIdleDispatch({}); // wait a tick for the pref to update await new Promise(executeSoon); @@ -148,7 +148,7 @@ add_task(async function test_retry_limit() { // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(resolve => setTimeout(resolve, 10)); - bs.createBackupOnIdleDispatch(); + bs.createBackupOnIdleDispatch({}); // #backupInProgress is set to true await bsInProgressStateUpdate(bs, true);