Borys Palka 5 роки тому
батько
коміт
e498d364ce
10 змінених файлів з 514 додано та 28 видалено
  1. 4 0
      .gitignore
  2. 49 0
      README.md
  3. 12 10
      package.json
  4. 21 5
      src/index.ts
  5. 168 0
      src/session.ts
  6. 11 3
      src/sidebar.ts
  7. 137 2
      src/tokens.ts
  8. 21 7
      tests/jest.config.js
  9. 8 1
      tests/run-test.py
  10. 83 0
      tests/src/session.spec.ts

+ 4 - 0
.gitignore

@@ -28,3 +28,7 @@ junit.xml
 *.tsbuildinfo
 
 yarn.lock
+
+# xeus-python debug logs
+xpython_debug_logs
+xeus.log

+ 49 - 0
README.md

@@ -1,4 +1,53 @@
 # @jupyterlab/debugger
+
 A JupyterLab debugger UI extension
 
 This extension is under active development and is not yet available.
+
+## Prerequisites
+
+- JupyterLab 1.1+
+
+## Development
+
+```bash
+# Create a new conda environment
+conda create -n jupyterlab-debugger -c conda-forge jupyterlab nodejs xeus-python ptvsd
+
+# Activate the conda environment
+conda activate jupyterlab-debugger
+
+# Create a directory for the kernel debug logs in the folder where JupyterLab is started
+mkdir xpython_debug_logs
+
+# Install dependencies
+jlpm
+
+# Build Typescript source
+jlpm build
+
+# Link your development version of the extension with JupyterLab
+jupyter labextension link .
+
+# Rebuild Typescript source after making changes
+jlpm build
+
+# Rebuild JupyterLab after making any changes
+jupyter lab build
+
+# Start JupyterLab with the kernel logs enabled and watch move enabled
+XEUS_LOG=1 jupyter lab --no-browser --watch
+```
+
+### Tests
+
+Make sure `xeus-python` is installed and `jupyter --paths` points to where the kernel is installed.
+
+To run the tests:
+
+```bash
+# [Optional] to enable the logs for xeus-python
+export XEUS_LOG=1
+
+jlpm run test
+```

+ 12 - 10
package.json

