Simplify settings

Change-type: patch
This commit is contained in:
Alexis Svinartchouk 2020-04-29 17:36:36 +02:00
parent ba39ff433d
commit ffe281f25d
18 changed files with 348 additions and 518 deletions

View File

@ -165,17 +165,16 @@ const COMPUTE_MODULE_DESCRIPTIONS: _.Dictionary<string> = {
[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<void> {
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'),

View File

@ -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) {

View File

@ -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);
}

View File

@ -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: (
<span>
Unsafe mode{' '}
<Badge danger fontSize={12}>
Dangerous
</Badge>
</span>
),
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<Setting[]> {
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: (
<span>
Unsafe mode{' '}
<Badge danger fontSize={12}>
Dangerous
</Badge>
</span>
),
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<any>,
React.Dispatch<React.SetStateAction<_.Dictionary<any>>>,
] = useState(settings.getAll());
const [warning, setWarning]: [
any,
React.Dispatch<React.SetStateAction<any>>,
] = 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.SetStateAction<Setting[]>>,
] = React.useState([]);
React.useEffect(() => {
(async () => {
if (settingsList.length === 0) {
setCurrentSettingsList(await getSettingsList());
}
})();
});
const [currentSettings, setCurrentSettings]: [
_.Dictionary<boolean>,
React.Dispatch<React.SetStateAction<_.Dictionary<boolean>>>,
] = React.useState({});
React.useEffect(() => {
(async () => {
if (_.isEmpty(currentSettings)) {
setCurrentSettings(await settings.getAll());
}
})();
});
const [warning, setWarning]: [
any,
React.Dispatch<React.SetStateAction<any>>,
] = 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 (
<Modal
id="settings-modal"
title="Settings"
done={() => toggleModal(false)}
style={{
width: 780,
height: 420,
}}
>
// Show warning since it's a dangerous setting
setWarning({
setting,
settingValue: value,
...options,
});
};
return (
<Modal
id="settings-modal"
title="Settings"
done={() => toggleModal(false)}
style={{
width: 780,
height: 420,
}}
>
<div>
{_.map(settingsList, (setting: Setting, i: number) => {
return setting.hide ? null : (
<div key={setting.name}>
<Checkbox
toggle
tabIndex={6 + i}
label={setting.label}
checked={currentSettings[setting.name]}
onChange={() => toggleSetting(setting.name, setting.options)}
/>
</div>
);
})}
<div>
{_.map(settingsList, (setting: Setting, i: number) => {
return setting.hide ? null : (
<div key={setting.name}>
<Checkbox
toggle
tabIndex={6 + i}
label={setting.label}
checked={currentSettings[setting.name]}
onChange={() => toggleSetting(setting.name, setting.options)}
/>
</div>
);
})}
<div>
<span
onClick={() =>
openExternal(
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
)
}
>
<FontAwesomeIcon icon={faGithub} /> {version}
</span>
</div>
<span
onClick={() =>
openExternal(
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
)
}
>
<FontAwesomeIcon icon={faGithub} /> {version}
</span>
</div>
</div>
{_.isEmpty(warning) ? null : (
<WarningModal
message={warning.description}
confirmLabel={warning.confirmLabel}
done={() => {
settings.set(warning.setting, !warning.settingValue);
setCurrentSettings({
...currentSettings,
[warning.setting]: true,
});
setWarning({});
}}
cancel={() => {
setWarning({});
}}
/>
)}
</Modal>
);
},
)`
> div:nth-child(3) {
justify-content: center;
}
`;
{_.isEmpty(warning) ? null : (
<WarningModal
message={warning.description}
confirmLabel={warning.confirmLabel}
done={async () => {
await settings.set(warning.setting, !warning.settingValue);
setCurrentSettings({
...currentSettings,
[warning.setting]: true,
});
setWarning({});
}}
cancel={() => {
setWarning({});
}}
/>
)}
</Modal>
);
}

View File

@ -66,7 +66,7 @@ interface DeviceFromState {
device: string;
}
export function init() {
export async function init(): Promise<void> {
// 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));
}

View File

@ -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<any> {
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<any> {
await fs.writeFile(filename, JSON.stringify(data, null, JSON_INDENT));
return data;
}
export async function readAll(): Promise<any> {
return await readConfigFile(CONFIG_PATH);
}
export async function writeAll(settings: any): Promise<any> {
return await writeConfigFile(CONFIG_PATH, settings);
}
export async function clear(): Promise<void> {
await fs.unlink(CONFIG_PATH);
}

View File

@ -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<any>> {
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<any> = {
export async function readAll() {
return await readConfigFile(CONFIG_PATH);
}
// exported for tests
export async function writeConfigFile(
filename: string,
data: _.Dictionary<any>,
): Promise<void> {
await fs.writeFile(filename, JSON.stringify(data, null, JSON_INDENT));
}
const DEFAULT_SETTINGS: _.Dictionary<any> = {
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<void> {
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<void> {
async function load(): Promise<void> {
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<void> {
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<void> {
}
}
/**
* @summary Get a setting value
*/
export function get(key: string): any {
export async function get(key: string): Promise<any> {
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);
}

View File

@ -165,7 +165,7 @@ function storeReducer(
);
const shouldAutoselectAll = Boolean(
settings.get('disableExplicitDriveSelection'),
settings.getSync('disableExplicitDriveSelection'),
);
const AUTOSELECT_DRIVE_COUNT = 1;
const nonStaleSelectedDevices = nonStaleNewState

View File

@ -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<void> {
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;

View File

@ -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[] = [

View File

@ -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);

View File

@ -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;
}

View File

@ -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;
}

View File

@ -53,7 +53,7 @@ const getDriveListLabel = () => {
};
const shouldShowDrivesButton = () => {
return !settings.get('disableExplicitDriveSelection');
return !settings.getSync('disableExplicitDriveSelection');
};
const getDriveSelectionStateSlice = () => ({

View File

@ -175,7 +175,7 @@ export class MainPage extends React.Component<
tabIndex={5}
onClick={() => this.setState({ hideSettings: false })}
/>
{!settings.get('disableExternalLinks') && (
{!settings.getSync('disableExternalLinks') && (
<Icon
icon={<FontAwesomeIcon icon={faQuestionCircle} />}
onClick={() =>

View File

@ -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<void> {
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);
}
}

View File

@ -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.",

View File

@ -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<any>, fn: (err: Error) => void) {
async function checkError(promise: Promise<any>, 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();
});
});
});