From ee62b9a4c762b793bde2d7472bfe5f5a61b4de30 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 23 Apr 2020 17:06:21 +0200 Subject: [PATCH] Decompress images before flashing, remove trim setting, trim ext partitions Changelog-entry: Decompress images before flashing, remove trim setting, trim ext partitions Change-type: patch --- lib/gui/app/app.ts | 41 ++++-- .../progress-button/progress-button.tsx | 119 ++++------------- lib/gui/app/components/settings/settings.tsx | 4 - .../source-selector/source-selector.tsx | 2 +- lib/gui/app/models/flash-state.ts | 7 - lib/gui/app/models/settings.ts | 3 +- lib/gui/app/models/store.ts | 21 +-- lib/gui/app/modules/image-writer.ts | 8 +- lib/gui/app/modules/progress-status.ts | 69 +++++----- lib/gui/app/pages/main/Flash.tsx | 19 +-- lib/gui/modules/child-writer.ts | 70 +++++----- npm-shrinkwrap.json | 58 ++++----- package.json | 3 +- tests/gui/models/flash-state.spec.ts | 120 +----------------- tests/gui/modules/progress-status.spec.ts | 34 ++--- tests/gui/os/window-progress.spec.ts | 10 +- 16 files changed, 202 insertions(+), 386 deletions(-) diff --git a/lib/gui/app/app.ts b/lib/gui/app/app.ts index 8c493267..865a22fb 100644 --- a/lib/gui/app/app.ts +++ b/lib/gui/app/app.ts @@ -89,30 +89,43 @@ analytics.logEvent('Application start', { applicationSessionUuid, }); +const debouncedLog = _.debounce(console.log, 1000, { maxWait: 1000 }); + +function pluralize(word: string, quantity: number) { + return `${quantity} ${word}${quantity === 1 ? '' : 's'}`; +} + observe(() => { if (!flashState.isFlashing()) { return; } - const currentFlashState = flashState.getFlashState(); - const stateType = - !currentFlashState.flashing && currentFlashState.verifying - ? `Verifying ${currentFlashState.verifying}` - : `Flashing ${currentFlashState.flashing}`; + windowProgress.set(currentFlashState); + 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()` // property being set, and the flashing actually starting, which // might cause some non-sense flashing state logs including // `undefined` values. - analytics.logDebug( - `${stateType} devices, ` + - `${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s ` + - `(total ${currentFlashState.totalSpeed} MB/s) ` + - `eta in ${currentFlashState.eta}s ` + - `with ${currentFlashState.failed} failed devices`, - ); - - windowProgress.set(currentFlashState); + debouncedLog(outdent({ newline: ' ' })` + ${_.capitalize(currentFlashState.type)} + ${active}, + ${currentFlashState.percentage}% + at + ${(currentFlashState.speed || 0).toFixed(2)} + MB/s + (total ${(currentFlashState.speed * currentFlashState.active).toFixed(2)} MB/s) + ${eta} + with + ${pluralize('failed device', currentFlashState.failed)} + `); }); /** diff --git a/lib/gui/app/components/progress-button/progress-button.tsx b/lib/gui/app/components/progress-button/progress-button.tsx index 052baa5b..deed25fb 100644 --- a/lib/gui/app/components/progress-button/progress-button.tsx +++ b/lib/gui/app/components/progress-button/progress-button.tsx @@ -14,39 +14,11 @@ * limitations under the License. */ -import * as Color from 'color'; import * as React from 'react'; 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 { 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; -`; +import { StepButton } from '../../styled-components'; const FlashProgressBar = styled(ProgressBar)` > div { @@ -54,6 +26,10 @@ const FlashProgressBar = styled(ProgressBar)` height: 48px; color: white !important; text-shadow: none !important; + transition-duration: 0s; + > div { + transition-duration: 0s; + } } width: 200px; @@ -61,40 +37,11 @@ const FlashProgressBar = styled(ProgressBar)` font-size: 16px; line-height: 48px; - background: ${Color(colors.warning.background) - .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; + background: #2f3033; `; interface ProgressButtonProps { - striped: boolean; + type: 'decompressing' | 'flashing' | 'verifying'; active: boolean; percentage: number; label: string; @@ -102,45 +49,35 @@ interface ProgressButtonProps { callback: () => any; } +const colors = { + decompressing: '#00aeef', + flashing: '#da60ff', + verifying: '#1ac135', +} as const; + /** * Progress Button component */ export class ProgressButton extends React.Component { public render() { if (this.props.active) { - if (this.props.striped) { - return ( - - - {this.props.label} - - - ); - } - return ( - - - {this.props.label} - - - ); - } - - return ( - - {this.props.label} - - + + ); + } + return ( + + {this.props.label} + ); } } diff --git a/lib/gui/app/components/settings/settings.tsx b/lib/gui/app/components/settings/settings.tsx index 3ff61072..f53681ad 100644 --- a/lib/gui/app/components/settings/settings.tsx +++ b/lib/gui/app/components/settings/settings.tsx @@ -87,10 +87,6 @@ const settingsList: Setting[] = [ name: 'validateWriteOnSuccess', label: 'Validate write on success', }, - { - name: 'trim', - label: 'Trim ext{2,3,4} partitions before writing (raw images only)', - }, { name: 'updatesEnabled', label: 'Auto-updates enabled', diff --git a/lib/gui/app/components/source-selector/source-selector.tsx b/lib/gui/app/components/source-selector/source-selector.tsx index 9fe329eb..f8c60d10 100644 --- a/lib/gui/app/components/source-selector/source-selector.tsx +++ b/lib/gui/app/components/source-selector/source-selector.tsx @@ -375,7 +375,7 @@ export class SourceSelector extends React.Component< analytics.logEvent('Unsupported protocol', { path: imagePath }); return; } - source = new sourceDestination.Http(imagePath); + source = new sourceDestination.Http({ url: imagePath }); } try { diff --git a/lib/gui/app/models/flash-state.ts b/lib/gui/app/models/flash-state.ts index 4c2c1d56..079eb0df 100644 --- a/lib/gui/app/models/flash-state.ts +++ b/lib/gui/app/models/flash-state.ts @@ -85,13 +85,6 @@ export function setProgressState( return _.round(bytesToMegabytes(state.speed), PRECISION); } - return null; - }), - totalSpeed: _.attempt(() => { - if (_.isFinite(state.totalSpeed)) { - return _.round(bytesToMegabytes(state.totalSpeed), PRECISION); - } - return null; }), }); diff --git a/lib/gui/app/models/settings.ts b/lib/gui/app/models/settings.ts index 2f3a62df..26849c70 100644 --- a/lib/gui/app/models/settings.ts +++ b/lib/gui/app/models/settings.ts @@ -29,13 +29,14 @@ export const DEFAULT_SETTINGS: _.Dictionary = { errorReporting: true, unmountOnSuccess: true, validateWriteOnSuccess: true, - trim: false, updatesEnabled: packageJSON.updates.enabled && !_.includes(['rpm', 'deb'], packageJSON.packageType), lastSleptUpdateNotifier: null, lastSleptUpdateNotifierVersion: null, desktopNotifications: true, + autoBlockmapping: true, + decompressFirst: true, }; let settings = _.cloneDeep(DEFAULT_SETTINGS); diff --git a/lib/gui/app/models/store.ts b/lib/gui/app/models/store.ts index 412e5ba8..8da31f22 100644 --- a/lib/gui/app/models/store.ts +++ b/lib/gui/app/models/store.ts @@ -45,7 +45,7 @@ function verifyNoNilFields( /** * @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 @@ -65,14 +65,11 @@ const DEFAULT_STATE = Immutable.fromJS({ isFlashing: false, flashResults: {}, flashState: { - flashing: 0, - verifying: 0, - successful: 0, + active: 0, failed: 0, percentage: 0, speed: null, averageSpeed: null, - totalSpeed: null, }, lastAverageFlashingSpeed: null, }); @@ -234,17 +231,7 @@ function storeReducer( verifyNoNilFields(action.data, flashStateNoNilFields, 'flash'); - if ( - !_.every( - _.pick(action.data, [ - 'flashing', - 'verifying', - 'successful', - 'failed', - ]), - _.isFinite, - ) - ) { + if (!_.every(_.pick(action.data, ['active', 'failed']), _.isFinite)) { throw errors.createError({ title: 'State quantity field(s) not finite number', }); @@ -266,7 +253,7 @@ function storeReducer( } 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); } return ret; diff --git a/lib/gui/app/modules/image-writer.ts b/lib/gui/app/modules/image-writer.ts index a78f7d8e..3b7b2690 100644 --- a/lib/gui/app/modules/image-writer.ts +++ b/lib/gui/app/modules/image-writer.ts @@ -168,7 +168,6 @@ export function performWrite( flashInstanceUuid: flashState.getFlashUuid(), unmountOnSuccess: settings.get('unmountOnSuccess'), validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), - trim: settings.get('trim'), }; ipc.server.on('fail', ({ error }: { error: Error & { code: string } }) => { @@ -196,8 +195,9 @@ export function performWrite( source, SourceType: source.SourceType.name, validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), - trim: settings.get('trim'), + autoBlockmapping: settings.get('autoBlockmapping'), unmountOnSuccess: settings.get('unmountOnSuccess'), + decompressFirst: settings.get('decompressFirst'), }); }); @@ -273,7 +273,6 @@ export async function flash( flashInstanceUuid: flashState.getFlashUuid(), unmountOnSuccess: settings.get('unmountOnSuccess'), validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), - trim: settings.get('trim'), applicationSessionUuid: store.getState().toJS().applicationSessionUuid, flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, }; @@ -318,6 +317,8 @@ export async function flash( errors: results.errors, devices: results.devices, status: 'finished', + bytesWritten: results.bytesWritten, + sourceMetadata: results.sourceMetadata, }; analytics.logEvent('Done', eventData); } @@ -336,7 +337,6 @@ export function cancel() { flashInstanceUuid: flashState.getFlashUuid(), unmountOnSuccess: settings.get('unmountOnSuccess'), validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), - trim: settings.get('trim'), applicationSessionUuid: store.getState().toJS().applicationSessionUuid, flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, status: 'cancel', diff --git a/lib/gui/app/modules/progress-status.ts b/lib/gui/app/modules/progress-status.ts index 789bc815..cdcb958b 100644 --- a/lib/gui/app/modules/progress-status.ts +++ b/lib/gui/app/modules/progress-status.ts @@ -15,16 +15,15 @@ */ import { bytesToClosestUnit } from '../../../shared/units'; -import * as settings from '../models/settings'; +// import * as settings from '../models/settings'; export interface FlashState { - flashing: number; - verifying: number; - successful: number; + active: number; failed: number; percentage?: number; speed: number; position: number; + type?: 'decompressing' | 'flashing' | 'verifying'; } /** @@ -36,45 +35,47 @@ export interface FlashState { * * @example * const status = progressStatus.fromFlashState({ - * flashing: 1, - * verifying: 0, - * successful: 0, + * type: 'flashing' + * active: 1, * failed: 0, * percentage: 55, - * speed: 2049 + * speed: 2049, * }) * * console.log(status) * // '55% Flashing' */ -export function fromFlashState(state: FlashState): string { - const isFlashing = Boolean(state.flashing); - const isValidating = !isFlashing && Boolean(state.verifying); - const shouldValidate = settings.get('validateWriteOnSuccess'); - const shouldUnmount = settings.get('unmountOnSuccess'); - - if (state.percentage === 0 && !state.speed) { - if (isValidating) { - return 'Validating...'; - } - +export function fromFlashState({ + type, + percentage, + position, +}: FlashState): string { + if (type === undefined) { return 'Starting...'; - } else if (state.percentage === 100) { - if ((isValidating || !shouldValidate) && shouldUnmount) { - return 'Unmounting...'; + } else if (type === 'decompressing') { + if (percentage == null) { + return 'Decompressing...'; + } else { + return `${percentage}% Decompressing`; } - - return 'Finishing...'; - } else if (isFlashing) { - if (state.percentage != null) { - return `${state.percentage}% Flashing`; + } else if (type === 'flashing') { + if (percentage != null) { + if (percentage < 100) { + return `${percentage}% Flashing`; + } else { + return 'Finishing...'; + } + } else { + return `${bytesToClosestUnit(position)} flashed`; + } + } else if (type === 'verifying') { + if (percentage == null) { + return 'Validating...'; + } else if (percentage < 100) { + return `${percentage}% Validating`; + } else { + return 'Finishing...'; } - return `${bytesToClosestUnit(state.position)} flashed`; - } else if (isValidating) { - return `${state.percentage}% Validating`; - } else if (!isFlashing && !isValidating) { - return 'Failed'; } - - throw new Error(`Invalid state: ${JSON.stringify(state)}`); + return 'Failed'; } diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index c9530235..209796a9 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -33,6 +33,7 @@ import { scanner as driveScanner } from '../../modules/drive-scanner'; import * as imageWriter from '../../modules/image-writer'; import * as progressStatus from '../../modules/progress-status'; import * as notification from '../../os/notification'; +import { StepSelection } from '../../styled-components'; const COMPLETED_PERCENTAGE = 100; const SPEED_PRECISION = 2; @@ -243,14 +244,16 @@ export const Flash = ({
- + + + {isFlashing && (