1// Copyright (C) 2022 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import { 16 showAllowUSBDebugging, 17 showConnectionLostError, 18 showExtensionNotInstalled, 19 showFailedToPushBinary, 20 showIssueParsingTheTracedResponse, 21 showNoDeviceSelected, 22 showWebsocketConnectionIssue, 23 showWebUSBErrorV2, 24} from '../../frontend/error_dialog'; 25import {getErrorMessage} from '../errors'; 26 27import {OnMessageCallback} from './recording_interfaces_v2'; 28import { 29 ALLOW_USB_DEBUGGING, 30 BINARY_PUSH_FAILURE, 31 BINARY_PUSH_UNKNOWN_RESPONSE, 32 EXTENSION_NOT_INSTALLED, 33 NO_DEVICE_SELECTED, 34 PARSING_UNABLE_TO_DECODE_METHOD, 35 PARSING_UNKNWON_REQUEST_ID, 36 PARSING_UNRECOGNIZED_MESSAGE, 37 PARSING_UNRECOGNIZED_PORT, 38 WEBSOCKET_UNABLE_TO_CONNECT, 39} from './recording_utils'; 40 41 42// The pattern for handling recording error can have the following nesting in 43// case of errors: 44// A. wrapRecordingError -> wraps a promise 45// B. onFailure -> has user defined logic and calls showRecordingModal 46// C. showRecordingModal -> shows UX for a given error; this is not called 47// directly by wrapRecordingError, because we want the caller (such as the 48// UI) to dictate the UX 49 50// This method takes a promise and a callback to be execute in case the promise 51// fails. It then awaits the promise and executes the callback in case of 52// failure. In the recording code it is used to wrap: 53// 1. Acessing the WebUSB API. 54// 2. Methods returning promises which can be rejected. For instance: 55// a) When the user clicks 'Add a new device' but then doesn't select a valid 56// device. 57// b) When the user starts a tracing session, but cancels it before they 58// authorize the session on the device. 59export async function wrapRecordingError<T>( 60 promise: Promise<T>, onFailure: OnMessageCallback): Promise<T|undefined> { 61 try { 62 return await promise; 63 } catch (e) { 64 // Sometimes the message is wrapped in an Error object, sometimes not, so 65 // we make sure we transform it into a string. 66 const errorMessage = getErrorMessage(e); 67 onFailure(errorMessage); 68 return undefined; 69 } 70} 71 72// Shows a modal for every known type of error which can arise during recording. 73// In this way, errors occuring at different levels of the recording process 74// can be handled in a central location. 75export function showRecordingModal(message: string): void { 76 if ([ 77 'Unable to claim interface.', 78 'The specified endpoint is not part of a claimed and selected ' + 79 'alternate interface.', 80 // thrown when calling the 'reset' method on a WebUSB device. 81 'Unable to reset the device.', 82 ].some((partOfMessage) => message.includes(partOfMessage))) { 83 showWebUSBErrorV2(); 84 } else if ( 85 [ 86 'A transfer error has occurred.', 87 'The device was disconnected.', 88 'The transfer was cancelled.', 89 ].some((partOfMessage) => message.includes(partOfMessage)) || 90 isDeviceDisconnectedError(message)) { 91 showConnectionLostError(); 92 } else if (message === ALLOW_USB_DEBUGGING) { 93 showAllowUSBDebugging(); 94 } else if (isMessageComposedOf( 95 message, 96 [BINARY_PUSH_FAILURE, BINARY_PUSH_UNKNOWN_RESPONSE])) { 97 showFailedToPushBinary(message.substring(message.indexOf(':') + 1)); 98 } else if (message === NO_DEVICE_SELECTED) { 99 showNoDeviceSelected(); 100 } else if (WEBSOCKET_UNABLE_TO_CONNECT === message) { 101 showWebsocketConnectionIssue(message); 102 } else if (message === EXTENSION_NOT_INSTALLED) { 103 showExtensionNotInstalled(); 104 } else if (isMessageComposedOf(message, [ 105 PARSING_UNKNWON_REQUEST_ID, 106 PARSING_UNABLE_TO_DECODE_METHOD, 107 PARSING_UNRECOGNIZED_PORT, 108 PARSING_UNRECOGNIZED_MESSAGE, 109 ])) { 110 showIssueParsingTheTracedResponse(message); 111 } else { 112 throw new Error(`${message}`); 113 } 114} 115 116function isDeviceDisconnectedError(message: string) { 117 return message.includes('Device with serial') && 118 message.includes('was disconnected.'); 119} 120 121function isMessageComposedOf(message: string, issues: string[]) { 122 for (const issue of issues) { 123 if (message.includes(issue)) { 124 return true; 125 } 126 } 127 return false; 128} 129 130// Exception thrown by the Recording logic. 131export class RecordingError extends Error {} 132