handler.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  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, KernelMessage, Session } from '@jupyterlab/services';
  15. import { ITranslator, nullTranslator } from '@jupyterlab/translation';
  16. import { bugDotIcon, bugIcon } from '@jupyterlab/ui-components';
  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. const TOOLBAR_DEBUGGER_ITEM = 'debugger-icon';
  23. /**
  24. * Add a bug icon to the widget toolbar to enable and disable debugging.
  25. *
  26. * @param widget The widget to add the debug toolbar button to.
  27. * @param onClick The callback when the toolbar button is clicked.
  28. */
  29. function updateIconButton(
  30. widget: DebuggerHandler.SessionWidget[DebuggerHandler.SessionType],
  31. onClick: () => void,
  32. enabled?: boolean,
  33. pressed?: boolean,
  34. translator: ITranslator = nullTranslator
  35. ): ToolbarButton {
  36. const trans = translator.load('jupyterlab');
  37. const icon = new ToolbarButton({
  38. className: 'jp-DebuggerBugButton',
  39. icon: bugIcon,
  40. tooltip: trans.__('Enable Debugger'),
  41. pressedIcon: bugDotIcon,
  42. pressedTooltip: trans.__('Disable Debugger'),
  43. disabledTooltip: trans.__(
  44. 'Select a kernel that supports debugging to enable debugger'
  45. ),
  46. enabled,
  47. pressed,
  48. onClick
  49. });
  50. if (!widget.toolbar.insertBefore('kernelName', TOOLBAR_DEBUGGER_ITEM, icon)) {
  51. widget.toolbar.addItem(TOOLBAR_DEBUGGER_ITEM, icon);
  52. }
  53. return icon;
  54. }
  55. /**
  56. * Updates button state to on/off,
  57. * adds/removes css class to update styling
  58. *
  59. * @param widget the debug button widget
  60. * @param pressed true if pressed, false otherwise
  61. * @param enabled true if widget enabled, false otherwise
  62. * @param onClick click handler
  63. */
  64. function updateIconButtonState(
  65. widget: ToolbarButton,
  66. pressed: boolean,
  67. enabled: boolean = true,
  68. onClick?: () => void
  69. ) {
  70. if (widget) {
  71. widget.enabled = enabled;
  72. widget.pressed = pressed;
  73. if (onClick) {
  74. widget.onClick = onClick;
  75. }
  76. }
  77. }
  78. /**
  79. * A handler for debugging a widget.
  80. */
  81. export class DebuggerHandler implements DebuggerHandler.IHandler {
  82. /**
  83. * Instantiate a new DebuggerHandler.
  84. *
  85. * @param options The instantiation options for a DebuggerHandler.
  86. */
  87. constructor(options: DebuggerHandler.IOptions) {
  88. this._type = options.type;
  89. this._shell = options.shell;
  90. this._service = options.service;
  91. }
  92. /**
  93. * Get the active widget.
  94. */
  95. get activeWidget():
  96. | DebuggerHandler.SessionWidget[DebuggerHandler.SessionType]
  97. | null {
  98. return this._activeWidget;
  99. }
  100. /**
  101. * Update a debug handler for the given widget, and
  102. * handle kernel changed events.
  103. *
  104. * @param widget The widget to update.
  105. * @param connection The session connection.
  106. */
  107. async update(
  108. widget: DebuggerHandler.SessionWidget[DebuggerHandler.SessionType],
  109. connection: Session.ISessionConnection | null
  110. ): Promise<void> {
  111. if (!connection) {
  112. delete this._kernelChangedHandlers[widget.id];
  113. delete this._statusChangedHandlers[widget.id];
  114. delete this._iopubMessageHandlers[widget.id];
  115. return this.updateWidget(widget, connection);
  116. }
  117. const kernelChanged = (): void => {
  118. void this.updateWidget(widget, connection);
  119. };
  120. const kernelChangedHandler = this._kernelChangedHandlers[widget.id];
  121. if (kernelChangedHandler) {
  122. connection.kernelChanged.disconnect(kernelChangedHandler);
  123. }
  124. this._kernelChangedHandlers[widget.id] = kernelChanged;
  125. connection.kernelChanged.connect(kernelChanged);
  126. const statusChanged = (
  127. _: Session.ISessionConnection,
  128. status: Kernel.Status
  129. ): void => {
  130. // FIXME-TRANS: Localizable?
  131. if (status.endsWith('restarting')) {
  132. void this.updateWidget(widget, connection);
  133. }
  134. };
  135. const statusChangedHandler = this._statusChangedHandlers[widget.id];
  136. if (statusChangedHandler) {
  137. connection.statusChanged.disconnect(statusChangedHandler);
  138. }
  139. connection.statusChanged.connect(statusChanged);
  140. this._statusChangedHandlers[widget.id] = statusChanged;
  141. const iopubMessage = (
  142. _: Session.ISessionConnection,
  143. msg: KernelMessage.IIOPubMessage
  144. ): void => {
  145. if (
  146. msg.parent_header != {} &&
  147. (msg.parent_header as KernelMessage.IHeader).msg_type ==
  148. 'execute_request' &&
  149. this._service.isStarted &&
  150. !this._service.hasStoppedThreads()
  151. ) {
  152. void this._service.displayDefinedVariables();
  153. }
  154. };
  155. const iopubMessageHandler = this._iopubMessageHandlers[widget.id];
  156. if (iopubMessageHandler) {
  157. connection.iopubMessage.disconnect(iopubMessageHandler);
  158. }
  159. connection.iopubMessage.connect(iopubMessage);
  160. this._iopubMessageHandlers[widget.id] = iopubMessage;
  161. this._activeWidget = widget;
  162. return this.updateWidget(widget, connection);
  163. }
  164. /**
  165. * Update a debug handler for the given widget, and
  166. * handle connection kernel changed events.
  167. *
  168. * @param widget The widget to update.
  169. * @param sessionContext The session context.
  170. */
  171. async updateContext(
  172. widget: DebuggerHandler.SessionWidget[DebuggerHandler.SessionType],
  173. sessionContext: ISessionContext
  174. ): Promise<void> {
  175. const connectionChanged = (): void => {
  176. const { session: connection } = sessionContext;
  177. void this.update(widget, connection);
  178. };
  179. const contextKernelChangedHandlers = this._contextKernelChangedHandlers[
  180. widget.id
  181. ];
  182. if (contextKernelChangedHandlers) {
  183. sessionContext.kernelChanged.disconnect(contextKernelChangedHandlers);
  184. }
  185. this._contextKernelChangedHandlers[widget.id] = connectionChanged;
  186. sessionContext.kernelChanged.connect(connectionChanged);
  187. return this.update(widget, sessionContext.session);
  188. }
  189. /**
  190. * Update a debug handler for the given widget.
  191. *
  192. * @param widget The widget to update.
  193. * @param connection The session connection.
  194. */
  195. async updateWidget(
  196. widget: DebuggerHandler.SessionWidget[DebuggerHandler.SessionType],
  197. connection: Session.ISessionConnection | null
  198. ): Promise<void> {
  199. if (!this._service.model || !connection) {
  200. return;
  201. }
  202. const hasFocus = (): boolean => {
  203. return this._shell.currentWidget === widget;
  204. };
  205. const updateAttribute = (): void => {
  206. if (!this._handlers[widget.id]) {
  207. widget.node.removeAttribute('data-jp-debugger');
  208. return;
  209. }
  210. widget.node.setAttribute('data-jp-debugger', 'true');
  211. };
  212. const createHandler = (): void => {
  213. if (this._handlers[widget.id]) {
  214. return;
  215. }
  216. switch (this._type) {
  217. case 'notebook':
  218. this._handlers[widget.id] = new NotebookHandler({
  219. debuggerService: this._service,
  220. widget: widget as NotebookPanel
  221. });
  222. break;
  223. case 'console':
  224. this._handlers[widget.id] = new ConsoleHandler({
  225. debuggerService: this._service,
  226. widget: widget as ConsolePanel
  227. });
  228. break;
  229. case 'file':
  230. this._handlers[widget.id] = new FileHandler({
  231. debuggerService: this._service,
  232. widget: widget as DocumentWidget<FileEditor>
  233. });
  234. break;
  235. default:
  236. throw Error(`No handler for the type ${this._type}`);
  237. }
  238. updateAttribute();
  239. };
  240. const removeHandlers = (): void => {
  241. const handler = this._handlers[widget.id];
  242. if (!handler) {
  243. return;
  244. }
  245. handler.dispose();
  246. delete this._handlers[widget.id];
  247. delete this._kernelChangedHandlers[widget.id];
  248. delete this._statusChangedHandlers[widget.id];
  249. delete this._iopubMessageHandlers[widget.id];
  250. delete this._contextKernelChangedHandlers[widget.id];
  251. // Clear the model if the handler being removed corresponds
  252. // to the current active debug session, or if the connection
  253. // does not have a kernel.
  254. if (
  255. this._service.session?.connection?.path === connection?.path ||
  256. !this._service.session?.connection?.kernel
  257. ) {
  258. const model = this._service.model;
  259. model.clear();
  260. }
  261. updateAttribute();
  262. };
  263. const addToolbarButton = (enabled: boolean = true): void => {
  264. const debugButton = this._iconButtons[widget.id];
  265. if (!debugButton) {
  266. this._iconButtons[widget.id] = updateIconButton(
  267. widget,
  268. toggleDebugging,
  269. this._service.isStarted,
  270. enabled
  271. );
  272. } else {
  273. updateIconButtonState(
  274. debugButton,
  275. this._service.isStarted,
  276. enabled,
  277. toggleDebugging
  278. );
  279. }
  280. };
  281. const isDebuggerOn = (): boolean => {
  282. return (
  283. this._service.isStarted &&
  284. this._previousConnection?.id === connection?.id
  285. );
  286. };
  287. const stopDebugger = async (): Promise<void> => {
  288. this._service.session!.connection = connection;
  289. await this._service.stop();
  290. };
  291. const startDebugger = async (): Promise<void> => {
  292. this._service.session!.connection = connection;
  293. this._previousConnection = connection;
  294. await this._service.restoreState(true);
  295. await this._service.displayDefinedVariables();
  296. if (this._service.session?.capabilities?.supportsModulesRequest) {
  297. await this._service.displayModules();
  298. }
  299. };
  300. const toggleDebugging = async (): Promise<void> => {
  301. // bail if the widget doesn't have focus
  302. if (!hasFocus()) {
  303. return;
  304. }
  305. const debugButton = this._iconButtons[widget.id]!;
  306. if (isDebuggerOn()) {
  307. await stopDebugger();
  308. removeHandlers();
  309. updateIconButtonState(debugButton, false);
  310. } else {
  311. await startDebugger();
  312. createHandler();
  313. updateIconButtonState(debugButton, true);
  314. }
  315. };
  316. addToolbarButton(false);
  317. const debuggingEnabled = await this._service.isAvailable(connection);
  318. if (!debuggingEnabled) {
  319. removeHandlers();
  320. updateIconButtonState(this._iconButtons[widget.id]!, false, false);
  321. return;
  322. }
  323. // update the active debug session
  324. if (!this._service.session) {
  325. this._service.session = new Debugger.Session({ connection });
  326. } else {
  327. this._previousConnection = this._service.session!.connection?.kernel
  328. ? this._service.session.connection
  329. : null;
  330. this._service.session.connection = connection;
  331. }
  332. await this._service.restoreState(false);
  333. if (this._service.isStarted && !this._service.hasStoppedThreads()) {
  334. await this._service.displayDefinedVariables();
  335. if (this._service.session?.capabilities?.supportsModulesRequest) {
  336. await this._service.displayModules();
  337. }
  338. }
  339. updateIconButtonState(
  340. this._iconButtons[widget.id]!,
  341. this._service.isStarted,
  342. true
  343. );
  344. // check the state of the debug session
  345. if (!this._service.isStarted) {
  346. removeHandlers();
  347. this._service.session.connection = this._previousConnection ?? connection;
  348. await this._service.restoreState(false);
  349. return;
  350. }
  351. // if the debugger is started but there is no handler, create a new one
  352. createHandler();
  353. this._previousConnection = connection;
  354. // listen to the disposed signals
  355. widget.disposed.connect(removeHandlers);
  356. }
  357. private _type: DebuggerHandler.SessionType;
  358. private _shell: JupyterFrontEnd.IShell;
  359. private _service: IDebugger;
  360. private _previousConnection: Session.ISessionConnection | null;
  361. private _activeWidget:
  362. | DebuggerHandler.SessionWidget[DebuggerHandler.SessionType]
  363. | null;
  364. private _handlers: {
  365. [id: string]: DebuggerHandler.SessionHandler[DebuggerHandler.SessionType];
  366. } = {};
  367. private _contextKernelChangedHandlers: {
  368. [id: string]: (
  369. sender: SessionContext,
  370. args: IChangedArgs<
  371. Kernel.IKernelConnection,
  372. Kernel.IKernelConnection,
  373. 'kernel'
  374. >
  375. ) => void;
  376. } = {};
  377. private _kernelChangedHandlers: {
  378. [id: string]: (
  379. sender: Session.ISessionConnection,
  380. args: IChangedArgs<
  381. Kernel.IKernelConnection,
  382. Kernel.IKernelConnection,
  383. 'kernel'
  384. >
  385. ) => void;
  386. } = {};
  387. private _statusChangedHandlers: {
  388. [id: string]: (
  389. sender: Session.ISessionConnection,
  390. status: Kernel.Status
  391. ) => void;
  392. } = {};
  393. private _iopubMessageHandlers: {
  394. [id: string]: (
  395. sender: Session.ISessionConnection,
  396. msg: KernelMessage.IIOPubMessage
  397. ) => void;
  398. } = {};
  399. private _iconButtons: {
  400. [id: string]: ToolbarButton | undefined;
  401. } = {};
  402. }
  403. /**
  404. * A namespace for DebuggerHandler `statics`
  405. */
  406. export namespace DebuggerHandler {
  407. /**
  408. * Instantiation options for a DebuggerHandler.
  409. */
  410. export interface IOptions {
  411. /**
  412. * The type of session.
  413. */
  414. type: SessionType;
  415. /**
  416. * The application shell.
  417. */
  418. shell: JupyterFrontEnd.IShell;
  419. /**
  420. * The debugger service.
  421. */
  422. service: IDebugger;
  423. }
  424. /**
  425. * An interface for debugger handler.
  426. */
  427. export interface IHandler {
  428. /**
  429. * Get the active widget.
  430. */
  431. activeWidget:
  432. | DebuggerHandler.SessionWidget[DebuggerHandler.SessionType]
  433. | null;
  434. /**
  435. * Update a debug handler for the given widget, and
  436. * handle kernel changed events.
  437. *
  438. * @param widget The widget to update.
  439. * @param connection The session connection.
  440. */
  441. update(
  442. widget: DebuggerHandler.SessionWidget[DebuggerHandler.SessionType],
  443. connection: Session.ISessionConnection | null
  444. ): Promise<void>;
  445. /**
  446. * Update a debug handler for the given widget, and
  447. * handle connection kernel changed events.
  448. *
  449. * @param widget The widget to update.
  450. * @param sessionContext The session context.
  451. */
  452. updateContext(
  453. widget: DebuggerHandler.SessionWidget[DebuggerHandler.SessionType],
  454. sessionContext: ISessionContext
  455. ): Promise<void>;
  456. }
  457. /**
  458. * The types of sessions that can be debugged.
  459. */
  460. export type SessionType = keyof SessionHandler;
  461. /**
  462. * The types of handlers.
  463. */
  464. export type SessionHandler = {
  465. notebook: NotebookHandler;
  466. console: ConsoleHandler;
  467. file: FileHandler;
  468. };
  469. /**
  470. * The types of widgets that can be debugged.
  471. */
  472. export type SessionWidget = {
  473. notebook: NotebookPanel;
  474. console: ConsolePanel;
  475. file: DocumentWidget;
  476. };
  477. }