blob: ca03929189b75b8a4ff429acde2968982ca1d7ae [file] [log] [blame]
// Copyright (C) 2022 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {
showAllowUSBDebugging,
showConnectionLostError,
showExtensionNotInstalled,
showFailedToPushBinary,
showIssueParsingTheTracedResponse,
showNoDeviceSelected,
showWebsocketConnectionIssue,
showWebUSBErrorV2,
} from '../../frontend/error_dialog';
import {getErrorMessage} from '../errors';
import {OnMessageCallback} from './recording_interfaces_v2';
import {
ALLOW_USB_DEBUGGING,
BINARY_PUSH_FAILURE,
BINARY_PUSH_UNKNOWN_RESPONSE,
EXTENSION_NOT_INSTALLED,
NO_DEVICE_SELECTED,
PARSING_UNABLE_TO_DECODE_METHOD,
PARSING_UNKNWON_REQUEST_ID,
PARSING_UNRECOGNIZED_MESSAGE,
PARSING_UNRECOGNIZED_PORT,
WEBSOCKET_UNABLE_TO_CONNECT,
} from './recording_utils';
// The pattern for handling recording error can have the following nesting in
// case of errors:
// A. wrapRecordingError -> wraps a promise
// B. onFailure -> has user defined logic and calls showRecordingModal
// C. showRecordingModal -> shows UX for a given error; this is not called
// directly by wrapRecordingError, because we want the caller (such as the
// UI) to dictate the UX
// This method takes a promise and a callback to be execute in case the promise
// fails. It then awaits the promise and executes the callback in case of
// failure. In the recording code it is used to wrap:
// 1. Acessing the WebUSB API.
// 2. Methods returning promises which can be rejected. For instance:
// a) When the user clicks 'Add a new device' but then doesn't select a valid
// device.
// b) When the user starts a tracing session, but cancels it before they
// authorize the session on the device.
export async function wrapRecordingError<T>(
promise: Promise<T>, onFailure: OnMessageCallback): Promise<T|undefined> {
try {
return await promise;
} catch (e) {
// Sometimes the message is wrapped in an Error object, sometimes not, so
// we make sure we transform it into a string.
const errorMessage = getErrorMessage(e);
onFailure(errorMessage);
return undefined;
}
}
// Shows a modal for every known type of error which can arise during recording.
// In this way, errors occuring at different levels of the recording process
// can be handled in a central location.
export function showRecordingModal(message: string): void {
if ([
'Unable to claim interface.',
'The specified endpoint is not part of a claimed and selected ' +
'alternate interface.',
// thrown when calling the 'reset' method on a WebUSB device.
'Unable to reset the device.',
].some((partOfMessage) => message.includes(partOfMessage))) {
showWebUSBErrorV2();
} else if (
[
'A transfer error has occurred.',
'The device was disconnected.',
'The transfer was cancelled.',
].some((partOfMessage) => message.includes(partOfMessage)) ||
isDeviceDisconnectedError(message)) {
showConnectionLostError();
} else if (message === ALLOW_USB_DEBUGGING) {
showAllowUSBDebugging();
} else if (isMessageComposedOf(
message,
[BINARY_PUSH_FAILURE, BINARY_PUSH_UNKNOWN_RESPONSE])) {
showFailedToPushBinary(message.substring(message.indexOf(':') + 1));
} else if (message === NO_DEVICE_SELECTED) {
showNoDeviceSelected();
} else if (WEBSOCKET_UNABLE_TO_CONNECT === message) {
showWebsocketConnectionIssue(message);
} else if (message === EXTENSION_NOT_INSTALLED) {
showExtensionNotInstalled();
} else if (isMessageComposedOf(message, [
PARSING_UNKNWON_REQUEST_ID,
PARSING_UNABLE_TO_DECODE_METHOD,
PARSING_UNRECOGNIZED_PORT,
PARSING_UNRECOGNIZED_MESSAGE,
])) {
showIssueParsingTheTracedResponse(message);
} else {
throw new Error(`${message}`);
}
}
function isDeviceDisconnectedError(message: string) {
return message.includes('Device with serial') &&
message.includes('was disconnected.');
}
function isMessageComposedOf(message: string, issues: string[]) {
for (const issue of issues) {
if (message.includes(issue)) {
return true;
}
}
return false;
}
// Exception thrown by the Recording logic.
export class RecordingError extends Error {}