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 {getErrorMessage} from '../../base/errors'; 16import { 17 showAllowUSBDebugging, 18 showConnectionLostError, 19 showExtensionNotInstalled, 20 showFailedToPushBinary, 21 showIssueParsingTheTracedResponse, 22 showNoDeviceSelected, 23 showWebsocketConnectionIssue, 24 showWebUSBErrorV2, 25} from '../../frontend/error_dialog'; 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// The pattern for handling recording error can have the following nesting in 42// case of errors: 43// A. wrapRecordingError -> wraps a promise 44// B. onFailure -> has user defined logic and calls showRecordingModal 45// C. showRecordingModal -> shows UX for a given error; this is not called 46// directly by wrapRecordingError, because we want the caller (such as the 47// UI) to dictate the UX 48 49// This method takes a promise and a callback to be execute in case the promise 50// fails. It then awaits the promise and executes the callback in case of 51// failure. In the recording code it is used to wrap: 52// 1. Acessing the WebUSB API. 53// 2. Methods returning promises which can be rejected. For instance: 54// a) When the user clicks 'Add a new device' but then doesn't select a valid 55// device. 56// b) When the user starts a tracing session, but cancels it before they 57// authorize the session on the device. 58export async function wrapRecordingError<T>( 59 promise: Promise<T>, 60 onFailure: OnMessageCallback, 61): Promise<T | undefined> { 62 try { 63 return await promise; 64 } catch (e) { 65 // Sometimes the message is wrapped in an Error object, sometimes not, so 66 // we make sure we transform it into a string. 67 const errorMessage = getErrorMessage(e); 68 onFailure(errorMessage); 69 return undefined; 70 } 71} 72 73// Shows a modal for every known type of error which can arise during recording. 74// In this way, errors occuring at different levels of the recording process 75// can be handled in a central location. 76export function showRecordingModal(message: string): void { 77 if ( 78 [ 79 'Unable to claim interface.', 80 'The specified endpoint is not part of a claimed and selected ' + 81 'alternate interface.', 82 // thrown when calling the 'reset' method on a WebUSB device. 83 'Unable to reset the device.', 84 ].some((partOfMessage) => message.includes(partOfMessage)) 85 ) { 86 showWebUSBErrorV2(); 87 } else if ( 88 [ 89 'A transfer error has occurred.', 90 'The device was disconnected.', 91 'The transfer was cancelled.', 92 ].some((partOfMessage) => message.includes(partOfMessage)) || 93 isDeviceDisconnectedError(message) 94 ) { 95 showConnectionLostError(); 96 } else if (message === ALLOW_USB_DEBUGGING) { 97 showAllowUSBDebugging(); 98 } else if ( 99 isMessageComposedOf(message, [ 100 BINARY_PUSH_FAILURE, 101 BINARY_PUSH_UNKNOWN_RESPONSE, 102 ]) 103 ) { 104 showFailedToPushBinary(message.substring(message.indexOf(':') + 1)); 105 } else if (message === NO_DEVICE_SELECTED) { 106 showNoDeviceSelected(); 107 } else if (WEBSOCKET_UNABLE_TO_CONNECT === message) { 108 showWebsocketConnectionIssue(message); 109 } else if (message === EXTENSION_NOT_INSTALLED) { 110 showExtensionNotInstalled(); 111 } else if ( 112 isMessageComposedOf(message, [ 113 PARSING_UNKNWON_REQUEST_ID, 114 PARSING_UNABLE_TO_DECODE_METHOD, 115 PARSING_UNRECOGNIZED_PORT, 116 PARSING_UNRECOGNIZED_MESSAGE, 117 ]) 118 ) { 119 showIssueParsingTheTracedResponse(message); 120 } else { 121 throw new Error(`${message}`); 122 } 123} 124 125function isDeviceDisconnectedError(message: string) { 126 return ( 127 message.includes('Device with serial') && 128 message.includes('was disconnected.') 129 ); 130} 131 132function isMessageComposedOf(message: string, issues: string[]) { 133 for (const issue of issues) { 134 if (message.includes(issue)) { 135 return true; 136 } 137 } 138 return false; 139} 140 141// Exception thrown by the Recording logic. 142export class RecordingError extends Error {} 143