add proper error handling for firmware zip error

This commit is contained in:
David Lechner
2021-01-21 19:49:53 -06:00
parent a7ae66021b
commit ab0c850b80
5 changed files with 212 additions and 107 deletions
@@ -10,7 +10,7 @@ Object {
}
`;
exports[`flashFirmware user supplied firmware.zip 1`] = `
exports[`flashFirmware user supplied firmware.zip success 1`] = `
Object {
"options": Array [
"-mno-unicode",
+162 -101
View File
@@ -1,10 +1,16 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { FirmwareMetadata } from '@pybricks/firmware';
import {
FirmwareMetadata,
FirmwareReaderError,
FirmwareReaderErrorCode,
} from '@pybricks/firmware';
import JSZip from 'jszip';
import { AsyncSaga } from '../../test';
import {
FailToStartReasonType,
didFailToStart,
didFinish,
didProgress,
didStart,
@@ -170,131 +176,186 @@ describe('flashFirmware', () => {
await saga.end();
});
test('user supplied firmware.zip', async () => {
const metadata: FirmwareMetadata = {
'metadata-version': '1.0.0',
'device-id': HubType.MoveHub,
'checksum-type': 'sum',
'firmware-version': '1.2.3',
'max-firmware-size': 1024,
'mpy-abi-version': 5,
'mpy-cross-options': ['-mno-unicode'],
'user-mpy-offset': 100,
};
describe('user supplied firmware.zip', () => {
test('success', async () => {
const metadata: FirmwareMetadata = {
'metadata-version': '1.0.0',
'device-id': HubType.MoveHub,
'checksum-type': 'sum',
'firmware-version': '1.2.3',
'max-firmware-size': 1024,
'mpy-abi-version': 5,
'mpy-cross-options': ['-mno-unicode'],
'user-mpy-offset': 100,
};
const zip = new JSZip();
zip.file('firmware-base.bin', new Uint8Array(64));
zip.file('firmware.metadata.json', JSON.stringify(metadata));
zip.file('main.py', 'print("test")');
zip.file('ReadMe_OSS.txt', 'test');
const zip = new JSZip();
zip.file('firmware-base.bin', new Uint8Array(64));
zip.file('firmware.metadata.json', JSON.stringify(metadata));
zip.file('main.py', 'print("test")');
zip.file('ReadMe_OSS.txt', 'test');
const saga = new AsyncSaga(flashFirmware, { nextMessageId: createCountFunc() });
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
});
saga.setState({ settings: { flashCurrentProgram: false } as SettingsState });
saga.setState({
settings: { flashCurrentProgram: false } as SettingsState,
});
// saga is triggered by this action
// saga is triggered by this action
saga.put(flashFirmwareAction(await zip.generateAsync({ type: 'arraybuffer' })));
saga.put(
flashFirmwareAction(await zip.generateAsync({ type: 'arraybuffer' })),
);
// the first step is to compile main.py to .mpy
// the first step is to compile main.py to .mpy
let action = await saga.take();
expect(action).toMatchSnapshot();
let action = await saga.take();
expect(action).toMatchSnapshot();
const mpySize = 20;
const mpyBinaryData = new Uint8Array(mpySize);
saga.put(didCompile(mpyBinaryData));
const mpySize = 20;
const mpyBinaryData = new Uint8Array(mpySize);
saga.put(didCompile(mpyBinaryData));
// then connect to the hub bootloader
// then connect to the hub bootloader
action = await saga.take();
expect(action).toEqual(connect());
saga.put(didConnect());
// then find out what kind of hub it is
action = await saga.take();
expect(action).toEqual(infoRequest(0));
saga.put(didRequest(0));
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub));
// then start flashing the firmware
// should get didStart action just before starting to erase
action = await saga.take();
expect(action).toEqual(didStart());
// erase first
action = await saga.take();
expect(action).toEqual(eraseRequest(1));
saga.put(didRequest(1));
saga.put(eraseResponse(Result.OK));
// then write the new firmware
const totalFirmwareSize = metadata['user-mpy-offset'] + mpySize + 8;
action = await saga.take();
expect(action).toEqual(initRequest(2, totalFirmwareSize));
saga.put(didRequest(2));
saga.put(initResponse(Result.OK));
const dummyPayload = new ArrayBuffer(0);
let id = 2;
for (let count = 1, offset = 0; ; count++, offset += 14) {
action = await saga.take();
expect(action).toEqual(
programRequest(++id, 0x08005000 + offset, dummyPayload),
);
expect((action as BootloaderProgramRequestAction).payload.byteLength).toBe(
Math.min(14, totalFirmwareSize - offset),
);
expect(action).toEqual(connect());
saga.put(didConnect());
// then find out what kind of hub it is
action = await saga.take();
expect(action).toEqual(infoRequest(0));
saga.put(didRequest(0));
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub));
// then start flashing the firmware
// should get didStart action just before starting to erase
action = await saga.take();
expect(action).toEqual(didStart());
// erase first
action = await saga.take();
expect(action).toEqual(eraseRequest(1));
saga.put(didRequest(1));
saga.put(eraseResponse(Result.OK));
// then write the new firmware
const totalFirmwareSize = metadata['user-mpy-offset'] + mpySize + 8;
action = await saga.take();
expect(action).toEqual(initRequest(2, totalFirmwareSize));
saga.put(didRequest(2));
saga.put(initResponse(Result.OK));
const dummyPayload = new ArrayBuffer(0);
let id = 2;
for (let count = 1, offset = 0; ; count++, offset += 14) {
action = await saga.take();
expect(action).toEqual(
programRequest(++id, 0x08005000 + offset, dummyPayload),
);
expect(
(action as BootloaderProgramRequestAction).payload.byteLength,
).toBe(Math.min(14, totalFirmwareSize - offset));
saga.put(didRequest(id));
action = await saga.take();
expect(action).toEqual(didProgress(offset / totalFirmwareSize));
// Have to be careful that a checksum request is not sent after
// last payload is sent, otherwise the hub gets confused.
if (offset + 14 >= totalFirmwareSize) {
break;
}
if (count % 10 === 0) {
action = await saga.take();
expect(action).toEqual(checksumRequest(++id));
saga.put(didRequest(id));
saga.put(checksumResponse(0));
}
}
// hub indicates success
saga.put(programResponse(0, totalFirmwareSize));
action = await saga.take();
expect(action).toEqual(didProgress(1));
// and finally reboot the hub
action = await saga.take();
expect(action).toEqual(rebootRequest(++id));
saga.put(didRequest(id));
// then we are done
action = await saga.take();
expect(action).toEqual(didProgress(offset / totalFirmwareSize));
expect(action).toEqual(didFinish());
// Have to be careful that a checksum request is not sent after
// last payload is sent, otherwise the hub gets confused.
await saga.end();
});
if (offset + 14 >= totalFirmwareSize) {
break;
}
test('zip error', async () => {
const metadata: FirmwareMetadata = {
'metadata-version': '1.0.0',
'device-id': HubType.MoveHub,
'checksum-type': 'sum',
'firmware-version': '1.2.3',
'max-firmware-size': 1024,
'mpy-abi-version': 5,
'mpy-cross-options': ['-mno-unicode'],
'user-mpy-offset': 100,
};
if (count % 10 === 0) {
action = await saga.take();
expect(action).toEqual(checksumRequest(++id));
const zip = new JSZip();
// no firmware-base.bin - triggers zip error
zip.file('firmware.metadata.json', JSON.stringify(metadata));
zip.file('main.py', 'print("test")');
zip.file('ReadMe_OSS.txt', 'test');
saga.put(didRequest(id));
saga.put(checksumResponse(0));
}
}
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
});
// hub indicates success
saga.setState({
settings: { flashCurrentProgram: false } as SettingsState,
});
saga.put(programResponse(0, totalFirmwareSize));
// saga is triggered by this action
action = await saga.take();
expect(action).toEqual(didProgress(1));
saga.put(
flashFirmwareAction(await zip.generateAsync({ type: 'arraybuffer' })),
);
// and finally reboot the hub
// should get failure due to missing file
action = await saga.take();
expect(action).toEqual(rebootRequest(++id));
const action = await saga.take();
expect(action).toStrictEqual(
didFailToStart(
FailToStartReasonType.ZipError,
new FirmwareReaderError(
FirmwareReaderErrorCode.MissingFirmwareBaseBin,
),
),
);
saga.put(didRequest(id));
// then we are done
action = await saga.take();
expect(action).toEqual(didFinish());
await saga.end();
await saga.end();
});
});
test('user supplied main.py', async () => {
+26 -4
View File
@@ -1,15 +1,22 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2021 The Pybricks Authors
import { FirmwareMetadata, FirmwareReader, HubType } from '@pybricks/firmware';
import {
FirmwareMetadata,
FirmwareReader,
FirmwareReaderError,
HubType,
} from '@pybricks/firmware';
import cityHubZip from '@pybricks/firmware/build/cityhub.zip';
import moveHubZip from '@pybricks/firmware/build/movehub.zip';
import technicHubZip from '@pybricks/firmware/build/technichub.zip';
import { Ace } from 'ace-builds';
import {
Effect,
StrictEffect,
all,
call,
cancel,
delay,
getContext,
put,
@@ -20,8 +27,10 @@ import {
} from 'redux-saga/effects';
import { Action } from '../actions';
import {
FailToStartReasonType,
FlashFirmwareActionType,
FlashFirmwareFlashAction,
didFailToStart,
didFinish,
didProgress,
didStart,
@@ -65,6 +74,7 @@ import {
import * as notification from '../actions/notification';
import { MaxProgramFlashSize } from '../protocols/lwp3-bootloader';
import { RootState } from '../reducers';
import { Maybe, maybe } from '../utils';
import { fmod, sumComplement32 } from '../utils/math';
const firmwareZipMap = new Map<HubType, string>([
@@ -123,8 +133,20 @@ function* firmwareIterator(data: DataView, maxSize: number): Generator<number> {
function* loadFirmware(
data: ArrayBuffer,
program: string | undefined,
): Generator<unknown, { firmware: Uint8Array; deviceId: HubType }> {
const reader = (yield call(() => FirmwareReader.load(data))) as FirmwareReader;
): Generator<StrictEffect, { firmware: Uint8Array; deviceId: HubType }> {
const reader = (yield call(() =>
maybe(FirmwareReader.load(data)),
)) as Maybe<FirmwareReader>;
if (reader instanceof Error) {
if (reader instanceof FirmwareReaderError) {
yield put(didFailToStart(FailToStartReasonType.ZipError, reader));
} else {
yield put(didFailToStart(FailToStartReasonType.Unknown, reader));
}
yield cancel();
throw 'not reached';
}
const firmwareBase = (yield call(() => reader.readFirmwareBase())) as Uint8Array;
const metadata = (yield call(() => reader.readMetadata())) as FirmwareMetadata;
+12 -1
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { assert, hex } from '.';
import { assert, hex, maybe } from '.';
test('assert', () => {
const assertTrue = jest.fn(() => assert(true, 'should not throw'));
@@ -11,6 +11,17 @@ test('assert', () => {
expect(() => assert(false, 'should throw')).toThrow();
});
describe('maybe', () => {
test('resolved', async () => {
const result = await maybe(Promise.resolve('test'));
expect(result).toBe('test');
});
test('rejected', async () => {
const result = await maybe(Promise.reject(new Error('test')));
expect(result).toBeInstanceOf(Error);
});
});
test('hex', () => {
expect(hex(0, 2)).toBe('0x00');
expect(hex(1, 4)).toBe('0x0001');
+11
View File
@@ -13,6 +13,17 @@ export function assert(condition: boolean, message: string): void {
}
}
export type Maybe<T> = T | Error;
/** Wraps a promise in try/catch and returns the promise result or error. */
export async function maybe<T>(promise: Promise<T>): Promise<Maybe<T>> {
try {
return await promise;
} catch (err) {
return err;
}
}
/**
* Formats a number as hex (0x00...)
* @param n The number to format