sessioncontext.tsx 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { PathExt, IChangedArgs } from '@jupyterlab/coreutils';
  4. import { UUID } from '@lumino/coreutils';
  5. import {
  6. Kernel,
  7. KernelMessage,
  8. KernelSpec,
  9. ServerConnection,
  10. Session
  11. } from '@jupyterlab/services';
  12. import { IterableOrArrayLike, each, find } from '@lumino/algorithm';
  13. import { PromiseDelegate } from '@lumino/coreutils';
  14. import { IDisposable, IObservableDisposable } from '@lumino/disposable';
  15. import { ISignal, Signal } from '@lumino/signaling';
  16. import { Widget } from '@lumino/widgets';
  17. import * as React from 'react';
  18. import { showDialog, Dialog } from './dialog';
  19. /**
  20. * A context object to manage a widget's kernel session connection.
  21. *
  22. * #### Notes
  23. * The current session connection is `.session`, the current session's kernel
  24. * connection is `.session.kernel`. For convenience, we proxy several kernel
  25. * connection and and session connection signals up to the session context so
  26. * that you do not have to manage slots as sessions and kernels change. For
  27. * example, to act on whatever the current kernel's iopubMessage signal is
  28. * producing, connect to the session context `.iopubMessage` signal.
  29. *
  30. */
  31. export interface ISessionContext extends IObservableDisposable {
  32. /**
  33. * The current session connection.
  34. */
  35. session: Session.ISessionConnection | null;
  36. /**
  37. * Initialize the session context.
  38. *
  39. * @returns A promise that resolves with whether to ask the user to select a kernel.
  40. *
  41. * #### Notes
  42. * This includes starting up an initial kernel if needed.
  43. */
  44. initialize(): Promise<boolean>;
  45. /**
  46. * Whether the session context is ready.
  47. */
  48. readonly isReady: boolean;
  49. /**
  50. * A promise that is fulfilled when the session context is ready.
  51. */
  52. readonly ready: Promise<void>;
  53. /**
  54. * A signal emitted when the session connection changes.
  55. */
  56. readonly sessionChanged: ISignal<
  57. this,
  58. IChangedArgs<
  59. Session.ISessionConnection | null,
  60. Session.ISessionConnection | null,
  61. 'session'
  62. >
  63. >;
  64. // Signals proxied from the session connection for convenience.
  65. /**
  66. * A signal emitted when the kernel changes, proxied from the session connection.
  67. */
  68. readonly kernelChanged: ISignal<
  69. this,
  70. IChangedArgs<
  71. Kernel.IKernelConnection | null,
  72. Kernel.IKernelConnection | null,
  73. 'kernel'
  74. >
  75. >;
  76. /**
  77. * A signal emitted when the kernel status changes, proxied from the session connection.
  78. */
  79. readonly statusChanged: ISignal<this, Kernel.Status>;
  80. /**
  81. * A signal emitted when the kernel connection status changes, proxied from the session connection.
  82. */
  83. readonly connectionStatusChanged: ISignal<this, Kernel.ConnectionStatus>;
  84. /**
  85. * A signal emitted for a kernel messages, proxied from the session connection.
  86. */
  87. readonly iopubMessage: ISignal<this, KernelMessage.IMessage>;
  88. /**
  89. * A signal emitted for an unhandled kernel message, proxied from the session connection.
  90. */
  91. readonly unhandledMessage: ISignal<this, KernelMessage.IMessage>;
  92. /**
  93. * A signal emitted when a session property changes, proxied from the session connection.
  94. */
  95. readonly propertyChanged: ISignal<this, 'path' | 'name' | 'type'>;
  96. /**
  97. * The kernel preference for starting new kernels.
  98. */
  99. kernelPreference: ISessionContext.IKernelPreference;
  100. /**
  101. * The sensible display name for the kernel, or Private.NO_KERNEL
  102. *
  103. * #### Notes
  104. * This is at this level since the underlying kernel connection does not
  105. * have access to the kernel spec manager.
  106. */
  107. readonly kernelDisplayName: string;
  108. /**
  109. * A sensible status to display
  110. *
  111. * #### Notes
  112. * This combines the status and connection status into a single status for the user.
  113. */
  114. readonly kernelDisplayStatus: ISessionContext.KernelDisplayStatus;
  115. /**
  116. * The session path.
  117. *
  118. * #### Notes
  119. * Typically `.session.path` should be used. This attribute is useful if
  120. * there is no current session.
  121. */
  122. readonly path: string;
  123. /**
  124. * The session type.
  125. *
  126. * #### Notes
  127. * Typically `.session.type` should be used. This attribute is useful if
  128. * there is no current session.
  129. */
  130. readonly type: string;
  131. /**
  132. * The session name.
  133. *
  134. * #### Notes
  135. * Typically `.session.name` should be used. This attribute is useful if
  136. * there is no current session.
  137. */
  138. readonly name: string;
  139. /**
  140. * The previous kernel name.
  141. */
  142. readonly prevKernelName: string;
  143. /**
  144. * The session manager used by the session.
  145. */
  146. readonly sessionManager: Session.IManager;
  147. /**
  148. * The kernel spec manager
  149. */
  150. readonly specsManager: KernelSpec.IManager;
  151. /**
  152. * Kill the kernel and shutdown the session.
  153. *
  154. * @returns A promise that resolves when the session is shut down.
  155. */
  156. shutdown(): Promise<void>;
  157. /**
  158. * Change the kernel associated with the session.
  159. *
  160. * @param options The optional kernel model parameters to use for the new kernel.
  161. *
  162. * @returns A promise that resolves with the new kernel connection.
  163. */
  164. changeKernel(
  165. options?: Partial<Kernel.IModel>
  166. ): Promise<Kernel.IKernelConnection | null>;
  167. }
  168. /**
  169. * The namespace for session context related interfaces.
  170. */
  171. export namespace ISessionContext {
  172. /**
  173. * A kernel preference.
  174. *
  175. * #### Notes
  176. * Preferences for a kernel are considered in the order `id`, `name`,
  177. * `language`. If no matching kernels can be found and `autoStartDefault` is
  178. * `true`, then the default kernel for the server is preferred.
  179. */
  180. export interface IKernelPreference {
  181. /**
  182. * The name of the kernel.
  183. */
  184. readonly name?: string;
  185. /**
  186. * The preferred kernel language.
  187. */
  188. readonly language?: string;
  189. /**
  190. * The id of an existing kernel.
  191. */
  192. readonly id?: string;
  193. /**
  194. * A kernel should be started automatically (default `true`).
  195. */
  196. readonly shouldStart?: boolean;
  197. /**
  198. * A kernel can be started (default `true`).
  199. */
  200. readonly canStart?: boolean;
  201. /**
  202. * Shut down the session when session context is disposed (default `false`).
  203. */
  204. readonly shutdownOnDispose?: boolean;
  205. /**
  206. * Automatically start the default kernel if no other matching kernel is
  207. * found (default `true`).
  208. */
  209. readonly autoStartDefault?: boolean;
  210. }
  211. export type KernelDisplayStatus =
  212. | Kernel.Status
  213. | Kernel.ConnectionStatus
  214. | 'initializing'
  215. | '';
  216. /**
  217. * An interface for a session context dialog provider.
  218. */
  219. export interface IDialogs {
  220. /**
  221. * Select a kernel for the session.
  222. */
  223. selectKernel(session: ISessionContext): Promise<void>;
  224. /**
  225. * Restart the session context.
  226. *
  227. * @returns A promise that resolves with whether the kernel has restarted.
  228. *
  229. * #### Notes
  230. * If there is a running kernel, present a dialog.
  231. * If there is no kernel, we start a kernel with the last run
  232. * kernel name and resolves with `true`. If no kernel has been started,
  233. * this is a no-op, and resolves with `false`.
  234. */
  235. restart(session: ISessionContext): Promise<boolean>;
  236. }
  237. }
  238. /**
  239. * The default implementation for a session context object.
  240. */
  241. export class SessionContext implements ISessionContext {
  242. /**
  243. * Construct a new session context.
  244. */
  245. constructor(options: SessionContext.IOptions) {
  246. this.sessionManager = options.sessionManager;
  247. this.specsManager = options.specsManager;
  248. this._path = options.path ?? UUID.uuid4();
  249. this._type = options.type ?? '';
  250. this._name = options.name ?? '';
  251. this._setBusy = options.setBusy;
  252. this._kernelPreference = options.kernelPreference ?? {};
  253. }
  254. /**
  255. * The current session connection.
  256. */
  257. get session(): Session.ISessionConnection | null {
  258. return this._session ?? null;
  259. }
  260. /**
  261. * The session path.
  262. *
  263. * #### Notes
  264. * Typically `.session.path` should be used. This attribute is useful if
  265. * there is no current session.
  266. */
  267. get path(): string {
  268. return this._path;
  269. }
  270. /**
  271. * The session type.
  272. *
  273. * #### Notes
  274. * Typically `.session.type` should be used. This attribute is useful if
  275. * there is no current session.
  276. */
  277. get type(): string {
  278. return this._type;
  279. }
  280. /**
  281. * The session name.
  282. *
  283. * #### Notes
  284. * Typically `.session.name` should be used. This attribute is useful if
  285. * there is no current session.
  286. */
  287. get name(): string {
  288. return this._name;
  289. }
  290. /**
  291. * A signal emitted when the kernel connection changes, proxied from the session connection.
  292. */
  293. get kernelChanged(): ISignal<
  294. this,
  295. Session.ISessionConnection.IKernelChangedArgs
  296. > {
  297. return this._kernelChanged;
  298. }
  299. /**
  300. * A signal emitted when the session connection changes.
  301. */
  302. get sessionChanged(): ISignal<
  303. this,
  304. IChangedArgs<
  305. Session.ISessionConnection | null,
  306. Session.ISessionConnection | null,
  307. 'session'
  308. >
  309. > {
  310. return this._sessionChanged;
  311. }
  312. /**
  313. * A signal emitted when the kernel status changes, proxied from the kernel.
  314. */
  315. get statusChanged(): ISignal<this, Kernel.Status> {
  316. return this._statusChanged;
  317. }
  318. /**
  319. * A signal emitted when the kernel status changes, proxied from the kernel.
  320. */
  321. get connectionStatusChanged(): ISignal<this, Kernel.ConnectionStatus> {
  322. return this._connectionStatusChanged;
  323. }
  324. /**
  325. * A signal emitted for iopub kernel messages, proxied from the kernel.
  326. */
  327. get iopubMessage(): ISignal<this, KernelMessage.IIOPubMessage> {
  328. return this._iopubMessage;
  329. }
  330. /**
  331. * A signal emitted for an unhandled kernel message, proxied from the kernel.
  332. */
  333. get unhandledMessage(): ISignal<this, KernelMessage.IMessage> {
  334. return this._unhandledMessage;
  335. }
  336. /**
  337. * A signal emitted when a session property changes, proxied from the current session.
  338. */
  339. get propertyChanged(): ISignal<this, 'path' | 'name' | 'type'> {
  340. return this._propertyChanged;
  341. }
  342. /**
  343. * The kernel preference of this client session.
  344. *
  345. * This is used when selecting a new kernel, and should reflect the sort of
  346. * kernel the activity prefers.
  347. */
  348. get kernelPreference(): ISessionContext.IKernelPreference {
  349. return this._kernelPreference;
  350. }
  351. set kernelPreference(value: ISessionContext.IKernelPreference) {
  352. this._kernelPreference = value;
  353. }
  354. /**
  355. * Whether the context is ready.
  356. */
  357. get isReady(): boolean {
  358. return this._isReady;
  359. }
  360. /**
  361. * A promise that is fulfilled when the context is ready.
  362. */
  363. get ready(): Promise<void> {
  364. return this._ready.promise;
  365. }
  366. /**
  367. * The session manager used by the session.
  368. */
  369. readonly sessionManager: Session.IManager;
  370. /**
  371. * The kernel spec manager
  372. */
  373. readonly specsManager: KernelSpec.IManager;
  374. /**
  375. * The display name of the current kernel, or a sensible alternative.
  376. *
  377. * #### Notes
  378. * This is a convenience function to have a consistent sensible name for the
  379. * kernel.
  380. */
  381. get kernelDisplayName(): string {
  382. const kernel = this.session?.kernel;
  383. if (this._pendingKernelName === Private.NO_KERNEL) {
  384. return Private.NO_KERNEL;
  385. }
  386. if (
  387. !kernel &&
  388. !this.isReady &&
  389. this.kernelPreference.canStart !== false &&
  390. this.kernelPreference.shouldStart !== false
  391. ) {
  392. let name =
  393. this._pendingKernelName ||
  394. SessionContext.getDefaultKernel({
  395. specs: this.specsManager.specs,
  396. sessions: this.sessionManager.running(),
  397. preference: this.kernelPreference
  398. }) ||
  399. '';
  400. if (name) {
  401. name = this.specsManager.specs!.kernelspecs[name]!.display_name;
  402. return name;
  403. }
  404. return Private.NO_KERNEL;
  405. }
  406. if (this._pendingKernelName) {
  407. return this.specsManager.specs!.kernelspecs[this._pendingKernelName]!
  408. .display_name;
  409. }
  410. if (!kernel) {
  411. return Private.NO_KERNEL;
  412. }
  413. return (
  414. this.specsManager.specs?.kernelspecs[kernel.name]?.display_name ??
  415. kernel.name
  416. );
  417. }
  418. /**
  419. * A sensible status to display
  420. *
  421. * #### Notes
  422. * This combines the status and connection status into a single status for
  423. * the user.
  424. */
  425. get kernelDisplayStatus(): ISessionContext.KernelDisplayStatus {
  426. const kernel = this.session?.kernel;
  427. if (this._pendingKernelName === Private.NO_KERNEL) {
  428. return 'idle';
  429. }
  430. if (!kernel && this._pendingKernelName) {
  431. return 'initializing';
  432. }
  433. if (
  434. !kernel &&
  435. !this.isReady &&
  436. this.kernelPreference.canStart !== false &&
  437. this.kernelPreference.shouldStart !== false
  438. ) {
  439. return 'initializing';
  440. }
  441. return (
  442. (kernel?.connectionStatus === 'connected'
  443. ? kernel?.status
  444. : kernel?.connectionStatus) ?? 'unknown'
  445. );
  446. }
  447. /**
  448. * The name of the previously started kernel.
  449. */
  450. get prevKernelName(): string {
  451. return this._prevKernelName;
  452. }
  453. /**
  454. * Test whether the context is disposed.
  455. */
  456. get isDisposed(): boolean {
  457. return this._isDisposed;
  458. }
  459. /**
  460. * A signal emitted when the poll is disposed.
  461. */
  462. get disposed(): ISignal<this, void> {
  463. return this._disposed;
  464. }
  465. /**
  466. * Dispose of the resources held by the context.
  467. */
  468. dispose(): void {
  469. if (this._isDisposed) {
  470. return;
  471. }
  472. this._isDisposed = true;
  473. this._disposed.emit();
  474. if (this._session) {
  475. if (this.kernelPreference.shutdownOnDispose) {
  476. // Fire and forget the session shutdown request
  477. this.sessionManager.shutdown(this._session.id).catch(reason => {
  478. console.error(`Kernel not shut down ${reason}`);
  479. });
  480. }
  481. // Dispose the session connection
  482. this._session.dispose();
  483. this._session = null;
  484. }
  485. if (this._dialog) {
  486. this._dialog.dispose();
  487. }
  488. if (this._busyDisposable) {
  489. this._busyDisposable.dispose();
  490. this._busyDisposable = null;
  491. }
  492. Signal.clearData(this);
  493. }
  494. /**
  495. * Change the current kernel associated with the session.
  496. */
  497. async changeKernel(
  498. options: Partial<Kernel.IModel> = {}
  499. ): Promise<Kernel.IKernelConnection | null> {
  500. if (this.isDisposed) {
  501. throw new Error('Disposed');
  502. }
  503. // Wait for the initialization method to try
  504. // and start its kernel first to ensure consistent
  505. // ordering.
  506. await this._initStarted.promise;
  507. return this._changeKernel(options);
  508. }
  509. /**
  510. * Kill the kernel and shutdown the session.
  511. *
  512. * @returns A promise that resolves when the session is shut down.
  513. */
  514. async shutdown(): Promise<void> {
  515. if (this.isDisposed || !this._initializing) {
  516. return;
  517. }
  518. await this._initStarted.promise;
  519. this._pendingSessionRequest = '';
  520. this._pendingKernelName = Private.NO_KERNEL;
  521. return this._shutdownSession();
  522. }
  523. /**
  524. * Initialize the session context
  525. *
  526. * @returns A promise that resolves with whether to ask the user to select a kernel.
  527. *
  528. * #### Notes
  529. * If a server session exists on the current path, we will connect to it.
  530. * If preferences include disabling `canStart` or `shouldStart`, no
  531. * server session will be started.
  532. * If a kernel id is given, we attempt to start a session with that id.
  533. * If a default kernel is available, we connect to it.
  534. * Otherwise we ask the user to select a kernel.
  535. */
  536. async initialize(): Promise<boolean> {
  537. if (this._initializing) {
  538. return this._initPromise.promise;
  539. }
  540. this._initializing = true;
  541. const needsSelection = await this._initialize();
  542. if (!needsSelection) {
  543. this._isReady = true;
  544. this._ready.resolve(undefined);
  545. }
  546. if (!this._pendingSessionRequest) {
  547. this._initStarted.resolve(void 0);
  548. }
  549. this._initPromise.resolve(needsSelection);
  550. return needsSelection;
  551. }
  552. /**
  553. * Inner initialize function that doesn't handle promises.
  554. * This makes it easier to consolidate promise handling logic.
  555. */
  556. async _initialize(): Promise<boolean> {
  557. const manager = this.sessionManager;
  558. await manager.ready;
  559. await manager.refreshRunning();
  560. const model = find(manager.running(), item => {
  561. return item.path === this._path;
  562. });
  563. if (model) {
  564. try {
  565. const session = manager.connectTo({ model });
  566. this._handleNewSession(session);
  567. } catch (err) {
  568. void this._handleSessionError(err);
  569. return Promise.reject(err);
  570. }
  571. }
  572. return await this._startIfNecessary();
  573. }
  574. /**
  575. * Shut down the current session.
  576. */
  577. private async _shutdownSession(): Promise<void> {
  578. const session = this._session;
  579. this._session = null;
  580. const kernel = session?.kernel || null;
  581. this._kernelChanged.emit({
  582. name: 'kernel',
  583. oldValue: kernel,
  584. newValue: null
  585. });
  586. this._statusChanged.emit('unknown');
  587. await session?.shutdown();
  588. session?.dispose();
  589. this._sessionChanged.emit({
  590. name: 'session',
  591. oldValue: session,
  592. newValue: null
  593. });
  594. }
  595. /**
  596. * Start the session if necessary.
  597. *
  598. * @returns Whether to ask the user to pick a kernel.
  599. */
  600. private async _startIfNecessary(): Promise<boolean> {
  601. const preference = this.kernelPreference;
  602. if (
  603. this.isDisposed ||
  604. this.session?.kernel ||
  605. preference.shouldStart === false ||
  606. preference.canStart === false
  607. ) {
  608. // Not necessary to start a kernel
  609. return false;
  610. }
  611. let options: Partial<Kernel.IModel> | undefined;
  612. if (preference.id) {
  613. options = { id: preference.id };
  614. } else {
  615. const name = SessionContext.getDefaultKernel({
  616. specs: this.specsManager.specs,
  617. sessions: this.sessionManager.running(),
  618. preference
  619. });
  620. if (name) {
  621. options = { name };
  622. }
  623. }
  624. if (options) {
  625. try {
  626. await this._changeKernel(options);
  627. return false;
  628. } catch (err) {
  629. /* no-op */
  630. }
  631. }
  632. // Always fall back to selecting a kernel
  633. return true;
  634. }
  635. /**
  636. * Change the kernel.
  637. */
  638. private async _changeKernel(
  639. model: Partial<Kernel.IModel> = {},
  640. isInit = false
  641. ): Promise<Kernel.IKernelConnection | null> {
  642. if (model.name) {
  643. this._pendingKernelName = model.name;
  644. }
  645. if (this._session) {
  646. await this._shutdownSession();
  647. } else {
  648. this._kernelChanged.emit({
  649. name: 'kernel',
  650. oldValue: null,
  651. newValue: null
  652. });
  653. }
  654. // Guarantee that the initialized kernel
  655. // will be started first.
  656. if (!this._pendingSessionRequest) {
  657. this._initStarted.resolve(void 0);
  658. }
  659. // Use a UUID for the path to overcome a race condition on the server
  660. // where it will re-use a session for a given path but only after
  661. // the kernel finishes starting.
  662. // We later switch to the real path below.
  663. // Use the correct directory so the kernel will be started in that directory.
  664. const dirName = PathExt.dirname(this._path);
  665. const requestId = (this._pendingSessionRequest = PathExt.join(
  666. dirName,
  667. UUID.uuid4()
  668. ));
  669. try {
  670. this._statusChanged.emit('starting');
  671. const session = await this.sessionManager.startNew({
  672. path: requestId,
  673. type: this._type,
  674. name: this._name,
  675. kernel: model
  676. });
  677. // Handle a preempt.
  678. if (this._pendingSessionRequest !== session.path) {
  679. await session.shutdown();
  680. session.dispose();
  681. return null;
  682. }
  683. // Change to the real path.
  684. await session.setPath(this._path);
  685. // Update the name in case it has changed since we launched the session.
  686. await session.setName(this._name);
  687. if (this._session) {
  688. await this._shutdownSession();
  689. }
  690. return this._handleNewSession(session);
  691. } catch (err) {
  692. void this._handleSessionError(err);
  693. throw err;
  694. }
  695. }
  696. /**
  697. * Handle a new session object.
  698. */
  699. private _handleNewSession(
  700. session: Session.ISessionConnection | null
  701. ): Kernel.IKernelConnection | null {
  702. if (this.isDisposed) {
  703. throw Error('Disposed');
  704. }
  705. if (!this._isReady) {
  706. this._isReady = true;
  707. this._ready.resolve(undefined);
  708. }
  709. if (this._session) {
  710. this._session.dispose();
  711. }
  712. this._session = session;
  713. this._pendingKernelName = '';
  714. if (session) {
  715. this._prevKernelName = session.kernel?.name ?? '';
  716. session.disposed.connect(this._onSessionDisposed, this);
  717. session.propertyChanged.connect(this._onPropertyChanged, this);
  718. session.kernelChanged.connect(this._onKernelChanged, this);
  719. session.statusChanged.connect(this._onStatusChanged, this);
  720. session.connectionStatusChanged.connect(
  721. this._onConnectionStatusChanged,
  722. this
  723. );
  724. session.iopubMessage.connect(this._onIopubMessage, this);
  725. session.unhandledMessage.connect(this._onUnhandledMessage, this);
  726. if (session.path !== this._path) {
  727. this._onPropertyChanged(session, 'path');
  728. }
  729. if (session.name !== this._name) {
  730. this._onPropertyChanged(session, 'name');
  731. }
  732. if (session.type !== this._type) {
  733. this._onPropertyChanged(session, 'type');
  734. }
  735. }
  736. // Any existing session/kernel connection was disposed above when the session was
  737. // disposed, so the oldValue should be null.
  738. this._sessionChanged.emit({
  739. name: 'session',
  740. oldValue: null,
  741. newValue: session
  742. });
  743. this._kernelChanged.emit({
  744. oldValue: null,
  745. newValue: session?.kernel || null,
  746. name: 'kernel'
  747. });
  748. this._statusChanged.emit(session?.kernel?.status || 'unknown');
  749. return session?.kernel || null;
  750. }
  751. /**
  752. * Handle an error in session startup.
  753. */
  754. private async _handleSessionError(
  755. err: ServerConnection.ResponseError
  756. ): Promise<void> {
  757. this._handleNewSession(null);
  758. let traceback = '';
  759. let message = '';
  760. try {
  761. traceback = err.traceback;
  762. message = err.message;
  763. } catch (err) {
  764. // no-op
  765. }
  766. const body = (
  767. <div>
  768. <pre>{err.message}</pre>
  769. {message && <pre>{message}</pre>}
  770. {traceback && (
  771. <details className="jp-mod-wide">
  772. <pre>{traceback}</pre>
  773. </details>
  774. )}
  775. </div>
  776. );
  777. const dialog = (this._dialog = new Dialog({
  778. title: 'Error Starting Kernel',
  779. body,
  780. buttons: [Dialog.okButton()]
  781. }));
  782. await dialog.launch();
  783. this._dialog = null;
  784. }
  785. /**
  786. * Handle a session termination.
  787. */
  788. private _onSessionDisposed(): void {
  789. if (this._session) {
  790. const oldValue = this._session;
  791. this._session = null;
  792. const newValue = this._session;
  793. this._sessionChanged.emit({ name: 'session', oldValue, newValue });
  794. }
  795. }
  796. /**
  797. * Handle a change to a session property.
  798. */
  799. private _onPropertyChanged(
  800. sender: Session.ISessionConnection,
  801. property: 'path' | 'name' | 'type'
  802. ) {
  803. switch (property) {
  804. case 'path':
  805. this._path = sender.path;
  806. break;
  807. case 'name':
  808. this._name = sender.name;
  809. break;
  810. case 'type':
  811. this._type = sender.type;
  812. break;
  813. default:
  814. throw new Error(`unrecognized property ${property}`);
  815. }
  816. this._propertyChanged.emit(property);
  817. }
  818. /**
  819. * Handle a change to the kernel.
  820. */
  821. private _onKernelChanged(
  822. sender: Session.ISessionConnection,
  823. args: Session.ISessionConnection.IKernelChangedArgs
  824. ): void {
  825. this._kernelChanged.emit(args);
  826. }
  827. /**
  828. * Handle a change to the session status.
  829. */
  830. private _onStatusChanged(
  831. sender: Session.ISessionConnection,
  832. status: Kernel.Status
  833. ): void {
  834. // Set that this kernel is busy, if we haven't already
  835. // If we have already, and now we aren't busy, dispose
  836. // of the busy disposable.
  837. if (this._setBusy) {
  838. if (status === 'busy') {
  839. if (!this._busyDisposable) {
  840. this._busyDisposable = this._setBusy();
  841. }
  842. } else {
  843. if (this._busyDisposable) {
  844. this._busyDisposable.dispose();
  845. this._busyDisposable = null;
  846. }
  847. }
  848. }
  849. // Proxy the signal
  850. this._statusChanged.emit(status);
  851. }
  852. /**
  853. * Handle a change to the session status.
  854. */
  855. private _onConnectionStatusChanged(
  856. sender: Session.ISessionConnection,
  857. status: Kernel.ConnectionStatus
  858. ): void {
  859. // Proxy the signal
  860. this._connectionStatusChanged.emit(status);
  861. }
  862. /**
  863. * Handle an iopub message.
  864. */
  865. private _onIopubMessage(
  866. sender: Session.ISessionConnection,
  867. message: KernelMessage.IIOPubMessage
  868. ): void {
  869. this._iopubMessage.emit(message);
  870. }
  871. /**
  872. * Handle an unhandled message.
  873. */
  874. private _onUnhandledMessage(
  875. sender: Session.ISessionConnection,
  876. message: KernelMessage.IMessage
  877. ): void {
  878. this._unhandledMessage.emit(message);
  879. }
  880. private _path = '';
  881. private _name = '';
  882. private _type = '';
  883. private _prevKernelName: string = '';
  884. private _kernelPreference: ISessionContext.IKernelPreference;
  885. private _isDisposed = false;
  886. private _disposed = new Signal<this, void>(this);
  887. private _session: Session.ISessionConnection | null = null;
  888. private _ready = new PromiseDelegate<void>();
  889. private _initializing = false;
  890. private _initStarted = new PromiseDelegate<void>();
  891. private _initPromise = new PromiseDelegate<boolean>();
  892. private _isReady = false;
  893. private _kernelChanged = new Signal<
  894. this,
  895. Session.ISessionConnection.IKernelChangedArgs
  896. >(this);
  897. private _sessionChanged = new Signal<
  898. this,
  899. IChangedArgs<
  900. Session.ISessionConnection | null,
  901. Session.ISessionConnection | null,
  902. 'session'
  903. >
  904. >(this);
  905. private _statusChanged = new Signal<this, Kernel.Status>(this);
  906. private _connectionStatusChanged = new Signal<this, Kernel.ConnectionStatus>(
  907. this
  908. );
  909. private _iopubMessage = new Signal<this, KernelMessage.IIOPubMessage>(this);
  910. private _unhandledMessage = new Signal<this, KernelMessage.IMessage>(this);
  911. private _propertyChanged = new Signal<this, 'path' | 'name' | 'type'>(this);
  912. private _dialog: Dialog<any> | null = null;
  913. private _setBusy: (() => IDisposable) | undefined;
  914. private _busyDisposable: IDisposable | null = null;
  915. private _pendingKernelName = '';
  916. private _pendingSessionRequest = '';
  917. }
  918. /**
  919. * A namespace for `SessionContext` statics.
  920. */
  921. export namespace SessionContext {
  922. /**
  923. * The options used to initialize a context.
  924. */
  925. export interface IOptions {
  926. /**
  927. * A session manager instance.
  928. */
  929. sessionManager: Session.IManager;
  930. /**
  931. * A kernel spec manager instance.
  932. */
  933. specsManager: KernelSpec.IManager;
  934. /**
  935. * The initial path of the file.
  936. */
  937. path?: string;
  938. /**
  939. * The name of the session.
  940. */
  941. name?: string;
  942. /**
  943. * The type of the session.
  944. */
  945. type?: string;
  946. /**
  947. * A kernel preference.
  948. */
  949. kernelPreference?: ISessionContext.IKernelPreference;
  950. /**
  951. * A function to call when the session becomes busy.
  952. */
  953. setBusy?: () => IDisposable;
  954. }
  955. /**
  956. * An interface for populating a kernel selector.
  957. */
  958. export interface IKernelSearch {
  959. /**
  960. * The Kernel specs.
  961. */
  962. specs: KernelSpec.ISpecModels | null;
  963. /**
  964. * The kernel preference.
  965. */
  966. preference: ISessionContext.IKernelPreference;
  967. /**
  968. * The current running sessions.
  969. */
  970. sessions?: IterableOrArrayLike<Session.IModel>;
  971. }
  972. /**
  973. * Get the default kernel name given select options.
  974. */
  975. export function getDefaultKernel(options: IKernelSearch): string | null {
  976. return Private.getDefaultKernel(options);
  977. }
  978. }
  979. /**
  980. * The default implementation of the client sesison dialog provider.
  981. */
  982. export const sessionContextDialogs: ISessionContext.IDialogs = {
  983. /**
  984. * Select a kernel for the session.
  985. */
  986. async selectKernel(sessionContext: ISessionContext): Promise<void> {
  987. if (sessionContext.isDisposed) {
  988. return Promise.resolve();
  989. }
  990. // If there is no existing kernel, offer the option
  991. // to keep no kernel.
  992. let label = 'Cancel';
  993. if (sessionContext.kernelDisplayName === Private.NO_KERNEL) {
  994. label = Private.NO_KERNEL;
  995. }
  996. const buttons = [
  997. Dialog.cancelButton({ label }),
  998. Dialog.okButton({ label: 'Select' })
  999. ];
  1000. const dialog = new Dialog({
  1001. title: 'Select Kernel',
  1002. body: new Private.KernelSelector(sessionContext),
  1003. buttons
  1004. });
  1005. const result = await dialog.launch();
  1006. if (sessionContext.isDisposed || !result.button.accept) {
  1007. return;
  1008. }
  1009. const model = result.value;
  1010. if (
  1011. model === null &&
  1012. sessionContext.kernelDisplayName !== Private.NO_KERNEL
  1013. ) {
  1014. return sessionContext.shutdown();
  1015. }
  1016. if (model) {
  1017. await sessionContext.changeKernel(model);
  1018. }
  1019. },
  1020. /**
  1021. * Restart the session.
  1022. *
  1023. * @returns A promise that resolves with whether the kernel has restarted.
  1024. *
  1025. * #### Notes
  1026. * If there is a running kernel, present a dialog.
  1027. * If there is no kernel, we start a kernel with the last run
  1028. * kernel name and resolves with `true`.
  1029. */
  1030. async restart(sessionContext: ISessionContext): Promise<boolean> {
  1031. await sessionContext.initialize();
  1032. if (sessionContext.isDisposed) {
  1033. throw new Error('session already disposed');
  1034. }
  1035. const kernel = sessionContext.session?.kernel;
  1036. if (!kernel && sessionContext.prevKernelName) {
  1037. await sessionContext.changeKernel({
  1038. name: sessionContext.prevKernelName
  1039. });
  1040. return true;
  1041. }
  1042. // Bail if there is no previous kernel to start.
  1043. if (!kernel) {
  1044. throw new Error('No kernel to restart');
  1045. }
  1046. const restartBtn = Dialog.warnButton({ label: 'Restart' });
  1047. const result = await showDialog({
  1048. title: 'Restart Kernel?',
  1049. body:
  1050. 'Do you want to restart the current kernel? All variables will be lost.',
  1051. buttons: [Dialog.cancelButton(), restartBtn]
  1052. });
  1053. if (kernel.isDisposed) {
  1054. return false;
  1055. }
  1056. if (result.button.accept) {
  1057. await kernel.restart();
  1058. return true;
  1059. }
  1060. return false;
  1061. }
  1062. };
  1063. /**
  1064. * The namespace for module private data.
  1065. */
  1066. namespace Private {
  1067. /**
  1068. * The text to show for no kernel.
  1069. */
  1070. export const NO_KERNEL = 'No Kernel';
  1071. /**
  1072. * A widget that provides a kernel selection.
  1073. */
  1074. export class KernelSelector extends Widget {
  1075. /**
  1076. * Create a new kernel selector widget.
  1077. */
  1078. constructor(sessionContext: ISessionContext) {
  1079. super({ node: createSelectorNode(sessionContext) });
  1080. }
  1081. /**
  1082. * Get the value of the kernel selector widget.
  1083. */
  1084. getValue(): Kernel.IModel {
  1085. const selector = this.node.querySelector('select') as HTMLSelectElement;
  1086. return JSON.parse(selector.value) as Kernel.IModel;
  1087. }
  1088. }
  1089. /**
  1090. * Create a node for a kernel selector widget.
  1091. */
  1092. function createSelectorNode(sessionContext: ISessionContext) {
  1093. // Create the dialog body.
  1094. const body = document.createElement('div');
  1095. const text = document.createElement('label');
  1096. text.textContent = `Select kernel for: "${sessionContext.name}"`;
  1097. body.appendChild(text);
  1098. const options = getKernelSearch(sessionContext);
  1099. const selector = document.createElement('select');
  1100. populateKernelSelect(selector, options);
  1101. body.appendChild(selector);
  1102. return body;
  1103. }
  1104. /**
  1105. * Get the default kernel name given select options.
  1106. */
  1107. export function getDefaultKernel(
  1108. options: SessionContext.IKernelSearch
  1109. ): string | null {
  1110. const { specs, preference } = options;
  1111. const {
  1112. name,
  1113. language,
  1114. shouldStart,
  1115. canStart,
  1116. autoStartDefault
  1117. } = preference;
  1118. if (!specs || shouldStart === false || canStart === false) {
  1119. return null;
  1120. }
  1121. const defaultName = autoStartDefault ? specs.default : null;
  1122. if (!name && !language) {
  1123. return defaultName;
  1124. }
  1125. // Look for an exact match of a spec name.
  1126. for (const specName in specs.kernelspecs) {
  1127. if (specName === name) {
  1128. return name;
  1129. }
  1130. }
  1131. // Bail if there is no language.
  1132. if (!language) {
  1133. return defaultName;
  1134. }
  1135. // Check for a single kernel matching the language.
  1136. const matches: string[] = [];
  1137. for (const specName in specs.kernelspecs) {
  1138. const kernelLanguage = specs.kernelspecs[specName]?.language;
  1139. if (language === kernelLanguage) {
  1140. matches.push(specName);
  1141. }
  1142. }
  1143. if (matches.length === 1) {
  1144. const specName = matches[0];
  1145. console.warn(
  1146. 'No exact match found for ' +
  1147. specName +
  1148. ', using kernel ' +
  1149. specName +
  1150. ' that matches ' +
  1151. 'language=' +
  1152. language
  1153. );
  1154. return specName;
  1155. }
  1156. // No matches found.
  1157. return defaultName;
  1158. }
  1159. /**
  1160. * Populate a kernel select node for the session.
  1161. */
  1162. export function populateKernelSelect(
  1163. node: HTMLSelectElement,
  1164. options: SessionContext.IKernelSearch
  1165. ): void {
  1166. while (node.firstChild) {
  1167. node.removeChild(node.firstChild);
  1168. }
  1169. const { preference, sessions, specs } = options;
  1170. const { name, id, language, canStart, shouldStart } = preference;
  1171. if (!specs || canStart === false) {
  1172. node.appendChild(optionForNone());
  1173. node.value = 'null';
  1174. node.disabled = true;
  1175. return;
  1176. }
  1177. node.disabled = false;
  1178. // Create mappings of display names and languages for kernel name.
  1179. const displayNames: { [key: string]: string } = Object.create(null);
  1180. const languages: { [key: string]: string } = Object.create(null);
  1181. for (const name in specs.kernelspecs) {
  1182. const spec = specs.kernelspecs[name]!;
  1183. displayNames[name] = spec.display_name;
  1184. languages[name] = spec.language;
  1185. }
  1186. // Handle a kernel by name.
  1187. const names: string[] = [];
  1188. if (name && name in specs.kernelspecs) {
  1189. names.push(name);
  1190. }
  1191. // Then look by language.
  1192. if (language) {
  1193. for (const specName in specs.kernelspecs) {
  1194. if (name !== specName && languages[specName] === language) {
  1195. names.push(specName);
  1196. }
  1197. }
  1198. }
  1199. // Use the default kernel if no kernels were found.
  1200. if (!names.length) {
  1201. names.push(specs.default);
  1202. }
  1203. // Handle a preferred kernels in order of display name.
  1204. const preferred = document.createElement('optgroup');
  1205. preferred.label = 'Start Preferred Kernel';
  1206. names.sort((a, b) => displayNames[a].localeCompare(displayNames[b]));
  1207. for (const name of names) {
  1208. preferred.appendChild(optionForName(name, displayNames[name]));
  1209. }
  1210. if (preferred.firstChild) {
  1211. node.appendChild(preferred);
  1212. }
  1213. // Add an option for no kernel
  1214. node.appendChild(optionForNone());
  1215. const other = document.createElement('optgroup');
  1216. other.label = 'Start Other Kernel';
  1217. // Add the rest of the kernel names in alphabetical order.
  1218. const otherNames: string[] = [];
  1219. for (const specName in specs.kernelspecs) {
  1220. if (names.indexOf(specName) !== -1) {
  1221. continue;
  1222. }
  1223. otherNames.push(specName);
  1224. }
  1225. otherNames.sort((a, b) => displayNames[a].localeCompare(displayNames[b]));
  1226. for (const otherName of otherNames) {
  1227. other.appendChild(optionForName(otherName, displayNames[otherName]));
  1228. }
  1229. // Add a separator option if there were any other names.
  1230. if (otherNames.length) {
  1231. node.appendChild(other);
  1232. }
  1233. // Handle the default value.
  1234. if (shouldStart === false) {
  1235. node.value = 'null';
  1236. } else {
  1237. node.selectedIndex = 0;
  1238. }
  1239. // Bail if there are no sessions.
  1240. if (!sessions) {
  1241. return;
  1242. }
  1243. // Add the sessions using the preferred language first.
  1244. const matchingSessions: Session.IModel[] = [];
  1245. const otherSessions: Session.IModel[] = [];
  1246. each(sessions, session => {
  1247. if (
  1248. language &&
  1249. session.kernel &&
  1250. languages[session.kernel.name] === language &&
  1251. session.kernel.id !== id
  1252. ) {
  1253. matchingSessions.push(session);
  1254. } else if (session.kernel?.id !== id) {
  1255. otherSessions.push(session);
  1256. }
  1257. });
  1258. const matching = document.createElement('optgroup');
  1259. matching.label = 'Use Kernel from Preferred Session';
  1260. node.appendChild(matching);
  1261. if (matchingSessions.length) {
  1262. matchingSessions.sort((a, b) => {
  1263. return a.path.localeCompare(b.path);
  1264. });
  1265. each(matchingSessions, session => {
  1266. const name = session.kernel ? displayNames[session.kernel.name] : '';
  1267. matching.appendChild(optionForSession(session, name));
  1268. });
  1269. }
  1270. const otherSessionsNode = document.createElement('optgroup');
  1271. otherSessionsNode.label = 'Use Kernel from Other Session';
  1272. node.appendChild(otherSessionsNode);
  1273. if (otherSessions.length) {
  1274. otherSessions.sort((a, b) => {
  1275. return a.path.localeCompare(b.path);
  1276. });
  1277. each(otherSessions, session => {
  1278. const name = session.kernel
  1279. ? displayNames[session.kernel.name] || session.kernel.name
  1280. : '';
  1281. otherSessionsNode.appendChild(optionForSession(session, name));
  1282. });
  1283. }
  1284. }
  1285. /**
  1286. * Get the kernel search options given a session context and session manager.
  1287. */
  1288. function getKernelSearch(
  1289. sessionContext: ISessionContext
  1290. ): SessionContext.IKernelSearch {
  1291. return {
  1292. specs: sessionContext.specsManager.specs,
  1293. sessions: sessionContext.sessionManager.running(),
  1294. preference: sessionContext.kernelPreference
  1295. };
  1296. }
  1297. /**
  1298. * Create an option element for a kernel name.
  1299. */
  1300. function optionForName(name: string, displayName: string): HTMLOptionElement {
  1301. const option = document.createElement('option');
  1302. option.text = displayName;
  1303. option.value = JSON.stringify({ name });
  1304. return option;
  1305. }
  1306. /**
  1307. * Create an option for no kernel.
  1308. */
  1309. function optionForNone(): HTMLOptGroupElement {
  1310. const group = document.createElement('optgroup');
  1311. group.label = 'Use No Kernel';
  1312. const option = document.createElement('option');
  1313. option.text = Private.NO_KERNEL;
  1314. option.value = 'null';
  1315. group.appendChild(option);
  1316. return group;
  1317. }
  1318. /**
  1319. * Create an option element for a session.
  1320. */
  1321. function optionForSession(
  1322. session: Session.IModel,
  1323. displayName: string
  1324. ): HTMLOptionElement {
  1325. const option = document.createElement('option');
  1326. const sessionName = session.name || PathExt.basename(session.path);
  1327. option.text = sessionName;
  1328. option.value = JSON.stringify({ id: session.kernel?.id });
  1329. option.title =
  1330. `Path: ${session.path}\n` +
  1331. `Name: ${sessionName}\n` +
  1332. `Kernel Name: ${displayName}\n` +
  1333. `Kernel Id: ${session.kernel?.id}`;
  1334. return option;
  1335. }
  1336. }