Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/2 Fixes/11239.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Show unhandled widget messages in the jupyter output window.
3 changes: 2 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -473,5 +473,6 @@
"DataScience.useCDNForWidgets": "Widgets require us to download supporting files from a 3rd party website. Click [here](https://aka.ms/PVSCIPyWidgets) for more information.",
"DataScience.loadThirdPartyWidgetScriptsPostEnabled": "Please restart the Kernel when changing the setting 'python.dataScience.widgetScriptSources'.",
"DataScience.enableCDNForWidgetsSetting": "Widgets require us to download supporting files from a 3rd party website. Click <a href='https://command:python.datascience.enableLoadingWidgetScriptsFromThirdPartySource'>here</a> to enable this or click <a href='https://aka.ms/PVSCIPyWidgets'>here</a> for more information. (Error loading {0}:{1}).",
"DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork": "Unable to load a compatible version of the widget '{0}'. Expected behavior may be affected."
"DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork": "Unable to load a compatible version of the widget '{0}'. Expected behavior may be affected.",
"DataScience.unhandledMessage": "Unhandled kernel message from a widget: {0} : {1}"
}
6 changes: 6 additions & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,12 @@ export namespace DataScience {
'DataScience.enableCDNForWidgetsSetting',
"Widgets require us to download supporting files from a 3rd party website. Click <a href='https://command:python.datascience.enableLoadingWidgetScriptsFromThirdPartySource'>here</a> to enable this or click <a href='https://aka.ms/PVSCIPyWidgets'>here</a> for more information. (Error loading {0}:{1})."
);

export const unhandledMessage = localize(
'DataScience.unhandledMessage',
'Unhandled kernel message from a widget: {0} : {1}'
);

