UNPKG

@ledgerhq/live-common

Version:
119 lines (111 loc) 3.91 kB
import { Observable, throwError, timer } from "rxjs"; import { throttleTime, filter, map, catchError, retry, switchMap } from "rxjs/operators"; import { LockedDeviceError, ManagerAppDepInstallRequired, ManagerDeviceLockedError, UnresponsiveDeviceError, } from "@ledgerhq/errors"; import Transport from "@ledgerhq/hw-transport"; import type { ApplicationVersion, App } from "@ledgerhq/types-live"; import ManagerAPI from "../manager/api"; import { getDependencies } from "../apps/polyfill"; import { LocalTracer } from "@ledgerhq/logs"; import { LOG_TYPE } from "."; import { quitApp } from "../deviceSDK/commands/quitApp"; const APP_INSTALL_RETRY_DELAY = 500; const APP_INSTALL_RETRY_LIMIT = 5; /** * Command to install a given app to a device. * * On error (except on locked device errors), a retry mechanism is set. * * @param transport * @param targetId Device firmware target id * @param app Info of the app to install * @param others Extended params: * - retryLimit: number of time this command is retried when any error occurs. Default to 5. * - retryDelayMs: delay in ms before retrying on an error. Default to 500ms. * @returns An Observable emitting installation progress * - progress: float number from 0 to 1 representing the installation progress */ export default function installApp( transport: Transport, targetId: string | number, app: ApplicationVersion | App, { retryLimit = APP_INSTALL_RETRY_LIMIT, retryDelayMs = APP_INSTALL_RETRY_DELAY, }: { retryLimit?: number; retryDelayMs?: number } = {}, ): Observable<{ progress: number; }> { const tracer = new LocalTracer(LOG_TYPE, { ...transport.getTraceContext(), function: "installApp", }); tracer.trace("Install app", { targetId, appName: app?.name, appVersion: app?.version, retryLimit, retryDelayMs, }); // Run quitApp just before ManagerAPI.install return quitApp(transport).pipe( switchMap(() => ManagerAPI.install(transport, "install-app", { targetId, perso: app.perso, deleteKey: app.delete_key, firmware: app.firmware, firmwareKey: app.firmware_key, hash: app.hash, }).pipe( retry({ count: retryLimit, delay: (error: unknown, retryCount: number) => { // Not retrying on locked device errors if ( error instanceof LockedDeviceError || error instanceof ManagerDeviceLockedError || error instanceof UnresponsiveDeviceError ) { tracer.trace(`Not retrying on error: ${error}`, { error, }); return throwError(() => error); } tracer.trace(`Retrying (${retryCount}/${retryLimit}) on error: ${error}`, { error, retryLimit, retryDelayMs, }); return timer(retryDelayMs); }, }), filter((e: any) => e.type === "bulk-progress"), // only bulk progress interests the UI throttleTime(100), // throttle to only emit 10 event/s max, to not spam the UI map((e: any) => ({ progress: e.progress, })), // extract a stream of progress percentage catchError((e: Error) => { tracer.trace(`Error: ${e}`, { error: e }); if (!e || !e.message) return throwError(() => e); const status = e.message.slice(e.message.length - 4); if (status === "6a83" || status === "6811") { const dependencies = getDependencies(app.name); return throwError( () => new ManagerAppDepInstallRequired("", { appName: app.name, dependency: dependencies.join(", "), }), ); } return throwError(() => e); }), ), ), ); }