From 15056f16ff641991f66fd81bf363162f3dbe26db Mon Sep 17 00:00:00 2001 From: NguyenTranHoangSym <97150869+NguyenTranHoangSym@users.noreply.github.com> Date: Tue, 18 Jul 2023 16:07:50 +0700 Subject: [PATCH] SDA-4182: Add GPOs to auto update channel (#1896) --- config/Symphony.config | 1 + spec/registryHandler.spec.ts | 44 ++++++ src/app/auto-update-handler.ts | 254 ++++++++++++++++++------------- src/app/registry-handler.ts | 61 ++++++++ src/app/stores/registry-store.ts | 28 ++++ 5 files changed, 278 insertions(+), 110 deletions(-) create mode 100644 spec/registryHandler.spec.ts create mode 100644 src/app/registry-handler.ts create mode 100644 src/app/stores/registry-store.ts diff --git a/config/Symphony.config b/config/Symphony.config index c13d01ea..a920ad30 100644 --- a/config/Symphony.config +++ b/config/Symphony.config @@ -3,6 +3,7 @@ "autoUpdateUrl": "", "autoUpdateChannel": "latest", "isAutoUpdateEnabled": true, + "forceAutoUpdate": false, "autoUpdateCheckInterval": "30", "enableBrowserLogin": false, "browserLoginAutoConnect": false, diff --git a/spec/registryHandler.spec.ts b/spec/registryHandler.spec.ts new file mode 100644 index 00000000..f1d1e5b3 --- /dev/null +++ b/spec/registryHandler.spec.ts @@ -0,0 +1,44 @@ +import { retrieveWindowsRegistry } from '../src/app/registry-handler'; +import { + EChannelRegistry, + RegistryStore, +} from '../src/app/stores/registry-store'; + +let mockChannel = { value: '', type: 'REG_SZ' }; + +jest.mock('winreg', () => { + return jest.fn().mockImplementation(() => { + return { + get: (_file, callback) => callback(null, mockChannel), + }; + }); +}); + +jest.mock('../src/common/env', () => { + return { + isWindowsOS: true, + isLinux: false, + isMac: false, + isDevEnv: true, + }; +}); + +describe('Windows Registry', () => { + beforeEach(() => { + jest.clearAllMocks().resetModules(); + }); + + it('it should return channel - latest', async () => { + mockChannel.value = 'latest'; + await retrieveWindowsRegistry(); + const registry = RegistryStore.getRegistry(); + expect(registry.currentChannel).toBe(EChannelRegistry.LATEST); + }); + + it('it should return channel - beta', async () => { + mockChannel.value = 'beta'; + await retrieveWindowsRegistry(); + const registry = RegistryStore.getRegistry(); + expect(registry.currentChannel).toBe(EChannelRegistry.BETA); + }); +}); diff --git a/src/app/auto-update-handler.ts b/src/app/auto-update-handler.ts index efdb4d66..7ca0bc63 100644 --- a/src/app/auto-update-handler.ts +++ b/src/app/auto-update-handler.ts @@ -7,6 +7,8 @@ import { logger } from '../common/logger'; import { isUrl } from '../common/utils'; import { whitelistHandler } from '../common/whitelist-handler'; import { config } from './config-handler'; +import { retrieveWindowsRegistry } from './registry-handler'; +import { EChannelRegistry, RegistryStore } from './stores/registry-store'; import { windowHandler } from './window-handler'; const DEFAULT_AUTO_UPDATE_CHANNEL = 'client-bff/sda-update'; @@ -23,122 +25,139 @@ export class AutoUpdate { private autoUpdateTrigger: AutoUpdateTrigger | undefined = undefined; constructor() { - const opts = this.getGenericServerOptions(); - if (isMac) { - this.autoUpdater = new MacUpdater(opts); - } else if (isWindowsOS) { - this.autoUpdater = new NsisUpdater(opts); - } + this.getGenericServerOptions().then((opts) => { + if (isMac) { + this.autoUpdater = new MacUpdater(opts); + } else if (isWindowsOS) { + this.autoUpdater = new NsisUpdater(opts); + } - if (this.autoUpdater) { - this.autoUpdater.logger = electronLog; - this.autoUpdater.autoDownload = false; - this.autoUpdater.autoInstallOnAppQuit = true; - this.autoUpdater.allowDowngrade = true; + if (this.autoUpdater) { + this.autoUpdater.logger = electronLog; + this.autoUpdater.autoDownload = false; + this.autoUpdater.autoInstallOnAppQuit = true; + this.autoUpdater.allowDowngrade = true; - this.autoUpdater.on('update-not-available', () => { - if (this.autoUpdateTrigger === AutoUpdateTrigger.AUTOMATED) { - logger.info( - 'auto-update-handler: no update available found with automatic check', - ); + this.autoUpdater.on('update-not-available', () => { + if (this.autoUpdateTrigger === AutoUpdateTrigger.AUTOMATED) { + logger.info( + 'auto-update-handler: no update available found with automatic check', + ); + this.autoUpdateTrigger = undefined; + return; + } + const mainWebContents = windowHandler.mainWebContents; + // Display client banner + if (mainWebContents && !mainWebContents.isDestroyed()) { + mainWebContents.send('display-client-banner', { + reason: 'autoUpdate', + action: 'update-not-available', + }); + } this.autoUpdateTrigger = undefined; - return; - } - const mainWebContents = windowHandler.mainWebContents; - // Display client banner - if (mainWebContents && !mainWebContents.isDestroyed()) { - mainWebContents.send('display-client-banner', { - reason: 'autoUpdate', - action: 'update-not-available', - }); - } - this.autoUpdateTrigger = undefined; - }); + }); - const { autoUpdateChannel } = config.getConfigFields([ - 'autoUpdateChannel', - ]); - const { installVariant } = config.getConfigFields(['installVariant']); - this.autoUpdater.on('update-available', (info) => { - const mainWebContents = windowHandler.mainWebContents; - // Display client banner - if (mainWebContents && !mainWebContents.isDestroyed()) { - mainWebContents.send('display-client-banner', { - reason: 'autoUpdate', - action: 'update-available', - data: { - ...info, - autoUpdateTrigger: this.autoUpdateTrigger, - autoUpdateChannel, - installVariant, - channelConfigLocation: null, - sessionStartDatetime: null, - machineStartDatetime: null, - machineId: null, - }, - }); - } - }); + const { autoUpdateChannel } = config.getConfigFields([ + 'autoUpdateChannel', + ]); + let finalAutoUpdateChannel = autoUpdateChannel; - this.autoUpdater.on('download-progress', (info) => { - const mainWebContents = windowHandler.mainWebContents; - // Display client banner - if ( - mainWebContents && - !mainWebContents.isDestroyed() && - !this.didPublishDownloadProgress - ) { - mainWebContents.send('display-client-banner', { - reason: 'autoUpdate', - action: 'download-progress', - data: { - ...info, - autoUpdateTrigger: this.autoUpdateTrigger, - autoUpdateChannel, - installVariant, - channelConfigLocation: null, - sessionStartDatetime: null, - machineStartDatetime: null, - machineId: null, - }, - }); - this.didPublishDownloadProgress = true; - } - }); + if (isWindowsOS) { + const registryAutoUpdate = RegistryStore.getRegistry(); + const identifiedChannelFromRegistry = [ + EChannelRegistry.BETA, + EChannelRegistry.LATEST, + ].includes(registryAutoUpdate.currentChannel) + ? registryAutoUpdate.currentChannel + : ''; - this.autoUpdater.on('update-downloaded', (info) => { - this.isUpdateAvailable = true; - const mainWebContents = windowHandler.mainWebContents; - // Display client banner - if (mainWebContents && !mainWebContents.isDestroyed()) { - mainWebContents.send('display-client-banner', { - reason: 'autoUpdate', - action: 'update-downloaded', - data: { - ...info, - autoUpdateTrigger: this.autoUpdateTrigger, - autoUpdateChannel, - installVariant, - channelConfigLocation: null, - sessionStartDatetime: null, - machineStartDatetime: null, - machineId: null, - }, - }); + if (identifiedChannelFromRegistry) { + finalAutoUpdateChannel = identifiedChannelFromRegistry; + } } - if (isMac) { - config.backupGlobalConfig(); - } - }); - this.autoUpdater.on('error', (error) => { - this.autoUpdateTrigger = undefined; - logger.error( - 'auto-update-handler: Error occurred while updating. ', - error, - ); - }); - } + const { installVariant } = config.getConfigFields(['installVariant']); + this.autoUpdater.on('update-available', (info) => { + const mainWebContents = windowHandler.mainWebContents; + // Display client banner + if (mainWebContents && !mainWebContents.isDestroyed()) { + mainWebContents.send('display-client-banner', { + reason: 'autoUpdate', + action: 'update-available', + data: { + ...info, + autoUpdateTrigger: this.autoUpdateTrigger, + autoUpdateChannel: finalAutoUpdateChannel, + installVariant, + channelConfigLocation: null, + sessionStartDatetime: null, + machineStartDatetime: null, + machineId: null, + }, + }); + } + }); + + this.autoUpdater.on('download-progress', (info) => { + const mainWebContents = windowHandler.mainWebContents; + // Display client banner + if ( + mainWebContents && + !mainWebContents.isDestroyed() && + !this.didPublishDownloadProgress + ) { + mainWebContents.send('display-client-banner', { + reason: 'autoUpdate', + action: 'download-progress', + data: { + ...info, + autoUpdateTrigger: this.autoUpdateTrigger, + autoUpdateChannel: finalAutoUpdateChannel, + installVariant, + channelConfigLocation: null, + sessionStartDatetime: null, + machineStartDatetime: null, + machineId: null, + }, + }); + this.didPublishDownloadProgress = true; + } + }); + + this.autoUpdater.on('update-downloaded', (info) => { + this.isUpdateAvailable = true; + const mainWebContents = windowHandler.mainWebContents; + // Display client banner + if (mainWebContents && !mainWebContents.isDestroyed()) { + mainWebContents.send('display-client-banner', { + reason: 'autoUpdate', + action: 'update-downloaded', + data: { + ...info, + autoUpdateTrigger: this.autoUpdateTrigger, + autoUpdateChannel: finalAutoUpdateChannel, + installVariant, + channelConfigLocation: null, + sessionStartDatetime: null, + machineStartDatetime: null, + machineId: null, + }, + }); + } + if (isMac) { + config.backupGlobalConfig(); + } + }); + + this.autoUpdater.on('error', (error) => { + this.autoUpdateTrigger = undefined; + logger.error( + 'auto-update-handler: Error occurred while updating. ', + error, + ); + }); + } + }); } /** @@ -173,7 +192,7 @@ export class AutoUpdate { this.autoUpdateTrigger = trigger; logger.info('auto-update-handler: Checking for updates', trigger); if (this.autoUpdater) { - const opts: GenericServerOptions = this.getGenericServerOptions(); + const opts: GenericServerOptions = await this.getGenericServerOptions(); this.autoUpdater.setFeedURL(opts); const updateCheckResult = await this.autoUpdater.checkForUpdates(); logger.info('auto-update-handler: ', updateCheckResult); @@ -221,16 +240,31 @@ export class AutoUpdate { return updateUrl; }; - private getGenericServerOptions = (): GenericServerOptions => { + private getGenericServerOptions = async (): Promise => { let userAutoUpdateChannel; const { autoUpdateChannel, betaAutoUpdateChannelEnabled } = config.getConfigFields([ 'autoUpdateChannel', 'betaAutoUpdateChannelEnabled', ]); + userAutoUpdateChannel = betaAutoUpdateChannelEnabled ? 'beta' : autoUpdateChannel; + if (isWindowsOS) { + await retrieveWindowsRegistry(); + const registryAutoUpdate = RegistryStore.getRegistry(); + const identifiedChannelFromRegistry = [ + EChannelRegistry.BETA, + EChannelRegistry.LATEST, + ].includes(registryAutoUpdate.currentChannel) + ? registryAutoUpdate.currentChannel + : ''; + if (identifiedChannelFromRegistry) { + userAutoUpdateChannel = identifiedChannelFromRegistry; + } + } + logger.info(`auto-update-handler: using channel ${userAutoUpdateChannel}`); const opts: GenericServerOptions = { diff --git a/src/app/registry-handler.ts b/src/app/registry-handler.ts new file mode 100644 index 00000000..c4f29074 --- /dev/null +++ b/src/app/registry-handler.ts @@ -0,0 +1,61 @@ +import { logger } from '../common/logger'; +import { RegistryStore } from './stores/registry-store'; + +enum RegistryValueType { + REG_SZ = 'REG_SZ', +} + +const CHANNEL_NEST_LOCATION = '\\SOFTWARE\\Policies\\Symphony\\Update'; +const CHANNEL_KEY = 'channel'; + +export const retrieveWindowsRegistry = async (): Promise => { + const Registry = require('winreg'); + const registryLocalStore = RegistryStore; + const fetchLogic = (err, channel) => { + if (err) { + logger.info('registry-handler: error occurred. Details: ', err); + + return 'An error has occurred'; + } else { + if (channel.type === RegistryValueType.REG_SZ) { + registryLocalStore.setRegistry({ currentChannel: channel.value }); + logger.info( + 'registry-handler: value retrieved successfully, send to Registry Store', + ); + + return channel.value; + } else { + logger.info( + 'registry-handler: the value was looked for did not exist or its VALUE_TYPE is incorrect', + ); + + return 'Key Value doesnt exist'; + } + } + }; + + const regKeyLocal = new Registry({ + hive: Registry.HKLM, + key: CHANNEL_NEST_LOCATION, + }); + + const regKeyUser = new Registry({ + hive: Registry.HKCU, + key: CHANNEL_NEST_LOCATION, + }); + + return regKeyUser.get(CHANNEL_KEY, (error, channel) => { + if (error && !channel) { + regKeyLocal.get(CHANNEL_KEY, (err, localChannel) => { + return fetchLogic(err, localChannel); + }); + } else if (channel.type === RegistryValueType.REG_SZ) { + registryLocalStore.setRegistry({ currentChannel: channel.value }); + logger.info( + 'registry-handler: value retrieved successfully, send to Registry Store', + ); + + return channel; + } + }); +}; diff --git a/src/app/stores/registry-store.ts b/src/app/stores/registry-store.ts new file mode 100644 index 00000000..a14cc391 --- /dev/null +++ b/src/app/stores/registry-store.ts @@ -0,0 +1,28 @@ +export interface IRegistry { + currentChannel: string | 'beta' | 'latest'; +} + +export const EChannelRegistry = { + /** + * Has higher authority over autoUpdateChannel, utilized to set Update Channel to LATEST + */ + LATEST: 'latest', + + /** + * Has higher authority over autoUpdateChannel, utilized to set Update Channel to BETA + */ + BETA: 'beta', +}; + +class Registry { + private registry: IRegistry = { currentChannel: '' }; + public getRegistry = (): IRegistry => { + return { ...this.registry }; + }; + + public setRegistry = (newRegistry: IRegistry) => { + this.registry = { ...this.registry, ...newRegistry }; + }; +} + +export const RegistryStore = new Registry();