diff --git a/news/2 Fixes/13106.md b/news/2 Fixes/13106.md new file mode 100644 index 000000000000..68189e04e0a5 --- /dev/null +++ b/news/2 Fixes/13106.md @@ -0,0 +1 @@ +If a webpanel fails to load, dispose our webviewhost so that it can try again. \ No newline at end of file diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 2459ca1aded7..398f06a4ca35 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -1060,6 +1060,10 @@ export type WebPanelMessage = { // Wraps the VS Code webview panel export const IWebPanel = Symbol('IWebPanel'); export interface IWebPanel { + /** + * Event is fired when the load for a web panel fails + */ + readonly loadFailed: Event; /** * Convert a uri for the local file system to one that can be used inside webviews. * diff --git a/src/client/common/application/webPanels/webPanel.ts b/src/client/common/application/webPanels/webPanel.ts index c91c74b847a4..04c61e5583c1 100644 --- a/src/client/common/application/webPanels/webPanel.ts +++ b/src/client/common/application/webPanels/webPanel.ts @@ -4,8 +4,9 @@ import '../../extensions'; import * as path from 'path'; -import { Uri, Webview, WebviewOptions, WebviewPanel, window } from 'vscode'; +import { Event, EventEmitter, Uri, Webview, WebviewOptions, WebviewPanel, window } from 'vscode'; import { Identifiers } from '../../../datascience/constants'; +import { traceError } from '../../logger'; import { IFileSystem } from '../../platform/types'; import { IDisposableRegistry } from '../../types'; import * as localize from '../../utils/localize'; @@ -14,6 +15,7 @@ import { IWebPanel, IWebPanelOptions, WebPanelMessage } from '../types'; export class WebPanel implements IWebPanel { private panel: WebviewPanel | undefined; private loadPromise: Promise; + private loadFailedEmitter = new EventEmitter(); constructor( private fs: IFileSystem, @@ -43,6 +45,9 @@ export class WebPanel implements IWebPanel { this.loadPromise = this.load(); } + public get loadFailed(): Event { + return this.loadFailedEmitter.event; + } public async show(preserveFocus: boolean) { await this.loadPromise; if (this.panel) { @@ -89,42 +94,49 @@ export class WebPanel implements IWebPanel { // tslint:disable-next-line:no-any private async load() { - if (this.panel) { - const localFilesExist = await Promise.all(this.options.scripts.map((s) => this.fs.fileExists(s))); - if (localFilesExist.every((exists) => exists === true)) { - // Call our special function that sticks this script inside of an html page - // and translates all of the paths to vscode-resource URIs - this.panel.webview.html = await this.generateLocalReactHtml(this.panel.webview); - - // Reset when the current panel is closed - this.disposableRegistry.push( - this.panel.onDidDispose(() => { - this.panel = undefined; - this.options.listener.dispose().ignoreErrors(); - }) - ); - - this.disposableRegistry.push( - this.panel.webview.onDidReceiveMessage((message) => { - // Pass the message onto our listener - this.options.listener.onMessage(message.type, message.payload); - }) - ); - - this.disposableRegistry.push( - this.panel.onDidChangeViewState((_e) => { - // Pass the state change onto our listener - this.options.listener.onChangeViewState(this); - }) - ); - - // Set initial state - this.options.listener.onChangeViewState(this); - } else { - // Indicate that we can't load the file path - const badPanelString = localize.DataScience.badWebPanelFormatString(); - this.panel.webview.html = badPanelString.format(this.options.scripts.join(', ')); + try { + if (this.panel) { + const localFilesExist = await Promise.all(this.options.scripts.map((s) => this.fs.fileExists(s))); + if (localFilesExist.every((exists) => exists === true)) { + // Call our special function that sticks this script inside of an html page + // and translates all of the paths to vscode-resource URIs + this.panel.webview.html = await this.generateLocalReactHtml(this.panel.webview); + + // Reset when the current panel is closed + this.disposableRegistry.push( + this.panel.onDidDispose(() => { + this.panel = undefined; + this.options.listener.dispose().ignoreErrors(); + }) + ); + + this.disposableRegistry.push( + this.panel.webview.onDidReceiveMessage((message) => { + // Pass the message onto our listener + this.options.listener.onMessage(message.type, message.payload); + }) + ); + + this.disposableRegistry.push( + this.panel.onDidChangeViewState((_e) => { + // Pass the state change onto our listener + this.options.listener.onChangeViewState(this); + }) + ); + + // Set initial state + this.options.listener.onChangeViewState(this); + } else { + // Indicate that we can't load the file path + const badPanelString = localize.DataScience.badWebPanelFormatString(); + this.panel.webview.html = badPanelString.format(this.options.scripts.join(', ')); + } } + } catch (error) { + // If our web panel failes to load, report that out so whatever + // is hosting the panel can clean up + traceError(`Error Loading WebPanel: ${error}`); + this.loadFailedEmitter.fire(); } } diff --git a/src/client/datascience/webViewHost.ts b/src/client/datascience/webViewHost.ts index 36f1b262f65e..ba4c9c49eeaf 100644 --- a/src/client/datascience/webViewHost.ts +++ b/src/client/datascience/webViewHost.ts @@ -29,10 +29,9 @@ export abstract class WebViewHost implements IDisposable { private webPanel: IWebPanel | undefined; private webPanelInit: Deferred | undefined = createDeferred(); private messageListener: IWebPanelMessageListener; - private themeChangeHandler: IDisposable | undefined; - private settingsChangeHandler: IDisposable | undefined; private themeIsDarkPromise: Deferred | undefined = createDeferred(); private startupStopwatch = new StopWatch(); + private readonly _disposables: IDisposable[] = []; constructor( @unmanaged() protected configService: IConfigurationService, @@ -62,12 +61,12 @@ export abstract class WebViewHost implements IDisposable { ); // Listen for settings changes from vscode. - this.themeChangeHandler = this.workspaceService.onDidChangeConfiguration(this.onPossibleSettingsChange, this); + this._disposables.push(this.workspaceService.onDidChangeConfiguration(this.onPossibleSettingsChange, this)); // Listen for settings changes - this.settingsChangeHandler = this.configService - .getSettings(undefined) - .onDidChange(this.onDataScienceSettingsChanged.bind(this)); + this._disposables.push( + this.configService.getSettings(undefined).onDidChange(this.onDataScienceSettingsChanged.bind(this)) + ); } public async show(preserveFocus: boolean): Promise { @@ -91,14 +90,9 @@ export abstract class WebViewHost implements IDisposable { this.webPanel.close(); this.webPanel = undefined; } - if (this.themeChangeHandler) { - this.themeChangeHandler.dispose(); - this.themeChangeHandler = undefined; - } - if (this.settingsChangeHandler) { - this.settingsChangeHandler.dispose(); - this.settingsChangeHandler = undefined; - } + + this._disposables.forEach((item) => item.dispose()); + this.webPanelInit = undefined; this.themeIsDarkPromise = undefined; } @@ -272,6 +266,9 @@ export abstract class WebViewHost implements IDisposable { additionalPaths: workspaceFolder ? [workspaceFolder.fsPath] : [] }); + // Track to seee if our web panel fails to load + this._disposables.push(this.webPanel.loadFailed(this.onWebPanelLoadFailed, this)); + traceInfo('Web view created.'); } @@ -294,6 +291,11 @@ export abstract class WebViewHost implements IDisposable { this.postMessageInternal(SharedMessages.UpdateSettings, dsSettings).ignoreErrors(); }; + // If our webpanel fails to load then just dispose ourselves + private onWebPanelLoadFailed = async () => { + this.dispose(); + }; + private getValue(workspaceConfig: WorkspaceConfiguration, section: string, defaultValue: T): T { if (workspaceConfig) { return workspaceConfig.get(section, defaultValue); diff --git a/src/test/datascience/mountedWebView.ts b/src/test/datascience/mountedWebView.ts index 9b56c02bde5c..6da22b1a0693 100644 --- a/src/test/datascience/mountedWebView.ts +++ b/src/test/datascience/mountedWebView.ts @@ -58,6 +58,7 @@ export class MountedWebView implements IMountedWebView, IDisposable { private active = true; private visible = true; private disposedEvent = new EventEmitter(); + private loadFailedEmitter = new EventEmitter(); constructor(mount: () => ReactWrapper, React.Component>, public readonly id: string) { // Setup the acquireVsCodeApi. The react control will cache this value when it's mounted. @@ -97,6 +98,9 @@ export class MountedWebView implements IMountedWebView, IDisposable { public get onDisposed() { return this.disposedEvent.event; } + public get loadFailed(): Event { + return this.loadFailedEmitter.event; + } public attach(options: IWebPanelOptions) { this.webPanelListener = options.listener; diff --git a/src/test/datascience/uiTests/webBrowserPanel.ts b/src/test/datascience/uiTests/webBrowserPanel.ts index db1559d2ff03..055d22c32c87 100644 --- a/src/test/datascience/uiTests/webBrowserPanel.ts +++ b/src/test/datascience/uiTests/webBrowserPanel.ts @@ -134,6 +134,7 @@ export class WebBrowserPanel implements IWebPanel, IDisposable { private panel?: WebviewPanel; private server?: IWebServer; private serverUrl: string | undefined; + private loadFailedEmitter = new EventEmitter(); constructor(private readonly disposableRegistry: IDisposableRegistry, private readonly options: IWebPanelOptions) { this.disposableRegistry.push(this); const webViewOptions: WebviewOptions = { @@ -174,6 +175,11 @@ export class WebBrowserPanel implements IWebPanel, IDisposable { console.error('Failed to start Web Browser Panel', ex) ); } + + public get loadFailed(): Event { + return this.loadFailedEmitter.event; + } + public asWebviewUri(localResource: Uri): Uri { const filePath = localResource.fsPath; const name = path.basename(path.dirname(filePath));