service.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { Session, KernelSpec } from '@jupyterlab/services';
  4. import { IDisposable } from '@lumino/disposable';
  5. import { ISignal, Signal } from '@lumino/signaling';
  6. import { DebugProtocol } from 'vscode-debugprotocol';
  7. import { Debugger } from './debugger';
  8. import { VariablesModel } from './panels/variables/model';
  9. import { IDebugger } from './tokens';
  10. /**
  11. * A concrete implementation of the IDebugger interface.
  12. */
  13. export class DebuggerService implements IDebugger, IDisposable {
  14. /**
  15. * Instantiate a new DebuggerService.
  16. *
  17. * @param options The instantiation options for a DebuggerService.
  18. */
  19. constructor(options: DebuggerService.IOptions) {
  20. this._config = options.config;
  21. // Avoids setting session with invalid client
  22. // session should be set only when a notebook or
  23. // a console get the focus.
  24. // TODO: also checks that the notebook or console
  25. // runs a kernel with debugging ability
  26. this._session = null;
  27. this._specsManager = options.specsManager;
  28. this._model = new Debugger.Model();
  29. this._debuggerSources = options.debuggerSources;
  30. }
  31. /**
  32. * Signal emitted for debug event messages.
  33. */
  34. get eventMessage(): ISignal<IDebugger, IDebugger.ISession.Event> {
  35. return this._eventMessage;
  36. }
  37. /**
  38. * Whether the debug service is disposed.
  39. */
  40. get isDisposed(): boolean {
  41. return this._isDisposed;
  42. }
  43. /**
  44. * Whether the current debugger is started.
  45. */
  46. get isStarted(): boolean {
  47. return this._session?.isStarted ?? false;
  48. }
  49. /**
  50. * Returns the debugger service's model.
  51. */
  52. get model(): IDebugger.Model.IService {
  53. return this._model;
  54. }
  55. /**
  56. * Returns the current debug session.
  57. */
  58. get session(): IDebugger.ISession {
  59. return this._session;
  60. }
  61. /**
  62. * Sets the current debug session to the given parameter.
  63. *
  64. * @param session - the new debugger session.
  65. */
  66. set session(session: IDebugger.ISession) {
  67. if (this._session === session) {
  68. return;
  69. }
  70. if (this._session) {
  71. this._session.dispose();
  72. }
  73. this._session = session;
  74. this._session?.eventMessage.connect((_, event) => {
  75. if (event.event === 'stopped') {
  76. this._model.stoppedThreads.add(event.body.threadId);
  77. void this._getAllFrames();
  78. } else if (event.event === 'continued') {
  79. this._model.stoppedThreads.delete(event.body.threadId);
  80. this._clearModel();
  81. this._clearSignals();
  82. }
  83. this._eventMessage.emit(event);
  84. });
  85. this._sessionChanged.emit(session);
  86. }
  87. /**
  88. * Signal emitted upon session changed.
  89. */
  90. get sessionChanged(): ISignal<IDebugger, IDebugger.ISession> {
  91. return this._sessionChanged;
  92. }
  93. /**
  94. * Dispose the debug service.
  95. */
  96. dispose(): void {
  97. if (this.isDisposed) {
  98. return;
  99. }
  100. this._isDisposed = true;
  101. Signal.clearData(this);
  102. }
  103. /**
  104. * Computes an id based on the given code.
  105. *
  106. * @param code The source code.
  107. */
  108. getCodeId(code: string): string {
  109. try {
  110. return this._config.getCodeId(
  111. code,
  112. this.session?.connection?.kernel?.name
  113. );
  114. } catch {
  115. return '';
  116. }
  117. }
  118. /**
  119. * Whether there exists a thread in stopped state.
  120. */
  121. hasStoppedThreads(): boolean {
  122. return this._model?.stoppedThreads.size > 0 ?? false;
  123. }
  124. /**
  125. * Request whether debugging is available for the session connection.
  126. *
  127. * @param connection The session connection.
  128. */
  129. async isAvailable(connection: Session.ISessionConnection): Promise<boolean> {
  130. if (!this._specsManager) {
  131. return true;
  132. }
  133. await this._specsManager.ready;
  134. const kernel = connection?.kernel;
  135. if (!kernel) {
  136. return false;
  137. }
  138. const name = kernel.name;
  139. if (!this._specsManager.specs.kernelspecs[name]) {
  140. return true;
  141. }
  142. return !!(
  143. this._specsManager.specs.kernelspecs[name].metadata?.['debugger'] ?? false
  144. );
  145. }
  146. /**
  147. * Clear all the breakpoints for the current session.
  148. */
  149. async clearBreakpoints(): Promise<void> {
  150. if (!this.session.isStarted) {
  151. return;
  152. }
  153. this._model.breakpoints.breakpoints.forEach(
  154. async (breakpoints, path, _) => {
  155. await this._setBreakpoints([], path);
  156. }
  157. );
  158. let bpMap = new Map<string, IDebugger.IBreakpoint[]>();
  159. this._model.breakpoints.restoreBreakpoints(bpMap);
  160. }
  161. /**
  162. * Continues the execution of the current thread.
  163. */
  164. async continue(): Promise<void> {
  165. try {
  166. await this.session.sendRequest('continue', {
  167. threadId: this._currentThread()
  168. });
  169. this._model.stoppedThreads.delete(this._currentThread());
  170. } catch (err) {
  171. console.error('Error:', err.message);
  172. }
  173. }
  174. /**
  175. * Retrieve the content of a source file.
  176. *
  177. * @param source The source object containing the path to the file.
  178. */
  179. async getSource(source: DebugProtocol.Source): Promise<IDebugger.Source> {
  180. const reply = await this.session.sendRequest('source', {
  181. source,
  182. sourceReference: source.sourceReference
  183. });
  184. return { ...reply.body, path: source.path };
  185. }
  186. /**
  187. * Makes the current thread run again for one step.
  188. */
  189. async next(): Promise<void> {
  190. try {
  191. await this.session.sendRequest('next', {
  192. threadId: this._currentThread()
  193. });
  194. } catch (err) {
  195. console.error('Error:', err.message);
  196. }
  197. }
  198. /**
  199. * Request variables for a given variable reference.
  200. *
  201. * @param variablesReference The variable reference to request.
  202. */
  203. async inspectVariable(
  204. variablesReference: number
  205. ): Promise<DebugProtocol.Variable[]> {
  206. const reply = await this.session.sendRequest('variables', {
  207. variablesReference
  208. });
  209. return reply.body.variables;
  210. }
  211. /**
  212. * Restart the debugger.
  213. */
  214. async restart(): Promise<void> {
  215. const { breakpoints } = this._model.breakpoints;
  216. await this.stop();
  217. await this.start();
  218. // Re-send the breakpoints to the kernel and update the model.
  219. for (const [source, points] of breakpoints) {
  220. await this._setBreakpoints(
  221. points.map(({ line }) => ({ line })),
  222. source
  223. );
  224. }
  225. this._model.breakpoints.restoreBreakpoints(breakpoints);
  226. }
  227. /**
  228. * Restore the state of a debug session.
  229. *
  230. * @param autoStart - If true, starts the debugger if it has not been started.
  231. */
  232. async restoreState(autoStart: boolean): Promise<void> {
  233. if (!this.model || !this.session) {
  234. return;
  235. }
  236. const reply = await this.session.restoreState();
  237. const { body } = reply;
  238. const breakpoints = this._mapBreakpoints(reply.body.breakpoints);
  239. const stoppedThreads = new Set(reply.body.stoppedThreads);
  240. this._config.setHashParams({
  241. kernel: this.session.connection.kernel.name,
  242. method: body.hashMethod,
  243. seed: body.hashSeed
  244. });
  245. this._config.setTmpFileParams({
  246. kernel: this.session.connection.kernel.name,
  247. prefix: body.tmpFilePrefix,
  248. suffix: body.tmpFileSuffix
  249. });
  250. this._model.stoppedThreads = stoppedThreads;
  251. if (!this.isStarted && (autoStart || stoppedThreads.size !== 0)) {
  252. await this.start();
  253. }
  254. if (this.isStarted || autoStart) {
  255. this._model.title = this.isStarted ? this.session?.connection?.name : '-';
  256. }
  257. if (this._debuggerSources) {
  258. const filtered = this._filterBreakpoints(breakpoints);
  259. this._model.breakpoints.restoreBreakpoints(filtered);
  260. } else {
  261. this._model.breakpoints.restoreBreakpoints(breakpoints);
  262. }
  263. if (stoppedThreads.size !== 0) {
  264. await this._getAllFrames();
  265. } else if (this.isStarted) {
  266. this._clearModel();
  267. this._clearSignals();
  268. }
  269. }
  270. /**
  271. * Starts a debugger.
  272. * Precondition: !isStarted
  273. */
  274. start(): Promise<void> {
  275. return this.session.start();
  276. }
  277. /**
  278. * Makes the current thread step in a function / method if possible.
  279. */
  280. async stepIn(): Promise<void> {
  281. try {
  282. await this.session.sendRequest('stepIn', {
  283. threadId: this._currentThread()
  284. });
  285. } catch (err) {
  286. console.error('Error:', err.message);
  287. }
  288. }
  289. /**
  290. * Makes the current thread step out a function / method if possible.
  291. */
  292. async stepOut(): Promise<void> {
  293. try {
  294. await this.session.sendRequest('stepOut', {
  295. threadId: this._currentThread()
  296. });
  297. } catch (err) {
  298. console.error('Error:', err.message);
  299. }
  300. }
  301. /**
  302. * Stops the debugger.
  303. * Precondition: isStarted
  304. */
  305. async stop(): Promise<void> {
  306. await this.session.stop();
  307. if (this._model) {
  308. this._model.clear();
  309. }
  310. }
  311. /**
  312. * Update all breakpoints at once.
  313. *
  314. * @param code - The code in the cell where the breakpoints are set.
  315. * @param breakpoints - The list of breakpoints to set.
  316. * @param path - Optional path to the file where to set the breakpoints.
  317. */
  318. async updateBreakpoints(
  319. code: string,
  320. breakpoints: IDebugger.IBreakpoint[],
  321. path?: string
  322. ): Promise<void> {
  323. if (!this.session.isStarted) {
  324. return;
  325. }
  326. if (!path) {
  327. path = (await this._dumpCell(code)).body.sourcePath;
  328. }
  329. const state = await this.session.restoreState();
  330. const localBreakpoints = breakpoints.map(({ line }) => ({ line }));
  331. const remoteBreakpoints = this._mapBreakpoints(state.body.breakpoints);
  332. // Set the local copy of breakpoints to reflect only editors that exist.
  333. if (this._debuggerSources) {
  334. const filtered = this._filterBreakpoints(remoteBreakpoints);
  335. this._model.breakpoints.restoreBreakpoints(filtered);
  336. } else {
  337. this._model.breakpoints.restoreBreakpoints(remoteBreakpoints);
  338. }
  339. // Set the kernel's breakpoints for this path.
  340. const reply = await this._setBreakpoints(localBreakpoints, path);
  341. const updatedBreakpoints = reply.body.breakpoints.filter(
  342. (val, _, arr) => arr.findIndex(el => el.line === val.line) > -1
  343. );
  344. // Update the local model and finish kernel configuration.
  345. this._model.breakpoints.setBreakpoints(path, updatedBreakpoints);
  346. await this.session.sendRequest('configurationDone', {});
  347. }
  348. /**
  349. * Clear the current model.
  350. */
  351. private _clearModel(): void {
  352. this._model.callstack.frames = [];
  353. this._model.variables.scopes = [];
  354. }
  355. /**
  356. * Clear the signals set on the model.
  357. */
  358. private _clearSignals(): void {
  359. this._model.callstack.currentFrameChanged.disconnect(
  360. this._onCurrentFrameChanged,
  361. this
  362. );
  363. this._model.variables.variableExpanded.disconnect(
  364. this._onVariableExpanded,
  365. this
  366. );
  367. }
  368. /**
  369. * Map a list of scopes to a list of variables.
  370. *
  371. * @param scopes The list of scopes.
  372. * @param variables The list of variables.
  373. */
  374. private _convertScopes(
  375. scopes: DebugProtocol.Scope[],
  376. variables: DebugProtocol.Variable[]
  377. ): IDebugger.IScope[] {
  378. if (!variables || !scopes) {
  379. return;
  380. }
  381. return scopes.map(scope => {
  382. return {
  383. name: scope.name,
  384. variables: variables.map(variable => {
  385. return { ...variable };
  386. })
  387. };
  388. });
  389. }
  390. /**
  391. * Get the current thread from the model.
  392. */
  393. private _currentThread(): number {
  394. // TODO: ask the model for the current thread ID
  395. return 1;
  396. }
  397. /**
  398. * Dump the content of a cell.
  399. *
  400. * @param code The source code to dump.
  401. */
  402. private async _dumpCell(
  403. code: string
  404. ): Promise<IDebugger.ISession.IDumpCellResponse> {
  405. return this.session.sendRequest('dumpCell', { code });
  406. }
  407. /**
  408. * Filter breakpoints and only return those associated with a known editor.
  409. *
  410. * @param breakpoints - Map of breakpoints.
  411. *
  412. */
  413. private _filterBreakpoints(
  414. breakpoints: Map<string, IDebugger.IBreakpoint[]>
  415. ): Map<string, IDebugger.IBreakpoint[]> {
  416. let bpMapForRestore = new Map<string, IDebugger.IBreakpoint[]>();
  417. for (const collection of breakpoints) {
  418. const [id, list] = collection;
  419. list.forEach(() => {
  420. this._debuggerSources
  421. .find({
  422. focus: false,
  423. kernel: this.session.connection.kernel.name,
  424. path: this._session.connection.path,
  425. source: id
  426. })
  427. .forEach(() => {
  428. if (list.length > 0) {
  429. bpMapForRestore.set(id, list);
  430. }
  431. });
  432. });
  433. }
  434. return bpMapForRestore;
  435. }
  436. /**
  437. * Get all the frames from the kernel.
  438. */
  439. private async _getAllFrames(): Promise<void> {
  440. this._model.callstack.currentFrameChanged.connect(
  441. this._onCurrentFrameChanged,
  442. this
  443. );
  444. this._model.variables.variableExpanded.connect(
  445. this._onVariableExpanded,
  446. this
  447. );
  448. const stackFrames = await this._getFrames(this._currentThread());
  449. this._model.callstack.frames = stackFrames;
  450. }
  451. /**
  452. * Get all the frames for the given thread id.
  453. *
  454. * @param threadId The thread id.
  455. */
  456. private async _getFrames(
  457. threadId: number
  458. ): Promise<DebugProtocol.StackFrame[]> {
  459. const reply = await this.session.sendRequest('stackTrace', {
  460. threadId
  461. });
  462. const stackFrames = reply.body.stackFrames;
  463. return stackFrames;
  464. }
  465. /**
  466. * Get all the scopes for the given frame.
  467. *
  468. * @param frame The frame.
  469. */
  470. private async _getScopes(
  471. frame: DebugProtocol.StackFrame
  472. ): Promise<DebugProtocol.Scope[]> {
  473. if (!frame) {
  474. return;
  475. }
  476. const reply = await this.session.sendRequest('scopes', {
  477. frameId: frame.id
  478. });
  479. return reply.body.scopes;
  480. }
  481. /**
  482. * Get the variables for a given scope.
  483. *
  484. * @param scope The scope to get variables for.
  485. */
  486. private async _getVariables(
  487. scope: DebugProtocol.Scope
  488. ): Promise<DebugProtocol.Variable[]> {
  489. if (!scope) {
  490. return;
  491. }
  492. const reply = await this.session.sendRequest('variables', {
  493. variablesReference: scope.variablesReference
  494. });
  495. return reply.body.variables;
  496. }
  497. /**
  498. * Process the list of breakpoints from the server and return as a map.
  499. *
  500. * @param breakpoints - The list of breakpoints from the kernel.
  501. *
  502. */
  503. private _mapBreakpoints(
  504. breakpoints: IDebugger.ISession.IDebugInfoBreakpoints[]
  505. ): Map<string, IDebugger.IBreakpoint[]> {
  506. if (!breakpoints.length) {
  507. return new Map<string, IDebugger.IBreakpoint[]>();
  508. }
  509. return breakpoints.reduce(
  510. (
  511. map: Map<string, IDebugger.IBreakpoint[]>,
  512. val: IDebugger.ISession.IDebugInfoBreakpoints
  513. ) => {
  514. const { breakpoints, source } = val;
  515. map.set(
  516. source,
  517. breakpoints.map(point => ({ ...point, verified: true }))
  518. );
  519. return map;
  520. },
  521. new Map<string, IDebugger.IBreakpoint[]>()
  522. );
  523. }
  524. /**
  525. * Handle a change of the current active frame.
  526. *
  527. * @param _ The callstack model
  528. * @param frame The frame.
  529. */
  530. private async _onCurrentFrameChanged(
  531. _: IDebugger.Model.ICallstack,
  532. frame: IDebugger.IStackFrame
  533. ): Promise<void> {
  534. if (!frame) {
  535. return;
  536. }
  537. const scopes = await this._getScopes(frame);
  538. const variables = await this._getVariables(scopes[0]);
  539. const variableScopes = this._convertScopes(scopes, variables);
  540. this._model.variables.scopes = variableScopes;
  541. }
  542. /**
  543. * Handle a variable expanded event and request variables from the kernel.
  544. *
  545. * @param _ The variables model.
  546. * @param variable The expanded variable.
  547. */
  548. private async _onVariableExpanded(
  549. _: VariablesModel,
  550. variable: DebugProtocol.Variable
  551. ): Promise<DebugProtocol.Variable[]> {
  552. const reply = await this.session.sendRequest('variables', {
  553. variablesReference: variable.variablesReference
  554. });
  555. let newVariable = { ...variable, expanded: true };
  556. reply.body.variables.forEach((variable: DebugProtocol.Variable) => {
  557. newVariable = { [variable.name]: variable, ...newVariable };
  558. });
  559. const newScopes = this._model.variables.scopes.map(scope => {
  560. const findIndex = scope.variables.findIndex(
  561. ele => ele.variablesReference === variable.variablesReference
  562. );
  563. scope.variables[findIndex] = newVariable;
  564. return { ...scope };
  565. });
  566. this._model.variables.scopes = [...newScopes];
  567. return reply.body.variables;
  568. }
  569. /**
  570. * Set the breakpoints for a given file.
  571. *
  572. * @param breakpoints The list of breakpoints to set.
  573. * @param path The path to where to set the breakpoints.
  574. */
  575. private async _setBreakpoints(
  576. breakpoints: DebugProtocol.SourceBreakpoint[],
  577. path: string
  578. ): Promise<DebugProtocol.SetBreakpointsResponse> {
  579. return await this.session.sendRequest('setBreakpoints', {
  580. breakpoints: breakpoints,
  581. source: { path },
  582. sourceModified: false
  583. });
  584. }
  585. private _config: IDebugger.IConfig;
  586. private _debuggerSources: IDebugger.ISources | null;
  587. private _eventMessage = new Signal<IDebugger, IDebugger.ISession.Event>(this);
  588. private _isDisposed = false;
  589. private _model: IDebugger.Model.IService;
  590. private _session: IDebugger.ISession;
  591. private _sessionChanged = new Signal<IDebugger, IDebugger.ISession>(this);
  592. private _specsManager: KernelSpec.IManager;
  593. }
  594. /**
  595. * A namespace for `DebuggerService` statics.
  596. */
  597. export namespace DebuggerService {
  598. /**
  599. * Instantiation options for a `DebuggerService`.
  600. */
  601. export interface IOptions {
  602. /**
  603. * The configuration instance with hash method.
  604. */
  605. config: IDebugger.IConfig;
  606. /**
  607. * The optional debugger sources instance.
  608. */
  609. debuggerSources?: IDebugger.ISources;
  610. /**
  611. * The optional kernel specs manager.
  612. */
  613. specsManager?: KernelSpec.IManager;
  614. }
  615. }