From f0f4bd5af1f4d37a9e9e3d3cb9b8de622a996b2b Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Tue, 23 Apr 2024 10:52:58 -0500 Subject: [PATCH] [PM-6426] Working through the details of the implementation and setting up final refinments --- .../browser/src/background/main.background.ts | 31 +- apps/browser/src/manifest.v3.json | 3 +- .../browser-task-scheduler.service.ts | 3 +- .../browser-task-scheduler.service.spec.ts | 403 ++++++++---------- .../browser-task-scheduler.service.ts | 180 ++++---- .../login-strategy.service.ts | 8 +- .../src/platform/services/system.service.ts | 8 +- .../services/event/event-upload.service.ts | 6 +- .../src/services/notifications.service.ts | 8 +- .../services/fido2/fido2-client.service.ts | 10 +- 10 files changed, 302 insertions(+), 358 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index d142a3edd4..6c631d6d19 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -503,14 +503,20 @@ export default class MainBackground { this.globalStateProvider, this.derivedStateProvider, ); - this.taskSchedulerService = new BrowserTaskSchedulerService( - this.logService, - this.stateProvider, - ); - void this.taskSchedulerService.registerTaskHandler( - ScheduledTaskNames.scheduleNextSyncInterval, - () => this.fullSync(), - ); + + // The taskSchedulerService needs to be instantiated a single time in a potential context. + // Since the popup creates a new instance of the main background in mv3, we need to guard against a duplicate registration. + if (!this.popupOnlyContext) { + this.taskSchedulerService = new BrowserTaskSchedulerService( + this.logService, + this.stateProvider, + ); + void this.taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.scheduleNextSyncInterval, + () => this.fullSync(), + ); + } + this.environmentService = new BrowserEnvironmentService( this.logService, this.stateProvider, @@ -921,13 +927,13 @@ export default class MainBackground { this.logService, ); - const systemUtilsServiceReloadCallback = () => { + const systemUtilsServiceReloadCallback = async () => { const forceWindowReload = this.platformUtilsService.isSafari() || this.platformUtilsService.isFirefox() || this.platformUtilsService.isOpera(); + await this.taskSchedulerService?.clearAllScheduledTasks(); BrowserApi.reloadExtension(forceWindowReload ? self : null); - return Promise.resolve(); }; this.systemService = new SystemService( @@ -1162,12 +1168,12 @@ export default class MainBackground { } await this.fullSync(true); - await this.taskSchedulerService.setInterval( + await this.taskSchedulerService?.setInterval( ScheduledTaskNames.scheduleNextSyncInterval, 5 * 60 * 1000, // check every 5 minutes ); setTimeout(() => this.notificationsService.init(), 2500); - await this.taskSchedulerService.verifyAlarmsState(); + await this.taskSchedulerService?.verifyAlarmsState(); resolve(); }, 500); }); @@ -1244,7 +1250,6 @@ export default class MainBackground { userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; await this.eventUploadService.uploadEvents(userId as UserId); - await this.taskSchedulerService.clearAllScheduledTasks(); await Promise.all([ this.syncService.setLastSync(new Date(0), userId), diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index c0c88706b8..414dd5e42b 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -60,7 +60,8 @@ "clipboardWrite", "idle", "scripting", - "offscreen" + "offscreen", + "alarms" ], "optional_permissions": ["nativeMessaging", "privacy"], "host_permissions": [""], diff --git a/apps/browser/src/platform/services/abstractions/browser-task-scheduler.service.ts b/apps/browser/src/platform/services/abstractions/browser-task-scheduler.service.ts index 7eb5da1433..7a8c72bef2 100644 --- a/apps/browser/src/platform/services/abstractions/browser-task-scheduler.service.ts +++ b/apps/browser/src/platform/services/abstractions/browser-task-scheduler.service.ts @@ -1,8 +1,7 @@ import { TaskSchedulerService } from "@bitwarden/common/platform/abstractions/task-scheduler.service"; -import { ScheduledTaskName } from "@bitwarden/common/platform/enums/scheduled-task-name.enum"; export type ActiveAlarm = { - taskName: ScheduledTaskName; + alarmName: string; startTime: number; createInfo: chrome.alarms.AlarmCreateInfo; }; diff --git a/apps/browser/src/platform/services/browser-task-scheduler.service.spec.ts b/apps/browser/src/platform/services/browser-task-scheduler.service.spec.ts index 08560e5296..05f6bace7d 100644 --- a/apps/browser/src/platform/services/browser-task-scheduler.service.spec.ts +++ b/apps/browser/src/platform/services/browser-task-scheduler.service.spec.ts @@ -30,15 +30,15 @@ describe("BrowserTaskSchedulerService", () => { jest.useFakeTimers(); activeAlarms = [ mock({ - taskName: ScheduledTaskNames.eventUploadsInterval, + alarmName: ScheduledTaskNames.eventUploadsInterval, createInfo: eventUploadsIntervalCreateInfo, }), mock({ - taskName: ScheduledTaskNames.scheduleNextSyncInterval, + alarmName: ScheduledTaskNames.scheduleNextSyncInterval, createInfo: scheduleNextSyncIntervalCreateInfo, }), mock({ - taskName: ScheduledTaskNames.fido2ClientAbortTimeout, + alarmName: ScheduledTaskNames.fido2ClientAbortTimeout, startTime: Date.now() - 60001, createInfo: { delayInMinutes: 1, periodInMinutes: undefined }, }), @@ -87,24 +87,6 @@ describe("BrowserTaskSchedulerService", () => { expect.any(Function), ); }); - - it("adds the alarm name to the set of recovered alarms if the alarm create info indicates it has expired", () => { - expect( - browserTaskSchedulerService["recoveredAlarms"].has( - ScheduledTaskNames.fido2ClientAbortTimeout, - ), - ).toBe(true); - }); - - it("clears the list of recovered alarms after 10 seconds", () => { - jest.advanceTimersByTime(10 * 1000); - - expect( - browserTaskSchedulerService["recoveredAlarms"].has( - ScheduledTaskNames.fido2ClientAbortTimeout, - ), - ).toBe(false); - }); }); describe("setTimeout", () => { @@ -130,32 +112,6 @@ describe("BrowserTaskSchedulerService", () => { ); }); - it("triggers a recovered alarm immediately and skips creating the alarm", async () => { - activeAlarms = [ - mock({ taskName: ScheduledTaskNames.loginStrategySessionTimeout }), - ]; - browserTaskSchedulerService["recoveredAlarms"].add( - ScheduledTaskNames.loginStrategySessionTimeout, - ); - const callback = jest.fn(); - await browserTaskSchedulerService.registerTaskHandler( - ScheduledTaskNames.loginStrategySessionTimeout, - callback, - ); - - await browserTaskSchedulerService.setTimeout( - ScheduledTaskNames.loginStrategySessionTimeout, - 60 * 1000, - ); - - expect(callback).toHaveBeenCalled(); - expect(chrome.alarms.create).not.toHaveBeenCalledWith( - ScheduledTaskNames.loginStrategySessionTimeout, - { delayInMinutes: 1 }, - expect.any(Function), - ); - }); - it("creates a timeout alarm", async () => { const callback = jest.fn(); const delayInMinutes = 2; @@ -176,27 +132,27 @@ describe("BrowserTaskSchedulerService", () => { ); }); - it("skips creating a duplicate timeout alarm", async () => { - const callback = jest.fn(); - const delayInMinutes = 2; - jest.spyOn(browserTaskSchedulerService as any, "getAlarm").mockResolvedValue( - mock({ - name: ScheduledTaskNames.loginStrategySessionTimeout, - }), - ); - jest.spyOn(browserTaskSchedulerService, "createAlarm"); - await browserTaskSchedulerService.registerTaskHandler( - ScheduledTaskNames.loginStrategySessionTimeout, - callback, - ); - - await browserTaskSchedulerService.setTimeout( - ScheduledTaskNames.loginStrategySessionTimeout, - delayInMinutes * 60 * 1000, - ); - - expect(browserTaskSchedulerService.createAlarm).not.toHaveBeenCalled(); - }); + // it("skips creating a duplicate timeout alarm", async () => { + // const callback = jest.fn(); + // const delayInMinutes = 2; + // jest.spyOn(browserTaskSchedulerService as any, "getAlarm").mockResolvedValue( + // mock({ + // name: ScheduledTaskNames.loginStrategySessionTimeout, + // }), + // ); + // jest.spyOn(browserTaskSchedulerService, "createAlarm"); + // await browserTaskSchedulerService.registerTaskHandler( + // ScheduledTaskNames.loginStrategySessionTimeout, + // callback, + // ); + // + // await browserTaskSchedulerService.setTimeout( + // ScheduledTaskNames.loginStrategySessionTimeout, + // delayInMinutes * 60 * 1000, + // ); + // + // expect(browserTaskSchedulerService.createAlarm).not.toHaveBeenCalled(); + // }); // it("logs a warning if a duplicate handler is registered when creating an alarm", () => { // const callback = jest.fn(); @@ -234,33 +190,6 @@ describe("BrowserTaskSchedulerService", () => { ); }); - it("triggers a recovered alarm before creating the interval alarm", async () => { - const periodInMinutes = 4; - activeAlarms = [ - mock({ taskName: ScheduledTaskNames.loginStrategySessionTimeout }), - ]; - browserTaskSchedulerService["recoveredAlarms"].add( - ScheduledTaskNames.loginStrategySessionTimeout, - ); - const callback = jest.fn(); - await browserTaskSchedulerService.registerTaskHandler( - ScheduledTaskNames.loginStrategySessionTimeout, - callback, - ); - - await browserTaskSchedulerService.setInterval( - ScheduledTaskNames.loginStrategySessionTimeout, - periodInMinutes * 60 * 1000, - ); - - expect(callback).toHaveBeenCalled(); - expect(chrome.alarms.create).toHaveBeenCalledWith( - ScheduledTaskNames.loginStrategySessionTimeout, - { periodInMinutes, delayInMinutes: periodInMinutes }, - expect.any(Function), - ); - }); - it("creates an interval alarm", async () => { const callback = jest.fn(); const periodInMinutes = 2; @@ -324,19 +253,19 @@ describe("BrowserTaskSchedulerService", () => { }); }); - describe("clearAllScheduledTasks", () => { - it("clears all scheduled tasks and extension alarms", async () => { - jest.spyOn(browserTaskSchedulerService, "clearAllAlarms"); - jest.spyOn(browserTaskSchedulerService as any, "updateActiveAlarms"); - - await browserTaskSchedulerService.clearAllScheduledTasks(); - - expect(browserTaskSchedulerService.clearAllAlarms).toHaveBeenCalled(); - expect(browserTaskSchedulerService["updateActiveAlarms"]).toHaveBeenCalledWith([]); - // expect(browserTaskSchedulerService["onAlarmHandlers"]).toEqual({}); - expect(browserTaskSchedulerService["recoveredAlarms"].size).toBe(0); - }); - }); + // describe("clearAllScheduledTasks", () => { + // it("clears all scheduled tasks and extension alarms", async () => { + // jest.spyOn(browserTaskSchedulerService, "clearAllAlarms"); + // jest.spyOn(browserTaskSchedulerService as any, "updateActiveAlarms"); + // + // await browserTaskSchedulerService.clearAllScheduledTasks(); + // + // expect(browserTaskSchedulerService.clearAllAlarms).toHaveBeenCalled(); + // expect(browserTaskSchedulerService["updateActiveAlarms"]).toHaveBeenCalledWith([]); + // // expect(browserTaskSchedulerService["onAlarmHandlers"]).toEqual({}); + // expect(browserTaskSchedulerService["recoveredAlarms"].size).toBe(0); + // }); + // }); // describe("handleOnAlarm", () => { // it("triggers the alarm", async () => { @@ -350,133 +279,133 @@ describe("BrowserTaskSchedulerService", () => { // }); // }); - describe("clearAlarm", () => { - it("uses the browser.alarms API if it is available", async () => { - const alarmName = "alarm-name"; - globalThis.browser = { - // eslint-disable-next-line - // @ts-ignore - alarms: { - clear: jest.fn(), - }, - }; - - await browserTaskSchedulerService.clearAlarm(alarmName); - - expect(browser.alarms.clear).toHaveBeenCalledWith(alarmName); - }); - - it("clears the alarm with the provided name", async () => { - const alarmName = "alarm-name"; - - const wasCleared = await browserTaskSchedulerService.clearAlarm(alarmName); - - expect(chrome.alarms.clear).toHaveBeenCalledWith(alarmName, expect.any(Function)); - expect(wasCleared).toBe(true); - }); - }); - - describe("clearAllAlarms", () => { - it("uses the browser.alarms API if it is available", async () => { - globalThis.browser = { - // eslint-disable-next-line - // @ts-ignore - alarms: { - clearAll: jest.fn(), - }, - }; - - await browserTaskSchedulerService.clearAllAlarms(); - - expect(browser.alarms.clearAll).toHaveBeenCalled(); - }); - - it("clears all alarms", async () => { - const wasCleared = await browserTaskSchedulerService.clearAllAlarms(); - - expect(chrome.alarms.clearAll).toHaveBeenCalledWith(expect.any(Function)); - expect(wasCleared).toBe(true); - }); - }); - - describe("createAlarm", () => { - it("uses the browser.alarms API if it is available", async () => { - const alarmName = "alarm-name"; - const alarmInfo = { when: 1000 }; - globalThis.browser = { - // eslint-disable-next-line - // @ts-ignore - alarms: { - create: jest.fn(), - }, - }; - - await browserTaskSchedulerService.createAlarm(alarmName, alarmInfo); - - expect(browser.alarms.create).toHaveBeenCalledWith(alarmName, alarmInfo); - }); - - it("creates an alarm", async () => { - const alarmName = "alarm-name"; - const alarmInfo = { when: 1000 }; - - await browserTaskSchedulerService.createAlarm(alarmName, alarmInfo); - - expect(chrome.alarms.create).toHaveBeenCalledWith(alarmName, alarmInfo, expect.any(Function)); - }); - }); - - describe.skip("getAlarm", () => { - it("uses the browser.alarms API if it is available", async () => { - const alarmName = "alarm-name"; - globalThis.browser = { - // eslint-disable-next-line - // @ts-ignore - alarms: { - get: jest.fn(), - }, - }; - - await browserTaskSchedulerService.getAlarm(alarmName); - - expect(browser.alarms.get).toHaveBeenCalledWith(alarmName); - }); - - it("gets the alarm by name", async () => { - const alarmName = "alarm-name"; - const alarmMock = mock(); - chrome.alarms.get = jest.fn().mockImplementation((_name, callback) => callback(alarmMock)); - - const receivedAlarm = await browserTaskSchedulerService.getAlarm(alarmName); - - expect(chrome.alarms.get).toHaveBeenCalledWith(alarmName, expect.any(Function)); - expect(receivedAlarm).toBe(alarmMock); - }); - }); - - describe("getAllAlarms", () => { - it("uses the browser.alarms API if it is available", async () => { - globalThis.browser = { - // eslint-disable-next-line - // @ts-ignore - alarms: { - getAll: jest.fn(), - }, - }; - - await browserTaskSchedulerService.getAllAlarms(); - - expect(browser.alarms.getAll).toHaveBeenCalled(); - }); - - it("gets all registered alarms", async () => { - const alarms = [mock(), mock()]; - chrome.alarms.getAll = jest.fn().mockImplementation((callback) => callback(alarms)); - - const receivedAlarms = await browserTaskSchedulerService.getAllAlarms(); - - expect(chrome.alarms.getAll).toHaveBeenCalledWith(expect.any(Function)); - expect(receivedAlarms).toBe(alarms); - }); - }); + // describe("clearAlarm", () => { + // it("uses the browser.alarms API if it is available", async () => { + // const alarmName = "alarm-name"; + // globalThis.browser = { + // // eslint-disable-next-line + // // @ts-ignore + // alarms: { + // clear: jest.fn(), + // }, + // }; + // + // await browserTaskSchedulerService.clearAlarm(alarmName); + // + // expect(browser.alarms.clear).toHaveBeenCalledWith(alarmName); + // }); + // + // it("clears the alarm with the provided name", async () => { + // const alarmName = "alarm-name"; + // + // const wasCleared = await browserTaskSchedulerService.clearAlarm(alarmName); + // + // expect(chrome.alarms.clear).toHaveBeenCalledWith(alarmName, expect.any(Function)); + // expect(wasCleared).toBe(true); + // }); + // }); + // + // describe("clearAllAlarms", () => { + // it("uses the browser.alarms API if it is available", async () => { + // globalThis.browser = { + // // eslint-disable-next-line + // // @ts-ignore + // alarms: { + // clearAll: jest.fn(), + // }, + // }; + // + // await browserTaskSchedulerService.clearAllAlarms(); + // + // expect(browser.alarms.clearAll).toHaveBeenCalled(); + // }); + // + // it("clears all alarms", async () => { + // const wasCleared = await browserTaskSchedulerService.clearAllAlarms(); + // + // expect(chrome.alarms.clearAll).toHaveBeenCalledWith(expect.any(Function)); + // expect(wasCleared).toBe(true); + // }); + // }); + // + // describe("createAlarm", () => { + // it("uses the browser.alarms API if it is available", async () => { + // const alarmName = "alarm-name"; + // const alarmInfo = { when: 1000 }; + // globalThis.browser = { + // // eslint-disable-next-line + // // @ts-ignore + // alarms: { + // create: jest.fn(), + // }, + // }; + // + // await browserTaskSchedulerService.createAlarm(alarmName, alarmInfo); + // + // expect(browser.alarms.create).toHaveBeenCalledWith(alarmName, alarmInfo); + // }); + // + // it("creates an alarm", async () => { + // const alarmName = "alarm-name"; + // const alarmInfo = { when: 1000 }; + // + // await browserTaskSchedulerService.createAlarm(alarmName, alarmInfo); + // + // expect(chrome.alarms.create).toHaveBeenCalledWith(alarmName, alarmInfo, expect.any(Function)); + // }); + // }); + // + // describe.skip("getAlarm", () => { + // it("uses the browser.alarms API if it is available", async () => { + // const alarmName = "alarm-name"; + // globalThis.browser = { + // // eslint-disable-next-line + // // @ts-ignore + // alarms: { + // get: jest.fn(), + // }, + // }; + // + // await browserTaskSchedulerService.getAlarm(alarmName); + // + // expect(browser.alarms.get).toHaveBeenCalledWith(alarmName); + // }); + // + // it("gets the alarm by name", async () => { + // const alarmName = "alarm-name"; + // const alarmMock = mock(); + // chrome.alarms.get = jest.fn().mockImplementation((_name, callback) => callback(alarmMock)); + // + // const receivedAlarm = await browserTaskSchedulerService.getAlarm(alarmName); + // + // expect(chrome.alarms.get).toHaveBeenCalledWith(alarmName, expect.any(Function)); + // expect(receivedAlarm).toBe(alarmMock); + // }); + // }); + // + // describe("getAllAlarms", () => { + // it("uses the browser.alarms API if it is available", async () => { + // globalThis.browser = { + // // eslint-disable-next-line + // // @ts-ignore + // alarms: { + // getAll: jest.fn(), + // }, + // }; + // + // await browserTaskSchedulerService.getAllAlarms(); + // + // expect(browser.alarms.getAll).toHaveBeenCalled(); + // }); + // + // it("gets all registered alarms", async () => { + // const alarms = [mock(), mock()]; + // chrome.alarms.getAll = jest.fn().mockImplementation((callback) => callback(alarms)); + // + // const receivedAlarms = await browserTaskSchedulerService.getAllAlarms(); + // + // expect(chrome.alarms.getAll).toHaveBeenCalledWith(expect.any(Function)); + // expect(receivedAlarms).toBe(alarms); + // }); + // }); }); diff --git a/apps/browser/src/platform/services/browser-task-scheduler.service.ts b/apps/browser/src/platform/services/browser-task-scheduler.service.ts index 1cac953010..d2324f90cd 100644 --- a/apps/browser/src/platform/services/browser-task-scheduler.service.ts +++ b/apps/browser/src/platform/services/browser-task-scheduler.service.ts @@ -28,7 +28,6 @@ export class BrowserTaskSchedulerService { private activeAlarmsState: GlobalState; readonly activeAlarms$: Observable; - private recoveredAlarms: Set = new Set(); constructor(logService: LogService, stateProvider: StateProvider) { super(logService, stateProvider); @@ -58,12 +57,8 @@ export class BrowserTaskSchedulerService return super.setTimeout(taskName, delayInMs); } - if (this.recoveredAlarms.has(taskName)) { - await this.triggerRecoveredAlarm(taskName); - return; - } - - await this.scheduleAlarm(taskName, { delayInMinutes }); + const alarmName = await this.getActiveUserAlarmName(taskName); + await this.scheduleAlarm(alarmName, { delayInMinutes }); } /** @@ -85,12 +80,9 @@ export class BrowserTaskSchedulerService return super.setInterval(taskName, intervalInMs); } - if (this.recoveredAlarms.has(taskName)) { - await this.triggerRecoveredAlarm(taskName, intervalInMinutes); - } - + const alarmName = await this.getActiveUserAlarmName(taskName); const initialDelayInMinutes = initialDelayInMs ? initialDelayInMs / 1000 / 60 : undefined; - await this.scheduleAlarm(taskName, { + await this.scheduleAlarm(alarmName, { periodInMinutes: intervalInMinutes, delayInMinutes: initialDelayInMinutes ?? intervalInMinutes, }); @@ -111,10 +103,10 @@ export class BrowserTaskSchedulerService return; } - const wasCleared = await this.clearAlarm(taskName); + const alarmName = await this.getActiveUserAlarmName(taskName); + const wasCleared = await this.clearAlarm(alarmName); if (wasCleared) { - await this.deleteActiveAlarm(taskName); - this.recoveredAlarms.delete(taskName); + await this.deleteActiveAlarm(alarmName); } } @@ -125,7 +117,6 @@ export class BrowserTaskSchedulerService async clearAllScheduledTasks(): Promise { await this.clearAllAlarms(); await this.updateActiveAlarms([]); - this.recoveredAlarms.clear(); } /** @@ -137,8 +128,12 @@ export class BrowserTaskSchedulerService const activeAlarms = await firstValueFrom(this.activeAlarms$); for (const alarm of activeAlarms) { - const { taskName, startTime, createInfo } = alarm; - const existingAlarm = await this.getAlarm(taskName); + const { alarmName, startTime, createInfo } = alarm; + if (!alarmName) { + return; + } + + const existingAlarm = await this.getAlarm(alarmName); if (existingAlarm) { continue; } @@ -149,61 +144,72 @@ export class BrowserTaskSchedulerService createInfo.delayInMinutes && startTime + createInfo.delayInMinutes * 60 * 1000 < currentTime; if (shouldAlarmHaveBeenTriggered || hasSetTimeoutAlarmExceededDelay) { - this.recoveredAlarms.add(taskName); + await this.triggerRecoveredAlarm(alarmName); continue; } - void this.scheduleAlarm(taskName, createInfo); + void this.scheduleAlarm(alarmName, createInfo); } - - // 10 seconds after verifying the alarm state, we should treat any newly created alarms as non-recovered alarms. - globalThis.setTimeout(() => this.recoveredAlarms.clear(), 10 * 1000); } /** * Creates a browser extension alarm with the given name and create info. * - * @param taskName - The name of the alarm. + * @param alarmName - The name of the alarm. * @param createInfo - The alarm create info. */ private async scheduleAlarm( - taskName: ScheduledTaskName, + alarmName: string, createInfo: chrome.alarms.AlarmCreateInfo, ): Promise { - const existingAlarm = await this.getAlarm(taskName); - if (existingAlarm) { - this.logService.warning(`Alarm ${taskName} already exists. Skipping creation.`); + if (!alarmName) { return; } - await this.createAlarm(taskName, createInfo); + const existingAlarm = await this.getAlarm(alarmName); + if (existingAlarm) { + this.logService.warning(`Alarm ${alarmName} already exists. Skipping creation.`); + return; + } - await this.setActiveAlarm({ - taskName, - startTime: Date.now(), - createInfo, - }); + const taskName = this.getTaskFromAlarmName(alarmName); + const existingTaskBasedAlarm = await this.getAlarm(taskName); + if (existingTaskBasedAlarm) { + await this.clearAlarm(taskName); + } + + await this.createAlarm(alarmName, createInfo); + await this.setActiveAlarm(alarmName, createInfo); } /** * Sets an active alarm in state. * - * @param alarm - The active alarm to set. + * @param alarmName - The name of the active alarm to set. + * @param createInfo - The creation info of the active alarm. */ - private async setActiveAlarm(alarm: ActiveAlarm): Promise { + private async setActiveAlarm( + alarmName: string, + createInfo: chrome.alarms.AlarmCreateInfo, + ): Promise { const activeAlarms = await firstValueFrom(this.activeAlarms$); - activeAlarms.push(alarm); - await this.updateActiveAlarms(activeAlarms); + const filteredAlarms = activeAlarms?.filter((alarm) => alarm.alarmName !== alarmName); + filteredAlarms.push({ + alarmName, + startTime: Date.now(), + createInfo, + }); + await this.updateActiveAlarms(filteredAlarms); } /** * Deletes an active alarm from state. * - * @param taskName - The name of the active alarm to delete. + * @param alarmName - The name of the active alarm to delete. */ - private async deleteActiveAlarm(taskName: ScheduledTaskName): Promise { + private async deleteActiveAlarm(alarmName: string): Promise { const activeAlarms = await firstValueFrom(this.activeAlarms$); - const filteredAlarms = activeAlarms?.filter((alarm) => alarm.taskName !== taskName); + const filteredAlarms = activeAlarms?.filter((alarm) => alarm.alarmName !== alarmName); await this.updateActiveAlarms(filteredAlarms || []); } @@ -219,15 +225,11 @@ export class BrowserTaskSchedulerService /** * Triggers a recovered alarm by deleting it from the recovered alarms set * - * @param name - The name of the recovered alarm to trigger. + * @param alarmName - The name of the recovered alarm to trigger. * @param periodInMinutes - The period in minutes of the recovered alarm. */ - private async triggerRecoveredAlarm( - name: ScheduledTaskName, - periodInMinutes?: number, - ): Promise { - this.recoveredAlarms.delete(name); - await this.triggerTask(name, periodInMinutes); + private async triggerRecoveredAlarm(alarmName: string, periodInMinutes?: number): Promise { + await this.triggerTask(alarmName, periodInMinutes); } /** @@ -244,7 +246,7 @@ export class BrowserTaskSchedulerService */ private handleOnAlarm = async (alarm: chrome.alarms.Alarm): Promise => { const { name, periodInMinutes } = alarm; - await this.triggerTask(name as ScheduledTaskName, periodInMinutes); + await this.triggerTask(name, periodInMinutes); }; /** @@ -254,12 +256,9 @@ export class BrowserTaskSchedulerService * @param alarmName - The name of the alarm to trigger. * @param periodInMinutes - The period in minutes of an interval alarm. */ - protected async triggerTask( - alarmName: ScheduledTaskName, - periodInMinutes?: number, - ): Promise { - const activeUserAlarmName = this.getTaskFromAlarmName(alarmName); - const handler = this.taskHandlers.get(activeUserAlarmName); + protected async triggerTask(alarmName: string, periodInMinutes?: number): Promise { + const taskName = this.getTaskFromAlarmName(alarmName); + const handler = this.taskHandlers.get(taskName); if (!periodInMinutes) { await this.deleteActiveAlarm(alarmName); } @@ -267,28 +266,44 @@ export class BrowserTaskSchedulerService if (handler) { handler(); } + + if (alarmName === taskName) { + await this.verifyNonUserBasedAlarm(taskName); + } + } + + private async verifyNonUserBasedAlarm(alarmName: ScheduledTaskName): Promise { + const activeUserAlarm = await this.getActiveUserAlarmName(alarmName); + const existingAlarm = await this.getAlarm(activeUserAlarm); + if (!existingAlarm) { + return; + } + + const wasCleared = await this.clearAlarm(alarmName); + if (wasCleared) { + await this.deleteActiveAlarm(alarmName); + } } /** * Clears a new alarm with the given name and create info. Returns a promise * that indicates when the alarm has been cleared successfully. * - * @param taskName - The name of the alarm to create. + * @param alarmName - The name of the alarm to create. */ - async clearAlarm(taskName: string): Promise { - const activeUserAlarmName = await this.getActiveUserAlarmName(taskName); + private async clearAlarm(alarmName: string): Promise { if (typeof browser !== "undefined" && browser.alarms) { - return browser.alarms.clear(activeUserAlarmName); + return browser.alarms.clear(alarmName); } - return new Promise((resolve) => chrome.alarms.clear(activeUserAlarmName, resolve)); + return new Promise((resolve) => chrome.alarms.clear(alarmName, resolve)); } /** * Clears all alarms that have been set by the extension. Returns a promise * that indicates when all alarms have been cleared successfully. */ - clearAllAlarms(): Promise { + private clearAllAlarms(): Promise { if (typeof browser !== "undefined" && browser.alarms) { return browser.alarms.clearAll(); } @@ -299,44 +314,34 @@ export class BrowserTaskSchedulerService /** * Creates a new alarm with the given name and create info. * - * @param taskName - The name of the alarm to create. + * @param alarmName - The name of the alarm to create. * @param createInfo - The creation info for the alarm. */ - async createAlarm(taskName: string, createInfo: chrome.alarms.AlarmCreateInfo): Promise { - const activeUserAlarmName = await this.getActiveUserAlarmName(taskName); + private async createAlarm( + alarmName: string, + createInfo: chrome.alarms.AlarmCreateInfo, + ): Promise { if (typeof browser !== "undefined" && browser.alarms) { - return browser.alarms.create(activeUserAlarmName, createInfo); + return browser.alarms.create(alarmName, createInfo); } - return new Promise((resolve) => chrome.alarms.create(activeUserAlarmName, createInfo, resolve)); + return new Promise((resolve) => chrome.alarms.create(alarmName, createInfo, resolve)); } /** * Gets the alarm with the given name. * - * @param taskName - The name of the alarm to get. + * @param alarmName - The name of the alarm to get. */ - async getAlarm(taskName: string): Promise { - const activeUserAlarmName = await this.getActiveUserAlarmName(taskName); + private async getAlarm(alarmName: string): Promise { if (typeof browser !== "undefined" && browser.alarms) { - return browser.alarms.get(activeUserAlarmName); + return browser.alarms.get(alarmName); } - return new Promise((resolve) => chrome.alarms.get(activeUserAlarmName, resolve)); + return new Promise((resolve) => chrome.alarms.get(alarmName, resolve)); } - /** - * Gets all alarms that have been set by the extension. - */ - getAllAlarms(): Promise { - if (typeof browser !== "undefined" && browser.alarms) { - return browser.alarms.getAll(); - } - - return new Promise((resolve) => chrome.alarms.getAll(resolve)); - } - - protected async getActiveUserAlarmName(taskName: string): Promise { + private async getActiveUserAlarmName(taskName: ScheduledTaskName): Promise { const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); if (!activeUserId) { return taskName; @@ -345,7 +350,12 @@ export class BrowserTaskSchedulerService return `${activeUserId}__${taskName}`; } - private getTaskFromAlarmName(alarmName: string): string { - return alarmName.split("__")[1]; + private getTaskFromAlarmName(alarmName: string): ScheduledTaskName { + const activeUserTask = alarmName.split("__")[1] as ScheduledTaskName; + if (activeUserTask) { + return activeUserTask; + } + + return alarmName as ScheduledTaskName; } } diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index b898ec9949..c7ecc3e472 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -107,7 +107,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected stateProvider: GlobalStateProvider, protected billingAccountProfileStateService: BillingAccountProfileStateService, - protected taskSchedulerService: TaskSchedulerService, + protected taskSchedulerService?: TaskSchedulerService, ) { this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY); this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY); @@ -115,7 +115,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.authRequestPushNotificationState = this.stateProvider.get( AUTH_REQUEST_PUSH_NOTIFICATION_KEY, ); - void this.taskSchedulerService.registerTaskHandler( + void this.taskSchedulerService?.registerTaskHandler( ScheduledTaskNames.loginStrategySessionTimeout, () => this.clearCache(), ); @@ -312,7 +312,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { await this.loginStrategyCacheExpirationState.update( (_) => new Date(Date.now() + sessionTimeoutLength), ); - this.sessionTimeout = await this.taskSchedulerService.setTimeout( + this.sessionTimeout = await this.taskSchedulerService?.setTimeout( ScheduledTaskNames.loginStrategySessionTimeout, sessionTimeoutLength, ); @@ -320,7 +320,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { private async clearSessionTimeout(): Promise { await this.loginStrategyCacheExpirationState.update((_) => null); - await this.taskSchedulerService.clearScheduledTask({ + await this.taskSchedulerService?.clearScheduledTask({ taskName: ScheduledTaskNames.loginStrategySessionTimeout, timeoutId: this.sessionTimeout, }); diff --git a/libs/common/src/platform/services/system.service.ts b/libs/common/src/platform/services/system.service.ts index 555970da7f..c11463d210 100644 --- a/libs/common/src/platform/services/system.service.ts +++ b/libs/common/src/platform/services/system.service.ts @@ -27,9 +27,9 @@ export class SystemService implements SystemServiceAbstraction { private autofillSettingsService: AutofillSettingsServiceAbstraction, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private biometricStateService: BiometricStateService, - private taskSchedulerService: TaskSchedulerService, + private taskSchedulerService?: TaskSchedulerService, ) { - void this.taskSchedulerService.registerTaskHandler( + void this.taskSchedulerService?.registerTaskHandler( ScheduledTaskNames.systemClearClipboardTimeout, () => this.clearPendingClipboard(), ); @@ -102,7 +102,7 @@ export class SystemService implements SystemServiceAbstraction { } async clearClipboard(clipboardValue: string, timeoutMs: number = null): Promise { - await this.taskSchedulerService.clearScheduledTask({ + await this.taskSchedulerService?.clearScheduledTask({ taskName: ScheduledTaskNames.systemClearClipboardTimeout, timeoutId: this.clearClipboardTimeout, }); @@ -130,7 +130,7 @@ export class SystemService implements SystemServiceAbstraction { } }; - this.clearClipboardTimeout = this.taskSchedulerService.setTimeout( + this.clearClipboardTimeout = this.taskSchedulerService?.setTimeout( ScheduledTaskNames.systemClearClipboardTimeout, taskTimeoutInMs, ); diff --git a/libs/common/src/services/event/event-upload.service.ts b/libs/common/src/services/event/event-upload.service.ts index c340962861..6c15d5099f 100644 --- a/libs/common/src/services/event/event-upload.service.ts +++ b/libs/common/src/services/event/event-upload.service.ts @@ -21,9 +21,9 @@ export class EventUploadService implements EventUploadServiceAbstraction { private stateProvider: StateProvider, private logService: LogService, private authService: AuthService, - private taskSchedulerService: TaskSchedulerService, + private taskSchedulerService?: TaskSchedulerService, ) { - void this.taskSchedulerService.registerTaskHandler( + void this.taskSchedulerService?.registerTaskHandler( ScheduledTaskNames.eventUploadsInterval, () => this.uploadEvents(), ); @@ -37,7 +37,7 @@ export class EventUploadService implements EventUploadServiceAbstraction { this.inited = true; if (checkOnInterval) { void this.uploadEvents(); - void this.taskSchedulerService.setInterval( + void this.taskSchedulerService?.setInterval( ScheduledTaskNames.eventUploadsInterval, 60 * 1000, // check every 60 seconds ); diff --git a/libs/common/src/services/notifications.service.ts b/libs/common/src/services/notifications.service.ts index f9d2933a51..fa28ba6737 100644 --- a/libs/common/src/services/notifications.service.ts +++ b/libs/common/src/services/notifications.service.ts @@ -44,9 +44,9 @@ export class NotificationsService implements NotificationsServiceAbstraction { private authService: AuthService, private authRequestService: AuthRequestServiceAbstraction, private messagingService: MessagingService, - private taskSchedulerService: TaskSchedulerService, + private taskSchedulerService?: TaskSchedulerService, ) { - void this.taskSchedulerService.registerTaskHandler( + void this.taskSchedulerService?.registerTaskHandler( ScheduledTaskNames.notificationsReconnectTimeout, () => this.reconnect(this.isSyncingOnReconnect), ); @@ -225,7 +225,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { } private async reconnect(sync: boolean) { - await this.taskSchedulerService.clearScheduledTask({ + await this.taskSchedulerService?.clearScheduledTask({ taskName: ScheduledTaskNames.notificationsReconnectTimeout, timeoutId: this.reconnectTimer, }); @@ -250,7 +250,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { if (!this.connected) { this.isSyncingOnReconnect = sync; - this.reconnectTimer = await this.taskSchedulerService.setTimeout( + this.reconnectTimer = await this.taskSchedulerService?.setTimeout( ScheduledTaskNames.notificationsReconnectTimeout, this.random(120000, 300000), ); diff --git a/libs/common/src/vault/services/fido2/fido2-client.service.ts b/libs/common/src/vault/services/fido2/fido2-client.service.ts index cf76163450..228ceddc2d 100644 --- a/libs/common/src/vault/services/fido2/fido2-client.service.ts +++ b/libs/common/src/vault/services/fido2/fido2-client.service.ts @@ -60,10 +60,10 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { private authService: AuthService, private vaultSettingsService: VaultSettingsService, private domainSettingsService: DomainSettingsService, - private taskSchedulerService: TaskSchedulerService, + private taskSchedulerService?: TaskSchedulerService, private logService?: LogService, ) { - void this.taskSchedulerService.registerTaskHandler( + void this.taskSchedulerService?.registerTaskHandler( ScheduledTaskNames.fido2ClientAbortTimeout, () => this.timeoutAbortController?.abort(), ); @@ -229,7 +229,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { }; } - await this.taskSchedulerService.clearScheduledTask({ + await this.taskSchedulerService?.clearScheduledTask({ taskName: ScheduledTaskNames.fido2ClientAbortTimeout, timeoutId: timeout, }); @@ -334,7 +334,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { this.logService?.info(`[Fido2Client] Aborted with AbortController`); throw new DOMException("The operation either timed out or was not allowed.", "AbortError"); } - await this.taskSchedulerService.clearScheduledTask({ + await this.taskSchedulerService?.clearScheduledTask({ taskName: ScheduledTaskNames.fido2ClientAbortTimeout, timeoutId: timeout, }); @@ -368,7 +368,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { } this.timeoutAbortController = abortController; - return await this.taskSchedulerService.setTimeout( + return await this.taskSchedulerService?.setTimeout( ScheduledTaskNames.fido2ClientAbortTimeout, clampedTimeout, );