Merge pull request #3131 from balena-io/decompress-first

Decompress first
This commit is contained in:
Alexis Svinartchouk 2020-04-30 14:04:52 +02:00 committed by GitHub
commit 5d8a211961
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 1710 additions and 2385 deletions

View File

@ -149,7 +149,7 @@ sass:
node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css
lint-ts: lint-ts:
resin-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts
lint-sass: lint-sass:
sass-lint -v lib/gui/app/scss/**/*.scss lib/gui/app/scss/*.scss sass-lint -v lib/gui/app/scss/**/*.scss lib/gui/app/scss/*.scss

View File

@ -33,7 +33,6 @@ import { Actions, observe, store } from './models/store';
import * as analytics from './modules/analytics'; import * as analytics from './modules/analytics';
import { scanner as driveScanner } from './modules/drive-scanner'; import { scanner as driveScanner } from './modules/drive-scanner';
import * as exceptionReporter from './modules/exception-reporter'; import * as exceptionReporter from './modules/exception-reporter';
import { updateLock } from './modules/update-lock';
import * as osDialog from './os/dialog'; import * as osDialog from './os/dialog';
import * as windowProgress from './os/window-progress'; import * as windowProgress from './os/window-progress';
import MainPage from './pages/main/MainPage'; import MainPage from './pages/main/MainPage';
@ -86,33 +85,45 @@ const currentVersion = packageJSON.version;
analytics.logEvent('Application start', { analytics.logEvent('Application start', {
packageType: packageJSON.packageType, packageType: packageJSON.packageType,
version: currentVersion, version: currentVersion,
applicationSessionUuid,
}); });
const debouncedLog = _.debounce(console.log, 1000, { maxWait: 1000 });
function pluralize(word: string, quantity: number) {
return `${quantity} ${word}${quantity === 1 ? '' : 's'}`;
}
observe(() => { observe(() => {
if (!flashState.isFlashing()) { if (!flashState.isFlashing()) {
return; return;
} }
const currentFlashState = flashState.getFlashState(); const currentFlashState = flashState.getFlashState();
const stateType = windowProgress.set(currentFlashState);
!currentFlashState.flashing && currentFlashState.verifying
? `Verifying ${currentFlashState.verifying}`
: `Flashing ${currentFlashState.flashing}`;
let eta = '';
if (currentFlashState.eta !== undefined) {
eta = `eta in ${currentFlashState.eta.toFixed(0)}s`;
}
let active = '';
if (currentFlashState.type !== 'decompressing') {
active = pluralize('device', currentFlashState.active);
}
// NOTE: There is usually a short time period between the `isFlashing()` // NOTE: There is usually a short time period between the `isFlashing()`
// property being set, and the flashing actually starting, which // property being set, and the flashing actually starting, which
// might cause some non-sense flashing state logs including // might cause some non-sense flashing state logs including
// `undefined` values. // `undefined` values.
analytics.logDebug( debouncedLog(outdent({ newline: ' ' })`
`${stateType} devices, ` + ${_.capitalize(currentFlashState.type)}
`${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s ` + ${active},
`(total ${currentFlashState.totalSpeed} MB/s) ` + ${currentFlashState.percentage}%
`eta in ${currentFlashState.eta}s ` + at
`with ${currentFlashState.failed} failed devices`, ${(currentFlashState.speed || 0).toFixed(2)}
); MB/s
(total ${(currentFlashState.speed * currentFlashState.active).toFixed(2)} MB/s)
windowProgress.set(currentFlashState); ${eta}
with
${pluralize('failed device', currentFlashState.failed)}
`);
}); });
/** /**
@ -154,17 +165,16 @@ const COMPUTE_MODULE_DESCRIPTIONS: _.Dictionary<string> = {
[USB_PRODUCT_ID_BCM2710_BOOT]: 'Compute Module 3', [USB_PRODUCT_ID_BCM2710_BOOT]: 'Compute Module 3',
}; };
let BLACKLISTED_DRIVES: string[] = []; async function driveIsAllowed(drive: {
function driveIsAllowed(drive: {
devicePath: string; devicePath: string;
device: string; device: string;
raw: string; raw: string;
}) { }) {
const driveBlacklist = (await settings.get('driveBlacklist')) || [];
return !( return !(
BLACKLISTED_DRIVES.includes(drive.devicePath) || driveBlacklist.includes(drive.devicePath) ||
BLACKLISTED_DRIVES.includes(drive.device) || driveBlacklist.includes(drive.device) ||
BLACKLISTED_DRIVES.includes(drive.raw) driveBlacklist.includes(drive.raw)
); );
} }
@ -185,7 +195,7 @@ function prepareDrive(drive: Drive) {
// @ts-ignore // @ts-ignore
drive.progress = 0; drive.progress = 0;
drive.disabled = true; drive.disabled = true;
drive.on('progress', progress => { drive.on('progress', (progress) => {
updateDriveProgress(drive, progress); updateDriveProgress(drive, progress);
}); });
return drive; return drive;
@ -229,9 +239,9 @@ function getDrives() {
return _.keyBy(availableDrives.getDrives() || [], 'device'); return _.keyBy(availableDrives.getDrives() || [], 'device');
} }
function addDrive(drive: Drive) { async function addDrive(drive: Drive) {
const preparedDrive = prepareDrive(drive); const preparedDrive = prepareDrive(drive);
if (!driveIsAllowed(preparedDrive)) { if (!(await driveIsAllowed(preparedDrive))) {
return; return;
} }
const drives = getDrives(); const drives = getDrives();
@ -262,7 +272,7 @@ function updateDriveProgress(
driveScanner.on('attach', addDrive); driveScanner.on('attach', addDrive);
driveScanner.on('detach', removeDrive); driveScanner.on('detach', removeDrive);
driveScanner.on('error', error => { driveScanner.on('error', (error) => {
// Stop the drive scanning loop in case of errors, // Stop the drive scanning loop in case of errors,
// otherwise we risk presenting the same error over // otherwise we risk presenting the same error over
// and over again to the user, while also heavily // and over again to the user, while also heavily
@ -276,11 +286,10 @@ driveScanner.start();
let popupExists = false; let popupExists = false;
window.addEventListener('beforeunload', async event => { window.addEventListener('beforeunload', async (event) => {
if (!flashState.isFlashing() || popupExists) { if (!flashState.isFlashing() || popupExists) {
analytics.logEvent('Close application', { analytics.logEvent('Close application', {
isFlashing: flashState.isFlashing(), isFlashing: flashState.isFlashing(),
applicationSessionUuid,
}); });
return; return;
} }
@ -291,10 +300,7 @@ window.addEventListener('beforeunload', async event => {
// Don't open any more popups // Don't open any more popups
popupExists = true; popupExists = true;
analytics.logEvent('Close attempt while flashing', { analytics.logEvent('Close attempt while flashing');
applicationSessionUuid,
flashingWorkflowUuid,
});
try { try {
const confirmed = await osDialog.showWarning({ const confirmed = await osDialog.showWarning({
@ -306,8 +312,6 @@ window.addEventListener('beforeunload', async event => {
if (confirmed) { if (confirmed) {
analytics.logEvent('Close confirmed while flashing', { analytics.logEvent('Close confirmed while flashing', {
flashInstanceUuid: flashState.getFlashUuid(), flashInstanceUuid: flashState.getFlashUuid(),
applicationSessionUuid,
flashingWorkflowUuid,
}); });
// This circumvents the 'beforeunload' event unlike // This circumvents the 'beforeunload' event unlike
@ -325,24 +329,8 @@ window.addEventListener('beforeunload', async event => {
} }
}); });
function extendLock() { async function main() {
updateLock.extend(); await ledsInit();
}
window.addEventListener('click', extendLock);
window.addEventListener('touchstart', extendLock);
// Initial update lock acquisition
extendLock();
async function main(): Promise<void> {
try {
await settings.load();
} catch (error) {
exceptionReporter.report(error);
}
BLACKLISTED_DRIVES = settings.get('driveBlacklist') || [];
ledsInit();
ReactDOM.render( ReactDOM.render(
React.createElement(MainPage), React.createElement(MainPage),
document.getElementById('main'), document.getElementById('main'),

View File

@ -49,8 +49,6 @@ function toggleDrive(drive: DrivelistDrive) {
analytics.logEvent('Toggle drive', { analytics.logEvent('Toggle drive', {
drive, drive,
previouslySelected: selectionState.isDriveSelected(drive.device), previouslySelected: selectionState.isDriveSelected(drive.device),
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
}); });
selectionState.toggleDrive(drive.device); selectionState.toggleDrive(drive.device);
@ -113,8 +111,6 @@ export function DriveSelectorModal({ close }: { close: () => void }) {
if (drive.link) { if (drive.link) {
analytics.logEvent('Open driver link modal', { analytics.logEvent('Open driver link modal', {
url: drive.link, url: drive.link,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
}); });
setMissingDriversModal({ drive }); setMissingDriversModal({ drive });
} }
@ -131,10 +127,7 @@ export function DriveSelectorModal({ close }: { close: () => void }) {
if (canChangeDriveSelectionState) { if (canChangeDriveSelectionState) {
selectionState.selectDrive(drive.device); selectionState.selectDrive(drive.device);
analytics.logEvent('Drive selected (double click)', { analytics.logEvent('Drive selected (double click)');
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
close(); close();
} }
@ -190,7 +183,7 @@ export function DriveSelectorModal({ close }: { close: () => void }) {
<div <div
className="list-group-item-section list-group-item-section-expanded" className="list-group-item-section list-group-item-section-expanded"
tabIndex={15 + index} tabIndex={15 + index}
onKeyPress={evt => keyboardToggleDrive(drive, evt)} onKeyPress={(evt) => keyboardToggleDrive(drive, evt)}
> >
<h6 className="list-group-item-heading"> <h6 className="list-group-item-heading">
{drive.description} {drive.description}

View File

@ -34,15 +34,15 @@ import {
} from '../../styled-components'; } from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis'; import { middleEllipsis } from '../../utils/middle-ellipsis';
const TargetDetail = styled(props => <Txt.span {...props}></Txt.span>)` const TargetDetail = styled((props) => <Txt.span {...props}></Txt.span>)`
float: ${({ float }) => float}; float: ${({ float }) => float};
`; `;
interface TargetSelectorProps { interface TargetSelectorProps {
targets: any[]; targets: any[];
disabled: boolean; disabled: boolean;
openDriveSelector: () => any; openDriveSelector: () => void;
reselectDrive: () => any; reselectDrive: () => void;
flashing: boolean; flashing: boolean;
show: boolean; show: boolean;
tooltip: string; tooltip: string;

View File

@ -37,10 +37,10 @@ export class FeaturedProject extends React.Component<
this.state = { endpoint: null }; this.state = { endpoint: null };
} }
public componentDidMount() { public async componentDidMount() {
try { try {
const endpoint = const endpoint =
settings.get('featuredProjectEndpoint') || (await settings.get('featuredProjectEndpoint')) ||
'https://assets.balena.io/etcher-featured/index.html'; 'https://assets.balena.io/etcher-featured/index.html';
this.setState({ endpoint }); this.setState({ endpoint });
} catch (error) { } catch (error) {

View File

@ -22,29 +22,14 @@ import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state'; import * as selectionState from '../../models/selection-state';
import { store } from '../../models/store'; import { store } from '../../models/store';
import * as analytics from '../../modules/analytics'; import * as analytics from '../../modules/analytics';
import { updateLock } from '../../modules/update-lock';
import { open as openExternal } from '../../os/open-external/services/open-external'; import { open as openExternal } from '../../os/open-external/services/open-external';
import { FlashAnother } from '../flash-another/flash-another'; import { FlashAnother } from '../flash-another/flash-another';
import { FlashResults } from '../flash-results/flash-results'; import { FlashResults } from '../flash-results/flash-results';
import { SVGIcon } from '../svg-icon/svg-icon'; import { SVGIcon } from '../svg-icon/svg-icon';
const restart = (options: any, goToMain: () => void) => { function restart(goToMain: () => void) {
const {
applicationSessionUuid,
flashingWorkflowUuid,
} = store.getState().toJS();
if (!options.preserveImage) {
selectionState.deselectImage();
}
selectionState.deselectAllDrives(); selectionState.deselectAllDrives();
analytics.logEvent('Restart', { analytics.logEvent('Restart');
...options,
applicationSessionUuid,
flashingWorkflowUuid,
});
// Re-enable lock release on inactivity
updateLock.resume();
// Reset the flashing workflow uuid // Reset the flashing workflow uuid
store.dispatch({ store.dispatch({
@ -53,17 +38,17 @@ const restart = (options: any, goToMain: () => void) => {
}); });
goToMain(); goToMain();
}; }
const formattedErrors = () => { function formattedErrors() {
const errors = _.map( const errors = _.map(
_.get(flashState.getFlashResults(), ['results', 'errors']), _.get(flashState.getFlashResults(), ['results', 'errors']),
error => { (error) => {
return `${error.device}: ${error.message || error.code}`; return `${error.device}: ${error.message || error.code}`;
}, },
); );
return errors.join('\n'); return errors.join('\n');
}; }
function FinishPage({ goToMain }: { goToMain: () => void }) { function FinishPage({ goToMain }: { goToMain: () => void }) {
const results = flashState.getFlashResults().results || {}; const results = flashState.getFlashResults().results || {};
@ -74,8 +59,10 @@ function FinishPage({ goToMain }: { goToMain: () => void }) {
<FlashResults results={results} errors={formattedErrors()} /> <FlashResults results={results} errors={formattedErrors()} />
<FlashAnother <FlashAnother
onClick={(options: any) => restart(options, goToMain)} onClick={() => {
></FlashAnother> restart(goToMain);
}}
/>
</div> </div>
<div className="box center"> <div className="box center">

View File

@ -26,17 +26,14 @@ const Div = styled.div<any>`
`; `;
export interface FlashAnotherProps { export interface FlashAnotherProps {
onClick: (options: { preserveImage: boolean }) => void; onClick: () => void;
} }
export const FlashAnother = (props: FlashAnotherProps) => { export const FlashAnother = (props: FlashAnotherProps) => {
return ( return (
<ThemedProvider> <ThemedProvider>
<Div position="absolute" right="152px"> <Div position="absolute" right="152px">
<BaseButton <BaseButton primary onClick={props.onClick}>
primary
onClick={props.onClick.bind(null, { preserveImage: true })}
>
Flash Another Flash Another
</BaseButton> </BaseButton>
</Div> </Div>

View File

@ -14,39 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
import * as Color from 'color';
import * as React from 'react'; import * as React from 'react';
import { ProgressBar } from 'rendition'; import { ProgressBar } from 'rendition';
import { css, default as styled, keyframes } from 'styled-components'; import { default as styled } from 'styled-components';
import { StepButton, StepSelection } from '../../styled-components'; import { StepButton } from '../../styled-components';
import { colors } from '../../theme';
const darkenForegroundStripes = 0.18;
const desaturateForegroundStripes = 0.2;
const progressButtonStripesForegroundColor = Color(colors.primary.background)
.darken(darkenForegroundStripes)
.desaturate(desaturateForegroundStripes)
.string();
const desaturateBackgroundStripes = 0.05;
const progressButtonStripesBackgroundColor = Color(colors.primary.background)
.desaturate(desaturateBackgroundStripes)
.string();
const ProgressButtonStripes = keyframes`
0% {
background-position: 0 0;
}
100% {
background-position: 20px 20px;
}
`;
const ProgressButtonStripesRule = css`
${ProgressButtonStripes} 1s linear infinite;
`;
const FlashProgressBar = styled(ProgressBar)` const FlashProgressBar = styled(ProgressBar)`
> div { > div {
@ -54,6 +26,10 @@ const FlashProgressBar = styled(ProgressBar)`
height: 48px; height: 48px;
color: white !important; color: white !important;
text-shadow: none !important; text-shadow: none !important;
transition-duration: 0s;
> div {
transition-duration: 0s;
}
} }
width: 200px; width: 200px;
@ -61,78 +37,40 @@ const FlashProgressBar = styled(ProgressBar)`
font-size: 16px; font-size: 16px;
line-height: 48px; line-height: 48px;
background: ${Color(colors.warning.background) background: #2f3033;
.darken(darkenForegroundStripes)
.string()};
`;
const FlashProgressBarValidating = styled(FlashProgressBar)`
// Notice that we add 0.01 to certain gradient stop positions.
// That workarounds a Chrome rendering issue where diagonal
// lines look spiky.
// See https://github.com/balena-io/etcher/issues/472
background-image: -webkit-gradient(
linear,
0 0,
100% 100%,
color-stop(0.25, ${progressButtonStripesForegroundColor}),
color-stop(0.26, ${progressButtonStripesBackgroundColor}),
color-stop(0.5, ${progressButtonStripesBackgroundColor}),
color-stop(0.51, ${progressButtonStripesForegroundColor}),
color-stop(0.75, ${progressButtonStripesForegroundColor}),
color-stop(0.76, ${progressButtonStripesBackgroundColor}),
to(${progressButtonStripesBackgroundColor})
);
background-color: white;
animation: ${ProgressButtonStripesRule};
overflow: hidden;
background-size: 20px 20px;
`; `;
interface ProgressButtonProps { interface ProgressButtonProps {
striped: boolean; type: 'decompressing' | 'flashing' | 'verifying';
active: boolean; active: boolean;
percentage: number; percentage: number;
label: string; label: string;
disabled: boolean; disabled: boolean;
callback: () => any; callback: () => void;
} }
const colors = {
decompressing: '#00aeef',
flashing: '#da60ff',
verifying: '#1ac135',
} as const;
/** /**
* Progress Button component * Progress Button component
*/ */
export class ProgressButton extends React.Component<ProgressButtonProps> { export class ProgressButton extends React.Component<ProgressButtonProps> {
public render() { public render() {
if (this.props.active) { if (this.props.active) {
if (this.props.striped) {
return ( return (
<StepSelection> <FlashProgressBar
<FlashProgressBarValidating background={colors[this.props.type]}
primary
emphasized
value={this.props.percentage} value={this.props.percentage}
> >
{this.props.label} {this.props.label}
</FlashProgressBarValidating>
</StepSelection>
);
}
return (
<StepSelection>
<FlashProgressBar warning emphasized value={this.props.percentage}>
{this.props.label}
</FlashProgressBar> </FlashProgressBar>
</StepSelection>
); );
} }
return ( return (
<StepSelection>
<StepButton <StepButton
primary primary
onClick={this.props.callback} onClick={this.props.callback}
@ -140,7 +78,6 @@ export class ProgressButton extends React.Component<ProgressButtonProps> {
> >
{this.props.label} {this.props.label}
</StepButton> </StepButton>
</StepSelection>
); );
} }
} }

View File

@ -20,7 +20,6 @@ import * as React from 'react';
import * as packageJSON from '../../../../../package.json'; import * as packageJSON from '../../../../../package.json';
import * as settings from '../../models/settings'; import * as settings from '../../models/settings';
import { store } from '../../models/store';
import * as analytics from '../../modules/analytics'; import * as analytics from '../../modules/analytics';
/** /**
@ -92,7 +91,7 @@ export class SafeWebview extends React.PureComponent<
url.searchParams.set(API_VERSION_PARAM, API_VERSION); url.searchParams.set(API_VERSION_PARAM, API_VERSION);
url.searchParams.set( url.searchParams.set(
OPT_OUT_ANALYTICS_PARAM, OPT_OUT_ANALYTICS_PARAM,
(!settings.get('errorReporting')).toString(), (!settings.getSync('errorReporting')).toString(),
); );
this.entryHref = url.href; this.entryHref = url.href;
// Events steal 'this' // Events steal 'this'
@ -182,11 +181,7 @@ export class SafeWebview extends React.PureComponent<
// only care about this event if it's a request for the main frame // only care about this event if it's a request for the main frame
if (event.resourceType === 'mainFrame') { if (event.resourceType === 'mainFrame') {
const HTTP_OK = 200; const HTTP_OK = 200;
analytics.logEvent('SafeWebview loaded', { analytics.logEvent('SafeWebview loaded', { event });
event,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
this.setState({ this.setState({
shouldShow: event.statusCode === HTTP_OK, shouldShow: event.statusCode === HTTP_OK,
}); });
@ -197,15 +192,13 @@ export class SafeWebview extends React.PureComponent<
} }
// Open link in browser if it's opened as a 'foreground-tab' // 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); const url = new window.URL(event.url);
if ( if (
_.every([ (url.protocol === 'http:' || url.protocol === 'https:') &&
url.protocol === 'http:' || url.protocol === 'https:', event.disposition === 'foreground-tab' &&
event.disposition === 'foreground-tab',
// Don't open links if they're disabled by the env var // Don't open links if they're disabled by the env var
!settings.get('disableExternalLinks'), !(await settings.get('disableExternalLinks'))
])
) { ) {
electron.shell.openExternal(url.href); electron.shell.openExternal(url.href);
} }

View File

@ -20,15 +20,12 @@ import * as _ from 'lodash';
import * as os from 'os'; import * as os from 'os';
import * as React from 'react'; import * as React from 'react';
import { Badge, Checkbox, Modal } from 'rendition'; import { Badge, Checkbox, Modal } from 'rendition';
import styled from 'styled-components';
import { version } from '../../../../../package.json'; import { version } from '../../../../../package.json';
import * as settings from '../../models/settings'; import * as settings from '../../models/settings';
import { store } from '../../models/store';
import * as analytics from '../../modules/analytics'; import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external'; import { open as openExternal } from '../../os/open-external/services/open-external';
const { useState } = React;
const platform = os.platform(); const platform = os.platform();
interface WarningModalProps { interface WarningModalProps {
@ -64,11 +61,15 @@ const WarningModal = ({
interface Setting { interface Setting {
name: string; name: string;
label: string | JSX.Element; label: string | JSX.Element;
options?: any; options?: {
description: string;
confirmLabel: string;
};
hide?: boolean; hide?: boolean;
} }
const settingsList: Setting[] = [ async function getSettingsList(): Promise<Setting[]> {
return [
{ {
name: 'errorReporting', name: 'errorReporting',
label: 'Anonymously report errors and usage statistics to balena.io', label: 'Anonymously report errors and usage statistics to balena.io',
@ -87,10 +88,6 @@ const settingsList: Setting[] = [
name: 'validateWriteOnSuccess', name: 'validateWriteOnSuccess',
label: 'Validate write on success', label: 'Validate write on success',
}, },
{
name: 'trim',
label: 'Trim ext{2,3,4} partitions before writing (raw images only)',
},
{ {
name: 'updatesEnabled', name: 'updatesEnabled',
label: 'Auto-updates enabled', label: 'Auto-updates enabled',
@ -110,52 +107,72 @@ const settingsList: Setting[] = [
You will be able to overwrite your system drives if you're not careful.`, You will be able to overwrite your system drives if you're not careful.`,
confirmLabel: 'Enable unsafe mode', confirmLabel: 'Enable unsafe mode',
}, },
hide: settings.get('disableUnsafeMode'), hide: await settings.get('disableUnsafeMode'),
}, },
]; ];
}
interface Warning {
setting: string;
settingValue: boolean;
description: string;
confirmLabel: string;
}
interface SettingsModalProps { interface SettingsModalProps {
toggleModal: (value: boolean) => void; toggleModal: (value: boolean) => void;
} }
export const SettingsModal: any = styled( export function SettingsModal({ toggleModal }: SettingsModalProps) {
({ toggleModal }: SettingsModalProps) => { const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]);
const [currentSettings, setCurrentSettings]: [ React.useEffect(() => {
_.Dictionary<any>, (async () => {
React.Dispatch<React.SetStateAction<_.Dictionary<any>>>, if (settingsList.length === 0) {
] = useState(settings.getAll()); setCurrentSettingsList(await getSettingsList());
const [warning, setWarning]: [ }
any, })();
React.Dispatch<React.SetStateAction<any>>, });
] = useState({}); const [currentSettings, setCurrentSettings] = React.useState<
_.Dictionary<boolean>
>({});
React.useEffect(() => {
(async () => {
if (_.isEmpty(currentSettings)) {
setCurrentSettings(await settings.getAll());
}
})();
});
const [warning, setWarning] = React.useState<Warning | undefined>(undefined);
const toggleSetting = async (setting: string, options?: any) => { const toggleSetting = async (
setting: string,
options?: Setting['options'],
) => {
const value = currentSettings[setting]; const value = currentSettings[setting];
const dangerous = !_.isUndefined(options); const dangerous = options !== undefined;
analytics.logEvent('Toggle setting', { analytics.logEvent('Toggle setting', {
setting, setting,
value, value,
dangerous, dangerous,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
}); });
if (value || !dangerous) { if (value || options === undefined) {
await settings.set(setting, !value); await settings.set(setting, !value);
setCurrentSettings({ setCurrentSettings({
...currentSettings, ...currentSettings,
[setting]: !value, [setting]: !value,
}); });
setWarning({}); setWarning(undefined);
return; return;
} } else {
// Show warning since it's a dangerous setting // Show warning since it's a dangerous setting
setWarning({ setWarning({
setting, setting,
settingValue: value, settingValue: value,
...options, ...options,
}); });
}
}; };
return ( return (
@ -195,28 +212,23 @@ export const SettingsModal: any = styled(
</div> </div>
</div> </div>
{_.isEmpty(warning) ? null : ( {warning === undefined ? null : (
<WarningModal <WarningModal
message={warning.description} message={warning.description}
confirmLabel={warning.confirmLabel} confirmLabel={warning.confirmLabel}
done={() => { done={async () => {
settings.set(warning.setting, !warning.settingValue); await settings.set(warning.setting, !warning.settingValue);
setCurrentSettings({ setCurrentSettings({
...currentSettings, ...currentSettings,
[warning.setting]: true, [warning.setting]: true,
}); });
setWarning({}); setWarning(undefined);
}} }}
cancel={() => { cancel={() => {
setWarning({}); setWarning(undefined);
}} }}
/> />
)} )}
</Modal> </Modal>
); );
},
)`
> div:nth-child(3) {
justify-content: center;
} }
`;

View File

@ -29,7 +29,7 @@ import * as messages from '../../../../shared/messages';
import * as supportedFormats from '../../../../shared/supported-formats'; import * as supportedFormats from '../../../../shared/supported-formats';
import * as shared from '../../../../shared/units'; import * as shared from '../../../../shared/units';
import * as selectionState from '../../models/selection-state'; import * as selectionState from '../../models/selection-state';
import { observe, store } from '../../models/store'; import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics'; import * as analytics from '../../modules/analytics';
import * as exceptionReporter from '../../modules/exception-reporter'; import * as exceptionReporter from '../../modules/exception-reporter';
import * as osDialog from '../../os/dialog'; import * as osDialog from '../../os/dialog';
@ -148,7 +148,7 @@ const URLSelector = ({ done }: { done: (imageURL: string) => void }) => {
Recent Recent
<Card <Card
style={{ padding: '10px 15px' }} style={{ padding: '10px 15px' }}
rows={_.map(recentImages, recent => ( rows={_.map(recentImages, (recent) => (
<Txt <Txt
key={recent} key={recent}
onClick={() => { onClick={() => {
@ -254,8 +254,6 @@ export class SourceSelector extends React.Component<
private reselectImage() { private reselectImage() {
analytics.logEvent('Reselect image', { analytics.logEvent('Reselect image', {
previousImage: selectionState.getImage(), previousImage: selectionState.getImage(),
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
}); });
selectionState.deselectImage(); selectionState.deselectImage();
@ -275,17 +273,7 @@ export class SourceSelector extends React.Component<
}); });
osDialog.showError(invalidImageError); osDialog.showError(invalidImageError);
analytics.logEvent( analytics.logEvent('Invalid image', image);
'Invalid image',
_.merge(
{
applicationSessionUuid: store.getState().toJS()
.applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
},
image,
),
);
return; return;
} }
@ -294,21 +282,11 @@ export class SourceSelector extends React.Component<
let title = null; let title = null;
if (supportedFormats.looksLikeWindowsImage(image.path)) { if (supportedFormats.looksLikeWindowsImage(image.path)) {
analytics.logEvent('Possibly Windows image', { analytics.logEvent('Possibly Windows image', { image });
image,
applicationSessionUuid: store.getState().toJS()
.applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
message = messages.warning.looksLikeWindowsImage(); message = messages.warning.looksLikeWindowsImage();
title = 'Possible Windows image detected'; title = 'Possible Windows image detected';
} else if (!image.hasMBR) { } else if (!image.hasMBR) {
analytics.logEvent('Missing partition table', { analytics.logEvent('Missing partition table', { image });
image,
applicationSessionUuid: store.getState().toJS()
.applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
title = 'Missing partition table'; title = 'Missing partition table';
message = messages.warning.missingPartitionTable(); message = messages.warning.missingPartitionTable();
} }
@ -331,8 +309,6 @@ export class SourceSelector extends React.Component<
logo: Boolean(image.logo), logo: Boolean(image.logo),
blockMap: Boolean(image.blockMap), blockMap: Boolean(image.blockMap),
}, },
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
}); });
} catch (error) { } catch (error) {
exceptionReporter.report(error); exceptionReporter.report(error);
@ -375,7 +351,7 @@ export class SourceSelector extends React.Component<
analytics.logEvent('Unsupported protocol', { path: imagePath }); analytics.logEvent('Unsupported protocol', { path: imagePath });
return; return;
} }
source = new sourceDestination.Http(imagePath); source = new sourceDestination.Http({ url: imagePath });
} }
try { try {
@ -420,21 +396,14 @@ export class SourceSelector extends React.Component<
} }
private async openImageSelector() { private async openImageSelector() {
analytics.logEvent('Open image selector', { analytics.logEvent('Open image selector');
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
try { try {
const imagePath = await osDialog.selectImage(); const imagePath = await osDialog.selectImage();
// Avoid analytics and selection state changes // Avoid analytics and selection state changes
// if no file was resolved from the dialog. // if no file was resolved from the dialog.
if (!imagePath) { if (!imagePath) {
analytics.logEvent('Image selector closed', { analytics.logEvent('Image selector closed');
applicationSessionUuid: store.getState().toJS()
.applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
return; return;
} }
this.selectImageByPath({ this.selectImageByPath({
@ -457,11 +426,7 @@ export class SourceSelector extends React.Component<
} }
private openURLSelector() { private openURLSelector() {
analytics.logEvent('Open image URL selector', { analytics.logEvent('Open image URL selector');
applicationSessionUuid:
store.getState().toJS().applicationSessionUuid || '',
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
this.setState({ this.setState({
showURLSelector: true, showURLSelector: true,
@ -481,8 +446,6 @@ export class SourceSelector extends React.Component<
private showSelectedImageDetails() { private showSelectedImageDetails() {
analytics.logEvent('Show selected image tooltip', { analytics.logEvent('Show selected image tooltip', {
imagePath: selectionState.getImagePath(), imagePath: selectionState.getImagePath(),
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
}); });
this.setState({ this.setState({
@ -606,12 +569,7 @@ export class SourceSelector extends React.Component<
// Avoid analytics and selection state changes // Avoid analytics and selection state changes
// if no file was resolved from the dialog. // if no file was resolved from the dialog.
if (!imagePath) { if (!imagePath) {
analytics.logEvent('URL selector closed', { analytics.logEvent('URL selector closed');
applicationSessionUuid: store.getState().toJS()
.applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS()
.flashingWorkflowUuid,
});
this.setState({ this.setState({
showURLSelector: false, showURLSelector: false,
}); });

View File

@ -82,7 +82,7 @@ export class SVGIcon extends React.Component<SVGIconProps> {
let svgData = ''; let svgData = '';
_.find(this.props.contents, content => { _.find(this.props.contents, (content) => {
const attempt = tryParseSVGContents(content); const attempt = tryParseSVGContents(content);
if (attempt) { if (attempt) {
@ -94,7 +94,7 @@ export class SVGIcon extends React.Component<SVGIconProps> {
}); });
if (!svgData) { if (!svgData) {
_.find(this.props.paths, relativePath => { _.find(this.props.paths, (relativePath) => {
// This means the path to the icon should be // This means the path to the icon should be
// relative to *this directory*. // relative to *this directory*.
// TODO: There might be a way to compute the path // TODO: There might be a way to compute the path

View File

@ -85,13 +85,6 @@ export function setProgressState(
return _.round(bytesToMegabytes(state.speed), PRECISION); return _.round(bytesToMegabytes(state.speed), PRECISION);
} }
return null;
}),
totalSpeed: _.attempt(() => {
if (_.isFinite(state.totalSpeed)) {
return _.round(bytesToMegabytes(state.totalSpeed), PRECISION);
}
return null; return null;
}), }),
}); });
@ -107,10 +100,7 @@ export function getFlashResults() {
} }
export function getFlashState() { export function getFlashState() {
return store return store.getState().get('flashState').toJS();
.getState()
.get('flashState')
.toJS();
} }
export function wasLastFlashCancelled() { export function wasLastFlashCancelled() {

View File

@ -66,7 +66,7 @@ interface DeviceFromState {
device: string; device: string;
} }
export function init() { export async function init(): Promise<void> {
// ledsMapping is something like: // ledsMapping is something like:
// { // {
// 'platform-xhci-hcd.0.auto-usb-0:1.1.1:1.0-scsi-0:0:0:0': [ // 'platform-xhci-hcd.0.auto-usb-0:1.1.1:1.0-scsi-0:0:0:0': [
@ -77,11 +77,11 @@ export function init() {
// ... // ...
// } // }
const ledsMapping: _.Dictionary<[string, string, string]> = const ledsMapping: _.Dictionary<[string, string, string]> =
settings.get('ledsMapping') || {}; (await settings.get('ledsMapping')) || {};
for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) { for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) {
leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames)); leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames));
} }
observe(state => { observe((state) => {
const availableDrives = state const availableDrives = state
.get('availableDrives') .get('availableDrives')
.toJS() .toJS()

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

@ -51,10 +51,7 @@ export function selectImage(image: any) {
* @summary Get all selected drives' devices * @summary Get all selected drives' devices
*/ */
export function getSelectedDevices(): string[] { export function getSelectedDevices(): string[] {
return store return store.getState().getIn(['selection', 'devices']).toJS();
.getState()
.getIn(['selection', 'devices'])
.toJS();
} }
/** /**
@ -62,7 +59,7 @@ export function getSelectedDevices(): string[] {
*/ */
export function getSelectedDrives(): any[] { export function getSelectedDrives(): any[] {
const drives = availableDrives.getDrives(); const drives = availableDrives.getDrives();
return _.map(getSelectedDevices(), device => { return _.map(getSelectedDevices(), (device) => {
return _.find(drives, { device }); return _.find(drives, { device });
}); });
} }

View File

@ -15,71 +15,93 @@
*/ */
import * as _debug from 'debug'; import * as _debug from 'debug';
import * as electron from 'electron';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { promises as fs } from 'fs';
import { join } from 'path';
import * as packageJSON from '../../../../package.json'; import * as packageJSON from '../../../../package.json';
import * as errors from '../../../shared/errors';
import * as localSettings from './local-settings';
const debug = _debug('etcher:models: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 // 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, unsafeMode: false,
errorReporting: true, errorReporting: true,
unmountOnSuccess: true, unmountOnSuccess: true,
validateWriteOnSuccess: true, validateWriteOnSuccess: true,
trim: false, updatesEnabled: !_.includes(['rpm', 'deb'], packageJSON.packageType),
updatesEnabled:
packageJSON.updates.enabled &&
!_.includes(['rpm', 'deb'], packageJSON.packageType),
lastSleptUpdateNotifier: null,
lastSleptUpdateNotifierVersion: null,
desktopNotifications: true, desktopNotifications: true,
autoBlockmapping: true,
decompressFirst: true,
}; };
let settings = _.cloneDeep(DEFAULT_SETTINGS); const settings = _.cloneDeep(DEFAULT_SETTINGS);
/** async function load(): Promise<void> {
* @summary Reset settings to their default values
*/
export async function reset(): Promise<void> {
debug('reset');
// TODO: Remove default settings from config file (?)
settings = _.cloneDeep(DEFAULT_SETTINGS);
return await localSettings.writeAll(settings);
}
/**
* @summary Extend the application state with the local settings
*/
export async function load(): Promise<void> {
debug('load'); 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); _.assign(settings, loadedSettings);
} }
/** const loaded = load();
* @summary Set a setting value
*/
export async function set(key: string, value: any): Promise<void> { export async function set(key: string, value: any): Promise<void> {
debug('set', key, value); debug('set', key, value);
if (_.isNil(key)) { await loaded;
throw errors.createError({
title: 'Missing setting key',
});
}
if (!_.isString(key)) {
throw errors.createError({
title: `Invalid setting key: ${key}`,
});
}
const previousValue = settings[key]; const previousValue = settings[key];
settings[key] = value; settings[key] = value;
try { try {
await localSettings.writeAll(settings); // Use exports.writeConfigFile() so it can be mocked in tests
await exports.writeConfigFile(CONFIG_PATH, settings);
} catch (error) { } catch (error) {
// Revert to previous value if persisting settings failed // Revert to previous value if persisting settings failed
settings[key] = previousValue; settings[key] = previousValue;
@ -87,24 +109,17 @@ export async function set(key: string, value: any): Promise<void> {
} }
} }
/** export async function get(key: string): Promise<any> {
* @summary Get a setting value await loaded;
*/ return getSync(key);
export function get(key: string): any {
return _.cloneDeep(_.get(settings, [key]));
} }
/** export function getSync(key: string): any {
* @summary Check if setting value exists return _.cloneDeep(settings[key]);
*/
export function has(key: string): boolean {
return settings[key] != null;
} }
/** export async function getAll() {
* @summary Get all setting values
*/
export function getAll() {
debug('getAll'); debug('getAll');
await loaded;
return _.cloneDeep(settings); return _.cloneDeep(settings);
} }

