From b0e114fb85a6054f582933cf3079620740bbd637 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Mon, 14 Oct 2024 13:36:07 +0200 Subject: [PATCH] Redirect logs/uncaught errors from webview to the main extension logger (#621) This PR adds a mechanism for redirecting logs and unhandled error from webview to the main extension. The reason for this change is that we can see potential issues and logs in a unified log output that we create for the extension context. This way we provide better visibility of errors that happen in the webview. Without this change, we'd always need to check both the extension log output, and the vscode devtools panel. With this change, having everything in a single output is specifically helpful when receiving logs from the users, as we no longer need to ask them to check devtools console and can focus on a single log file they need to check or send as a part of the report. ### How Has This Been Tested: Add log statement and unhandled error to the main webview App.tsx. See that this gets included in the main log output. --- packages/vscode-extension/src/common/utils.ts | 2 + .../src/panels/WebviewController.ts | 60 ++++++++++--------- .../vscode-extension/src/utilities/utils.ts | 4 ++ .../vscode-extension/src/webview/index.jsx | 4 +- .../src/webview/providers/UtilsProvider.tsx | 28 ++++++++- .../src/webview/utilities/rpc.ts | 10 +++- 6 files changed, 76 insertions(+), 32 deletions(-) diff --git a/packages/vscode-extension/src/common/utils.ts b/packages/vscode-extension/src/common/utils.ts index 1601433f1..43ed9fcbe 100644 --- a/packages/vscode-extension/src/common/utils.ts +++ b/packages/vscode-extension/src/common/utils.ts @@ -14,4 +14,6 @@ export interface UtilsInterface { showDismissableError(errorMessage: string): Promise; openExternalUrl(uriString: string): Promise; + + log(type: "info" | "error" | "warn" | "log", message: string, ...args: any[]): Promise; } diff --git a/packages/vscode-extension/src/panels/WebviewController.ts b/packages/vscode-extension/src/panels/WebviewController.ts index 854e575cb..f4230198a 100644 --- a/packages/vscode-extension/src/panels/WebviewController.ts +++ b/packages/vscode-extension/src/panels/WebviewController.ts @@ -3,7 +3,6 @@ import { DependencyManager } from "../dependency/DependencyManager"; import { DeviceManager } from "../devices/DeviceManager"; import { Project } from "../project/project"; import { Logger } from "../Logger"; -import { extensionContext } from "../utilities/extensionContext"; import { WorkspaceConfigController } from "./WorkspaceConfigController"; import { getTelemetryReporter } from "../utilities/telemetry"; import { Utils } from "../utilities/utils"; @@ -15,10 +14,10 @@ type CallArgs = { method: string; args: unknown[]; }; -export type WebviewEvent = - | { - command: "call"; - } & CallArgs; + +export type WebviewEvent = { + command: "call"; +} & CallArgs; export class WebviewController implements Disposable { private readonly dependencyManager: DependencyManager; @@ -92,15 +91,13 @@ export class WebviewController implements Disposable { private setWebviewMessageListener(webview: Webview) { webview.onDidReceiveMessage( (message: WebviewEvent) => { - const isTouchEvent = message.command === "call" && message.method === "dispatchTouches"; - if (!isTouchEvent) { + // ignore dispatchTouches and log calls from being logged as "Message from webview" + if (message.method !== "dispatchTouches" && message.method !== "log") { Logger.log("Message from webview", message); } - switch (message.command) { - case "call": - this.handleRemoteCall(message); - return; + if (message.command === "call") { + this.handleRemoteCall(message); } }, undefined, @@ -113,27 +110,32 @@ export class WebviewController implements Disposable { const callableObject = this.callableObjects.get(object); if (callableObject && method in callableObject) { const argsWithCallbacks = args.map((arg: any) => { - if (typeof arg === "object" && arg !== null && "__callbackId" in arg) { - const callbackId = arg.__callbackId; - let callback = this.idToCallback.get(callbackId)?.deref(); - if (!callback) { - callback = (...options: any[]) => { - this.webview.postMessage({ - command: "callback", - callbackId, - args: options, - }); - }; - this.idToCallback.set(callbackId, new WeakRef(callback)); - if (this.idToCallback.size > 200) { - Logger.warn("Too many callbacks in memory! Something is wrong!"); + if (typeof arg === "object" && arg !== null) { + if ("__callbackId" in arg) { + const callbackId = arg.__callbackId; + let callback = this.idToCallback.get(callbackId)?.deref(); + if (!callback) { + callback = (...options: any[]) => { + this.webview.postMessage({ + command: "callback", + callbackId, + args: options, + }); + }; + this.idToCallback.set(callbackId, new WeakRef(callback)); + if (this.idToCallback.size > 200) { + Logger.warn("Too many callbacks in memory! Something is wrong!"); + } + this.idToCallbackFinalizationRegistry.register(callback, callbackId); } - this.idToCallbackFinalizationRegistry.register(callback, callbackId); + return callback; + } else if ("__error" in arg) { + const error = new Error(arg.__error.message); + Object.assign(error, arg.__error); + return error; } - return callback; - } else { - return arg; } + return arg; }); // @ts-ignore const result = callableObject[method](...argsWithCallbacks); diff --git a/packages/vscode-extension/src/utilities/utils.ts b/packages/vscode-extension/src/utilities/utils.ts index 7d5405feb..416f6e9b5 100644 --- a/packages/vscode-extension/src/utilities/utils.ts +++ b/packages/vscode-extension/src/utilities/utils.ts @@ -112,4 +112,8 @@ export class Utils implements UtilsInterface { public async openExternalUrl(uriString: string) { env.openExternal(Uri.parse(uriString)); } + + public async log(type: "info" | "error" | "warn" | "log", message: string, ...args: any[]) { + Logger[type]("[WEBVIEW LOG]", message, ...args); + } } diff --git a/packages/vscode-extension/src/webview/index.jsx b/packages/vscode-extension/src/webview/index.jsx index 4f2a60fdb..1e793bec6 100644 --- a/packages/vscode-extension/src/webview/index.jsx +++ b/packages/vscode-extension/src/webview/index.jsx @@ -10,9 +10,11 @@ import AlertProvider from "./providers/AlertProvider"; import WorkspaceConfigProvider from "./providers/WorkspaceConfigProvider"; import "./styles/theme.css"; -import UtilsProvider from "./providers/UtilsProvider"; +import { UtilsProvider, installLogOverrides } from "./providers/UtilsProvider"; import LaunchConfigProvider from "./providers/LaunchConfigProvider"; +installLogOverrides(); + const container = document.getElementById("root"); const root = createRoot(container); diff --git a/packages/vscode-extension/src/webview/providers/UtilsProvider.tsx b/packages/vscode-extension/src/webview/providers/UtilsProvider.tsx index 41702844e..24dbc4051 100644 --- a/packages/vscode-extension/src/webview/providers/UtilsProvider.tsx +++ b/packages/vscode-extension/src/webview/providers/UtilsProvider.tsx @@ -21,7 +21,7 @@ const utils = makeProxy("Utils"); const UtilsContext = createContext(utils); -export default function UtilsProvider({ children }: PropsWithChildren) { +export function UtilsProvider({ children }: PropsWithChildren) { return {children}; } @@ -33,3 +33,29 @@ export function useUtils() { } return context; } + +export function installLogOverrides() { + function wrapConsole(methodName: "log" | "info" | "warn" | "error") { + const consoleMethod = console[methodName]; + console[methodName] = (message: string, ...args: any[]) => { + utils.log(methodName, message, ...args); + consoleMethod(message, ...args); + }; + } + + (["log", "info", "warn", "error"] as const).forEach(wrapConsole); + + // install uncaught exception handler + window.addEventListener("error", (event) => { + utils.log("error", "Uncaught exception", event.error.stack); + // rethrow the error to be caught by the global error handler + throw event.error; + }); + + // install uncaught promise rejection handler + window.addEventListener("unhandledrejection", (event) => { + utils.log("error", "Uncaught promise rejection", event.reason); + // rethrow the error to be caught by the global error handler + throw event.reason; + }); +} diff --git a/packages/vscode-extension/src/webview/utilities/rpc.ts b/packages/vscode-extension/src/webview/utilities/rpc.ts index 576d1fcc7..d1725ce7e 100644 --- a/packages/vscode-extension/src/webview/utilities/rpc.ts +++ b/packages/vscode-extension/src/webview/utilities/rpc.ts @@ -68,7 +68,15 @@ export function makeProxy(objectName: string) { return (...args: any[]) => { const currentCallId = `${instanceToken}:${globalCallCounter++}`; let argsWithCallbacks = args.map((arg) => { - if (typeof arg === "function") { + if (arg instanceof Error) { + return { + __error: { + name: arg.name, + message: arg.message, + stack: arg.stack, + }, + }; + } else if (typeof arg === "function") { maybeInitializeCallbackMessageListener(); const callbackId = callbackToID.get(arg) || `${instanceToken}:${globalCallbackCounter++}`;