clientsession.tsx 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265
  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 '@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 sessionChanged: ISignal<
  35. this,
  36. IChangedArgs<Session.ISession | null, 'session'>
  37. >;
  38. session: Session.ISession | null;
  39. /**
  40. * A signal emitted when the kernel changes.
  41. */
  42. readonly kernelChanged: ISignal<
  43. this,
  44. IChangedArgs<Kernel.IKernelConnection | null, 'kernel'>
  45. >;
  46. /**
  47. * A signal emitted when the kernel status changes.
  48. */
  49. readonly statusChanged: ISignal<this, Kernel.Status>;
  50. /**
  51. * A signal emitted for a kernel messages.
  52. */
  53. readonly iopubMessage: ISignal<this, KernelMessage.IMessage>;
  54. /**
  55. * A signal emitted for an unhandled kernel message.
  56. */
  57. readonly unhandledMessage: ISignal<this, KernelMessage.IMessage>;
  58. /**
  59. * A signal emitted when a session property changes.
  60. */
  61. readonly propertyChanged: ISignal<this, 'path' | 'name' | 'type'>;
  62. /**
  63. * The current kernel associated with the document.
  64. */
  65. readonly kernel: Kernel.IKernelConnection | null;
  66. /**
  67. * The current path associated with the client session.
  68. */
  69. readonly path: string;
  70. /**
  71. * The current name associated with the client session.
  72. */
  73. readonly name: string;
  74. /**
  75. * The type of the client session.
  76. */
  77. readonly type: string;
  78. /**
  79. * Whether the session is ready.
  80. */
  81. readonly isReady: boolean;
  82. /**
  83. * A promise that is fulfilled when the session is ready.
  84. */
  85. readonly ready: Promise<void>;
  86. /**
  87. * The kernel preference.
  88. */
  89. kernelPreference: IClientSession.IKernelPreference;
  90. /**
  91. * The display name of the kernel.
  92. */
  93. readonly kernelDisplayName: string;
  94. /**
  95. * Change the current kernel associated with the document.
  96. */
  97. changeKernel(
  98. options: Partial<Kernel.IModel>
  99. ): Promise<Kernel.IKernelConnection>;
  100. /**
  101. * Kill the kernel and shutdown the session.
  102. *
  103. * @returns A promise that resolves when the session is shut down.
  104. */
  105. shutdown(): Promise<void>;
  106. /**
  107. * Select a kernel for the session.
  108. */
  109. selectKernel(): Promise<void>;
  110. /**
  111. * Restart the session.
  112. *
  113. * @returns A promise that resolves with whether the kernel has restarted.
  114. *
  115. * #### Notes
  116. * If there is a running kernel, present a dialog.
  117. * If there is no kernel, we start a kernel with the last run
  118. * kernel name and resolves with `true`. If no kernel has been started,
  119. * this is a no-op, and resolves with `false`.
  120. */
  121. restart(): Promise<boolean>;
  122. /**
  123. * Change the session path.
  124. *
  125. * @param path - The new session path.
  126. *
  127. * @returns A promise that resolves when the session has renamed.
  128. *
  129. * #### Notes
  130. * This uses the Jupyter REST API, and the response is validated.
  131. * The promise is fulfilled on a valid response and rejected otherwise.
  132. */
  133. setPath(path: string): Promise<void>;
  134. /**
  135. * Change the session name.
  136. */
  137. setName(name: string): Promise<void>;
  138. /**
  139. * Change the session type.
  140. */
  141. setType(type: string): Promise<void>;
  142. }
  143. /**
  144. * The namespace for Client Session related interfaces.
  145. */
  146. export namespace IClientSession {
  147. /**
  148. * A kernel preference.
  149. */
  150. export interface IKernelPreference {
  151. /**
  152. * The name of the kernel.
  153. */
  154. readonly name?: string;
  155. /**
  156. * The preferred kernel language.
  157. */
  158. readonly language?: string;
  159. /**
  160. * The id of an existing kernel.
  161. */
  162. readonly id?: string;
  163. /**
  164. * Whether to prefer starting a kernel.
  165. */
  166. readonly shouldStart?: boolean;
  167. /**
  168. * Whether a kernel can be started.
  169. */
  170. readonly canStart?: boolean;
  171. /**
  172. * Whether a kernel needs to be close with the associated session
  173. */
  174. readonly shutdownOnClose?: boolean;
  175. /**
  176. * Whether to auto-start the default kernel if no matching kernel is found.
  177. */
  178. readonly autoStartDefault?: boolean;
  179. }
  180. }
  181. /**
  182. * The default implementation of client session object.
  183. */
  184. export class ClientSession implements IClientSession {
  185. /**
  186. * Construct a new client session.
  187. */
  188. constructor(options: ClientSession.IOptions) {
  189. this.manager = options.manager;
  190. this._path = options.path || UUID.uuid4();
  191. this._type = options.type || '';
  192. this._name = options.name || '';
  193. this._setBusy = options.setBusy;
  194. this._kernelPreference = options.kernelPreference || {};
  195. }
  196. /**
  197. * A signal emitted when the session is shut down.
  198. */
  199. get terminated(): ISignal<this, void> {
  200. return this._terminated;
  201. }
  202. /**
  203. * A signal emitted when the kernel changes.
  204. */
  205. get kernelChanged(): ISignal<this, Session.IKernelChangedArgs> {
  206. return this._kernelChanged;
  207. }
  208. /**
  209. * A signal emitted when the status changes.
  210. */
  211. get statusChanged(): ISignal<this, Kernel.Status> {
  212. return this._statusChanged;
  213. }
  214. /**
  215. * A signal emitted for iopub kernel messages.
  216. */
  217. get iopubMessage(): ISignal<this, KernelMessage.IIOPubMessage> {
  218. return this._iopubMessage;
  219. }
  220. /**
  221. * A signal emitted for an unhandled kernel message.
  222. */
  223. get unhandledMessage(): ISignal<this, KernelMessage.IMessage> {
  224. return this._unhandledMessage;
  225. }
  226. /**
  227. * A signal emitted when a session property changes.
  228. */
  229. get propertyChanged(): ISignal<this, 'path' | 'name' | 'type'> {
  230. return this._propertyChanged;
  231. }
  232. /**
  233. * The current kernel of the session.
  234. */
  235. get kernel(): Kernel.IKernelConnection | null {
  236. return this._session ? this._session.kernel : null;
  237. }
  238. /**
  239. * The current path of the session.
  240. */
  241. get path(): string {
  242. return this._path;
  243. }
  244. /**
  245. * The current name of the session.
  246. */
  247. get name(): string {
  248. return this._name;
  249. }
  250. /**
  251. * The type of the client session.
  252. */
  253. get type(): string {
  254. return this._type;
  255. }
  256. /**
  257. * The kernel preference of the session.
  258. */
  259. get kernelPreference(): IClientSession.IKernelPreference {
  260. return this._kernelPreference;
  261. }
  262. set kernelPreference(value: IClientSession.IKernelPreference) {
  263. this._kernelPreference = value;
  264. }
  265. /**
  266. * The session manager used by the session.
  267. */
  268. readonly manager: Session.IManager;
  269. /**
  270. * Whether the session is ready.
  271. */
  272. get isReady(): boolean {
  273. return this._isReady;
  274. }
  275. /**
  276. * A promise that is fulfilled when the session is ready.
  277. */
  278. get ready(): Promise<void> {
  279. return this._ready.promise;
  280. }
  281. /**
  282. * The display name of the current kernel.
  283. */
  284. get kernelDisplayName(): string {
  285. let kernel = this.kernel;
  286. if (!kernel) {
  287. return 'No Kernel!';
  288. }
  289. let specs = this.manager.specs;
  290. if (!specs) {
  291. return 'Unknown!';
  292. }
  293. let spec = specs.kernelspecs[kernel.name];
  294. return spec ? spec.display_name : kernel.name;
  295. }
  296. /**
  297. * Test whether the context is disposed.
  298. */
  299. get isDisposed(): boolean {
  300. return this._isDisposed;
  301. }
  302. /**
  303. * Dispose of the resources held by the context.
  304. */
  305. dispose(): void {
  306. if (this._isDisposed) {
  307. return;
  308. }
  309. this._isDisposed = true;
  310. if (this._session) {
  311. if (this.kernelPreference.shutdownOnClose) {
  312. this._session.shutdown().catch(reason => {
  313. console.error(`Kernel not shut down ${reason}`);
  314. });
  315. }
  316. this._session.dispose();
  317. this._session = null;
  318. }
  319. if (this._dialog) {
  320. this._dialog.dispose();
  321. }
  322. if (this._busyDisposable) {
  323. this._busyDisposable.dispose();
  324. this._busyDisposable = null;
  325. }
  326. Signal.clearData(this);
  327. }
  328. /**
  329. * Change the current kernel associated with the document.
  330. */
  331. changeKernel(
  332. options: Partial<Kernel.IModel>
  333. ): Promise<Kernel.IKernelConnection> {
  334. return this.initialize().then(() => {
  335. if (this.isDisposed) {
  336. return Promise.reject('Disposed');
  337. }
  338. return this._changeKernel(options);
  339. });
  340. }
  341. /**
  342. * Select a kernel for the session.
  343. */
  344. selectKernel(): Promise<void> {
  345. return this.initialize().then(() => {
  346. if (this.isDisposed) {
  347. return Promise.reject('Disposed');
  348. }
  349. return this._selectKernel(true);
  350. });
  351. }
  352. /**
  353. * Kill the kernel and shutdown the session.
  354. *
  355. * @returns A promise that resolves when the session is shut down.
  356. */
  357. shutdown(): Promise<void> {
  358. const session = this._session;
  359. if (this.isDisposed || !session) {
  360. return Promise.resolve();
  361. }
  362. this._session = null;
  363. return session.shutdown();
  364. }
  365. /**
  366. * Restart the session.
  367. *
  368. * @returns A promise that resolves with whether the kernel has restarted.
  369. *
  370. * #### Notes
  371. * If there is a running kernel, present a dialog.
  372. * If there is no kernel, we start a kernel with the last run
  373. * kernel name and resolves with `true`.
  374. */
  375. restart(): Promise<boolean> {
  376. return this.initialize().then(() => {
  377. if (this.isDisposed) {
  378. return Promise.reject('session already disposed');
  379. }
  380. let kernel = this.kernel;
  381. if (!kernel) {
  382. if (this._prevKernelName) {
  383. return this.changeKernel({ name: this._prevKernelName }).then(
  384. () => true
  385. );
  386. }
  387. // Bail if there is no previous kernel to start.
  388. return Promise.reject('No kernel to restart');
  389. }
  390. return ClientSession.restartKernel(kernel);
  391. });
  392. }
  393. /**
  394. * Change the session path.
  395. *
  396. * @param path - The new session path.
  397. *
  398. * @returns A promise that resolves when the session has renamed.
  399. *
  400. * #### Notes
  401. * This uses the Jupyter REST API, and the response is validated.
  402. * The promise is fulfilled on a valid response and rejected otherwise.
  403. */
  404. setPath(path: string): Promise<void> {
  405. if (this.isDisposed || this._path === path) {
  406. return Promise.resolve();
  407. }
  408. this._path = path;
  409. if (this._session) {
  410. return this._session.setPath(path);
  411. }
  412. this._propertyChanged.emit('path');
  413. return Promise.resolve();
  414. }
  415. /**
  416. * Change the session name.
  417. */
  418. setName(name: string): Promise<void> {
  419. if (this.isDisposed || this._name === name) {
  420. return Promise.resolve();
  421. }
  422. this._name = name;
  423. if (this._session) {
  424. return this._session.setName(name);
  425. }
  426. this._propertyChanged.emit('name');
  427. return Promise.resolve();
  428. }
  429. /**
  430. * Change the session type.
  431. */
  432. setType(type: string): Promise<void> {
  433. if (this.isDisposed || this._type === type) {
  434. return Promise.resolve();
  435. }
  436. this._type = type;
  437. if (this._session) {
  438. return this._session.setType(name);
  439. }
  440. this._propertyChanged.emit('type');
  441. return Promise.resolve();
  442. }
  443. /**
  444. * Initialize the session.
  445. *
  446. * #### Notes
  447. * If a server session exists on the current path, we will connect to it.
  448. * If preferences include disabling `canStart` or `shouldStart`, no
  449. * server session will be started.
  450. * If a kernel id is given, we attempt to start a session with that id.
  451. * If a default kernel is available, we connect to it.
  452. * Otherwise we ask the user to select a kernel.
  453. */
  454. async initialize(): Promise<void> {
  455. if (this._initializing || this._isReady) {
  456. return this._ready.promise;
  457. }
  458. this._initializing = true;
  459. let manager = this.manager;
  460. await manager.ready;
  461. let model = find(manager.running(), item => {
  462. return item.path === this._path;
  463. });
  464. if (model) {
  465. try {
  466. let session = manager.connectTo(model);
  467. this._handleNewSession(session);
  468. } catch (err) {
  469. void this._handleSessionError(err);
  470. return Promise.reject(err);
  471. }
  472. }
  473. await this._startIfNecessary();
  474. this._isReady = true;
  475. this._ready.resolve(undefined);
  476. }
  477. /**
  478. * Start the session if necessary.
  479. */
  480. private _startIfNecessary(): Promise<void> {
  481. let preference = this.kernelPreference;
  482. if (
  483. this.isDisposed ||
  484. this.kernel ||
  485. preference.shouldStart === false ||
  486. preference.canStart === false
  487. ) {
  488. return Promise.resolve();
  489. }
  490. // Try to use an existing kernel.
  491. if (preference.id) {
  492. return this._changeKernel({ id: preference.id })
  493. .then(() => undefined)
  494. .catch(() => this._selectKernel(false));
  495. }
  496. let name = ClientSession.getDefaultKernel({
  497. specs: this.manager.specs,
  498. sessions: this.manager.running(),
  499. preference
  500. });
  501. if (name) {
  502. return this._changeKernel({ name })
  503. .then(() => undefined)
  504. .catch(() => this._selectKernel(false));
  505. }
  506. return this._selectKernel(false);
  507. }
  508. /**
  509. * Change the kernel.
  510. */
  511. private _changeKernel(
  512. options: Partial<Kernel.IModel>
  513. ): Promise<Kernel.IKernelConnection> {
  514. if (this.isDisposed) {
  515. return Promise.reject('Disposed');
  516. }
  517. let session = this._session;
  518. if (session && session.kernel.status !== 'dead') {
  519. return session.changeKernel(options).catch(err => {
  520. void this._handleSessionError(err);
  521. return Promise.reject(err);
  522. });
  523. } else {
  524. return this._startSession(options);
  525. }
  526. }
  527. /**
  528. * Select a kernel.
  529. *
  530. * @param cancelable: whether the dialog should have a cancel button.
  531. */
  532. private _selectKernel(cancelable: boolean): Promise<void> {
  533. if (this.isDisposed) {
  534. return Promise.resolve();
  535. }
  536. const buttons = cancelable
  537. ? [Dialog.cancelButton(), Dialog.okButton({ label: 'Select' })]
  538. : [Dialog.okButton({ label: 'Select' })];
  539. let dialog = (this._dialog = new Dialog({
  540. title: 'Select Kernel',
  541. body: new Private.KernelSelector(this),
  542. buttons
  543. }));
  544. return dialog
  545. .launch()
  546. .then(result => {
  547. if (this.isDisposed || !result.button.accept) {
  548. return;
  549. }
  550. let model = result.value;
  551. if (model === null && this._session) {
  552. return this.shutdown().then(() => {
  553. this._kernelChanged.emit({ oldValue: null, newValue: null });
  554. });
  555. }
  556. if (model) {
  557. return this._changeKernel(model).then(() => undefined);
  558. }
  559. })
  560. .then(() => {
  561. this._dialog = null;
  562. });
  563. }
  564. /**
  565. * Start a session and set up its signals.
  566. */
  567. private _startSession(
  568. model: Partial<Kernel.IModel>
  569. ): Promise<Kernel.IKernelConnection> {
  570. if (this.isDisposed) {
  571. return Promise.reject('Session is disposed.');
  572. }
  573. return this.manager
  574. .startNew({
  575. path: this._path,
  576. type: this._type,
  577. name: this._name,
  578. kernelName: model ? model.name : undefined,
  579. kernelId: model ? model.id : undefined
  580. })
  581. .then(session => {
  582. return this._handleNewSession(session);
  583. })
  584. .catch(err => {
  585. void this._handleSessionError(err);
  586. return Promise.reject(err);
  587. });
  588. }
  589. /**
  590. * Handle a new session object.
  591. */
  592. private _handleNewSession(
  593. session: Session.ISession
  594. ): Kernel.IKernelConnection {
  595. if (this.isDisposed) {
  596. throw Error('Disposed');
  597. }
  598. if (this._session) {
  599. this._session.dispose();
  600. }
  601. this._session = session;
  602. if (session.path !== this._path) {
  603. this._path = session.path;
  604. this._propertyChanged.emit('path');
  605. }
  606. if (session.name !== this._name) {
  607. this._name = session.name;
  608. this._propertyChanged.emit('name');
  609. }
  610. if (session.type !== this._type) {
  611. this._type = session.type;
  612. this._propertyChanged.emit('type');
  613. }
  614. session.disposed.connect(this._onTerminated, this);
  615. session.propertyChanged.connect(this._onPropertyChanged, this);
  616. session.kernelChanged.connect(this._onKernelChanged, this);
  617. session.statusChanged.connect(this._onStatusChanged, this);
  618. session.iopubMessage.connect(this._onIopubMessage, this);
  619. session.unhandledMessage.connect(this._onUnhandledMessage, this);
  620. this._prevKernelName = session.kernel.name;
  621. // The session kernel was disposed above when the session was disposed, so
  622. // the oldValue should be null.
  623. this._kernelChanged.emit({ oldValue: null, newValue: session.kernel });
  624. return session.kernel;
  625. }
  626. /**
  627. * Handle an error in session startup.
  628. */
  629. private _handleSessionError(
  630. err: ServerConnection.ResponseError
  631. ): Promise<void> {
  632. return err.response
  633. .text()
  634. .then(text => {
  635. let message = err.message;
  636. try {
  637. message = JSON.parse(text)['traceback'];
  638. } catch (err) {
  639. // no-op
  640. }
  641. let dialog = (this._dialog = new Dialog({
  642. title: 'Error Starting Kernel',
  643. body: <pre>{message}</pre>,
  644. buttons: [Dialog.okButton()]
  645. }));
  646. return dialog.launch();
  647. })
  648. .then(() => {
  649. this._dialog = null;
  650. });
  651. }
  652. /**
  653. * Handle a session termination.
  654. */
  655. private _onTerminated(): void {
  656. let kernel = this.kernel;
  657. if (this._session) {
  658. this._session.dispose();
  659. }
  660. this._session = null;
  661. this._terminated.emit(undefined);
  662. if (kernel) {
  663. this._kernelChanged.emit({ oldValue: null, newValue: null });
  664. }
  665. }
  666. /**
  667. * Handle a change to a session property.
  668. */
  669. private _onPropertyChanged(
  670. sender: Session.ISession,
  671. property: 'path' | 'name' | 'type'
  672. ) {
  673. switch (property) {
  674. case 'path':
  675. this._path = sender.path;
  676. break;
  677. case 'name':
  678. this._name = sender.name;
  679. break;
  680. default:
  681. this._type = sender.type;
  682. break;
  683. }
  684. this._propertyChanged.emit(property);
  685. }
  686. /**
  687. * Handle a change to the kernel.
  688. */
  689. private _onKernelChanged(
  690. sender: Session.ISession,
  691. args: Session.IKernelChangedArgs
  692. ): void {
  693. this._kernelChanged.emit(args);
  694. }
  695. /**
  696. * Handle a change to the session status.
  697. */
  698. private _onStatusChanged(): void {
  699. // Set that this kernel is busy, if we haven't already
  700. // If we have already, and now we aren't busy, dispose
  701. // of the busy disposable.
  702. if (this._setBusy) {
  703. if (this.kernel.status === 'busy') {
  704. if (!this._busyDisposable) {
  705. this._busyDisposable = this._setBusy();
  706. }
  707. } else {
  708. if (this._busyDisposable) {
  709. this._busyDisposable.dispose();
  710. this._busyDisposable = null;
  711. }
  712. }
  713. }
  714. this._statusChanged.emit(this.kernel.status);
  715. }
  716. /**
  717. * Handle an iopub message.
  718. */
  719. private _onIopubMessage(
  720. sender: Session.ISession,
  721. message: KernelMessage.IIOPubMessage
  722. ): void {
  723. this._iopubMessage.emit(message);
  724. }
  725. /**
  726. * Handle an unhandled message.
  727. */
  728. private _onUnhandledMessage(
  729. sender: Session.ISession,
  730. message: KernelMessage.IMessage
  731. ): void {
  732. this._unhandledMessage.emit(message);
  733. }
  734. private _path = '';
  735. private _name = '';
  736. private _type = '';
  737. private _prevKernelName = '';
  738. private _kernelPreference: IClientSession.IKernelPreference;
  739. private _isDisposed = false;
  740. private _session: Session.ISession | null = null;
  741. private _ready = new PromiseDelegate<void>();
  742. private _initializing = false;
  743. private _isReady = false;
  744. private _terminated = new Signal<this, void>(this);
  745. private _kernelChanged = new Signal<this, Session.IKernelChangedArgs>(this);
  746. private _statusChanged = new Signal<this, Kernel.Status>(this);
  747. private _iopubMessage = new Signal<this, KernelMessage.IIOPubMessage>(this);
  748. private _unhandledMessage = new Signal<this, KernelMessage.IMessage>(this);
  749. private _propertyChanged = new Signal<this, 'path' | 'name' | 'type'>(this);
  750. private _dialog: Dialog<any> | null = null;
  751. private _setBusy: () => IDisposable | undefined;
  752. private _busyDisposable: IDisposable | null = null;
  753. }
  754. /**
  755. * A namespace for `ClientSession` statics.
  756. */
  757. export namespace ClientSession {
  758. /**
  759. * The options used to initialize a context.
  760. */
  761. export interface IOptions {
  762. /**
  763. * A session manager instance.
  764. */
  765. manager: Session.IManager;
  766. /**
  767. * The initial path of the file.
  768. */
  769. path?: string;
  770. /**
  771. * The name of the session.
  772. */
  773. name?: string;
  774. /**
  775. * The type of the session.
  776. */
  777. type?: string;
  778. /**
  779. * A kernel preference.
  780. */
  781. kernelPreference?: IClientSession.IKernelPreference;
  782. /**
  783. * A function to call when the session becomes busy.
  784. */
  785. setBusy?: () => IDisposable;
  786. }
  787. /**
  788. * Restart a kernel if the user accepts the risk.
  789. *
  790. * Returns a promise resolving with whether the kernel was restarted.
  791. */
  792. export async function restartKernel(
  793. kernel: Kernel.IKernelConnection
  794. ): Promise<boolean> {
  795. let restartBtn = Dialog.warnButton({ label: 'Restart' });
  796. const result = await showDialog({
  797. title: 'Restart Kernel?',
  798. body:
  799. 'Do you want to restart the current kernel? All variables will be lost.',
  800. buttons: [Dialog.cancelButton(), restartBtn]
  801. });
  802. if (kernel.isDisposed) {
  803. return false;
  804. }
  805. if (result.button.accept) {
  806. await kernel.restart();
  807. return true;
  808. }
  809. return false;
  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.textContent = `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. }