From 7e7ca9524e6486fdccc59fc4964454be8d925e30 Mon Sep 17 00:00:00 2001 From: Lorenzo Alberto Maria Ambrosi Date: Wed, 8 Jul 2020 16:07:15 +0200 Subject: [PATCH] Add skip function to validation Change-type: patch Changelog-entry: Add skip function to validation Signed-off-by: Lorenzo Alberto Maria Ambrosi --- lib/gui/app/components/finish/finish.tsx | 30 +++++++- .../flash-results/flash-results.tsx | 73 +++++++++++-------- .../progress-button/progress-button.tsx | 41 +++++++---- lib/gui/app/models/flash-state.ts | 23 ++++-- lib/gui/app/models/leds.ts | 5 +- lib/gui/app/models/store.ts | 14 ++-- lib/gui/app/modules/image-writer.ts | 23 ++++-- lib/gui/app/pages/main/Flash.tsx | 10 +-- lib/gui/modules/child-writer.ts | 32 ++++++-- 9 files changed, 170 insertions(+), 81 deletions(-) diff --git a/lib/gui/app/components/finish/finish.tsx b/lib/gui/app/components/finish/finish.tsx index 373c9cc2..88c4cf2d 100644 --- a/lib/gui/app/components/finish/finish.tsx +++ b/lib/gui/app/components/finish/finish.tsx @@ -23,7 +23,7 @@ import * as selectionState from '../../models/selection-state'; import { Actions, store } from '../../models/store'; import * as analytics from '../../modules/analytics'; import { FlashAnother } from '../flash-another/flash-another'; -import { FlashResults } from '../flash-results/flash-results'; +import { FlashResults, FlashError } from '../flash-results/flash-results'; import { SafeWebview } from '../safe-webview/safe-webview'; function restart(goToMain: () => void) { @@ -41,8 +41,31 @@ function restart(goToMain: () => void) { function FinishPage({ goToMain }: { goToMain: () => void }) { const [webviewShowing, setWebviewShowing] = React.useState(false); - const errors = flashState.getFlashResults().results?.errors; - const results = flashState.getFlashResults().results || {}; + const flashResults = flashState.getFlashResults(); + const errors: FlashError[] = ( + store.getState().toJS().failedDeviceErrors || [] + ).map(([, error]: [string, FlashError]) => ({ + ...error, + })); + const { + averageSpeed, + blockmappedSize, + bytesWritten, + failed, + size, + } = flashState.getFlashState(); + const { + skip, + results = { + bytesWritten, + sourceMetadata: { + size, + blockmappedSize, + }, + averageFlashingSpeed: averageSpeed, + devices: { failed, successful: 0 }, + }, + } = flashState.getFlashResults(); return ( void }) { diff --git a/lib/gui/app/components/flash-results/flash-results.tsx b/lib/gui/app/components/flash-results/flash-results.tsx index 764cac53..0bcc1ed0 100644 --- a/lib/gui/app/components/flash-results/flash-results.tsx +++ b/lib/gui/app/components/flash-results/flash-results.tsx @@ -26,6 +26,9 @@ import { progress } from '../../../../shared/messages'; import { bytesToMegabytes } from '../../../../shared/units'; import FlashSvg from '../../../assets/flash.svg'; +import { getDrives } from '../../models/available-drives'; +import { resetState } from '../../models/flash-state'; +import * as selection from '../../models/selection-state'; import { middleEllipsis } from '../../utils/middle-ellipsis'; import { Modal } from '../../styled-components'; @@ -78,7 +81,7 @@ const DoneIcon = (props: { ); }; -interface FlashError extends Error { +export interface FlashError extends Error { description: string; device: string; code: string; @@ -112,10 +115,12 @@ export function FlashResults({ image = '', errors, results, + skip, ...props }: { image?: string; errors: FlashError[]; + skip: boolean; results: { bytesWritten: number; sourceMetadata: { @@ -128,7 +133,7 @@ export function FlashResults({ } & FlexProps) { const [showErrorsInfo, setShowErrorsInfo] = React.useState(false); const allFailed = results.devices.successful === 0; - const someFailed = results.devices.failed !== 0; + const someFailed = results.devices.failed !== 0 || errors.length !== 0; const effectiveSpeed = _.round( bytesToMegabytes( results.sourceMetadata.size / @@ -160,32 +165,31 @@ export function FlashResults({ {skip ? Validation has been skipped : null} - {Object.entries(results.devices).map(([type, quantity]) => { - const failedTargets = type === 'failed'; - return quantity ? ( - - - - {quantity} - - - {progress[type](quantity)} - - {failedTargets && ( - setShowErrorsInfo(true)}> - more info - - )} - - ) : null; - })} + {results.devices.successful !== 0 ? ( + + + + {results.devices.successful} + + + {progress.successful(results.devices.successful)} + + + ) : null} + {errors.length !== 0 ? ( + + + + {errors.length} + + + {progress.failed(errors.length)} + + setShowErrorsInfo(true)}> + more info + + + ) : null} {!allFailed && ( } - done={() => setShowErrorsInfo(false)} + action="Retry failed targets" + cancel={() => setShowErrorsInfo(false)} + done={() => { + setShowErrorsInfo(false); + resetState(); + getDrives() + .filter((drive) => + errors.some((error) => error.device === drive.device), + ) + .forEach((drive) => selection.selectDrive(drive.device)); + goToMain(); + }} > diff --git a/lib/gui/app/components/progress-button/progress-button.tsx b/lib/gui/app/components/progress-button/progress-button.tsx index c46f85ed..9e328eea 100644 --- a/lib/gui/app/components/progress-button/progress-button.tsx +++ b/lib/gui/app/components/progress-button/progress-button.tsx @@ -49,7 +49,7 @@ interface ProgressButtonProps { percentage: number; position: number; disabled: boolean; - cancel: () => void; + cancel: (type: string) => void; callback: () => void; warning?: boolean; } @@ -60,11 +60,14 @@ const colors = { verifying: '#1ac135', } as const; -const CancelButton = styled((props) => ( - -))` +const CancelButton = styled(({ type, onClick, ...props }) => { + const status = type === 'verifying' ? 'Skip' : 'Cancel'; + return ( + + ); +})` font-weight: 600; &&& { width: auto; @@ -75,10 +78,13 @@ const CancelButton = styled((props) => ( export class ProgressButton extends React.PureComponent { public render() { + const type = this.props.type; + const percentage = this.props.percentage; + const warning = this.props.warning; const { status, position } = fromFlashState({ - type: this.props.type, + type, + percentage, position: this.props.position, - percentage: this.props.percentage, }); if (this.props.active) { return ( @@ -96,21 +102,24 @@ export class ProgressButton extends React.PureComponent { > {status}  - {position} + {position} - + {type && ( + + )} - + ); } return ( devicePath, + ); const newLedsState = { step, sourceDrive: sourceDrivePath, availableDrives: availableDrivesPaths, selectedDrives: selectedDrivesPaths, - failedDrives: s.failedDevicePaths, + failedDrives: failedDevicePaths, }; if (!_.isEqual(newLedsState, ledsState)) { updateLeds(newLedsState); diff --git a/lib/gui/app/models/store.ts b/lib/gui/app/models/store.ts index 0a1ac58b..5484d104 100644 --- a/lib/gui/app/models/store.ts +++ b/lib/gui/app/models/store.ts @@ -62,7 +62,7 @@ export const DEFAULT_STATE = Immutable.fromJS({ }, isFlashing: false, devicePaths: [], - failedDevicePaths: [], + failedDeviceErrors: [], flashResults: {}, flashState: { active: 0, @@ -79,7 +79,7 @@ export const DEFAULT_STATE = Immutable.fromJS({ */ export enum Actions { SET_DEVICE_PATHS, - SET_FAILED_DEVICE_PATHS, + SET_FAILED_DEVICE_ERRORS, SET_AVAILABLE_TARGETS, SET_FLASH_STATE, RESET_FLASH_STATE, @@ -269,7 +269,7 @@ function storeReducer( .set('flashState', DEFAULT_STATE.get('flashState')) .set('flashResults', DEFAULT_STATE.get('flashResults')) .set('devicePaths', DEFAULT_STATE.get('devicePaths')) - .set('failedDevicePaths', DEFAULT_STATE.get('failedDevicePaths')) + .set('failedDeviceErrors', DEFAULT_STATE.get('failedDeviceErrors')) .set( 'lastAverageFlashingSpeed', DEFAULT_STATE.get('lastAverageFlashingSpeed'), @@ -295,6 +295,7 @@ function storeReducer( _.defaults(action.data, { cancelled: false, + skip: false, }); if (!_.isBoolean(action.data.cancelled)) { @@ -337,8 +338,7 @@ function storeReducer( return state .set('isFlashing', false) - .set('flashResults', Immutable.fromJS(action.data)) - .set('flashState', DEFAULT_STATE.get('flashState')); + .set('flashResults', Immutable.fromJS(action.data)); } case Actions.SELECT_TARGET: { @@ -509,8 +509,8 @@ function storeReducer( return state.set('devicePaths', action.data); } - case Actions.SET_FAILED_DEVICE_PATHS: { - return state.set('failedDevicePaths', action.data); + case Actions.SET_FAILED_DEVICE_ERRORS: { + return state.set('failedDeviceErrors', action.data); } default: { diff --git a/lib/gui/app/modules/image-writer.ts b/lib/gui/app/modules/image-writer.ts index 8091ede7..4abd207a 100644 --- a/lib/gui/app/modules/image-writer.ts +++ b/lib/gui/app/modules/image-writer.ts @@ -131,6 +131,7 @@ function writerEnv() { } interface FlashResults { + skip?: boolean; cancelled?: boolean; } @@ -140,6 +141,7 @@ async function performWrite( onProgress: sdk.multiWrite.OnProgressFunction, ): Promise<{ cancelled?: boolean }> { let cancelled = false; + let skip = false; ipc.serve(); const { unmountOnSuccess, @@ -171,7 +173,7 @@ async function performWrite( ipc.server.on('fail', ({ device, error }) => { if (device.devicePath) { - flashState.addFailedDevicePath(device.devicePath); + flashState.addFailedDeviceError({ device, error }); } handleErrorLogging(error, analyticsData); }); @@ -188,6 +190,11 @@ async function performWrite( cancelled = true; }); + ipc.server.on('skip', () => { + terminateServer(); + skip = true; + }); + ipc.server.on('state', onProgress); ipc.server.on('ready', (_data, socket) => { @@ -213,6 +220,7 @@ async function performWrite( environment: env, }); flashResults.cancelled = cancelled || results.cancelled; + flashResults.skip = skip; } catch (error) { // This happens when the child is killed using SIGKILL const SIGKILL_EXIT_CODE = 137; @@ -229,6 +237,7 @@ async function performWrite( // This likely means the child died halfway through if ( !flashResults.cancelled && + !flashResults.skip && !_.get(flashResults, ['results', 'bytesWritten']) ) { reject( @@ -286,8 +295,7 @@ export async function flash( } catch (error) { flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code }); windowProgress.clear(); - let { results } = flashState.getFlashResults(); - results = results || {}; + const { results = {} } = flashState.getFlashResults(); const eventData = { ...analyticsData, errors: results.errors, @@ -306,7 +314,7 @@ export async function flash( }; analytics.logEvent('Elevation cancelled', eventData); } else { - const { results } = flashState.getFlashResults(); + const { results = {} } = flashState.getFlashResults(); const eventData = { ...analyticsData, errors: results.errors, @@ -322,7 +330,8 @@ export async function flash( /** * @summary Cancel write operation */ -export async function cancel() { +export async function cancel(type: string) { + const status = type.toLowerCase(); const drives = selectionState.getSelectedDevices(); const analyticsData = { image: selectionState.getImagePath(), @@ -332,7 +341,7 @@ export async function cancel() { flashInstanceUuid: flashState.getFlashUuid(), unmountOnSuccess: await settings.get('unmountOnSuccess'), validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'), - status: 'cancel', + status, }; analytics.logEvent('Cancel', analyticsData); @@ -342,7 +351,7 @@ export async function cancel() { // @ts-ignore (no Server.sockets in @types/node-ipc) const [socket] = ipc.server.sockets; if (socket !== undefined) { - ipc.server.emit(socket, 'cancel'); + ipc.server.emit(socket, status); } } catch (error) { analytics.logException(error); diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index 57c4b4f3..2722db07 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -82,14 +82,12 @@ async function flashImageToDrive( try { await imageWriter.flash(image, drives); if (!flashState.wasLastFlashCancelled()) { - const flashResults: any = flashState.getFlashResults(); + const { + results = { devices: { successful: 0, failed: 0 } }, + } = flashState.getFlashResults(); notification.send( 'Flash complete!', - messages.info.flashComplete( - basename, - drives as any, - flashResults.results.devices, - ), + messages.info.flashComplete(basename, drives as any, results.devices), iconPath, ); goToSuccess(); diff --git a/lib/gui/modules/child-writer.ts b/lib/gui/modules/child-writer.ts index 4c135dac..46b148f2 100644 --- a/lib/gui/modules/child-writer.ts +++ b/lib/gui/modules/child-writer.ts @@ -71,14 +71,25 @@ async function handleError(error: Error) { terminate(GENERAL_ERROR); } -interface WriteResult { - bytesWritten: number; - devices: { +export interface FlashError extends Error { + description: string; + device: string; + code: string; +} + +export interface WriteResult { + bytesWritten?: number; + devices?: { failed: number; successful: number; }; - errors: Array; - sourceMetadata: sdk.sourceDestination.Metadata; + errors: FlashError[]; + sourceMetadata?: sdk.sourceDestination.Metadata; +} + +export interface FlashResults extends WriteResult { + skip?: boolean; + cancelled?: boolean; } /** @@ -136,7 +147,7 @@ async function writeAndValidate({ sourceMetadata, }; for (const [destination, error] of failures) { - const err = error as Error & { device: string; description: string }; + const err = error as FlashError; const drive = destination as sdk.sourceDestination.BlockDevice; err.device = drive.device; err.description = drive.description; @@ -208,8 +219,17 @@ ipc.connectTo(IPC_SERVER_ID, () => { terminate(exitCode); }; + const onSkip = async () => { + log('Skip validation'); + ipc.of[IPC_SERVER_ID].emit('skip'); + await delay(DISCONNECT_DELAY); + terminate(exitCode); + }; + ipc.of[IPC_SERVER_ID].on('cancel', onAbort); + ipc.of[IPC_SERVER_ID].on('skip', onSkip); + /** * @summary Failure handler (non-fatal errors) * @param {SourceDestination} destination - destination