clientsession.tsx 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { PathExt } from '@jupyterlab/coreutils';
  4. import { UUID } from '@phosphor/coreutils';
  5. import {
  6. Kernel,
  7. KernelMessage,
  8. ServerConnection,
  9. Session
  10. } from '@jupyterlab/services';
  11. import { IterableOrArrayLike, each, find } from '@phosphor/algorithm';
  12. import { PromiseDelegate } from '@phosphor/coreutils';
  13. import { IDisposable } from '@phosphor/disposable';
  14. import { ISignal, Signal } from '@phosphor/signaling';
  15. import { Widget } from '@phosphor/widgets';
  16. import * as React from 'react';
  17. import { showDialog, Dialog } from './dialog';
  18. /**
  19. * The interface of client session object.
  20. *
  21. * The client session represents the link between
  22. * a path and its kernel for the duration of the lifetime
  23. * of the session object. The session can have no current
  24. * kernel, and can start a new kernel at any time.
  25. */
  26. export interface IClientSession extends IDisposable {
  27. /**
  28. * A signal emitted when the session is shut down.
  29. */
  30. readonly terminated: ISignal<this, void>;
  31. /**
  32. * A signal emitted when the kernel changes.
  33. */
  34. readonly kernelChanged: ISignal<this, Session.IKernelChangedArgs>;
  35. /**
  36. * A signal emitted when the kernel status changes.
  37. */
  38. readonly statusChanged: ISignal<this, Kernel.Status>;
  39. /**
  40. * A signal emitted for a kernel messages.
  41. */
  42. readonly iopubMessage: ISignal<this, KernelMessage.IMessage>;
  43. /**
  44. * A signal emitted for an unhandled kernel message.
  45. */
  46. readonly unhandledMessage: ISignal<this, KernelMessage.IMessage>;
  47. /**
  48. * A signal emitted when a session property changes.
  49. */
  50. readonly propertyChanged: ISignal<this, 'path' | 'name' | 'type'>;
  51. /**
  52. * The current kernel associated with the document.
  53. */
  54. readonly kernel: Kernel.IKernelConnection | null;
  55. /**
  56. * The current path associated with the client session.
  57. */
  58. readonly path: string;
  59. /**
  60. * The current name associated with the client session.
  61. */
  62. readonly name: string;
  63. /**
  64. * The type of the client session.
  65. */
  66. readonly type: string;
  67. /**
  68. * The current status of the client session.
  69. */
  70. readonly status: Kernel.Status;
  71. /**
  72. * Whether the session is ready.
  73. */
  74. readonly isReady: boolean;
  75. /**
  76. * A promise that is fulfilled when the session is ready.
  77. */
  78. readonly ready: Promise<void>;
  79. /**
  80. * The kernel preference.
  81. */
  82. kernelPreference: IClientSession.IKernelPreference;
  83. /**
  84. * The display name of the kernel.
  85. */
  86. readonly kernelDisplayName: string;
  87. /**
  88. * Change the current kernel associated with the document.
  89. */
  90. changeKernel(
  91. options: Partial<Kernel.IModel>
  92. ): Promise<Kernel.IKernelConnection>;
  93. /**
  94. * Kill the kernel and shutdown the session.
  95. *
  96. * @returns A promise that resolves when the session is shut down.
  97. */
  98. shutdown(): Promise<void>;
  99. /**
  100. * Select a kernel for the session.
  101. */
  102. selectKernel(): Promise<void>;
  103. /**
  104. * Restart the session.
  105. *
  106. * @returns A promise that resolves with whether the kernel has restarted.
  107. *
  108. * #### Notes
  109. * If there is a running kernel, present a dialog.
  110. * If there is no kernel, we start a kernel with the last run
  111. * kernel name and resolves with `true`. If no kernel has been started,
  112. * this is a no-op, and resolves with `false`.
  113. */
  114. restart(): Promise<boolean>;
  115. /**
  116. * Change the session path.
  117. *
  118. * @param path - The new session path.
  119. *
  120. * @returns A promise that resolves when the session has renamed.
  121. *
  122. * #### Notes
  123. * This uses the Jupyter REST API, and the response is validated.
  124. * The promise is fulfilled on a valid response and rejected otherwise.
  125. */
  126. setPath(path: string): Promise<void>;
  127. /**
  128. * Change the session name.
  129. */
  130. setName(name: string): Promise<void>;
  131. /**
  132. * Change the session type.
  133. */
  134. setType(type: string): Promise<void>;
  135. }
  136. /**
  137. * The namespace for Client Session related interfaces.
  138. */
  139. export namespace IClientSession {
  140. /**
  141. * A kernel preference.
  142. */
  143. export interface IKernelPreference {
  144. /**
  145. * The name of the kernel.
  146. */
  147. readonly name?: string;
  148. /**
  149. * The preferred kernel language.
  150. */
  151. readonly language?: string;
  152. /**
  153. * The id of an existing kernel.
  154. */
  155. readonly id?: string;
  156. /**
  157. * Whether to prefer starting a kernel.
  158. */
  159. readonly shouldStart?: boolean;
  160. /**
  161. * Whether a kernel can be started.
  162. */
  163. readonly canStart?: boolean;
  164. /**
  165. * Whether a kernel needs to be close with the associated session
  166. */
  167. readonly shutdownOnClose?: boolean;
  168. /**
  169. * Whether to auto-start the default kernel if no matching kernel is found.
  170. */
  171. readonly autoStartDefault?: boolean;
  172. }
  173. }
  174. /**
  175. * The default implementation of client session object.
  176. */
  177. export class ClientSession implements IClientSession {
  178. /**
  179. * Construct a new client session.
  180. */
  181. constructor(options: ClientSession.IOptions) {
  182. this.manager = options.manager;
  183. this._path = options.path || UUID.uuid4();
  184. this._type = options.type || '';
  185. this._name = options.name || '';
  186. this._setBusy = options.setBusy;
  187. this._kernelPreference = options.kernelPreference || {};
  188. }
  189. /**
  190. * A signal emitted when the session is shut down.
  191. */
  192. get terminated(): ISignal<this, void> {
  193. return this._terminated;
  194. }
  195. /**
  196. * A signal emitted when the kernel changes.
  197. */
  198. get kernelChanged(): ISignal<this, Session.IKernelChangedArgs> {
  199. return this._kernelChanged;
  200. }
  201. /**
  202. * A signal emitted when the status changes.
  203. */
  204. get statusChanged(): ISignal<this, Kernel.Status> {
  205. return this._statusChanged;
  206. }
  207. /**
  208. * A signal emitted for iopub kernel messages.
  209. */
  210. get iopubMessage(): ISignal<this, KernelMessage.IMessage> {
  211. return this._iopubMessage;
  212. }
  213. /**
  214. * A signal emitted for an unhandled kernel message.
  215. */
  216. get unhandledMessage(): ISignal<this, KernelMessage.IMessage> {
  217. return this._unhandledMessage;
  218. }
  219. /**
  220. * A signal emitted when a session property changes.
  221. */
  222. get propertyChanged(): ISignal<this, 'path' | 'name' | 'type'> {
  223. return this._propertyChanged;
  224. }
  225. /**
  226. * The current kernel of the session.
  227. */
  228. get kernel(): Kernel.IKernelConnection | null {
  229. return this._session ? this._session.kernel : null;
  230. }
  231. /**
  232. * The current path of the session.
  233. */
  234. get path(): string {
  235. return this._path;
  236. }
  237. /**
  238. * The current name of the session.
  239. */
  240. get name(): string {
  241. return this._name;
  242. }
  243. /**
  244. * The type of the client session.
  245. */
  246. get type(): string {
  247. return this._type;
  248. }
  249. /**
  250. * The kernel preference of the session.
  251. */
  252. get kernelPreference(): IClientSession.IKernelPreference {
  253. return this._kernelPreference;
  254. }
  255. set kernelPreference(value: IClientSession.IKernelPreference) {
  256. this._kernelPreference = value;
  257. }
  258. /**
  259. * The session manager used by the session.
  260. */
  261. readonly manager: Session.IManager;
  262. /**
  263. * The current status of the session.
  264. */
  265. get status(): Kernel.Status {
  266. if (!this.isReady) {
  267. return 'starting';
  268. }
  269. return this._session ? this._session.status : 'dead';
  270. }
  271. /**
  272. * Whether the session is ready.
  273. */
  274. get isReady(): boolean {
  275. return this._isReady;
  276. }
  277. /**
  278. * A promise that is fulfilled when the session is ready.
  279. */
  280. get ready(): Promise<void> {
  281. return this._ready.promise;
  282. }
  283. /**
  284. * The display name of the current kernel.
  285. */
  286. get kernelDisplayName(): string {
  287. let kernel = this.kernel;
  288. if (!kernel) {
  289. return 'No Kernel!';
  290. }
  291. let specs = this.manager.specs;
  292. if (!specs) {
  293. return 'Unknown!';
  294. }
  295. let spec = specs.kernelspecs[kernel.name];
  296. return spec ? spec.display_name : kernel.name;
  297. }
  298. /**
  299. * Test whether the context is disposed.
  300. */
  301. get isDisposed(): boolean {
  302. return this._isDisposed;
  303. }
  304. /**
  305. * Dispose of the resources held by the context.
  306. */
  307. dispose(): void {
  308. if (this._isDisposed) {
  309. return;
  310. }
  311. this._isDisposed = true;
  312. if (this._session) {
  313. if (this.kernelPreference.shutdownOnClose) {
  314. this._session.shutdown().catch(reason => {
  315. console.error(`Kernel not shut down ${reason}`);
  316. });
  317. }
  318. this._session = null;
  319. }
  320. if (this._dialog) {
  321. this._dialog.dispose();
  322. }
  323. if (this._busyDisposable) {
  324. this._busyDisposable.dispose();
  325. this._busyDisposable = null;
  326. }
  327. Signal.clearData(this);
  328. }
  329. /**
  330. * Change the current kernel associated with the document.
  331. */
  332. changeKernel(
  333. options: Partial<Kernel.IModel>
  334. ): Promise<Kernel.IKernelConnection> {
  335. return this.initialize().then(() => {
  336. if (this.isDisposed) {
  337. return Promise.reject('Disposed');
  338. }
  339. return this._changeKernel(options);
  340. });
  341. }
  342. /**
  343. * Select a kernel for the session.
  344. */
  345. selectKernel(): Promise<void> {
  346. return this.initialize().then(() => {
  347. if (this.isDisposed) {
  348. return Promise.reject('Disposed');
  349. }
  350. return this._selectKernel(true);
  351. });
  352. }
  353. /**
  354. * Kill the kernel and shutdown the session.
  355. *
  356. * @returns A promise that resolves when the session is shut down.
  357. */
  358. shutdown(): Promise<void> {
  359. const session = this._session;
  360. if (this.isDisposed || !session) {
  361. return Promise.resolve();
  362. }
  363. this._session = null;
  364. return session.shutdown();
  365. }
  366. /**
  367. * Restart the session.
  368. *
  369. * @returns A promise that resolves with whether the kernel has restarted.
  370. *
  371. * #### Notes
  372. * If there is a running kernel, present a dialog.
  373. * If there is no kernel, we start a kernel with the last run
  374. * kernel name and resolves with `true`.
  375. */
  376. restart(): Promise<boolean> {
  377. return this.initialize().then(() => {
  378. if (this.isDisposed) {
  379. return Promise.reject('session already disposed');
  380. }
  381. let kernel = this.kernel;
  382. if (!kernel) {
  383. if (this._prevKernelName) {
  384. return this.changeKernel({ name: this._prevKernelName }).then(
  385. () => true
  386. );
  387. }
  388. // Bail if there is no previous kernel to start.
  389. return Promise.reject('No kernel to restart');
  390. }
  391. return ClientSession.restartKernel(kernel);
  392. });
  393. }
  394. /**
  395. * Change the session path.
  396. *
  397. * @param path - The new session path.
  398. *
  399. * @returns A promise that resolves when the session has renamed.
  400. *
  401. * #### Notes
  402. * This uses the Jupyter REST API, and the response is validated.
  403. * The promise is fulfilled on a valid response and rejected otherwise.
  404. */
  405. setPath(path: string): Promise<void> {
  406. if (this.isDisposed || this._path === path) {
  407. return Promise.resolve();
  408. }
  409. this._path = path;
  410. if (this._session) {
  411. return this._session.setPath(path);
  412. }
  413. this._propertyChanged.emit('path');
  414. return Promise.resolve();
  415. }
  416. /**
  417. * Change the session name.
  418. */
  419. setName(name: string): Promise<void> {
  420. if (this.isDisposed || this._name === name) {
  421. return Promise.resolve();
  422. }
  423. this._name = name;
  424. if (this._session) {
  425. return this._session.setName(name);
  426. }
  427. this._propertyChanged.emit('name');
  428. return Promise.resolve();
  429. }
  430. /**
  431. * Change the session type.
  432. */
  433. setType(type: string): Promise<void> {
  434. if (this.isDisposed || this._type === type) {
  435. return Promise.resolve();
  436. }
  437. this._type = type;
  438. if (this._session) {
  439. return this._session.setType(name);
  440. }
  441. this._propertyChanged.emit('type');
  442. return Promise.resolve();
  443. }
  444. /**
  445. * Initialize the session.
  446. *
  447. * #### Notes
  448. * If a server session exists on the current path, we will connect to it.
  449. * If preferences include disabling `canStart` or `shouldStart`, no
  450. * server session will be started.
  451. * If a kernel id is given, we attempt to start a session with that id.
  452. * If a default kernel is available, we connect to it.
  453. * Otherwise we ask the user to select a kernel.
  454. */
  455. async initialize(): Promise<void> {
  456. if (this._initializing || this._isReady) {
  457. return this._ready.promise;
  458. }
  459. this._initializing = true;
  460. let manager = this.manager;
  461. await manager.ready;
  462. let model = find(manager.running(), item => {
  463. return item.path === this._path;
  464. });
  465. if (model) {
  466. try {
  467. let session = manager.connectTo(model);
  468. this._handleNewSession(session);
  469. } catch (err) {
  470. void this._handleSessionError(err);
  471. return Promise.reject(err);
  472. }
  473. }
  474. await this._startIfNecessary();
  475. this._isReady = true;
  476. this._ready.resolve(undefined);
  477. }
  478. /**
  479. * Start the session if necessary.
  480. */
  481. private _startIfNecessary(): Promise<void> {
  482. let preference = this.kernelPreference;
  483. if (
  484. this.isDisposed ||
  485. this.kernel ||
  486. preference.shouldStart === false ||
  487. preference.canStart === false
  488. ) {
  489. return Promise.resolve();
  490. }
  491. // Try to use an existing kernel.
  492. if (preference.id) {
  493. return this._changeKernel({ id: preference.id })
  494. .then(() => undefined)
  495. .catch(() => this._selectKernel(false));
  496. }
  497. let name = ClientSession.getDefaultKernel({
  498. specs: this.manager.specs,
  499. sessions: this.manager.running(),
  500. preference
  501. });
  502. if (name) {
  503. return this._changeKernel({ name })
  504. .then(() => undefined)
  505. .catch(() => this._selectKernel(false));
  506. }
  507. return this._selectKernel(false);
  508. }
  509. /**
  510. * Change the kernel.
  511. */
  512. private _changeKernel(
  513. options: Partial<Kernel.IModel>
  514. ): Promise<Kernel.IKernelConnection> {
  515. if (this.isDisposed) {
  516. return Promise.reject('Disposed');
  517. }
  518. let session = this._session;
  519. if (session && session.status !== 'dead') {
  520. return session.changeKernel(options);
  521. } else {
  522. return this._startSession(options);
  523. }
  524. }
  525. /**
  526. * Select a kernel.
  527. *
  528. * @param cancelable: whether the dialog should have a cancel button.
  529. */
  530. private _selectKernel(cancelable: boolean): Promise<void> {
  531. if (this.isDisposed) {
  532. return Promise.resolve();
  533. }
  534. const buttons = cancelable
  535. ? [Dialog.cancelButton(), Dialog.okButton({ label: 'SELECT' })]
  536. : [Dialog.okButton({ label: 'SELECT' })];
  537. let dialog = (this._dialog = new Dialog({
  538. title: 'Select Kernel',
  539. body: new Private.KernelSelector(this),
  540. buttons
  541. }));
  542. return dialog
  543. .launch()
  544. .then(result => {
  545. if (this.isDisposed || !result.button.accept) {
  546. return;
  547. }
  548. let model = result.value;
  549. if (model === null && this._session) {
  550. return this.shutdown().then(() => {
  551. this._kernelChanged.emit({ oldValue: null, newValue: null });
  552. });
  553. }
  554. if (model) {
  555. return this._changeKernel(model).then(() => undefined);
  556. }
  557. })
  558. .then(() => {
  559. this._dialog = null;
  560. });
  561. }
  562. /**
  563. * Start a session and set up its signals.
  564. */
  565. private _startSession(
  566. model: Partial<Kernel.IModel>
  567. ): Promise<Kernel.IKernelConnection> {
  568. if (this.isDisposed) {
  569. return Promise.reject('Session is disposed.');
  570. }
  571. return this.manager
  572. .startNew({
  573. path: this._path,
  574. type: this._type,
  575. name: this._name,
  576. kernelName: model ? model.name : undefined,
  577. kernelId: model ? model.id : undefined
  578. })
  579. .then(session => {
  580. return this._handleNewSession(session);
  581. })
  582. .catch(err => {
  583. void this._handleSessionError(err);
  584. return Promise.reject(err);
  585. });
  586. }
  587. /**
  588. * Handle a new session object.
  589. */
  590. private _handleNewSession(
  591. session: Session.ISession
  592. ): Kernel.IKernelConnection {
  593. if (this.isDisposed) {
  594. throw Error('Disposed');
  595. }
  596. if (this._session) {
  597. this._session.dispose();
  598. }
  599. this._session = session;
  600. if (session.path !== this._path) {
  601. this._path = session.path;
  602. this._propertyChanged.emit('path');
  603. }
  604. if (session.name !== this._name) {
  605. this._name = session.name;
  606. this._propertyChanged.emit('name');
  607. }
  608. if (session.type !== this._type) {
  609. this._type = session.type;
  610. this._propertyChanged.emit('type');
  611. }
  612. session.terminated.connect(this._onTerminated, this);
  613. session.propertyChanged.connect(this._onPropertyChanged, this);
  614. session.kernelChanged.connect(this._onKernelChanged, this);
  615. session.statusChanged.connect(this._onStatusChanged, this);
  616. session.iopubMessage.connect(this._onIopubMessage, this);
  617. session.unhandledMessage.connect(this._onUnhandledMessage, this);
  618. this._prevKernelName = session.kernel.name;
  619. // The session kernel was disposed above when the session was disposed, so
  620. // the oldValue should be null.
  621. this._kernelChanged.emit({ oldValue: null, newValue: session.kernel });
  622. return session.kernel;
  623. }
  624. /**
  625. * Handle an error in session startup.
  626. */
  627. private _handleSessionError(
  628. err: ServerConnection.ResponseError
  629. ): Promise<void> {
  630. return err.response
  631. .text()
  632. .then(text => {
  633. let message = err.message;
  634. try {
  635. message = JSON.parse(text)['traceback'];
  636. } catch (err) {
  637. // no-op
  638. }
  639. let dialog = (this._dialog = new Dialog({
  640. title: 'Error Starting Kernel',
  641. body: <pre>{message}</pre>,
  642. buttons: [Dialog.okButton()]
  643. }));
  644. return dialog.launch();
  645. })
  646. .then(() => {
  647. this._dialog = null;
  648. });
  649. }
  650. /**
  651. * Handle a session termination.
  652. */
  653. private _onTerminated(): void {
  654. let kernel = this.kernel;
  655. if (this._session) {
  656. this._session.dispose();
  657. }
  658. this._session = null;
  659. this._terminated.emit(undefined);
  660. if (kernel) {
  661. this._kernelChanged.emit({ oldValue: null, newValue: null });
  662. }
  663. }
  664. /**
  665. * Handle a change to a session property.
  666. */
  667. private _onPropertyChanged(
  668. sender: Session.ISession,
  669. property: 'path' | 'name' | 'type'
  670. ) {
  671. switch (property) {
  672. case 'path':
  673. this._path = sender.path;
  674. break;
  675. case 'name':
  676. this._name = sender.name;
  677. break;
  678. default:
  679. this._type = sender.type;
  680. break;
  681. }
  682. this._propertyChanged.emit(property);
  683. }
  684. /**
  685. * Handle a change to the kernel.
  686. */
  687. private _onKernelChanged(
  688. sender: Session.ISession,
  689. args: Session.IKernelChangedArgs
  690. ): void {
  691. this._kernelChanged.emit(args);
  692. }
  693. /**
  694. * Handle a change to the session status.
  695. */
  696. private _onStatusChanged(): void {
  697. // Set that this kernel is busy, if we haven't already
  698. // If we have already, and now we aren't busy, dispose
  699. // of the busy disposable.
  700. if (this._setBusy) {
  701. if (this.status === 'busy') {
  702. if (!this._busyDisposable) {
  703. this._busyDisposable = this._setBusy();
  704. }
  705. } else {
  706. if (this._busyDisposable) {
  707. this._busyDisposable.dispose();
  708. this._busyDisposable = null;
  709. }
  710. }
  711. }
  712. this._statusChanged.emit(this.status);
  713. }
  714. /**
  715. * Handle an iopub message.
  716. */
  717. private _onIopubMessage(
  718. sender: Session.ISession,
  719. message: KernelMessage.IIOPubMessage
  720. ): void {
  721. this._iopubMessage.emit(message);
  722. }
  723. /**
  724. * Handle an unhandled message.
  725. */
  726. private _onUnhandledMessage(
  727. sender: Session.ISession,
  728. message: KernelMessage.IMessage
  729. ): void {
  730. this._unhandledMessage.emit(message);
  731. }
  732. private _path = '';
  733. private _name = '';
  734. private _type = '';
  735. private _prevKernelName = '';
  736. private _kernelPreference: IClientSession.IKernelPreference;
  737. private _isDisposed = false;
  738. private _session: Session.ISession | null = null;
  739. private _ready = new PromiseDelegate<void>();
  740. private _initializing = false;
  741. private _isReady = false;
  742. private _terminated = new Signal<this, void>(this);
  743. private _kernelChanged = new Signal<this, Session.IKernelChangedArgs>(this);
  744. private _statusChanged = new Signal<this, Kernel.Status>(this);
  745. private _iopubMessage = new Signal<this, KernelMessage.IMessage>(this);
  746. private _unhandledMessage = new Signal<this, KernelMessage.IMessage>(this);
  747. private _propertyChanged = new Signal<this, 'path' | 'name' | 'type'>(this);
  748. private _dialog: Dialog<any> | null = null;
  749. private _setBusy: () => IDisposable | undefined;
  750. private _busyDisposable: IDisposable | null = null;
  751. }
  752. /**
  753. * A namespace for `ClientSession` statics.
  754. */
  755. export namespace ClientSession {
  756. /**
  757. * The options used to initialize a context.
  758. */
  759. export interface IOptions {
  760. /**
  761. * A session manager instance.
  762. */
  763. manager: Session.IManager;
  764. /**
  765. * The initial path of the file.
  766. */
  767. path?: string;
  768. /**
  769. * The name of the session.
  770. */
  771. name?: string;
  772. /**
  773. * The type of the session.
  774. */
  775. type?: string;
  776. /**
  777. * A kernel preference.
  778. */
  779. kernelPreference?: IClientSession.IKernelPreference;
  780. /**
  781. * A function to call when the session becomes busy.
  782. */
  783. setBusy?: () => IDisposable;
  784. }
  785. /**
  786. * Restart a kernel if the user accepts the risk.
  787. *
  788. * Returns a promise resolving with whether the kernel was restarted.
  789. */
  790. export function restartKernel(
  791. kernel: Kernel.IKernelConnection
  792. ): Promise<boolean> {
  793. let restartBtn = Dialog.warnButton({ label: 'RESTART ' });
  794. return showDialog({
  795. title: 'Restart Kernel?',
  796. body:
  797. 'Do you want to restart the current kernel? All variables will be lost.',
  798. buttons: [Dialog.cancelButton(), restartBtn]
  799. }).then(result => {
  800. if (kernel.isDisposed) {
  801. return Promise.resolve(false);
  802. }
  803. if (result.button.accept) {
  804. return kernel.restart().then(() => {
  805. return true;
  806. });
  807. }
  808. return false;
  809. });
  810. }
  811. /**
  812. * An interface for populating a kernel selector.
  813. */
  814. export interface IKernelSearch {
  815. /**
  816. * The Kernel specs.
  817. */
  818. specs: Kernel.ISpecModels | null;
  819. /**
  820. * The kernel preference.
  821. */
  822. preference: IClientSession.IKernelPreference;
  823. /**
  824. * The current running sessions.
  825. */
  826. sessions?: IterableOrArrayLike<Session.IModel>;
  827. }
  828. /**
  829. * Get the default kernel name given select options.
  830. */
  831. export function getDefaultKernel(options: IKernelSearch): string | null {
  832. return Private.getDefaultKernel(options);
  833. }
  834. /**
  835. * Populate a kernel dropdown list.
  836. *
  837. * @param node - The node to populate.
  838. *
  839. * @param options - The options used to populate the kernels.
  840. *
  841. * #### Notes
  842. * Populates the list with separated sections:
  843. * - Kernels matching the preferred language (display names).
  844. * - "None" signifying no kernel.
  845. * - The remaining kernels.
  846. * - Sessions matching the preferred language (file names).
  847. * - The remaining sessions.
  848. * If no preferred language is given or no kernels are found using
  849. * the preferred language, the default kernel is used in the first
  850. * section. Kernels are sorted by display name. Sessions display the
  851. * base name of the file with an ellipsis overflow and a tooltip with
  852. * the explicit session information.
  853. */
  854. export function populateKernelSelect(
  855. node: HTMLSelectElement,
  856. options: IKernelSearch
  857. ): void {
  858. return Private.populateKernelSelect(node, options);
  859. }
  860. }
  861. /**
  862. * The namespace for module private data.
  863. */
  864. namespace Private {
  865. /**
  866. * A widget that provides a kernel selection.
  867. */
  868. export class KernelSelector extends Widget {
  869. /**
  870. * Create a new kernel selector widget.
  871. */
  872. constructor(session: ClientSession) {
  873. super({ node: createSelectorNode(session) });
  874. }
  875. /**
  876. * Get the value of the kernel selector widget.
  877. */
  878. getValue(): Kernel.IModel {
  879. let selector = this.node.querySelector('select') as HTMLSelectElement;
  880. return JSON.parse(selector.value) as Kernel.IModel;
  881. }
  882. }
  883. /**
  884. * Create a node for a kernel selector widget.
  885. */
  886. function createSelectorNode(session: ClientSession) {
  887. // Create the dialog body.
  888. let body = document.createElement('div');
  889. let text = document.createElement('label');
  890. text.innerHTML = `Select kernel for: "${session.name}"`;
  891. body.appendChild(text);
  892. let options = getKernelSearch(session);
  893. let selector = document.createElement('select');
  894. ClientSession.populateKernelSelect(selector, options);
  895. body.appendChild(selector);
  896. return body;
  897. }
  898. /**
  899. * Get the default kernel name given select options.
  900. */
  901. export function getDefaultKernel(
  902. options: ClientSession.IKernelSearch
  903. ): string | null {
  904. let { specs, preference } = options;
  905. let {
  906. name,
  907. language,
  908. shouldStart,
  909. canStart,
  910. autoStartDefault
  911. } = preference;
  912. if (!specs || shouldStart === false || canStart === false) {
  913. return null;
  914. }
  915. let defaultName = autoStartDefault ? specs.default : null;
  916. if (!name && !language) {
  917. return defaultName;
  918. }
  919. // Look for an exact match of a spec name.
  920. for (let specName in specs.kernelspecs) {
  921. if (specName === name) {
  922. return name;
  923. }
  924. }
  925. // Bail if there is no language.
  926. if (!language) {
  927. return defaultName;
  928. }
  929. // Check for a single kernel matching the language.
  930. let matches: string[] = [];
  931. for (let specName in specs.kernelspecs) {
  932. let kernelLanguage = specs.kernelspecs[specName].language;
  933. if (language === kernelLanguage) {
  934. matches.push(specName);
  935. }
  936. }
  937. if (matches.length === 1) {
  938. let specName = matches[0];
  939. console.log(
  940. 'No exact match found for ' +
  941. specName +
  942. ', using kernel ' +
  943. specName +
  944. ' that matches ' +
  945. 'language=' +
  946. language
  947. );
  948. return specName;
  949. }
  950. // No matches found.
  951. return defaultName;
  952. }
  953. /**
  954. * Populate a kernel select node for the session.
  955. */
  956. export function populateKernelSelect(
  957. node: HTMLSelectElement,
  958. options: ClientSession.IKernelSearch
  959. ): void {
  960. while (node.firstChild) {
  961. node.removeChild(node.firstChild);
  962. }
  963. let { preference, sessions, specs } = options;
  964. let { name, id, language, canStart, shouldStart } = preference;
  965. if (!specs || canStart === false) {
  966. node.appendChild(optionForNone());
  967. node.value = 'null';
  968. node.disabled = true;
  969. return;
  970. }
  971. node.disabled = false;
  972. // Create mappings of display names and languages for kernel name.
  973. let displayNames: { [key: string]: string } = Object.create(null);
  974. let languages: { [key: string]: string } = Object.create(null);
  975. for (let name in specs.kernelspecs) {
  976. let spec = specs.kernelspecs[name];
  977. displayNames[name] = spec.display_name;
  978. languages[name] = spec.language;
  979. }
  980. // Handle a kernel by name.
  981. let names: string[] = [];
  982. if (name && name in specs.kernelspecs) {
  983. names.push(name);
  984. }
  985. // Then look by language.
  986. if (language) {
  987. for (let specName in specs.kernelspecs) {
  988. if (name !== specName && languages[specName] === language) {
  989. names.push(specName);
  990. }
  991. }
  992. }
  993. // Use the default kernel if no kernels were found.
  994. if (!names.length) {
  995. names.push(specs.default);
  996. }
  997. // Handle a preferred kernels in order of display name.
  998. let preferred = document.createElement('optgroup');
  999. preferred.label = 'Start Preferred Kernel';
  1000. names.sort((a, b) => displayNames[a].localeCompare(displayNames[b]));
  1001. for (let name of names) {
  1002. preferred.appendChild(optionForName(name, displayNames[name]));
  1003. }
  1004. if (preferred.firstChild) {
  1005. node.appendChild(preferred);
  1006. }
  1007. // Add an option for no kernel
  1008. node.appendChild(optionForNone());
  1009. let other = document.createElement('optgroup');
  1010. other.label = 'Start Other Kernel';
  1011. // Add the rest of the kernel names in alphabetical order.
  1012. let otherNames: string[] = [];
  1013. for (let specName in specs.kernelspecs) {
  1014. if (names.indexOf(specName) !== -1) {
  1015. continue;
  1016. }
  1017. otherNames.push(specName);
  1018. }
  1019. otherNames.sort((a, b) => displayNames[a].localeCompare(displayNames[b]));
  1020. for (let otherName of otherNames) {
  1021. other.appendChild(optionForName(otherName, displayNames[otherName]));
  1022. }
  1023. // Add a separator option if there were any other names.
  1024. if (otherNames.length) {
  1025. node.appendChild(other);
  1026. }
  1027. // Handle the default value.
  1028. if (shouldStart === false) {
  1029. node.value = 'null';
  1030. } else {
  1031. node.selectedIndex = 0;
  1032. }
  1033. // Bail if there are no sessions.
  1034. if (!sessions) {
  1035. return;
  1036. }
  1037. // Add the sessions using the preferred language first.
  1038. let matchingSessions: Session.IModel[] = [];
  1039. let otherSessions: Session.IModel[] = [];
  1040. each(sessions, session => {
  1041. if (
  1042. language &&
  1043. languages[session.kernel.name] === language &&
  1044. session.kernel.id !== id
  1045. ) {
  1046. matchingSessions.push(session);
  1047. } else if (session.kernel.id !== id) {
  1048. otherSessions.push(session);
  1049. }
  1050. });
  1051. let matching = document.createElement('optgroup');
  1052. matching.label = 'Use Kernel from Preferred Session';
  1053. node.appendChild(matching);
  1054. if (matchingSessions.length) {
  1055. matchingSessions.sort((a, b) => {
  1056. return a.path.localeCompare(b.path);
  1057. });
  1058. each(matchingSessions, session => {
  1059. let name = displayNames[session.kernel.name];
  1060. matching.appendChild(optionForSession(session, name));
  1061. });
  1062. }
  1063. let otherSessionsNode = document.createElement('optgroup');
  1064. otherSessionsNode.label = 'Use Kernel from Other Session';
  1065. node.appendChild(otherSessionsNode);
  1066. if (otherSessions.length) {
  1067. otherSessions.sort((a, b) => {
  1068. return a.path.localeCompare(b.path);
  1069. });
  1070. each(otherSessions, session => {
  1071. let name = displayNames[session.kernel.name] || session.kernel.name;
  1072. otherSessionsNode.appendChild(optionForSession(session, name));
  1073. });
  1074. }
  1075. }
  1076. /**
  1077. * Get the kernel search options given a client session and sesion manager.
  1078. */
  1079. function getKernelSearch(
  1080. session: ClientSession
  1081. ): ClientSession.IKernelSearch {
  1082. return {
  1083. specs: session.manager.specs,
  1084. sessions: session.manager.running(),
  1085. preference: session.kernelPreference
  1086. };
  1087. }
  1088. /**
  1089. * Create an option element for a kernel name.
  1090. */
  1091. function optionForName(name: string, displayName: string): HTMLOptionElement {
  1092. let option = document.createElement('option');
  1093. option.text = displayName;
  1094. option.value = JSON.stringify({ name });
  1095. return option;
  1096. }
  1097. /**
  1098. * Create an option for no kernel.
  1099. */
  1100. function optionForNone(): HTMLOptGroupElement {
  1101. let group = document.createElement('optgroup');
  1102. group.label = 'Use No Kernel';
  1103. let option = document.createElement('option');
  1104. option.text = 'No Kernel';
  1105. option.value = 'null';
  1106. group.appendChild(option);
  1107. return group;
  1108. }
  1109. /**
  1110. * Create an option element for a session.
  1111. */
  1112. function optionForSession(
  1113. session: Session.IModel,
  1114. displayName: string
  1115. ): HTMLOptionElement {
  1116. let option = document.createElement('option');
  1117. let sessionName = session.name || PathExt.basename(session.path);
  1118. option.text = sessionName;
  1119. option.value = JSON.stringify({ id: session.kernel.id });
  1120. option.title =
  1121. `Path: ${session.path}\n` +
  1122. `Name: ${sessionName}\n` +
  1123. `Kernel Name: ${displayName}\n` +
  1124. `Kernel Id: ${session.kernel.id}`;
  1125. return option;
  1126. }
  1127. }