Browse Source

Backport PR #11752: Pause on exception (#11923)

* Backport PR #11752: Pause on exception

* fix tests for 3.3.x

* linting
Andrew Fulton 3 years ago
parent
commit
7dbabbfe5f

+ 30 - 1
packages/debugger-extension/src/index.ts

@@ -517,9 +517,15 @@ const sidebar: JupyterFrontEndPlugin<IDebugger.ISidebar> = {
       evaluate: CommandIDs.evaluate
     };
 
+    const breakpointsCommands = {
+      registry: commands,
+      pause: CommandIDs.pause
+    };
+
     const sidebar = new Debugger.Sidebar({
       service,
       callstackCommands,
+      breakpointsCommands,
       editorServices,
       themeManager,
       translator
@@ -709,6 +715,28 @@ const main: JupyterFrontEndPlugin<void> = {
       }
     });
 
+    commands.addCommand(CommandIDs.pause, {
+      label: service.isPausingOnExceptions
+        ? trans.__('Disable pausing on exceptions')
+        : trans.__('Enable pausing on exceptions'),
+      caption: trans.__('Enable / Disable pausing on exceptions'),
+      className: 'jp-PauseOnExceptions',
+      icon: Debugger.Icons.pauseOnExceptionsIcon,
+      isToggled: () => {
+        return service.isPausingOnExceptions;
+      },
+      isEnabled: () => {
+        return !!service.isStarted;
+      },
+      isVisible: () => {
+        return service.pauseOnExceptionsIsValid();
+      },
+      execute: async () => {
+        await service.pauseOnExceptions(!service.isPausingOnExceptions);
+        commands.notifyCommandChanged();
+      }
+    });
+
     service.eventMessage.connect((_, event): void => {
       commands.notifyCommandChanged();
       if (labShell && event.event === 'initialized') {
@@ -737,7 +765,8 @@ const main: JupyterFrontEndPlugin<void> = {
         CommandIDs.next,
         CommandIDs.stepIn,
         CommandIDs.stepOut,
-        CommandIDs.evaluate
+        CommandIDs.evaluate,
+        CommandIDs.pause
       ].forEach(command => {
         palette.addItem({ command, category });
       });

+ 1 - 0
packages/debugger/package.json

@@ -71,6 +71,7 @@
     "@lumino/messaging": "^1.4.3",
     "@lumino/signaling": "^1.4.3",
     "@lumino/widgets": "^1.19.0",
+    "@vscode/debugprotocol": "~1.51.0",
     "codemirror": "~5.61.0",
     "react": "^17.0.1",
     "vscode-debugprotocol": "^1.37.0"

+ 4 - 0
packages/debugger/src/debugger.ts

@@ -15,6 +15,7 @@ import { EditorHandler as DebuggerEditorHandler } from './handlers/editor';
 
 import {
   closeAllIcon as closeAll,
+  pauseOnExceptionsIcon as pauseOnExceptions,
   stepIntoIcon as stepInto,
   stepOutIcon as stepOut,
   stepOverIcon as stepOver,
@@ -116,6 +117,8 @@ export namespace Debugger {
     export const evaluate = 'debugger:evaluate';
 
     export const restartDebug = 'debugger:restart-debug';
+
+    export const pause = 'debugger:pause';
   }
 
   /**
@@ -131,6 +134,7 @@ export namespace Debugger {
     export const terminateIcon = stopIcon;
     export const variableIcon = variable;
     export const viewBreakpointIcon = viewBreakpoint;
+    export const pauseOnExceptionsIcon = pauseOnExceptions;
   }
 
   /**

+ 6 - 0
packages/debugger/src/icons.ts

@@ -8,6 +8,7 @@ import stepIntoSvgStr from '../style/icons/step-into.svg';
 import stepOutSvgStr from '../style/icons/step-out.svg';
 import stepOverSvgStr from '../style/icons/step-over.svg';
 import variableSvgStr from '../style/icons/variable.svg';
+import pauseSvgStr from '../style/icons/pause.svg';
 import viewBreakpointSvgStr from '../style/icons/view-breakpoint.svg';
 
 export {
@@ -20,6 +21,11 @@ export const closeAllIcon = new LabIcon({
   svgstr: closeAllSvgStr
 });
 
+export const pauseOnExceptionsIcon = new LabIcon({
+  name: 'debugger:pause',
+  svgstr: pauseSvgStr
+});
+
 export const stepIntoIcon = new LabIcon({
   name: 'debugger:step-into',
   svgstr: stepIntoSvgStr

+ 39 - 5
packages/debugger/src/panels/breakpoints/index.ts

@@ -1,10 +1,15 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-import { Dialog, showDialog, ToolbarButton } from '@jupyterlab/apputils';
+import {
+  CommandToolbarButton,
+  Dialog,
+  showDialog,
+  ToolbarButton
+} from '@jupyterlab/apputils';
 
 import { ITranslator, nullTranslator } from '@jupyterlab/translation';
-
+import { CommandRegistry } from '@lumino/commands';
 import { Signal } from '@lumino/signaling';
 
 import { Panel } from '@lumino/widgets';
@@ -27,14 +32,24 @@ export class Breakpoints extends Panel {
    * @param options The instantiation options for a Breakpoints Panel.
    */
   constructor(options: Breakpoints.IOptions) {
-    super();
-    const { model, service } = options;
+    super(options);
+    const { model, service, commands } = options;
     const translator = options.translator || nullTranslator;
-    const trans = translator.load('jupyterlab');
+    const trans = (options.translator ?? nullTranslator).load('jupyterlab');
+    this.title.label = trans.__('Breakpoints');
 
     const header = new BreakpointsHeader(translator);
     const body = new BreakpointsBody(model);
 
+    header.toolbar.addItem(
+      'pause',
+      new CommandToolbarButton({
+        commands: commands.registry,
+        id: commands.pause,
+        label: ''
+      })
+    );
+
     header.toolbar.addItem(
       'closeAll',
       new ToolbarButton({
@@ -73,6 +88,20 @@ export class Breakpoints extends Panel {
  * A namespace for Breakpoints `statics`.
  */
 export namespace Breakpoints {
+  /**
+   * The toolbar commands and registry for the breakpoints.
+   */
+  export interface ICommands {
+    /**
+     * The command registry.
+     */
+    registry: CommandRegistry;
+
+    /**
+     * The pause command ID.
+     */
+    pause: string;
+  }
   /**
    * Instantiation options for `Breakpoints`.
    */
@@ -87,6 +116,11 @@ export namespace Breakpoints {
      */
     service: IDebugger;
 
+    /**
+     * The toolbar commands interface for the callstack.
+     */
+    commands: ICommands;
+
     /**
      * The application language translator..
      */

+ 83 - 0
packages/debugger/src/service.ts

@@ -58,6 +58,23 @@ export class DebuggerService implements IDebugger, IDisposable {
     return this._session?.isStarted ?? false;
   }
 
+  /**
+   * Whether the current debugger is pausing on exceptions.
+   */
+  get isPausingOnExceptions(): boolean {
+    const kernel = this.session?.connection?.kernel?.name ?? '';
+    if (kernel) {
+      const tmpFileParams = this._config.getTmpFileParams(kernel);
+      if (tmpFileParams) {
+        return (
+          this._session?.pausingOnExceptions.includes(tmpFileParams.prefix) ??
+          false
+        );
+      }
+    }
+    return false;
+  }
+
   /**
    * Returns the debugger service's model.
    */
@@ -491,6 +508,72 @@ export class DebuggerService implements IDebugger, IDisposable {
     await this.session.sendRequest('configurationDone', {});
   }
 
+  /**
+   * Determines if pausing on exceptions is supported by the kernel
+   *
+   */
+  pauseOnExceptionsIsValid(): boolean {
+    if (this.isStarted) {
+      if (this.session?.exceptionBreakpointFilters) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Enable or disable pausing on exceptions.
+   *
+   * @param enable - Whether to enbale or disable pausing on exceptions.
+   */
+  async pauseOnExceptions(enable: boolean): Promise<void> {
+    if (!this.session?.isStarted) {
+      return;
+    }
+
+    const kernel = this.session?.connection?.kernel?.name ?? '';
+    if (!kernel) {
+      return;
+    }
+    const tmpFileParams = this._config.getTmpFileParams(kernel);
+    if (!tmpFileParams) {
+      return;
+    }
+    let prefix = tmpFileParams.prefix;
+    const exceptionBreakpointFilters = this.session.exceptionBreakpointFilters;
+    let pauseOnExceptionKernels = this.session.pausingOnExceptions;
+    if (enable) {
+      if (!this.session.pausingOnExceptions.includes(prefix)) {
+        pauseOnExceptionKernels.push(prefix);
+        this.session.pausingOnExceptions = pauseOnExceptionKernels;
+      }
+    } else {
+      let prefixIndex = this.session.pausingOnExceptions.indexOf(prefix);
+      if (prefixIndex > -1) {
+        this.session.pausingOnExceptions = pauseOnExceptionKernels.splice(
+          prefixIndex,
+          1
+        );
+        this.session.pausingOnExceptions = pauseOnExceptionKernels;
+      }
+    }
+    const filters: string[] = [];
+    const exceptionOptions: DebugProtocol.ExceptionOptions[] = [];
+    const breakMode = enable ? 'userUnhandled' : 'never';
+    for (let filterDict of exceptionBreakpointFilters ?? []) {
+      filters.push(filterDict.filter);
+      exceptionOptions.push({
+        path: [{ names: this.session.exceptionPaths }],
+        breakMode: breakMode
+      });
+    }
+    const options: DebugProtocol.SetExceptionBreakpointsArguments = {
+      filters: filters,
+      exceptionOptions: exceptionOptions
+    };
+    await this.session.sendRequest('setExceptionBreakpoints', options);
+  }
+
   /**
    * Get the debugger state
    *

+ 37 - 3
packages/debugger/src/session.ts

@@ -9,6 +9,8 @@ import { PromiseDelegate } from '@lumino/coreutils';
 
 import { ISignal, Signal } from '@lumino/signaling';
 
+import { DebugProtocol } from '@vscode/debugprotocol';
+
 import { IDebugger } from './tokens';
 
 /**
@@ -80,12 +82,39 @@ export class DebuggerSession implements IDebugger.ISession {
   }
 
   /**
-   * Whether the debug session is started
+   * Whether the debug session is started.
    */
   get isStarted(): boolean {
     return this._isStarted;
   }
 
+  /**
+   * Whether to pause on exceptions
+   */
+  get pausingOnExceptions(): string[] {
+    return this._pausingOnExceptions;
+  }
+
+  set pausingOnExceptions(updatedPausingOnExceptions: string[]) {
+    this._pausingOnExceptions = updatedPausingOnExceptions;
+  }
+
+  /**
+   * Exception paths defined by the debugger
+   */
+  get exceptionPaths(): string[] {
+    return this._exceptionPaths;
+  }
+
+  /**
+   * Exception breakpoint filters defined by the debugger
+   */
+  get exceptionBreakpointFilters():
+    | DebugProtocol.ExceptionBreakpointsFilter[]
+    | undefined {
+    return this._exceptionBreakpointFilters;
+  }
+
   /**
    * Signal emitted for debug event messages.
    */
@@ -125,9 +154,8 @@ export class DebuggerSession implements IDebugger.ISession {
     if (!reply.success) {
       throw new Error(`Could not start the debugger: ${reply.message}`);
     }
-
     this._isStarted = true;
-
+    this._exceptionBreakpointFilters = reply.body?.exceptionBreakpointFilters;
     await this.sendRequest('attach', {});
   }
 
@@ -148,6 +176,7 @@ export class DebuggerSession implements IDebugger.ISession {
   async restoreState(): Promise<IDebugger.ISession.Response['debugInfo']> {
     const message = await this.sendRequest('debugInfo', {});
     this._isStarted = message.body.isStarted;
+    this._exceptionPaths = message.body?.exceptionPaths;
     return message;
   }
 
@@ -218,6 +247,11 @@ export class DebuggerSession implements IDebugger.ISession {
   private _connection: Session.ISessionConnection | null;
   private _isDisposed = false;
   private _isStarted = false;
+  private _pausingOnExceptions: string[] = [];
+  private _exceptionPaths: string[] = [];
+  private _exceptionBreakpointFilters:
+    | DebugProtocol.ExceptionBreakpointsFilter[]
+    | undefined = [];
   private _disposed = new Signal<this, void>(this);
   private _eventMessage = new Signal<
     IDebugger.ISession,

+ 8 - 0
packages/debugger/src/sidebar.ts

@@ -38,6 +38,7 @@ export class DebuggerSidebar extends Panel implements IDebugger.ISidebar {
 
     const {
       callstackCommands,
+      breakpointsCommands,
       editorServices,
       service,
       themeManager
@@ -61,6 +62,7 @@ export class DebuggerSidebar extends Panel implements IDebugger.ISidebar {
 
     this.breakpoints = new BreakpointsPanel({
       service,
+      commands: breakpointsCommands,
       model: model.breakpoints,
       translator
     });
@@ -183,6 +185,12 @@ export namespace DebuggerSidebar {
      * The callstack toolbar commands.
      */
     callstackCommands: CallstackPanel.ICommands;
+
+    /**
+     * The callstack toolbar commands.
+     */
+    breakpointsCommands: BreakpointsPanel.ICommands;
+
     /**
      * The editor services.
      */

+ 35 - 2
packages/debugger/src/tokens.ts

@@ -31,6 +31,11 @@ export interface IDebugger {
    */
   readonly isStarted: boolean;
 
+  /**
+   * Whether the session is pausing for exceptions.
+   */
+  readonly isPausingOnExceptions: boolean;
+
   /**
    * The debugger service's model.
    */
@@ -51,6 +56,16 @@ export interface IDebugger {
    */
   clearBreakpoints(): Promise<void>;
 
+  /**
+   * Used to determine if kernel has pause on exception capabilities
+   */
+  pauseOnExceptionsIsValid(): boolean;
+
+  /**
+   * Handles enabling and disabling of Pause on Exception
+   */
+  pauseOnExceptions(enable: boolean): Promise<void>;
+
   /**
    * Continues the execution of the current thread.
    */
@@ -292,10 +307,27 @@ export namespace IDebugger {
     connection: Session.ISessionConnection | null;
 
     /**
-     * Whether the debug session is started
+     * Whether the debug session is started.
      */
     readonly isStarted: boolean;
 
+    /**
+     * Whether the debug session is pausing on exceptions.
+     */
+    pausingOnExceptions: string[];
+
+    /**
+     * Whether the debug session is pausing on exceptions.
+     */
+    exceptionPaths: string[];
+
+    /**
+     * Get exception filters and default values.
+     */
+    exceptionBreakpointFilters:
+      | DebugProtocol.ExceptionBreakpointsFilter[]
+      | undefined;
+
     /**
      * Signal emitted for debug event messages.
      */
@@ -541,9 +573,10 @@ export namespace IDebugger {
          * Whether the kernel supports variable rich rendering or not.
          */
         richRendering?: boolean;
-        stoppedThreads: number[];
         tmpFilePrefix: string;
         tmpFileSuffix: string;
+        stoppedThreads: number[];
+        exceptionPaths: string[];
       };
     }
 

+ 13 - 3
packages/debugger/style/icons.css

@@ -14,14 +14,24 @@ button.jp-Button.bp3-button.bp3-minimal.jp-TreeView.jp-TableView {
 
 button.jp-Button.bp3-button.bp3-minimal.jp-TreeView:hover {
   cursor: pointer;
-  background-color: var(--jp-layout-color2);
+  background-color: var(--jp-layout-color1);
 }
 
 button.jp-Button.bp3-button.bp3-minimal.jp-TableView:hover {
   cursor: pointer;
-  background-color: var(--jp-layout-color2);
+  background-color: var(--jp-layout-color1);
 }
 
 .jp-ViewModeSelected {
-  background-color: var(--jp-layout-color2);
+  background-color: var(--jp-layout-color1);
+}
+
+/* Pause on exceptions */
+button.jp-Button.jp-mod-minimal.jp-PauseOnExceptions:hover {
+  cursor: pointer;
+  background-color: var(--jp-layout-color1);
+}
+
+button.jp-Button.jp-PauseOnExceptions.lm-mod-toggled {
+  background-color: var(--jp-layout-color1);
 }

+ 6 - 0
packages/debugger/style/icons/pause.svg

@@ -0,0 +1,6 @@
+<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <g class="jp-icon3" fill="#616161">
+        <path d="m 7,6 h 4 V 18 H 7 Z" />
+        <path d="m 13,6 h 4 v 12 h -4 z" />
+    </g>
+</svg>

+ 79 - 0
packages/debugger/test/debugger.spec.ts

@@ -105,6 +105,10 @@ describe('Debugger', () => {
         stepOut: '',
         evaluate: ''
       },
+      breakpointsCommands: {
+        registry,
+        pause: ''
+      },
       editorServices: {
         factoryService,
         mimeTypeService
@@ -155,6 +159,81 @@ describe('Debugger', () => {
     });
   });
 
+  describe('Panel', () => {
+    describe('Variable toolbar', () => {
+      let toolbar: Element;
+      beforeEach(() => {
+        toolbar = sidebar.variables.node;
+      });
+      it('should have title', () => {
+        const title = toolbar.querySelectorAll(
+          'div.jp-stack-panel-header > h2'
+        );
+        expect(title.length).toBe(1);
+        expect(title[0].innerHTML).toContain('Variables');
+      });
+      it('should have two buttons', () => {
+        const buttons = toolbar.querySelectorAll('button');
+        expect(buttons.length).toBe(2);
+        expect(buttons[0].title).toBe('Tree View');
+        expect(buttons[1].title).toBe('Table View');
+      });
+    });
+    describe('Callstack toolbar', () => {
+      let toolbar: Element;
+      beforeEach(() => {
+        toolbar = sidebar.callstack.node;
+      });
+      it('should have title', () => {
+        const title = toolbar.querySelectorAll(
+          'div.jp-stack-panel-header > h2'
+        );
+        console;
+        expect(title.length).toBe(1);
+        expect(title[0].innerHTML).toContain('Callstack');
+      });
+      it('should have six buttons', () => {
+        const buttons = toolbar.querySelectorAll('button');
+        expect(buttons.length).toBe(6);
+      });
+    });
+    describe('Breakpoints toolbar', () => {
+      let toolbar: Element;
+      beforeEach(() => {
+        toolbar = sidebar.breakpoints.node;
+      });
+      it('should have title', () => {
+        const title = toolbar.querySelectorAll(
+          'div.jp-stack-panel-header > h2'
+        );
+        expect(title.length).toBe(1);
+        expect(title[0].innerHTML).toContain('Breakpoints');
+      });
+      it('should have two buttons', () => {
+        const buttons = toolbar.querySelectorAll('button');
+        expect(buttons.length).toBe(2);
+      });
+    });
+    describe('Source toolbar', () => {
+      let toolbar: Element;
+      beforeEach(() => {
+        toolbar = sidebar.sources.node;
+      });
+      it('should have title', () => {
+        const title = toolbar.querySelectorAll(
+          'div.jp-stack-panel-header > h2'
+        );
+        expect(title.length).toBe(1);
+        expect(title[0].innerHTML).toContain('Source');
+      });
+
+      it('should have one button', () => {
+        const buttons = toolbar.querySelectorAll('button');
+        expect(buttons.length).toBe(1);
+      });
+    });
+  });
+
   describe('#callstack', () => {
     it('should have a header and a body', () => {
       expect(sidebar.callstack.widgets.length).toEqual(2);

+ 5 - 0
yarn.lock

@@ -4155,6 +4155,11 @@
   resolved "https://registry.yarnpkg.com/@verdaccio/ui-theme/-/ui-theme-3.1.0.tgz#21108f3c1b97e6db5901509d935e1f4ce475950a"
   integrity sha512-NmJOcv25/OtF84YrmYxi31beFde7rt+/y2qlnq0wYR4ZCFRE5TsuqisTVTe1OyJ8D8JwwPMyMSMSMtlMwUfqIQ==
 
+"@vscode/debugprotocol@~1.51.0":
+  version "1.51.0"
+  resolved "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.51.0.tgz#1d28a8581f8ea74b8e2fd465d4448717589a0ae3"
+  integrity sha512-39ShbKzI+0r53haLZQVEhY4XhdMJVSqfcliaDFigQjqiWattno5Ex0jXq2WRHrAtPf+W5Un9/HtED0K3pAiqZg==
+
 "@webassemblyjs/ast@1.11.1":
   version "1.11.1"
   resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"