@@ -39,22 +39,24 @@
     "watch": "tsc -b --watch"
   },
   "dependencies": {
-    "@jupyterlab/application": "^1.0.0",
-    "@jupyterlab/apputils": "^1.0.0",
-    "@jupyterlab/codeeditor": "^1.0.0",
-    "@jupyterlab/console": "^1.0.2",
-    "@jupyterlab/coreutils": "^3.0.0",
-    "@jupyterlab/fileeditor": "^1.0.2",
-    "@jupyterlab/launcher": "^1.0.0",
-    "@jupyterlab/notebook": "^1.0.0",
+    "@jupyterlab/application": "^1.1.0-alpha.1",
+    "@jupyterlab/apputils": "^1.1.0-alpha.1",
+    "@jupyterlab/codeeditor": "^1.1.0-alpha.1",
+    "@jupyterlab/console": "^1.1.0-alpha.1",
+    "@jupyterlab/coreutils": "^3.1.0-alpha.1",
+    "@jupyterlab/fileeditor": "^1.1.0-alpha.1",
+    "@jupyterlab/launcher": "^1.1.0-alpha.1",
+    "@jupyterlab/services": "^4.1.0-alpha.1",
+    "@jupyterlab/notebook": "^1.1.0-alpha.1",
     "@phosphor/coreutils": "^1.3.1",
     "@phosphor/disposable": "^1.2.0",
-    "@phosphor/widgets": "^1.8.0"
+    "@phosphor/widgets": "^1.8.0",
+    "vscode-debugprotocol": "1.35.0"
   },
   "devDependencies": {
     "@babel/core": "^7.5.5",
     "@babel/preset-env": "^7.5.5",
-    "@jupyterlab/testutils": "^1.0.2",
+    "@jupyterlab/testutils": "^1.1.0-alpha.1",
     "@types/chai": "^4.1.3",
     "@types/jest": "^24.0.17",
     "chai": "^4.2.0",

+ 21 - 5
src/index.ts

@@ -21,7 +21,7 @@ import { Debugger } from './debugger';
 
 import { DebuggerSidebar } from './sidebar';
 
-import { IDebugger } from './tokens';
+import { IDebugger, IDebuggerSidebar } from './tokens';
 
 /**
  * The command IDs used by the debugger plugin.
@@ -96,11 +96,14 @@ const notebooks: JupyterFrontEndPlugin<void> = {
 /**
  * A plugin providing a condensed sidebar UI for debugging.
  */
-const sidebar: JupyterFrontEndPlugin<void> = {
+const sidebar: JupyterFrontEndPlugin<IDebuggerSidebar> = {
   id: '@jupyterlab/debugger:sidebar',
   optional: [ILayoutRestorer],
   autoStart: true,
-  activate: (app: JupyterFrontEnd, restorer: ILayoutRestorer | null) => {
+  activate: (
+    app: JupyterFrontEnd,
+    restorer: ILayoutRestorer | null
+  ): DebuggerSidebar => {
     const { shell } = app;
     const label = 'Environment';
     const namespace = 'jp-debugger-sidebar';
@@ -113,6 +116,8 @@ const sidebar: JupyterFrontEndPlugin<void> = {
     if (restorer) {
       restorer.add(sidebar, sidebar.id);
     }
+
+    return sidebar;
   }
 };
 
@@ -121,14 +126,15 @@ const sidebar: JupyterFrontEndPlugin<void> = {
  */
 const tracker: JupyterFrontEndPlugin<IDebugger> = {
   id: '@jupyterlab/debugger:tracker',
-  optional: [ILayoutRestorer],
+  optional: [ILayoutRestorer, IDebuggerSidebar],
   requires: [IStateDB],
   provides: IDebugger,
   autoStart: true,
   activate: (
     app: JupyterFrontEnd,
     state: IStateDB,
-    restorer: ILayoutRestorer | null
+    restorer: ILayoutRestorer | null,
+    sidebar: IDebuggerSidebar | null
   ): IDebugger => {
     const tracker = new WidgetTracker<MainAreaWidget<Debugger>>({
       namespace: 'debugger'
@@ -138,6 +144,10 @@ const tracker: JupyterFrontEndPlugin<IDebugger> = {
       execute: args => {
         const id = (args.id as string) || '';
 
+        if (id) {
+          console.log('Debugger ID: ', id);
+        }
+
         if (tracker.find(widget => id === widget.content.model.id)) {
           return;
         }
@@ -161,6 +171,12 @@ const tracker: JupyterFrontEndPlugin<IDebugger> = {
       });
     }
 
+    if (sidebar) {
+      tracker.currentChanged.connect((_, current) => {
+        sidebar.model = current ? current.content.model : null;
+      });
+    }
+
     return tracker;
   }
 };

+ 168 - 0
src/session.ts

@@ -0,0 +1,168 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import { IClientSession } from '@jupyterlab/apputils';
+
+import { CodeEditor } from '@jupyterlab/codeeditor';
+
+import { KernelMessage } from '@jupyterlab/services';
+
+import { PromiseDelegate } from '@phosphor/coreutils';
+
+import { ISignal, Signal } from '@phosphor/signaling';
+
+import { IDebugger } from './tokens';
+
+export class DebugSession implements IDebugger.ISession {
+  /**
+   * Instantiate a new debug session
+   *
+   * @param options - The debug session instantiation options.
+   */
+  constructor(options: DebugSession.IOptions) {
+    this.client = options.client;
+    this.client.iopubMessage.connect(this._handleEvent, this);
+  }
+
+  /**
+   * The client session to connect to a debugger.
+   */
+  client: IClientSession;
+
+  /**
+   * The code editors in a debugger session.
+   */
+  editors: CodeEditor.IEditor[];
+
+  /**
+   * Dispose the debug session.
+   */
+  dispose(): void {
+    if (this.isDisposed) {
+      return;
+    }
+    this._isDisposed = true;
+    this._disposed.emit();
+    Signal.clearData(this);
+  }
+
+  /**
+   * A signal emitted when the debug session is disposed.
+   */
+  get disposed(): ISignal<this, void> {
+    return this._disposed;
+  }
+
+  /**
+   * Whether the debug session is disposed.
+   */
+  get isDisposed(): boolean {
+    return this._isDisposed;
+  }
+
+  /**
+   * Start a new debug session
+   */
+  async start(): Promise<void> {
+    await this.sendRequest('initialize', {
+      clientID: 'jupyterlab',
+      clientName: 'JupyterLab',
+      adapterID: 'python',
+      pathFormat: 'path',
+      linesStartAt1: true,
+      columnsStartAt1: true,
+      supportsVariableType: true,
+      supportsVariablePaging: true,
+      supportsRunInTerminalRequest: true,
+      locale: 'en-us'
+    });
+
+    await this.sendRequest('attach', {});
+  }
+
+  /**
+   * Stop the running debug session.
+   */
+  async stop(): Promise<void> {
+    await this.sendRequest('disconnect', {
+      restart: false,
+      terminateDebuggee: true
+    });
+  }
+
+  /**
+   * Send a custom debug request to the kernel.
+   * @param command debug command.
+   * @param args arguments for the debug command.
+   */
+  async sendRequest<K extends keyof IDebugger.ISession.Request>(
+    command: K,
+    args: IDebugger.ISession.Request[K]
+  ): Promise<IDebugger.ISession.Response[K]> {
+    const message = await this._sendDebugMessage({
+      type: 'request',
+      seq: this._seq++,
+      command,
+      arguments: args
+    });
+    return message.content as IDebugger.ISession.Response[K];
+  }
+
+  /**
+   * Signal emitted for debug event messages.
+   */
+  get eventMessage(): ISignal<DebugSession, IDebugger.ISession.Event> {
+    return this._eventMessage;
+  }
+
+  /**
+   * Handle debug events sent on the 'iopub' channel.
+   */
+  private _handleEvent(
+    sender: IClientSession,
+    message: KernelMessage.IIOPubMessage
+  ): void {
+    const msgType = message.header.msg_type;
+    if (msgType !== 'debug_event') {
+      return;
+    }
+    const event = message.content as IDebugger.ISession.Event;
+    this._eventMessage.emit(event);
+  }
+
+  /**
+   * Send a debug request message to the kernel.
+   * @param msg debug request message to send to the kernel.
+   */
+  private async _sendDebugMessage(
+    msg: KernelMessage.IDebugRequestMsg['content']
+  ): Promise<KernelMessage.IDebugReplyMsg> {
+    const reply = new PromiseDelegate<KernelMessage.IDebugReplyMsg>();
+    const kernel = this.client.kernel;
+    const future = kernel.requestDebug(msg);
+    future.onReply = (msg: KernelMessage.IDebugReplyMsg) => {
+      return reply.resolve(msg);
+    };
+    await future.done;
+    return reply.promise;
+  }
+
+  private _disposed = new Signal<this, void>(this);
+  private _isDisposed: boolean = false;
+  private _eventMessage = new Signal<DebugSession, IDebugger.ISession.Event>(
+    this
+  );
+  private _seq: number = 0;
+}
+
+/**
+ * A namespace for `DebugSession` statics.
+ */
+export namespace DebugSession {
+  export interface IOptions {
+    /**
+     * The client session used by the debug session.
+     */
+    client: IClientSession;
+  }
+}

+ 11 - 3
src/sidebar.ts

@@ -9,7 +9,6 @@ import { CallstackWidget } from './callstack';
 import { BreakPointsWidget } from './breakpoints';
 
 export class DebuggerSidebar extends SplitPanel {
-
   variables: VariablesWidget;
   callstack: CallstackWidget;
   breakPoints: BreakPointsWidget;
@@ -29,7 +28,16 @@ export class DebuggerSidebar extends SplitPanel {
     this.addWidget(this.breakPoints);
   }
 
+  get model(): Debugger.Model | null {
+    return this._model;
+  }
+  set model(model: Debugger.Model | null) {
+    if (this._model === model) {
+      return;
+    }
+    this._model = model;
+    this.update();
+  }
 
-
-  public model: Debugger.Model | null = null;
+  private _model: Debugger.Model | null = null;
 }

+ 137 - 2
src/tokens.ts

@@ -13,13 +13,16 @@ import { Token } from '@phosphor/coreutils';
 
 import { IObservableDisposable } from '@phosphor/disposable';
 
+import { DebugProtocol } from 'vscode-debugprotocol';
+
 import { Debugger } from './debugger';
 
+import { DebuggerSidebar } from './sidebar';
+
 /**
  * An interface describing an application's visual debugger.
  */
-export interface IDebugger
-  extends IWidgetTracker<MainAreaWidget<Debugger>> {}
+export interface IDebugger extends IWidgetTracker<MainAreaWidget<Debugger>> {}
 
 /**
  * A namespace for visual debugger types.
@@ -38,6 +41,126 @@ export namespace IDebugger {
      * The code editors in a debugger session.
      */
     editors: CodeEditor.IEditor[];
+
+    /**
+     * Start a new debug session.
+     */
+    start(): void;
+
+    /**
+     * Stop a running debug session.
+     */
+    stop(): void;
+  }
+
+  export namespace ISession {
+    /**
+     * Arguments for 'updateCell' request.
+     * This is an addition to the Debug Adapter Protocol to support
+     * setting breakpoints for cells
+     */
+    export interface IUpdateCellArguments {
+      cellId: number;
+      nextId: number;
+      code: string;
+    }
+
+    /**
+     * Response to 'updateCell' request.
+     * This is an addition to the Debug Adapter Protocol to support
+     * setting breakpoints for cells
+     */
+    export interface IUpdateCellResponse extends DebugProtocol.Response {
+      body: {
+        sourcePath: string;
+      };
+    }
+
+    /**
+     * Expose all the debug requests types.
+     */
+    export type Request = {
+      attach: DebugProtocol.AttachRequestArguments;
+      completions: DebugProtocol.CompletionsArguments;
+      configurationDone: DebugProtocol.ConfigurationDoneArguments;
+      continue: DebugProtocol.ContinueArguments;
+      disconnect: DebugProtocol.DisconnectArguments;
+      evaluate: DebugProtocol.EvaluateArguments;
+      exceptionInfo: DebugProtocol.ExceptionInfoArguments;
+      goto: DebugProtocol.GotoArguments;
+      gotoTargets: DebugProtocol.GotoTargetsArguments;
+      initialize: DebugProtocol.InitializeRequestArguments;
+      launch: DebugProtocol.LaunchRequestArguments;
+      loadedSources: DebugProtocol.LoadedSourcesArguments;
+      modules: DebugProtocol.ModulesArguments;
+      next: DebugProtocol.NextArguments;
+      pause: DebugProtocol.PauseArguments;
+      restart: DebugProtocol.RestartArguments;
+      restartFrame: DebugProtocol.RestartFrameArguments;
+      reverseContinue: DebugProtocol.ReverseContinueArguments;
+      scopes: DebugProtocol.ScopesArguments;
+      setBreakpoints: DebugProtocol.SetBreakpointsArguments;
+      setExceptionBreakpoints: DebugProtocol.SetExceptionBreakpointsArguments;
+      setExpression: DebugProtocol.SetExpressionArguments;
+      setFunctionBreakpoints: DebugProtocol.SetFunctionBreakpointsArguments;
+      setVariable: DebugProtocol.SetVariableArguments;
+      source: DebugProtocol.SourceArguments;
+      stackTrace: DebugProtocol.StackTraceArguments;
+      stepBack: DebugProtocol.StepBackArguments;
+      stepIn: DebugProtocol.StepInArguments;
+      stepInTargets: DebugProtocol.StepInTargetsArguments;
+      stepOut: DebugProtocol.StepOutArguments;
+      terminate: DebugProtocol.TerminateArguments;
+      terminateThreads: DebugProtocol.TerminateThreadsArguments;
+      threads: {};
+      updateCell: IUpdateCellArguments;
+    };
+
+    /**
+     * Expose all the debug response types.
+     */
+    export type Response = {
+      attach: DebugProtocol.AttachResponse;
+      completions: DebugProtocol.CompletionsResponse;
+      configurationDone: DebugProtocol.ConfigurationDoneResponse;
+      continue: DebugProtocol.ContinueResponse;
+      disconnect: DebugProtocol.DisconnectResponse;
+      evaluate: DebugProtocol.EvaluateResponse;
+      exceptionInfo: DebugProtocol.ExceptionInfoResponse;
+      goto: DebugProtocol.GotoResponse;
+      gotoTargets: DebugProtocol.GotoTargetsResponse;
+      initialize: DebugProtocol.InitializeResponse;
+      launch: DebugProtocol.LaunchResponse;
+      loadedSources: DebugProtocol.LoadedSourcesResponse;
+      modules: DebugProtocol.ModulesResponse;
+      next: DebugProtocol.NextResponse;
+      pause: DebugProtocol.PauseResponse;
+      restart: DebugProtocol.RestartResponse;
+      restartFrame: DebugProtocol.RestartFrameResponse;
+      reverseContinue: DebugProtocol.ReverseContinueResponse;
+      scopes: DebugProtocol.ScopesResponse;
+      setBreakpoints: DebugProtocol.SetBreakpointsResponse;
+      setExceptionBreakpoints: DebugProtocol.SetExceptionBreakpointsResponse;
+      setExpression: DebugProtocol.SetExpressionResponse;
+      setFunctionBreakpoints: DebugProtocol.SetFunctionBreakpointsResponse;
+      setVariable: DebugProtocol.SetVariableResponse;
+      source: DebugProtocol.SourceResponse;
+      stackTrace: DebugProtocol.StackTraceResponse;
+      stepBack: DebugProtocol.StepBackResponse;
+      stepIn: DebugProtocol.StepInResponse;
+      stepInTargets: DebugProtocol.StepInTargetsResponse;
+      stepOut: DebugProtocol.StepOutResponse;
+      terminate: DebugProtocol.TerminateResponse;
+      terminateThreads: DebugProtocol.TerminateThreadsResponse;
+      threads: DebugProtocol.ThreadsResponse;
+      updateCell: IUpdateCellResponse;
+      variables: DebugProtocol.VariablesResponse;
+    };
+
+    /**
+     * A generic debug event.
+     */
+    export type Event = DebugProtocol.Event;
   }
 }
 
@@ -45,3 +168,15 @@ export namespace IDebugger {
  * A token for a tracker for an application's visual debugger instances.
  */
 export const IDebugger = new Token<IDebugger>('@jupyterlab/debugger');
+
+/**
+ * An interface describing an application's visual debugger.
+ */
+export interface IDebuggerSidebar extends DebuggerSidebar {}
+
+/**
+ * A token for a tracker for an application's visual debugger condensed sidebar.
+ */
+export const IDebuggerSidebar = new Token<IDebuggerSidebar>(
+  '@jupyterlab/debugger-sidebar'
+);

+ 21 - 7
tests/jest.config.js

@@ -1,13 +1,27 @@
-module.exports = {
-  preset: "ts-jest/presets/js-with-babel",
-  moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
-  transformIgnorePatterns: ["/node_modules/(?!(@jupyterlab/.*)/)"],
+const func = require('@jupyterlab/testutils/lib/jest-config');
+const upstream = func('@jupyterlab/debugger', __dirname);
+
+let local = {
+  preset: 'ts-jest/presets/js-with-babel',
+  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
+  transformIgnorePatterns: ['/node_modules/(?!(@jupyterlab/.*)/)'],
   globals: {
-    "ts-jest": {
-      tsConfig: "./tsconfig.json"
+    'ts-jest': {
+      tsConfig: './tsconfig.json'
     }
   },
   transform: {
-    "\\.(ts|tsx)?$": "ts-jest"
+    '\\.(ts|tsx)?$': 'ts-jest'
   }
 };
+
+[
+  'moduleNameMapper',
+  'setupFilesAfterEnv',
+  'setupFiles',
+  'moduleFileExtensions'
+].forEach(option => {
+  local[option] = upstream[option];
+});
+
+module.exports = local;

+ 8 - 1
tests/run-test.py

@@ -1,7 +1,14 @@
+# Copyright (c) Jupyter Development Team.
+# Distributed under the terms of the Modified BSD License.
+
 import os
-from jupyterlab.tests.test_app import run_jest
+from jupyterlab.tests.test_app import run_jest, JestApp
 
 HERE = os.path.realpath(os.path.dirname(__file__))
 
 if __name__ == '__main__':
+    # xeus-python requires the xpython_debug_logs folder
+    jest_app = JestApp.instance()
+    os.mkdir(os.path.join(jest_app.notebook_dir, 'xpython_debug_logs'))
+
     run_jest(HERE)

+ 83 - 0
tests/src/session.spec.ts

@@ -0,0 +1,83 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import { expect } from 'chai';
+
+import { ClientSession, IClientSession } from '@jupyterlab/apputils';
+
+import { createClientSession } from '@jupyterlab/testutils';
+
+import { DebugSession } from '../../lib/session';
+
+describe('DebugSession', () => {
+  let client: IClientSession;
+
+  beforeEach(async () => {
+    client = await createClientSession({
+      kernelPreference: {
+        name: 'xpython'
+      }
+    });
+    await (client as ClientSession).initialize();
+    await client.kernel.ready;
+  });
+
+  afterEach(async () => {
+    await client.shutdown();
+  });
+
+  describe('#isDisposed', () => {
+    it('should return whether the object is disposed', () => {
+      const debugSession = new DebugSession({ client });
+      expect(debugSession.isDisposed).to.equal(false);
+      debugSession.dispose();
+      expect(debugSession.isDisposed).to.equal(true);
+    });
+  });
+
+  describe('#eventMessage', () => {
+    it('should be emitted when sending debug messages', async () => {
+      const debugSession = new DebugSession({ client });
+      let events: string[] = [];
+      debugSession.eventMessage.connect((sender, event) => {
+        events.push(event.event);
+      });
+      await debugSession.start();
+      await debugSession.stop();
+      expect(events).to.deep.equal(['output', 'initialized', 'process']);
+    });
+  });
+
+  describe('#sendRequest', () => {
+    let debugSession: DebugSession;
+
+    beforeEach(async () => {
+      debugSession = new DebugSession({ client });
+      await debugSession.start();
+    });
+
+    afterEach(async () => {
+      await debugSession.stop();
+      debugSession.dispose();
+    });
+
+    it('should send debug messages to the kernel', async () => {
+      const code = 'i=0\ni+=1\ni+=1';
+      const reply = await debugSession.sendRequest('updateCell', {
+        cellId: 0,
+        nextId: 1,
+        code
+      });
+      expect(reply.body.sourcePath).to.contain('.py');
+    });
+
+    it('should handle replies with success false', async () => {
+      const reply = await debugSession.sendRequest('evaluate', {
+        expression: 'a'
+      });
+      const { success, message } = reply;
+      expect(success).to.be.false;
+      expect(message).to.contain('Unable to find thread for evaluation');
+    });
+  });
+});