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