chore(kboot): add more logging (#1008)

* chore(kboot): add more logging

* fix: add uncaughtException handler

* fix: wait to prevent race condition

* fix: don't close device after success left keyboard connection

* revert: remove extra delay

* revert: add back the waiting

* fix: always initialize new KBoot instance when try to configure I2C

* fix: increase the wait time between 2 IC2 reconnection

* fix: timing and usb reconnection

* fix: dont close kboot device

* feat: append the WithKboot to the firmware upgrade methods

* feat: revert back the blhost functionality
This commit is contained in:
Róbert Kiss
2019-09-13 10:17:44 +02:00
committed by László Monda
parent 8a7f30dbb1
commit b53751b408
16 changed files with 268 additions and 42 deletions

View File

@@ -12,15 +12,19 @@ export class KBoot {
}
open(): void {
logger('Open peripheral');
this.peripheral.open();
}
close(): void {
logger('Close peripheral');
this.peripheral.close();
}
// ================= Read properties ==================
async getProperty(property: Properties, memoryId = MemoryIds.Internal): Promise<CommandResponse> {
logger('Start read memory %o', { property, memoryId });
const command: CommandOption = {
command: Commands.GetProperty,
params: [
@@ -32,14 +36,17 @@ export class KBoot {
const response = await this.peripheral.sendCommand(command);
if (response.tag !== ResponseTags.Property) {
logger('Response tag is not property response: %d', property);
throw new Error('Response tag is not property response');
}
if (response.code === ResponseCodes.UnknownProperty) {
logger('Unknown property %d', response.code);
throw new Error('Unknown property!');
}
if (response.code !== ResponseCodes.Success) {
logger('Unknown error %d', response.code);
throw new Error(`Unknown error. Error code:${response.code}`);
}
@@ -47,6 +54,8 @@ export class KBoot {
}
async getBootloaderVersion(): Promise<BootloaderVersion> {
logger('Start to read Bootloader Version');
const response = await this.getProperty(Properties.BootloaderVersion);
const version: BootloaderVersion = {
@@ -64,7 +73,9 @@ export class KBoot {
// ================= End read properties ==================
async flashSecurityDisable(key: number[]): Promise<void> {
logger('Start flash security disable %o', { key });
if (key.length !== 8) {
logger('Error: Flash security key must be 8 byte. %o', key);
throw new Error('Flash security key must be 8 byte');
}
@@ -76,15 +87,18 @@ export class KBoot {
const response = await this.peripheral.sendCommand(command);
if (response.tag !== ResponseTags.Generic) {
logger('Response tag is not generic response: %d', response.tag);
throw new Error('Response tag is not generic response');
}
if (response.code !== ResponseCodes.Success) {
logger('Can not disable flash security: %d', response.code);
throw new Error(`Can not disable flash security`);
}
}
async flashEraseRegion(startAddress: number, count: number): Promise<void> {
logger('Start flash erase region');
const command: CommandOption = {
command: Commands.FlashEraseRegion,
params: [
@@ -96,15 +110,18 @@ export class KBoot {
const response = await this.peripheral.sendCommand(command);
if (response.tag !== ResponseTags.Generic) {
logger('Response tag is not generic response: %d', response.tag);
throw new Error('Response tag is not generic response');
}
if (response.code !== ResponseCodes.Success) {
throw new Error(`Can not disable flash security`);
logger('Can not flash erase region: %d', response.code);
throw new Error(`Can not flash erase region`);
}
}
async flashEraseAllUnsecure(): Promise<void> {
logger('Start flash erase all unsecure');
const command: CommandOption = {
command: Commands.FlashEraseAllUnsecure,
params: []
@@ -113,19 +130,23 @@ export class KBoot {
const response = await this.peripheral.sendCommand(command);
if (response.tag !== ResponseTags.Generic) {
logger('Response tag is not generic response: %d', response.tag);
throw new Error('Response tag is not generic response');
}
if (response.code !== ResponseCodes.Success) {
throw new Error(`Can not disable flash security`);
logger('Can not flash erase all unsecure: %d', response.code);
throw new Error(`Can not flash erase all unsecure`);
}
}
async readMemory(startAddress: number, count: number): Promise<any> {
logger('Start read memory %o', { startAddress, count });
return this.peripheral.readMemory(startAddress, count);
}
async writeMemory(options: DataOption): Promise<void> {
logger('Start write memory %o', { options });
return this.peripheral.writeMemory(options);
}
@@ -133,6 +154,7 @@ export class KBoot {
* Reset the bootloader
*/
async reset(): Promise<void> {
logger('Start reset the bootloader');
const command: CommandOption = {
command: Commands.Reset,
params: []
@@ -154,10 +176,12 @@ export class KBoot {
}
if (response.tag !== ResponseTags.Generic) {
logger('Response tag is not generic response: %d', response.tag);
throw new Error('Response tag is not generic response');
}
if (response.code !== ResponseCodes.Success) {
logger('Unknown error %d', response.code);
throw new Error(`Unknown error. Error code:${response.code}`);
}
}
@@ -168,8 +192,9 @@ export class KBoot {
* @param [speed=64] - Speed of the I2C
*/
async configureI2c(address: number, speed = 64): Promise<void> {
logger('Start configure I2C', { address, speed });
if (address > 127) {
logger('Only 7-bit i2c address is supported');
throw new Error('Only 7-bit i2c address is supported');
}
@@ -184,10 +209,12 @@ export class KBoot {
const response = await this.peripheral.sendCommand(command);
if (response.tag !== ResponseTags.Generic) {
logger('Response tag is not generic response: %d', response.tag);
throw new Error('Response tag is not generic response');
}
if (response.code !== ResponseCodes.Success) {
logger('Unknown error %d', response.code);
throw new Error(`Unknown error. Error code:${response.code}`);
}
}

View File

@@ -55,6 +55,8 @@ export class UsbPeripheral implements Peripheral {
close(): void {
if (this._device) {
this._device.close();
this._device.removeAllListeners('data');
this._device.removeAllListeners('error');
this._device = undefined;
}
}
@@ -222,6 +224,7 @@ export class UsbPeripheral implements Peripheral {
}
private _readFromBuffer(bufferName: string, byte: number, timeout: number): Promise<Buffer> {
logger('start read from buffer %o', { bufferName, byte, timeout });
return new Promise<Buffer>(async (resolve, reject) => {
const startTime = new Date();
while (startTime.getTime() + timeout > new Date().getTime()) {
@@ -266,6 +269,7 @@ export class UsbPeripheral implements Peripheral {
}
private async _getNextCommandResponse(): Promise<CommandResponse> {
logger('Start read next command response');
const response = await this._readFromCommandStream();
const commandResponse = decodeCommandResponse(response);
logger('next command response: %o', commandResponse);

View File

@@ -29,6 +29,7 @@
"scripts": {
"start": "cross-env DEBUG=kboot* electron ./dist/electron-main.js",
"electron:spe": "electron ./dist/electron-main.js --spe",
"electron:kboot": "cross-env DEBUG=kboot* electron ./dist/electron-main.js --useKboot",
"build": "webpack && npm run install:build-deps && npm run build:usb && npm run download-firmware && npm run copy-to-tmp-folder",
"build:usb": "electron-rebuild -w node-hid -p -m ./dist",
"lint": "tslint --project tsconfig.json",

View File

@@ -11,6 +11,7 @@ import * as commandLineArgs from 'command-line-args';
import { UhkHidDevice, UhkOperations } from 'uhk-usb';
// import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
import { CommandLineArgs, LogRegExps } from 'uhk-common';
import { UhkBlhost } from 'uhk-usb';
import { DeviceService } from './services/device.service';
import { logger } from './services/logger.service';
import { AppUpdateService } from './services/app-update.service';
@@ -22,7 +23,8 @@ import { loadWindowState, saveWindowState } from './util/window';
const optionDefinitions = [
{name: 'addons', type: Boolean},
{name: 'spe', type: Boolean} // simulate privilege escalation error
{name: 'spe', type: Boolean}, // simulate privilege escalation error
{name: 'useKboot', type: Boolean} // If it is true use kboot package instead of blhost for firmware upgrade
];
const options: CommandLineArgs = commandLineArgs(optionDefinitions);
@@ -36,6 +38,7 @@ let win: Electron.BrowserWindow;
autoUpdater.logger = logger;
let deviceService: DeviceService;
let uhkBlhost: UhkBlhost;
let uhkHidDeviceService: UhkHidDevice;
let uhkOperations: UhkOperations;
let appUpdateService: AppUpdateService;
@@ -100,8 +103,9 @@ function createWindow() {
setMenu(win);
uhkHidDeviceService = new UhkHidDevice(logger, options, packagesDir);
uhkOperations = new UhkOperations(logger, uhkHidDeviceService, packagesDir);
deviceService = new DeviceService(logger, win, uhkHidDeviceService, uhkOperations, packagesDir);
uhkBlhost = new UhkBlhost(logger, packagesDir);
uhkOperations = new UhkOperations(logger, uhkBlhost, uhkHidDeviceService, packagesDir);
deviceService = new DeviceService(logger, win, uhkHidDeviceService, uhkOperations, packagesDir, options);
appUpdateService = new AppUpdateService(logger, win, app);
appService = new AppService(logger, win, deviceService, options, uhkHidDeviceService);
sudoService = new SudoService(logger, options);

View File

@@ -1,6 +1,7 @@
import { ipcMain } from 'electron';
import { isEqual } from 'lodash';
import {
CommandLineArgs,
ConfigurationReply,
DeviceConnectionState,
FirmwareUpgradeIpcResponse,
@@ -41,7 +42,8 @@ export class DeviceService {
private win: Electron.BrowserWindow,
private device: UhkHidDevice,
private operations: UhkOperations,
private rootDir: string) {
private rootDir: string,
private options: CommandLineArgs) {
this.startPollUhkDevice();
this.uhkDevicePoller()
.catch(error => {
@@ -182,15 +184,25 @@ export class DeviceService {
const packageJson = await getPackageJsonFromPathAsync(firmwarePathData.packageJsonPath);
this.logService.debug('New firmware version:', packageJson.firmwareVersion);
await this.operations.updateRightFirmware(firmwarePathData.rightFirmwarePath);
await this.operations.updateLeftModule(firmwarePathData.leftFirmwarePath);
if (this.options.useKboot) {
await this.operations.updateRightFirmwareWithKboot(firmwarePathData.rightFirmwarePath);
await this.operations.updateLeftModuleWithKboot(firmwarePathData.leftFirmwarePath);
} else {
await this.operations.updateRightFirmwareWithBlhost(firmwarePathData.rightFirmwarePath);
await this.operations.updateLeftModuleWithBlhost(firmwarePathData.leftFirmwarePath);
}
} else {
const packageJsonPath = path.join(this.rootDir, 'packages/firmware/package.json');
const packageJson = await getPackageJsonFromPathAsync(packageJsonPath);
this.logService.debug('New firmware version:', packageJson.firmwareVersion);
await this.operations.updateRightFirmware();
await this.operations.updateLeftModule();
if (this.options.useKboot) {
await this.operations.updateRightFirmwareWithKboot();
await this.operations.updateLeftModuleWithKboot();
} else {
await this.operations.updateRightFirmwareWithBlhost();
await this.operations.updateLeftModuleWithBlhost();
}
}
response.success = true;
@@ -220,7 +232,11 @@ export class DeviceService {
try {
await this.stopPollUhkDevice();
await this.operations.updateRightFirmware();
if (this.options.useKboot) {
await this.operations.updateRightFirmwareWithKboot();
} else {
await this.operations.updateRightFirmwareWithBlhost();
}
response.modules = await this.getHardwareModules(false);
response.success = true;
@@ -270,19 +286,21 @@ export class DeviceService {
while (true) {
if (this._pollerAllowed) {
this._uhkDevicePolling = true;
try {
const state = await this.device.getDeviceConnectionStateAsync();
if (!isEqual(state, savedState)) {
savedState = state;
this.win.webContents.send(IpcEvents.device.deviceConnectionStateChanged, state);
this.logService.info('[DeviceService] Device connection state changed to:', state);
const state = await this.device.getDeviceConnectionStateAsync();
if (!isEqual(state, savedState)) {
savedState = state;
this.win.webContents.send(IpcEvents.device.deviceConnectionStateChanged, state);
this.logService.info('[DeviceService] Device connection state changed to:', state);
}
} catch (err) {
this.logService.error('[DeviceService] Device connection state query error', err);
}
this._uhkDevicePolling = false;
}
this._uhkDevicePolling = false;
await snooze(250);
}
}

View File

@@ -7,4 +7,8 @@ export interface CommandLineArgs {
* simulate privilege escalation error
*/
spe?: boolean;
/**
* If it is true use kboot package instead of blhost for firmware upgrade
*/
useKboot?: boolean;
}

View File

@@ -71,9 +71,9 @@ export enum EnumerationNameToProductId {
}
export enum ModuleSlotToI2cAddress {
leftHalf = 0x10,
leftModule = 0x20,
rightModule = 0x30
leftHalf = '0x10',
leftModule = '0x20',
rightModule = '0x30'
}
export enum ModuleSlotToId {

View File

@@ -1,4 +1,5 @@
export * from './constants';
export * from './uhk-blhost';
export * from './uhk-hid-device';
export * from './uhk-operations';
export * from './util';

View File

@@ -0,0 +1,89 @@
import * as path from 'path';
import { spawn } from 'child_process';
import { LogService } from 'uhk-common';
import { retry } from './util';
export class UhkBlhost {
private blhostPath: string;
constructor(private logService: LogService,
private rootDir: string) {
}
public async runBlhostCommand(params: Array<string>): Promise<void> {
const self = this;
return new Promise<void>((resolve, reject) => {
const blhostPath = this.getBlhostPath();
self.logService.debug(`[blhost] RUN: ${blhostPath} ${params.join(' ')}`);
const childProcess = spawn(`"${blhostPath}"`, params, {shell: true});
let finished = false;
childProcess.stdout.on('data', data => {
self.logService.debug(`[blhost] STDOUT: ${data}`);
});
childProcess.stderr.on('data', data => {
self.logService.error(`[blhost] STDERR: ${data}`);
});
childProcess.on('close', code => {
self.logService.debug(`[blhost] CLOSE_CODE: ${code}`);
finish(code);
});
childProcess.on('exit', code => {
self.logService.debug(`[blhost] EXIT_CODE: ${code}`);
finish(code);
});
childProcess.on('error', err => {
self.logService.debug(`[blhost] ERROR: ${err}`);
});
function finish(code) {
if (finished) {
return;
}
finished = true;
self.logService.debug(`[blhost] FINISHED: ${code}`);
if (code !== 0) {
return reject(new Error(`blhost error code:${code}`));
}
resolve();
}
});
}
public async runBlhostCommandRetry(params: Array<string>, maxTry = 100): Promise<void> {
return await retry(async () => await this.runBlhostCommand(params), maxTry, this.logService);
}
private getBlhostPath(): string {
if (this.blhostPath) {
return this.blhostPath;
}
let blhostPath;
switch (process.platform) {
case 'linux':
blhostPath = 'linux/x86_64/blhost';
break;
case 'darwin':
blhostPath = 'mac/blhost';
break;
case 'win32':
blhostPath = 'win/blhost.exe';
break;
default:
throw new Error(`Could not find blhost path. Unknown platform:${process.platform}`);
}
this.blhostPath = path.join(this.rootDir, `packages/blhost/${blhostPath}`);
return this.blhostPath;
}
}

View File

@@ -208,7 +208,7 @@ export class UhkHidDevice {
while (new Date().getTime() - startTime.getTime() < 20000) {
const devs = devices();
this.logService.silly('[UhkHidDevice] reenumeration devices', devs);
this.logService.debug('[UhkHidDevice] reenumeration devices', devs);
const inBootloaderMode = devs.some((x: Device) =>
x.vendorId === Constants.VENDOR_ID &&
@@ -219,7 +219,7 @@ export class UhkHidDevice {
return;
}
this.logService.silly(`[UhkHidDevice] Could not find reenumerated device: ${reenumMode}. Waiting...`);
this.logService.debug(`[UhkHidDevice] Could not find reenumerated device: ${reenumMode}. Waiting...`);
await snooze(100);
if (!jumped) {
@@ -232,7 +232,7 @@ export class UhkHidDevice {
device.close();
jumped = true;
} else {
this.logService.silly(`[UhkHidDevice] USB[T]: Enumerate device is not ready yet}`);
this.logService.debug(`[UhkHidDevice] USB[T]: Enumerate device is not ready yet}`);
}
}
}
@@ -249,7 +249,7 @@ export class UhkHidDevice {
if (command === KbootCommands.idle) {
transfer = Buffer.from([UsbCommand.SendKbootCommandToModule, command]);
} else {
transfer = Buffer.from([UsbCommand.SendKbootCommandToModule, command, module]);
transfer = Buffer.from([UsbCommand.SendKbootCommandToModule, command, Number.parseInt(module, 16)]);
}
await retry(async () => await this.write(transfer), maxTry, this.logService);
}

View File

@@ -13,6 +13,7 @@ import {
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import { UhkBlhost } from './uhk-blhost';
import { UhkHidDevice } from './uhk-hid-device';
import { readBootloaderFirmwareFromHexFileAsync, snooze, waitForDevice } from './util';
import { ConfigBufferId, convertBufferToIntArray, DevicePropertyIds, getTransferBuffers, UsbCommand } from '../index';
@@ -20,11 +21,67 @@ import { LoadConfigurationsResult } from './models/load-configurations-result';
export class UhkOperations {
constructor(private logService: LogService,
private blhost: UhkBlhost,
private device: UhkHidDevice,
private rootDir: string) {
}
public async updateRightFirmware(firmwarePath = this.getFirmwarePath()) {
public async updateRightFirmwareWithBlhost(firmwarePath = this.getFirmwarePath()) {
this.logService.debug(`[UhkOperations] Operating system: ${os.type()} ${os.release()} ${os.arch()}`);
this.logService.debug('[UhkOperations] Start flashing right firmware');
const prefix = [`--usb 0x1d50,0x${EnumerationNameToProductId.bootloader.toString(16)}`];
await this.device.reenumerate(EnumerationModes.Bootloader);
this.device.close();
await this.blhost.runBlhostCommand([...prefix, 'flash-security-disable', '0403020108070605']);
await this.blhost.runBlhostCommand([...prefix, 'flash-erase-region', '0xc000', '475136']);
await this.blhost.runBlhostCommand([...prefix, 'flash-image', `"${firmwarePath}"`]);
await this.blhost.runBlhostCommand([...prefix, 'reset']);
this.logService.debug('[UhkOperations] Right firmware successfully flashed');
}
public async updateLeftModuleWithBlhost(firmwarePath = this.getLeftModuleFirmwarePath()) {
this.logService.debug('[UhkOperations] Start flashing left module firmware');
const prefix = [`--usb 0x1d50,0x${EnumerationNameToProductId.buspal.toString(16)}`];
const buspalPrefix = [...prefix, `--buspal i2c,${ModuleSlotToI2cAddress.leftHalf}`];
await this.device.reenumerate(EnumerationModes.NormalKeyboard);
this.device.close();
await snooze(1000);
await this.device.sendKbootCommandToModule(ModuleSlotToI2cAddress.leftHalf, KbootCommands.ping, 100);
await snooze(1000);
await this.device.jumpToBootloaderModule(ModuleSlotToId.leftHalf);
this.device.close();
const leftModuleBricked = await this.waitForKbootIdle();
if (!leftModuleBricked) {
const msg = '[UhkOperations] Couldn\'t connect to the left keyboard half.';
this.logService.error(msg);
throw new Error(msg);
}
await this.device.reenumerate(EnumerationModes.Buspal);
this.device.close();
await this.blhost.runBlhostCommandRetry([...buspalPrefix, 'get-property', '1']);
await this.blhost.runBlhostCommand([...buspalPrefix, 'flash-erase-all-unsecure']);
await this.blhost.runBlhostCommand([...buspalPrefix, 'write-memory', '0x0', `"${firmwarePath}"`]);
await this.blhost.runBlhostCommand([...prefix, 'reset']);
await snooze(1000);
await this.device.reenumerate(EnumerationModes.NormalKeyboard);
this.device.close();
await snooze(1000);
await this.device.sendKbootCommandToModule(ModuleSlotToI2cAddress.leftHalf, KbootCommands.reset, 100);
this.device.close();
await snooze(1000);
await this.device.sendKbootCommandToModule(ModuleSlotToI2cAddress.leftHalf, KbootCommands.idle);
this.device.close();
this.logService.debug('[UhkOperations] Left firmware successfully flashed');
this.logService.debug('[UhkOperations] Both left and right firmwares successfully flashed');
}
public async updateRightFirmwareWithKboot(firmwarePath = this.getFirmwarePath()) {
this.logService.debug(`[UhkOperations] Operating system: ${os.type()} ${os.release()} ${os.arch()}`);
this.logService.debug('[UhkOperations] Start flashing right firmware');
@@ -58,9 +115,10 @@ export class UhkOperations {
this.logService.debug('[UhkOperations] Right firmware successfully flashed');
}
public async updateLeftModule(firmwarePath = this.getLeftModuleFirmwarePath()) {
public async updateLeftModuleWithKboot(firmwarePath = this.getLeftModuleFirmwarePath()) {
this.logService.debug('[UhkOperations] Start flashing left module firmware');
const i2cAddressOfLeftModule = Number.parseInt(ModuleSlotToI2cAddress.leftHalf, 16);
await this.device.reenumerate(EnumerationModes.NormalKeyboard);
this.device.close();
await snooze(1000);
@@ -86,28 +144,31 @@ export class UhkOperations {
while (true) {
try {
this.logService.debug('[UhkOperations] Try to connect to the LEFT keyboard');
await kboot.configureI2c(ModuleSlotToI2cAddress.leftHalf);
await kboot.configureI2c(i2cAddressOfLeftModule);
await kboot.getProperty(Properties.BootloaderVersion);
break;
} catch {
if (tryCount > 100) {
throw new Error('Can not connect to the LEFT keyboard');
}
} finally {
kboot.close();
await snooze(2000);
}
await snooze(100);
tryCount++;
}
// https://github.com/node-hid/node-hid/issues/230
this.logService.debug('[UhkOperations] Wait 1 sec to prevent node-hid race condition');
await snooze(1000);
this.logService.debug('[UhkOperations] Flash erase all on LEFT keyboard');
await kboot.configureI2c(ModuleSlotToI2cAddress.leftHalf);
await kboot.configureI2c(i2cAddressOfLeftModule);
await kboot.flashEraseAllUnsecure();
this.logService.debug('[UhkOperations] Read LEFT firmware from file');
const configData = fs.readFileSync(firmwarePath);
this.logService.debug('[UhkOperations] Write memory');
await kboot.configureI2c(ModuleSlotToI2cAddress.leftHalf);
await kboot.configureI2c(i2cAddressOfLeftModule);
await kboot.writeMemory({ startAddress: 0, data: configData });
this.logService.debug('[UhkOperations] Reset LEFT keyboard');

View File

@@ -7,9 +7,18 @@ const rootDir = path.join(__dirname, '../../tmp');
const uhkHidDevice = new UhkHidDevice(logService, {}, rootDir);
const uhkOperations = new UhkOperations(logService, uhkHidDevice, rootDir);
process.on('uncaughtException', error => {
console.error('uncaughtException', error);
process.exit(1);
});
process.on('unhandledRejection', (reason: any, promise: Promise<any>): void => {
console.error('unhandledRejection', { reason, promise });
});
uhkOperations
.updateRightFirmware()
.then(() => uhkOperations.updateLeftModule())
.updateRightFirmwareWithKboot()
.then(() => uhkOperations.updateLeftModuleWithKboot())
.then(() => console.log('Firmware upgrade finished'))
.catch(error => {
console.error(error);