export const widgetScriptNotFoundOnCDNWidgetMightNotWork = localize(
'DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork',
"Unable to load a compatible version of the widget '{0}'. Expected behavior may be affected."
Expand Down
3 changes: 2 additions & 1 deletion src/client/datascience/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,8 @@ export enum Telemetry {
IPyWidgetPromptToUseCDN = 'DS_INTERNAL.IPYWIDGET_PROMPT_TO_USE_CDN',
IPyWidgetPromptToUseCDNSelection = 'DS_INTERNAL.IPYWIDGET_PROMPT_TO_USE_CDN_SELECTION',
IPyWidgetOverhead = 'DS_INTERNAL.IPYWIDGET_OVERHEAD',
IPyWidgetRenderFailure = 'DS_INTERNAL.IPYWIDGET_RENDER_FAILURE'
IPyWidgetRenderFailure = 'DS_INTERNAL.IPYWIDGET_RENDER_FAILURE',
IPyWidgetUnhandledMessage = 'DS_INTERNAL.IPYWIDGET_UNHANDLED_MESSAGE'
}

export enum NativeKeyboardCommandTelemetry {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ export enum InteractiveWindowMessages {
UpdateDisplayData = 'update_display_data',
IPyWidgetLoadSuccess = 'ipywidget_load_success',
IPyWidgetLoadFailure = 'ipywidget_load_failure',
IPyWidgetRenderFailure = 'ipywidget_render_failure'
IPyWidgetRenderFailure = 'ipywidget_render_failure',
IPyWidgetUnhandledKernelMessage = 'ipywidget_unhandled_kernel_message'
}

export enum IPyWidgetMessages {
Expand Down Expand Up @@ -582,4 +583,5 @@ export class IInteractiveWindowMapping {
public [InteractiveWindowMessages.ConvertUriForUseInWebViewRequest]: Uri;
public [InteractiveWindowMessages.ConvertUriForUseInWebViewResponse]: { request: Uri; response: Uri };
public [InteractiveWindowMessages.IPyWidgetRenderFailure]: Error;
public [InteractiveWindowMessages.IPyWidgetUnhandledKernelMessage]: KernelMessage.IMessage;
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ const messageWithMessageTypes: MessageMapping<IInteractiveWindowMapping> & Messa
[InteractiveWindowMessages.IPyWidgetLoadSuccess]: MessageType.other,
[InteractiveWindowMessages.IPyWidgetLoadFailure]: MessageType.other,
[InteractiveWindowMessages.IPyWidgetRenderFailure]: MessageType.other,
[InteractiveWindowMessages.IPyWidgetUnhandledKernelMessage]: MessageType.other,
[InteractiveWindowMessages.LoadAllCells]: MessageType.other,
[InteractiveWindowMessages.LoadAllCellsComplete]: MessageType.other,
[InteractiveWindowMessages.LoadOnigasmAssemblyRequest]: MessageType.other,
Expand Down
36 changes: 31 additions & 5 deletions src/client/datascience/ipywidgets/ipywidgetHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@

'use strict';

import { inject, injectable } from 'inversify';
import type { KernelMessage } from '@jupyterlab/services';
import { inject, injectable, named } from 'inversify';
import stripAnsi from 'strip-ansi';
import { Event, EventEmitter, Uri } from 'vscode';
import {
ILoadIPyWidgetClassFailureAction,
LoadIPyWidgetClassLoadAction
} from '../../../datascience-ui/interactive-common/redux/reducers/types';
import { EnableIPyWidgets } from '../../common/experimentGroups';
import { traceError } from '../../common/logger';
import { IDisposableRegistry, IExperimentsManager } from '../../common/types';
import { traceError, traceInfo } from '../../common/logger';
import { IDisposableRegistry, IExperimentsManager, IOutputChannel } from '../../common/types';
import * as localize from '../../common/utils/localize';
import { sendTelemetryEvent } from '../../telemetry';
import { Telemetry } from '../constants';
import { JUPYTER_OUTPUT_CHANNEL, Telemetry } from '../constants';
import { INotebookIdentity, InteractiveWindowMessages } from '../interactive-common/interactiveWindowTypes';
import { IInteractiveWindowListener, INotebookProvider } from '../types';
import { IPyWidgetMessageDispatcherFactory } from './ipyWidgetMessageDispatcherFactory';
Expand Down Expand Up @@ -46,7 +49,8 @@ export class IPyWidgetHandler implements IInteractiveWindowListener {
@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry,
@inject(IPyWidgetMessageDispatcherFactory)
private readonly widgetMessageDispatcherFactory: IPyWidgetMessageDispatcherFactory,
@inject(IExperimentsManager) readonly experimentsManager: IExperimentsManager
@inject(IExperimentsManager) readonly experimentsManager: IExperimentsManager,
@inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) private jupyterOutput: IOutputChannel
) {
disposables.push(
notebookProvider.onNotebookCreated(async (e) => {
Expand All @@ -73,6 +77,8 @@ export class IPyWidgetHandler implements IInteractiveWindowListener {
this.sendLoadFailureTelemetry(payload);
} else if (message === InteractiveWindowMessages.IPyWidgetRenderFailure) {
this.sendRenderFailureTelemetry(payload);
} else if (message === InteractiveWindowMessages.IPyWidgetUnhandledKernelMessage) {
this.handleUnhandledMessage(payload);
}
// tslint:disable-next-line: no-any
this.getIPyWidgetMessageDispatcher()?.receiveMessage({ message: message as any, payload }); // NOSONAR
Expand Down Expand Up @@ -112,6 +118,26 @@ export class IPyWidgetHandler implements IInteractiveWindowListener {
// Do nothing on a failure
}
}

private handleUnhandledMessage(msg: KernelMessage.IMessage) {
// Skip status messages
if (msg.header.msg_type !== 'status') {
try {
// Special case errors, strip ansi codes from tracebacks so they print better.
if (msg.header.msg_type === 'error') {
const errorMsg = msg as KernelMessage.IErrorMsg;
errorMsg.content.traceback = errorMsg.content.traceback.map(stripAnsi);
}
traceInfo(`Unhandled widget kernel message: ${msg.header.msg_type} ${msg.content}`);
this.jupyterOutput.appendLine(
localize.DataScience.unhandledMessage().format(msg.header.msg_type, JSON.stringify(msg.content))
);
sendTelemetryEvent(Telemetry.IPyWidgetUnhandledMessage, undefined, { msg_type: msg.header.msg_type });
} catch {
// Don't care if this doesn't get logged
}
}
}
private getIPyWidgetMessageDispatcher() {
if (!this.notebookIdentity || !this.enabled) {
return;
Expand Down
6 changes: 6 additions & 0 deletions src/client/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2005,4 +2005,10 @@ export interface IEventNamePropertyMapping {
* Telemetry event sent when the widget render function fails (note, this may not be sufficient to capture all failures).
*/
[Telemetry.IPyWidgetRenderFailure]: never | undefined;
/**
* Telemetry event sent when the widget tries to send a kernel message but nothing was listening
*/
[Telemetry.IPyWidgetUnhandledMessage]: {
msg_type: string;
};
}
12 changes: 12 additions & 0 deletions src/datascience-ui/ipywidgets/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ReplaySubject } from 'rxjs/ReplaySubject';
import { createDeferred, Deferred } from '../../client/common/utils/async';
import {
IInteractiveWindowMapping,
InteractiveWindowMessages,
IPyWidgetMessages
} from '../../client/datascience/interactive-common/interactiveWindowTypes';
import { KernelSocketOptions } from '../../client/datascience/types';
Expand Down Expand Up @@ -161,6 +162,9 @@ export class WidgetManager implements IIPyWidgetManager, IMessageHandler {
// Listen for display data messages so we can prime the model for a display data
this.proxyKernel.iopubMessage.connect(this.handleDisplayDataMessage.bind(this));

// Listen for unhandled IO pub so we can forward to the extension
this.manager.onUnhandledIOPubMessage.connect(this.handleUnhanldedIOPubMessage.bind(this));

// Tell the observable about our new manager
WidgetManager._instance.next(this);
} catch (ex) {
Expand Down Expand Up @@ -204,4 +208,12 @@ export class WidgetManager implements IIPyWidgetManager, IMessageHandler {
}
}
}

private handleUnhanldedIOPubMessage(_manager: any, msg: KernelMessage.IIOPubMessage) {
// Send this to the other side
this.postOffice.sendMessage<IInteractiveWindowMapping>(
InteractiveWindowMessages.IPyWidgetUnhandledKernelMessage,
msg
);
}
}
5 changes: 5 additions & 0 deletions src/datascience-ui/ipywidgets/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import * as jupyterlab from '@jupyter-widgets/base/lib';
import type { Kernel, KernelMessage } from '@jupyterlab/services';
import type { nbformat } from '@jupyterlab/services/node_modules/@jupyterlab/coreutils';
import { ISignal } from '@phosphor/signaling';
import { Widget } from '@phosphor/widgets';
import { IInteractiveWindowMapping } from '../../client/datascience/interactive-common/interactiveWindowTypes';

Expand All @@ -25,6 +26,10 @@ export type IJupyterLabWidgetManagerCtor = new (
) => IJupyterLabWidgetManager;

export interface IJupyterLabWidgetManager {
/**
* Signal emitted when a view emits an IO Pub message but nothing handles it.
*/
readonly onUnhandledIOPubMessage: ISignal<this, KernelMessage.IIOPubMessage>;
dispose(): void;
/**
* Close all widgets and empty the widget state.
Expand Down
4 changes: 4 additions & 0 deletions src/ipywidgets/src/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ export class WidgetManager extends jupyterlab.WidgetManager {
// This throws errors if enabled, can be added later.
}

public get onUnhandledIOPubMessage() {
return super.onUnhandledIOPubMessage;
}

protected async loadClass(className: string, moduleName: string, moduleVersion: string): Promise<any> {
// Call the base class to try and load. If that fails, look locally
window.console.log(`WidgetManager: Loading class ${className}:${moduleName}:${moduleVersion}`);
Expand Down