diff --git a/lib/gui/app/app.ts b/lib/gui/app/app.ts index 038806ee..18a565fa 100644 --- a/lib/gui/app/app.ts +++ b/lib/gui/app/app.ts @@ -165,17 +165,16 @@ const COMPUTE_MODULE_DESCRIPTIONS: _.Dictionary = { [USB_PRODUCT_ID_BCM2710_BOOT]: 'Compute Module 3', }; -let BLACKLISTED_DRIVES: string[] = []; - -function driveIsAllowed(drive: { +async function driveIsAllowed(drive: { devicePath: string; device: string; raw: string; }) { + const driveBlacklist = (await settings.get('driveBlacklist')) || []; return !( - BLACKLISTED_DRIVES.includes(drive.devicePath) || - BLACKLISTED_DRIVES.includes(drive.device) || - BLACKLISTED_DRIVES.includes(drive.raw) + driveBlacklist.includes(drive.devicePath) || + driveBlacklist.includes(drive.device) || + driveBlacklist.includes(drive.raw) ); } @@ -240,9 +239,9 @@ function getDrives() { return _.keyBy(availableDrives.getDrives() || [], 'device'); } -function addDrive(drive: Drive) { +async function addDrive(drive: Drive) { const preparedDrive = prepareDrive(drive); - if (!driveIsAllowed(preparedDrive)) { + if (!(await driveIsAllowed(preparedDrive))) { return; } const drives = getDrives(); @@ -330,14 +329,8 @@ window.addEventListener('beforeunload', async (event) => { } }); -async function main(): Promise { - try { - await settings.load(); - } catch (error) { - exceptionReporter.report(error); - } - BLACKLISTED_DRIVES = settings.get('driveBlacklist') || []; - ledsInit(); +async function main() { + await ledsInit(); ReactDOM.render( React.createElement(MainPage), document.getElementById('main'), diff --git a/lib/gui/app/components/featured-project/featured-project.tsx b/lib/gui/app/components/featured-project/featured-project.tsx index 98d39d54..9aab2d2c 100644 --- a/lib/gui/app/components/featured-project/featured-project.tsx +++ b/lib/gui/app/components/featured-project/featured-project.tsx @@ -37,10 +37,10 @@ export class FeaturedProject extends React.Component< this.state = { endpoint: null }; } - public componentDidMount() { + public async componentDidMount() { try { const endpoint = - settings.get('featuredProjectEndpoint') || + (await settings.get('featuredProjectEndpoint')) || 'https://assets.balena.io/etcher-featured/index.html'; this.setState({ endpoint }); } catch (error) { diff --git a/lib/gui/app/components/safe-webview/safe-webview.tsx b/lib/gui/app/components/safe-webview/safe-webview.tsx index 069de7c9..234d7f23 100644 --- a/lib/gui/app/components/safe-webview/safe-webview.tsx +++ b/lib/gui/app/components/safe-webview/safe-webview.tsx @@ -91,7 +91,7 @@ export class SafeWebview extends React.PureComponent< url.searchParams.set(API_VERSION_PARAM, API_VERSION); url.searchParams.set( OPT_OUT_ANALYTICS_PARAM, - (!settings.get('errorReporting')).toString(), + (!settings.getSync('errorReporting')).toString(), ); this.entryHref = url.href; // Events steal 'this' @@ -192,15 +192,13 @@ export class SafeWebview extends React.PureComponent< } // Open link in browser if it's opened as a 'foreground-tab' - public static newWindow(event: electron.NewWindowEvent) { + public static async newWindow(event: electron.NewWindowEvent) { const url = new window.URL(event.url); if ( - _.every([ - url.protocol === 'http:' || url.protocol === 'https:', - event.disposition === 'foreground-tab', - // Don't open links if they're disabled by the env var - !settings.get('disableExternalLinks'), - ]) + (url.protocol === 'http:' || url.protocol === 'https:') && + event.disposition === 'foreground-tab' && + // Don't open links if they're disabled by the env var + !(await settings.get('disableExternalLinks')) ) { electron.shell.openExternal(url.href); } diff --git a/lib/gui/app/components/settings/settings.tsx b/lib/gui/app/components/settings/settings.tsx index 7b0b2f4a..ef0f2d1a 100644 --- a/lib/gui/app/components/settings/settings.tsx +++ b/lib/gui/app/components/settings/settings.tsx @@ -20,14 +20,12 @@ import * as _ from 'lodash'; import * as os from 'os'; import * as React from 'react'; import { Badge, Checkbox, Modal } from 'rendition'; -import styled from 'styled-components'; import { version } from '../../../../../package.json'; import * as settings from '../../models/settings'; import * as analytics from '../../modules/analytics'; import { open as openExternal } from '../../os/open-external/services/open-external'; -const { useState } = React; const platform = os.platform(); interface WarningModalProps { @@ -67,150 +65,164 @@ interface Setting { hide?: boolean; } -const settingsList: Setting[] = [ - { - name: 'errorReporting', - label: 'Anonymously report errors and usage statistics to balena.io', - }, - { - name: 'unmountOnSuccess', - /** - * On Windows, "Unmounting" basically means "ejecting". - * On top of that, Windows users are usually not even - * familiar with the meaning of "unmount", which comes - * from the UNIX world. - */ - label: `${platform === 'win32' ? 'Eject' : 'Auto-unmount'} on success`, - }, - { - name: 'validateWriteOnSuccess', - label: 'Validate write on success', - }, - { - name: 'updatesEnabled', - label: 'Auto-updates enabled', - }, - { - name: 'unsafeMode', - label: ( - - Unsafe mode{' '} - - Dangerous - - - ), - options: { - description: `Are you sure you want to turn this on? - You will be able to overwrite your system drives if you're not careful.`, - confirmLabel: 'Enable unsafe mode', +async function getSettingsList(): Promise { + return [ + { + name: 'errorReporting', + label: 'Anonymously report errors and usage statistics to balena.io', }, - hide: settings.get('disableUnsafeMode'), - }, -]; + { + name: 'unmountOnSuccess', + /** + * On Windows, "Unmounting" basically means "ejecting". + * On top of that, Windows users are usually not even + * familiar with the meaning of "unmount", which comes + * from the UNIX world. + */ + label: `${platform === 'win32' ? 'Eject' : 'Auto-unmount'} on success`, + }, + { + name: 'validateWriteOnSuccess', + label: 'Validate write on success', + }, + { + name: 'updatesEnabled', + label: 'Auto-updates enabled', + }, + { + name: 'unsafeMode', + label: ( + + Unsafe mode{' '} + + Dangerous + + + ), + options: { + description: `Are you sure you want to turn this on? + You will be able to overwrite your system drives if you're not careful.`, + confirmLabel: 'Enable unsafe mode', + }, + hide: await settings.get('disableUnsafeMode'), + }, + ]; +} interface SettingsModalProps { toggleModal: (value: boolean) => void; } -export const SettingsModal: any = styled( - ({ toggleModal }: SettingsModalProps) => { - const [currentSettings, setCurrentSettings]: [ - _.Dictionary, - React.Dispatch>>, - ] = useState(settings.getAll()); - const [warning, setWarning]: [ - any, - React.Dispatch>, - ] = useState({}); - - const toggleSetting = async (setting: string, options?: any) => { - const value = currentSettings[setting]; - const dangerous = !_.isUndefined(options); - - analytics.logEvent('Toggle setting', { - setting, - value, - dangerous, - }); - - if (value || !dangerous) { - await settings.set(setting, !value); - setCurrentSettings({ - ...currentSettings, - [setting]: !value, - }); - setWarning({}); - return; +export function SettingsModal({ toggleModal }: SettingsModalProps) { + const [settingsList, setCurrentSettingsList]: [ + Setting[], + React.Dispatch>, + ] = React.useState([]); + React.useEffect(() => { + (async () => { + if (settingsList.length === 0) { + setCurrentSettingsList(await getSettingsList()); } + })(); + }); + const [currentSettings, setCurrentSettings]: [ + _.Dictionary, + React.Dispatch>>, + ] = React.useState({}); + React.useEffect(() => { + (async () => { + if (_.isEmpty(currentSettings)) { + setCurrentSettings(await settings.getAll()); + } + })(); + }); + const [warning, setWarning]: [ + any, + React.Dispatch>, + ] = React.useState({}); - // Show warning since it's a dangerous setting - setWarning({ - setting, - settingValue: value, - ...options, + const toggleSetting = async (setting: string, options?: any) => { + const value = currentSettings[setting]; + const dangerous = !_.isUndefined(options); + + analytics.logEvent('Toggle setting', { + setting, + value, + dangerous, + }); + + if (value || !dangerous) { + await settings.set(setting, !value); + setCurrentSettings({ + ...currentSettings, + [setting]: !value, }); - }; + setWarning({}); + return; + } - return ( - toggleModal(false)} - style={{ - width: 780, - height: 420, - }} - > + // Show warning since it's a dangerous setting + setWarning({ + setting, + settingValue: value, + ...options, + }); + }; + + return ( + toggleModal(false)} + style={{ + width: 780, + height: 420, + }} + > +
+ {_.map(settingsList, (setting: Setting, i: number) => { + return setting.hide ? null : ( +
+ toggleSetting(setting.name, setting.options)} + /> +
+ ); + })}
- {_.map(settingsList, (setting: Setting, i: number) => { - return setting.hide ? null : ( -
- toggleSetting(setting.name, setting.options)} - /> -
- ); - })} -
- - openExternal( - 'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md', - ) - } - > - {version} - -
+ + openExternal( + 'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md', + ) + } + > + {version} +
+
- {_.isEmpty(warning) ? null : ( - { - settings.set(warning.setting, !warning.settingValue); - setCurrentSettings({ - ...currentSettings, - [warning.setting]: true, - }); - setWarning({}); - }} - cancel={() => { - setWarning({}); - }} - /> - )} -
- ); - }, -)` - > div:nth-child(3) { - justify-content: center; - } -`; + {_.isEmpty(warning) ? null : ( + { + await settings.set(warning.setting, !warning.settingValue); + setCurrentSettings({ + ...currentSettings, + [warning.setting]: true, + }); + setWarning({}); + }} + cancel={() => { + setWarning({}); + }} + /> + )} +
+ ); +} diff --git a/lib/gui/app/models/leds.ts b/lib/gui/app/models/leds.ts index 71747abc..406c2566 100644 --- a/lib/gui/app/models/leds.ts +++ b/lib/gui/app/models/leds.ts @@ -66,7 +66,7 @@ interface DeviceFromState { device: string; } -export function init() { +export async function init(): Promise { // ledsMapping is something like: // { // 'platform-xhci-hcd.0.auto-usb-0:1.1.1:1.0-scsi-0:0:0:0': [ @@ -77,7 +77,7 @@ export function init() { // ... // } const ledsMapping: _.Dictionary<[string, string, string]> = - settings.get('ledsMapping') || {}; + (await settings.get('ledsMapping')) || {}; for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) { leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames)); } diff --git a/lib/gui/app/models/local-settings.ts b/lib/gui/app/models/local-settings.ts deleted file mode 100644 index 2a9c8439..00000000 --- a/lib/gui/app/models/local-settings.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as electron from 'electron'; -import { promises as fs } from 'fs'; -import * as path from 'path'; - -const JSON_INDENT = 2; - -/** - * @summary Userdata directory path - * @description - * Defaults to the following: - * - `%APPDATA%/etcher` on Windows - * - `$XDG_CONFIG_HOME/etcher` or `~/.config/etcher` on Linux - * - `~/Library/Application Support/etcher` on macOS - * See https://electronjs.org/docs/api/app#appgetpathname - * - * NOTE: The ternary is due to this module being loaded both, - * Electron's main process and renderer process - */ -const USER_DATA_DIR = electron.app - ? electron.app.getPath('userData') - : electron.remote.app.getPath('userData'); - -const CONFIG_PATH = path.join(USER_DATA_DIR, 'config.json'); - -async function readConfigFile(filename: string): Promise { - let contents = '{}'; - try { - contents = await fs.readFile(filename, { encoding: 'utf8' }); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - } - try { - return JSON.parse(contents); - } catch (parseError) { - console.error(parseError); - return {}; - } -} - -async function writeConfigFile(filename: string, data: any): Promise { - await fs.writeFile(filename, JSON.stringify(data, null, JSON_INDENT)); - return data; -} - -export async function readAll(): Promise { - return await readConfigFile(CONFIG_PATH); -} - -export async function writeAll(settings: any): Promise { - return await writeConfigFile(CONFIG_PATH, settings); -} - -export async function clear(): Promise { - await fs.unlink(CONFIG_PATH); -} diff --git a/lib/gui/app/models/settings.ts b/lib/gui/app/models/settings.ts index ae24b290..a4b00eee 100644 --- a/lib/gui/app/models/settings.ts +++ b/lib/gui/app/models/settings.ts @@ -15,56 +15,93 @@ */ import * as _debug from 'debug'; +import * as electron from 'electron'; import * as _ from 'lodash'; +import { promises as fs } from 'fs'; +import { join } from 'path'; import * as packageJSON from '../../../../package.json'; -import * as localSettings from './local-settings'; const debug = _debug('etcher:models:settings'); +const JSON_INDENT = 2; + +/** + * @summary Userdata directory path + * @description + * Defaults to the following: + * - `%APPDATA%/etcher` on Windows + * - `$XDG_CONFIG_HOME/etcher` or `~/.config/etcher` on Linux + * - `~/Library/Application Support/etcher` on macOS + * See https://electronjs.org/docs/api/app#appgetpathname + * + * NOTE: The ternary is due to this module being loaded both, + * Electron's main process and renderer process + */ +const USER_DATA_DIR = electron.app + ? electron.app.getPath('userData') + : electron.remote.app.getPath('userData'); + +const CONFIG_PATH = join(USER_DATA_DIR, 'config.json'); + +async function readConfigFile(filename: string): Promise<_.Dictionary> { + let contents = '{}'; + try { + contents = await fs.readFile(filename, { encoding: 'utf8' }); + } catch (error) { + // noop + } + try { + return JSON.parse(contents); + } catch (parseError) { + console.error(parseError); + return {}; + } +} + // exported for tests -export const DEFAULT_SETTINGS: _.Dictionary = { +export async function readAll() { + return await readConfigFile(CONFIG_PATH); +} + +// exported for tests +export async function writeConfigFile( + filename: string, + data: _.Dictionary, +): Promise { + await fs.writeFile(filename, JSON.stringify(data, null, JSON_INDENT)); +} + +const DEFAULT_SETTINGS: _.Dictionary = { unsafeMode: false, errorReporting: true, unmountOnSuccess: true, validateWriteOnSuccess: true, - updatesEnabled: - packageJSON.updates.enabled && - !_.includes(['rpm', 'deb'], packageJSON.packageType), + updatesEnabled: !_.includes(['rpm', 'deb'], packageJSON.packageType), desktopNotifications: true, autoBlockmapping: true, decompressFirst: true, }; -let settings = _.cloneDeep(DEFAULT_SETTINGS); +const settings = _.cloneDeep(DEFAULT_SETTINGS); -/** - * @summary Reset settings to their default values - */ -export async function reset(): Promise { - debug('reset'); - settings = _.cloneDeep(DEFAULT_SETTINGS); - return await localSettings.writeAll(settings); -} - -/** - * @summary Extend the application state with the local settings - */ -export async function load(): Promise { +async function load(): Promise { debug('load'); - const loadedSettings = await localSettings.readAll(); + // Use exports.readAll() so it can be mocked in tests + const loadedSettings = await exports.readAll(); _.assign(settings, loadedSettings); } -/** - * @summary Set a setting value - */ +const loaded = load(); + export async function set(key: string, value: any): Promise { debug('set', key, value); + await loaded; const previousValue = settings[key]; settings[key] = value; try { - await localSettings.writeAll(settings); + // Use exports.writeConfigFile() so it can be mocked in tests + await exports.writeConfigFile(CONFIG_PATH, settings); } catch (error) { // Revert to previous value if persisting settings failed settings[key] = previousValue; @@ -72,24 +109,17 @@ export async function set(key: string, value: any): Promise { } } -/** - * @summary Get a setting value - */ -export function get(key: string): any { +export async function get(key: string): Promise { + await loaded; + return getSync(key); +} + +export function getSync(key: string): any { return _.cloneDeep(settings[key]); } -/** - * @summary Check if setting value exists - */ -export function has(key: string): boolean { - return settings[key] != null; -} - -/** - * @summary Get all setting values - */ -export function getAll() { +export async function getAll() { debug('getAll'); + await loaded; return _.cloneDeep(settings); } diff --git a/lib/gui/app/models/store.ts b/lib/gui/app/models/store.ts index b9789a2c..b509d292 100644 --- a/lib/gui/app/models/store.ts +++ b/lib/gui/app/models/store.ts @@ -165,7 +165,7 @@ function storeReducer( ); const shouldAutoselectAll = Boolean( - settings.get('disableExplicitDriveSelection'), + settings.getSync('disableExplicitDriveSelection'), ); const AUTOSELECT_DRIVE_COUNT = 1; const nonStaleSelectedDevices = nonStaleNewState diff --git a/lib/gui/app/modules/analytics.ts b/lib/gui/app/modules/analytics.ts index 8cf335a5..1bc95117 100644 --- a/lib/gui/app/modules/analytics.ts +++ b/lib/gui/app/modules/analytics.ts @@ -22,33 +22,29 @@ import { getConfig, hasProps } from '../../../shared/utils'; import * as settings from '../models/settings'; import { store } from '../models/store'; -const sentryToken = - settings.get('analyticsSentryToken') || - _.get(packageJSON, ['analytics', 'sentry', 'token']); -const mixpanelToken = - settings.get('analyticsMixpanelToken') || - _.get(packageJSON, ['analytics', 'mixpanel', 'token']); - -const configUrl = - settings.get('configUrl') || 'https://balena.io/etcher/static/config.json'; - const DEFAULT_PROBABILITY = 0.1; -const services = { - sentry: sentryToken, - mixpanel: mixpanelToken, -}; - -resinCorvus.install({ - services, - options: { - release: packageJSON.version, - shouldReport: () => { - return settings.get('errorReporting'); +async function installCorvus(): Promise { + const sentryToken = + (await settings.get('analyticsSentryToken')) || + _.get(packageJSON, ['analytics', 'sentry', 'token']); + const mixpanelToken = + (await settings.get('analyticsMixpanelToken')) || + _.get(packageJSON, ['analytics', 'mixpanel', 'token']); + resinCorvus.install({ + services: { + sentry: sentryToken, + mixpanel: mixpanelToken, }, - mixpanelDeferred: true, - }, -}); + options: { + release: packageJSON.version, + shouldReport: () => { + return settings.getSync('errorReporting'); + }, + mixpanelDeferred: true, + }, + }); +} let mixpanelSample = DEFAULT_PROBABILITY; @@ -56,8 +52,12 @@ let mixpanelSample = DEFAULT_PROBABILITY; * @summary Init analytics configurations */ async function initConfig() { + await installCorvus(); let validatedConfig = null; try { + const configUrl = + (await settings.get('configUrl')) || + 'https://balena.io/etcher/static/config.json'; const config = await getConfig(configUrl); const mixpanel = _.get(config, ['analytics', 'mixpanel'], {}); mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY; diff --git a/lib/gui/app/modules/drive-scanner.ts b/lib/gui/app/modules/drive-scanner.ts index 5d236e19..bc550433 100644 --- a/lib/gui/app/modules/drive-scanner.ts +++ b/lib/gui/app/modules/drive-scanner.ts @@ -23,7 +23,9 @@ import * as settings from '../models/settings'; * @summary returns true if system drives should be shown */ function includeSystemDrives() { - return settings.get('unsafeMode') && !settings.get('disableUnsafeMode'); + return ( + settings.getSync('unsafeMode') && !settings.getSync('disableUnsafeMode') + ); } const adapters: sdk.scanner.adapters.Adapter[] = [ diff --git a/lib/gui/app/modules/image-writer.ts b/lib/gui/app/modules/image-writer.ts index 6f524fb6..bdf87207 100644 --- a/lib/gui/app/modules/image-writer.ts +++ b/lib/gui/app/modules/image-writer.ts @@ -136,7 +136,7 @@ interface FlashResults { * @description * This function is extracted for testing purposes. */ -export function performWrite( +export async function performWrite( image: string, drives: DrivelistDrive[], onProgress: sdk.multiWrite.OnProgressFunction, @@ -144,7 +144,13 @@ export function performWrite( ): Promise<{ cancelled?: boolean }> { let cancelled = false; ipc.serve(); - return new Promise((resolve, reject) => { + const { + unmountOnSuccess, + validateWriteOnSuccess, + autoBlockmapping, + decompressFirst, + } = await settings.getAll(); + return await new Promise((resolve, reject) => { ipc.server.on('error', (error) => { terminateServer(); const errorObject = errors.fromJSON(error); @@ -162,8 +168,8 @@ export function performWrite( driveCount: drives.length, uuid: flashState.getFlashUuid(), flashInstanceUuid: flashState.getFlashUuid(), - unmountOnSuccess: settings.get('unmountOnSuccess'), - validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), + unmountOnSuccess, + validateWriteOnSuccess, }; ipc.server.on('fail', ({ error }: { error: Error & { code: string } }) => { @@ -190,10 +196,10 @@ export function performWrite( destinations: drives, source, SourceType: source.SourceType.name, - validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), - autoBlockmapping: settings.get('autoBlockmapping'), - unmountOnSuccess: settings.get('unmountOnSuccess'), - decompressFirst: settings.get('decompressFirst'), + validateWriteOnSuccess, + autoBlockmapping, + unmountOnSuccess, + decompressFirst, }); }); @@ -266,8 +272,8 @@ export async function flash( uuid: flashState.getFlashUuid(), status: 'started', flashInstanceUuid: flashState.getFlashUuid(), - unmountOnSuccess: settings.get('unmountOnSuccess'), - validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), + unmountOnSuccess: await settings.get('unmountOnSuccess'), + validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'), }; analytics.logEvent('Flash', analyticsData); @@ -320,7 +326,7 @@ export async function flash( /** * @summary Cancel write operation */ -export function cancel() { +export async function cancel() { const drives = selectionState.getSelectedDevices(); const analyticsData = { image: selectionState.getImagePath(), @@ -328,8 +334,8 @@ export function cancel() { driveCount: drives.length, uuid: flashState.getFlashUuid(), flashInstanceUuid: flashState.getFlashUuid(), - unmountOnSuccess: settings.get('unmountOnSuccess'), - validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), + unmountOnSuccess: await settings.get('unmountOnSuccess'), + validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'), status: 'cancel', }; analytics.logEvent('Cancel', analyticsData); diff --git a/lib/gui/app/os/notification.ts b/lib/gui/app/os/notification.ts index 0a074094..2d6b808e 100644 --- a/lib/gui/app/os/notification.ts +++ b/lib/gui/app/os/notification.ts @@ -21,9 +21,9 @@ import * as settings from '../models/settings'; /** * @summary Send a notification */ -export function send(title: string, body: string, icon: string) { +export async function send(title: string, body: string, icon: string) { // Bail out if desktop notifications are disabled - if (!settings.get('desktopNotifications')) { + if (!(await settings.get('desktopNotifications'))) { return; } diff --git a/lib/gui/app/os/open-external/services/open-external.ts b/lib/gui/app/os/open-external/services/open-external.ts index 6a3e6374..ef7c99c5 100644 --- a/lib/gui/app/os/open-external/services/open-external.ts +++ b/lib/gui/app/os/open-external/services/open-external.ts @@ -21,9 +21,9 @@ import { logEvent } from '../../../modules/analytics'; /** * @summary Open an external resource */ -export function open(url: string) { +export async function open(url: string) { // Don't open links if they're disabled by the env var - if (settings.get('disableExternalLinks')) { + if (await settings.get('disableExternalLinks')) { return; } diff --git a/lib/gui/app/pages/main/DriveSelector.tsx b/lib/gui/app/pages/main/DriveSelector.tsx index 6cebe963..fcd02747 100644 --- a/lib/gui/app/pages/main/DriveSelector.tsx +++ b/lib/gui/app/pages/main/DriveSelector.tsx @@ -53,7 +53,7 @@ const getDriveListLabel = () => { }; const shouldShowDrivesButton = () => { - return !settings.get('disableExplicitDriveSelection'); + return !settings.getSync('disableExplicitDriveSelection'); }; const getDriveSelectionStateSlice = () => ({ diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx index f99fc6d2..257d1a04 100644 --- a/lib/gui/app/pages/main/MainPage.tsx +++ b/lib/gui/app/pages/main/MainPage.tsx @@ -175,7 +175,7 @@ export class MainPage extends React.Component< tabIndex={5} onClick={() => this.setState({ hideSettings: false })} /> - {!settings.get('disableExternalLinks') && ( + {!settings.getSync('disableExternalLinks') && ( } onClick={() => diff --git a/lib/gui/etcher.ts b/lib/gui/etcher.ts index be202280..c644281a 100644 --- a/lib/gui/etcher.ts +++ b/lib/gui/etcher.ts @@ -28,8 +28,6 @@ import * as settings from './app/models/settings'; import * as analytics from './app/modules/analytics'; import { buildWindowMenu } from './menu'; -const configUrl = - settings.get('configUrl') || 'https://balena.io/etcher/static/config.json'; const updatablePackageTypes = ['appimage', 'nsis', 'dmg']; const packageUpdatable = _.includes(updatablePackageTypes, packageType); let packageUpdated = false; @@ -38,7 +36,7 @@ async function checkForUpdates(interval: number) { // We use a while loop instead of a setInterval to preserve // async execution time between each function call while (!packageUpdated) { - if (settings.get('updatesEnabled')) { + if (await settings.get('updatesEnabled')) { try { const release = await autoUpdater.checkForUpdates(); const isOutdated = @@ -56,8 +54,8 @@ async function checkForUpdates(interval: number) { } } -function createMainWindow() { - const fullscreen = Boolean(settings.get('fullscreen')); +async function createMainWindow() { + const fullscreen = Boolean(await settings.get('fullscreen')); const defaultWidth = 800; const defaultHeight = 480; let width = defaultWidth; @@ -116,6 +114,9 @@ function createMainWindow() { }); if (packageUpdatable) { try { + const configUrl = + (await settings.get('configUrl')) || + 'https://balena.io/etcher/static/config.json'; const onlineConfig = await getConfig(configUrl); const autoUpdaterConfig = _.get( onlineConfig, @@ -151,18 +152,10 @@ electron.app.on('before-quit', () => { }); async function main(): Promise { - try { - await settings.load(); - } catch (error) { - // TODO: What do if loading the config fails? - console.error('Error loading settings:'); - console.error(error); - } finally { - if (electron.app.isReady()) { - createMainWindow(); - } else { - electron.app.on('ready', createMainWindow); - } + if (electron.app.isReady()) { + await createMainWindow(); + } else { + electron.app.on('ready', createMainWindow); } } diff --git a/package.json b/package.json index d845dd72..e3c52b89 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,6 @@ "displayName": "balenaEtcher", "version": "1.5.82", "packageType": "local", - "updates": { - "enabled": true, - "sleepDays": 7, - "semverRange": "<2.0.0" - }, "main": "generated/etcher.js", "description": "Flash OS images to SD cards and USB drives, safely and easily.", "productDescription": "Etcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more.", diff --git a/tests/gui/models/settings.spec.ts b/tests/gui/models/settings.spec.ts index 2dc35d9d..3f7bf83a 100644 --- a/tests/gui/models/settings.spec.ts +++ b/tests/gui/models/settings.spec.ts @@ -18,206 +18,80 @@ import { expect } from 'chai'; import * as _ from 'lodash'; import { stub } from 'sinon'; -import * as localSettings from '../../../lib/gui/app/models/local-settings'; import * as settings from '../../../lib/gui/app/models/settings'; -async function checkError(promise: Promise, fn: (err: Error) => void) { +async function checkError(promise: Promise, fn: (err: Error) => any) { try { await promise; } catch (error) { - fn(error); + await fn(error); return; } throw new Error('Expected error was not thrown'); } -describe('Browser: settings', function () { - beforeEach(function () { - return settings.reset(); +describe('Browser: settings', () => { + it('should be able to set and read values', async () => { + expect(await settings.get('foo')).to.be.undefined; + await settings.set('foo', true); + expect(await settings.get('foo')).to.be.true; + await settings.set('foo', false); + expect(await settings.get('foo')).to.be.false; }); - const DEFAULT_SETTINGS = _.cloneDeep(settings.DEFAULT_SETTINGS); - - it('should be able to set and read values', function () { - expect(settings.get('foo')).to.be.undefined; - return settings - .set('foo', true) - .then(() => { - expect(settings.get('foo')).to.be.true; - return settings.set('foo', false); - }) - .then(() => { - expect(settings.get('foo')).to.be.false; - }); - }); - - describe('.reset()', function () { - it('should reset the settings to their default values', function () { - expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS); - return settings - .set('foo', 1234) - .then(() => { - expect(settings.getAll()).to.not.deep.equal(DEFAULT_SETTINGS); - return settings.reset(); - }) - .then(() => { - expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS); - }); - }); - - it('should reset the local settings to their default values', function () { - return settings - .set('foo', 1234) - .then(localSettings.readAll) - .then((data) => { - expect(data).to.not.deep.equal(DEFAULT_SETTINGS); - return settings.reset(); - }) - .then(localSettings.readAll) - .then((data) => { - expect(data).to.deep.equal(DEFAULT_SETTINGS); - }); - }); - - describe('given the local settings are cleared', function () { - beforeEach(function () { - return localSettings.clear(); - }); - - it('should set the local settings to their default values', function () { - return settings - .reset() - .then(localSettings.readAll) - .then((data) => { - expect(data).to.deep.equal(DEFAULT_SETTINGS); - }); - }); - }); - }); - - describe('.set()', function () { - it('should store the settings to the local machine', function () { - return localSettings - .readAll() - .then((data) => { - expect(data.foo).to.be.undefined; - expect(data.bar).to.be.undefined; - return settings.set('foo', 'bar'); - }) - .then(() => { - return settings.set('bar', 'baz'); - }) - .then(localSettings.readAll) - .then((data) => { - expect(data.foo).to.equal('bar'); - expect(data.bar).to.equal('baz'); - }); - }); - - it('should not change the application state if storing to the local machine results in an error', async function () { + describe('.set()', () => { + it('should not change the application state if storing to the local machine results in an error', async () => { await settings.set('foo', 'bar'); - expect(settings.get('foo')).to.equal('bar'); + expect(await settings.get('foo')).to.equal('bar'); - const localSettingsWriteAllStub = stub(localSettings, 'writeAll'); - localSettingsWriteAllStub.returns( - Promise.reject(new Error('localSettings error')), - ); + const writeConfigFileStub = stub(settings, 'writeConfigFile'); + writeConfigFileStub.returns(Promise.reject(new Error('settings error'))); - await checkError(settings.set('foo', 'baz'), (error) => { + const p = settings.set('foo', 'baz'); + await checkError(p, async (error) => { expect(error).to.be.an.instanceof(Error); - expect(error.message).to.equal('localSettings error'); - localSettingsWriteAllStub.restore(); - expect(settings.get('foo')).to.equal('bar'); + expect(error.message).to.equal('settings error'); + expect(await settings.get('foo')).to.equal('bar'); }); + writeConfigFileStub.restore(); }); }); - describe('.load()', function () { - it('should extend the application state with the local settings content', function () { - const object = { - foo: 'bar', - }; - - expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS); - - return localSettings - .writeAll(object) - .then(() => { - expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS); - return settings.load(); - }) - .then(() => { - expect(settings.getAll()).to.deep.equal( - _.assign({}, DEFAULT_SETTINGS, object), - ); - }); + describe('.set()', () => { + it('should set an unknown key', async () => { + expect(await settings.get('foobar')).to.be.undefined; + await settings.set('foobar', true); + expect(await settings.get('foobar')).to.be.true; }); - it('should keep the application state intact if there are no local settings', function () { - expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS); - return localSettings - .clear() - .then(settings.load) - .then(() => { - expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS); - }); - }); - }); - - describe('.set()', function () { - it('should set an unknown key', function () { - expect(settings.get('foobar')).to.be.undefined; - return settings.set('foobar', true).then(() => { - expect(settings.get('foobar')).to.be.true; - }); - }); - - it('should set the key to undefined if no value', function () { - return settings - .set('foo', 'bar') - .then(() => { - expect(settings.get('foo')).to.equal('bar'); - return settings.set('foo', undefined); - }) - .then(() => { - expect(settings.get('foo')).to.be.undefined; - }); - }); - - it('should store the setting to the local machine', function () { - return localSettings - .readAll() - .then((data) => { - expect(data.foo).to.be.undefined; - return settings.set('foo', 'bar'); - }) - .then(localSettings.readAll) - .then((data) => { - expect(data.foo).to.equal('bar'); - }); - }); - - it('should not change the application state if storing to the local machine results in an error', async function () { + it('should set the key to undefined if no value', async () => { await settings.set('foo', 'bar'); - expect(settings.get('foo')).to.equal('bar'); - - const localSettingsWriteAllStub = stub(localSettings, 'writeAll'); - localSettingsWriteAllStub.returns( - Promise.reject(new Error('localSettings error')), - ); - - await checkError(settings.set('foo', 'baz'), (error) => { - expect(error).to.be.an.instanceof(Error); - expect(error.message).to.equal('localSettings error'); - localSettingsWriteAllStub.restore(); - expect(settings.get('foo')).to.equal('bar'); - }); + expect(await settings.get('foo')).to.equal('bar'); + await settings.set('foo', undefined); + expect(await settings.get('foo')).to.be.undefined; }); - }); - describe('.getAll()', function () { - it('should initial return all default values', function () { - expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS); + it('should store the setting to the local machine', async () => { + const data = await settings.readAll(); + expect(data.foo).to.be.undefined; + await settings.set('foo', 'bar'); + const data1 = await settings.readAll(); + expect(data1.foo).to.equal('bar'); + }); + + it('should not change the application state if storing to the local machine results in an error', async () => { + await settings.set('foo', 'bar'); + expect(await settings.get('foo')).to.equal('bar'); + + const writeConfigFileStub = stub(settings, 'writeConfigFile'); + writeConfigFileStub.returns(Promise.reject(new Error('settings error'))); + + await checkError(settings.set('foo', 'baz'), async (error) => { + expect(error).to.be.an.instanceof(Error); + expect(error.message).to.equal('settings error'); + expect(await settings.get('foo')).to.equal('bar'); + }); + writeConfigFileStub.restore(); }); }); });