handler.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { JupyterFrontEnd } from '@jupyterlab/application';
  4. import {
  5. ISessionContext,
  6. SessionContext,
  7. ToolbarButton
  8. } from '@jupyterlab/apputils';
  9. import { ConsolePanel } from '@jupyterlab/console';
  10. import { IChangedArgs } from '@jupyterlab/coreutils';
  11. import { DocumentWidget } from '@jupyterlab/docregistry';
  12. import { FileEditor } from '@jupyterlab/fileeditor';
  13. import { NotebookPanel } from '@jupyterlab/notebook';
  14. import { Kernel, Session } from '@jupyterlab/services';
  15. import { bugIcon } from '@jupyterlab/ui-components';
  16. import { DisposableSet } from '@lumino/disposable';
  17. import { Debugger } from './debugger';
  18. import { IDebugger } from './tokens';
  19. import { ConsoleHandler } from './handlers/console';
  20. import { FileHandler } from './handlers/file';
  21. import { NotebookHandler } from './handlers/notebook';
  22. /**
  23. * Add a button to the widget toolbar to enable and disable debugging.
  24. *
  25. * @param widget The widget to add the debug toolbar button to.
  26. * @param onClick The callback when the toolbar button is clicked.
  27. */
  28. function updateToolbar(
  29. widget: DebuggerHandler.SessionWidget[DebuggerHandler.SessionType],
  30. onClick: () => void
  31. ): DisposableSet {
  32. const icon = new ToolbarButton({
  33. className: 'jp-DebuggerBugButton',
  34. icon: bugIcon,
  35. tooltip: 'Enable / Disable Debugger',
  36. onClick
  37. });
  38. widget.toolbar.addItem('debugger-icon', icon);
  39. const button = new ToolbarButton({
  40. iconClass: 'jp-ToggleSwitch',
  41. tooltip: 'Enable / Disable Debugger',
  42. onClick
  43. });
  44. widget.toolbar.addItem('debugger-button', button);
  45. const elements = new DisposableSet();
  46. elements.add(icon);
  47. elements.add(button);
  48. return elements;
  49. }
  50. /**
  51. * A handler for debugging a widget.
  52. */
  53. export class DebuggerHandler {
  54. /**
  55. * Instantiate a new DebuggerHandler.
  56. *
  57. * @param options The instantiation options for a DebuggerHandler.
  58. */
  59. constructor(options: DebuggerHandler.IOptions) {
  60. this._type = options.type;
  61. this._shell = options.shell;
  62. this._service = options.service;
  63. }
  64. /**
  65. * Update a debug handler for the given widget, and
  66. * handle kernel changed events.
  67. *
  68. * @param widget The widget to update.
  69. * @param connection The session connection.
  70. */
  71. async update(
  72. widget: DebuggerHandler.SessionWidget[DebuggerHandler.SessionType],
  73. connection: Session.ISessionConnection | null
  74. ): Promise<void> {
  75. if (!connection) {
  76. delete this._kernelChangedHandlers[widget.id];
  77. delete this._statusChangedHandlers[widget.id];
  78. return this._update(widget, connection);
  79. }
  80. const kernelChanged = (): void => {
  81. void this._update(widget, connection);
  82. };
  83. const kernelChangedHandler = this._kernelChangedHandlers[widget.id];
  84. if (kernelChangedHandler) {
  85. connection.kernelChanged.disconnect(kernelChangedHandler);
  86. }
  87. this._kernelChangedHandlers[widget.id] = kernelChanged;
  88. connection.kernelChanged.connect(kernelChanged);
  89. const statusChanged = (
  90. _: Session.ISessionConnection,
  91. status: Kernel.Status
  92. ): void => {
  93. if (status.endsWith('restarting')) {
  94. void this._update(widget, connection);
  95. }
  96. };
  97. const statusChangedHandler = this._statusChangedHandlers[widget.id];
  98. if (statusChangedHandler) {
  99. connection.statusChanged.disconnect(statusChangedHandler);
  100. }
  101. connection.statusChanged.connect(statusChanged);
  102. this._statusChangedHandlers[widget.id] = statusChanged;
  103. return this._update(widget, connection);
  104. }
  105. /**
  106. * Update a debug handler for the given widget, and
  107. * handle connection kernel changed events.
  108. *
  109. * @param widget The widget to update.
  110. * @param sessionContext The session context.
  111. */
  112. async updateContext(
  113. widget: DebuggerHandler.SessionWidget[DebuggerHandler.SessionType],
  114. sessionContext: ISessionContext
  115. ): Promise<void> {
  116. const connectionChanged = (): void => {
  117. const { session: connection } = sessionContext;
  118. void this.update(widget, connection);
  119. };
  120. const contextKernelChangedHandlers = this._contextKernelChangedHandlers[
  121. widget.id
  122. ];
  123. if (contextKernelChangedHandlers) {
  124. sessionContext.kernelChanged.disconnect(contextKernelChangedHandlers);
  125. }
  126. this._contextKernelChangedHandlers[widget.id] = connectionChanged;
  127. sessionContext.kernelChanged.connect(connectionChanged);
  128. return this.update(widget, sessionContext.session);
  129. }
  130. /**
  131. * Update a debug handler for the given widget.
  132. *
  133. * @param widget The widget to update.
  134. * @param connection The session connection.
  135. */
  136. private async _update(
  137. widget: DebuggerHandler.SessionWidget[DebuggerHandler.SessionType],
  138. connection: Session.ISessionConnection | null
  139. ): Promise<void> {
  140. if (!this._service.model || !connection) {
  141. return;
  142. }
  143. const hasFocus = (): boolean => {
  144. return this._shell.currentWidget === widget;
  145. };
  146. const updateAttribute = (): void => {
  147. if (!this._handlers[widget.id]) {
  148. widget.node.removeAttribute('data-jp-debugger');
  149. return;
  150. }
  151. widget.node.setAttribute('data-jp-debugger', 'true');
  152. };
  153. const createHandler = (): void => {
  154. if (this._handlers[widget.id]) {
  155. return;
  156. }
  157. switch (this._type) {
  158. case 'notebook':
  159. this._handlers[widget.id] = new NotebookHandler({
  160. debuggerService: this._service,
  161. widget: widget as NotebookPanel
  162. });
  163. break;
  164. case 'console':
  165. this._handlers[widget.id] = new ConsoleHandler({
  166. debuggerService: this._service,
  167. widget: widget as ConsolePanel
  168. });
  169. break;
  170. case 'file':
  171. this._handlers[widget.id] = new FileHandler({
  172. debuggerService: this._service,
  173. widget: widget as DocumentWidget<FileEditor>
  174. });
  175. break;
  176. default:
  177. throw Error(`No handler for the type ${this._type}`);
  178. }
  179. updateAttribute();
  180. };
  181. const removeHandlers = (): void => {
  182. const handler = this._handlers[widget.id];
  183. if (!handler) {
  184. return;
  185. }
  186. handler.dispose();
  187. delete this._handlers[widget.id];
  188. delete this._kernelChangedHandlers[widget.id];
  189. delete this._statusChangedHandlers[widget.id];
  190. delete this._contextKernelChangedHandlers[widget.id];
  191. // Clear the model if the handler being removed corresponds
  192. // to the current active debug session, or if the connection
  193. // does not have a kernel.
  194. if (
  195. this._service.session?.connection?.path === connection?.path ||
  196. !this._service.session?.connection?.kernel
  197. ) {
  198. const model = this._service.model;
  199. model.clear();
  200. }
  201. updateAttribute();
  202. };
  203. const addToolbarButton = (): void => {
  204. const button = this._buttons[widget.id];
  205. if (button) {
  206. return;
  207. }
  208. const newButton = updateToolbar(widget, toggleDebugging);
  209. this._buttons[widget.id] = newButton;
  210. };
  211. const removeToolbarButton = (): void => {
  212. const button = this._buttons[widget.id];
  213. if (!button) {
  214. return;
  215. }
  216. button.dispose();
  217. delete this._buttons[widget.id];
  218. };
  219. const toggleDebugging = async (): Promise<void> => {
  220. // bail if the widget doesn't have focus
  221. if (!hasFocus()) {
  222. return;
  223. }
  224. if (
  225. this._service.isStarted &&
  226. this._previousConnection?.id === connection?.id
  227. ) {
  228. this._service.session!.connection = connection;
  229. await this._service.stop();
  230. removeHandlers();
  231. } else {
  232. this._service.session!.connection = connection;
  233. this._previousConnection = connection;
  234. await this._service.restoreState(true);
  235. createHandler();
  236. }
  237. };
  238. const debuggingEnabled = await this._service.isAvailable(connection);
  239. if (!debuggingEnabled) {
  240. removeHandlers();
  241. removeToolbarButton();
  242. return;
  243. }
  244. // update the active debug session
  245. if (!this._service.session) {
  246. this._service.session = new Debugger.Session({ connection });
  247. } else {
  248. this._previousConnection = this._service.session!.connection?.kernel
  249. ? this._service.session.connection
  250. : null;
  251. this._service.session.connection = connection;
  252. }
  253. await this._service.restoreState(false);
  254. addToolbarButton();
  255. // check the state of the debug session
  256. if (!this._service.isStarted) {
  257. removeHandlers();
  258. this._service.session.connection = this._previousConnection ?? connection;
  259. await this._service.restoreState(false);
  260. return;
  261. }
  262. // if the debugger is started but there is no handler, create a new one
  263. createHandler();
  264. this._previousConnection = connection;
  265. // listen to the disposed signals
  266. widget.disposed.connect(removeHandlers);
  267. }
  268. private _type: DebuggerHandler.SessionType;
  269. private _shell: JupyterFrontEnd.IShell;
  270. private _service: IDebugger;
  271. private _previousConnection: Session.ISessionConnection | null;
  272. private _handlers: {
  273. [id: string]: DebuggerHandler.SessionHandler[DebuggerHandler.SessionType];
  274. } = {};
  275. private _contextKernelChangedHandlers: {
  276. [id: string]: (
  277. sender: SessionContext,
  278. args: IChangedArgs<
  279. Kernel.IKernelConnection,
  280. Kernel.IKernelConnection,
  281. 'kernel'
  282. >
  283. ) => void;
  284. } = {};
  285. private _kernelChangedHandlers: {
  286. [id: string]: (
  287. sender: Session.ISessionConnection,
  288. args: IChangedArgs<
  289. Kernel.IKernelConnection,
  290. Kernel.IKernelConnection,
  291. 'kernel'
  292. >
  293. ) => void;
  294. } = {};
  295. private _statusChangedHandlers: {
  296. [id: string]: (
  297. sender: Session.ISessionConnection,
  298. status: Kernel.Status
  299. ) => void;
  300. } = {};
  301. private _buttons: { [id: string]: DisposableSet } = {};
  302. }
  303. /**
  304. * A namespace for DebuggerHandler `statics`
  305. */
  306. export namespace DebuggerHandler {
  307. /**
  308. * Instantiation options for a DebuggerHandler.
  309. */
  310. export interface IOptions {
  311. /**
  312. * The type of session.
  313. */
  314. type: SessionType;
  315. /**
  316. * The application shell.
  317. */
  318. shell: JupyterFrontEnd.IShell;
  319. /**
  320. * The debugger service.
  321. */
  322. service: IDebugger;
  323. }
  324. /**
  325. * The types of sessions that can be debugged.
  326. */
  327. export type SessionType = keyof SessionHandler;
  328. /**
  329. * The types of handlers.
  330. */
  331. export type SessionHandler = {
  332. notebook: NotebookHandler;
  333. console: ConsoleHandler;
  334. file: FileHandler;
  335. };
  336. /**
  337. * The types of widgets that can be debugged.
  338. */
  339. export type SessionWidget = {
  340. notebook: NotebookPanel;
  341. console: ConsolePanel;
  342. file: DocumentWidget;
  343. };
  344. }