• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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