View File

@ -34,7 +34,7 @@ function verifyNoNilFields(
fields: string[], fields: string[],
name: string, name: string,
) { ) {
const nilFields = _.filter(fields, field => { const nilFields = _.filter(fields, (field) => {
return _.isNil(_.get(object, field)); return _.isNil(_.get(object, field));
}); });
if (nilFields.length) { if (nilFields.length) {
@ -45,7 +45,7 @@ function verifyNoNilFields(
/** /**
* @summary FLASH_STATE fields that can't be nil * @summary FLASH_STATE fields that can't be nil
*/ */
const flashStateNoNilFields = ['speed', 'totalSpeed']; const flashStateNoNilFields = ['speed'];
/** /**
* @summary SELECT_IMAGE fields that can't be nil * @summary SELECT_IMAGE fields that can't be nil
@ -65,14 +65,11 @@ const DEFAULT_STATE = Immutable.fromJS({
isFlashing: false, isFlashing: false,
flashResults: {}, flashResults: {},
flashState: { flashState: {
flashing: 0, active: 0,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
percentage: 0, percentage: 0,
speed: null, speed: null,
averageSpeed: null, averageSpeed: null,
totalSpeed: null,
}, },
lastAverageFlashingSpeed: null, lastAverageFlashingSpeed: null,
}); });
@ -136,9 +133,9 @@ function storeReducer(
drives = _.sortBy(drives, [ drives = _.sortBy(drives, [
// Devices with no devicePath first (usbboot) // Devices with no devicePath first (usbboot)
d => !!d.devicePath, (d) => !!d.devicePath,
// Then sort by devicePath (only available on Linux with udev) or device // Then sort by devicePath (only available on Linux with udev) or device
d => d.devicePath || d.device, (d) => d.devicePath || d.device,
]); ]);
const newState = state.set('availableDrives', Immutable.fromJS(drives)); const newState = state.set('availableDrives', Immutable.fromJS(drives));
@ -168,7 +165,7 @@ function storeReducer(
); );
const shouldAutoselectAll = Boolean( const shouldAutoselectAll = Boolean(
settings.get('disableExplicitDriveSelection'), settings.getSync('disableExplicitDriveSelection'),
); );
const AUTOSELECT_DRIVE_COUNT = 1; const AUTOSELECT_DRIVE_COUNT = 1;
const nonStaleSelectedDevices = nonStaleNewState const nonStaleSelectedDevices = nonStaleNewState
@ -234,17 +231,7 @@ function storeReducer(
verifyNoNilFields(action.data, flashStateNoNilFields, 'flash'); verifyNoNilFields(action.data, flashStateNoNilFields, 'flash');
if ( if (!_.every(_.pick(action.data, ['active', 'failed']), _.isFinite)) {
!_.every(
_.pick(action.data, [
'flashing',
'verifying',
'successful',
'failed',
]),
_.isFinite,
)
) {
throw errors.createError({ throw errors.createError({
title: 'State quantity field(s) not finite number', title: 'State quantity field(s) not finite number',
}); });
@ -266,7 +253,7 @@ function storeReducer(
} }
let ret = state.set('flashState', Immutable.fromJS(action.data)); let ret = state.set('flashState', Immutable.fromJS(action.data));
if (action.data.flashing) { if (action.data.type === 'flashing') {
ret = ret.set('lastAverageFlashingSpeed', action.data.averageSpeed); ret = ret.set('lastAverageFlashingSpeed', action.data.averageSpeed);
} }
return ret; return ret;

View File

@ -20,34 +20,31 @@ import * as resinCorvus from 'resin-corvus/browser';
import * as packageJSON from '../../../../package.json'; import * as packageJSON from '../../../../package.json';
import { getConfig, hasProps } from '../../../shared/utils'; import { getConfig, hasProps } from '../../../shared/utils';
import * as settings from '../models/settings'; 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 DEFAULT_PROBABILITY = 0.1;
const services = { 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, sentry: sentryToken,
mixpanel: mixpanelToken, mixpanel: mixpanelToken,
}; },
resinCorvus.install({
services,
options: { options: {
release: packageJSON.version, release: packageJSON.version,
shouldReport: () => { shouldReport: () => {
return settings.get('errorReporting'); return settings.getSync('errorReporting');
}, },
mixpanelDeferred: true, mixpanelDeferred: true,
}, },
}); });
}
let mixpanelSample = DEFAULT_PROBABILITY; let mixpanelSample = DEFAULT_PROBABILITY;
@ -55,9 +52,10 @@ let mixpanelSample = DEFAULT_PROBABILITY;
* @summary Init analytics configurations * @summary Init analytics configurations
*/ */
async function initConfig() { async function initConfig() {
await installCorvus();
let validatedConfig = null; let validatedConfig = null;
try { try {
const config = await getConfig(configUrl); const config = await getConfig();
const mixpanel = _.get(config, ['analytics', 'mixpanel'], {}); const mixpanel = _.get(config, ['analytics', 'mixpanel'], {});
mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY; mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY;
if (isClientEligible(mixpanelSample)) { if (isClientEligible(mixpanelSample)) {
@ -96,22 +94,23 @@ function validateMixpanelConfig(config: {
return mixpanelConfig; return mixpanelConfig;
} }
/**
* @summary Log a debug message
*
* @description
* This function sends the debug message to error reporting services.
*/
export const logDebug = resinCorvus.logDebug;
/** /**
* @summary Log an event * @summary Log an event
* *
* @description * @description
* This function sends the debug message to product analytics services. * This function sends the debug message to product analytics services.
*/ */
export function logEvent(message: string, data: any) { export function logEvent(message: string, data: _.Dictionary<any> = {}) {
resinCorvus.logEvent(message, { ...data, sample: mixpanelSample }); const {
applicationSessionUuid,
flashingWorkflowUuid,
} = store.getState().toJS();
resinCorvus.logEvent(message, {
...data,
sample: mixpanelSample,
applicationSessionUuid,
flashingWorkflowUuid,
});
} }
/** /**

View File

@ -23,7 +23,9 @@ import * as settings from '../models/settings';
* @summary returns true if system drives should be shown * @summary returns true if system drives should be shown
*/ */
function includeSystemDrives() { function includeSystemDrives() {
return settings.get('unsafeMode') && !settings.get('disableUnsafeMode'); return (
settings.getSync('unsafeMode') && !settings.getSync('disableUnsafeMode')
);
} }
const adapters: sdk.scanner.adapters.Adapter[] = [ const adapters: sdk.scanner.adapters.Adapter[] = [

View File

@ -29,10 +29,8 @@ import { SourceOptions } from '../components/source-selector/source-selector';
import * as flashState from '../models/flash-state'; import * as flashState from '../models/flash-state';
import * as selectionState from '../models/selection-state'; import * as selectionState from '../models/selection-state';
import * as settings from '../models/settings'; import * as settings from '../models/settings';
import { store } from '../models/store';
import * as analytics from '../modules/analytics'; import * as analytics from '../modules/analytics';
import * as windowProgress from '../os/window-progress'; import * as windowProgress from '../os/window-progress';
import { updateLock } from './update-lock';
const THREADS_PER_CPU = 16; const THREADS_PER_CPU = 16;
@ -61,8 +59,6 @@ function handleErrorLogging(
) { ) {
const eventData = { const eventData = {
...analyticsData, ...analyticsData,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
flashInstanceUuid: flashState.getFlashUuid(), flashInstanceUuid: flashState.getFlashUuid(),
}; };
@ -140,7 +136,7 @@ interface FlashResults {
* @description * @description
* This function is extracted for testing purposes. * This function is extracted for testing purposes.
*/ */
export function performWrite( export async function performWrite(
image: string, image: string,
drives: DrivelistDrive[], drives: DrivelistDrive[],
onProgress: sdk.multiWrite.OnProgressFunction, onProgress: sdk.multiWrite.OnProgressFunction,
@ -148,14 +144,20 @@ export function performWrite(
): Promise<{ cancelled?: boolean }> { ): Promise<{ cancelled?: boolean }> {
let cancelled = false; let cancelled = false;
ipc.serve(); ipc.serve();
return new Promise((resolve, reject) => { const {
ipc.server.on('error', error => { unmountOnSuccess,
validateWriteOnSuccess,
autoBlockmapping,
decompressFirst,
} = await settings.getAll();
return await new Promise((resolve, reject) => {
ipc.server.on('error', (error) => {
terminateServer(); terminateServer();
const errorObject = errors.fromJSON(error); const errorObject = errors.fromJSON(error);
reject(errorObject); reject(errorObject);
}); });
ipc.server.on('log', message => { ipc.server.on('log', (message) => {
console.log(message); console.log(message);
}); });
@ -166,17 +168,16 @@ export function performWrite(
driveCount: drives.length, driveCount: drives.length,
uuid: flashState.getFlashUuid(), uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(), flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: settings.get('unmountOnSuccess'), unmountOnSuccess,
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), validateWriteOnSuccess,
trim: settings.get('trim'),
}; };
ipc.server.on('fail', ({ error }: { error: Error & { code: string } }) => { ipc.server.on('fail', ({ error }: { error: Error & { code: string } }) => {
handleErrorLogging(error, analyticsData); handleErrorLogging(error, analyticsData);
}); });
ipc.server.on('done', event => { ipc.server.on('done', (event) => {
event.results.errors = _.map(event.results.errors, data => { event.results.errors = _.map(event.results.errors, (data) => {
return errors.fromJSON(data); return errors.fromJSON(data);
}); });
_.merge(flashResults, event); _.merge(flashResults, event);
@ -195,9 +196,10 @@ export function performWrite(
destinations: drives, destinations: drives,
source, source,
SourceType: source.SourceType.name, SourceType: source.SourceType.name,
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), validateWriteOnSuccess,
trim: settings.get('trim'), autoBlockmapping,
unmountOnSuccess: settings.get('unmountOnSuccess'), unmountOnSuccess,
decompressFirst,
}); });
}); });
@ -245,7 +247,6 @@ export function performWrite(
// Clear the update lock timer to prevent longer // Clear the update lock timer to prevent longer
// flashing timing it out, and releasing the lock // flashing timing it out, and releasing the lock
updateLock.pause();
ipc.server.start(); ipc.server.start();
}); });
} }
@ -271,11 +272,8 @@ export async function flash(
uuid: flashState.getFlashUuid(), uuid: flashState.getFlashUuid(),
status: 'started', status: 'started',
flashInstanceUuid: flashState.getFlashUuid(), flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: settings.get('unmountOnSuccess'), unmountOnSuccess: await settings.get('unmountOnSuccess'),
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
trim: settings.get('trim'),
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
}; };
analytics.logEvent('Flash', analyticsData); analytics.logEvent('Flash', analyticsData);
@ -318,6 +316,8 @@ export async function flash(
errors: results.errors, errors: results.errors,
devices: results.devices, devices: results.devices,
status: 'finished', status: 'finished',
bytesWritten: results.bytesWritten,
sourceMetadata: results.sourceMetadata,
}; };
analytics.logEvent('Done', eventData); analytics.logEvent('Done', eventData);
} }
@ -326,7 +326,7 @@ export async function flash(
/** /**
* @summary Cancel write operation * @summary Cancel write operation
*/ */
export function cancel() { export async function cancel() {
const drives = selectionState.getSelectedDevices(); const drives = selectionState.getSelectedDevices();
const analyticsData = { const analyticsData = {
image: selectionState.getImagePath(), image: selectionState.getImagePath(),
@ -334,17 +334,13 @@ export function cancel() {
driveCount: drives.length, driveCount: drives.length,
uuid: flashState.getFlashUuid(), uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(), flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: settings.get('unmountOnSuccess'), unmountOnSuccess: await settings.get('unmountOnSuccess'),
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
trim: settings.get('trim'),
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
status: 'cancel', status: 'cancel',
}; };
analytics.logEvent('Cancel', analyticsData); analytics.logEvent('Cancel', analyticsData);
// Re-enable lock release on inactivity // Re-enable lock release on inactivity
updateLock.resume();
try { try {
// @ts-ignore (no Server.sockets in @types/node-ipc) // @ts-ignore (no Server.sockets in @types/node-ipc)

View File

@ -15,16 +15,15 @@
*/ */
import { bytesToClosestUnit } from '../../../shared/units'; import { bytesToClosestUnit } from '../../../shared/units';
import * as settings from '../models/settings'; // import * as settings from '../models/settings';
export interface FlashState { export interface FlashState {
flashing: number; active: number;
verifying: number;
successful: number;
failed: number; failed: number;
percentage?: number; percentage?: number;
speed: number; speed: number;
position: number; position: number;
type?: 'decompressing' | 'flashing' | 'verifying';
} }
/** /**
@ -36,45 +35,47 @@ export interface FlashState {
* *
* @example * @example
* const status = progressStatus.fromFlashState({ * const status = progressStatus.fromFlashState({
* flashing: 1, * type: 'flashing'
* verifying: 0, * active: 1,
* successful: 0,
* failed: 0, * failed: 0,
* percentage: 55, * percentage: 55,
* speed: 2049 * speed: 2049,
* }) * })
* *
* console.log(status) * console.log(status)
* // '55% Flashing' * // '55% Flashing'
*/ */
export function fromFlashState(state: FlashState): string { export function fromFlashState({
const isFlashing = Boolean(state.flashing); type,
const isValidating = !isFlashing && Boolean(state.verifying); percentage,
const shouldValidate = settings.get('validateWriteOnSuccess'); position,
const shouldUnmount = settings.get('unmountOnSuccess'); }: FlashState): string {
if (type === undefined) {
if (state.percentage === 0 && !state.speed) {
if (isValidating) {
return 'Validating...';
}
return 'Starting...'; return 'Starting...';
} else if (state.percentage === 100) { } else if (type === 'decompressing') {
if ((isValidating || !shouldValidate) && shouldUnmount) { if (percentage == null) {
return 'Unmounting...'; return 'Decompressing...';
} else {
return `${percentage}% Decompressing`;
} }
} else if (type === 'flashing') {
if (percentage != null) {
if (percentage < 100) {
return `${percentage}% Flashing`;
} else {
return 'Finishing...'; return 'Finishing...';
} else if (isFlashing) {
if (state.percentage != null) {
return `${state.percentage}% Flashing`;
} }
return `${bytesToClosestUnit(state.position)} flashed`; } else {
} else if (isValidating) { return `${bytesToClosestUnit(position)} flashed`;
return `${state.percentage}% Validating`; }
} else if (!isFlashing && !isValidating) { } else if (type === 'verifying') {
if (percentage == null) {
return 'Validating...';
} else if (percentage < 100) {
return `${percentage}% Validating`;
} else {
return 'Finishing...';
}
}
return 'Failed'; return 'Failed';
} }
throw new Error(`Invalid state: ${JSON.stringify(state)}`);
}

View File

@ -1,188 +0,0 @@
/*
* Copyright 2018 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 _debug from 'debug';
import * as electron from 'electron';
import { EventEmitter } from 'events';
import * as createInactivityTimer from 'inactivity-timer';
import * as settings from '../models/settings';
import { logException } from './analytics';
const debug = _debug('etcher:update-lock');
/**
* Interaction timeout in milliseconds (defaults to 5 minutes)
* @type {Number}
* @constant
*/
const INTERACTION_TIMEOUT_MS = settings.has('interactionTimeout')
? parseInt(settings.get('interactionTimeout'), 10)
: 5 * 60 * 1000;
class UpdateLock extends EventEmitter {
private paused: boolean;
private lockTimer: any;
constructor() {
super();
this.paused = false;
this.on('inactive', UpdateLock.onInactive);
this.lockTimer = createInactivityTimer(INTERACTION_TIMEOUT_MS, () => {
debug('inactive');
this.emit('inactive');
});
}
/**
* @summary Inactivity event handler, releases the balena update lock on inactivity
*/
private static onInactive() {
if (settings.get('resinUpdateLock')) {
UpdateLock.check((checkError: Error, isLocked: boolean) => {
debug('inactive-check', Boolean(checkError));
if (checkError) {
logException(checkError);
}
if (isLocked) {
UpdateLock.release((error?: Error) => {
debug('inactive-release', Boolean(error));
if (error) {
logException(error);
}
});
}
});
}
}
/**
* @summary Acquire the update lock
*/
private static acquire(callback: (error?: Error) => void) {
debug('lock');
if (settings.get('resinUpdateLock')) {
electron.ipcRenderer.once('resin-update-lock', (_event, error) => {
callback(error);
});
electron.ipcRenderer.send('resin-update-lock', 'lock');
} else {
callback(new Error('Update lock disabled'));
}
}
/**
* @summary Release the update lock
*/
public static release(callback: (error?: Error) => void) {
debug('unlock');
if (settings.get('resinUpdateLock')) {
electron.ipcRenderer.once('resin-update-lock', (_event, error) => {
callback(error);
});
electron.ipcRenderer.send('resin-update-lock', 'unlock');
} else {
callback(new Error('Update lock disabled'));
}
}
/**
* @summary Check the state of the update lock
* @param {Function} callback - callback(error, isLocked)
* @example
* UpdateLock.check((error, isLocked) => {
* if (isLocked) {
* // ...
* }
* })
*/
private static check(
callback: (error: Error | null, isLocked?: boolean) => void,
) {
debug('check');
if (settings.get('resinUpdateLock')) {
electron.ipcRenderer.once(
'resin-update-lock',
(_event, error, isLocked) => {
callback(error, isLocked);
},
);
electron.ipcRenderer.send('resin-update-lock', 'check');
} else {
callback(new Error('Update lock disabled'));
}
}
/**
* @summary Extend the lock timer
*/
public extend() {
debug('extend');
if (this.paused) {
debug('extend:paused');
return;
}
this.lockTimer.signal();
// When extending, check that we have the lock,
// and acquire it, if not
if (settings.get('resinUpdateLock')) {
UpdateLock.check((checkError, isLocked) => {
if (checkError) {
logException(checkError);
}
if (!isLocked) {
UpdateLock.acquire(error => {
if (error) {
logException(error);
}
debug('extend-acquire', Boolean(error));
});
}
});
}
}
/**
* @summary Clear the lock timer
*/
private clearTimer() {
debug('clear');
this.lockTimer.clear();
}
/**
* @summary Clear the lock timer, and pause extension, avoiding triggering until resume()d
*/
public pause() {
debug('pause');
this.paused = true;
this.clearTimer();
}
/**
* @summary Un-pause lock extension, and restart the timer
*/
public resume() {
debug('resume');
this.paused = false;
this.extend();
}
}
export const updateLock = new UpdateLock();

View File

@ -21,9 +21,9 @@ import * as settings from '../models/settings';
/** /**
* @summary Send a notification * @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 // Bail out if desktop notifications are disabled
if (!settings.get('desktopNotifications')) { if (!(await settings.get('desktopNotifications'))) {
return; return;
} }

View File

@ -16,22 +16,18 @@
import * as electron from 'electron'; import * as electron from 'electron';
import * as settings from '../../../models/settings'; import * as settings from '../../../models/settings';
import { store } from '../../../models/store';
import { logEvent } from '../../../modules/analytics'; import { logEvent } from '../../../modules/analytics';
/** /**
* @summary Open an external resource * @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 // Don't open links if they're disabled by the env var
if (settings.get('disableExternalLinks')) { if (await settings.get('disableExternalLinks')) {
return; return;
} }
logEvent('Open external link', { logEvent('Open external link', { url });
url,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
});
if (url) { if (url) {
electron.shell.openExternal(url); electron.shell.openExternal(url);

View File

@ -88,7 +88,7 @@ async function getWindowsNetworkDrives(): Promise<Map<string, string>> {
trim(str.slice(colonPosition + 1)), trim(str.slice(colonPosition + 1)),
]; ];
}) })
.filter(couple => couple[1].length > 0) .filter((couple) => couple[1].length > 0)
.value(); .value();
return new Map(couples); return new Map(couples);
} }

View File

@ -22,7 +22,7 @@ import { TargetSelector } from '../../components/drive-selector/target-selector'
import { SVGIcon } from '../../components/svg-icon/svg-icon'; import { SVGIcon } from '../../components/svg-icon/svg-icon';
import { getImage, getSelectedDrives } from '../../models/selection-state'; import { getImage, getSelectedDrives } from '../../models/selection-state';
import * as settings from '../../models/settings'; import * as settings from '../../models/settings';
import { observe, store } from '../../models/store'; import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics'; import * as analytics from '../../modules/analytics';
const StepBorder = styled.div<{ const StepBorder = styled.div<{
@ -31,7 +31,7 @@ const StepBorder = styled.div<{
right?: boolean; right?: boolean;
}>` }>`
height: 2px; height: 2px;
background-color: ${props => background-color: ${(props) =>
props.disabled props.disabled
? props.theme.customColors.dark.disabled.foreground ? props.theme.customColors.dark.disabled.foreground
: props.theme.customColors.dark.foreground}; : props.theme.customColors.dark.foreground};
@ -39,8 +39,8 @@ const StepBorder = styled.div<{
width: 124px; width: 124px;
top: 19px; top: 19px;
left: ${props => (props.left ? '-67px' : undefined)}; left: ${(props) => (props.left ? '-67px' : undefined)};
right: ${props => (props.right ? '-67px' : undefined)}; right: ${(props) => (props.right ? '-67px' : undefined)};
`; `;
const getDriveListLabel = () => { const getDriveListLabel = () => {
@ -53,7 +53,7 @@ const getDriveListLabel = () => {
}; };
const shouldShowDrivesButton = () => { const shouldShowDrivesButton = () => {
return !settings.get('disableExplicitDriveSelection'); return !settings.getSync('disableExplicitDriveSelection');
}; };
const getDriveSelectionStateSlice = () => ({ const getDriveSelectionStateSlice = () => ({
@ -117,12 +117,7 @@ export const DriveSelector = ({
setShowDriveSelectorModal(true); setShowDriveSelectorModal(true);
}} }}
reselectDrive={() => { reselectDrive={() => {
analytics.logEvent('Reselect drive', { analytics.logEvent('Reselect drive');
applicationSessionUuid: store.getState().toJS()
.applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS()
.flashingWorkflowUuid,
});
setShowDriveSelectorModal(true); setShowDriveSelectorModal(true);
}} }}
flashing={flashing} flashing={flashing}

View File

@ -27,12 +27,12 @@ import { SVGIcon } from '../../components/svg-icon/svg-icon';
import * as availableDrives from '../../models/available-drives'; import * as availableDrives from '../../models/available-drives';
import * as flashState from '../../models/flash-state'; import * as flashState from '../../models/flash-state';
import * as selection from '../../models/selection-state'; import * as selection from '../../models/selection-state';
import { store } from '../../models/store';
import * as analytics from '../../modules/analytics'; import * as analytics from '../../modules/analytics';
import { scanner as driveScanner } from '../../modules/drive-scanner'; import { scanner as driveScanner } from '../../modules/drive-scanner';
import * as imageWriter from '../../modules/image-writer'; import * as imageWriter from '../../modules/image-writer';
import * as progressStatus from '../../modules/progress-status'; import * as progressStatus from '../../modules/progress-status';
import * as notification from '../../os/notification'; import * as notification from '../../os/notification';
import { StepSelection } from '../../styled-components';
const COMPLETED_PERCENTAGE = 100; const COMPLETED_PERCENTAGE = 100;
const SPEED_PRECISION = 2; const SPEED_PRECISION = 2;
@ -200,10 +200,7 @@ export const Flash = ({
setErrorMessage(''); setErrorMessage('');
flashState.resetState(); flashState.resetState();
if (shouldRetry) { if (shouldRetry) {
analytics.logEvent('Restart after failure', { analytics.logEvent('Restart after failure');
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
} else { } else {
selection.clear(); selection.clear();
} }
@ -243,14 +240,16 @@ export const Flash = ({
</div> </div>
<div className="space-vertical-large"> <div className="space-vertical-large">
<StepSelection>
<ProgressButton <ProgressButton
striped={state.type === 'verifying'} type={state.type}
active={isFlashing} active={isFlashing}
percentage={state.percentage} percentage={state.percentage}
label={getProgressButtonLabel()} label={getProgressButtonLabel()}
disabled={Boolean(flashErrorCode) || shouldFlashStepBeDisabled} disabled={Boolean(flashErrorCode) || shouldFlashStepBeDisabled}
callback={tryFlash} callback={tryFlash}
></ProgressButton> />
</StepSelection>
{isFlashing && ( {isFlashing && (
<button <button

View File

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

View File

@ -61,7 +61,7 @@ export const BaseButton = styled(Button)`
height: 48px; height: 48px;
`; `;
export const IconButton = styled(props => <Button plain {...props} />)` export const IconButton = styled((props) => <Button plain {...props} />)`
&&& { &&& {
width: 24px; width: 24px;
height: 24px; height: 24px;

View File

@ -28,8 +28,6 @@ import * as settings from './app/models/settings';
import * as analytics from './app/modules/analytics'; import * as analytics from './app/modules/analytics';
import { buildWindowMenu } from './menu'; import { buildWindowMenu } from './menu';
const configUrl =
settings.get('configUrl') || 'https://balena.io/etcher/static/config.json';
const updatablePackageTypes = ['appimage', 'nsis', 'dmg']; const updatablePackageTypes = ['appimage', 'nsis', 'dmg'];
const packageUpdatable = _.includes(updatablePackageTypes, packageType); const packageUpdatable = _.includes(updatablePackageTypes, packageType);
let packageUpdated = false; let packageUpdated = false;
@ -38,7 +36,7 @@ async function checkForUpdates(interval: number) {
// We use a while loop instead of a setInterval to preserve // We use a while loop instead of a setInterval to preserve
// async execution time between each function call // async execution time between each function call
while (!packageUpdated) { while (!packageUpdated) {
if (settings.get('updatesEnabled')) { if (await settings.get('updatesEnabled')) {
try { try {
const release = await autoUpdater.checkForUpdates(); const release = await autoUpdater.checkForUpdates();
const isOutdated = const isOutdated =
@ -56,8 +54,8 @@ async function checkForUpdates(interval: number) {
} }
} }
function createMainWindow() { async function createMainWindow() {
const fullscreen = Boolean(settings.get('fullscreen')); const fullscreen = Boolean(await settings.get('fullscreen'));
const defaultWidth = 800; const defaultWidth = 800;
const defaultHeight = 480; const defaultHeight = 480;
let width = defaultWidth; let width = defaultWidth;
@ -100,7 +98,7 @@ function createMainWindow() {
// Prevent external resources from being loaded (like images) // Prevent external resources from being loaded (like images)
// when dropping them on the WebView. // when dropping them on the WebView.
// See https://github.com/electron/electron/issues/5919 // See https://github.com/electron/electron/issues/5919
mainWindow.webContents.on('will-navigate', event => { mainWindow.webContents.on('will-navigate', (event) => {
event.preventDefault(); event.preventDefault();
}); });
@ -111,12 +109,12 @@ function createMainWindow() {
const page = mainWindow.webContents; const page = mainWindow.webContents;
page.once('did-frame-finish-load', async () => { page.once('did-frame-finish-load', async () => {
autoUpdater.on('error', err => { autoUpdater.on('error', (err) => {
analytics.logException(err); analytics.logException(err);
}); });
if (packageUpdatable) { if (packageUpdatable) {
try { try {
const onlineConfig = await getConfig(configUrl); const onlineConfig = await getConfig();
const autoUpdaterConfig = _.get( const autoUpdaterConfig = _.get(
onlineConfig, onlineConfig,
['autoUpdates', 'autoUpdaterConfig'], ['autoUpdates', 'autoUpdaterConfig'],
@ -151,20 +149,12 @@ electron.app.on('before-quit', () => {
}); });
async function main(): Promise<void> { 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()) { if (electron.app.isReady()) {
createMainWindow(); await createMainWindow();
} else { } else {
electron.app.on('ready', createMainWindow); electron.app.on('ready', createMainWindow);
} }
} }
}
main(); main();

View File

@ -17,6 +17,7 @@
import { delay } from 'bluebird'; import { delay } from 'bluebird';
import { Drive as DrivelistDrive } from 'drivelist'; import { Drive as DrivelistDrive } from 'drivelist';
import * as sdk from 'etcher-sdk'; import * as sdk from 'etcher-sdk';
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as ipc from 'node-ipc'; import * as ipc from 'node-ipc';
@ -77,6 +78,7 @@ interface WriteResult {
successful: number; successful: number;
}; };
errors: Array<Error & { device: string }>; errors: Array<Error & { device: string }>;
sourceMetadata: sdk.sourceDestination.Metadata;
} }
/** /**
@ -84,38 +86,42 @@ interface WriteResult {
* @param {SourceDestination} source - source * @param {SourceDestination} source - source
* @param {SourceDestination[]} destinations - destinations * @param {SourceDestination[]} destinations - destinations
* @param {Boolean} verify - whether to validate the writes or not * @param {Boolean} verify - whether to validate the writes or not
* @param {Boolean} trim - whether to trim ext partitions before writing * @param {Boolean} autoBlockmapping - whether to trim ext partitions before writing
* @param {Function} onProgress - function to call on progress * @param {Function} onProgress - function to call on progress
* @param {Function} onFail - function to call on fail * @param {Function} onFail - function to call on fail
* @returns {Promise<{ bytesWritten, devices, errors} >} * @returns {Promise<{ bytesWritten, devices, errors} >}
*/ */
async function writeAndValidate( async function writeAndValidate({
source: sdk.sourceDestination.SourceDestination, source,
destinations: sdk.sourceDestination.BlockDevice[], destinations,
verify: boolean, verify,
trim: boolean, autoBlockmapping,
onProgress: sdk.multiWrite.OnProgressFunction, decompressFirst,
onFail: sdk.multiWrite.OnFailFunction, onProgress,
): Promise<WriteResult> { onFail,
let innerSource: sdk.sourceDestination.SourceDestination = await source.getInnerSource(); }: {
if (trim && (await innerSource.canRead())) { source: sdk.sourceDestination.SourceDestination;
innerSource = new sdk.sourceDestination.ConfiguredSource({ destinations: sdk.sourceDestination.BlockDevice[];
source: innerSource, verify: boolean;
shouldTrimPartitions: trim, autoBlockmapping: boolean;
createStreamFromDisk: true, decompressFirst: boolean;
}); onProgress: sdk.multiWrite.OnProgressFunction;
} onFail: sdk.multiWrite.OnFailFunction;
}): Promise<WriteResult> {
const { const {
sourceMetadata,
failures, failures,
bytesWritten, bytesWritten,
} = await sdk.multiWrite.pipeSourceToDestinations( } = await sdk.multiWrite.decompressThenFlash({
innerSource, source,
destinations, destinations,
onFail, onFail,
onProgress, onProgress,
verify, verify,
32, trim: autoBlockmapping,
); numBuffers: 32,
decompressFirst,
});
const result: WriteResult = { const result: WriteResult = {
bytesWritten, bytesWritten,
devices: { devices: {
@ -123,6 +129,7 @@ async function writeAndValidate(
successful: destinations.length - failures.size, successful: destinations.length - failures.size,
}, },
errors: [], errors: [],
sourceMetadata,
}; };
for (const [destination, error] of failures) { for (const [destination, error] of failures) {
const err = error as Error & { device: string }; const err = error as Error & { device: string };
@ -137,12 +144,15 @@ interface WriteOptions {
destinations: DrivelistDrive[]; destinations: DrivelistDrive[];
unmountOnSuccess: boolean; unmountOnSuccess: boolean;
validateWriteOnSuccess: boolean; validateWriteOnSuccess: boolean;
trim: boolean; autoBlockmapping: boolean;
decompressFirst: boolean;
source: SourceOptions; source: SourceOptions;
SourceType: string; SourceType: string;
} }
ipc.connectTo(IPC_SERVER_ID, () => { ipc.connectTo(IPC_SERVER_ID, () => {
// Remove leftover tmp files older than 1 hour
cleanupTmpFiles(Date.now() - 60 * 60 * 1000);
process.once('uncaughtException', handleError); process.once('uncaughtException', handleError);
// Gracefully exit on the following cases. If the parent // Gracefully exit on the following cases. If the parent
@ -219,8 +229,9 @@ ipc.connectTo(IPC_SERVER_ID, () => {
log(`Devices: ${destinations.join(', ')}`); log(`Devices: ${destinations.join(', ')}`);
log(`Umount on success: ${options.unmountOnSuccess}`); log(`Umount on success: ${options.unmountOnSuccess}`);
log(`Validate on success: ${options.validateWriteOnSuccess}`); log(`Validate on success: ${options.validateWriteOnSuccess}`);
log(`Trim: ${options.trim}`); log(`Auto blockmapping: ${options.autoBlockmapping}`);
const dests = _.map(options.destinations, destination => { log(`Decompress first: ${options.decompressFirst}`);
const dests = _.map(options.destinations, (destination) => {
return new sdk.sourceDestination.BlockDevice({ return new sdk.sourceDestination.BlockDevice({
drive: destination, drive: destination,
unmountOnSuccess: options.unmountOnSuccess, unmountOnSuccess: options.unmountOnSuccess,
@ -235,19 +246,20 @@ ipc.connectTo(IPC_SERVER_ID, () => {
path: options.imagePath, path: options.imagePath,
}); });
} else { } else {
source = new Http(options.imagePath); source = new Http({ url: options.imagePath });
} }
try { try {
const results = await writeAndValidate( const results = await writeAndValidate({
source, source,
dests, destinations: dests,
options.validateWriteOnSuccess, verify: options.validateWriteOnSuccess,
options.trim, autoBlockmapping: options.autoBlockmapping,
decompressFirst: options.decompressFirst,
onProgress, onProgress,
onFail, onFail,
); });
log(`Finish: ${results.bytesWritten}`); log(`Finish: ${results.bytesWritten}`);
results.errors = _.map(results.errors, error => { results.errors = _.map(results.errors, (error) => {
return toJSON(error); return toJSON(error);
}); });
ipc.of[IPC_SERVER_ID].emit('done', { results }); ipc.of[IPC_SERVER_ID].emit('done', { results });

View File

@ -67,7 +67,7 @@ export function isSourceDrive(drive: DrivelistDrive, image: Image): boolean {
} }
return _.some( return _.some(
_.map(mountpoints, mountpoint => { _.map(mountpoints, (mountpoint) => {
return pathIsInside(imagePath, mountpoint.path); return pathIsInside(imagePath, mountpoint.path);
}), }),
); );
@ -235,7 +235,7 @@ export function getListDriveImageCompatibilityStatuses(
drives: DrivelistDrive[], drives: DrivelistDrive[],
image: Image, image: Image,
) { ) {
return _.flatMap(drives, drive => { return _.flatMap(drives, (drive) => {
return getDriveImageCompatibilityStatuses(drive, image); return getDriveImageCompatibilityStatuses(drive, image);
}); });
} }

View File

@ -24,7 +24,7 @@ function createErrorDetails(options: {
description: (error: Error) => string; description: (error: Error) => string;
} { } {
return _.pick( return _.pick(
_.mapValues(options, value => { _.mapValues(options, (value) => {
return _.isFunction(value) ? value : _.constant(value); return _.isFunction(value) ? value : _.constant(value);
}), }),
['title', 'description'], ['title', 'description'],

View File

@ -26,11 +26,7 @@ import { lookup } from 'mime-types';
* > [ 'img', 'gz' ] * > [ 'img', 'gz' ]
*/ */
export function getFileExtensions(filePath: string): string[] { export function getFileExtensions(filePath: string): string[] {
return _.chain(filePath) return _.chain(filePath).split('.').tail().map(_.toLower).value();
.split('.')
.tail()
.map(_.toLower)
.value();
} }
/** /**

View File

@ -21,6 +21,7 @@ import * as tmp from 'tmp';
import { promisify } from 'util'; import { promisify } from 'util';
import * as errors from './errors'; import * as errors from './errors';
import * as settings from '../gui/app/models/settings';
const getAsync = promisify(request.get); const getAsync = promisify(request.get);
@ -40,8 +41,8 @@ export function percentageToFloat(percentage: any) {
/** /**
* @summary Check if obj has one or many specific props * @summary Check if obj has one or many specific props
*/ */
export function hasProps(obj: any, props: string[]): boolean { export function hasProps(obj: _.Dictionary<any>, props: string[]): boolean {
return _.every(props, prop => { return _.every(props, (prop) => {
return _.has(obj, prop); return _.has(obj, prop);
}); });
} }
@ -50,7 +51,10 @@ export function hasProps(obj: any, props: string[]): boolean {
* @summary Get etcher configs stored online * @summary Get etcher configs stored online
* @param {String} - url where config.json is stored * @param {String} - url where config.json is stored
*/ */
export async function getConfig(configUrl: string): Promise<any> { export async function getConfig(): Promise<_.Dictionary<any>> {
const configUrl =
(await settings.get('configUrl')) ||
'https://balena.io/etcher/static/config.json';
return (await getAsync({ url: configUrl, json: true })).body; return (await getAsync({ url: configUrl, json: true })).body;
} }

800
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,11 +4,6 @@
"displayName": "balenaEtcher", "displayName": "balenaEtcher",
"version": "1.5.82", "version": "1.5.82",
"packageType": "local", "packageType": "local",
"updates": {
"enabled": true,
"sleepDays": 7,
"semverRange": "<2.0.0"
},
"main": "generated/etcher.js", "main": "generated/etcher.js",
"description": "Flash OS images to SD cards and USB drives, safely and easily.", "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.", "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.",
@ -57,11 +52,10 @@
"bindings": "^1.3.0", "bindings": "^1.3.0",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
"bootstrap-sass": "^3.3.6", "bootstrap-sass": "^3.3.6",
"color": "^2.0.1",
"d3": "^4.13.0", "d3": "^4.13.0",
"debug": "^3.1.0", "debug": "^3.1.0",
"electron-updater": "4.0.6", "electron-updater": "4.0.6",
"etcher-sdk": "^3.0.1", "etcher-sdk": "^4.0.0",
"flexboxgrid": "^6.3.0", "flexboxgrid": "^6.3.0",
"immutable": "^3.8.1", "immutable": "^3.8.1",
"inactivity-timer": "^1.0.0", "inactivity-timer": "^1.0.0",
@ -87,6 +81,7 @@
"uuid": "^3.0.1" "uuid": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {
"@balena/lint": "^5.0.4",
"@types/bindings": "^1.3.0", "@types/bindings": "^1.3.0",
"@types/bluebird": "^3.5.30", "@types/bluebird": "^3.5.30",
"@types/chai": "^4.2.7", "@types/chai": "^4.2.7",
@ -113,7 +108,6 @@
"node-gyp": "^3.8.0", "node-gyp": "^3.8.0",
"node-sass": "^4.12.0", "node-sass": "^4.12.0",
"omit-deep-lodash": "1.1.4", "omit-deep-lodash": "1.1.4",
"resin-lint": "^3.2.0",
"sass-lint": "^1.12.1", "sass-lint": "^1.12.1",
"simple-progress-webpack-plugin": "^1.1.2", "simple-progress-webpack-plugin": "^1.1.2",
"sinon": "^8.0.4", "sinon": "^8.0.4",

View File

@ -28,16 +28,12 @@ describe('Model: flashState', function() {
it('should be able to reset the progress state', function () { it('should be able to reset the progress state', function () {
flashState.setFlashingFlag(); flashState.setFlashingFlag();
flashState.setProgressState({ flashState.setProgressState({
flashing: 2,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
type: 'flashing', type: 'flashing',
percentage: 50, percentage: 50,
eta: 15, eta: 15,
speed: 100000000000, speed: 100000000000,
averageSpeed: 100000000000, averageSpeed: 100000000000,
totalSpeed: 200000000000,
bytes: 0, bytes: 0,
position: 0, position: 0,
active: 0, active: 0,
@ -46,14 +42,11 @@ describe('Model: flashState', function() {
flashState.resetState(); flashState.resetState();
expect(flashState.getFlashState()).to.deep.equal({ expect(flashState.getFlashState()).to.deep.equal({
flashing: 0, active: 0,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
percentage: 0, percentage: 0,
speed: null, speed: null,
averageSpeed: null, averageSpeed: null,
totalSpeed: null,
}); });
}); });
@ -100,16 +93,12 @@ describe('Model: flashState', function() {
expect(function () { expect(function () {
flashState.setProgressState({ flashState.setProgressState({
flashing: 2,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
type: 'flashing', type: 'flashing',
percentage: 50, percentage: 50,
eta: 15, eta: 15,
speed: 100000000000, speed: 100000000000,
averageSpeed: 100000000000, averageSpeed: 100000000000,
totalSpeed: 200000000000,
bytes: 0, bytes: 0,
position: 0, position: 0,
active: 0, active: 0,
@ -121,16 +110,12 @@ describe('Model: flashState', function() {
flashState.setFlashingFlag(); flashState.setFlashingFlag();
expect(function () { expect(function () {
flashState.setProgressState({ flashState.setProgressState({
flashing: 2,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
type: 'flashing', type: 'flashing',
percentage: 0, percentage: 0,
eta: 15, eta: 15,
speed: 100000000000, speed: 100000000000,
averageSpeed: 100000000000, averageSpeed: 100000000000,
totalSpeed: 200000000000,
bytes: 0, bytes: 0,
position: 0, position: 0,
active: 0, active: 0,
@ -142,16 +127,12 @@ describe('Model: flashState', function() {
flashState.setFlashingFlag(); flashState.setFlashingFlag();
expect(function () { expect(function () {
flashState.setProgressState({ flashState.setProgressState({
flashing: 2,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
type: 'flashing', type: 'flashing',
percentage: 101, percentage: 101,
eta: 15, eta: 15,
speed: 0, speed: 0,
averageSpeed: 0, averageSpeed: 0,
totalSpeed: 1,
bytes: 0, bytes: 0,
position: 0, position: 0,
active: 0, active: 0,
@ -163,16 +144,12 @@ describe('Model: flashState', function() {
flashState.setFlashingFlag(); flashState.setFlashingFlag();
expect(function () { expect(function () {
flashState.setProgressState({ flashState.setProgressState({
flashing: 2,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
type: 'flashing', type: 'flashing',
percentage: -1, percentage: -1,
eta: 15, eta: 15,
speed: 0, speed: 0,
averageSpeed: 0, averageSpeed: 0,
totalSpeed: 1,
bytes: 0, bytes: 0,
position: 0, position: 0,
active: 0, active: 0,
@ -184,16 +161,12 @@ describe('Model: flashState', function() {
flashState.setFlashingFlag(); flashState.setFlashingFlag();
expect(function () { expect(function () {
flashState.setProgressState({ flashState.setProgressState({
flashing: 2,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
type: 'flashing', type: 'flashing',
percentage: 50, percentage: 50,
eta: 0, eta: 0,
speed: 100000000000, speed: 100000000000,
averageSpeed: 100000000000, averageSpeed: 100000000000,
totalSpeed: 200000000000,
bytes: 0, bytes: 0,
position: 0, position: 0,
active: 0, active: 0,
@ -205,9 +178,6 @@ describe('Model: flashState', function() {
flashState.setFlashingFlag(); flashState.setFlashingFlag();
expect(function () { expect(function () {
flashState.setProgressState({ flashState.setProgressState({
flashing: 2,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
type: 'flashing', type: 'flashing',
percentage: 50, percentage: 50,
@ -215,7 +185,6 @@ describe('Model: flashState', function() {
eta: '15', eta: '15',
speed: 100000000000, speed: 100000000000,
averageSpeed: 100000000000, averageSpeed: 100000000000,
totalSpeed: 200000000000,
bytes: 0, bytes: 0,
position: 0, position: 0,
active: 0, active: 0,
@ -228,15 +197,11 @@ describe('Model: flashState', function() {
expect(function () { expect(function () {
// @ts-ignore // @ts-ignore
flashState.setProgressState({ flashState.setProgressState({
flashing: 2,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
type: 'flashing', type: 'flashing',
percentage: 50, percentage: 50,
eta: 15, eta: 15,
averageSpeed: 0, averageSpeed: 0,
totalSpeed: 1,
bytes: 0, bytes: 0,
position: 0, position: 0,
active: 0, active: 0,
@ -248,16 +213,12 @@ describe('Model: flashState', function() {
flashState.setFlashingFlag(); flashState.setFlashingFlag();
expect(function () { expect(function () {
flashState.setProgressState({ flashState.setProgressState({
flashing: 2,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
type: 'flashing', type: 'flashing',
percentage: 50, percentage: 50,
eta: 15, eta: 15,
speed: 0, speed: 0,
averageSpeed: 0, averageSpeed: 0,
totalSpeed: 1,
bytes: 0, bytes: 0,
position: 0, position: 0,
active: 0, active: 0,
@ -265,61 +226,15 @@ describe('Model: flashState', function() {
}).to.not.throw('Missing flash fields: speed'); }).to.not.throw('Missing flash fields: speed');
}); });
it('should throw if totalSpeed is missing', function() {
flashState.setFlashingFlag();
expect(function() {
// @ts-ignore
flashState.setProgressState({
flashing: 2,
verifying: 0,
successful: 0,
failed: 0,
type: 'flashing',
percentage: 50,
eta: 15,
speed: 1,
averageSpeed: 1,
bytes: 0,
position: 0,
active: 0,
});
}).to.throw('Missing flash fields: totalSpeed');
});
it('should not throw if totalSpeed is 0', function() {
flashState.setFlashingFlag();
expect(function() {
flashState.setProgressState({
flashing: 2,
verifying: 0,
successful: 0,
failed: 0,
type: 'flashing',
percentage: 50,
eta: 15,
speed: 0,
averageSpeed: 0,
totalSpeed: 0,
bytes: 0,
position: 0,
active: 0,
});
}).to.not.throw('Missing flash fields: totalSpeed');
});
it('should floor the percentage number', function () { it('should floor the percentage number', function () {
flashState.setFlashingFlag(); flashState.setFlashingFlag();
flashState.setProgressState({ flashState.setProgressState({
flashing: 2,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
type: 'flashing', type: 'flashing',
percentage: 50.253559459485, percentage: 50.253559459485,
eta: 15, eta: 15,
speed: 0, speed: 0,
averageSpeed: 0, averageSpeed: 0,
totalSpeed: 1,
bytes: 0, bytes: 0,
position: 0, position: 0,
active: 0, active: 0,
@ -344,7 +259,6 @@ describe('Model: flashState', function() {
eta: 0, eta: 0,
speed: 0, speed: 0,
averageSpeed: 0, averageSpeed: 0,
totalSpeed: 0,
bytes: 0, bytes: 0,
position: 0, position: 0,
active: 0, active: 0,
@ -357,15 +271,11 @@ describe('Model: flashState', function() {
expect(() => { expect(() => {
flashState.setFlashingFlag(); flashState.setFlashingFlag();
flashState.setProgressState({ flashState.setProgressState({
flashing: 0,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
percentage: 0, percentage: 0,
eta: 0, eta: 0,
speed: 0, speed: 0,
averageSpeed: 0, averageSpeed: 0,
totalSpeed: 0,
bytes: 0, bytes: 0,
position: 0, position: 0,
active: 0, active: 0,
@ -395,28 +305,21 @@ describe('Model: flashState', function() {
flashState.resetState(); flashState.resetState();
const currentFlashState = flashState.getFlashState(); const currentFlashState = flashState.getFlashState();
expect(currentFlashState).to.deep.equal({ expect(currentFlashState).to.deep.equal({
flashing: 0, active: 0,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
percentage: 0, percentage: 0,
speed: null, speed: null,
averageSpeed: null, averageSpeed: null,
totalSpeed: null,
}); });
}); });
it('should return the current flash state', function () { it('should return the current flash state', function () {
const state = { const state = {
flashing: 1,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
percentage: 50, percentage: 50,
eta: 15, eta: 15,
speed: 0, speed: 0,
averageSpeed: 0, averageSpeed: 0,
totalSpeed: 0,
bytes: 0, bytes: 0,
position: 0, position: 0,
active: 0, active: 0,
@ -427,15 +330,11 @@ describe('Model: flashState', function() {
flashState.setProgressState(state); flashState.setProgressState(state);
const currentFlashState = flashState.getFlashState(); const currentFlashState = flashState.getFlashState();
expect(currentFlashState).to.deep.equal({ expect(currentFlashState).to.deep.equal({
flashing: 1,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
percentage: 50, percentage: 50,
eta: 15, eta: 15,
speed: 0, speed: 0,
averageSpeed: 0, averageSpeed: 0,
totalSpeed: 0,
bytes: 0, bytes: 0,
position: 0, position: 0,
active: 0, active: 0,
@ -532,30 +431,22 @@ describe('Model: flashState', function() {
flashState.setFlashingFlag(); flashState.setFlashingFlag();
flashState.setProgressState({ flashState.setProgressState({
flashing: 2,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
type: 'flashing', type: 'flashing',
percentage: 50, percentage: 50,
eta: 15, eta: 15,
speed: 100000000000, speed: 100000000000,
averageSpeed: 100000000000, averageSpeed: 100000000000,
totalSpeed: 200000000000,
bytes: 0, bytes: 0,
position: 0, position: 0,
active: 0, active: 2,
}); });
expect(flashState.getFlashState()).to.not.deep.equal({ expect(flashState.getFlashState()).to.not.deep.equal({
flashing: 2,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
percentage: 0, percentage: 0,
speed: 0, speed: 0,
averageSpeed: 0, averageSpeed: 0,
totalSpeed: 0,
}); });
flashState.unsetFlashingFlag({ flashState.unsetFlashingFlag({
@ -564,14 +455,11 @@ describe('Model: flashState', function() {
}); });
expect(flashState.getFlashState()).to.deep.equal({ expect(flashState.getFlashState()).to.deep.equal({
flashing: 0, active: 0,
verifying: 0,
successful: 0,
failed: 0, failed: 0,
percentage: 0, percentage: 0,
speed: null, speed: null,
averageSpeed: null, averageSpeed: null,
totalSpeed: null,
}); });
}); });

View File

@ -18,206 +18,80 @@ import { expect } from 'chai';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { stub } from 'sinon'; import { stub } from 'sinon';
import * as localSettings from '../../../lib/gui/app/models/local-settings';
import * as settings from '../../../lib/gui/app/models/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 { try {
await promise; await promise;
} catch (error) { } catch (error) {
fn(error); await fn(error);
return; return;
} }
throw new Error('Expected error was not thrown'); throw new Error('Expected error was not thrown');
} }
describe('Browser: settings', function() { describe('Browser: settings', () => {
beforeEach(function() { it('should be able to set and read values', async () => {
return settings.reset(); 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); describe('.set()', () => {
it('should not change the application state if storing to the local machine results in an error', async () => {
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() {
await settings.set('foo', 'bar'); 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'); const writeConfigFileStub = stub(settings, 'writeConfigFile');
localSettingsWriteAllStub.returns( writeConfigFileStub.returns(Promise.reject(new Error('settings error')));
Promise.reject(new Error('localSettings 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).to.be.an.instanceof(Error);
expect(error.message).to.equal('localSettings error'); expect(error.message).to.equal('settings error');
localSettingsWriteAllStub.restore(); expect(await settings.get('foo')).to.equal('bar');
expect(settings.get('foo')).to.equal('bar');
}); });
writeConfigFileStub.restore();
}); });
}); });
describe('.load()', function() { describe('.set()', () => {
it('should extend the application state with the local settings content', function() { it('should set an unknown key', async () => {
const object = { expect(await settings.get('foobar')).to.be.undefined;
foo: 'bar', await settings.set('foobar', true);
}; expect(await settings.get('foobar')).to.be.true;
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),
);
});
});
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 the key to undefined if no value', async () => {
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() {
await settings.set('foo', 'bar'); await settings.set('foo', 'bar');
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;
});
const localSettingsWriteAllStub = stub(localSettings, 'writeAll'); it('should store the setting to the local machine', async () => {
localSettingsWriteAllStub.returns( const data = await settings.readAll();
Promise.reject(new Error('localSettings error')), expect(data.foo).to.be.undefined;
); await settings.set('foo', 'bar');
const data1 = await settings.readAll();
expect(data1.foo).to.equal('bar');
});
await checkError(settings.set('foo', 'baz'), error => { 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).to.be.an.instanceof(Error);
expect(error.message).to.equal('localSettings error'); expect(error.message).to.equal('settings error');
localSettingsWriteAllStub.restore(); expect(await settings.get('foo')).to.equal('bar');
expect(settings.get('foo')).to.equal('bar'); });
}); writeConfigFileStub.restore();
});
});
describe('.getAll()', function() {
it('should initial return all default values', function() {
expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS);
}); });
}); });
}); });

View File

@ -86,7 +86,7 @@ describe('Browser: imageWriter', () => {
let rejectError: Error; let rejectError: Error;
imageWriter imageWriter
.flash(imagePath, [fakeDrive], sourceOptions) .flash(imagePath, [fakeDrive], sourceOptions)
.catch(error => { .catch((error) => {
rejectError = error; rejectError = error;
}) })
.finally(() => { .finally(() => {
@ -140,7 +140,7 @@ describe('Browser: imageWriter', () => {
let rejection: Error; let rejection: Error;
imageWriter imageWriter
.flash(imagePath, [fakeDrive], sourceOptions) .flash(imagePath, [fakeDrive], sourceOptions)
.catch(error => { .catch((error) => {
rejection = error; rejection = error;
}) })
.finally(() => { .finally(() => {

View File

@ -23,9 +23,8 @@ describe('Browser: progressStatus', function() {
describe('.fromFlashState()', function () { describe('.fromFlashState()', function () {
beforeEach(function () { beforeEach(function () {
this.state = { this.state = {
flashing: 1, active: 1,
verifying: 0, type: 'flashing',
successful: 0,
failed: 0, failed: 0,
percentage: 0, percentage: 0,
eta: 15, eta: 15,
@ -42,31 +41,29 @@ describe('Browser: progressStatus', function() {
it('should handle percentage == 0, flashing, unmountOnSuccess', function () { it('should handle percentage == 0, flashing, unmountOnSuccess', function () {
this.state.speed = 0; this.state.speed = 0;
expect(progressStatus.fromFlashState(this.state)).to.equal('Starting...'); expect(progressStatus.fromFlashState(this.state)).to.equal('0% Flashing');
}); });
it('should handle percentage == 0, flashing, !unmountOnSuccess', function () { it('should handle percentage == 0, flashing, !unmountOnSuccess', function () {
this.state.speed = 0; this.state.speed = 0;
settings.set('unmountOnSuccess', false); settings.set('unmountOnSuccess', false);
expect(progressStatus.fromFlashState(this.state)).to.equal('Starting...'); expect(progressStatus.fromFlashState(this.state)).to.equal('0% Flashing');
}); });
it('should handle percentage == 0, verifying, unmountOnSuccess', function () { it('should handle percentage == 0, verifying, unmountOnSuccess', function () {
this.state.speed = 0; this.state.speed = 0;
this.state.flashing = 0; this.state.type = 'verifying';
this.state.verifying = 1;
expect(progressStatus.fromFlashState(this.state)).to.equal( expect(progressStatus.fromFlashState(this.state)).to.equal(
'Validating...', '0% Validating',
); );
}); });
it('should handle percentage == 0, verifying, !unmountOnSuccess', function () { it('should handle percentage == 0, verifying, !unmountOnSuccess', function () {
this.state.speed = 0; this.state.speed = 0;
this.state.flashing = 0; this.state.type = 'verifying';
this.state.verifying = 1;
settings.set('unmountOnSuccess', false); settings.set('unmountOnSuccess', false);
expect(progressStatus.fromFlashState(this.state)).to.equal( expect(progressStatus.fromFlashState(this.state)).to.equal(
'Validating...', '0% Validating',
); );
}); });
@ -86,18 +83,16 @@ describe('Browser: progressStatus', function() {
}); });
it('should handle percentage == 50, verifying, unmountOnSuccess', function () { it('should handle percentage == 50, verifying, unmountOnSuccess', function () {
this.state.flashing = 0;
this.state.verifying = 1;
this.state.percentage = 50; this.state.percentage = 50;
this.state.type = 'verifying';
expect(progressStatus.fromFlashState(this.state)).to.equal( expect(progressStatus.fromFlashState(this.state)).to.equal(
'50% Validating', '50% Validating',
); );
}); });
it('should handle percentage == 50, verifying, !unmountOnSuccess', function () { it('should handle percentage == 50, verifying, !unmountOnSuccess', function () {
this.state.flashing = 0;
this.state.verifying = 1;
this.state.percentage = 50; this.state.percentage = 50;
this.state.type = 'verifying';
settings.set('unmountOnSuccess', false); settings.set('unmountOnSuccess', false);
expect(progressStatus.fromFlashState(this.state)).to.equal( expect(progressStatus.fromFlashState(this.state)).to.equal(
'50% Validating', '50% Validating',
@ -115,7 +110,7 @@ describe('Browser: progressStatus', function() {
this.state.percentage = 100; this.state.percentage = 100;
settings.set('validateWriteOnSuccess', false); settings.set('validateWriteOnSuccess', false);
expect(progressStatus.fromFlashState(this.state)).to.equal( expect(progressStatus.fromFlashState(this.state)).to.equal(
'Unmounting...', 'Finishing...',
); );
}); });
@ -129,17 +124,14 @@ describe('Browser: progressStatus', function() {
}); });
it('should handle percentage == 100, verifying, unmountOnSuccess', function () { it('should handle percentage == 100, verifying, unmountOnSuccess', function () {
this.state.flashing = 0;
this.state.verifying = 1;
this.state.percentage = 100; this.state.percentage = 100;
this.state.type = 'verifying';
expect(progressStatus.fromFlashState(this.state)).to.equal( expect(progressStatus.fromFlashState(this.state)).to.equal(
'Unmounting...', 'Finishing...',
); );
}); });
it('should handle percentage == 100, validatinf, !unmountOnSuccess', function () { it('should handle percentage == 100, validatinf, !unmountOnSuccess', function () {
this.state.flashing = 0;
this.state.verifying = 1;
this.state.percentage = 100; this.state.percentage = 100;
settings.set('unmountOnSuccess', false); settings.set('unmountOnSuccess', false);
expect(progressStatus.fromFlashState(this.state)).to.equal( expect(progressStatus.fromFlashState(this.state)).to.equal(

View File

@ -30,9 +30,8 @@ describe('Browser: WindowProgress', function() {
windowProgress.currentWindow.setTitle = this.setTitleSpy; windowProgress.currentWindow.setTitle = this.setTitleSpy;
this.state = { this.state = {
flashing: 1, active: 1,
verifying: 0, type: 'flashing',
successful: 0,
failed: 0, failed: 0,
percentage: 85, percentage: 85,
speed: 100, speed: 100,
@ -79,8 +78,7 @@ describe('Browser: WindowProgress', function() {
}); });
it('should set the verifying title', function () { it('should set the verifying title', function () {
this.state.flashing = 0; this.state.type = 'verifying';
this.state.verifying = 1;
windowProgress.set(this.state); windowProgress.set(this.state);
assert.calledWith(this.setTitleSpy, ' 85% Validating'); assert.calledWith(this.setTitleSpy, ' 85% Validating');
}); });
@ -89,7 +87,7 @@ describe('Browser: WindowProgress', function() {
this.state.percentage = 0; this.state.percentage = 0;
this.state.speed = 0; this.state.speed = 0;
windowProgress.set(this.state); windowProgress.set(this.state);
assert.calledWith(this.setTitleSpy, ' Starting...'); assert.calledWith(this.setTitleSpy, ' 0% Flashing');
}); });
it('should set the finishing title', function () { it('should set the finishing title', function () {

View File

@ -981,7 +981,7 @@ describe('Shared: DriveConstraints', function() {
) => { ) => {
// Sort so that order doesn't matter // Sort so that order doesn't matter
const expectedTuplesSorted = _.sortBy( const expectedTuplesSorted = _.sortBy(
_.map(expectedTuples, tuple => { _.map(expectedTuples, (tuple) => {
return { return {
type: constraints.COMPATIBILITY_STATUS_TYPES[tuple[0]], type: constraints.COMPATIBILITY_STATUS_TYPES[tuple[0]],
// @ts-ignore // @ts-ignore

View File

@ -28,7 +28,7 @@ describe('Shared: Errors', function() {
it('should contain title and description function properties', function () { it('should contain title and description function properties', function () {
expect( expect(
_.every( _.every(
_.map(errors.HUMAN_FRIENDLY, error => { _.map(errors.HUMAN_FRIENDLY, (error) => {
return _.isFunction(error.title) && _.isFunction(error.description); return _.isFunction(error.title) && _.isFunction(error.description);
}), }),
), ),
@ -693,7 +693,7 @@ describe('Shared: Errors', function() {
}); });
describe('.isUserError()', function () { describe('.isUserError()', function () {
_.each([0, '', false], value => { _.each([0, '', false], (value) => {
it(`should return true if report equals ${value}`, function () { it(`should return true if report equals ${value}`, function () {
const error = new Error('foo bar'); const error = new Error('foo bar');
// @ts-ignore // @ts-ignore
@ -702,7 +702,7 @@ describe('Shared: Errors', function() {
}); });
}); });
_.each([undefined, null, true, 1, 3, 'foo'], value => { _.each([undefined, null, true, 1, 3, 'foo'], (value) => {
it(`should return false if report equals ${value}`, function () { it(`should return false if report equals ${value}`, function () {
const error = new Error('foo bar'); const error = new Error('foo bar');
// @ts-ignore // @ts-ignore

View File

@ -83,7 +83,7 @@ describe('Shared: fileExtensions', function() {
extensions: ['dmg'], extensions: ['dmg'],
}, },
], ],
testCase => { (testCase) => {
it(`should return ${testCase.extensions} for ${testCase.file}`, function () { it(`should return ${testCase.extensions} for ${testCase.file}`, function () {
expect(fileExtensions.getFileExtensions(testCase.file)).to.deep.equal( expect(fileExtensions.getFileExtensions(testCase.file)).to.deep.equal(
testCase.extensions, testCase.extensions,

View File

@ -38,7 +38,7 @@ describe('Shared: Messages', function() {
}); });
it('should contain function properties in each category', function () { it('should contain function properties in each category', function () {
_.each(messages, category => { _.each(messages, (category) => {
expect(_.every(_.map(category, _.isFunction))).to.be.true; expect(_.every(_.map(category, _.isFunction))).to.be.true;
}); });
}); });

View File

@ -89,7 +89,7 @@ describe('Shared: SupportedFormats', function() {
'path/to/filename.sdcard', 'path/to/filename.sdcard',
'path/to/filename.wic', 'path/to/filename.wic',
], ],
filename => { (filename) => {
it(`should return true for ${filename}`, function () { it(`should return true for ${filename}`, function () {
const isSupported = supportedFormats.isSupportedImage(filename); const isSupported = supportedFormats.isSupportedImage(filename);
expect(isSupported).to.be.true; expect(isSupported).to.be.true;
@ -254,7 +254,7 @@ describe('Shared: SupportedFormats', function() {
'/path/to/Win10_1607_SingleLang_English_x32.iso', '/path/to/Win10_1607_SingleLang_English_x32.iso',
'/path/to/en_winxp_pro_x86_build2600_iso.img', '/path/to/en_winxp_pro_x86_build2600_iso.img',
], ],
imagePath => { (imagePath) => {
it(`should return true if filename is ${imagePath}`, function () { it(`should return true if filename is ${imagePath}`, function () {
const looksLikeWindowsImage = supportedFormats.looksLikeWindowsImage( const looksLikeWindowsImage = supportedFormats.looksLikeWindowsImage(
imagePath, imagePath,
@ -269,7 +269,7 @@ describe('Shared: SupportedFormats', function() {
'C:\\path\\to\\2017-01-11-raspbian-jessie.img', 'C:\\path\\to\\2017-01-11-raspbian-jessie.img',
'/path/to/2017-01-11-raspbian-jessie.img', '/path/to/2017-01-11-raspbian-jessie.img',
], ],
imagePath => { (imagePath) => {
it(`should return false if filename is ${imagePath}`, function () { it(`should return false if filename is ${imagePath}`, function () {
const looksLikeWindowsImage = supportedFormats.looksLikeWindowsImage( const looksLikeWindowsImage = supportedFormats.looksLikeWindowsImage(
imagePath, imagePath,